学习一个框架
功能 - 解决的问题
如何使用 - 配置
我做的配置如何影响功能
登录校验流程
未登录: 前端发现localstorage中没有token redirect到登录页面
登录过程: 前端发送用户名和密码的请求
后端在数据库中比对用户名和密码
正确 -> 用用户名和密码登生成jwt
返回jwt
有jwt后请求其他接口,接口解析jwt中的用户名和密码,进行权限判断
Authentication 接口代表正在认证的用户
UserDetail封装了用户信息
RBAC
What:
Role-Based Access Control 基于角色的访问控制
角色继承的 RBAC 模型: 上层角色继承下层角色的所有权限,并且可以额外拥有其他权限。
RBAC 由四个核心要素构成:
用户(User) — 系统的实际使用者
角色(Role) — 一组权限的集合,如"管理员"、"编辑"、"访客"
权限(Permission) — 对某个资源的某种操作,如"读取订单"、"删除用户"
资源(Resource) — 被保护的系统对象,如文件、数据库表、API 接口

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 是否过期
注意: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 方案的核心流程如下:---
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 对象,框架才能做后续的权限判断。
三个参数的含义:
// ❌ 两个参数的构造: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()— 其余所有请求都需要认证。
JwtAuthFilter 与 addFilterBefore
.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 速查
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
下图是UsernamePasswordAuthenticationFilter内部逻辑,我们可以自定义实现UserDetailService的Bean取代默认的inMemoryUserDetailsManager,如果想要代替ProviderManager也是同理
下一个问题是这些接口的含义是什么
一、认证(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(认证前后都使用它)。
二、授权(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+ 推荐使用
SecurityFilterChainBean 代替继承适配器,但原理相同。
四、总结:如何决定实现哪个接口?
在 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中自己实现