jwt与session和cookie详解和对比以及jwt在项目中的应用(二)

项目采用 是JWT + Redis的混合方案实现认证授权,下面来详细看看流程

一、token的创建

关于token的创建和保存我认为应该从以下几点出发,首先是怎么创建的,然后是何时创建的,。

怎么创建的

token是由三部分拼接而成的,具体的操作方式是由我们程序员自己决定的,在我们的项目里有一个核心的JwtUtil放在travel-common-core这个项目里,根据尾缀可以判断放在工具的软件包里,在这个工具里可以看到最核心的拼接方式

    public static String createToken(Map<String, Object> claims)
    {
        String token = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS512secret).compact();
        return token;
    }

可以看到这段代码非常的短嗷,但其实这是一个方便调用的二次封装过的方法,可以看到token的生成方式就在这中间,Jwts.builder()是创建一个jwt的构造器对象,是一个组装用的空壳然后是setClaims(claims),claim的英文意思是载荷/声明,所以这个方法的意思是将数据添加到PayLoad(载荷)中,signWith(SignatureAlgorithm.HS512, secret),中间有个HS512还有secret是秘密的意思不难猜到,这个方法的作用就是指定加密方式和传入密钥的,这两步都是为了制作签名而做的准备,最重要的就是最后一个compact()方法了,负责完成全部 3 段的编码 + 签名,生成 header.payload.signature 格式字符串,这样我们就明白jwt是怎样创建的了

何时创建的

答案是:在用户登录的时候创建的(注意:不是注册时候创建),我们可以在travel-auth的controller中看到

 // 用户登录
        LoginUser userInfo = null;
        if (form.getType().equals(TokenConstants.ADMIN)) {
            userInfo = sysLoginService.login(form.getUsername(), form.getPassword());
        } else {
            userInfo = pcLoginService.login(form.getUsername(), form.getPassword());
        }
        // 获取登录token
        return R.ok(tokenService.createToken(userInfo));

有一个tokenService中的createToken方法,可以看到与jwtUtil中的方法名是一模一样,那到底有何区别呢,我们来看看看这个createToken方法

        String token = IdUtils.fastUUID();
        Long userId = loginUser.getUserid();
        String userName = loginUser.getUsername();
        loginUser.setToken(token);
        loginUser.setUserid(userId);
        loginUser.setUsername(userName);
        loginUser.setIpaddr(IpUtils.getIpAddr());
        refreshToken(loginUser);

        // Jwt存储信息
        Map<String, Object> claimsMap = new HashMap<String, Object>();
        claimsMap.put(SecurityConstants.USER_KEY, token);
        claimsMap.put(SecurityConstants.DETAILS_USER_ID, userId);
        claimsMap.put(SecurityConstants.DETAILS_USERNAME, userName);

        // 接口返回信息
        Map<String, Object> rspMap = new HashMap<String, Object>();
        rspMap.put("access_token", JwtUtils.createToken(claimsMap));
        rspMap.put("expires_in", expireTime);
        return rspMap;

;第一段代码的目的是为了把封装好的 loginUser对象传给这个refreshToken方法,这里的token并非真正的token,他其实是登录用户的唯一标识,第二段代码new了一个claimMap,前面我们也讲过这个clain就是载荷的意思,那么很明显这个,这第二段代码是用来存储jwt信息的,第三段new了一个rspMap,很明显就是response这个单词,也就是响应给前端的数据,往里面放了token和expireTime(过期时间),很明显,这个createToken的方法是面向登录这个业务的,而另一个则是一个纯粹的生成token的工具类。

刚才的方法体中的refreshToken方法非常重要,意思是刷新token,我们按ctrl跟进

    {
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
        // 根据uuid将loginUser缓存
        String userKey = getTokenKey(loginUser.getToken());
        redisService.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
    }

设置了过期时间。userKey,还调用了 redisService.setCacheObject,这里无疑就是登录信息(包括用户的全部信息)存到reids中,这也是项目中的高明之处,JWT 只存储轻量级标识信息(如 user_key、user_id、username),而完整的用户信息(权限、角色等)存储在 Redis 中,这样做的好处有很多:

  • 支持服务端主动注销(删除 Redis 缓存即可)
  • Token 体积小,传输效率高
  • 权限数据可实时变更,无需重新签发 Token,(token一经签发无法作废)

好了,到这里,token的创建流程已经明确了,继续来看看token接下来的走向

网关过滤器

在travel-gateway的filter包中,可以看到过滤器对应的方法,我们来找对应token的部分

   if (!request.getURI().getPath().startsWith("/admin/")) {
            String token = getToken(request);
            if (StringUtils.isNotEmpty(token)) {
                return checkToken(token, exchange, mutate, chain);
            }
            return chain.filter(exchange);
        }

        String token = getToken(request);
        if (StringUtils.isEmpty(token)) {
            return unauthorizedResponse(exchange, "令牌不能为空");
        }
        return checkToken(token, exchange, mutate, chain);

这里首先对是否带/admin的路径进行了判断,如果是管理的员的话,没有token也放行,剩下的由业务进行判断,如果不是管理员的话,必须有令牌,不然会报错

业务服务拦截器

 String token = SecurityUtils.getToken();
        if (StringUtils.isNotEmpty(token))
        {
            LoginUser loginUser = AuthUtil.getLoginUser(token);
            if (StringUtils.isNotNull(loginUser))
            {
                AuthUtil.verifyLoginUserExpire(loginUser);
                SecurityContextHolder.set(SecurityConstants.LOGIN_USER, loginUser);
            }
        }
        return true;

首先获取token(注意:这里的token指的是UserKey,用来在redis查用户数据的),如果用户数据不为空进行 AuthUtil.verifyLoginUserExpire(loginUser),校验是否快过期,做续活逻辑(每次登录都会延长token的存活时间),把登录用户放到本地线程上下文(ThreadLocal 封装)。后面 Controller / Service 通过 SecurityUtils.getLoginUser() 就可以拿到登录用户,拿 userId 做业务。到这里token的使命就基本完成了,可以看到并没有对是否能访问controller的方法进行限制,这里就要靠注解了,最后token传进来的数据会随着业务的调用游走整个项目,这样jwt的完整的工作流程就结束了

整个流程

前端带 token → 网关 GlobalFilter → 执行 checkToken()

校验 JWT、查 Redis

token 合法:删掉原来的 JWT,把 userId、username、userkey 放进请求头,放行

http 请求转发到后端业务微服务

后端微服务的拦截器 (HandlerInterceptor) 拿到 header 里面的用户 id

把用户信息存入 ThreadLocal,后续 Controller、Service 直接取用

到达 Controller

posted @ 2026-08-14 20:43  霜华序  阅读(6)  评论(0)    收藏  举报