本文主要解析一下spring security oauth2中AuthorizationServerConfigurerAdapter的allowFormAuthenticationForClients的原理html
主要是讓/oauth/token支持client_id以及client_secret做登陸認證java
spring-security-oauth2-2.0.14.RELEASE-sources.jar!/org/springframework/security/oauth2/config/annotation/web/configuration/AuthorizationServerSecurityConfiguration.javaweb
@Configuration @Order(0) @Import({ ClientDetailsServiceConfiguration.class, AuthorizationServerEndpointsConfiguration.class }) public class AuthorizationServerSecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired private List<AuthorizationServerConfigurer> configurers = 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); } } }
這裏有幾個關鍵點:spring
spring-security-oauth2-2.0.14.RELEASE-sources.jar!/org/springframework/security/oauth2/config/annotation/web/configurers/AuthorizationServerSecurityConfigurer.javajson
public final class AuthorizationServerSecurityConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { 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<Filter> tokenEndpointAuthenticationFilters = new ArrayList<Filter>(); @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<HttpSecurity> 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端登陸的驗證api
spring-security-web-4.2.3.RELEASE-sources.jar!/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.javasession
/** * 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: * <ol> * <li>An <tt>Authentication</tt> 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</li> * <li>An <tt>AuthenticationException</tt> occurs during authentication. The * {@link #unsuccessfulAuthentication(HttpServletRequest, HttpServletResponse, AuthenticationException) * unsuccessfulAuthentication} method will be invoked</li> * <li>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 <tt>Authentication</tt> object is not null. * </ol> */ 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. * <p> * It strips any parameters from the "path" section of the request URL (such as the * jsessionid parameter in <em>http://host/myapp/index.html;jsessionid=blah</em>) * before matching against the <code>filterProcessesUrl</code> property. * <p> * Subclasses may override for special requirements, such as Tapestry integration. * * @return <code>true</code> if the filter should attempt authentication, * <code>false</code> otherwise. */ protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { return requiresAuthenticationRequestMatcher.matches(request); }
這裏先調用了requiresAuthentication來判斷是否須要攔截app
2.0.14.RELEASE/spring-security-oauth2-2.0.14.RELEASE-sources.jar!/org/springframework/security/oauth2/provider/client/ClientCredentialsTokenEndpointFilter.java#ClientCredentialsRequestMatchercurl
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來作。ide
所以,若是是這樣調用,是走這個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); }
@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值小的配置會優先使用,會覆蓋後者。