目录
简单示例
自定义clientDetailsService
ClientDetailServiceImpl
程序流程
Endpoint
EnableAuthorizationServer
AuthorizationServerSecurityConfiguration
ClientDetailsServiceConfiguration
自定义authorizationCodeServices
RedisAuthenticationCodeServices
程序流程
AuthorizationServerEndpointsConfiguration
authorizationCodeServices
getEndpointsConfigurer
AuthorizationEndpoint
getAuthorizationCodeResponse
generateCode
自定义tokenServices
DefaultTokenServices
getTokenStore
RedisTokenStore
程序流程
postAccessToken
grant
getAccessToken
总结
本篇文章我们来研究spring-security框架里如何自定义clientDetailsService, authorizationCodeServices和tokenServices。
只要使用了spring-security框架,那么,开发人员就免不了要对这些服务进行自定义。
简单示例
首先,新建一个config包用于存放spring-security通用配置;然后,新建一个WebSecurityConfig类,使其继承WebSecurityConfigurerAdapter。
然后,给WebSecutiryConfig类中加上@EnableWebSecurity 注解后,这样便会自动被 Spring发现并注册。
如下所示:
@EnableAuthorizationServer public class OAuth2ServerConfig extends AuthorizationServerConfigurerAdapter { ... ... @Autowired private JedisConnectionFactory jedisConnectionFactory; @Autowired private RedisAuthenticationCodeServices redisAuthorizationCodeServices; @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { ... ... endpoints.authorizationCodeServices(redisAuthorizationCodeServices); DefaultTokenServices tokenServices = new DefaultTokenServices(); tokenServices.setSupportRefreshToken(true); tokenServices.setTokenStore(getTokenStore()); tokenServices.setAccessTokenValiditySeconds((int)TimeUnit.DAYS.toSeconds(180)); tokenServices.setRefreshTokenValiditySeconds((int)TimeUnit.DAYS.toSeconds(360)); tokenServices.setReuseRefreshToken(false); endpoints.tokenServices(tokenServices); } @Bean TokenStore getTokenStore() { return new RedisTokenStore(jedisConnectionFactory); } @Bean public ClientDetailServiceImpl getClientDetails() { return new ClientDetailServiceImpl(); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // 配置客户端, 用于client认证 clients.withClientDetails(getClientDetails()); } |
在这里:
通过clients.withClientDetails(getClientDetails())进行自定义服务clientDetailsService;
通过endpoints.authorizationCodeServices(redisAuthorizationCodeServices)进行自定义服务authorizationCodeServices;
通过endpoints.tokenServices(tokenServices)进行自定义服务tokenServices。
自定义clientDetailsService
点击简单示例里的ClientDetailServiceImpl()方法,如下所示:
ClientDetailServiceImpl
package com.example.author.service.impl; ... ... public class ClientDetailServiceImpl implements ClientDetailsService { @Override public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException { SysOauthClient sysOauthClient = selectByClientId(clientId); if (sysOauthClient != null) { String clientSecret = sysOauthClient.getClientSecret(); String redirectUrl = sysOauthClient.getRedirectUrl(); BaseClientDetails client = new BaseClientDetails(); client.setClientId(clientId); client.setClientSecret(clientSecret); client.setRegisteredRedirectUri(new HashSet<String>() {{ add(redirectUrl); }}); client.setAuthorizedGrantTypes(new HashSet<String>() {{ add("authorization_code"); add("refresh_token"); add("client_credentials"); add("password"); add("implicit"); }}); client.setScope(new HashSet<String>() {{ add("all"); }}); return client; } else { throw new NoSuchClientException("No client with requested id: " + clientId); } } } |
在这里,通过实现框架提供的接口ClientDetailsService并重写loadClientByClientId()方法的方式来实现自定义clientDetailsService的功能。
程序流程
通过前面的文章,我们知道/oauth/authorize和/oauth/token两个端点都有依赖服务clientDetailServices,如下所示:
Endpoint
@Bean public AuthorizationEndpoint authorizationEndpoint() throws Exception { AuthorizationEndpoint authorizationEndpoint = new AuthorizationEndpoint(); ... ... authorizationEndpoint.setClientDetailsService(clientDetailsService); ... ... return authorizationEndpoint; } @Bean public TokenEndpoint tokenEndpoint() throws Exception { TokenEndpoint tokenEndpoint = new TokenEndpoint(); tokenEndpoint.setClientDetailsService(clientDetailsService); ... ... return tokenEndpoint; } |
在这里,直接将clientDetailServices赋值给两个端点实例authorizationEndpoint和tokenEndpoint。
接下来,我们探究一下ClientDetailsServiceConfigurer.withClientDetails()是如何实例化clientDetailsService对象的?
点击简单示例里的EnableAuthorizationServer,如下所示:
EnableAuthorizationServer
@Import({AuthorizationServerEndpointsConfiguration.class, AuthorizationServerSecurityConfiguration.class}) public @interface EnableAuthorizationServer { } |
点击AuthorizationServerSecurityConfiguration类,如下所示:
AuthorizationServerSecurityConfiguration
@Import({ ClientDetailsServiceConfiguration.class, AuthorizationServerEndpointsConfiguration.class }) public class AuthorizationServerSecurityConfiguration extends WebSecurityConfigurerAdapter { ... ... |
点击ClientDetailsServiceConfiguration类,如下所示:
ClientDetailsServiceConfiguration
@Configuration public class ClientDetailsServiceConfiguration { private ClientDetailsServiceConfigurer configurer = new ClientDetailsServiceConfigurer(new ClientDetailsServiceBuilder()); @Bean public ClientDetailsServiceConfigurer clientDetailsServiceConfigurer() { return configurer; } @Bean @Lazy @Scope(proxyMode=ScopedProxyMode.INTERFACES) public ClientDetailsService clientDetailsService() throws Exception { return configurer.and().build(); } } |
在这里,我们看到了Bean对象clientDetailsService的定义了,而且也知道是通过configurer.and().build()的方式来实例化clientDetailsService对象。具体configurer.and().build()是如何实例化的呢?这里涉及的内容也不少,留着后续再探究。
自定义authorizationCodeServices
点击简单示例里的RedisAuthenticationCodeServices类,如下所示:
RedisAuthenticationCodeServices
package com.example.author.service; @Service public class RedisAuthenticationCodeServices extends RandomValueAuthorizationCodeServices { ... ... @Resource private RedisTemplate redisTemplate; @Override protected OAuth2Authentication remove(final String code) { OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) redisTemplate.execute(new RedisCallback<OAuth2Authentication>() { @Override public OAuth2Authentication doInRedis(RedisConnection connection) throws DataAccessException { byte[] keyByte = codeKey(code).getBytes(); byte[] valueByte = connection.get(keyByte); if (valueByte != null) { connection.del(keyByte); return (OAuth2Authentication)SerializationUtils.deserialize(valueByte); } return null; } }); return oAuth2Authentication; } @Override protected void store(String code, OAuth2Authentication authentication) { try { redisTemplate.execute(new RedisCallback<Long>() { @Override public Long doInRedis(RedisConnection connection) throws DataAccessException { connection.set(codeKey(code).getBytes(), SerializationUtils.serialize(authentication), Expiration.from(5, TimeUnit.MINUTES), RedisStringCommands.SetOption.UPSERT); return 1L; } }); } catch (Exception e) { log.error("保存authentication code 失败", e); } } } |
在这里,通过继承类RandomValueAuthorizationCodeServices并重写remove()和store()方法来实现自定义authorizationCodeServices的功能。
程序流程
通过前面的文章,我们知道/oauth/authorize端点有依赖服务authorizationCodeServices,如下所示:
AuthorizationServerEndpointsConfiguration
@Import(TokenKeyEndpointRegistrar.class) public class AuthorizationServerEndpointsConfiguration { ... ... @Bean public AuthorizationEndpoint authorizationEndpoint() throws Exception { AuthorizationEndpoint authorizationEndpoint = new AuthorizationEndpoint(); ... ... authorizationEndpoint.setAuthorizationCodeServices(authorizationCodeServices()); ... ... return authorizationEndpoint; } |
在这里,将authorizationCodeServices()方法返回的对象赋值给authorizationEndpoint对象的authorizationCodeServices属性。
点击authorizationCodeServices()方法,如下所示:
authorizationCodeServices
private AuthorizationCodeServices authorizationCodeServices() throws Exception { return getEndpointsConfigurer().getAuthorizationCodeServices(); } |
在这里,返回的对象就是我们自定义的authorizationCodeServices对象,点击getAuthorizationCodeServices()方法,如下所示:
private AuthorizationServerEndpointsConfigurer endpoints = new AuthorizationServerEndpointsConfigurer(); public AuthorizationServerEndpointsConfigurer getEndpointsConfigurer() { if (!endpoints.isTokenServicesOverride()) { try { endpoints.tokenServices(endpoints.getDefaultAuthorizationServerTokenServices()); } catch (Exception e) { throw new BeanCreationException("Cannot create token services", e); } } return endpoints; } |
在这里,由于我们自定义了tokenServices对象,endpoints.isTokenServicesOverride()是true,所以,getEndpointsConfigurer()方法直接返回AuthorizationServerEndpointsConfigurer对象。
接下来,我们来探究一下/oauth/authorize端点在接收到HTTP请求后,是如何访问authorizationCodeServices服务的?
AuthorizationEndpoint
@FrameworkEndpoint @SessionAttributes({AuthorizationEndpoint.AUTHORIZATION_REQUEST_ATTR_NAME, AuthorizationEndpoint.ORIGINAL_AUTHORIZATION_REQUEST_ATTR_NAME}) public class AuthorizationEndpoint extends AbstractEndpoint { ... ... private AuthorizationCodeServices authorizationCodeServices = new InMemoryAuthorizationCodeServices(); ... ... @RequestMapping(value = "/oauth/authorize") public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters, SessionStatus sessionStatus, Principal principal) { AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters); Set<String> responseTypes = authorizationRequest.getResponseTypes(); if (!responseTypes.contains("token") && !responseTypes.contains("code")) { throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes); } ... ... if (authorizationRequest.isApproved()) { if (responseTypes.contains("token")) { return getImplicitGrantResponse(authorizationRequest); } if (responseTypes.contains("code")) { return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,(Authentication) principal)); } } ... ... } |
点击getAuthorizationCodeResponse()方法,如下所示:
getAuthorizationCodeResponse
private View getAuthorizationCodeResponse(AuthorizationRequest authorizationRequest, Authentication authUser) { try { return new RedirectView(getSuccessfulRedirect(authorizationRequest, generateCode(authorizationRequest, authUser)), false, true, false); } catch (OAuth2Exception e) { return new RedirectView(getUnsuccessfulRedirect(authorizationRequest, e, false), false, true, false); } } |
点击generateCode()方法,如下所示:
generateCode
private String generateCode(AuthorizationRequest authorizationRequest, Authentication authentication) throws AuthenticationException { try { OAuth2Request storedOAuth2Request = getOAuth2RequestFactory().createOAuth2Request(authorizationRequest); OAuth2Authentication combinedAuth = new OAuth2Authentication(storedOAuth2Request, authentication); String code = authorizationCodeServices.createAuthorizationCode(combinedAuth); return code; } catch (OAuth2Exception e) { if (authorizationRequest.getState() != null) { e.addAdditionalInformation("state", authorizationRequest.getState()); } throw e; } } |
在这里,可以看到调用了我们自定义的服务authorizationCodeServices的方法createAuthorizationCode()来生成授权码。
自定义tokenServices
点击简单示例里的DefaultTokenServices类,如下所示:
DefaultTokenServices
package org.springframework.security.oauth2.provider.token; ... ... public class DefaultTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices, ConsumerTokenServices, InitializingBean { ... ... |
在这里,通过实例化DefaultTokenServices 对象,并设置tokenStore等属性的方式来实现自定义tokenServices的功能。
点击简单示例里的getTokenStore()方法,如下所示:
getTokenStore
@Bean TokenStore getTokenStore() { return new RedisTokenStore(jedisConnectionFactory); } |
点击RedisTokenStore类,如下所示:
RedisTokenStore
package org.springframework.security.oauth2.provider.token.store.redis; ... ... public class RedisTokenStore implements TokenStore { ... ... |
在这里,类RedisTokenStore也是框架提供的类,可以直接使用。
程序流程
通过前面的文章,我们知道/oauth/token端点有依赖服务tokenServices,如下所示:
postAccessToken
public class TokenEndpoint extends AbstractEndpoint { ... ... @RequestMapping(value = "/oauth/token", method=RequestMethod.POST) public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { ... ... OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest); if (token == null) { throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType()); } return getResponse(token); } |
在这里,点击getTokenGranter().grant()方法,如下所示:
grant
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) { if (!this.grantType.equals(grantType)) { return null; } String clientId = tokenRequest.getClientId(); ClientDetails client = clientDetailsService.loadClientByClientId(clientId); validateGrantType(grantType, client); if (logger.isDebugEnabled()) { logger.debug("Getting access token for: " + clientId); } return getAccessToken(client, tokenRequest); } |
在这里,点击getAccessToken()方法,如下所示:
getAccessToken
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) { return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest)); } |
在这里,可以看到调用了我们自定义的服务tokenServices服务的createAccessToken()方法来生成TOKEN。
总结
spring-security框架里自定义clientDetailsService, authorizationCodeServices和tokenServices,同样都是自定义服务,可是差别还是比较大的。
这个给开发带来比较大的麻烦,显得不太友好。