对JWT的进一步理解

一般项目的登录可以用JWT进行登录认证,主要作用是

  • 认证授权(比如有没有资格请求某个接口)
  • 信息交换(可以存放一些业务信息)

JWT的主要构成有三部分

  • 头部 声明类型 和 使用签名的算法(如RSA256 非对称加密算法 HS256 对称加密)
{
  "typ": "JWT",
  "alg": "RSA256"
}
  • 负载 存放一些有效的信息,除官方定义的外,也可以只定义加入字段和内容(结合业务)存放一些不敏感的信息,不要存密码等。
    一些信息
    iss:该JWT的签发者
    sub : 该JWT所面向的用户
    aud : 接收该JWT的一方
    exp(expires) : 什么时候过期,这里是一个Unix时间戳
    iat(issued at): 在什么时候签发的
{
	"userId" : "1",
	"userName" : "wjj"
}
  • 签名
    包含三个部分 编码( base64)后的头部(header)和负载(payload)以及一个secret(私钥)
    签名过程如下
RSA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)

用的是头部定义的加密算法,secret是私钥,要在服务端保存好,泄露了就可以伪造token(token是在服务端生成的),还可以根据发来的token进行校验发送方。
JWT工作流程
工作流程(网上找的)
解密流程

解密的时候,只要客户端带着jwt来发起请求,服务端就直接使用secret进行解密,解签证解出第一部分和第二部分,然后比对第二部分的信息和客户端穿过来的信息是否一致。如果一致验证成功,否则验证失败。第二部分的信息还可以判断有没有过期。

结合官网
在这里插入图片描述

结合项目来看jwt认证流程

public class TokenUtil {

    //签发者
    private static final String ISSUE = "签发者";

    /**
     * 生成token
     * @param userId
     * @return
     * @throws Exception
     */
    public static String generateToken(Long userId) throws Exception {
        Algorithm algorithm = Algorithm.RSA256(RSAUtil.getPublicKey(), RSAUtil.getPrivateKey());
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        // 30秒过期
        calendar.add(Calendar.MINUTE, 30);

        return JWT.create().
                // 负载
                //用户信息 这里存放的userId
                withKeyId(String.valueOf(userId))
                //签发者
                .withIssuer(ISSUE)
                //过期时间
                .withExpiresAt(calendar.getTime())
                //签名 使用加密算法 用私钥签名
                .sign(algorithm);

    }

    /**
     * 解析token
     * @param token
     * @return
     */
    public static Long verifyToken(String token) {
        try {
            Algorithm algorithm = Algorithm.RSA256(RSAUtil.getPublicKey(), RSAUtil.getPrivateKey());
            JWTVerifier verifier = JWT.require(algorithm).build();
            DecodedJWT jwt = verifier.verify(token);
            // 获取解析token信息
            String userId = jwt.getKeyId();
            return Long.valueOf(userId);
        } catch (TokenExpiredException e) {
            throw new ConditionException("555", "token过期");
        }catch (Exception e){
            throw new ConditionException("非法用户token!");
        }

    }

    /**
     * 刷新token
     * @param userId
     * @return
     * @throws Exception
     */
    public static String generateRefreshToken(Long userId) throws Exception {
        Algorithm algorithm = Algorithm.RSA256(RSAUtil.getPublicKey(), RSAUtil.getPrivateKey());
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        calendar.add(Calendar.DAY_OF_MONTH, 30);

        return JWT.create().
                //用户信息 这里存放的userId
                withKeyId(String.valueOf(userId))
                //签发者
                .withIssuer(ISSUE)
                //过期时间
                .withExpiresAt(calendar.getTime())
                //使用加密算法
                .sign(algorithm);

    }


}

登录请求是不用认证token的,登录是用来生成token的
登录中和token相关的代码

   TokenUtil tokenUtil = new TokenUtil();
        //根据id生成token
        return tokenUtil.generateToken(dbUser.getId());

这里的根据id生成token,id是放在负载里面的(这个可以用于信息交换),这里用id生成token,根据不同的业务需求,可以根据不用的信息生成token。
为什么要在token中加入业务信息呢
把信息放在token里面,就不用每次取redis或者数据库中取了,消耗一些解析token的时间,可以减少一些IO操作。比如我们每次请求的时候需要获取userId的信息,我们可以不用从数据库中取,直接从token中获得,这是一种信息的获取。
代码如下

