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