基于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配置项逐条解释
authorizeHttpRequests接口放行规则/api/auth/**:全部放行(登录、登出接口,不需要 Token)anyRequest().authenticated():其余所有接口必须携带有效 Token 登录后访问
formLogin表单登录配置- 登录提交地址:
POST /api/auth/login successHandler(this::onAuthenticationSuccess):登录成功处理器(生成 Token 返回前端)failureHandler(this::onAuthenticationFailure):登录失败处理器(返回 401 错误)
- 登录提交地址:
logout登出配置- 登出接口地址:
/api/auth/logout logoutSuccessHandler(this::onLogoutSuccess):登出处理器(Token 加入 Redis 黑名单失效)
- 登出接口地址:
exceptionHandling全局鉴权异常处理authenticationEntryPoint:未登录 / Token 失效,返回 401accessDeniedHandler:登录但权限不足,返回 403
csrf(AbstractHttpConfigurer::disable):关闭 CSRF 防护,前后端分离 JWT 无 session 不需要sessionCreationPolicy.STATELESS:无状态,完全放弃 Session,靠 Token 鉴权addFilterBefore(jwtAuthorizeFilter, UsernamePasswordAuthenticationFilter.class)请求进入时优先执行 JWT 过滤器,提前解析 Token 写入登录上下文
5 个核心处理器方法
onAuthenticationSuccess登录成功 查询完整账号 → 调用 JwtUtils 生成 Token → 封装 AuthorizeVO → RestBean 包装 JSON 返回前端onAuthenticationFailure登录失败 统一返回RestBean.unauthorized(异常信息),401 未授权onLogoutSuccess退出登录 获取请求头 Token → JwtUtils.invalidateJwt 将 Token 存入 Redis 黑名单 → 返回登出成功onUnauthorized未登录 / Token 过期 接口无有效 Token 时触发,返回 401onAccessDeny权限不足 用户已登录,但角色无访问接口权限,返回 403
三、流程 1:用户登录完整执行链路(前端 POST /api/auth/login)
总流程文字流程图
前端提交 username + 明文密码 → Security 过滤链接收请求 → 自动调用 AccountService.loadUserByUsername → 查询数据库账号 → Security 密码比对 → 成功 / 失败分支
分步详细执行
- 前端发起 POST 请求
http://localhost:端口/api/auth/login,表单携带 username、password - 请求进入 Security 过滤链,匹配 formLogin 登录处理地址,跳过 JwtAuthorizeFilter(放行接口无需鉴权)
- 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 对象,包含用户名、加密密码、角色 - Security 自动注入
BCryptPasswordEncoder,比对前端明文密码与数据库加密密码
分支 A:密码错误 / 账号不存在
- 进入
onAuthenticationFailure失败处理器 - 构造
RestBean.unauthorized("用户名或密码错误") - 调用
asJsonString()转为 JSON 写入 Response 返回前端,登录流程结束
分支 B:账号存在且密码匹配成功
- 进入
onAuthenticationSuccess登录成功处理器 - 从认证凭证获取用户名,再次调用
service.findAccountByNameOrEmail(user.getUsername())获取完整 Account 实体(id、role) - 调用
JwtUtils.createJwt(user, account.getId(), account.getUsername())生成 JWT 令牌 - 实例化 AuthorizeVO,填充 username、role、token、expire 过期时间
- 外层包装
RestBean.success(vo),转 JSON 字符串返回前端 - 前端接收 JSON,提取 token 存入浏览器 localStorage,后续所有接口请求头携带
Authorization: Bearer 令牌
四、流程 2:携带 Token 访问普通业务接口(如 GET /api/test/hello)
完整执行链路
前端请求头携带Authorization: Bearer xxx→ JwtAuthorizeFilter 优先拦截 → JwtUtils 校验 Token → 合法写入 Security 上下文 → 放行到 Controller
- 前端发起 GET 请求
GET /api/test/hello,Header 携带 Token - 请求进入 Security 过滤链,先执行 JwtAuthorizeFilter 过滤器
- 过滤器执行
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 权限校验
- Security 匹配规则
anyRequest().authenticated(),检测无登录认证信息,触发onUnauthorized处理器,返回 401 未登录 JSON - Token 合法场景:Security 检测到已存在登录认证,放行至 TestController
- Controller 执行 test () 方法,返回字符串
Hello World给前端
五、流程 3:退出登录完整执行链路(POST /api/auth/logout)
执行流程图
前端携带有效 Token 请求 logout 接口 → Security 登出拦截 → JwtUtils.invalidateJwt 将 Token 存入 Redis 黑名单 → 返回登出成功
- 前端发起请求
POST /api/auth/logout,Header 携带有效 Token - 接口路径属于
/api/auth/**放行路径,进入 Security logout 配置逻辑 - 触发
onLogoutSuccess登出成功处理器 - 读取请求头
Authorization完整 Token 字符串 - 调用
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 剩余有效期,过期自动清除缓存 - Redis 存入成功:返回
RestBean.success()无数据成功 JSON - Redis 存入失败(Token 已拉黑):返回
RestBean.failure(400,"退出登录失败") - 前端收到响应后,清除本地 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 返回结果
