Spring Security OAuth2源码分析(一)

Stella981
• 阅读 402

本文开始从源码的层面,讲解一些Spring Security Oauth2的认证流程。由于文章涉及了比较多的源码且较长,适合在空余时间段观看,非关键性代码以...代替。

准备工作

首先开启debug信息:

  1. logging:

  2. level:

  3. org.springframework: DEBUG

可以完整的看到内部的运转流程。

client模式稍微简单一些,使用client模式获取token http://localhost:8080/oauth/token?client_id=client_1&client_secret=123456&scope=select&grant_type=client_credentials

由于debug信息太多了,我简单按照顺序列了一下关键的几个类:

  1. ClientCredentialsTokenEndpointFilter

  2. DaoAuthenticationProvider

  3. TokenEndpoint

  4. TokenGranter

@EnableAuthorizationServer

上一篇博客中我们尝试使用了password模式和client模式,有一个比较关键的endpoint:/oauth/token。从这个入口开始分析,spring security oauth2内部是如何生成token的。获取token,与第一篇文章中的两个重要概念之一有关,也就是AuthorizationServer与ResourceServer中的AuthorizationServer。

在之前的配置中

  1. @Configuration

  2. @EnableAuthorizationServer

  3. protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {}

出现了AuthorizationServerConfigurerAdapter 关键类,他关联了三个重要的配置类,分别是

  1. public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {

  2. @Override

  3. public void configure(AuthorizationServerSecurityConfigurer security <1>) throws Exception{

  4. }

  5. @Override

  6. public void configure(ClientDetailsServiceConfigurer clients <2>) throws Exception {

  7. }

  8. @Override

  9. public void configure(AuthorizationServerEndpointsConfigurer endpoints <3>) throws Exception {

  10. }

  11. }

<1> 配置AuthorizationServer安全认证的相关信息,创建ClientCredentialsTokenEndpointFilter核心过滤器

<2> 配置OAuth2的客户端相关信息

<3> 配置AuthorizationServerEndpointsConfigurer众多相关类,包括配置身份认证器,配置认证方式,TokenStore,TokenGranter,OAuth2RequestFactory

我们逐步分析其中关键的类

客户端身份认证核心过滤器ClientCredentialsTokenEndpointFilter(掌握)

截取关键的代码,可以分析出大概的流程 在请求到达/oauth/token之前经过了ClientCredentialsTokenEndpointFilter这个过滤器,关键方法如下

  1. public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)

  2. throws AuthenticationException, IOException, ServletException {

  3. ...

  4. String clientId = request.getParameter("client_id");

  5. String clientSecret = request.getParameter("client_secret");

  6. ...

  7. clientId = clientId.trim();

  8. UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,

  9. clientSecret);

  10. return this.getAuthenticationManager().authenticate(authRequest);

  11. }

顶级身份管理者AuthenticationManager(掌握)

用来从请求中获取clientid,clientsecret,组装成一个UsernamePasswordAuthenticationToken作为身份标识,使用容器中的顶级身份管理器AuthenticationManager去进行身份认证(AuthenticationManager的实现类一般是ProviderManager。而ProviderManager内部维护了一个List ,真正的身份认证是由一系列AuthenticationProvider去完成。而AuthenticationProvider的常用实现类则是DaoAuthenticationProvider,DaoAuthenticationProvider内部又聚合了一个UserDetailsService接口,UserDetailsService才是获取用户详细信息的最终接口,而我们上一篇文章中在内存中配置用户,就是使用了UserDetailsService的一个实现类InMemoryUserDetailsManager)。UML类图可以大概理解下这些类的关系,省略了授权部分。  Spring Security OAuth2源码分析(一)

图1 认证相关UML类图

可能机智的读者会发现一个问题,我前面一篇文章已经提到了client模式是不存在“用户”的概念的,那么这里的身份认证是在认证什么呢?debug可以发现UserDetailsService的实现被适配成了ClientDetailsUserDetailsService,这个设计是将client客户端的信息(clientid,clientsecret)适配成用户的信息(username,password),这样我们的认证流程就不需要修改了。

经过ClientCredentialsTokenEndpointFilter之后,身份信息已经得到了AuthenticationManager的验证。接着便到达了 TokenEndpoint。

Token处理端点TokenEndpoint(掌握)

前面的两个ClientCredentialsTokenEndpointFilter和AuthenticationManager可以理解为一些前置校验,和身份封装,而这个类一看名字就知道和我们的token是密切相关的。

  1. @FrameworkEndpoint

  2. public class TokenEndpoint extends AbstractEndpoint {

  3. @RequestMapping(value = "/oauth/token", method=RequestMethod.POST)

  4. public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam

  5. Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {

  6. ...

  7. String clientId = getClientId(principal);

  8. ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);//<1>

  9. ...

  10. TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);//<2>

  11. ...

  12. OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);//<3>

  13. ...

  14. return getResponse(token);

  15. }

  16. private TokenGranter tokenGranter;

  17. }

<1> 加载客户端信息

<2> 结合请求信息,创建TokenRequest

<3> 将TokenRequest传递给TokenGranter颁发token

