对spring security oauth2 的客户端模式的研究。
首要要下载spring security oauth2的源码,源码中有官方的例子,sparklr2 和tonr2。sparklr2模拟授权服务器和资源服务器,tonr2模拟客户端。
在tonr2的index.jsp中加了一行代码:
<li><a href="${base}trusted/message">trusted message</a></li>
然后打开http://localhost/tonr2/ 就可以点击trusted message这个链接,然后就是触发了oauth2的客户端模式的例子。
1.通过mvc的映射先触发了tonr2的SparklrController类的trusted函数。
2.tonr2的WebMvcConfig中已经初始化了SparklrServiceImpl。
3.trusted函数调用SparklrServiceImpl的getTrustedMessage()函数。
4.getTrustedMessage函数中调用trustedClientRestTemplate.getForObject(URI.create(sparklrTrustedMessageURL), String.class);
5.sparklrTrustedMessageURL同样是通过WebMvcConfig中@Value("${sparklrTrustedMessageURL}") String sparklrTrustedMessageURL,来初始化的。
6.trustedClientRestTemplate同样是通过WebMvcConfig中 @Qualifier("trustedClientRestTemplate") RestOperations trustedClientRestTemplate,来初始化的。
7.trustedClientRestTemplate是通过下面代码初始化的,实际上就是个OAuth2RestTemplate对象。
public OAuth2RestTemplate trustedClientRestTemplate() { return new OAuth2RestTemplate(trusted(), new DefaultOAuth2ClientContext()); }8.trustedClientRestTemplate.getForObject 也就是OAuth2RestTemplate.getForObject 这个方法写在他的父类里。
public <T> T getForObject(String url, Class<T> responseType, Object... urlVariables) throws RestClientException { RequestCallback requestCallback = acceptHeaderRequestCallback(responseType); HttpMessageConverterExtractor<T> responseExtractor = new HttpMessageConverterExtractor<T>(responseType, getMessageConverters(), logger); return execute(url, HttpMethod.GET, requestCallback, responseExtractor, urlVariables); }9.getForObject 里调用了acceptHeaderRequestCallback,acceptHeaderRequestCallback里调用了AcceptHeaderRequestCallback.
@Override protected ClientHttpRequest createRequest(URI uri, HttpMethod method) throws IOException { OAuth2AccessToken accessToken = getAccessToken(); AuthenticationScheme authenticationScheme = resource.getAuthenticationScheme(); if (AuthenticationScheme.query.equals(authenticationScheme) || AuthenticationScheme.form.equals(authenticationScheme)) { uri = appendQueryParameter(uri, accessToken); } ClientHttpRequest req = super.createRequest(uri, method); if (AuthenticationScheme.header.equals(authenticationScheme)) { authenticator.authenticate(resource, getOAuth2ClientContext(), req); } return req; }
public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails resource, AccessTokenRequest request) throws UserRedirectRequiredException, AccessDeniedException { OAuth2AccessToken accessToken = null; OAuth2AccessToken existingToken = null; Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth instanceof AnonymousAuthenticationToken) { if (!resource.isClientOnly()) { throw new InsufficientAuthenticationException( "Authentication is required to obtain an access token (anonymous not allowed)"); } } if (resource.isClientOnly() || (auth != null && auth.isAuthenticated())) { existingToken = request.getExistingToken(); if (existingToken == null && clientTokenServices != null) { existingToken = clientTokenServices.getAccessToken(resource, auth); } if (existingToken != null) { if (existingToken.isExpired()) { if (clientTokenServices != null) { clientTokenServices.removeAccessToken(resource, auth); } OAuth2RefreshToken refreshToken = existingToken.getRefreshToken(); if (refreshToken != null) { accessToken = refreshAccessToken(resource, refreshToken, request); } } else { accessToken = existingToken; } } } // Give unauthenticated users a chance to get a token and be redirected if (accessToken == null) { // looks like we need to try to obtain a new token. accessToken = obtainNewAccessTokenInternal(resource, request); if (accessToken == null) { throw new IllegalStateException("An OAuth 2 access token must be obtained or an exception thrown."); } } if (clientTokenServices != null && (resource.isClientOnly() || auth != null && auth.isAuthenticated())) { clientTokenServices.saveAccessToken(resource, auth, accessToken); } return accessToken; }
for (AccessTokenProvider tokenProvider : chain) { if (tokenProvider.supportsResource(details)) { return tokenProvider.obtainAccessToken(details, request); } }
return getRestTemplate().execute(getAccessTokenUri(resource, form), getHttpMethod(), getRequestCallback(resource, form, headers), extractor , form.toSingleValueMap());
tonr2 10:10:12.997 [DEBUG] ClientCredentialsAccessTokenProvider - Retrieving token from http://localhost:80/sparklr2/oauth/token tonr2 10:10:13.027 [DEBUG] RestTemplate - Created POST request for "http://localhost:80/sparklr2/oauth/token" tonr2 10:10:13.028 [DEBUG] ClientCredentialsAccessTokenProvider - Encoding and sending form: {grant_type=[client_credentials], scope=[trust]} sparklr23 10:10:13.050 [DEBUG] AntPathRequestMatcher - Checking match of request : '/oauth/token'; against '/webjars/**' sparklr23 10:10:13.050 [DEBUG] AntPathRequestMatcher - Checking match of request : '/oauth/token'; against '/images/**' sparklr23 10:10:13.050 [DEBUG] AntPathRequestMatcher - Checking match of request : '/oauth/token'; against '/oauth/uncache_approvals' sparklr23 10:10:13.050 [DEBUG] AntPathRequestMatcher - Checking match of request : '/oauth/token'; against '/oauth/cache_approvals' sparklr23 10:10:13.050 [DEBUG] OrRequestMatcher - Trying to match using Ant [pattern='/oauth/token'] sparklr23 10:10:13.050 [DEBUG] AntPathRequestMatcher - Checking match of request : '/oauth/token'; against '/oauth/token' sparklr23 10:10:13.050 [DEBUG] OrRequestMatcher - matched sparklr23 10:10:13.052 [DEBUG] FilterChainProxy - /oauth/token at position 1 of 11 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter' sparklr23 10:10:13.058 [DEBUG] FilterChainProxy - /oauth/token at position 2 of 11 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter' sparklr23 10:10:13.059 [DEBUG] FilterChainProxy - /oauth/token at position 3 of 11 in additional filter chain; firing Filter: 'HeaderWriterFilter' sparklr23 10:10:13.059 [DEBUG] HstsHeaderWriter - Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@bb64568 sparklr23 10:10:13.059 [DEBUG] FilterChainProxy - /oauth/token at position 4 of 11 in additional filter chain; firing Filter: 'LogoutFilter' sparklr23 10:10:13.059 [DEBUG] AntPathRequestMatcher - Checking match of request : '/oauth/token'; against '/logout' sparklr23 10:10:13.059 [DEBUG] FilterChainProxy - /oauth/token at position 5 of 11 in additional filter chain; firing Filter: 'BasicAuthenticationFilter' sparklr23 10:10:13.063 [DEBUG] BasicAuthenticationFilter - Basic Authentication Authorization header found for user 'my-client-with-registered-redirect' sparklr23 10:10:13.064 [DEBUG] ProviderManager - Authentication attempt using org.springframework.security.authentication.dao.DaoAuthenticationProvider sparklr23 10:10:13.083 [DEBUG] BasicAuthenticationFilter - Authentication success: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@f3bb37ef: Principal: org.springframework.security.core.userdetails.User@3c4746e1: Username: my-client-with-registered-redirect; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_CLIENT; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_CLIENT sparklr23 10:10:13.083 [DEBUG] FilterChainProxy - /oauth/token at position 6 of 11 in additional filter chain; firing Filter: 'RequestCacheAwareFilter' sparklr23 10:10:13.084 [DEBUG] FilterChainProxy - /oauth/token at position 7 of 11 in additional filter chain; firing Filter: 'SecurityContextHolderAwareRequestFilter' sparklr23 10:10:13.086 [DEBUG] FilterChainProxy - /oauth/token at position 8 of 11 in additional filter chain; firing Filter: 'AnonymousAuthenticationFilter' sparklr23 10:10:13.086 [DEBUG] AnonymousAuthenticationFilter - SecurityContextHolder not populated with anonymous token, as it already contained: 'org.springframework.security.authentication.UsernamePasswordAuthenticationToken@f3bb37ef: Principal: org.springframework.security.core.userdetails.User@3c4746e1: Username: my-client-with-registered-redirect; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_CLIENT; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_CLIENT' sparklr23 10:10:13.086 [DEBUG] FilterChainProxy - /oauth/token at position 9 of 11 in additional filter chain; firing Filter: 'SessionManagementFilter' sparklr23 10:10:13.086 [DEBUG] CompositeSessionAuthenticationStrategy - Delegating to org.springframework.security.web.authentication.session.ChangeSessionIdAuthenticationStrategy@135de411 sparklr23 10:10:13.086 [DEBUG] FilterChainProxy - /oauth/token at position 10 of 11 in additional filter chain; firing Filter: 'ExceptionTranslationFilter' sparklr23 10:10:13.086 [DEBUG] FilterChainProxy - /oauth/token at position 11 of 11 in additional filter chain; firing Filter: 'FilterSecurityInterceptor' sparklr23 10:10:13.087 [DEBUG] AntPathRequestMatcher - Checking match of request : '/oauth/token'; against '/oauth/token' sparklr23 10:10:13.088 [DEBUG] FilterSecurityInterceptor - Secure object: FilterInvocation: URL: /oauth/token; Attributes: [fullyAuthenticated] sparklr23 10:10:13.088 [DEBUG] FilterSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@f3bb37ef: Principal: org.springframework.security.core.userdetails.User@3c4746e1: Username: my-client-with-registered-redirect; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_CLIENT; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_CLIENT sparklr23 10:10:13.096 [DEBUG] AffirmativeBased - Voter: org.springframework.security.web.access.expression.WebExpressionVoter@51e2ee04, returned: 1 sparklr23 10:10:13.096 [DEBUG] FilterSecurityInterceptor - Authorization successful sparklr23 10:10:13.096 [DEBUG] FilterSecurityInterceptor - RunAsManager did not change Authentication object sparklr23 10:10:13.097 [DEBUG] FilterChainProxy - /oauth/token reached end of additional filter chain; proceeding with original chain sparklr23 10:10:13.108 [DEBUG] FrameworkEndpointHandlerMapping - Looking up handler method for path /oauth/token sparklr23 10:10:13.111 [DEBUG] FrameworkEndpointHandlerMapping - Returning handler method [public org.springframework.http.ResponseEntity<org.springframework.security.oauth2.common.OAuth2AccessToken> org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.getAccessToken(java.security.Principal,java.util.Map<java.lang.String, java.lang.String>)] sparklr23 10:10:13.131 [DEBUG] ClientCredentialsTokenGranter - Getting access token for: my-client-with-registered-redirect sparklr23 10:10:13.219 [DEBUG] ExceptionTranslationFilter - Chain processed normally sparklr23 10:10:13.219 [DEBUG] SecurityContextPersistenceFilter - SecurityContextHolder now cleared, as request processing completed tonr2 10:10:13.227 [DEBUG] RestTemplate - POST request for "http://localhost:80/sparklr2/oauth/token" resulted in 200 (OK) tonr2 10:10:13.415 [DEBUG] HttpMessageConverterExtractor - Reading [interface org.springframework.security.oauth2.common.OAuth2AccessToken] as "application/json;charset=UTF-8" using [org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@4c825fa3]1.第一次请求被BasicAuthenticationFilter拦截,具体内容如下:
String[] tokens = extractAndDecodeHeader(header, request);先将request的header中Authorization参数拿出来。Authorization = Basic xxxxxxxxxxxxxxxxxxxxxxx;格式得,xxxxxxxxxxx是经过base64编码的。上句代码主要是将xxxxxxxxxxx解码。解码出来的内容应该是my-client-with-registered-redirect:nnnnnnnnn;其中my-client-with-registered-redirect是客户端id,是在你客户端配置的时候你自己设置进去的,同样在认证端也要配置,否则系统将认证失败。nnnnnnnnn是token。
public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported")); // Determine username String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { logger.debug("User '" + username + "' not found"); if (hideUserNotFoundExceptions) { throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } else { throw notFound; } } Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); } try { preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException exception) { if (cacheWasUsed) { // There was a problem, so try again after checking // we're using latest data (i.e. not from the cache) cacheWasUsed = false; user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } else { throw exception; } } postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } return createSuccessAuthentication(principalToReturn, authentication, user); }
public void init(HttpSecurity http) throws Exception { registerDefaultAuthenticationEntryPoint(http); if (passwordEncoder != null) { http.getSharedObject(AuthenticationManagerBuilder.class) .userDetailsService(new ClientDetailsUserDetailsService(clientDetailsService())) .passwordEncoder(passwordEncoder()); } else { http.userDetailsService(new ClientDetailsUserDetailsService(clientDetailsService())); } http.securityContext().securityContextRepository(new NullSecurityContextRepository()).and().csrf().disable() .httpBasic().realmName(realm); }
public InMemoryClientDetailsServiceBuilder inMemory() throws Exception { InMemoryClientDetailsServiceBuilder next = getBuilder().inMemory(); setBuilder(next); return next; }
clients.inMemory().withClient("tonr") .resourceIds(SPARKLR_RESOURCE_ID) .authorizedGrantTypes("authorization_code", "implicit") .authorities("ROLE_CLIENT") .scopes("read", "write") .secret("secret") .and() .withClient("tonr-with-redirect") .resourceIds(SPARKLR_RESOURCE_ID) .authorizedGrantTypes("authorization_code", "implicit") .authorities("ROLE_CLIENT") .scopes("read", "write") .secret("secret") .redirectUris(tonrRedirectUri) .and() .withClient("my-client-with-registered-redirect") .resourceIds(SPARKLR_RESOURCE_ID) .authorizedGrantTypes("authorization_code", "client_credentials") .authorities("ROLE_CLIENT") .scopes("read", "trust") .redirectUris("http://anywhere?key=value")2.第一次请求,被FilterSecurityInterceptor拦截,具体内容如下:
对spring security oauth2 的客户端模式的研究。
首要要下载spring security oauth2的源码,源码中有官方的例子,sparklr2 和tonr2。sparklr2模拟授权服务器和资源服务器,tonr2模拟客户端。
在tonr2的index.jsp中加了一行代码:
<li><a href="${base}trusted/message">trusted message</a></li>
然后打开http://localhost/tonr2/ 就可以点击trusted message这个链接,然后就是触发了oauth2的客户端模式的例子。
1.通过mvc的映射先触发了tonr2的SparklrController类的trusted函数。
2.tonr2的WebMvcConfig中已经初始化了SparklrServiceImpl。
3.trusted函数调用SparklrServiceImpl的getTrustedMessage()函数。
4.getTrustedMessage函数中调用trustedClientRestTemplate.getForObject(URI.create(sparklrTrustedMessageURL), String.class);
5.sparklrTrustedMessageURL同样是通过WebMvcConfig中@Value("${sparklrTrustedMessageURL}") String sparklrTrustedMessageURL,来初始化的。
6.trustedClientRestTemplate同样是通过WebMvcConfig中 @Qualifier("trustedClientRestTemplate") RestOperations trustedClientRestTemplate,来初始化的。
7.trustedClientRestTemplate是通过下面代码初始化的,实际上就是个OAuth2RestTemplate对象。
public OAuth2RestTemplate trustedClientRestTemplate() { return new OAuth2RestTemplate(trusted(), new DefaultOAuth2ClientContext()); }8.trustedClientRestTemplate.getForObject 也就是OAuth2RestTemplate.getForObject 这个方法写在他的父类里。
public <T> T getForObject(String url, Class<T> responseType, Object... urlVariables) throws RestClientException { RequestCallback requestCallback = acceptHeaderRequestCallback(responseType); HttpMessageConverterExtractor<T> responseExtractor = new HttpMessageConverterExtractor<T>(responseType, getMessageConverters(), logger); return execute(url, HttpMethod.GET, requestCallback, responseExtractor, urlVariables); }9.getForObject 里调用了acceptHeaderRequestCallback,acceptHeaderRequestCallback里调用了AcceptHeaderRequestCallback. 10.又调用了execute方法。execute方法有调用了doExecute方法。doExecute方法ClientHttpRequest request = createRequest(url, method);这句话调用了createRequest方法,这个方法是RestTemplate父类InterceptingHttpAccessor的父类HttpAccessor定义的,但是这个方法被RestTemplate的子类OAuth2RestTemplate重写了。代码如下:
@Override protected ClientHttpRequest createRequest(URI uri, HttpMethod method) throws IOException { OAuth2AccessToken accessToken = getAccessToken(); AuthenticationScheme authenticationScheme = resource.getAuthenticationScheme(); if (AuthenticationScheme.query.equals(authenticationScheme) || AuthenticationScheme.form.equals(authenticationScheme)) { uri = appendQueryParameter(uri, accessToken); } ClientHttpRequest req = super.createRequest(uri, method); if (AuthenticationScheme.header.equals(authenticationScheme)) { authenticator.authenticate(resource, getOAuth2ClientContext(), req); } return req; }11.其中getAccessToken方法中调用了acquireAccessToken方法。acquireAccessToken方法又调用了accessTokenProvider.obtainAccessToken。accessTokenProvider 是由AccessTokenProviderChain来实现的。
public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails resource, AccessTokenRequest request) throws UserRedirectRequiredException, AccessDeniedException { OAuth2AccessToken accessToken = null; OAuth2AccessToken existingToken = null; Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth instanceof AnonymousAuthenticationToken) { if (!resource.isClientOnly()) { throw new InsufficientAuthenticationException( "Authentication is required to obtain an access token (anonymous not allowed)"); } } if (resource.isClientOnly() || (auth != null && auth.isAuthenticated())) { existingToken = request.getExistingToken(); if (existingToken == null && clientTokenServices != null) { existingToken = clientTokenServices.getAccessToken(resource, auth); } if (existingToken != null) { if (existingToken.isExpired()) { if (clientTokenServices != null) { clientTokenServices.removeAccessToken(resource, auth); } OAuth2RefreshToken refreshToken = existingToken.getRefreshToken(); if (refreshToken != null) { accessToken = refreshAccessToken(resource, refreshToken, request); } } else { accessToken = existingToken; } } } // Give unauthenticated users a chance to get a token and be redirected if (accessToken == null) { // looks like we need to try to obtain a new token. accessToken = obtainNewAccessTokenInternal(resource, request); if (accessToken == null) { throw new IllegalStateException("An OAuth 2 access token must be obtained or an exception thrown."); } } if (clientTokenServices != null && (resource.isClientOnly() || auth != null && auth.isAuthenticated())) { clientTokenServices.saveAccessToken(resource, auth, accessToken); } return accessToken; }12.这里面调用了obtainNewAccessTokenInternal,obtainNewAccessTokenInternal中调用了ClientCredentialsAccessTokenProvider 的 obtainAccessToken方法。
for (AccessTokenProvider tokenProvider : chain) { if (tokenProvider.supportsResource(details)) { return tokenProvider.obtainAccessToken(details, request); } }13.ClientCredentialsAccessTokenProvider 的 obtainAccessToken方法中他父类的OAuth2AccessTokenSupport的retrieveToken方法。
return getRestTemplate().execute(getAccessTokenUri(resource, form), getHttpMethod(), getRequestCallback(resource, form, headers), extractor , form.toSingleValueMap());上面这句代码执行了accesstoken的获取动作。ORZ终于tmd得到了accesstoken,累死个人。剩下的就是一步一步的返回accesstoken。 14.OAuth2RestTemplate中他还是调用了super.createRequest。在父类的createRequest里有调用了SimpleClientHttpRequestFactory的createRequest,返回来了一个ClientHttpRequest对象。 15.doExecute方法中ClientHttpRequest request = createRequest(url, method); response = request.execute(); 得到父类的一个http请求,然后发出http请求。这里面是要到资源服务器端获取资源。 事实上sparklr2 包含了授权服务器和资源服务器的功能。从上面的分析可以看出客户端tonr2访问了sparklr2两次,第一次是为了获得accesstoken。第二次是为了获得资源。sparklr2 第一次扮演的是认证服务器的角色。第二次扮演的是资源服务器的角色。 接下来就是授权服务器端的逻辑:sparklr2 。 先说相应第一次请求accesstoken的情况。当sparklr2收到来至于tonr2的请求是,这个请求先被sparklr2的过滤器拦截(这些过滤器是spring security 自带的)。一共有10多个拦截器,被几个符合条件的拦截器拦截,BasicAuthenticationFilter AnonymousAuthenticationFilter SessionManagementFilter FilterSecurityInterceptor 具体拦截情况可以看log。
tonr2 10:10:12.997 [DEBUG] ClientCredentialsAccessTokenProvider - Retrieving token from http://localhost:80/sparklr2/oauth/token tonr2 10:10:13.027 [DEBUG] RestTemplate - Created POST request for "http://localhost:80/sparklr2/oauth/token" tonr2 10:10:13.028 [DEBUG] ClientCredentialsAccessTokenProvider - Encoding and sending form: {grant_type=[client_credentials], scope=[trust]} sparklr23 10:10:13.050 [DEBUG] AntPathRequestMatcher - Checking match of request : '/oauth/token'; against '/webjars/**' sparklr23 10:10:13.050 [DEBUG] AntPathRequestMatcher - Checking match of request : '/oauth/token'; against '/images/**' sparklr23 10:10:13.050 [DEBUG] AntPathRequestMatcher - Checking match of request : '/oauth/token'; against '/oauth/uncache_approvals' sparklr23 10:10:13.050 [DEBUG] AntPathRequestMatcher - Checking match of request : '/oauth/token'; against '/oauth/cache_approvals' sparklr23 10:10:13.050 [DEBUG] OrRequestMatcher - Trying to match using Ant [pattern='/oauth/token'] sparklr23 10:10:13.050 [DEBUG] AntPathRequestMatcher - Checking match of request : '/oauth/token'; against '/oauth/token' sparklr23 10:10:13.050 [DEBUG] OrRequestMatcher - matched sparklr23 10:10:13.052 [DEBUG] FilterChainProxy - /oauth/token at position 1 of 11 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter' sparklr23 10:10:13.058 [DEBUG] FilterChainProxy - /oauth/token at position 2 of 11 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter' sparklr23 10:10:13.059 [DEBUG] FilterChainProxy - /oauth/token at position 3 of 11 in additional filter chain; firing Filter: 'HeaderWriterFilter' sparklr23 10:10:13.059 [DEBUG] HstsHeaderWriter - Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@bb64568 sparklr23 10:10:13.059 [DEBUG] FilterChainProxy - /oauth/token at position 4 of 11 in additional filter chain; firing Filter: 'LogoutFilter' sparklr23 10:10:13.059 [DEBUG] AntPathRequestMatcher - Checking match of request : '/oauth/token'; against '/logout' sparklr23 10:10:13.059 [DEBUG] FilterChainProxy - /oauth/token at position 5 of 11 in additional filter chain; firing Filter: 'BasicAuthenticationFilter' sparklr23 10:10:13.063 [DEBUG] BasicAuthenticationFilter - Basic Authentication Authorization header found for user 'my-client-with-registered-redirect' sparklr23 10:10:13.064 [DEBUG] ProviderManager - Authentication attempt using org.springframework.security.authentication.dao.DaoAuthenticationProvider sparklr23 10:10:13.083 [DEBUG] BasicAuthenticationFilter - Authentication success: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@f3bb37ef: Principal: org.springframework.security.core.userdetails.User@3c4746e1: Username: my-client-with-registered-redirect; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_CLIENT; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_CLIENT sparklr23 10:10:13.083 [DEBUG] FilterChainProxy - /oauth/token at position 6 of 11 in additional filter chain; firing Filter: 'RequestCacheAwareFilter' sparklr23 10:10:13.084 [DEBUG] FilterChainProxy - /oauth/token at position 7 of 11 in additional filter chain; firing Filter: 'SecurityContextHolderAwareRequestFilter' sparklr23 10:10:13.086 [DEBUG] FilterChainProxy - /oauth/token at position 8 of 11 in additional filter chain; firing Filter: 'AnonymousAuthenticationFilter' sparklr23 10:10:13.086 [DEBUG] AnonymousAuthenticationFilter - SecurityContextHolder not populated with anonymous token, as it already contained: 'org.springframework.security.authentication.UsernamePasswordAuthenticationToken@f3bb37ef: Principal: org.springframework.security.core.userdetails.User@3c4746e1: Username: my-client-with-registered-redirect; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_CLIENT; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_CLIENT' sparklr23 10:10:13.086 [DEBUG] FilterChainProxy - /oauth/token at position 9 of 11 in additional filter chain; firing Filter: 'SessionManagementFilter' sparklr23 10:10:13.086 [DEBUG] CompositeSessionAuthenticationStrategy - Delegating to org.springframework.security.web.authentication.session.ChangeSessionIdAuthenticationStrategy@135de411 sparklr23 10:10:13.086 [DEBUG] FilterChainProxy - /oauth/token at position 10 of 11 in additional filter chain; firing Filter: 'ExceptionTranslationFilter' sparklr23 10:10:13.086 [DEBUG] FilterChainProxy - /oauth/token at position 11 of 11 in additional filter chain; firing Filter: 'FilterSecurityInterceptor' sparklr23 10:10:13.087 [DEBUG] AntPathRequestMatcher - Checking match of request : '/oauth/token'; against '/oauth/token' sparklr23 10:10:13.088 [DEBUG] FilterSecurityInterceptor - Secure object: FilterInvocation: URL: /oauth/token; Attributes: [fullyAuthenticated] sparklr23 10:10:13.088 [DEBUG] FilterSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@f3bb37ef: Principal: org.springframework.security.core.userdetails.User@3c4746e1: Username: my-client-with-registered-redirect; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_CLIENT; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_CLIENT sparklr23 10:10:13.096 [DEBUG] AffirmativeBased - Voter: org.springframework.security.web.access.expression.WebExpressionVoter@51e2ee04, returned: 1 sparklr23 10:10:13.096 [DEBUG] FilterSecurityInterceptor - Authorization successful sparklr23 10:10:13.096 [DEBUG] FilterSecurityInterceptor - RunAsManager did not change Authentication object sparklr23 10:10:13.097 [DEBUG] FilterChainProxy - /oauth/token reached end of additional filter chain; proceeding with original chain sparklr23 10:10:13.108 [DEBUG] FrameworkEndpointHandlerMapping - Looking up handler method for path /oauth/token sparklr23 10:10:13.111 [DEBUG] FrameworkEndpointHandlerMapping - Returning handler method [public org.springframework.http.ResponseEntity<org.springframework.security.oauth2.common.OAuth2AccessToken> org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.getAccessToken(java.security.Principal,java.util.Map<java.lang.String, java.lang.String>)] sparklr23 10:10:13.131 [DEBUG] ClientCredentialsTokenGranter - Getting access token for: my-client-with-registered-redirect sparklr23 10:10:13.219 [DEBUG] ExceptionTranslationFilter - Chain processed normally sparklr23 10:10:13.219 [DEBUG] SecurityContextPersistenceFilter - SecurityContextHolder now cleared, as request processing completed tonr2 10:10:13.227 [DEBUG] RestTemplate - POST request for "http://localhost:80/sparklr2/oauth/token" resulted in 200 (OK) tonr2 10:10:13.415 [DEBUG] HttpMessageConverterExtractor - Reading [interface org.springframework.security.oauth2.common.OAuth2AccessToken] as "application/json;charset=UTF-8" using [org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@4c825fa3]1.第一次请求被BasicAuthenticationFilter拦截,具体内容如下:
String[] tokens = extractAndDecodeHeader(header, request);先将request的header中Authorization参数拿出来。Authorization = Basic xxxxxxxxxxxxxxxxxxxxxxx;格式得,xxxxxxxxxxx是经过base64编码的。上句代码主要是将xxxxxxxxxxx解码。解码出来的内容应该是my-client-with-registered-redirect:nnnnnnnnn;其中my-client-with-registered-redirect是客户端id,是在你客户端配置的时候你自己设置进去的,同样在认证端也要配置,否则系统将认证失败。nnnnnnnnn是token。 然后生成一个UsernamePasswordAuthenticationToken,用my-client-with-registered-redirect做用户名,nnnnnnnnn做密码。如果是第一次客户端发请求是nnnnnnnnn是空的。接下来由authenticationManager提供认证功能。authenticationManager是通过调用ProviderManager来实现认证的,ProviderManager提供了两个provider,一个是AnonymousAuthenticationProvider,一个是DaoAuthenticationProvider。AnonymousAuthenticationProvider不支持UsernamePasswordAuthenticationToken。所以用DaoAuthenticationProvider的authenticate来认证。DaoAuthenticationProvider是通过它的父类AbstractUserDetailsAuthenticationProvider的authenticate来认证的,父类的authenticate又调用一些子类也就是DaoAuthenticationProvider的方法,例如retrieveUser,retrieveUser调用了UserDetailsService的实现类ClientDetailsUserDetailsService.loadUserByUsername方法。这个类中loadUserByUsername有调用了ClientDetailsService的实现类InMemoryClientDetailsService.loadClientByClientId方法来最终验证当前用户是否合法。
public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported")); // Determine username String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { logger.debug("User '" + username + "' not found"); if (hideUserNotFoundExceptions) { throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } else { throw notFound; } } Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); } try { preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException exception) { if (cacheWasUsed) { // There was a problem, so try again after checking // we're using latest data (i.e. not from the cache) cacheWasUsed = false; user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } else { throw exception; } } postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } return createSuccessAuthentication(principalToReturn, authentication, user); }ClientDetailsUserDetailsService这个实现赋值是通过AuthorizationServerSecurityConfigurer类中配置代码来实现的。
public void init(HttpSecurity http) throws Exception { registerDefaultAuthenticationEntryPoint(http); if (passwordEncoder != null) { http.getSharedObject(AuthenticationManagerBuilder.class) .userDetailsService(new ClientDetailsUserDetailsService(clientDetailsService())) .passwordEncoder(passwordEncoder()); } else { http.userDetailsService(new ClientDetailsUserDetailsService(clientDetailsService())); } http.securityContext().securityContextRepository(new NullSecurityContextRepository()).and().csrf().disable() .httpBasic().realmName(realm); }InMemoryClientDetailsService这个实现赋值是通过ClientDetailsServiceConfigurer类中的配置代码来实现的。OAuth2ServerConfig中AuthorizationServerConfiguration方法会设置相应的客户端的客户端名等信息。当客户端向授权服务器发请求请求时,请求信息会自动包括当前的客户端的客户端名。授权服务器收到后就会和 OAuth2ServerConfig中AuthorizationServerConfiguration方法也就是InMemoryClientDetailsService配置的客户端信息作对比。一致就通过认证,并返回token。不一致就认证失败。
public InMemoryClientDetailsServiceBuilder inMemory() throws Exception { InMemoryClientDetailsServiceBuilder next = getBuilder().inMemory(); setBuilder(next); return next; }
clients.inMemory().withClient("tonr") .resourceIds(SPARKLR_RESOURCE_ID) .authorizedGrantTypes("authorization_code", "implicit") .authorities("ROLE_CLIENT") .scopes("read", "write") .secret("secret") .and() .withClient("tonr-with-redirect") .resourceIds(SPARKLR_RESOURCE_ID) .authorizedGrantTypes("authorization_code", "implicit") .authorities("ROLE_CLIENT") .scopes("read", "write") .secret("secret") .redirectUris(tonrRedirectUri) .and() .withClient("my-client-with-registered-redirect") .resourceIds(SPARKLR_RESOURCE_ID) .authorizedGrantTypes("authorization_code", "client_credentials") .authorities("ROLE_CLIENT") .scopes("read", "trust") .redirectUris("http://anywhere?key=value")2.第一次请求,被FilterSecurityInterceptor拦截,具体内容如下: FilterSecurityInterceptor. invoke事件下有主要逻辑。 当次请求第一被拦截时。fi.getRequest().getAttribute(FILTER_APPLIED) == null 要为它赋值, fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);。 然后执行AbstractSecurityInterceptor.beforeInvocation函数。这里面先执行 Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);这个函数作用是根据当前的url获取,我们根据这个url定义的访问权限。这个数据在什么地方配置或是说在什么地方初始化,我们一会再说。然后通过authenticateIfRequired方法获得Authentication。 authenticateIfRequired方法主要是通过 SecurityContextHolder.getContext().getAuthentication();( SecurityContextHolder.getContext()是在上一个拦截器 BasicAuthenticationFilter中赋值的 SecurityContextHolder.getContext().setAuthentication(authResult);) 。获取authenticated后然后进行决定是否允许当前请求访问。 this.accessDecisionManager.decide(authenticated, object, attributes); 默认是用AffirmativeBased来实现的。HttpConfigurationBuilder.createFilterSecurityInterceptor有相应的逻辑,可以自己配置如果没有配置默认用 AffirmativeBased。有几种实现方式( UnanimousBased.java 只要有一个Voter不能完全通过权限要求,就禁止访问。 AffirmativeBased.java只要有一个Voter可以通过权限要求,就可以访问。 ConsensusBased.java只要通过的Voter比禁止的Voter数目多就可以访问了)。 HttpConfigurationBuilder.createFilterSecurityInterceptor还初始化了WebExpressionVoter等信息。 AffirmativeBased调用了 WebExpressionVoter的vote函数。 vote函数先找到当前类也就是 WebExpressionVoter能处理的类型: WebExpressionConfigAttribute。DefaultWebSecurityExpressionHandler用spring默认的spel处理器StandardEvaluationContext来解析SpEL语法。DefaultWebSecurityExpressionHandler.createEvaluationContext会建立一个评估的上下文,这个函数有连个参数,1是authentication,2FilterInvocation,主要是通过1认证结果和2的访问路径构建评估上下文。ExpressionUtils.evaluateAsBoolean通过weca.getAuthorizeExpression()比较ctx得出是否通过还是拒绝的结果。说的简单点 weca.getAuthorizeExpression()是配置中要求当前路径需要什么样的权限,例如/token需要fullauthorcation的权限, ctx是当前用户拥有的权限。 然后return InterceptorStatusToken,然后FilterSecurityInterceptor的invoke方法的 InterceptorStatusToken token = super.beforeInvocation(fi);这句话就走完了。