后端双token登陆快速入门:双token实现登陆,判断登陆过期

双token验证登陆过期

JWT

不了解token或者JWT的可以先看这里JWT 基础概念详解 | JavaGuide,了解后可以直接跳过这一部分

简单来说,Token是用来鉴别用户身份的令牌,JWT是JSON Web Token一种规范化之后的 JSON 结构的 Token。

JWT 本质上就是一组字串,通过(.)切分成三个为 Base64 编码的部分:

JWT通常长这样子:xxxxx.yyyyy.zzzzz

示例:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

JWT在加密之前是json格式的,加密后才变得如此抽象,用.分割的三部分在加密前都是JSON格式的数据,分别如下:

  • Header : 描述 JWT 的元数据,定义了生成签名的算法以及 Token 的类型
  • Payload : 用来存放实际需要传递的数据。其中我认为最主要的两个字段:sub字段存储想要保存的信息,exp字段保存该JWT的过期时间
  • Signature(签名):服务器通过 Payload、Header 和一个密钥(Secret)使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。

而服务器端通过加密算法生成JWT,并将诸如用户ID,用户权限等信息放入Payload的sub字段,前端调用后端接口时把token作为参数(一般以http头的形式)传给后端,后端解析后得知调用这个参数的用户是哪一个,由此做出相应的响应。

为什么要使用双token

由于token的过期时间在token生成后就难以更改,所有token的过期时长不宜设置过长:

  • 当用户信息发生更改时,token中的信息无法及时更改
  • 增加token中信息泄露的风险

然而token过期时间短的话,用户需要不停登陆,体验较差

解决方案:使用双token验证登陆过期:

  • 一个accessToken,过期时间较短,储存用户信息权限等
  • 一个refreshToken,过期时间较长,不储存额外信息,只储存用户id

双token运作流程

  1. 前端请求登录/注册接口后后端会返回accessToken和refreshToken
  2. 请求需要登录的接口时,在请求头携带accessToken,key为“Authorization”,value为accessToken的值
  3. 后端首先验证accessToken是否过期,没过期就正常处理请求、返回结果;过期就返回errCode=409,errDesc="用户未登录"的结果
  4. 前端如果接收到了上述过期的结果,需要不携带任何请求头请求/api/user/refreshToken接口,传入参数refreshToken=“xxxx”。
  5. 如果refreshToken没过期,后端会返回新的accessToken和refreshToken,此时前端可以用拿到的新的accessToken重新请求登陆接口
  6. 如果refreshToken也过期了,/api/user/refreshToken接口同样会返回errCode=409,errDesc="用户未登录"的结果,此时前端需要提示用户长时间未操作,需要重新登陆(这次登陆不携带token,需要用户输入账号密码等方式登陆)

这个流程需要前端配合:

  • 登陆接口收到409的响应后,需要请求/api/user/refreshToken接口
  • /api/user/refreshToken接口返回新的accessToken后需要重新请求登陆接口
  • /api/user/refreshToken接口返回409后需要跳转到登陆界面

后端双token代码:

JwtUtil.java:

JWT工具类,可以直接照搬使用

package com.neu.deliveryPlatform.util;

/**
 * @Author laobuzhang
 * @Description: jwt工具类,通过UUID算法生成JWT的id,通过subject参数将要储存的信息放在jwt的sub字段中
 */

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;

/**
 * JWT工具类
 */
public class JwtUtil {

    //有效期为
    public static final Long JWT_TTL = 60 * 60 * 24 * 1000L;//  一天
    //设置秘钥明文
    public static final String JWT_KEY = "BuXiWanGongZuoShi";

    public static String getUUID() {
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        return token;
    }