省略了一些校验代码之后,真正的/oauth/token端点暴露在了我们眼前,其中方法参数中的Principal经过之前的过滤器,已经被填充了相关的信息,而方法的内部则是依赖了一个TokenGranter 来颁发token。其中OAuth2AccessToken的实现类DefaultOAuth2AccessToken就是最终在控制台得到的token序列化之前的原始类:

  1. public class DefaultOAuth2AccessToken implements Serializable, OAuth2AccessToken {

  2. private static final long serialVersionUID = 914967629530462926L;

  3. private String value;

  4. private Date expiration;

  5. private String tokenType = BEARER_TYPE.toLowerCase();

  6. private OAuth2RefreshToken refreshToken;

  7. private Set<String> scope;

  8. private Map<String, Object> additionalInformation = Collections.emptyMap();

  9. //getter,setter

  10. }

  11. @org.codehaus.jackson.map.annotate.JsonSerialize(using = OAuth2AccessTokenJackson1Serializer.class)

  12. @org.codehaus.jackson.map.annotate.JsonDeserialize(using = OAuth2AccessTokenJackson1Deserializer.class)

  13. @com.fasterxml.jackson.databind.annotation.JsonSerialize(using = OAuth2AccessTokenJackson2Serializer.class)

  14. @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = OAuth2AccessTokenJackson2Deserializer.class)

  15. public interface OAuth2AccessToken {

  16. public static String BEARER_TYPE = "Bearer";

  17. public static String OAUTH2_TYPE = "OAuth2";

  18. public static String ACCESS_TOKEN = "access_token";

  19. public static String TOKEN_TYPE = "token_type";

  20. public static String EXPIRES_IN = "expires_in";

  21. public static String REFRESH_TOKEN = "refresh_token";

  22. public static String SCOPE = "scope";

  23. ...

  24. }

一个典型的样例token响应,如下所示,就是上述类序列化后的结果:

  1. {

  2. "access_token":"950a7cc9-5a8a-42c9-a693-40e817b1a4b0",

  3. "token_type":"bearer",

  4. "refresh_token":"773a0fcd-6023-45f8-8848-e141296cb3cb",

  5. "expires_in":27036,

  6. "scope":"select"

  7. }

TokenGranter(掌握)

先从UML类图对TokenGranter接口的设计有一个宏观的认识

Spring Security OAuth2源码分析(一)

图2 TokenGranter相关UML类图

TokenGranter的设计思路是使用CompositeTokenGranter管理一个List 列表,每一种grantType对应一个具体的真正授权者,在debug过程中可以发现CompositeTokenGranter 内部就是在循环调用五种TokenGranter实现类的grant方法,而granter内部则是通过grantType来区分是否是各自的授权类型。

  1. public class CompositeTokenGranter implements TokenGranter {

  2. private final List<TokenGranter> tokenGranters;

  3. public CompositeTokenGranter(List<TokenGranter> tokenGranters) {

  4. this.tokenGranters = new ArrayList<TokenGranter>(tokenGranters);

  5. }

  6. public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {

  7. for (TokenGranter granter : tokenGranters) {

  8. OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);

  9. if (grant!=null) {

  10. return grant;

  11. }

  12. }

  13. return null;

  14. }

  15. }

五种类型分别是:

  • ResourceOwnerPasswordTokenGranter ==> password密码模式

  • AuthorizationCodeTokenGranter ==> authorization_code授权码模式

  • ClientCredentialsTokenGranter ==> client_credentials客户端模式

  • ImplicitTokenGranter ==> implicit简化模式

  • RefreshTokenGranter ==>refresh_token 刷新token专用

以客户端模式为例,思考如何产生token的,则需要继续研究5种授权者的抽象类:AbstractTokenGranter

  1. public abstract class AbstractTokenGranter implements TokenGranter {

  2. protected final Log logger = LogFactory.getLog(getClass());

  3. //与token相关的service,重点

  4. private final AuthorizationServerTokenServices tokenServices;

  5. //与clientDetails相关的service,重点

  6. private final ClientDetailsService clientDetailsService;

  7. //创建oauth2Request的工厂,重点

  8. private final OAuth2RequestFactory requestFactory;

  9. private final String grantType;

  10. ...

  11. public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {

  12. ...

  13. String clientId = tokenRequest.getClientId();

  14. ClientDetails client = clientDetailsService.loadClientByClientId(clientId);

  15. validateGrantType(grantType, client);

  16. logger.debug("Getting access token for: " + clientId);

  17. return getAccessToken(client, tokenRequest);

  18. }

  19. protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {

  20. return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));

  21. }

  22. protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

  23. OAuth2Request storedOAuth2Request = requestFactory.createOAuth2Request(client, tokenRequest);

  24. return new OAuth2Authentication(storedOAuth2Request, null);

  25. }

  26. ...

  27. }

回过头去看TokenEndpoint中,正是调用了这里的三个重要的类变量的相关方法。由于篇幅限制,不能延展太多,不然没完没了,所以重点分析下AuthorizationServerTokenServices是何方神圣。

