Jwt token的几种设计方案及其实现

Jwt token设计方案及其实现

最近公司项目升级到spring security,使用到了Jwt token,遇到一些麻烦,特来记录一下

什么是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

包含声明,包含预定义的推荐标准声明和自定义声明。

图片1

图片来源: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主动失效的缺陷

  1. 把token失效时间设短一点。

    个人感觉这个方案还是很危险,假如用户A在早上八点登录并获得token,失效时间是十分钟,那么在八点过五分的时候降低了用户A的权限,他还是有五分钟能使用原来的权限,(当然也可以不把权限存储到token里面)。

  2. 维护token白名单

    数据库或redis保存一个token的白名单,即把有效的token存储到白名单,每一次的请求收到的token和白名单做匹配,如果不在白名单里面,认定为token失效。

    但是这种方案和原来的session/cookie没什么区别,还是要把token存一份到后端。

  3. 维护token黑名单

    数据库或redis维护一个token的黑名单,如果想让一个token失效,就把他加入到黑名单中。每一次请求收到的token和黑名单做匹配,如果在黑名单里面,认定token失效。直到废除的token过期,再从黑名单移除。

    这种方案个人感觉和原来的session/cookie依旧没什么区别,甚至更麻烦,因为一个用户可能有多个失效token未过期。

  4. 维护一个tokenId名单

    在生成token的时候讲token的版本设定进去,然后后端存一份这个用户的版本id,只要想让这个用户的原有token失效,就把版本id更新。

    这样和第二个方案没什么区别,因为每个token还是要保存一个东西到数据库

综上所述:无论如何,只要想实现主动失效token,都必须向数据库保存一个东西。这就是所谓的理想很丰满,现实很骨感了。

## 相关代码

再此采用第四种方案,使用雪花算法生成的数字作为token版本的id,保证唯一性,也方便token版本号存储到数据库时索引性能。

以user的id为键,将版本号存到redis,后期再做同步到数据库,避免redis挂了

导入依赖

在这里使用jwt的官方实现,需要导入依赖:

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.2</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId> 
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>

由于还使用了hutool的雪花算法,还需要导入hutool

  <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.4.6</version>
 </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
 */
@Component
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时运行,并只会执行一次
     * 在这里是初始化雪花算法所需要的东西
     * */
    @PostConstruct
    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
 */
@Data
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<GrandAuthories>时会丢失数据,所以自己手动实现了一个string到list的转化

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值