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做项目,不妨试试这个思路,灵活又实用。

相关推荐
JavaGuide9 分钟前
轻量开源版 IDEA 来了!
前端·后端
吴声子夜歌27 分钟前
Guava——散列
java·guava
用户31268748772028 分钟前
AQS 到底怎么排队的?CLH 队列、state 与条件变量全链路拆解
java
Json____34 分钟前
springboot开发高校选课管理系统:从零构建前后端分离的全栈实践
java·spring boot·后端·毕业设计·毕设·wwwoop.com
站大爷IP38 分钟前
Python的列表推导式把我写崩了,原来圆括号和方括号不是一回事
后端
xcl092544 分钟前
民宿预约系统开发实战:从需求分析到上线部署全流程解析
java·spring boot
梦在远山后1 小时前
Python 中字符串常用写法
人工智能·后端
geovindu1 小时前
python:HandWriting Recognition using paddlepaddle
开发语言·后端·python·paddlepaddle
步行cgn1 小时前
为何以继承方式引入SpringBoot
java·spring boot·后端
CoderYanger1 小时前
A.每日一题:1140. 石子游戏 II
java·程序人生·算法·leetcode·游戏·职场和发展·深度优先