AuthorizationServerTokenServices(了解)

AuthorizationServer端的token操作service,接口设计如下:

  1. public interface AuthorizationServerTokenServices {

  2. //创建token

  3. OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;

  4. //刷新token

  5. OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest)

  6. throws AuthenticationException;

  7. //获取token

  8. OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);

  9. }

在默认的实现类DefaultTokenServices中,可以看到token是如何产生的,并且了解了框架对token进行哪些信息的关联。

  1. @Transactional

  2. public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {

  3. OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);

  4. OAuth2RefreshToken refreshToken = null;

  5. if (existingAccessToken != null) {

  6. if (existingAccessToken.isExpired()) {

  7. if (existingAccessToken.getRefreshToken() != null) {

  8. refreshToken = existingAccessToken.getRefreshToken();

  9. // The token store could remove the refresh token when the

  10. // access token is removed, but we want to

  11. // be sure...

  12. tokenStore.removeRefreshToken(refreshToken);

  13. }

  14. tokenStore.removeAccessToken(existingAccessToken);

  15. }

  16. else {

  17. // Re-store the access token in case the authentication has changed

  18. tokenStore.storeAccessToken(existingAccessToken, authentication);

  19. return existingAccessToken;

  20. }

  21. }

  22. // Only create a new refresh token if there wasn't an existing one

  23. // associated with an expired access token.

  24. // Clients might be holding existing refresh tokens, so we re-use it in

  25. // the case that the old access token

  26. // expired.

  27. if (refreshToken == null) {

  28. refreshToken = createRefreshToken(authentication);

  29. }

  30. // But the refresh token itself might need to be re-issued if it has

  31. // expired.

  32. else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {

  33. ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;

  34. if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {

  35. refreshToken = createRefreshToken(authentication);

  36. }

  37. }

  38. OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);

  39. tokenStore.storeAccessToken(accessToken, authentication);

  40. // In case it was modified

  41. refreshToken = accessToken.getRefreshToken();

  42. if (refreshToken != null) {

  43. tokenStore.storeRefreshToken(refreshToken, authentication);

  44. }

  45. return accessToken;

  46. }

简单总结一下AuthorizationServerTokenServices的作用,他提供了创建token,刷新token,获取token的实现。在创建token时,他会调用tokenStore对产生的token和相关信息存储到对应的实现类中,可以是redis,数据库,内存,jwt。

总结

本篇总结了使用客户端模式获取Token时,spring security oauth2内部的运作流程,重点是在分析AuthenticationServer相关的类。其他模式有一定的不同,但抽象功能是固定的,只是具体的实现类会被相应地替换。阅读spring的源码,会发现它的设计中出现了非常多的抽象接口,这对我们理清楚内部工作流程产生了不小的困扰,我的方式是可以借助UML类图,先从宏观理清楚作者的设计思路,这会让我们的分析事半功倍。

下一篇文章重点分析用户携带token访问受限资源时,spring security oauth2内部的工作流程。即ResourceServer相关的类。

本文分享自微信公众号 - Kirito的技术分享(cnkirito)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
刚刚好 刚刚好
4个月前
css问题
1、在IOS中图片不显示(给图片加了圆角或者img没有父级)<div<imgsrc""/</divdiv{width:20px;height:20px;borderradius:20px;overflow:h
blmius blmius
1年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
晴空闲云 晴空闲云
4个月前
css中box-sizing解放盒子实际宽高计算
我们知道传统的盒子模型,如果增加内边距padding和边框border,那么会撑大整个盒子,造成盒子的宽度不好计算,在实务中特别不方便。boxsizing可以设置盒模型的方式,可以很好的设置固定宽高的盒模型。盒子宽高计算假如我们设置如下盒子:宽度和高度均为200px,那么这会这个盒子实际的宽高就都是200px。但是当我们设置这个盒子的边框和内间距的时候,那
艾木酱 艾木酱
3个月前
快速入门|使用MemFire Cloud构建React Native应用程序
MemFireCloud是一款提供云数据库,用户可以创建云数据库,并对数据库进行管理,还可以对数据库进行备份操作。它还提供后端即服务,用户可以在1分钟内新建一个应用,使用自动生成的API和SDK,访问云数据库、对象存储、用户认证与授权等功能,可专
Easter79 Easter79
1年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
1年前
MySQL查询按照指定规则排序
1.按照指定(单个)字段排序selectfromtable_nameorderiddesc;2.按照指定(多个)字段排序selectfromtable_nameorderiddesc,statusdesc;3.按照指定字段和规则排序selec
Stella981 Stella981
1年前
Angular material mat
IconIconNamematiconcode_add\_comment_addcommenticon<maticonadd\_comment</maticon_attach\_file_attachfileicon<maticonattach\_file</maticon_attach\
Wesley13 Wesley13
1年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
helloworld_28799839 helloworld_28799839
4个月前
常用知识整理
Javascript判断对象是否为空jsObject.keys(myObject).length0经常使用的三元运算我们经常遇到处理表格列状态字段如status的时候可以用到vue
helloworld_34035044 helloworld_34035044
7个月前
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为