Spring Cloud项目登录认证从JWT切换到Redis + UUID Token方案

背景介绍

在传统的Spring Boot项目中,用户登录认证常见的方案是使用JWT(JSON Web Token)来实现无状态的身份验证。JWT凭借自包含用户信息、方便前后端分离、性能较好等优势被广泛采用。

然而,在实际项目中,JWT也有一定缺点,比如:

  • 不能主动失效(除非设计复杂的黑名单机制)

  • token刷新逻辑复杂

  • 服务器无法灵活控制单点登出

为了提升认证的灵活性和安全性,我将项目的登录鉴权方案由JWT切换成了 Redis + UUID Token 的模式,实现了服务端存储与校验,且支持token的自动续期。


为什么切换?

  • 支持主动注销:退出登录时,服务器可以直接删除Redis中对应的Token。

  • 便于统一管理Token生命周期:可以灵活设置过期时间和续期策略。

  • 方便单点登出和多端管理

  • 实现滑动过期(自动续期),提升用户体验。


方案架构

流程图

复制代码
用户登录 -> 服务器生成UUID Token -> 保存Token对应的用户ID到Redis并设置过期 -> 返回Token给客户端 -> 客户端请求时携带Token -> 网关/服务端从Redis验证Token有效性 -> 每次请求时刷新Token过期时间(滑动过期) -> 用户退出登录时删除Redis中的Token

关键代码实现

1. 登录接口

java 复制代码
@PostMapping("/login")
public Result<UserVO> login(@RequestBody LoginRequest loginRequest) {
    // 认证逻辑验证用户名密码(略)
    User user = userService.findByUsername(loginRequest.getUsername());
    if (user == null || !passwordEncoder.matches(loginRequest.getPassword(), user.getPassword())) {
        return Result.fail("用户名或密码错误");
    }

    // 生成 UUID Token
    String token = UUID.randomUUID().toString();

    // 构建 Redis 键
    String redisKey = RedisKeyConstant.LOGIN_TOKEN_PREFIX + token;

    // 保存用户 ID 到 Redis,设置过期时间(30分钟)
    stringRedisTemplate.opsForValue().set(redisKey, String.valueOf(user.getId()), 30, TimeUnit.MINUTES);

    // 返回给前端
    UserVO userVO = new UserVO();
    userVO.setToken(token);
    // 其他用户信息设置略
    return Result.ok(userVO);
}

2. 网关全局过滤器(校验Token + 自动续期)

java 复制代码
@Component
@RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    private final StringRedisTemplate redisTemplate;
    private final AntPathMatcher antPathMatcher = new AntPathMatcher();
    private final long TOKEN_EXPIRE_MINUTES = 30;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getPath().toString();

        // 跳过无需认证路径
        if (isExclude(path)) {
            return chain.filter(exchange);
        }

        List<String> tokenList = exchange.getRequest().getHeaders().get("Authorization");
        if (tokenList == null || tokenList.isEmpty()) {
            return unauthorized(exchange);
        }
        String token = tokenList.get(0);

        // Redis校验Token是否有效
        String redisKey = RedisKeyConstant.LOGIN_TOKEN_PREFIX + token;
        String userId = redisTemplate.opsForValue().get(redisKey);
        if (userId == null) {
            return unauthorized(exchange);
        }

        // 续期(滑动过期)
        redisTemplate.expire(redisKey, TOKEN_EXPIRE_MINUTES, TimeUnit.MINUTES);

        // 把用户ID放入请求头,供后端服务使用
        ServerHttpRequest newRequest = exchange.getRequest().mutate()
                .header("user-info", userId)
                .build();

        return chain.filter(exchange.mutate().request(newRequest).build());
    }

    private boolean isExclude(String path) {
        // 这里可以配置白名单路径
        return path.startsWith("/public") || path.equals("/user/login");
    }

    private Mono<Void> unauthorized(ServerWebExchange exchange) {
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        return exchange.getResponse().setComplete();
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

3. 退出登录接口

java 复制代码
@PostMapping("/logout")
public Result<Void> logout(@RequestHeader("Authorization") String token) {
    if (token == null || token.isEmpty()) {
        return Result.fail("未登录");
    }
    String redisKey = RedisKeyConstant.LOGIN_TOKEN_PREFIX + token;
    Boolean deleted = stringRedisTemplate.delete(redisKey);
    if (Boolean.TRUE.equals(deleted)) {
        return Result.ok();
    } else {
        return Result.fail("退出失败或已过期");
    }
}

4. 前端请求示例(基于axios)

TypeScript 复制代码
import axios from 'axios';

const myAxios = axios.create({
    baseURL: 'http://localhost:9090',
    withCredentials: true,
});

myAxios.interceptors.request.use(config => {
    const token = localStorage.getItem('token');
    if (token) {
        config.headers['Authorization'] = token;
    }
    return config;
});

myAxios.interceptors.response.use(response => {
    if (response.data.code === 40101) {
        alert('未登录,请重新登录');
        window.location.href = '/user/login';
    }
    return response.data;
});

export default myAxios;

总结

  • 通过Redis + UUID Token方案,避免了JWT token的复杂管理。

  • 支持服务器主动失效token,支持滑动过期,提升了安全性和用户体验。

  • 网关统一校验token,简化后端服务实现。

  • 适合对token管理要求较高,需要灵活控制用户登录状态的项目。


未来展望

  • 可以结合Redis的Hash数据结构实现多端登录管理。

  • 增加刷新token接口,实现无感刷新。

  • 结合Spring Security进行更细粒度的权限控制。


希望这篇文章对你有所帮助,欢迎点赞和关注!如果你也在用Spring Boot做项目,不妨试试这个思路,灵活又实用。

相关推荐
2301_800256112 分钟前
第十一章中的函数解读(1)
后端·asp.net
喵爸的小作坊5 分钟前
StreamPanel:一个让 SSE 调试不再痛苦的 Chrome 插件
前端·后端·http
神奇小汤圆5 分钟前
字符串匹配算法
后端
无限大612 分钟前
为什么网站需要"域名"?——从 IP 地址到网址的演进
后端
树獭叔叔17 分钟前
LangGraph Memory 机制
后端·langchain·aigc
BullSmall18 分钟前
Tomcat11证书配置全指南
java·运维·tomcat
永不停歇的蜗牛20 分钟前
K8S之创建cm指令create和 apply的区别
java·容器·kubernetes
Java编程爱好者21 分钟前
OpenCVSharp:了解几种特征检测
后端
爱学习的小可爱卢25 分钟前
JavaEE进阶——SpringBoot统一功能处理全解析
java·spring boot·后端·java-ee