对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拦截,具体内容如下: