Administrator
Published on 2026-04-26 / 14 Visits
0
0

RAG 用户权限控制

学习一个框架

  1. 功能 - 解决的问题

  2. 如何使用 - 配置

  3. 我做的配置如何影响功能

登录校验流程
未登录: 前端发现localstorage中没有token redirect到登录页面
登录过程: 前端发送用户名和密码的请求
后端在数据库中比对用户名和密码
正确 -> 用用户名和密码登生成jwt
返回jwt
有jwt后请求其他接口,接口解析jwt中的用户名和密码,进行权限判断

Authentication 接口代表正在认证的用户
UserDetail封装了用户信息

RBAC

What:
Role-Based Access Control 基于角色的访问控制
角色继承的 RBAC 模型: 上层角色继承下层角色的所有权限,并且可以额外拥有其他权限。

RBAC 由四个核心要素构成:

  • 用户(User) — 系统的实际使用者

  • 角色(Role) — 一组权限的集合,如"管理员"、"编辑"、"访客"

  • 权限(Permission) — 对某个资源的某种操作,如"读取订单"、"删除用户"

  • 资源(Resource) — 被保护的系统对象,如文件、数据库表、API 接口
    image.png

JWT

前置知识:
加密:把内容变成另一种样子,只有有秘钥的人才能还原回原来的样子
RSA: 通过数学手段,我们可以生成一对秘钥,称为A,B 用A加密只能用B解密,用B加密只能用A解密。我们把A公布出去,B保密。

结构

JWT 就是一个用 . 分隔的三段字符串:

Header               . Payload              . Signature
eyJhbGciOiJIUzI1NiJ9 . eyJzdWIiOiJhZG1pbiJ9 . xK9s8fJdL3mP2nQ1rT

运算过程

Header:  { "alg": "HS256", "typ": "JWT" }
Payload: { "sub": "admin", "iat": 1710000000, "exp": 1710086400 }

第一步:Base64URL 编码

Base64URL 是 Base64 的变体,把 + 换成 -/ 换成 _,去掉末尾 =,目的是让结果能安全放在 URL 里

Base64URL({ "alg": "HS256", "typ": "JWT" })  →  eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Base64URL({ "sub": "admin", ... })            →  eyJzdWIiOiJhZG1pbiIsImlhdCI6MTcxMH0

Base64URL 不是加密,只是编码,任何人都能解码还原 JSON


第二步:HMAC 签名

待签名数据 = eyJhbGciOiJIUzI1NiJ9 + "." + eyJzdWIiOiJhZG1pbiJ9

HMAC_SHA256(待签名数据, 服务器私钥)
    ↓
原始字节:8a3f...(二进制)
    ↓
Base64URL编码
    ↓
Signature:xK9s8fJdL3mP2nQ1rT

注意:这里的服务器私钥是Base64存储的二进制字节数组

第三步:拼接

eyJhbGciOiJIUzI1NiJ9          ← Header
        +  .
eyJzdWIiOiJhZG1pbiJ9          ← Payload
        +  .
xK9s8fJdL3mP2nQ1rT            ← Signature
        ↓
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiJ9.xK9s8fJdL3mP2nQ1rT

验证

收到 JWT:AAA.BBB.CCC

1. 按 . 拆分成三段
2. 用密钥重新计算 HMAC(AAA + "." + BBB)  →  CCC'
3. 对比 CCC == CCC'  →  相同则合法
4. Base64URL 解码 BBB  →  还原 Payload JSON
5. 检查 exp 是否过期

内容

编码方式

Header

算法类型(如 HS256)

Base64URL

Payload

用户数据(username、过期时间等)

Base64URL

Signature

签名,防篡改

HMAC 计算结果

注意:JWT可以使用RSA也可以使用HMAC,这里不使用RSA而是HMAC因为JWT是服务器产生,也是服务器消费,因此不需要公钥

HMAC 是什么

HMAC是结合了秘钥的哈希运算
HMAC = Hash + 密钥
普通 Hash(如 SHA256)是这样的:

SHA256("hello") → 2cf24dba5fb0a...  (固定输出)

任何人都能算,无法证明来源。
HMAC 在此基础上加入密钥

HMAC_SHA256("hello", 密钥) → 完全不同的哈希值

SpringSecurity

