JWT整合springboot 自定义定时更换秘钥
jwt概要:
JWT(JSON WEB TOKEN):JSON网络令牌,JWT是一个轻便的安全跨平台传输格式,定义了一个紧凑的自包含的方式在不同实体之间安全传输信息(JSON格式)。它是在Web环境下两个实体之间传输数据的一项标准。实际上传输的就是一个字符串。广义上讲JWT是一个标准的名称;狭义上JWT指的就是用来传递的那个token字符串。
jwt是一种无状态token,可用于oss单点登录
JWT的数据结构以及签发的过程
JWT由三部分构成:header(头部)、payload(载荷)和signature(签名)。
Header 头部信息:指定类型和算法
Payload 荷载信息:存放Claims声明信息
Signature签名:把前两者对应的Json结构进行base64url编码之后的字符串拼接起来和密钥放一起加密后的签名 组成方式为header.payload.signature
Header的结构
- type 声明类型
- algorithm 声明加密算法
格式: {“typ”: “JWT”,“alg”: “HS256”}
payload的结构
- 声明信息
payload用来承载要传递的数据,它的json结构实际上是对JWT要传递的数据的声明,这些声明被JWT标准称为claims(声明),它的每个属性键值对其实就是一个claim,JWT常用的有两种声明,一种是Reserved claims(保留声明),也就是JWT规定的标准声明。
一种是Private claims(自定义声明),我们在这里定义要传递的信息
还有一种是public claims(公共声明),这个目前没用到。
标准声明(JWT保留声明)
iss(Issuser):代表这个JWT的签发主体;
sub(Subject):代表这个JWT的主体,即它的所有人;
aud(Audience):代表这个JWT的接收对象;
exp(Expiration time):是一个时间戳,代表这个JWT的过期时间;
nbf(Not Before):是一个时间戳,代表这个JWT生效的开始时间,意味着在这个时间之前验证JWT是会失败的;
iat(Issuedat):是一个时间戳,代表这个JWT的签发时间; jti(JWT ID):是JWT的唯一标识。
signature
- 签名
把header和payload对应的json结构进行base64url编码之后得到的字符串用点号拼接起来,然后根据header里面alg指定的签名算法生成出来的,然后添加自己设定的key进行加密签名
java代码实现:
maven依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
定义一个用户信息类,存放一些用户基本信息(如用户名、权限(可进行菜单鉴权))
/**
* @author : ljt
* @version V1.0
* @Description: 用户信息
* @date Date : 2021年08月06日 13:56
*/
@Data
public class UserLoginBO {
/**
* 用户id
*/
private Long id;
/**
* 用户名
*/
private String userName;
/**
* 用户昵称
*/
private String nickName;
/**
* 用户真实姓名
*/
private String realName;
/**
* 用户openid
*/
private String openId;
/**
* 用户token
*/
private String accessToken;
}
JWT工具类:
/**
* @author : ljt
* @version V1.0
* @Description: jwt工具类
* @date Date : 2021年08月06日 13:22
*/
@Component
public class JwtTokenUtil {
/**
* jwt生成token秘钥,此处动态更新所以为空,可随便自定义
*/
public static String TOKEN_SECRET = "";
/**
* 定义token有效期 秒
*/
public static Integer tokenExpiration=1800;
/**
* 生成token
* @param subject 用户名
* @param claims 用户信息
* @return token字符串
*/
public static String generateToken(String subject, Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setId(UUID.randomUUID().toString())
.setIssuedAt(new Date())
.setExpiration(generateExpirationDate(tokenExpiration))
.compressWith(CompressionCodecs.DEFLATE)
.signWith(SignatureAlgorithm.HS256, TOKEN_SECRET)
.compact();
}
/***
* 解析token 信息
* @param token token字符串
* @return 用户map信息
*/
private static Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(TOKEN_SECRET)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
/**
* 生成失效时间
* @param expiration 失效时长
* @return 到期时间 时间格式
*/
private static Date generateExpirationDate(long expiration) {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
/**
* 根据token 获取用户信息
* @param token token
* @return 自定义的用户对象
*/
public static UserLoginBO getUserFromToken(String token) {
UserLoginBO userDetail;
try {
final Claims claims = getClaimsFromToken(token);
userDetail = new UserLoginBO();
userDetail.setId(Long.parseLong(claims.get("id").toString()));
userDetail.setUserName(String.valueOf(claims.get("userName")));
userDetail.setNickName(String.valueOf(claims.get("nickName")));
userDetail.setRealName(String.valueOf(claims.get("UserName")));
userDetail.setOpenId(String.valueOf(claims.get("openId")));
} catch (Exception e) {
userDetail = null;
}
return userDetail;
}
/**
* 根据token 获取用户ID
* 和获取用户信息一致 此处新定义一个方法是用着方便
* @param token token
* @return 返回用户id
*/
private Long getUserIdFromToken(String token) {
Long userId;
try {
final Claims claims = getClaimsFromToken(token);
userId = Long.parseLong(claims.get("id").toString());
} catch (Exception e) {
userId = 0L;
}
return userId;
}
/**
* 根据token 获取用户名
* 和获取用户信息一致 此处新定义一个方法是用着方便
* @param token token字符串
* @return 用户昵称
*/
public static String getUsernameFromToken(String token) {
String username;
try {
final Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 刷新token
*
* @param token 原token
* @return 新token
*/
public static String refreshToken(String token,Integer tokenExpiration) {
String refreshedToken;
try {
final Claims claims = getClaimsFromToken(token);
refreshedToken = generateToken(claims.getSubject(), claims,tokenExpiration);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
/**
* 根据token 获取生成时间
* @param token token字符串
* @return 时间
*/
public Date getCreatedDateFromToken(String token) {
Date created;
try {
final Claims claims = getClaimsFromToken(token);
created = claims.getIssuedAt();
} catch (Exception e) {
created = null;
}
return created;
}
/**
* 根据token 获取过期时间
* @param token token
* @return 时间
*/
public static Date getExpirationDateFromToken(String token) {
Date expiration;
try {
final Claims claims = getClaimsFromToken(token);
expiration = claims.getExpiration();
} catch (Exception e) {
expiration = null;
}
return expiration;
}
/**
* 验证token 是否合法
* @param token token
* @param userDetail 用户信息
* @return true 或 false
*/
public boolean validateToken(String token, UserLoginBO userDetail) {
final long userId = getUserIdFromToken(token);
final String username = getUsernameFromToken(token);
return (userId == userDetail.getId()
&& username.equals(userDetail.getUserName())
&& !isTokenExpired(token)
);
}
/**
* 判断令牌是否过期
* @param token 令牌
* @return 是否过期
*/
public static Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
}
配置拦截器:
@Slf4j
public class LoginInterceptor extends HandlerInterceptorAdapter {
/**
* token剩余过期时间
* 此处设定剩余到期时间自动刷新token
* 根据需要手动编辑刷新token接口
*/
private final Long TOKEN_DATE = 5*60*1000L;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//token约定放置在请求头中以access_token的方式发送
String token = request.getHeader("access_token");
log.debug("用户请求鉴权token = {}", token);
//判断token是否存在 不存在无权限 直接返回
if(StringUtils.isNotEmpty(token)) {
//工具类 直接获取用户信息
UserLoginBO userFromToken = JwtTokenUtil.getUserFromToken(token);
//获取不到 用户授权过期 直接返回
if (userFromToken != null && !JwtTokenUtil.isTokenExpired(token)) {
//TODO 能正常获取用户信息 可判断用户信息是否一致 一致则鉴权成功 这里我懒省事
//判断token剩余过期时间 将过期自动签发新token 以response方式返回
if ( JwtTokenUtil.getExpirationDateFromToken(token).getTime() - System.currentTimeMillis() < TOKEN_DATE ){
String refreshToken = JwtTokenUtil.refreshToken(token,JwtTokenUtil.tokenExpiration);
Object value = CacheUtil.getInstance().getValue(token);
CacheUtil.getInstance().putValue(refreshToken,value, CacheConstant.PERMS_INFO);
response.setContentType("text/html; charset=UTF-8");
Map<String,String> map = new HashMap<>(16);
map.put("accessToken",refreshToken);
response.setHeader("access_token",refreshToken);
}
return true;
}
}
response.setContentType("text/html; charset=UTF-8");
ResponseBO responseBO = new ResponseBO();
responseBO.setCode(ResponseCode.L001.getCode());
responseBO.setMsg(ResponseCode.L001.getTitle());
response.getWriter().write(JSONObject.toJSONString(responseBO));
return false;
}
}
注册拦截器,定义拦截规则
@Configuration
public class AuthConfigurer extends WebMvcConfigurationSupport {
/**
* 拦截器注入
*/
@Bean
public LoginInterceptor loginInterceptor() {
return new LoginInterceptor();
}
/**
* 添加拦截
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry
//注册拦截器 定义拦截接口
.addInterceptor(loginInterceptor()).addPathPatterns("/**")
// 排除登录接口拦截
.excludePathPatterns("/user/login")
super.addInterceptors(registry);
}
/**
* 替换Spring默认JSON转换器为fastjson
* @param converters
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
super.configureMessageConverters(converters);
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
fastJsonConfig.setSerializerFeatures(SerializerFeature.DisableCircularReferenceDetect);
fastConverter.setFastJsonConfig(fastJsonConfig);
converters.add(fastConverter);
}
/**
* 发现如果继承了WebMvcConfigurationSupport,则在yml中配置的相关内容会失效。 需要重新指定静态资源
*
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//将所有/static/** 访问都映射到classpath:/static/ 目录下
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
registry.addResourceHandler("/templates/**").addResourceLocations("classpath:/templates/");
registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
}
/**
* 配置servlet处理
*/
@Override
public void configureDefaultServletHandling(
DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
生成JWT秘钥工具类:
/**
* @author : ljt
* @version V1.0
* @Description: 生成JWT秘钥
* @date Date : 2021年11月25日 15:34
*/
@Slf4j
public class CreateJWTKeyUtil {
//生成60位秘钥串
public static String createJwtKey(){
String str="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+<>?:,|./;'";
Random random=new Random();
StringBuilder sb=new StringBuilder();
for(int i=0;i<60;i++){
int number=random.nextInt(84);
sb.append(str.charAt(number));
}
log.info("key====" + sb.toString());
return sb.toString();
}
}
定时修改JWT秘钥:
/**
* @author : ljt
* @version V1.0
* @Description: 定时修改jwtkey
* @date Date : 2021年11月25日 15:43
*/
@Component
public class CreateJWTKey {
/**
* jwt秘钥存储,可选择mysql、redis
*/
@Autowired
private IJwtKeyService jwtKeyService;
/**
* 可自定义更换周期
* 注意:秘钥更换后,已签发token将无法解密,返回结果为token失效,需要重新签发
*/
@Scheduled(cron = "0 0 0 1/7 * ?")
public void createJtw(){
//获取新秘钥
String jwtKey = CreateJWTKeyUtil.createJwtKey();
//秘钥存储修改
jwtKeyService.updateJwtKey(jwtKey);
//秘钥静态变量修改 可减少实现层查询次数 直接使用静态数据
JwtTokenUtil.TOKEN_SECRET = jwtKey;
}
}
项目启动时获取秘钥:
@Component
public class ApplicationRunnerImplConfig implements ApplicationRunner {
@Autowired
private IJwtKeyService jwtKeyService;
/**
* 项目启动获取jwt秘钥
* @param args
* @throws Exception
*/
@Override
public void run(ApplicationArguments args) throws Exception {
//从存储库中获取秘钥
JwtTokenUtil.TOKEN_SECRET = jwtKeyService.findJwtKey();
}
}
使用示例:登录成功后返回token
Map<String, Object> map = new HashMap<>(16);
map.put("id",login.getId());
map.put("userName",login.getUserName());
map.put("nickName",login.getNickName());
map.put("realName",login.getRealName());
String token = JwtTokenUtil.generateToken("user", map,JwtTokenUtil.userTokenExpiration);
login.setAccessToken(token);
return login;
总结:
使用流程就是:
前端发起登录请求->后端签发token->前端每次请求都携带此token->后端经过拦截器校验->后端逻辑处理->返回前端