基于Springboot3+Security6+Jwt实现登录/登出功能

📅 2026/7/11 21:08:20 👁️ 阅读次数 📝 编程学习
基于Springboot3+Security6+Jwt实现登录/登出功能

一、项目整体技术栈

  • 核心框架:SpringBoot 3.3.5 + Spring Security 6
  • 持久层:MyBatis-Plus 3.5.7 + MySQL8
  • 鉴权:JWT(java-jwt 4.3.0)+ Redis(实现登出 Token 黑名单失效)
  • 工具:Lombok、FastJson2、BCrypt 密码加密
  • 构建工具:Maven,Java17
  • 架构模式:标准 MVC 三层架构 + 过滤器前置鉴权 + 前后端分离无状态登录(不使用 Session)

二、主要功能模块

SecurityConfiguration(整套安全规则核心)

核心过滤链SecurityFilterChain配置项逐条解释
  1. authorizeHttpRequests接口放行规则
    • /api/auth/**:全部放行(登录、登出接口,不需要 Token)
    • anyRequest().authenticated():其余所有接口必须携带有效 Token 登录后访问
  2. formLogin表单登录配置
    • 登录提交地址:POST /api/auth/login
    • successHandler(this::onAuthenticationSuccess):登录成功处理器(生成 Token 返回前端)
    • failureHandler(this::onAuthenticationFailure):登录失败处理器(返回 401 错误)
  3. logout登出配置
    • 登出接口地址:/api/auth/logout
    • logoutSuccessHandler(this::onLogoutSuccess):登出处理器(Token 加入 Redis 黑名单失效)
  4. exceptionHandling全局鉴权异常处理
    • authenticationEntryPoint:未登录 / Token 失效,返回 401
    • accessDeniedHandler:登录但权限不足,返回 403
  5. csrf(AbstractHttpConfigurer::disable):关闭 CSRF 防护,前后端分离 JWT 无 session 不需要
  6. sessionCreationPolicy.STATELESS:无状态,完全放弃 Session,靠 Token 鉴权
  7. addFilterBefore(jwtAuthorizeFilter, UsernamePasswordAuthenticationFilter.class)请求进入时优先执行 JWT 过滤器,提前解析 Token 写入登录上下文
5 个核心处理器方法
  1. onAuthenticationSuccess登录成功 查询完整账号 → 调用 JwtUtils 生成 Token → 封装 AuthorizeVO → RestBean 包装 JSON 返回前端
  2. onAuthenticationFailure登录失败 统一返回RestBean.unauthorized(异常信息),401 未授权
  3. onLogoutSuccess退出登录 获取请求头 Token → JwtUtils.invalidateJwt 将 Token 存入 Redis 黑名单 → 返回登出成功
  4. onUnauthorized未登录 / Token 过期 接口无有效 Token 时触发,返回 401
  5. onAccessDeny权限不足 用户已登录,但角色无访问接口权限,返回 403

三、流程 1:用户登录完整执行链路(前端 POST /api/auth/login)

总流程文字流程图

前端提交 username + 明文密码 → Security 过滤链接收请求 → 自动调用 AccountService.loadUserByUsername → 查询数据库账号 → Security 密码比对 → 成功 / 失败分支

分步详细执行

  1. 前端发起 POST 请求http://localhost:端口/api/auth/login,表单携带 username、password
  2. 请求进入 Security 过滤链,匹配 formLogin 登录处理地址,跳过 JwtAuthorizeFilter(放行接口无需鉴权)
  3. Security 框架自动执行认证逻辑,调用AccountService.loadUserByUsername(前端username)3.1loadUserByUsername内部调用findAccountByNameOrEmail(text)3.2 MyBatis-Plus 生成 SQLselect * from db_account where username=? or email=?,底层调用 AccountMapper 查询数据库 3.3 数据库返回 Account 实体;无匹配数据直接抛出UsernameNotFoundException3.4 封装 Security 内置 User 对象,包含用户名、加密密码、角色
  4. Security 自动注入BCryptPasswordEncoder,比对前端明文密码与数据库加密密码

分支 A:密码错误 / 账号不存在

  1. 进入onAuthenticationFailure失败处理器
  2. 构造RestBean.unauthorized("用户名或密码错误")
  3. 调用asJsonString()转为 JSON 写入 Response 返回前端,登录流程结束

分支 B:账号存在且密码匹配成功

  1. 进入onAuthenticationSuccess登录成功处理器
  2. 从认证凭证获取用户名,再次调用service.findAccountByNameOrEmail(user.getUsername())获取完整 Account 实体(id、role)
  3. 调用JwtUtils.createJwt(user, account.getId(), account.getUsername())生成 JWT 令牌
  4. 实例化 AuthorizeVO,填充 username、role、token、expire 过期时间
  5. 外层包装RestBean.success(vo),转 JSON 字符串返回前端
  6. 前端接收 JSON,提取 token 存入浏览器 localStorage,后续所有接口请求头携带Authorization: Bearer 令牌

四、流程 2:携带 Token 访问普通业务接口(如 GET /api/test/hello)

完整执行链路

前端请求头携带Authorization: Bearer xxx→ JwtAuthorizeFilter 优先拦截 → JwtUtils 校验 Token → 合法写入 Security 上下文 → 放行到 Controller

  1. 前端发起 GET 请求GET /api/test/hello,Header 携带 Token
  2. 请求进入 Security 过滤链,先执行 JwtAuthorizeFilter 过滤器
  3. 过滤器执行doFilterInternal3.1 读取 Header 中的 Authorization 字符串 3.2 调用utils.resolveJwt(authorization)校验 Token:
    • 剥离 Bearer 前缀,校验签名密钥一致性
    • 判断 Token 是否过期
    • 查询 Redis 黑名单,判断是否已执行退出登录失效 3.3 校验通过(返回 DecodedJWT 对象)
    • utils.toUser(jwt)解析 Token 内用户名、角色权限
    • 构造UsernamePasswordAuthenticationToken认证对象
    • 存入SecurityContextHolder.getContext()全局登录上下文
    • 将用户 ID 存入 request 域 3.4 校验失败(返回 null):不写入登录上下文,直接放行到 Security 权限校验
  4. Security 匹配规则anyRequest().authenticated(),检测无登录认证信息,触发onUnauthorized处理器,返回 401 未登录 JSON
  5. Token 合法场景:Security 检测到已存在登录认证,放行至 TestController
  6. Controller 执行 test () 方法,返回字符串Hello World给前端

五、流程 3:退出登录完整执行链路(POST /api/auth/logout)

执行流程图

前端携带有效 Token 请求 logout 接口 → Security 登出拦截 → JwtUtils.invalidateJwt 将 Token 存入 Redis 黑名单 → 返回登出成功

  1. 前端发起请求POST /api/auth/logout,Header 携带有效 Token
  2. 接口路径属于/api/auth/**放行路径,进入 Security logout 配置逻辑
  3. 触发onLogoutSuccess登出成功处理器
  4. 读取请求头Authorization完整 Token 字符串
  5. 调用utils.invalidateJwt(authorization)5.1convertToken剥离 Bearer 前缀,获取纯净 token 5.2 校验 Token 签名合法性,解析 Token 内部唯一 UUID(JWTId) 5.3 计算 Token 剩余有效时间,调用deleteToken(uuid, 过期时间)5.4 Redis 存入 Key:jwt:blacklist:UUID,过期时长 = Token 剩余有效期,过期自动清除缓存
  6. Redis 存入成功:返回RestBean.success()无数据成功 JSON
  7. Redis 存入失败(Token 已拉黑):返回RestBean.failure(400,"退出登录失败")
  8. 前端收到响应后,清除本地 localStorage 中的 token,跳转登录页

登出黑名单核心作用:

用户退出登录后,原有 Token 即便未过期也直接失效;下次携带该 Token 访问接口时,JwtUtils.resolveJwt检测 Redis 黑名单存在,直接判定 Token 非法,返回 401。

六、全模块调用依赖汇总表

1. 登录流程调用链

前端接口 → SecurityConfiguration.formLogin → AccountService.loadUserByUsername → AccountServiceImpl.findAccountByNameOrEmail → AccountMapper.selectOne → MySQL db_account 表 密码校验:Security 内置 BCryptPasswordEncoder(WebConfiguration 注册) 生成 Token:onAuthenticationSuccess → JwtUtils.createJwt → AuthorizeVO → RestBean 返回 JSON

2. 全局鉴权调用链

任意带 Token 请求 → JwtAuthorizeFilter → JwtUtils.resolveJwt → Redis 黑名单查询 + Token 验签 → SecurityContextHolder 写入登录信息 → Controller 业务接口

3. 退出登录调用链

前端 logout 接口 → SecurityConfiguration.logout → onLogoutSuccess → JwtUtils.invalidateJwt → JwtUtils.deleteToken → StringRedisTemplate 写入 Redis 黑名单 → RestBean 返回结果