历史文章

[Spring Security OAuth2.0认证授权一:框架搭建和认证测试]
[Spring Security OAuth2.0认证授权二:搭建资源服务]
[Spring Security OAuth2.0认证授权三:使用JWT令牌]
[Spring Security OAuth2.0认证授权四:分布式系统认证授权]

上一篇文章讲解了如何在分布式系统环境下进行认证和鉴权,总体来说就是网关认证,目标服务鉴权,但是存在着一个问题:关于用户信息,目标服务只能获取到网关转发过来的username信息,为啥呢,因为认证服务颁发jwt令牌的时候就只存放了这么多信息,我们到jwt.io网站上贴出jwt令牌查看下payload中内容就就知道有什么内容了:

本篇文章的目的就是为了解决该问题,把用户信息(用户名、头像、手机号、邮箱等)放到jwt token中,经过网关解析之后携带用户信息访问目标服务,目标服务将用户信息保存到上下文并保证线程安全性的情况下封装成工具类提供给各种环境下使用。

注:本文章基于源代码https://gitee.com/kdyzm/spring-security-oauth-study/tree/v5.0.0 分析和改造。

一、实现UserDetailsService接口

1.问题分析和修改

jwt令牌中用户信息过于少的原因在于认证服务auth-server中com.kdyzm.spring.security.auth.center.service.MyUserDetailsServiceImpl#loadUserByUsername 方法中的这段代码

return User.withUsername(tUser.getUsername()).password(tUser.getPassword()).authorities(array).build();

这里User类实现了UserDetailsService接口,并使用建造者模式生成了需要的UserDetailsService对象,可以看到生成该对象仅仅传了三个参数,而用户信息仅仅有用户名和password两个参数———那么如何扩展用户信息就一目了然了,我们自己也实现UserDetailsService接口然后返回改值不就好了吗?不好!!实现UserDetailsService接口要实现它需要的好几个方法,不如直接继承User类,在改动最小的情况下保持原有的功能基本不变,这里定义UserDetailsExpand继承User

public class UserDetailsExpand extends User {public UserDetailsExpand(String username, String password, Collection<? extends GrantedAuthority> authorities) {super(username, password, authorities);}//userIdprivate Integer id;//电子邮箱private String email;//手机号private String mobile;private String fullname;//Getter/Setter方法略}

之后,修改com.kdyzm.spring.security.auth.center.service.MyUserDetailsServiceImpl#loadUserByUsername方法返回该类的对象即可

        UserDetailsExpand userDetailsExpand = new UserDetailsExpand(tUser.getUsername(), tUser.getPassword(), AuthorityUtils.createAuthorityList(array));userDetailsExpand.setId(tUser.getId());userDetailsExpand.setMobile(tUser.getMobile());userDetailsExpand.setFullname(tUser.getFullname());return userDetailsExpand;

2.测试修改和源码分析

修改了以上代码之后我们启动服务,获取jwt token之后查看其中的内容,会发现用户信息并没有填充进去,测试失败。。。。再分析下,为什么会没有填充进去?关键在于JwtAccessTokenConverter这个类,该类未发起作用的时候,返回请求放的token只是一个uuid类型(好像是uuid)的简单字符串,经过该类的转换之后就将一个简单的uuid转换成了jwt字符串,该类中的org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter#convertAccessToken方法在起作用,顺着该方法找下去:org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter#convertAccessToken,然后就发现了这行代码

response.putAll(token.getAdditionalInformation());

这个token就是OAuth2AccessToken对象,也就是真正返回给请求者的对象,查看该类中该字段的解释

/*** The additionalInformation map is used by the token serializers to export any fields used by extensions of OAuth.* @return a map from the field name in the serialized token to the value to be exported. The default serializers * make use of Jackson's automatic JSON mapping for Java objects (for the Token Endpoint flows) or implicitly call * .toString() on the "value" object (for the implicit flow) as part of the serialization process.*/Map<String, Object> getAdditionalInformation();

可以看到,该字段是专门用来扩展OAuth字段的属性,万万没想到JWT同时用它扩展jwt串。。。接下来就该想想怎么给OAuth2AccessToken对象填充这个扩展字段了。

如果仔细看JwtAccessTokenConverter这个类的源码,可以看到有个方法org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter#enhance,该方法有个参数OAuth2AccessToken accessToken,同时它的返回值也是OAuth2AccessToken,也就是说这个方法,传入了OAuth2AccessToken对象,完事儿了之后还传出了OAuth2AccessToken对象,再根据enhance这个名字,可以推测出,它是一个增强方法,修改了或者代理了OAuth2AccessToken对象,查看父接口,是TokenEnhancer接口

public interface TokenEnhancer {/*** Provides an opportunity for customization of an access token (e.g. through its additional information map) during* the process of creating a new token for use by a client.* * @param accessToken the current access token with its expiration and refresh token* @param authentication the current authentication including client and user details* @return a new token enhanced with additional information*/OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication);}