先看依赖,再看代码结构。## 1. 依赖引入

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>

加入 spring-boot-starter-security 后,Spring Security 会立即生效,默认拦截所有请求,控制台打印一个随机密码用于 Basic Auth。


2. 整体架构

请求进入应用后,Spring Security 通过一条过滤器链拦截,JWT 方案的核心流程如下:---
image.png

3. 代码示例

UserDetailsService 实现 — 告诉 Security 怎么加载用户
认证请求

AuthenticationManager

调用 loadUserByUsername() ← UserDetailsService

返回 UserDetails

框架自动比对密码 + 验证状态

@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));

        return org.springframework.security.core.userdetails.User
            .withUsername(user.getUsername())
            .password(user.getPassword())           // 数据库里存的是加密后的密码
            .roles(user.getRole())                  // 自动加 ROLE_ 前缀
            .build();
    }
}

JwtUtil — JWT 生成与解析工具

@Component
public class JwtUtil {
    @Value("${jwt.secret}")
    private String secret;

    // 生成 token(登录成功时调用)
    public String generateToken(String username) {
        return Jwts.builder()
            .setSubject(username)
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24)) // 24h
            .signWith(Keys.hmacShaKeyFor(secret.getBytes()), SignatureAlgorithm.HS256)
            .compact();
    }

    // 从 token 中提取用户名
    public String extractUsername(String token) {
        return Jwts.parserBuilder()
            .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes()))
            .build()
            .parseClaimsJws(token)
            .getBody()
            .getSubject();
    }
}

JwtAuthFilter — 每个请求进来先过这里

ThreadLocal 讲解

每个 Thread 对象内部持有一个 threadLocals 是一个Map

// ThreadLocal.get() 源码
public T get() {
    Thread t = Thread.currentThread();          // 1. 拿到当前线程
    ThreadLocalMap map = getMap(t);            // 2. 取线程内部的Map
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);  // 3. 用 ThreadLocal实例自身 作key
        if (e != null) return (T) e.value;
    }
    return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;  // 就是 Thread 的成员变量,不是全局Map
}

另一个容易让人误解的是ThreadLocalMap.Entry e = map.getEntry(this);
我们使用时threadLocalUser.get(); 实际上内部就是用key去map中get value
通过this 实现无参函数变有参是之前没有见过的,值得记忆

static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();
threadLocalUser.set(user);
User u = threadLocalUser.get();

复习ThreadLocal是因为下面SecurityContextHolder基于ThreadLocal实现
SecurityContextHolder.getContext() 就是一个ThreadLocal
基于JWT的登陆验证是无状态的

public class SecurityContextHolder {

    // 本质就是一个 ThreadLocal<SecurityContext>
    private static final ThreadLocal<SecurityContext> contextHolder 
        = new ThreadLocal<>();

    public static SecurityContext getContext() {
        SecurityContext ctx = contextHolder.get();  // 就是 threadLocal.get()
        if (ctx == null) {
            ctx = createEmptyContext();
            contextHolder.set(ctx);
        }
        return ctx;
    }
}

JWT 只负责证明你是谁,权限数据要从数据库取最新的

@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest req,
                                    HttpServletResponse res,
                                    FilterChain chain) throws ServletException, IOException {

        String authHeader = req.getHeader("Authorization");

        // 没有 token,直接放行(后续路由规则会拦截需要认证的接口)
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            chain.doFilter(req, res);
            return;
        }

        String token = authHeader.substring(7);
        String username = jwtUtil.extractUsername(token);  // 解析失败会抛异常

        // SecurityContext 里还没有认证信息,才需要设置
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {// 检查是为了应对这一次访问出现的redirect,为了节省访问数据库的性能
            UserDetails userDetails = userDetailsService.loadUserByUsername(username); // 这里访问数据库

            UsernamePasswordAuthenticationToken authToken =
                new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities()
                );
            SecurityContextHolder.getContext().setAuthentication(authToken);
        }

        chain.doFilter(req, res);
    }
}

为什么要构造 authToken

SecurityContextHolder里必须放一个 Authentication 对象,框架才能做后续的权限判断。
三个参数的含义:

参数

作用

principal

userDetails

代表"当前是谁"

credentials

null

密码,JWT场景已认证,置null

authorities

getAuthorities()