    /**
     * 生成jtw
     *
     * @param subject token中要存放的数据(json格式)
     * @return
     */
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());
        return builder.compact();
    }

    /**
     * 创建token
     *
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
        return builder.compact();
    }

    /**
     * 生成jtw
     *
     * @param subject   token中要存放的数据(json格式)
     * @param ttlMillis token超时时间
     * @return
     */
    public static String createJWT(String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
        return builder.compact();
    }

    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
        // jwt签名加密算法HS256
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        // 密钥
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if (ttlMillis == null) {
            ttlMillis = JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        return Jwts.builder()
                .setId(uuid)           // 唯一的ID,通过这个ID可以在redis中找到该JWT对应的用户信息
                .setSubject(subject)   // 主题  可以是JSON数据
                .setIssuer("sg")       // 签发者
                .setIssuedAt(now)      // 签发时间
                .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate);
    }


    /**
     * 生成加密后的秘钥 secretKey
     *
     * @return
     */
    public static SecretKey generalKey() {
        // 使用base64将密钥编码成二进制
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        // 使用AES算法加密二进制密钥
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }

    /**
     * 解析jwt,最主要目的拿到jwt的id
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }


}

UserServiceImpl.java:

Response,openid等是我的项目中才有的,你的项目需要按照情况更改

public Response login(String userName,String password){
    //写一些判断登陆信息是否正确的逻辑
    
    if(成功登陆){
        // openid是微信登陆的凭证,你的项目没有就可以不要这个参数
        // user是登陆成功后从数据库中查到的用户信息实体类变量
        return Response.of(getLoginResponse(openid,user));
    }else{
        throw new BizException(ErrorCode.LOGIN_ERROR);
    }
}

private Map<String, Object> getLoginResponse(String openid, User user) {   
    Long userId = user.getId();
    //查询权限信息
    List<String> roles = userMapper.getRoles(userId);
    List<String> authorities = userMapper.getAuthorities(userId);
    UserPermission userPermission = new UserPermission();
    userPermission.setUserId(String.valueOf(userId));
    userPermission.setOpenid(openid);
    userPermission.setRoles(roles);
    userPermission.setAuthorities(authorities);
    //存入redis
    String key = KeyProperties.TOKEN_PREFIX + userId;
    String value = JSON.toJSONString(userPermission);
    stringRedisTemplate.opsForValue().set(key, value, configProperties.getAccessTokenExpiration(), TimeUnit.MILLISECONDS);
    stringRedisTemplate.opsForValue().set(key, String.valueOf(userId), configProperties.getRefreshTokenExpiration(), TimeUnit.MILLISECONDS);
    //根据userId生成jwt
    String accessToken = JwtUtil.createJWT(String.valueOf(userId), configProperties.getAccessTokenExpiration());
    String refreshToken = JwtUtil.createJWT(String.valueOf(userId), configProperties.getRefreshTokenExpiration());
    //封装jwt为token返回
    Map<String, Object> resp = new HashMap<>();
    resp.put(ACCESS_TOKEN, accessToken);
    resp.put(REFRESH_TOKEN, refreshToken);
    resp.put(USERNAME, user.getUsername());
    resp.put(USER_ID, userId);
    return resp;
}


public Response refreshToken(String refreshToken) {
    //解析token
    String userId = "";
    Claims claims = null;
    try {
        claims = JwtUtil.parseJWT(refreshToken);
    } catch (Exception e) {
        throw new BizException(ErrorCode.TOKEN_PARSE_ERROR);
    }
    userId = claims.getSubject();
    //从redis获取用户信息
    String key = KeyProperties.TOKEN_PREFIX + userId;
    String userInfo = stringRedisTemplate.opsForValue().get(key);
    //这之前应该加一个refreashToken过期的判断,这里没有写完全
    //重新设置redis Key
    stringRedisTemplate.delete(key);
    stringRedisTemplate.opsForValue()
            .set(key, userInfo, configProperties.getRefreshTokenExpiration(), TimeUnit.MILLISECONDS);
    //生成新的token
    //根据userId生成jwt
    String accessToken = JwtUtil.createJWT(String.valueOf(userId), configProperties.getAccessTokenExpiration());
    refreshToken = JwtUtil.createJWT(String.valueOf(userId), configProperties.getRefreshTokenExpiration());
    //封装jwt为token返回
    Map<String, String> resp = new HashMap<>(2);
    resp.put(ACCESS_TOKEN, accessToken);
    resp.put(REFRESH_TOKEN, refreshToken);
    return Response.of(resp);
}
  • 14
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值