一、前言
在传统 Web 项目中,用户登录后通常使用 Session 保存登录状态:
用户登录
↓
服务端创建Session
↓
浏览器保存Cookie
↓
后续请求携带Cookie
但是在微服务架构中:
Gateway
↓
User-Service
Order-Service
Product-Service
Session 会面临以下问题:
- 服务扩容 Session 不共享
- 微服务之间认证困难
- 前后端分离支持较差
因此目前企业项目大多采用:
Spring Security
+
JWT
实现无状态登录认证。
二、Spring Security 核心对象
Spring Security 最核心的是三个对象:
SecurityContextHolder
↓
SecurityContext
↓
Authentication
1、Authentication
Authentication 表示当前登录用户。
Spring Security 所有认证信息最终都会放入 Authentication。
例如:
Authentication authentication = SecurityContextHolder
.getContext()
.getAuthentication();
2、Principal
Authentication 中保存真正的用户对象:
Object principal = authentication.getPrincipal();
这个对象可以是:
User LoginUser JwtUser GlobalJwtUser
Spring Security 并不关心具体类型。
只要实现认证成功即可。
3、SecurityContextHolder
用于保存当前请求认证信息。
结构如下:
SecurityContextHolder
↓
SecurityContext
↓
Authentication
↓
JwtUser
三、JWT 登录认证整体流程
完整认证流程如下:
用户登录
↓
用户名密码校验
↓
生成JWT
↓
返回前端
================================
后续请求
Authorization: Bearer xxx
↓
JwtAuthenticationFilter
↓
解析JWT
↓
获取JwtUser
↓
构造Authentication
↓
放入SecurityContext
↓
Controller
Service
四、Bearer 是什么
很多人第一次接触 JWT 都会看到:
Authorization: Bearer eyJhbGciOiJIUzI1Ni...
这里:
Bearer
表示认证方案。
格式遵循 HTTP 标准:
Authorization: <认证方式> <认证信息>
例如:
Authorization: Basic xxxxx Authorization: Bearer xxxxx
JWT 标准写法就是:
Authorization: Bearer token
Spring Security 默认也是按照这个格式解析。
五、自定义用户对象
企业项目一般不会直接使用 User。
通常会定义自己的用户对象:
@Data
public class JwtUser {
private Long userId;
private String username;
private String deptCode;
}
这个对象最终会存入 Spring Security 上下文。
六、登录成功生成 JWT
登录接口:
@PostMapping("/login")
public String login(LoginRequest request){
Authentication authentication =
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()));
JwtUser user =
(JwtUser) authentication.getPrincipal();
return jwtService.generateToken(user);
}
认证成功:
用户名密码
↓
数据库校验
↓
生成JWT
↓
返回前端
返回结果:
{
"token":"eyJhbGciOiJIUzI1Ni..."
}
七、JWT 过滤器解析 Token
企业项目核心代码基本都在过滤器中。
例如:
public class JwtAuthenticationFilter
extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws IOException, ServletException {
String token =
request.getHeader("Authorization");
if(token != null){
JwtUser user =
parseToken(token);
JwtAuthentication authentication =
new JwtAuthentication(user);
SecurityContextHolder
.getContext()
.setAuthentication(authentication);
}
filterChain.doFilter(request,response);
}
}
作用:
请求进入 ↓ 解析JWT ↓ 获得JwtUser ↓ 存入Spring Security上下文
八、自定义 Authentication
Spring Security 实际保存的是 Authentication。
因此一般会定义:
public class JwtAuthentication
extends AbstractAuthenticationToken {
private final JwtUser jwtUser;
public JwtAuthentication(JwtUser jwtUser) {
super(null);
this.jwtUser = jwtUser;
setAuthenticated(true);
}
@Override
public Object getPrincipal() {
return jwtUser;
}
@Override
public Object getCredentials() {
return null;
}
}
重点:
getPrincipal()
返回:
JwtUser
九、为什么 setAuthentication 如此重要
认证成功后:
SecurityContextHolder
.getContext()
.setAuthentication(authentication);
实际上完成了:
SecurityContextHolder
↓
Authentication
↓
JwtUser
的绑定。
这是整个 Spring Security 认证体系最关键的一步。
如果没有这一步:
SecurityContextHolder
.getContext()
.getAuthentication();
获取到的就是空。
十、Controller 如何自动获得当前用户
很多项目中会看到:
@PostMapping("/save")
public void save(
@CurrentJwtUser JwtUser user) {
}
为什么能够自动注入?
1、自定义注解
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@AuthenticationPrincipal
public @interface CurrentJwtUser {
}
本质上:
@CurrentJwtUser
就是:
@AuthenticationPrincipal
的二次封装。
2、Spring Security 自动解析
Spring Security 内部提供:
AuthenticationPrincipalArgumentResolver
专门处理:
@AuthenticationPrincipal
参数。
执行流程:
Controller请求
↓
发现参数
@CurrentJwtUser
↓
读取Authentication
↓
调用
authentication.getPrincipal()
↓
获得JwtUser
↓
自动注入
因此:
@CurrentJwtUser JwtUser user
实际上等价于:
JwtUser user =
(JwtUser)
SecurityContextHolder
.getContext()
.getAuthentication()
.getPrincipal();
十一、业务代码获取当前用户
很多 Service 层也需要当前用户。
通常封装工具类:
public class SecurityUtil {
public static JwtUser currentUser(){
Authentication authentication =
SecurityContextHolder
.getContext()
.getAuthentication();
if(authentication == null){
return null;
}
return (JwtUser)
authentication.getPrincipal();
}
}
使用:
JwtUser user =
SecurityUtil.currentUser();
Long userId =
user.getUserId();
十二、完整认证链路分析
整个流程串起来如下:
用户请求
Authorization: Bearer token
↓
JwtAuthenticationFilter
↓
解析JWT
↓
JwtUser
↓
JwtAuthentication
↓
SecurityContextHolder
↓
Controller
↓
@AuthenticationPrincipal
↓
Authentication.getPrincipal()
↓
JwtUser自动注入
十三、为什么 Controller 和 Service 获取的是同一个用户
Controller:
@CurrentJwtUser JwtUser user
Service:
SecurityUtil.currentUser()
虽然写法不同。
但最终访问的都是:
SecurityContextHolder
↓
SecurityContext
↓
Authentication
↓
JwtUser
因此拿到的是同一个对象。
十四、企业项目最佳实践
推荐架构:
Spring Security
+
JWT
+
Gateway
+
Redis
流程:
登录 ↓ 认证中心 ↓ JWT ↓ Gateway统一校验 ↓ 解析用户信息 ↓ 写入上下文 ↓ 业务服务直接获取用户
优点:
- 无状态认证
- 支持微服务
- 支持水平扩容
- 支持统一权限控制
- 支持单点登录
十五、总结
Spring Security + JWT 的核心只有一句话:
SecurityContextHolder
.getContext()
.setAuthentication(authentication);
认证成功后:
JwtUser
↓
JwtAuthentication
↓
SecurityContextHolder
随后:
@AuthenticationPrincipal
或者:
SecurityContextHolder
都能获取当前登录用户。
整个认证链路可以概括为:
JWT
↓
Filter
↓
JwtUser
↓
Authentication
↓
SecurityContextHolder
↓
Controller/Service
理解了这条链路,就真正理解了 Spring Security 的认证机制。