权限列表,@PreAuthorize等注解靠它判断

// ❌ 两个参数的构造:isAuthenticated() = false,还没认证 new UsernamePasswordAuthenticationToken(principal, credentials) 
// ✅ 三个参数的构造:isAuthenticated() = true,已认证 new UsernamePasswordAuthenticationToken(principal, credentials, authorities) 

用三个参数的版本,框架才会认为你已经通过认证,不会再拦截你去登录页。

SecurityConfig — 把所有组件组装起来

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthFilter jwtAuthFilter;
    private final UserDetailsServiceImpl userDetailsService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(AbstractHttpConfigurer::disable)          // REST API 不需要 CSRF
            .sessionManagement(s -> s
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // JWT 无状态
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/users/register",
                                 "/api/v1/users/login").permitAll()
                .requestMatchers("/api/v1/upload/**").hasAnyRole("USER", "ADMIN")
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .authenticationProvider(authenticationProvider())
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();   // 密码必须 BCrypt 加密存储
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
        throws Exception {
        return config.getAuthenticationManager();
    }
}

@EnableWebSecurity
激活 Spring Security 的 Web 安全支持,让 Spring Boot 不再使用默认的安全配置,改为使用你定义的 SecurityFilterChain
SecurityFilterChain / filterChain(HttpSecurity http)
Spring Security 6 的核心配置方式(替代了旧版继承 WebSecurityConfigurerAdapter)。它返回一条过滤器链,HTTP 请求到达时会依次经过链上的各个过滤器。上面程序在这里完成三件事:

① CSRF 禁用

.csrf(AbstractHttpConfigurer::disable)

CSRF 防护依赖服务端 Session(在 Cookie 中存 token)。JWT 是无状态的,客户端每次在 Header 里带 token,不存在跨站请求伪造的攻击面,所以直接禁用。

② Session 策略设为无状态

.sessionCreationPolicy(SessionCreationPolicy.STATELESS)

告诉 Spring Security 不创建、不使用 HTTP Session,每次请求完全依赖 JWT 自带的认证信息。

③ 路径授权规则 authorizeHttpRequests 从上到下匹配,先匹配先生效

  • permitAll() — 白名单,注册/登录接口放行,不需要 token。

  • hasAnyRole("USER", "ADMIN") — 上传接口要求已登录用户。

  • hasRole("ADMIN") — 管理接口只有管理员能访问。

  • anyRequest().authenticated() — 其余所有请求都需要认证。

JwtAuthFilteraddFilterBefore

.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)

JwtAuthFilter 是自定义的过滤器(继承 OncePerRequestFilter)。把它插在 UsernamePasswordAuthenticationFilter 之前,意味着每个请求在走到用户名密码认证逻辑之前,就先由 JWT 过滤器解析 Header 中的 token、验证签名、把认证信息写入 SecurityContextHolder。后续的授权检查直接读这个上下文,不需要再问用户名密码。


AuthenticationProvider / DaoAuthenticationProvider

AuthenticationProvider 是认证执行者的抽象接口,Spring Security 支持多种实现(LDAP、OAuth 等)。DaoAuthenticationProvider 是最常用的实现,做两件事:

  • 调用 UserDetailsService.loadUserByUsername() 从数据库加载用户信息。

  • PasswordEncoder 对比请求中的明文密码和数据库里的哈希值。


AuthenticationManager

config.getAuthenticationManager()

AuthenticationManager 是认证的统一入口,内部持有多个 AuthenticationProvider,它把认证请求委托给合适的 Provider 处理。你把它暴露为 Bean,是为了让登录接口(/api/v1/users/login)中能注入它,调用 manager.authenticate(...) 执行实际的用户名+密码校验,成功后再生成 JWT 返回给客户端。


BCryptPasswordEncoder

BCrypt 是一种自适应哈希算法,内置随机 salt,即使同一个密码每次哈希结果也不同,天然防彩虹表攻击,并且可以调节计算轮数(strength)随硬件升级而加强。注意:数据库里存的密码必须用 BCrypt 加密,不能存明文或 MD5,否则 DaoAuthenticationProvider 的比对会失败。


整体请求流程

HTTP 请求
  → JwtAuthFilter(解析 token,写入 SecurityContext)
  → authorizeHttpRequests(路径权限检查)
  → 业务 Controller

