Spring Security + JWT 登录认证完整实践指南

目录

一、前言

在传统 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 的认证机制。

相关推荐
晚笙coding1 小时前
从零讲透 LangChain 输出格式化:让模型真的“能用”
java·开发语言·langchain
奋斗的小方1 小时前
Java进阶篇1-1:异常
java·开发语言·python
码语智行1 小时前
行政区划 ZIP 导入(importZip)
java
何中应1 小时前
Nexus如何设置端口号
java·服务器·maven·nexus
思麟呀1 小时前
C++11并发编程:条件变量
java·linux·jvm·c++·windows
Full Stack Developme1 小时前
Hutool CollUtil 教程
java·开发语言·windows·python
我是一颗柠檬1 小时前
【Java项目技术亮点】Kafka异步写+写聚合:吞吐量提升10倍的消息队列优化秘籍
java·分布式·kafka·linq
砍材农夫1 小时前
物联网实战:Spring Boot MQTT | 客户端框架比对
spring boot·后端·物联网
Gopher_HBo2 小时前
存储层LSM Tree
后端·架构