根据该注释可以看出该方法用于定制access_token,那么通过这个方法填充access token的AdditionalInformation属性貌似正合适(别忘了目的是干啥的)。

看下JwtAccessTokenConverter是如何集成到认证服务的

    @Beanpublic AuthorizationServerTokenServices tokenServices(){DefaultTokenServices services = new DefaultTokenServices();services.setClientDetailsService(clientDetailsService);services.setSupportRefreshToken(true);services.setTokenStore(tokenStore);services.setAccessTokenValiditySeconds(7200);services.setRefreshTokenValiditySeconds(259200);TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();tokenEnhancerChain.setTokenEnhancers(Collections.singletonList(jwtAccessTokenConverter));services.setTokenEnhancer(tokenEnhancerChain);return services;}

可以看到这里的tokenEnhancerChain可以传递一个列表,这里只传了一个jwtAccessTokenConverter对象,那么解决方案就有了,实现TokenEnhancer接口并将对象填到该列表中就可以了

3.实现TokenEnhancer接口

@Slf4j@Componentpublic class CustomTokenEnhancer implements TokenEnhancer {@Autowiredprivate ObjectMapper objectMapper;@Overridepublic OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {Map<String,Object> additionalInfo = new HashMap<>();Object principal = authentication.getPrincipal();try {String s = objectMapper.writeValueAsString(principal);Map map = objectMapper.readValue(s, Map.class);map.remove("password");map.remove("authorities");map.remove("accountNonExpired");map.remove("accountNonLocked");map.remove("credentialsNonExpired");map.remove("enabled");additionalInfo.put("user_info",map);((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(additionalInfo);} catch (IOException e) {log.error("",e);}return accessToken;}}

以上代码干了以下几件事儿:

  • 从OAuth2Authentication对象取出principal对象
  • 转换principal对象为map并删除map对象中的若干个不想要的字段属性
  • 将map对象填充进入OAuth2AccessToken对象的additionalInfo属性

实现TokenEnhancer接口后将该对象加入到TokenEnhancerChain中

TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();tokenEnhancerChain.setTokenEnhancers(Arrays.asList(customTokenEnhancer,jwtAccessTokenConverter));

4.接口测试

POST请求http://127.0.0.1:30000/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=zhangsan&password=123得到结果

{"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX2luZm8iOnsidXNlcm5hbWUiOiJ6aGFuZ3NhbiIsImlkIjoxLCJlbWFpbCI6IjEyMzQ1NkBmb3htYWlsLmNvbSIsIm1vYmlsZSI6IjEyMzQ1Njc4OTEyIiwiZnVsbG5hbWUiOiLlvKDkuIkifSwidXNlcl9uYW1lIjoiemhhbmdzYW4iLCJzY29wZSI6WyJST0xFX0FETUlOIiwiUk9MRV9VU0VSIiwiUk9MRV9BUEkiXSwiZXhwIjoxNjEwNjM4NjQzLCJhdXRob3JpdGllcyI6WyJwMSIsInAyIl0sImp0aSI6IjFkOGY3OGFmLTg1N2EtNGUzMS05ODYxLTZkYWJjNjU4NzcyNiIsImNsaWVudF9pZCI6ImMxIn0.Y9f5psNCgZi_I2KY3PLBLjuK5-U1VhXIB1vjKjMb9fc","token_type": "bearer","refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX2luZm8iOnsidXNlcm5hbWUiOiJ6aGFuZ3NhbiIsImlkIjoxLCJlbWFpbCI6IjEyMzQ1NkBmb3htYWlsLmNvbSIsIm1vYmlsZSI6IjEyMzQ1Njc4OTEyIiwiZnVsbG5hbWUiOiLlvKDkuIkifSwidXNlcl9uYW1lIjoiemhhbmdzYW4iLCJzY29wZSI6WyJST0xFX0FETUlOIiwiUk9MRV9VU0VSIiwiUk9MRV9BUEkiXSwiYXRpIjoiMWQ4Zjc4YWYtODU3YS00ZTMxLTk4NjEtNmRhYmM2NTg3NzI2IiwiZXhwIjoxNjEwODkwNjQzLCJhdXRob3JpdGllcyI6WyJwMSIsInAyIl0sImp0aSI6IjM1OGFkMzA1LTU5NzUtNGM3MS05ODI4LWQ2N2ZjN2MwNDMyMCIsImNsaWVudF9pZCI6ImMxIn0._bhajMIdqnUL1zgc8d-5xlXSzhsCWbZ2jBWlNb8m_hw","expires_in": 7199,"scope": "ROLE_ADMIN ROLE_USER ROLE_API","user_info": {"username": "zhangsan","id": 1,"email": "123456@foxmail.com","mobile": "12345678912","fullname": "张三"},"jti": "1d8f78af-857a-4e31-9861-6dabc6587726"}

可以看到结果中多了user_info字段,而且access_token长了很多,我们的目的是为了在jwt也就是access_token中放入用户信息,先不管为何user_info会以明文出现在这里,我们先看下access_token中多了哪些内容

POST请求hhttp://127.0.0.1:30000/oauth/check_token?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX2luZm8iOnsidXNlcm5hbWUiOiJ6aGFuZ3NhbiIsImlkIjoxLCJlbWFpbCI6IjEyMzQ1NkBmb3htYWlsLmNvbSIsIm1vYmlsZSI6IjEyMzQ1Njc4OTEyIiwiZnVsbG5hbWUiOiLlvKDkuIkifSwidXNlcl9uYW1lIjoiemhhbmdzYW4iLCJzY29wZSI6WyJST0xFX0FETUlOIiwiUk9MRV9VU0VSIiwiUk9MRV9BUEkiXSwiZXhwIjoxNjEwNjM4NjQzLCJhdXRob3JpdGllcyI6WyJwMSIsInAyIl0sImp0aSI6IjFkOGY3OGFmLTg1N2EtNGUzMS05ODYxLTZkYWJjNjU4NzcyNiIsImNsaWVudF9pZCI6ImMxIn0.Y9f5psNCgZi_I2KY3PLBLjuK5-U1VhXIB1vjKjMb9fc,得到相应结果

{"aud": ["res1"],"user_info": {"username": "zhangsan","id": 1,"email": "123456@foxmail.com","mobile": "12345678912","fullname": "张三"},"user_name": "zhangsan","scope": ["ROLE_ADMIN","ROLE_USER","ROLE_API"],"exp": 1610638643,"authorities": ["p1","p2"],"jti": "1d8f78af-857a-4e31-9861-6dabc6587726","client_id": "c1"}

可以看到user_info也已经填充到了jwt串中,那么为什么这个串还会以明文的形式出现在相应结果的其它字段中呢?还记得本文章中说过的一句话"可以看到,该字段是专门用来扩展OAuth字段的属性,万万没想到JWT同时用它扩展jwt串",我们给OAuth2AccessToken对象填充了AdditionalInformation字段,而这本来是为了扩展OAuth用的,所以返回结果中自然会出现这个字段。

到此为止,接口测试已经成功了,接下来修改网关和目标服务(这里是资源服务),将用户信息提取出来并保存到上下文中

二、修改网关

网关其实不需要做啥大的修改,但是会出现中文乱码问题,这里使用Base64编码之后再将用户数据放到请求头带给目标服务。修改TokenFilter类

//builder.header("token-info", payLoad).build();builder.header("token-info", Base64.encode(payLoad.getBytes(StandardCharsets.UTF_8))).build();

三、修改资源服务

1.修改AuthFilterCustom

上一篇文章中床架了该类并将userName填充到了UsernamePasswordAuthenticationToken对象的Principal,这里我们需要将扩展的UserInfo整个填充到Principal,完整代码如下

public class AuthFilterCustom extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {ObjectMapper objectMapper = new ObjectMapper();String base64Token = request.getHeader("token-info");if(StringUtils.isEmpty(base64Token)){log.info("未找到token信息");filterChain.doFilter(request,response);return;}byte[] decode = Base64.decode(base64Token);String tokenInfo = new String(decode, StandardCharsets.UTF_8);JwtTokenInfo jwtTokenInfo = objectMapper.readValue(tokenInfo, JwtTokenInfo.class);List<String> authorities1 = jwtTokenInfo.getAuthorities();String[] authorities=new String[authorities1.size()];authorities1.toArray(authorities);//将用户信息和权限填充 到用户身份token对象中UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(jwtTokenInfo.getUser_info(),null,AuthorityUtils.createAuthorityList(authorities));authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));//将authenticationToken填充到安全上下文SecurityContextHolder.getContext().setAuthentication(authenticationToken);filterChain.doFilter(request,response);}}

这里JwtTokenInfo新增了user_info字段,而其类型正是前面说的UserDetailsExpand类型。

通过上述修改,我们可以在Controller中使用如下代码获取到上下文中的信息

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();UserDetailsExpand principal = (UserDetailsExpand)authentication.getPrincipal();

经过测试,结果良好,但是还存在问题,那就是在异步情况下,比如使用线程池或者新开线程的情况下,极有可能出现线程池内缓存或者取不到数据的情况(未测试,瞎猜的),具体可以参考我以前的文章使用 transmittable-thread-local 组件解决 ThreadLocal 父子线程数据传递问题

2.解决线程安全性问题

这一步是选做,但是还是建议做,如果不考虑线程安全性问题,上一步就可以了。

首先新增AuthContextHolder类维护我们需要的ThreadLocal,这里一定要使用TransmittableThreadLocal。

public class AuthContextHolder {private TransmittableThreadLocal threadLocal = new TransmittableThreadLocal();private static final AuthContextHolder instance = new AuthContextHolder();private AuthContextHolder() {}public static AuthContextHolder getInstance() {return instance;}public void setContext(UserDetailsExpand t) {this.threadLocal.set(t);}public UserDetailsExpand getContext() {return (UserDetailsExpand)this.threadLocal.get();}public void clear() {this.threadLocal.remove();}}

然后新建拦截器AuthContextIntercepter

@Componentpublic class AuthContextIntercepter implements HandlerInterceptor {@Autowiredprivate ObjectMapper objectMapper;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if(Objects.isNull(authentication) || Objects.isNull(authentication.getPrincipal())){//无上下文信息,直接放行return true;}UserDetailsExpand principal = (UserDetailsExpand) authentication.getPrincipal();AuthContextHolder.getInstance().setContext(principal);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {AuthContextHolder.getInstance().clear();}}

该拦截器在AuthFilter之后执行的,所以一定能获取到SecurityContextHolder中的内容,之后,我们就可以在Controller中使用如下代码获取用户信息了

UserDetailsExpand context = AuthContextHolder.getInstance().getContext();

是不是简单了很多~

3.其他问题

如果走到了上一步,则一定要使用阿里巴巴配套的TransmittableThreadLocal解决方案,否则TransmittableThreadLocal和普通的ThreadLocal没什么区别。具体参考使用 transmittable-thread-local 组件解决 ThreadLocal 父子线程数据传递问题

四、源代码

源码地址:https://gitee.com/kdyzm/spring-security-oauth-study/tree/v6.0.0

Spring Security OAuth2.0认证授权五:用户信息扩展到jwt相关推荐

  1. Spring Security OAuth2.0认证授权知识概括

    Spring Security OAuth2.0认证授权知识概括 安全框架基本概念 基于Session的认证方式 Spring Security简介 SpringSecurity详解 分布式系统认证方 ...

  2. Spring Security OAuth2.0认证授权三:使用JWT令牌

    历史文章 [Spring Security OAuth2.0认证授权一:框架搭建和认证测试] [Spring Security OAuth2.0认证授权二:搭建资源服务] 前面两篇文章详细讲解了如何基 ...

  3. Spring Security OAuth2.0认证授权

    文章目录 1.基本概念 1.1.什么是认证 1.2 什么是会话 1.3什么是授权 1.4授权的数据模型 1.4 RBAC 1.4.1 基于角色的访问控制 2.基于Session的认证方式 3.整合案例 ...

  4. 基于Session的认证方式_实现授权功能_Spring Security OAuth2.0认证授权---springcloud工作笔记118

    技术交流QQ群[JAVA,C++,Python,.NET,BigData,AI]:170933152 我们来实现基本的,session的授权功能,很简单实际上就是利用了springmvc的拦截器.不多 ...

  5. 基于Spring Security的认证方式_创建工程_Spring Security OAuth2.0认证授权---springcloud工作笔记119

    技术交流QQ群[JAVA,C++,Python,.NET,BigData,AI]:170933152 首先加入依赖,除了以前的springmvc的依赖,还要security的依赖,这一次 然后这里配置 ...

  6. Spring Security OAuth2.0_实现分布式认证授权_集成测试_Spring Security OAuth2.0认证授权---springcloud工作笔记155

    技术交流QQ群[JAVA,C++,Python,.NET,BigData,AI]:170933152 然后前面我们已经把分布式微服务的,认证授权全部集成了,然后我们来测试. 启动资源微服务order微 ...

  7. Spring Security OAuth2.0_实现分布式认证授权_转发明文token给微服务_Spring Security OAuth2.0认证授权---springcloud工作笔记153

    技术交流QQ群[JAVA,C++,Python,.NET,BigData,AI]:170933152 然后关键来了,我们通过网关微服务来转发明文数据给微服务. 我们通过zuul配置前置过滤器,在前置过 ...

  8. Spring Security OAuth2.0_实现分布式认证授权_微服务解析令牌并鉴权_Spring Security OAuth2.0认证授权---springcloud工作笔记154

    技术交流QQ群[JAVA,C++,Python,.NET,BigData,AI]:170933152 然后我们接着去看,我们需要其他的微服务就解析令牌,并且进行根据权限判断能不能来访问我们的某个方法 ...

  9. OAuth2.0_环境介绍_授权服务和资源服务_Spring Security OAuth2.0认证授权---springcloud工作笔记138

    技术交流QQ群[JAVA,C++,Python,.NET,BigData,AI]:170933152 环境介绍 1.可以看到客户端首先访问/oauth/token 这个路径来请求令牌,这个接口是spr ...

最新文章

  1. ARM Linux 3.x的设备树(Device Tree)【转】
  2. pandas使用replace函数替换dataframe中的值:replace函数对dataframe中指定数据列的值进行替换、替换具体数据列的相关值
  3. C语言指出下列程序的错误,2012年计算机二级C语言精编教程第二章(8)
  4. 将台式机组成云服务器_云桌面是什么?用了两年云桌面的真实感受
  5. 茶叶的游离态咖啡因与结合态咖啡因
  6. python中pandas安装视频教程_详解Python中pandas的安装操作说明(傻瓜版)
  7. 虚拟机连接网络_Parallels Desktop 16教程PD16虚拟机共享网络和桥接网络设置方法
  8. junit 测试执行顺序_JUnit 5中的测试执行顺序
  9. 《明日方舟》Python版公开招募工具
  10. 如何优雅地测量一只猫的体积?
  11. python之字符串操作
  12. .NETFramework、C#、VisualStudio 这三者之间关系,你了解吗!
  13. 丽江,是否一群失意的人聚合地
  14. view绘制流程学习心得
  15. 计算机内存条能装几个,4G内存条和2G内存条能不能装到一个电脑上?
  16. 计算机播放音乐无声音,笔记本电脑放歌没声音的解决方法
  17. 访问网站php直接下载,访问php文件自动下载及502问题-Go语言中文社区
  18. c语言程序的上标怎么打出来,c上标2下标5怎么算
  19. 利用Pyecharts绘制仪表盘图的案例【含参数解释】
  20. 机器学习基础---pr曲线的绘制

热门文章

  1. C++11新特性总结
  2. HKEY_LOCAL_MACHINE\Software\WOW6432Node
  3. VirtualBox打开虚拟电脑提示Call to NEMR0InitVMPart2 failed: VERR_NEM_INIT_FAILED (VERR_NEM_VM_CREATE_FAILED).
  4. c语言知识点总结(摘自head first c)
  5. 电脑上复制粘贴同名文件被覆盖了怎么办
  6. xampp的下载安装及使用
  7. 深圳软件测试培训:软件测试的需求评审
  8. 【数字图像处理】毛笔字细化
  9. 读《一个聪明的投资者 本杰明 - 格雷厄姆》
  10. jar包(依赖jar 运行jar)