登录流程(无 token):

/login 请求(permitAll 放行)
  → Controller 调用 AuthenticationManager.authenticate()
  → DaoAuthenticationProvider → UserDetailsService + BCrypt 校验
  → 认证成功 → 生成 JWT 返回客户端

4. 常用 API 速查

分类

API

作用

路由权限

.permitAll()

完全放行,匿名可访问

路由权限

.authenticated()

登录即可,不限角色

路由权限

.hasRole("ADMIN")

单角色,自动加 ROLE_ 前缀

路由权限

.hasAnyRole("A","B")

多角色之一即可

路由权限

.hasAuthority("READ")

精确匹配字符串,不加前缀

路由权限

.denyAll()

全部拒绝(常用于兜底)

会话管理

STATELESS

JWT 模式,不创建 Session

过滤器

.addFilterBefore(f, X.class)

在某过滤器之前插入自定义过滤器

密码

BCryptPasswordEncoder

生产环境标准加密方式

获取当前用户

SecurityContextHolder.getContext().getAuthentication()

任何地方获取登录用户

方法级权限

@PreAuthorize("hasRole('ADMIN')")

加在 Controller 方法上做细粒度控制


5. 方法级权限(进阶)

路由规则是粗粒度控制,如果需要在方法层面精确控制,在配置类上加 @EnableMethodSecurity,然后:

// 需要在 SecurityConfig 上加注解:@EnableMethodSecurity
@RestController
public class ArticleController {

    @GetMapping("/articles/{id}")
    @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
    public Article getArticle(@PathVariable Long id) { ... }