@Component
public class UserSupport {
    public Long getCurrentUserId() {
        //抓取请求上下文
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        String token = requestAttributes.getRequest().getHeader("token");
        Long userId = TokenUtil.verifyToken(token);
        if (userId < 0) {
            throw new ConditionException("非法用户!");
        }
        return userId;
    }
}

获取请求头中的token,解析出token中的userId,就可以直接使用了。
在请求接口加上

 Long userId = userSupport.getCurrentUserId();

除此之外也可以使用不含负载信息的token, 把token信息存到redis里面
比如

key value
userId token
token userId

这样也可以获取信息。
生成token的时候,前端会在local storage中储存token,下一次使用的时候可以直接携带token。
此外还要如何刷新token的问题…

双token机制

refresh token

  • 退出登录了(防止使用还在有效期的token)token就不能再用了(与数据库关联解决)
  • 无感刷新,不能用着用着就不能用了
    工具类
/**
     * 刷新token 时间变长了
     * @param userId
     * @return
     * @throws Exception
     */
    public static String generateRefreshToken(Long userId) throws Exception {
        Algorithm algorithm = Algorithm.RSA256(RSAUtil.getPublicKey(), RSAUtil.getPrivateKey());
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        //7天
        calendar.add(Calendar.DAY_OF_MONTH, 7);

        return JWT.create().
                //用户信息 这里存放的userId
                withKeyId(String.valueOf(userId))
                //签发者
                .withIssuer(ISSUE)
                //过期时间
                .withExpiresAt(calendar.getTime())
                //使用加密算法
                .sign(algorithm);

    }

生成双token

		String phone = user.getPhone() == null ? "" : user.getPhone();
        String email = user.getEmail() == null ? "" : user.getEmail();
        if(StringUtils.isNullOrEmpty(phone) && StringUtils.isNullOrEmpty(email)){
            throw new ConditionException("参数异常!");
        }
        User dbUser = userDao.getUserByPhoneOrEmail(phone, email);
        if (dbUser == null) {
            throw new ConditionException("当前用户不存在");
        }
        String password = user.getPassword();
        String rawPassword;
        try {
            rawPassword = RSAUtil.decrypt(password);
        } catch (Exception e) {
            throw new ConditionException("密码解密失败");
        }
        String salt = dbUser.getSalt();
        String md5Password = MD5Util.sign(rawPassword, salt, "UTF-8");
        if (!md5Password.equals(dbUser.getPassword())){
            throw new ConditionException("密码错误");
        }
        TokenUtil tokenUtil = new TokenUtil();
        Long userId = dbUser.getId();
        //根据id生成token
        //接入token
        String accessToken =  tokenUtil.generateToken(userId);
        //刷新token
        String refreshToken = TokenUtil.generateRefreshToken(userId);
        //保存refresh token到数据库
        userDao.deleteRefreshToken(refreshToken, userId);
        userDao.addRefreshToken(refreshToken, userId, new Date());
        Map<String, Object> result = new HashMap<>();
        result.put("accessToken", accessToken);
        result.put("refreshToken", refreshToken);
        return result;

退出登录

	userDao.deleteRefreshToken(refreshToken, userId);

实现无感登录,生成新的token

/**
     * 刷新accessToken生成新的token
     * @param request
     * @return
     */
    @PostMapping("/access-tokens")
    public JsonResponse<String> refreshAccessToken(HttpServletRequest request) throws Exception {
        String refreshToken = request.getHeader("refreshToken");
        String accessToken = userService.refreshAccessToken(refreshToken);
        return new JsonResponse<>(accessToken);
    }
    public String refreshAccessToken(String refreshToken) throws Exception {
        RefreshTokenDetail refreshTokenDetail = userDao.getRefreshTokenDetail(refreshToken);
        if (refreshTokenDetail == null) {
            throw new ConditionException("555", "token过期!");
        }
        Long userId = refreshTokenDetail.getUserId();
        return TokenUtil.generateToken(userId);
    }

前端可以在状态码为555的时候调用刷新token接口,生成新token完成无感登录。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值