序
本文主要解析一下spring security oauth2中AuthorizationServerConfigurerAdapter的allowFormAuthenticationForClients的原理
allowFormAuthenticationForClients的作用
主要是让/oauth/token支持client_id以及client_secret作登录认证
AuthorizationServerSecurityConfiguration
spring-security-oauth2-2.0.14.RELEASE-sources.jar!/org/springframework/security/oauth2/config/annotation/web/configuration/AuthorizationServerSecurityConfiguration.java
@Configuration@Order(0)@Import({ ClientDetailsServiceConfiguration.class, AuthorizationServerEndpointsConfiguration.class })public class AuthorizationServerSecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired private Listconfigurers = Collections.emptyList(); @Autowired private ClientDetailsService clientDetailsService; @Autowired private AuthorizationServerEndpointsConfiguration endpoints; @Autowired public void configure(ClientDetailsServiceConfigurer clientDetails) throws Exception { for (AuthorizationServerConfigurer configurer : configurers) { configurer.configure(clientDetails); } } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // Over-riding to make sure this.disableLocalConfigureAuthenticationBldr = false // This will ensure that when this configurer builds the AuthenticationManager it will not attempt // to find another 'Global' AuthenticationManager in the ApplicationContext (if available), // and set that as the parent of this 'Local' AuthenticationManager. // This AuthenticationManager should only be wired up with an AuthenticationProvider // composed of the ClientDetailsService (wired in this configuration) for authenticating 'clients' only. } @Override protected void configure(HttpSecurity http) throws Exception { AuthorizationServerSecurityConfigurer configurer = new AuthorizationServerSecurityConfigurer(); FrameworkEndpointHandlerMapping handlerMapping = endpoints.oauth2EndpointHandlerMapping(); http.setSharedObject(FrameworkEndpointHandlerMapping.class, handlerMapping); configure(configurer); http.apply(configurer); String tokenEndpointPath = handlerMapping.getServletPath("/oauth/token"); String tokenKeyPath = handlerMapping.getServletPath("/oauth/token_key"); String checkTokenPath = handlerMapping.getServletPath("/oauth/check_token"); if (!endpoints.getEndpointsConfigurer().isUserDetailsServiceOverride()) { UserDetailsService userDetailsService = http.getSharedObject(UserDetailsService.class); endpoints.getEndpointsConfigurer().userDetailsService(userDetailsService); } // @formatter:off http .authorizeRequests() .antMatchers(tokenEndpointPath).fullyAuthenticated() .antMatchers(tokenKeyPath).access(configurer.getTokenKeyAccess()) .antMatchers(checkTokenPath).access(configurer.getCheckTokenAccess()) .and() .requestMatchers() .antMatchers(tokenEndpointPath, tokenKeyPath, checkTokenPath) .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER); // @formatter:on http.setSharedObject(ClientDetailsService.class, clientDetailsService); } protected void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { for (AuthorizationServerConfigurer configurer : configurers) { configurer.configure(oauthServer); } }}
这里有几个关键点:
- 扩展了WebSecurityConfigurerAdapter方法
- 指定了Order顺序为0,该顺序是值越小优先级别越高
- 配置HttpSecurity的requestMatchers、filter以及相应的AuthenticationManager
AuthorizationServerSecurityConfigurer
spring-security-oauth2-2.0.14.RELEASE-sources.jar!/org/springframework/security/oauth2/config/annotation/web/configurers/AuthorizationServerSecurityConfigurer.java
public final class AuthorizationServerSecurityConfigurer extends SecurityConfigurerAdapter{ private AuthenticationEntryPoint authenticationEntryPoint; private AccessDeniedHandler accessDeniedHandler = new OAuth2AccessDeniedHandler(); private PasswordEncoder passwordEncoder; // for client secrets private String realm = "oauth2/client"; private boolean allowFormAuthenticationForClients = false; private String tokenKeyAccess = "denyAll()"; private String checkTokenAccess = "denyAll()"; private boolean sslOnly = false; /** * Custom authentication filters for the TokenEndpoint. Filters will be set upstream of the default * BasicAuthenticationFilter. */ private List tokenEndpointAuthenticationFilters = new ArrayList (); @Override public void init(HttpSecurity http) throws Exception { registerDefaultAuthenticationEntryPoint(http); 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())); } http.securityContext().securityContextRepository(new NullSecurityContextRepository()).and().csrf().disable() .httpBasic().realmName(realm); } @SuppressWarnings("unchecked") private void registerDefaultAuthenticationEntryPoint(HttpSecurity http) { ExceptionHandlingConfigurer exceptionHandling = http .getConfigurer(ExceptionHandlingConfigurer.class); if (exceptionHandling == null) { return; } if (authenticationEntryPoint==null) { BasicAuthenticationEntryPoint basicEntryPoint = new BasicAuthenticationEntryPoint(); basicEntryPoint.setRealmName(realm); authenticationEntryPoint = basicEntryPoint; } ContentNegotiationStrategy contentNegotiationStrategy = http.getSharedObject(ContentNegotiationStrategy.class); if (contentNegotiationStrategy == null) { contentNegotiationStrategy = new HeaderContentNegotiationStrategy(); } MediaTypeRequestMatcher preferredMatcher = new MediaTypeRequestMatcher(contentNegotiationStrategy, MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_XML, MediaType.MULTIPART_FORM_DATA, MediaType.TEXT_XML); preferredMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); exceptionHandling.defaultAuthenticationEntryPointFor(postProcess(authenticationEntryPoint), preferredMatcher); } @Override public void configure(HttpSecurity http) throws Exception { // ensure this is initialized frameworkEndpointHandlerMapping(); if (allowFormAuthenticationForClients) { clientCredentialsTokenEndpointFilter(http); } for (Filter filter : tokenEndpointAuthenticationFilters) { http.addFilterBefore(filter, BasicAuthenticationFilter.class); } http.exceptionHandling().accessDeniedHandler(accessDeniedHandler); if (sslOnly) { http.requiresChannel().anyRequest().requiresSecure(); } } 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; } //......}
使用了SecurityConfigurerAdapter来进行HttpSecurity的配置,这里主要做的事情,就是如果开启了allowFormAuthenticationForClients,那么就在BasicAuthenticationFilter之前添加clientCredentialsTokenEndpointFilter,使用ClientDetailsUserDetailsService来进行client端登录的验证
AbstractAuthenticationProcessingFilter
spring-security-web-4.2.3.RELEASE-sources.jar!/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java
/** * Invokes the * {@link #requiresAuthentication(HttpServletRequest, HttpServletResponse) * requiresAuthentication} method to determine whether the request is for * authentication and should be handled by this filter. If it is an authentication * request, the * {@link #attemptAuthentication(HttpServletRequest, HttpServletResponse) * attemptAuthentication} will be invoked to perform the authentication. There are * then three possible outcomes: **
*/ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } if (logger.isDebugEnabled()) { logger.debug("Request is to process authentication"); } Authentication authResult; try { authResult = attemptAuthentication(request, response); if (authResult == null) { // return immediately as subclass has indicated that it hasn't completed // authentication return; } sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { logger.error( "An internal error occurred while trying to authenticate the user.", failed); unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { // Authentication failed unsuccessfulAuthentication(request, response, failed); return; } // Authentication success if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } successfulAuthentication(request, response, chain, authResult); } /** * Indicates whether this filter should attempt to process a login request for the * current invocation. *- An Authentication object is returned. The configured * {@link SessionAuthenticationStrategy} will be invoked (to handle any * session-related behaviour such as creating a new session to protect against * session-fixation attacks) followed by the invocation of * {@link #successfulAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, Authentication)} * method
*- An AuthenticationException occurs during authentication. The * {@link #unsuccessfulAuthentication(HttpServletRequest, HttpServletResponse, AuthenticationException) * unsuccessfulAuthentication} method will be invoked
*- Null is returned, indicating that the authentication process is incomplete. The * method will then return immediately, assuming that the subclass has done any * necessary work (such as redirects) to continue the authentication process. The * assumption is that a later request will be received by this method where the * returned Authentication object is not null. *
* It strips any parameters from the "path" section of the request URL (such as the * jsessionid parameter in http://host/myapp/index.html;jsessionid=blah) * before matching against the
filterProcessesUrl
property. ** Subclasses may override for special requirements, such as Tapestry integration. * * @return
true
if the filter should attempt authentication, *false
otherwise. */ protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { return requiresAuthenticationRequestMatcher.matches(request); }
这里先调用了requiresAuthentication来判断是否需要拦截
2.0.14.RELEASE/spring-security-oauth2-2.0.14.RELEASE-sources.jar!/org/springframework/security/oauth2/provider/client/ClientCredentialsTokenEndpointFilter.java#ClientCredentialsRequestMatcher
protected static class ClientCredentialsRequestMatcher implements RequestMatcher { private String path; public ClientCredentialsRequestMatcher(String path) { this.path = path; } @Override public boolean matches(HttpServletRequest request) { String uri = request.getRequestURI(); int pathParamIndex = uri.indexOf(';'); if (pathParamIndex > 0) { // strip everything after the first semi-colon uri = uri.substring(0, pathParamIndex); } String clientId = request.getParameter("client_id"); if (clientId == null) { // Give basic auth a chance to work instead (it's preferred anyway) return false; } if ("".equals(request.getContextPath())) { return uri.endsWith(path); } return uri.endsWith(request.getContextPath() + path); } }
而这个filter只会拦截url中带有client_id和client_secret的请求,而会把使用basic认证传递的方式交给BasicAuthenticationFilter来做。
因此,如果是这样调用,是走这个filter
curl -H "Accept: application/json" http://localhost:8080/oauth/token -d "grant_type=client_credentials&client_id=demoApp&client_secret=demoAppSecret"
如果是这样调用,则是走basic认证
curl -i -X POST -H "Accept: application/json" -u "demoApp:demoAppSecret" http://localhost:8080/oauth/token
而之前spring-security-oauth2-2.0.14.RELEASE-sources.jar!/org/springframework/security/oauth2/config/annotation/web/configurers/AuthorizationServerSecurityConfigurer.java已经设置了ClientDetailsUserDetailsService,因而是支持client_id和client_secret作为用户密码登录(这样就支持不了普通用户的账号密码登录,例外:password方式支持,但前提也需要经过client_id和client_secret认证
)
@Override public void init(HttpSecurity http) throws Exception { registerDefaultAuthenticationEntryPoint(http); 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())); } http.securityContext().securityContextRepository(new NullSecurityContextRepository()).and().csrf().disable() .httpBasic().realmName(realm); }
WebSecurityConfigurerAdapter实例
@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled = true)public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.csrf().disable(); http .requestMatchers().antMatchers("/oauth/**","/login/**","/logout/**") .and() .authorizeRequests() .antMatchers("/oauth/**").authenticated() .and() .formLogin().permitAll(); //新增login form支持用户登录及授权 } @Bean @Override protected UserDetailsService userDetailsService(){ InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser(User.withUsername("demoUser1").password("123456").authorities("USER").build()); manager.createUser(User.withUsername("demoUser2").password("123456").authorities("USER").build()); return manager; } /** * support password grant type * @return * @throws Exception */ @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }}
AuthorizationServerSecurityConfiguration的配置,order为0,则无论后面的WebSecurityConfigurerAdapter怎么配置,只要优先级不比它高,他们针对/oauth/**相关的配置都不生效,都会优先被这里的ClientCredentialsTokenEndpointFilter拦截处理。
小结
这里使用order来提升优先级。没有配置order的话,则不能生效。
比如ResourceServerConfigurerAdapter中配置拦截了/api/**,但是没有配置优先级,最后的WebSecurityConfigurerAdapter如果也有相同的/api/**认证配置的话,则会覆盖前者。
使用多个WebSecurityConfigurerAdapter的话,一般是每个配置分别拦截各自的url,互补重复。如果有配置order的话,则order值小的配置会优先使用,会覆盖后者。