    @DeleteMapping("/articles/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    public void deleteArticle(@PathVariable Long id) { ... }
}

@PreAuthorize 支持 SpEL 表达式,可以引用方法参数(#id)和当前认证对象(authentication),实现比路由规则更细粒度的权限判断。

Spring Security Architecture

Architecture :: Spring Security
不涉及实现细节,这里只讲解架构
Filter 放行 - 可以修改Request/不放行 自己返回Response




区分两个container
Servlet container: Tomcat等
Spring container: ApplicationContext

加密的作用是即使数据库泄漏,无法还原原文用户的账号依然不会被盗
authenticate后authentication.priciple中从用户名变成UserDetails对象
一个Filter会被请求和响应通过两次

redis + jwt 让用户权限改变能够立即作用

如何使用SpringSecurity

SecurityFilterChain中的Bean,如下图
内部实现了大部分变化不大的逻辑,在产生变化的地方,通过@ConditionalOnMissingBean实现在ApplicationContext中查找是否有 我们自定义的实现特定接口的Bean,如果有,就调用我们的Bean,否则使用默认Bean
image-20211214144425527.png
下图是UsernamePasswordAuthenticationFilter内部逻辑,我们可以自定义实现UserDetailService的Bean取代默认的inMemoryUserDetailsManager,如果想要代替ProviderManager也是同理
image-20211214144425527.png

下一个问题是这些接口的含义是什么

一、认证(Authentication)相关组件

目的功能链条:获取用户名/密码 → 查询用户 → 比对密码 → 生成认证信息。

1. UserDetailsService

  • 职责:根据用户名加载用户信息(从数据库、内存、LDAP 等)。

  • 唯一方法UserDetails loadUserByUsername(String username) throws UsernameNotFoundException

  • 返回值 UserDetails:包含用户的所有认证相关信息(密码、权限、账户状态等)。

  • 框架如何使用

    • 在认证流程中,DaoAuthenticationProvider 会调用这个方法拿到 UserDetails

    • UserDetails 中取出 密码,与用户提交的密码进行比对。

    • UserDetails 中取出 权限集合 getAuthorities(),在认证成功后填充到 Authentication 对象中。

    • 还会检查 isAccountNonLocked()isEnabled() 等方法判断账户是否可用。

  • 什么时候需要自定义:默认的 InMemoryUserDetailsManager 只适用于测试。实际项目必须自己实现,从数据库或其它源查询用户。

2. UserDetails

  • 职责:代表一个完整的用户信息,供 Spring Security 内部使用。

  • 关键方法及返回值

    • String getPassword():返回存储的密码(已加密)。

    • String getUsername():返回用户名。

    • Collection<? extends GrantedAuthority> getAuthorities():返回该用户拥有的所有权限(如 "ROLE_ADMIN""user:read")。

    • boolean isAccountNonExpired() / isAccountNonLocked() / isCredentialsNonExpired() / isEnabled():账户状态标志。

  • 框架如何使用

    • 密码比对:将 getPassword() 与用户输入的密码(经过 PasswordEncoder 比对)进行匹配。

    • 授权决策:认证成功后,getAuthorities() 返回的权限会被存入 SecurityContext,后续访问控制(如 @PreAuthorize("hasRole('ADMIN')"))会基于这些权限进行判断。

    • 账户状态检查:任何一个状态方法返回 false,认证就会失败(抛出相应异常)。

  • 常用实现:Spring Security 提供的 org.springframework.security.core.userdetails.User(Builder 模式创建)。

3. GrantedAuthority

  • 职责:表示一个授予给用户的权限(通常是一个字符串)。

  • 唯一方法String getAuthority()

  • 框架如何使用:授权时,会调用 getAuthority() 获取权限字符串,与配置的访问规则(如 .hasAuthority("user:read"))进行比较。

  • 常用实现SimpleGrantedAuthority

4. PasswordEncoder

  • 职责:密码加密与比对。

  • 关键方法

    • String encode(CharSequence rawPassword):加密原始密码,用于注册时存储。

    • boolean matches(CharSequence rawPassword, String encodedPassword):比对原始密码与存储的密文。

  • 框架如何使用

    • 认证时,DaoAuthenticationProvider 调用 matches(rawPwd, userDetails.getPassword()),返回 true 则认证通过,否则失败。

  • 常用实现BCryptPasswordEncoder(推荐)。你也可以自己实现,但通常只需配置一个 Bean,框架会自动装配。

5. AuthenticationProvider

  • 职责:执行特定类型的认证逻辑(如用户名/密码认证、短信验证码认证)。

  • 关键方法

    • Authentication authenticate(Authentication authentication):核心认证逻辑,成功返回完整的 Authentication 对象,失败抛出异常。

    • boolean supports(Class<?> authentication):判断该 Provider 是否支持当前 Authentication 类型。

  • 返回值 Authentication:认证成功后,必须包含 principal(用户身份,通常是 UserDetails)、credentials(通常清空或置 null)、authorities(权限集合),且 isAuthenticated()true

  • 框架如何使用

    • ProviderManager 遍历所有 AuthenticationProvider,找到第一个 supports 返回 true 的 Provider。

    • 调用该 Provider 的 authenticate 方法。

    • 如果认证成功,返回的 Authentication 对象会被用于 SecurityContextHolder 存储。

  • 什么时候需要自定义:默认的 DaoAuthenticationProvider 已经支持用户名/密码认证。只有当你想实现其他认证方式(如短信、OAuth 自定义)时才需要实现该接口。

6. Authentication(接口)

  • 职责:代表一次认证请求或认证成功后的用户身份凭证。

  • 关键方法

    • Object getPrincipal():用户身份,通常认证后是 UserDetails 对象。

    • Object getCredentials():凭证(如密码),认证完成后通常会被清除。

    • Collection<? extends GrantedAuthority> getAuthorities():当前认证用户的权限。

    • boolean isAuthenticated():是否认证成功。

  • 框架如何使用

    • 认证成功后的 Authentication 对象会被存入 SecurityContextHolder.getContext().setAuthentication(...)

    • 在后续的请求中(同一个会话),通过 SecurityContextHolder.getContext().getAuthentication() 获取用户信息和权限,用于授权判断。

  • 常用实现UsernamePasswordAuthenticationToken(认证前后都使用它)。

维度

参数(传入的 Authentication

返回值(认证成功后返回的 Authentication

isAuthenticated()

永远为 false

永远为 true

getCredentials()

包含原始凭证(如明文密码、验证码等)

通常被清空或设为 null(出于安全考虑)

getPrincipal()

通常是用户名(String 类型)

通常是完整的 UserDetails 对象(包含权限、账户状态等)

getAuthorities()

通常为空集合(Collections.emptyList()

包含用户的所有权限(GrantedAuthority 集合)

用途

作为“认证请求”携带凭证

作为“认证结果”供后续授权使用


二、授权(Authorization)相关组件

授权目标是你有什么权限(What you are allowed to do)。在 Spring Security 中,授权通常发生在 FilterSecurityInterceptor 或方法级别的 @PreAuthorize 中。

7. AccessDecisionManager

  • 职责:根据当前用户的 Authentication 和请求所需的 ConfigAttribute(如角色要求),决定是否允许访问。

  • 关键方法void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)

    • 如果投票通过,方法正常返回;否则抛出 AccessDeniedException

  • 框架如何使用:在访问资源前,FilterSecurityInterceptor 会调用 AccessDecisionManager.decide(...),根据投票结果决定是否放行。

  • 默认实现AffirmativeBased(只要有一个投票器同意就放行)。通常不需要自定义,除非你有特殊的投票逻辑。

8. SecurityContextRepository

  • 职责:在请求之间保存和加载 SecurityContext(默认基于 Session)。

  • 关键方法

    • SecurityContext loadContext(HttpRequestResponseHolder holder):从存储介质中加载。

    • void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response):保存上下文。

  • 框架如何使用:在 SecurityContextPersistenceFilter 中,先调用 loadContext 获取上下文,请求结束时调用 saveContext 保存。

  • 什么时候需要自定义:例如你想将 SecurityContext 存储在 Redis 或 JWT 中时,可以实现该接口。


三、过滤器链(Filter)层面的扩展

9. WebSecurityConfigurerAdapter(已过时,但逻辑仍适用)

  • 职责:配置 HttpSecurity,定义哪些 URL 需要保护、使用什么认证方式等。

  • 常用覆盖方法

    • configure(HttpSecurity http):配置路径规则、表单登录、退出、CSRF 等。

  • 框架如何使用:在构建 SecurityFilterChain 时,会调用你的配置。

注意:Spring Security 5.8+ 推荐使用 SecurityFilterChain Bean 代替继承适配器,但原理相同。


四、总结:如何决定实现哪个接口?

你的需求

需要实现的接口/类

从数据库读取用户

实现 UserDetailsService

定义用户数据模型(包含密码、权限、账户状态)

实现 UserDetails(或使用框架提供的 User 类)

自定义密码加密方式(如国密算法)

实现 PasswordEncoder

实现短信验证码登录

实现 AuthenticationProvider

在代码中获取当前用户信息

直接使用 SecurityContextHolder,不需要实现接口

改变权限存储方式(如从接口动态获取)

UserDetailsService 中填充 GrantedAuthority

在 Session 之外保存 SecurityContext(如 JWT)

实现 SecurityContextRepository

自定义授权逻辑(如基于用户时间、IP)

实现 AccessDecisionManagerPermissionEvaluator

在 Servlet 容器内部使用 RequestDispatcher 进行 forward (转发)或 include (包含)时,普通的 Filter可能被调用多次

OncePerRequestFilter 用来实现JWT无状态认证中的UsernamePasswordAuthenticationFilter,验证过一次后Authentication对象会存入SpringSecurityContext供后面Filter使用,重复校验无效且降低效率;
如何使用: 假设我们用JwtAuthenticationFilter 实现 OncePerRequestFilter 那么这样配置

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // ... 其他配置,如 authorizeRequests 等
            .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
            // 或者 .addFilterAfter(...)
            // 或者 .addFilterAt(...)
            .build();
        return http.build();
    }

    // 自定义过滤器,继承 OncePerRequestFilter
    public static class JwtAuthenticationFilter extends OncePerRequestFilter {
        @Override
        protected void doFilterInternal(HttpServletRequest request, 
                                        HttpServletResponse response, 
                                        FilterChain chain) throws ServletException, IOException {
            // ... 你的 JWT 认证逻辑
            chain.doFilter(request, response); // 继续执行过滤器链
        }
    }
}

SpringSecurity6 | 核心过滤器-腾讯云开发者社区-腾讯云
在Filter层,通过HttpSecurit

  • 关闭某些不需要的过滤器

  • 配置默认过滤器的行为(如登录页、成功处理器等)

  • 增加自己的 Filter(包括 OncePerRequestFilter 子类)到过滤器链的指定位置

细节: 封装权限到UserDetail时,我们的GrantedAuthority
用户的身份,身份对应的权限写在数据库,随用随查

在Utils中自己实现


Comment