上一篇博客在spring security oauth的基础上实现了资源服务器和认证服务器,使得服务能顺利按照常用的4种授权模式签发token,这一篇博客就是在原来已经实现好的短信验证码登录,表单登录以及社交登录的基础上,实现基于token的认证方式
这篇博客的主要目的是使得我们自己的认证模式嫁接到标准的oauth协议上,在此之前,我们需要简单研究一下spring security oauth的源码
先给出一张图
肯定是懵逼的
这个图只是涉及到了的接口和相关的类,蓝色的表示接口,其中的括号表示实现类
这个是用来处理/oauth/token
这个请求的,上篇博客说道,简单集成spring-security-oauth之后,会自动注入一些请求路径,其中就包括oauth/token,这个请求就是获取token的关键请求,而TokenEndPoint可以看成是处理这个请求的controller,其部分源码如下
//以下源码位于org.springframework.security.oauth2.provider.endpoint.TokenEndpoint @FrameworkEndpoint public class TokenEndpoint extends AbstractEndpoint { private OAuth2RequestValidator oAuth2RequestValidator = new DefaultOAuth2RequestValidator(); private Set<HttpMethod> allowedRequestMethods = new HashSet<HttpMethod>(Arrays.asList(HttpMethod.POST)); //针对oauth/token的get请求处理 @RequestMapping(value = "/oauth/token", method=RequestMethod.GET) public ResponseEntity<OAuth2AccessToken> getAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { if (!allowedRequestMethods.contains(HttpMethod.GET)) { throw new HttpRequestMethodNotSupportedException("GET"); } return postAccessToken(principal, parameters); } //针对oauth/token的 post 请求处理 @RequestMapping(value = "/oauth/token", method=RequestMethod.POST) public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { if (!(principal instanceof Authentication)) { throw new InsufficientAuthenticationException( "There is no client authentication. Try adding an appropriate authentication filter."); } String clientId = getClientId(principal); //TODO:注意这里,父类中维护了一个clientDetailsService的属性,这里是获取clientDetailsService,然后调用clientDetailsService的loadClientByClientId方法 ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId); //TODO:根据ClientDetails信息封装TokenRequest, 这个tokenRequest中包含请求中的其他信息,比如:认证类型grant_type,以及对应的认证参数 TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient); //判断clientid是否匹配 if (clientId != null && !clientId.equals("")) { // Only validate the client details if a client authenticated during this // request. if (!clientId.equals(tokenRequest.getClientId())) { // double check to make sure that the client ID in the token request is the same as that in the // authenticated client throw new InvalidClientException("Given client ID does not match authenticated client"); } } //校验scope if (authenticatedClient != null) { oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient); } if (!StringUtils.hasText(tokenRequest.getGrantType())) { throw new InvalidRequestException("Missing grant type"); } if (tokenRequest.getGrantType().equals("implicit")) { throw new InvalidGrantException("Implicit grant type not supported from token endpoint"); } //是否是授权码模式(这里涉及一个稍微复杂点的scope的读取) if (isAuthCodeRequest(parameters)) { // The scope was requested or determined during the authorization step if (!tokenRequest.getScope().isEmpty()) { logger.debug("Clearing scope of incoming token request"); tokenRequest.setScope(Collections.<String> emptySet()); } } if (isRefreshTokenRequest(parameters)) { // A refresh token has its own default scopes, so we should ignore any added by the factory here. tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE))); } //TODO:这里会调用TokenGrant中的grant方法,TokenGrant其实封装的是我们四种认证方式的具体实现, //TODO:这里会根据请求中指定的grant_type调用具体的认证方式,并返回相关token //TODO:这个token由两部分组成,一部分为OAuth2Request,另一部分为Authentication(这个是具体用户的认证信息) OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest); if (token == null) { throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType()); } return getResponse(token); } //以下省略 }
可以联想到我们之前的UserDetailsService,这个用于获取用户详细信息的服务类,ClientDetailsService
就是用来根据请求中传递进来的clientId,去读取客户端的相关信息,然后封装到ClientDetails中
这个接口定义了根据不同的认证类型,生成token的方式,其源码只有一行
public interface TokenGranter { OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest); }
在TokenEndPoint
请求处理方法的最后,会调用TokenGranter中的该方法完成OAuth2AccessToken
的生成。下面以密码认证方式为例,从TokenGranter开始,走一遍源码流程。
在之前的分析基础上,oauth/token的请求,会首先走到这里
//TODO:这里会调用TokenGrant中的grant方法,TokenGrant其实封装的是我们四种认证方式的具体实现, //TODO:这里会根据请求中指定的grant_type调用具体的认证方式,并返回相关token //TODO:这个token由两部分组成,一部分为OAuth2Request,另一部分为Authentication(这个是具体用户的认证信息) OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
之后调用CompositeTokenGranter
中的grant方法(CompositeTokenGranter
是一个聚合了所有TokenGranter的组件)。
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) { for (TokenGranter granter : tokenGranters) { OAuth2AccessToken grant = granter.grant(grantType, tokenRequest); if (grant!=null) { return grant; } } return null; }
逻辑也很简单,根据上传的grantType选择合适的TokenGranter
,并调用其中的grant方法
TokenGranter还有一个抽象类,调用grant方法的时候,会先进入到这个抽象类中的方法
//以下代码位于:org.springframework.security.oauth2.provider.token.AbstractTokenGranter#grant public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) { //TODO:校验grantType if (!this.grantType.equals(grantType)) { return null; } //获取ClientDetails String clientId = tokenRequest.getClientId(); ClientDetails client = clientDetailsService.loadClientByClientId(clientId); validateGrantType(grantType, client); logger.debug("Getting access token for: " + clientId); //TODO:这里就是利用ClientDetails和tokenRequest创建AccessToken return getAccessToken(client, tokenRequest); } //org.springframework.security.oauth2.provider.token.AbstractTokenGranter#getAccessToken protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) { //TODO:在创建AccessToken之前,会先去获取OAuth2Authentication对象 return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest)); }
构造OAuth2Authentication,不同的授权方式也会有不同,毕竟认证的方式不同。以密码授权模式为例。如果是密码的授权模式,则TokenGranter中getAccessToken中获取OAuth2Authentication对象的方法,最终会走到如下实例中
//以下代码位于: //org.springframework.security.oauth2.provider.password.ResourceOwnerPasswordTokenGranter#getOAuth2Authentication @Override protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) { Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters()); String username = parameters.get("username"); String password = parameters.get("password"); // Protect from downstream leaks of password parameters.remove("password"); //初始化一个UsernamePasswordAuthenticationToken Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password); ((AbstractAuthenticationToken) userAuth).setDetails(parameters); try { //TODO:调用authenticate进行认证,这里实际就会调用我们自定义的认证UserDetailsService userAuth = authenticationManager.authenticate(userAuth); } catch (AccountStatusException ase) { //covers expired, locked, disabled cases (mentioned in section 5.2, draft 31) throw new InvalidGrantException(ase.getMessage()); } catch (BadCredentialsException e) { // If the username/password are wrong the spec says we should send 400/invalid grant throw new InvalidGrantException(e.getMessage()); } if (userAuth == null || !userAuth.isAuthenticated()) { throw new InvalidGrantException("Could not authenticate user: " + username); } //初始化 OAuth2Request(包含ClientDetails信息和TokenRequest) OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest); //初始化OAuth2Authentication(包含OAuth2Request和认证成功之后的用户认证信息) return new OAuth2Authentication(storedOAuth2Request, userAuth); } //TODO:org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory#createOAuth2Request(org.springframework.security.oauth2.provider.ClientDetails, org.springframework.security.oauth2.provider.TokenRequest) //TODO:简单的将client传给tokenRequest创建OAuth2Request public OAuth2Request createOAuth2Request(ClientDetails client, TokenRequest tokenRequest) { return tokenRequest.createOAuth2Request(client); }
在创建完成了OAuth2Authentication之后,就是创建令牌了,spring-security-oauth提供了一个默认的tokenService的实现,名称为DefaultTokenServices
,相关的源码如下
//TODO:位于org.springframework.security.oauth2.provider.token.DefaultTokenServices#createAccessToken(org.springframework.security.oauth2.provider.OAuth2Authentication) @Transactional public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException { //TODO:根据OAuth2Authentication获取之前的 AccessToken OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication); OAuth2RefreshToken refreshToken = null; if (existingAccessToken != null) {//如果之前的 AccessToken 不为空, if (existingAccessToken.isExpired()) {//如果之前的AccessToken过期了 if (existingAccessToken.getRefreshToken() != null) { refreshToken = existingAccessToken.getRefreshToken(); // The token store could remove the refresh token when the // access token is removed, but we want to // be sure... tokenStore.removeRefreshToken(refreshToken);//移除 } tokenStore.removeAccessToken(existingAccessToken);//移除 } else {//如果没有过期,则重新存储一下原有的AccessToken // Re-store the access token in case the authentication has changed tokenStore.storeAccessToken(existingAccessToken, authentication); return existingAccessToken; } } // Only create a new refresh token if there wasn't an existing one // associated with an expired access token. // Clients might be holding existing refresh tokens, so we re-use it in // the case that the old access token // expired. if (refreshToken == null) {//TODO:如果原有的AccessToken 为空,则创建一个刷新令牌,不是初始化的AccessToken refreshToken = createRefreshToken(authentication); } // But the refresh token itself might need to be re-issued if it has // expired. else if (refreshToken instanceof ExpiringOAuth2RefreshToken) { ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken; if (System.currentTimeMillis() > expiring.getExpiration().getTime()) { refreshToken = createRefreshToken(authentication); } } //TODO:创建 AccessToken OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken); tokenStore.storeAccessToken(accessToken, authentication); // In case it was modified refreshToken = accessToken.getRefreshToken(); if (refreshToken != null) { tokenStore.storeRefreshToken(refreshToken, authentication); } return accessToken; } //TODO:org.springframework.security.oauth2.provider.token.DefaultTokenServices#createAccessToken(org.springframework.security.oauth2.provider.OAuth2Authentication, org.springframework.security.oauth2.common.OAuth2RefreshToken) //TODO:创建 AccessToken private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) { DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString()); int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request()); if (validitySeconds > 0) { token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L))); } token.setRefreshToken(refreshToken); token.setScope(authentication.getOAuth2Request().getScope()); //TODO:如果有 accessTokenEnhancer 则调用其enhance方法,这个方法中可以完成对token的自定义 return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token; }
有点絮叨,有点凌乱,不过看完需要耐心,看到这里,再回过头看总结的图片,或许会清晰很多