24

Spring Cloud Alibaba 微服务实战(二十一):JWT 增强

 3 years ago
source link: https://mp.weixin.qq.com/s/m17gNfTdG8s6JYjKSPKvRQ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Vzaye2.png!mobile

今天内容主要是解决一位粉丝提的问题:如何在jwt中添加用户的额外信息并在资源服务器中获取这些数据。

涉及的知识点有以下三个:

  • 如何在返回的jwt中添加自定义数据

  • 如何在jwt中添加用户的额外数据,比如用户id、手机号码

  • 如何在资源服务器中取出这些自定义数据

下面我们分别来看如何实现。

如何在返回的jwt中添加自定义数据

这个问题比较简单,只要按照如下两步即可:

  1. 编写自定义token增强器

package com.javadaily.auth.security;

import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

import java.util.HashMap;
import java.util.Map;

/**
* <p>
* <code>JwtTokenEnhancer</code>
* </p>
* Description:
* 自定义Token增强
* @author javadaily
* @date 2020/7/4 15:56
*/

public class CustomJwtTokenConverter extends JwtAccessTokenConverter{
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication authentication) {
Object principal = authentication.getUserAuthentication().getPrincipal();
final Map<String,Object> additionalInformation = new HashMap<>(4);
additionalInformation.put("author","java日知录");
additionalInformation.put("weixin","javadaily");
((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(additionalInformation);
return super.enhance(oAuth2AccessToken,authentication);
}
}

  1. 在认证服务器 AuthorizationServerConfig 中配置自定义token增强器
@Bean
public JwtAccessTokenConverter jwtTokenEnhancer(){
//自定义jwt 输出内容,若不需要就直接使用JwtAccessTokenConverter
JwtAccessTokenConverter converter = new CustomJwtTokenConverter();
// 设置对称签名
converter.setSigningKey("javadaily");
return converter;
}

通过上述两步配置,我们生成的jwt token中就可以带上 authorweixin 两个属性了,效果如下: bYruueJ.png!mobile

有的同学可能要问,为什么配置了这个增强器就会生成额外属性了呢?

这是因为我们会使用 DefaultTokenServices#createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) 方法时有如下一段代码:

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());

return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
}

如果系统配置了 accessTokenEnhancer 就会调用 accessTokenEnhancerenhance() 方法进行token增强。我们是继承了 JwtAccessTokenConverter ,所以会在jwt token的基础上增加额外的信息。

如何在jwt中添加用户的额外数据

要添加额外数据我们还是要从 CustomJwtTokenConverter 想办法,要添加用户的额外数据比如用户id和手机号码那就必须要在用户中包含这些信息。

原来我们自定义的 UserDetailServiceImpl 中返回的是默认的 UserDetails 。里面只包含用户名属性,即username,代码调试效果如下:

vYBfy22.png!mobile

所以我们这里需要自定一个 UserDetails ,包含用户的额外属性,然后在 UserDetailServiceImpl 中再返回我们这个自定义对象,最后在 enhance 方法中强转成自定义用户对象并添加额外属性。

实现顺序如下:

  1. 自定义UserDetails

import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;

/**
* <p>
* <code>CustomUser</code>
* </p>
* Description:
* 自定义用户信息
* @author jianzh5
* @date 2020/11/17 15:05
*/

public class SecurityUser extends User {
@Getter
private Integer id;

@Getter
private String mobile;

public SecurityUser(Integer id, String mobile,
String username, String password,
Collection<? extends GrantedAuthority> authorities)
{
super(username, password, authorities);
this.id = id;
this.mobile = mobile;
}
}
  1. 在UserDetailServiceImpl中返回自定义对象

private UserDetails buildUserDetails(SysUser sysUser) {
Set<String> authSet = new HashSet<>();
List<String> roles = sysUser.getRoles();
if(!CollectionUtils.isEmpty(roles)){
roles.forEach(item -> authSet.add(CloudConstant.ROLE_PREFIX + item));
authSet.addAll(sysUser.getPermissions());
}

List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(authSet.toArray(new String[0]));

return new SecurityUser(
sysUser.getId(),
sysUser.getMobile(),
sysUser.getUsername(),
sysUser.getPassword(),
authorityList
);
}
  1. 在ehance方法中获取当前用户并设置用户信息

