Jwt token设计方案及其实现
最近公司项目升级到spring security,使用到了Jwt token,遇到一些麻烦,特来记录一下
Jwt token设计方案及其实现什么是Jwtjwt内容headerpayloadsignatureJwt token的优点Jwt token的缺点解决无法使Jwt token主动失效的缺陷相关代码导入依赖准备雪花算法工具类准备要写入的DTO类Jwt Token在Java的实现工具类
什么是Jwt
Jwt是一种无状态的token方案。包括Header,payload和Signature三个部分。
原来的token方案是cookie/session方案,即后端生成了token以后存一份到,然后发到前端,前端也存一份到浏览器。但是无论怎么存,反正服务器都要存一个东西,非常之麻烦且负担重。
Jwt解决了这个问题。jwt是服务端使用特定密钥将用户信息加密为token发给前端,前端在每次请求带上这个token。后端接受到token后用密钥解密,如果解不出来,那么证明这个信息被篡改过,就不通过验证。
参照:https://blog.csdn.net/u011277123/article/details/78918390
jwt内容
header
header有两部分组成:token的类型和算法的名称。
{
“alg”:”HS256”,
“typ”:”JWT” }
使用base64对他编码就形成了header
payload
包含声明,包含预定义的推荐标准声明和自定义声明。
图片来源:https://blog.csdn.net/u011277123/article/details/78918390
不过由于这个地方也是base64,所以可认定为是明文。使用时可自行加密。
signature
由前两个部分在base64加密后通过header声明的加密方式通过服务器定义的一个secret加盐组合加密,生成第三个部分,如果前两个部分被篡改,那么解密时必然和第三个部分不匹配,就知道是被篡改过的了。
Jwt token的优点
不需要在后端存token,减轻存储压力,只需要每次用密钥把用户信息之类的解出来就好,甚至还省去了查数据库的时间
Jwt token的缺点
同样是因为不需要在后端存token,所以后端对token的控制在只限于签发和设定过期时间,不能主动让token失效,除非换了加密密钥,但那样又会让所有的token都失效。
所以如果一个人本来是高级管理员,但是系统操作把他降成了普通管理员,但是由于原有token无法主动失效,她(他)在失效前还是能以高级管理员的身份通过校验,非常危险
解决无法使Jwt token主动失效的缺陷
- 把token失效时间设短一点。 个人感觉这个方案还是很危险,假如用户A在早上八点登录并获得token,失效时间是十分钟,那么在八点过五分的时候降低了用户A的权限,他还是有五分钟能使用原来的权限,(当然也可以不把权限存储到token里面)。
- 维护token白名单 数据库或redis保存一个token的白名单,即把有效的token存储到白名单,每一次的请求收到的token和白名单做匹配,如果不在白名单里面,认定为token失效。 但是这种方案和原来的session/cookie没什么区别,还是要把token存一份到后端。
- 维护token黑名单 数据库或redis维护一个token的黑名单,如果想让一个token失效,就把他加入到黑名单中。每一次请求收到的token和黑名单做匹配,如果在黑名单里面,认定token失效。直到废除的token过期,再从黑名单移除。 这种方案个人感觉和原来的session/cookie依旧没什么区别,甚至更麻烦,因为一个用户可能有多个失效token未过期。
- 维护一个tokenId名单 在生成token的时候讲token的版本设定进去,然后后端存一份这个用户的版本id,只要想让这个用户的原有token失效,就把版本id更新。 这样和第二个方案没什么区别,因为每个token还是要保存一个东西到数据库
综上所述:无论如何,只要想实现主动失效token,都必须向数据库保存一个东西。这就是所谓的理想很丰满,现实很骨感了。
相关代码
再此采用第四种方案,使用雪花算法生成的数字作为token版本的id,保证唯一性,也方便token版本号存储到数据库时索引性能。
以user的id为键,将版本号存到redis,后期再做同步到数据库,避免redis挂了
导入依赖
在这里使用jwt的官方实现,需要导入依赖:
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-apiartifactId>
<version>0.11.2version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-implartifactId>
<version>0.11.2version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-jacksonartifactId>
<version>0.11.2version>
<scope>runtimescope>
dependency>
由于还使用了hutool的雪花算法,还需要导入hutool
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.4.6version>
dependency>
在使用时版本有升级,请自行对应版本号
准备雪花算法工具类
雪花算法类
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.IdUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
* @author welt
* @date 2020-10-27
*/
public class SnowFlakeUtil {
private static final Logger logger = LoggerFactory.getLogger(SnowFlakeUtil.class);
private long workerId = 0;
private final long datacenterId = 1;
private Snowflake snowflake = IdUtil.createSnowflake(workerId, datacenterId);
/**
* 被@PostConstruct注解的方法会在服务器加载servlet时运行,并只会执行一次
* 在这里是初始化雪花算法所需要的东西
* */
public void init() {
try {
workerId = NetUtil.ipv4ToLong(NetUtil.getLocalhostStr());
logger.info("当前机器的workerId:{}", workerId);
} catch (Exception e) {
logger.info("当前机器的workerId获取失败", e);
workerId = NetUtil.getLocalhostStr().hashCode();
logger.info("当前机器 workId:{}", workerId);
}
}
public synchronized long getSnowflakeId() {
return snowflake.nextId();
}
public synchronized long getSnowflakeId(long workerId, long datacenterId) {
snowflake = IdUtil.createSnowflake(workerId, datacenterId);
return snowflake.nextId();
}
}
准备要写入的DTO类
authorities是spring security的权限列表,如不需要可删除
UserTokenDTO类
/**
* @author chenbl
* @date 2020-10-30
*/
public class UserTokenDTO {
private Integer id;
private String userName;
private List<GrantedAuthority> authorities;
private Integer company;
/**验证时的返回信息*/
private String message;
}
Jwt Token在Java的实现工具类
JwtTokenUtil
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author welt
* @date 2020/09/11
*/
@Slf4j
@Component
public class JwtTokenUtil {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Autowired
private SnowFlakeUtil snowFlakeUtil;
@Value("${jwt.secret}")
private String secret;
/**
* 系统禁止重复登录,每一次新的登录都必然会刷新token,使原来的token失效,token30天后失效
* 使用每次更新token版本,固定密钥
* @param user
* 接收jwt的一方
* @param subject
* jwt所面向的一方
* */
public String generateToken(String subject, UserTokenDTO user){
//TODO 设定密钥和算法
SecretKey key =new SecretKeySpec(secret.getBytes(), SignatureAlgorithm.HS256.getJcaName());
LocalDateTime expiration = LocalDateTime.now(ZoneId.of("Asia/Shanghai"));
//过期时间三天
expiration = expiration.plusDays(3);
Date setExpiration = Date.from(expiration.atZone(ZoneId.of("Asia/Shanghai")).toInstant());
long newTokenId = snowFlakeUtil.getSnowflakeId();
//拿到这个人的tokenId
List<String> authorities = user.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
String token = Jwts.builder()
.setSubject(subject)
.setAudience(String.valueOf(user.getUserName()))
.setExpiration(setExpiration)
.setId(String.valueOf(newTokenId))
.claim(UserConstant.USER_ID,user.getId())
.claim(UserConstant.PRIVILEGE_RANK, String.join(",",authorities))
.claim(UserConstant.COMPANY,user.getCompany())
.setIssuer("hr-system")
.signWith(key)
.setIssuedAt(new Date(System.currentTimeMillis()))
.compact();
stringRedisTemplate.opsForValue().set(RedisConstant.JWT_TOKEN_ID+user.getId(), String.valueOf(newTokenId));
return token;
}
/**
* @param token
* 接收到的token
* @return
* 返回解析到的payload信息,不同的失败会在message中注明不同的码,未注明的错误将直接返回500到前端,并终止执行代码
* */
public UserTokenDTO identify(String token){
try {
SecretKey key = new SecretKeySpec(secret.getBytes(),
SignatureAlgorithm.HS256.getJcaName());
//验证token是否被篡改,如果被篡改,会报错JwtException
Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
Claims body = claimsJws.getBody();
UserTokenDTO user = new UserTokenDTO();
user.setUserName(body.getAudience());
user.setId((Integer) body.get(UserConstant.USER_ID));
String role = (String) body.get(UserConstant.PRIVILEGE_RANK);
user.setAuthorities(Arrays.stream(role.split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList()));
//验证token是否过期
String tokenId = body.getId();
String realTokenId = stringRedisTemplate.opsForValue().get(RedisConstant.JWT_TOKEN_ID+user.getId());
if (realTokenId == null){
throw new JwtException(ResponseCode.TOKEN_FAILURE);
}
if (!realTokenId.equals(tokenId)){
user.setMessage(ResponseCode.TOKEN_EXPIRED);
throw new JwtException(ResponseCode.TOKEN_EXPIRED);
}
user.setMessage("success");
return user;
} catch (JwtException jwtException){
log.error("验证失败,token:{}",token);
UserTokenDTO userTokenDTO = new UserTokenDTO();
if (jwtException.getMessage()!=null){
userTokenDTO.setMessage(jwtException.getMessage());
}else {
userTokenDTO.setMessage(ResponseCode.TOKEN_FAILURE);
}
return userTokenDTO;
}catch (Exception e){
e.printStackTrace();
UserTokenDTO userTokenDTO = new UserTokenDTO();
userTokenDTO.setMessage(String.valueOf(ResponseCode.SERVER_ERROR));
return userTokenDTO;
}
}
/**
* 删除用户token
* @param userId
* 用户名
* */
public void removeToken(String userId){
stringRedisTemplate.delete(RedisConstant.JWT_TOKEN_ID+userId);
}
public boolean isTokenExpired(String token){
SecretKey key = new SecretKeySpec(secret.getBytes(),SignatureAlgorithm.HS256.getJcaName());
//验证token是否被篡改,如果被篡改,会报错JwtException
Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
Claims body = claimsJws.getBody();
Date expiredDate =body.getExpiration();
return expiredDate.before(new Date());
}
}
**注:**fastjson在将jsonString转化为List
注意:本文归作者所有,未经作者允许,不得转载