初始化时调用AuthorizationServerConfigurerAdapter.configure(xxxConfigure)
开发定义的配置列表:
1. ClientDetailsService:ClientDetails信息加载实现类。
2. TokenStore:token管理服务。
3. TokenEnhancer:token信息的额外信息处理。
4. PasswordEncoder: clientDetail 信息里的client_secret字段加解密器。
一、核心类:
1. AuthorizationServerConfigurerAdapter: AuthorizationServer配置的适配类。
2. OAuth2AuthorizationServerConfiguration:系统提供的配置实现类,用户自定时可参考这类的实现。
但这对象生成条件:
//1.没有其它AuthorizationServerConfigurer的配置对象,如果用户自己有定义就不产生。
//2.yml配置文件有:security.oauth2.authorization属性配置。@ConditionalOnMissingBean(AuthorizationServerConfigurer.class)
@EnableConfigurationProperties(AuthorizationServerProperties.class)
3. 开发自定义MyAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter:
完成相关的配置(向这个些类注入自定义的Bean):
(1)ClientDetailsServiceConfigurer:clientDetailsService注入,决定clientDeatils信息的处理服务。
(2)AuthorizationServerEndpointsConfigurer:访问端点配置。tokenStroe、tokenEnhancer服务
(3)AuthorizationServerSecurityConfigurer:访问安全配置。
二、配置类:
1. ClientDetailsServiceConfigurer:
其它地方可以通过ClientDetailsServiceConfigurer调用开发配置的ClientDetailsService。系统提供的二个ClientDetailsService实现类:JdbcClientDetailsService、InMemoryClientDetailsService。
回调配置ClientDetailsServiceConfigurer
/**
*
* 配置从哪里获取ClientDetails信息。
* 在client_credentials授权方式下,只要这个ClientDetails信息。
* @param clientsDetails
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clientsDetails) throws Exception {
//认证信息从数据库获取
clientsDetails.withClientDetails(clientDetailsService);
// 测试用,将客户端信息存储在内存中
clientsDetails.inMemory()
.withClient("client") // client_id
.secret("secret") // client_secret
.authorizedGrantTypes("authorization_code") // 该client允许的授权类型
.scopes("app") // 允许的授权范围
.autoApprove(true); //登录后绕过批准询问(/oauth/confirm_access)
}
JdbcClientDetailsService类
public class JdbcClientDetailsService implements ClientDetailsService, ClientRegistrationService {
//操作oauth_client_details数据库SQL语句,另外可以自行注入
private static final String CLIENT_FIELDS_FOR_UPDATE = "resource_ids, scope, "
+ "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, "
+ "refresh_token_validity, additional_information, autoapprove";
private static final String CLIENT_FIELDS = "client_secret, " + CLIENT_FIELDS_FOR_UPDATE;
private static final String BASE_FIND_STATEMENT = "select client_id, " + CLIENT_FIELDS + " from oauth_client_details";
private static final String DEFAULT_FIND_STATEMENT = BASE_FIND_STATEMENT + " order by client_id";
private static final String DEFAULT_SELECT_STATEMENT = BASE_FIND_STATEMENT + " where client_id = ?";
private static final String DEFAULT_INSERT_STATEMENT = "insert into oauth_client_details (" + CLIENT_FIELDS
+ ", client_id) values (?,?,?,?,?,?,?,?,?,?,?)";
private static final String DEFAULT_UPDATE_STATEMENT = "update oauth_client_details " + "set "
+ CLIENT_FIELDS_FOR_UPDATE.replaceAll(", ", "=?, ") + "=? where client_id = ?";
private static final String DEFAULT_UPDATE_SECRET_STATEMENT = "update oauth_client_details "
+ "set client_secret = ? where client_id = ?";
private static final String DEFAULT_DELETE_STATEMENT = "delete from oauth_client_details where client_id = ?";
private RowMapper<ClientDetails> rowMapper = new ClientDetailsRowMapper();
//1.用于client_secret密码入库与出库时转化
private PasswordEncoder passwordEncoder = NoOpPasswordEncoder.getInstance();
//2.数据库存操作。
private final JdbcTemplate jdbcTemplate;
private JdbcListFactory listFactory;
public JdbcClientDetailsService(DataSource dataSource) {
Assert.notNull(dataSource, "DataSource required");
this.jdbcTemplate = new JdbcTemplate(dataSource);
this.listFactory = new DefaultJdbcListFactory(new NamedParameterJdbcTemplate(jdbcTemplate));
}
/**
* 核心方法。加载ClientDetails by clientId
*/
public ClientDetails loadClientByClientId(String clientId) throws InvalidClientException {
ClientDetails details;
try {
details = jdbcTemplate.queryForObject(selectClientDetailsSql, new ClientDetailsRowMapper(), clientId);
}
catch (EmptyResultDataAccessException e) {
throw new NoSuchClientException("No client with requested id: " + clientId);
}
return details;
}
}
InMemoryClientDetailsService类
public class InMemoryClientDetailsService implements ClientDetailsService {
private Map<String, ClientDetails> clientDetailsStore = new HashMap<String, ClientDetails>();
public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
ClientDetails details = clientDetailsStore.get(clientId);
return details;
}
}
2. AuthorizationServerEndpointsConfigurer端点配置:
AuthorizationServerEndpointsConfigurer其实是一个装载类,装载Endpoints所有相关的类配置(AuthorizationServer、TokenServices、TokenStore、ClientDetailsService、UserDetailsService)。
配置方法:
/**
* 注入相关配置:
* 1. 密码模式下配置认证管理器 AuthenticationManager
* 2. 设置 AccessToken的存储介质tokenStore, 默认使用内存当做存储介质。
* 3. userDetailsService注入
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
endpoints
.authenticationManager(authenticationManager)
//末确认点.userDetailsService(userDetailsService) //MUST:密码模式下需设置一个AuthenticationManager对象,获取 UserDetails信息
.tokenStore(tokenStore)//token的保存方式
.tokenEnhancer(tokenEnhancerChain);//token里加点信息
}
AuthorizationServerEndpointsConfigurer类:
public final class AuthorizationServerEndpointsConfigurer {
private AuthorizationServerTokenServices tokenServices;
private ConsumerTokenServices consumerTokenServices;
private AuthorizationCodeServices authorizationCodeServices;
private ResourceServerTokenServices resourceTokenServices;
private TokenStore tokenStore;
private TokenEnhancer tokenEnhancer;
private AccessTokenConverter accessTokenConverter;
private ApprovalStore approvalStore;
private TokenGranter tokenGranter;
private OAuth2RequestFactory requestFactory;
private OAuth2RequestValidator requestValidator;
private UserApprovalHandler userApprovalHandler;
private AuthenticationManager authenticationManager;
private ClientDetailsService clientDetailsService;
private String prefix;
private Map<String, String> patternMap = new HashMap<String, String>();
private Set<HttpMethod> allowedTokenEndpointRequestMethods = new HashSet<HttpMethod>();
private FrameworkEndpointHandlerMapping frameworkEndpointHandlerMapping;
private boolean approvalStoreDisabled;
private List<Object> interceptors = new ArrayList<Object>();
private DefaultTokenServices defaultTokenServices;
private UserDetailsService userDetailsService;
private boolean tokenServicesOverride = false;
private boolean userDetailsServiceOverride = false;
private boolean reuseRefreshToken = true;
private WebResponseExceptionTranslator<OAuth2Exception> exceptionTranslator;
private RedirectResolver redirectResolver;
/**
* tokenServices重新生成一个PreAuthenticatedAuthenticationProvider作为认证。
*/
private void addUserDetailsService(DefaultTokenServices tokenServices, UserDetailsService userDetailsService) {
if (userDetailsService != null) {
PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
provider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken>(
userDetailsService));
tokenServices
.setAuthenticationManager(new ProviderManager(Arrays.<AuthenticationProvider> asList(provider)));
}
}
}
三、AuthorizationServerSecurityConfigurer端点安全配置:
AuthorizationServerSecurityConfigurer继承SecurityConfigurerAdapter.也就是一个 Spring Security安全配置提供给AuthorizationServer去配置AuthorizationServer的端点(/oauth/****)的安全访问规则、过滤器Filter。
类继承关系:
可配置属性项:
1. ClientDetail加密方式
2. allowFormAuthenticationForClients 允许表单认证。针对/oauth/token端点。
3. 添加开发配置tokenEndpointAuthenticationFilters
4. tokenKeyAccess、checkTokenAccess访问权限。
/**
* 配置:安全检查流程,用来配置令牌端点(Token Endpoint)的安全与权限访问
* 默认过滤器:BasicAuthenticationFilter
* 1、oauth_client_details表中clientSecret字段加密【ClientDetails属性secret】
* 2、CheckEndpoint类的接口 oauth/check_token 无需经过过滤器过滤,默认值:denyAll()
* 对以下的几个端点进行权限配置:
* /oauth/authorize:授权端点
* /oauth/token:令牌端点
* /oauth/confirm_access:用户确认授权提交端点
* /oauth/error:授权服务错误信息端点
* /oauth/check_token:用于资源服务访问的令牌解析端点
* /oauth/token_key:提供公有密匙的端点,如果使用JWT令牌的话
**/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients()//允许客户表单认证
.passwordEncoder(new BCryptPasswordEncoder())//设置oauth_client_details中的密码编码器
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.passwordEncoder(oauthClientPasswordEncoder);
}
AuthorizationServerSecurityConfigurer类
public final class AuthorizationServerSecurityConfigurer extends
SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private AuthenticationEntryPoint authenticationEntryPoint;
private AccessDeniedHandler accessDeniedHandler = new OAuth2AccessDeniedHandler();
//client secrets加密器
private PasswordEncoder passwordEncoder;
private String realm = "oauth2/client";
private boolean allowFormAuthenticationForClients = false;
private String tokenKeyAccess = "denyAll()";
private String checkTokenAccess = "denyAll()";
private boolean sslOnly = false;
//开发定义过滤器
private List<Filter> tokenEndpointAuthenticationFilters = new ArrayList<Filter>();
@Override
public void init(HttpSecurity http) throws Exception {
//1.异常发生时的入口配置
registerDefaultAuthenticationEntryPoint(http);
//1. passwordEncoder注入到ClientDetailsUserDetailsService(
// UserDetailsService对象存储在HttpSecurity.SharedObject里。
if (passwordEncoder != null) {
ClientDetailsUserDetailsService clientDetailsUserDetailsService = new ClientDetailsUserDetailsService(clientDetailsService());
clientDetailsUserDetailsService.setPasswordEncoder(passwordEncoder());
http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(clientDetailsUserDetailsService)
.passwordEncoder(passwordEncoder());
}
else {
http.userDetailsService(new ClientDetailsUserDetailsService(clientDetailsService()));
}
//2.配置/oaut/***端点 httpBasic安全规则
http.securityContext().securityContextRepository(new NullSecurityContextRepository()).and().csrf().disable()
.httpBasic().realmName(realm);
//3. ssl 通道安全
if (sslOnly) {
http.requiresChannel().anyRequest().requiresSecure();
}
}
/**
* 开发配置:
* 1. allowFormAuthenticationForClients 允许表单认证。针对/oauth/token端点添加ClientCredentialsTokenEndpointFilter
* 2. 添加开发配置tokenEndpointAuthenticationFilters。(在 BasicAuthenticationFilter之前)
* 3. 添加在 accessDeniedHandler
*/
@Override
public void configure(HttpSecurity http) throws Exception {
// ensure this is initialized
frameworkEndpointHandlerMapping();
//2. 针对/oauth/token端点添加ClientCredentialsTokenEndpointFilter
if (allowFormAuthenticationForClients) {
clientCredentialsTokenEndpointFilter(http);
}
//3.添加开发配置tokenEndpointAuthenticationFilters。(在 BasicAuthenticationFilter之前)
for (Filter filter : tokenEndpointAuthenticationFilters) {
http.addFilterBefore(filter, BasicAuthenticationFilter.class);
}
//4. 添加在 accessDeniedHandler,处理访问AccessDeniedException发生时处理。
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
}
/**
* 配置异常发生时入口点AuthenticationEntryPoint
* @param http
*/
private void registerDefaultAuthenticationEntryPoint(HttpSecurity http) {
ExceptionHandlingConfigurer<HttpSecurity> exceptionHandling = http.getConfigurer(ExceptionHandlingConfigurer.class);
if (exceptionHandling == null) {
return;
}
//1.端点入口:BasicAuthenticationEntryPoint设置response配置。
// response.addHeader("WWW-Authenticate", "Basic realm=\"" + realmName + "\"");
// response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
if (authenticationEntryPoint==null) {
BasicAuthenticationEntryPoint basicEntryPoint = new BasicAuthenticationEntryPoint();
basicEntryPoint.setRealmName(realm);
authenticationEntryPoint = basicEntryPoint;
}
//发生异常时,会调用到BasicAuthenticationEntryPoint.commence()
exceptionHandling.defaultAuthenticationEntryPointFor(postProcess(authenticationEntryPoint), preferredMatcher);
}
/**
* 核心:
* "/oauth/token"端点的添加过滤器clientCredentialsTokenEndpointFilter(clientId, client_secert)密码比对。而且
* 放到BasicAuthenticationFilter之前。
* http.addFilterBefore(clientCredentialsTokenEndpointFilter, BasicAuthenticationFilter.class);
*/
private ClientCredentialsTokenEndpointFilter clientCredentialsTokenEndpointFilter(HttpSecurity http) {
ClientCredentialsTokenEndpointFilter clientCredentialsTokenEndpointFilter = new ClientCredentialsTokenEndpointFilter(
frameworkEndpointHandlerMapping().getServletPath("/oauth/token"));
clientCredentialsTokenEndpointFilter
.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
OAuth2AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
authenticationEntryPoint.setTypeName("Form");
authenticationEntryPoint.setRealmName(realm);
clientCredentialsTokenEndpointFilter.setAuthenticationEntryPoint(authenticationEntryPoint);
clientCredentialsTokenEndpointFilter = postProcess(clientCredentialsTokenEndpointFilter);
http.addFilterBefore(clientCredentialsTokenEndpointFilter, BasicAuthenticationFilter.class);
return clientCredentialsTokenEndpointFilter;
}
/**
* 居然使用共享方式
*/
private ClientDetailsService clientDetailsService() {
return getBuilder().getSharedObject(ClientDetailsService.class);
}
private FrameworkEndpointHandlerMapping frameworkEndpointHandlerMapping() {
return getBuilder().getSharedObject(FrameworkEndpointHandlerMapping.class);
}
private PasswordEncoder passwordEncoder() {
return new PasswordEncoder() {
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return StringUtils.hasText(encodedPassword) ? passwordEncoder.matches(rawPassword, encodedPassword)
: true;
}
@Override
public String encode(CharSequence rawPassword) {
return passwordEncoder.encode(rawPassword);
}
};
}
}
public class ClientCredentialsTokenEndpointFilter extends AbstractAuthenticationProcessingFilter {
private AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
public ClientCredentialsTokenEndpointFilter() {
this("/oauth/token");
}
public ClientCredentialsTokenEndpointFilter(String path) {
super(path);
setRequiresAuthenticationRequestMatcher(new ClientCredentialsRequestMatcher(path));
// If authentication fails the type is "Form"
((OAuth2AuthenticationEntryPoint) authenticationEntryPoint).setTypeName("Form");
}
@Override
public void afterPropertiesSet() {
super.afterPropertiesSet();
setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
if (exception instanceof BadCredentialsException) {
exception = new BadCredentialsException(exception.getMessage(), new BadClientCredentialsException());
}
authenticationEntryPoint.commence(request, response, exception);
}
});
setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
// no-op - just allow filter chain to continue to token endpoint
}
});
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
//1.获取请求参数:clientId、clientSecret用来认证
String clientId = request.getParameter("client_id");
String clientSecret = request.getParameter("client_secret");
//2.如果认证过就不需要再认证
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
return authentication;
}
if (clientId == null) {
throw new BadCredentialsException("No client credentials presented");
}
if (clientSecret == null) {
clientSecret = "";
}
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,
clientSecret);
//4.开始认证
return this.getAuthenticationManager().authenticate(authRequest);
}