public class CustomJwtTokenConverter extends JwtAccessTokenConverter{
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication authentication) {
SecurityUser securityUser = (SecurityUser) authentication.getUserAuthentication().getPrincipal();
final Map<String,Object> additionalInformation = new HashMap<>(4);
additionalInformation.put("userId", securityUser.getId());
additionalInformation.put("mobile", securityUser.getMobile());
...
((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(additionalInformation);
return super.enhance(oAuth2AccessToken,authentication);
}
}

如何在资源服务器中获取这些自定义信息

通过上面的配置我们可以往jwt的token中添加上用户的数据信息,但是在资源服务器中还是获取不到,通过

SecurityContextHolder.getContext().getAuthentication().getPrincipal() 获取到的用户信息还是只包含用户名。

NBj6Jba.png!mobile

这里还是得从token的转换器入手,默认情况下 JwtAccessTokenConverter 会调用 DefaultUserAuthenticationConverter 中的 extractAuthentication 方法从token中获取用户信息。

我们先看看具体实现逻辑:

public class DefaultUserAuthenticationConverter implements UserAuthenticationConverter {
...
public Authentication extractAuthentication(Map<String, ?> map) {
if (map.containsKey(USERNAME)) {
Object principal = map.get(USERNAME);
Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
if (userDetailsService != null) {
UserDetails user = userDetailsService.loadUserByUsername((String) map.get(USERNAME));
authorities = user.getAuthorities();
principal = user;
}
return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
}
return null;
}
...
}

在没有注入 UserDetailService 的情况下oauth2只会获取用户名 user_name 。如果注入了 UserDetailService 就可以返回所有用户信息。

所以这里我们对应的实现方式也有两种:

  1. 在资源服务器中也注入 UserDetailService ,这种方法不推荐,资源服务器与认证服务器分开的情况下强行耦合在一起,也需要加入用户认证的功能。
  2. DefaultUserAuthenticationConverter
    extractAuthentication
    AccessTokenConverter
    

这里我们采用第二种方法实现,实现顺序如下:

  1. 自定义token解析器,从jwt token中解析用户信息。

package com.javadaily.common.security.component;

import com.javadaily.common.security.user.SecurityUser;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter;
import org.springframework.util.StringUtils;

import java.util.Collection;
import java.util.Map;

public class CustomUserAuthenticationConverter extends DefaultUserAuthenticationConverter {

/**
* 重写抽取用户数据方法
* @author javadaily
* @date 2020/11/18 10:56
* @param map 用户认证信息
* @return Authentication
*/

@Override
public Authentication extractAuthentication(Map<String, ?> map) {
if (map.containsKey(USERNAME)) {
Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
String username = (String) map.get(USERNAME);
Integer id = (Integer) map.get("userId");
String mobile = (String) map.get("mobile");
SecurityUser user = new SecurityUser(id, mobile, username,"N/A", authorities);
return new UsernamePasswordAuthenticationToken(user, "N/A", authorities);
}
return null;
}

private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) {
Object authorities = map.get(AUTHORITIES);
if (authorities instanceof String) {
return AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities);
}
if (authorities instanceof Collection) {
return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils
.collectionToCommaDelimitedString((Collection<?>) authorities));
}
throw new IllegalArgumentException("Authorities must be either a String or a Collection");
}

}
  1. 编写自定义token转换器,注入自定义解压器

public class CustomAccessTokenConverter extends DefaultAccessTokenConverter{

public CustomAccessTokenConverter(){
super.setUserTokenConverter(new CustomUserAuthenticationConverter());
}
}
  1. 在资源服务器中配置类ResourceServerConfig中注入自定义token转换器

@Bean
public JwtAccessTokenConverter jwtTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey("javadaily");
jwtAccessTokenConverter.setAccessTokenConverter(new CustomAccessTokenConverter());

return jwtAccessTokenConverter;
}

通过上面三步配置我们再调用 SecurityContextHolder.getContext().getAuthentication().getPrincipal() 方法时就可以获取到用户的额外信息了。

YNj63qr.png!mobile

当然我们可以再来一个工具类,从上下文中直接获取用户信息:

@UtilityClass
public class SecurityUtils {
/**
* 获取Authentication
*/

public Authentication getAuthentication() {
return SecurityContextHolder.getContext().getAuthentication();
}

public SecurityUser getUser(){
Authentication authentication = getAuthentication();
if (authentication == null) {
return null;
}
return getUser(authentication);
}

/**
* 获取当前用户
* @param authentication 认证信息
* @return 当前用户
*/

private static SecurityUser getUser(Authentication authentication) {
Object principal = authentication.getPrincipal();
if(principal instanceof SecurityUser){
return (SecurityUser) principal;
}
return null;
}
}

如果本文对你有帮助,

别忘记来个三连:

点赞,转发,评论

咱们下期见!

收藏  等于白嫖 点赞  才是真情!

End

干货分享

这里为大家准备了一份小小的礼物,关注公众号,输入如下代码,即可获得百度网盘地址,无套路领取!

001:《程序员必读书籍》

002:《从无到有搭建中小型互联网公司后台服务架构与运维架构》

003:《互联网企业高并发解决方案》

004:《互联网架构教学视频》

006:《SpringBoot实现点餐系统》

007:《SpringSecurity实战视频》

008:《Hadoop实战教学视频》

009:《腾讯2019Techo开发者大会PPT》

010: 微信交流群

近期热文top

我就知道你“在看”

nUvUvei.gif!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK