https://github.com/Cxhahalala/SpringSecurity-and-Jwt
背景与目标
什么是 JWT?
JSON Web Token (JWT) 是一种开放标准(RFC 7519),主要用于:
- 身份认证:如登录后生成 JWT,代替传统的 Session 认证。
- 信息交换:传递安全、不可篡改的信息。
项目目标
通过 Spring Security 和 JWT: - 实现登录认证,返回 JWT Token。
- 保护指定接口,只有携带合法 JWT 的用户可访问。
- 支持权限验证(基于用户角色),实现细粒度的资源控制。
JWT与SpringSecurity工作流程
SpringBoot容器运行在Tomcat服务器中,当接收到Http Requests时,会由JwtAuthFilter判断,request是否带有Jwt,或者根据Jwt解析出用户信息后,发现数据库中并不存在此用户,那么会直接返回Http 403状态。
若满足,Validate JWT会调用JwtService判断当前Jwt是否合法,例如,Jwt可能会出现过期,或者被篡改的情况。
满足上述情况,才会将相应的http request发送给DispatcherServlet,调用相关Controller之后向前端返回结果。
Jwt与Spring Security实现认证授权的整体逻辑:
1,客户端发送http requests,由JwtAuthFilter(Jwt认证过滤器)判断,请求是否存在Jwt,若不存在,证明客户端没有携带认证信息,会返回http 403错误;若存在,则会判断Jwt的合法性,如果Jwt的拥有者存在于数据库并且未被认证,那么更新SecurityContextHolder安全上下文,将请求发送给DispachterServlet。
2.Spring Security可以指定哪些url是收到保护的,即需要认证用户的身份和权限才能访问。
整体认证授权流程:
客户端发送请求
- 客户端在登录成功后会获得一个有效的 JWT 令牌。
- 每次客户端请求时,会在 Authorization 请求头中携带 JWT(通常以 Bearer
格式)。
JwtAuthenticationFilter 处理请求 - Spring Security 的过滤器链首先会经过自定义的 JwtAuthenticationFilter。
- JwtAuthenticationFilter 的主要职责:
- 检查 Authorization 请求头是否存在。
- 如果存在 JWT:
- 验证 JWT 的合法性(签名、过期时间等)。
- 从 JWT 中提取用户信息(如 username 或 email)。
- 判断用户是否已认证(SecurityContext 是否为空)。
- 如果用户未认证:
- 查询数据库确认用户是否存在。
- 将用户信息(通常是 UserDetails)放入 SecurityContextHolder,以便后续的权限校验。
DispatcherServlet 接管请求
- 如果 JwtAuthenticationFilter 验证成功,请求会被转发到 DispatcherServlet,然后进入控制器(@Controller)。
- 在进入控制器前,Spring Security 的拦截器会根据配置检查用户是否有权限访问该 URL。
权限控制 - Spring Security 根据配置的 URL 权限规则(如 .authorizeHttpRequests)或方法级权限规则(如 @PreAuthorize),判断当前用户是否有权限访问目标资源。
- 如果用户权限不足,则返回 HTTP 403 错误。
- 客户端发起 HTTP 请求
|
v- JwtAuthenticationFilter 检查请求头是否存在 JWT
|
+-- 如果不存在 JWT,直接返回 HTTP 403
|
+-- 如果存在 JWT,验证签名和提取用户信息
|
v- 如果用户未认证:
- 从数据库加载用户信息
- 更新 SecurityContextHolder
|
v
- DispatcherServlet 处理请求
|
v- Spring Security 检查 URL 或方法权限
|
+-- 如果权限不足,返回 HTTP 403
|
+-- 如果权限足够,调用目标方法
实现UserDetails接口
在 Spring Security 中,用户认证和授权的核心依赖于两个接口:UserDetails 和 UserDetailsService。
1.UserDetails
UserDetails 是 Spring Security 提供的用户信息接口,用于存储和提供用户的基本信息。
Spring Security 通过 UserDetails 提供的方法获取用户的用户名、密码以及权限信息(如角色)。
我们的用户类需要实现 UserDetails 接口,告诉 Spring Security 用户的基本信息,例如用户名、密码、是否启用、是否过期等。
实现这个接口后,Spring Security 可以直接使用用户类中的信息来完成认证和权限管理。
2.UserDetailsService
UserDetailsService 是 Spring Security 提供的用户加载接口,主要用于从数据库或其他存储介质中查询用户信息。
它包含一个方法 loadUserByUsername(String username),用于根据用户名加载用户。
我们需要实现 UserDetailsService 接口,并定义自定义的用户查询逻辑,例如从数据库中查询用户及其权限信息。
实现 loadUserByUsername 后,Spring Security 会在用户登录时调用这个方法加载用户信息,并将其与用户输入的凭据(如密码)进行验证。
我们的User类即用户类,因此先实现UserDetails接口。
package com.example.security.user;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
@Data
@Builder
// @builder,为类生成 Builder 模式,让我们可以更加优雅和灵活地构造对象。
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "_user")
// 实现UserDetails的相关接口
public class User implements UserDetails {
@Id
@GeneratedValue
private Integer id;
//自动生成主键
private String firstname;
protected String lastname;
private String email;
private String password;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getUsername() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}
创建权限枚举类
本枚举类中只有user,admin两种权限
package com.example.security.user;
public enum Role {
user,
admin;
}
User实体类中加入权限
@Enumerated(EnumType.STRING)
private Role role;
重写实现的UserDetails接口的方法
getAuthorities()
定义:返回用户的权限信息(GrantedAuthority 的集合)。
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(role.name()));
}
将用户的 role 转换为一个 GrantedAuthority 对象,并返回。
role.name() 获取的是枚举值的名称,例如 "ADMIN" 或 "USER"。
SimpleGrantedAuthority 是 Spring Security 中一个常用的实现类,用于表示用户的权限或角色。
授权时,Spring Security 会根据这些权限(如 "ROLE_ADMIN")来验证用户是否具备访问特定资源的权限。
getUsername()
定义:返回用户的唯一标识符(用户名)。
@Override
public String getUsername() {
return email;
}
在本例中,用户名设置为用户的 email。
Spring Security 会通过这个方法获取用户的用户名,用于身份验证(如登录时提供的用户名与数据库中的用户名匹配)。
虽然方法名是 getUsername(),但它返回的可以是任何唯一标识用户的字段,例如邮箱或手机号。
isAccountNonExpired()
定义:检查账户是否已过期。
@Override
public boolean isAccountNonExpired() {
return true;
}
返回 true 表示账户未过期,可以正常使用。
如果返回 false,Spring Security 会禁止该账户登录。
可通过数据库字段记录账户的过期时间,在这里动态返回 true 或 false。
isAccountNonLocked()
定义:检查账户是否被锁定。
@Override
public boolean isAccountNonLocked() {
return true;
}
返回 true 表示账户未被锁定。
返回 false 时,Spring Security 会拒绝用户登录。
可以在数据库中设置一个字段(如 is_locked),用于标记用户账户是否被锁定。
isCredentialsNonExpired()
检查用户的凭据(例如密码)是否过期。
@Override
public boolean isCredentialsNonExpired() {
return true;
}
返回 true 表示用户凭据未过期。
如果返回 false,Spring Security 会拒绝用户登录。
在增强安全性的场景中,可以设置密码的有效期(如 90 天)。
isEnabled()
定义:检查账户是否被启用。
@Override
public boolean isEnabled() {
return true;
}
返回 true 表示账户被启用。
返回 false 时,Spring Security 会禁止该账户登录。
数据库中可以有一个字段(如 is_enabled),标记用户是否被管理员禁用。
总结
getAuthorities():决定用户拥有哪些权限(如角色)。
getUsername():返回用户的唯一标识(用于登录认证)。
isAccountNonExpired()、isAccountNonLocked()、isCredentialsNonExpired()、isEnabled():决定用户账户是否可以正常使用(比如账户是否过期、是否被锁定等)。
注意
当实现 UserDetails 接口的方法时,如果实体类中没有 password 字段,可以通过自定义的方式实现 getPassword() 方法。例如,可以从其他字段或数据源中获取密码。因此,实现的具体方法数量和内容可以根据实体类的字段动态调整。
例如,当我把password字段修改部分后,则会立即报错
而之前没有报错是因为lombok根据password字段自动为我们生成了getPassword()方法
创建Jwt操作类
客户端请求首先要经过Jwt过滤器类,需要对Jwt令牌进行操作,因此需要先创建Jwt操作类。
对应架构图中的JwtService
引入相关依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
什么是Jwt Token
在对jwt进行操作之前,首先了解什么是Jwt Token
JWT (JSON Web Token) 是一种开放标准(RFC 7519),用于在各方之间作为 JSON 对象安全地传输信息。它通常用于身份认证和信息交换。JWT 的主要特点是信息可以被验证和信任,因为它是使用数字签名进行验证的。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywiaXNzdWVyIjoiYXBpLmV4YW1wbGUuY29tIiwiaWF0IjoxNjY4MTU5MjAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT以.分隔,分为三个部分,Header,PAYLOAD,SIGNATURE
Header:
typ: 声明令牌的类型,通常为 JWT。
alg: 声明签名的加密算法,比如 HMAC SHA256 (HS256) 或 RSA。
{
"alg": "HS256",
"typ": "JWT"
}
PAYLOAD
Payload 是 JWT 的数据部分,包含实际需要传输的信息。它由多个 Claim(声明) 组成。
标准声明(可选):
iss(Issuer):发布者。
sub(Subject):主题。
aud(Audience):受众。
exp(Expiration Time):过期时间。
iat(Issued At):签发时间。
nbf(Not Before):生效时间。
自定义声明: 开发者可以自定义 Payload 中的键值对。例如用户 ID、角色等:
{
"userId": 123,
"role": "admin",
"iat": 1668159200
}
SIGNATURE
Signature 是 JWT 的安全保证部分,用于确保数据的完整性和真实性。
生成签名的过程:
将 Header 和 Payload 使用 Base64Url 编码后,通过 . 拼接
base64UrlHeader + "." + base64UrlPayload
使用指定的算法(如 HS256)和秘钥对上述数据进行签名:
HMACSHA256(base64UrlHeader + "." + base64UrlPayload,secret)
签名的作用是防止数据被篡改。如果数据被修改,签名校验将失败。
密钥可以是普通字符串,也可以是生成的随机随机密钥。
Jwt网址
package com.example.security.config;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Service
public class JwtService {
// 密钥
private static final String SECRET_KEY = "5v8PsJz6G+Jk69fxWkMsXtbGyO1HTK+7OdjwNWUeeHE="; // 确保这是Base64字符串
// 获取用户名
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject); // 提取主题作为用户名
}
// 获取过期时间
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
// 获取单个的Claim
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
// 提取Claims(有效荷载)
private Claims extractAllClaims(String token) {
return Jwts
.parserBuilder() // 构建解析器
.setSigningKey(getSignKey()) // 设置签名密钥
.build() // 构建解析器对象
.parseClaimsJws(token) // 解析JWT,验证签名,并提取Claims
.getBody(); // 提取Claims 对象(有效荷载)
}
// 获取签名密钥
private Key getSignKey() {
byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY); // Base64解码
return Keys.hmacShaKeyFor(keyBytes); // 生成HS256签名密钥
}
// 生成JWT
public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
System.out.println("Generating token for user: " + userDetails.getUsername());
return Jwts
.builder()
.setClaims(extraClaims) // 设置额外声明
.setSubject(userDetails.getUsername()) // 设置主题(用户名)
.setIssuedAt(new Date(System.currentTimeMillis())) // Token 生效时间
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60)) // Token 失效时间,以毫秒为单位
.signWith(getSignKey(), SignatureAlgorithm.HS256) // 签名,使用HS256算法加密
.compact();
}
// 生成不包含额外声明的JWT
public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}
// 判断Token是否合法
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token); // 使用equals比较字符串
}
// 判断Token是否过期
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
}
在JwtService类中,我们创建了对Jwt操作的相关方法,因此,已经可以从Jwt中提取userEmail,
在Jwt身份验证过滤器类中,就可以对userEmail进行相关操作
SpringSecurity为我们提供了UserDetailsService接口,我们只需要实现这个接口中的loadUserByUsername即可。
实现UserDetailsService接口
新建一个配置类,在这里注入一些需要Spring容器管理的Bean。
ApplicationConfig.java
package com.example.security.config;
import com.example.security.user.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
@Configuration
@RequiredArgsConstructor
public class ApplicationConfig {
private final UserRepository userRepository;
@Bean
public UserDetailsService userDetailsService (){
return new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//通过Spring Data Jpa实现获取用户逻辑。
return userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found with email: " + username));
}
};
}
}
创建Jwt过滤器类
从Jwt架构图中可以看出,拦截http requests的时候第一步需要JwtAuthFilterA,因此,需要创建身份验证过滤器类。
我们需要身份验证过滤器类对每一次的http请求都做验证,因此,需要将身份验证过滤器类拓展为"每个请求一次"过滤器的类。
OncePerRequestFilter 是一个抽象类,用于确保过滤器在每个请求中只被执行一次,避免重复执行。
package com.example.security.config;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
// 生成一个构造函数,初始化所有标注了 final 或 @NonNull 的字段。
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
//继承OncePerRequestFilter的方法,确保每个过滤器在每个请求中只执行一次
/**
*
* @param request http请求
* @param response 响应
* @param filterChain 过滤器链
*/
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
}
}
doFilterInternal 方法:
此方法是 OncePerRequestFilter 的核心方法,开发者需要在这里实现自定义的过滤逻辑。
在请求进入 Spring Security 的过滤链时,Spring 会调用此方法。
通常,doFilterInternal 方法会实现以下功能:
从请求头中提取 JWT Token。
验证 Token 的有效性。
如果 Token 合法,将认证信息存储在 SecurityContext 中,使后续的 Spring Security 组件可以使用。
调用 filterChain.doFilter(request, response),将请求交给下一个过滤器。
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
// 该字段通常携带用户的认证信息
final String authHeader = request.getHeader("Authorization");
final String jwt;
//如果不存在Authorization字段,则客户端请求没有携带认证信息
//Bearer 是 JWT 的标准认证头格式,用于标识后续的内容是一个 Token。
//如果不符合该格式,则说明请求头中没有合法的 JWT。
//如果 authHeader 为空或格式不正确,直接调用过滤器链的 doFilter 方法,将请求传递给下一个过滤器。
//返回结束当前过滤器的执行,不再处理当前请求的 Token 验证逻辑。
if(authHeader == null || !authHeader.startsWith("Bearer ")){
filterChain.doFilter(request,response);
return;
}
}
现在已经从请求中得到了Jwt,就可以从Jwt中解析用户信息了。
如果userEmail不为null,并且当前线程尚未设置认证信息,进行后续操作
获取当前线程的认证信息需要使用SecurityContextHolder, 下面是对其的解释。
SecurityContextHolder 的作用:
SpringSecurity核心架构的一部分,涉及当前用户认证信息的存储和访问。
核心功能
- 存储认证信息:SecurityContextHolder 用于存储当前线程的安全上下文(SecurityContext)。
- 获取认证信息:提供便捷方法,获取当前用户的身份认证信息(Authentication 对象)。
安全上下文的内容
SecurityContext是存储 用户认证信息的核心对象,主要包括
- Authentication 对象:表示当前用户的认证状态、身份信息和权限。
包含:用户名、角色、权限、凭据等信息。
示例:UsernamePasswordAuthenticationToken 是常用实现类。- 详细信息(Details):通常包括请求的相关细节,如 IP 地址、会话 ID。
>存储在 WebAuthenticationDetails 中。
线程隔离性
Spring Security 使用 ThreadLocal 存储 SecurityContext,确保每个线程都有独立的安全上下文,多线程环境下,认证信息不会互相干扰,若存在异步调用,则需要手动传递SecurityContext。
SecurityContextHolder的方法详解
获取认证信息
getContext():获取当前线程的 SecurityContext。
getAuthentication():从 SecurityContext 获取当前用户的 Authentication 对象。
如果返回 null,说明当前用户尚未认证或未设置认证信息。如果返回有效对象,说明用户已通过认证。
设置认证信息
setContext(SecurityContext context):设置当前线程的安全上下文。
setAuthentication(Authentication auth):通过 SecurityContextHolder.getContext().setAuthentication() 设置当前用户的认证信息,通常由认证过滤器(如 JwtAuthenticationFilter)完成。
示例代码
// 获取当前用户的 Authentication 对象
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
String username = authentication.getName(); // 获取用户名
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); // 获取权限
System.out.println("当前用户: " + username);
System.out.println("权限: " + authorities);
}
// 设置当前用户的认证信息
// UsernamePasswordAuthenticationToken是Authentication接口的实现类
Authentication auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
典型应用场景
- 获取当前用户信息
在Controller或Service层获取当前登录用户的用户名或权限
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated()) {
String username = auth.getName(); // 当前用户名
System.out.println("当前用户: " + username);
}
- 权限检查
使用 SecurityContextHolder 检查用户是否拥有指定权限:
if (SecurityContextHolder.getContext().getAuthentication().getAuthorities()
.contains(new SimpleGrantedAuthority("ROLE_ADMIN"))) {
System.out.println("当前用户是管理员");
}
- 手动设置认证信息
在测试场景或特殊业务场景中,手动设置当前用户
Authentication auth = new UsernamePasswordAuthenticationToken("testUser", null, List.of(new SimpleGrantedAuthority("ROLE_USER")));
SecurityContextHolder.getContext().setAuthentication(auth);
- 异步线程传递认证信息
在异步任务中,需要手动传递 SecurityContext
SecurityContext context = SecurityContextHolder.getContext();
CompletableFuture.runAsync(() -> {
SecurityContextHolder.setContext(context); // 手动设置
// 执行需要认证信息的任务
});
注意事项
- 认证信息丢失问题
异步任务执行时,ThreadLocal 不会自动传播上下文,需要手动设置。
解决方案:使用 Spring 提供的 DelegatingSecurityContextExecutor 或 DelegatingSecurityContextCallable。
- 清空认证信息
当用户注销时,需要清空 SecurityContextHolder:
SecurityContextHolder.clearContext();
- 避免直接修改 ThreadLocal
推荐使用 SecurityContextHolder 提供的方法进行上下文的设置和清空,而非直接操作 ThreadLocal。
总结
SecurityContextHolder 是 Spring Security 提供的核心工具,用于在应用程序的任何地方便捷地访问用户认证信息。无论是权限验证、用户信息获取,还是特殊场景下的认证状态管理,都离不开它。通过理解其机制和应用场景,可以更高效地实现安全相关功能。
了解完SecurityContextHolder的用法后,就可以读懂判断语句的内容。
若userEmail存在,并且当前线程没有认证信息,那么就加载这个userEmail的用户信息。
jwt = authHeader.substring(7);
userEmail=jwtService.extractUsername(jwt);
if(userEmail !=null && SecurityContextHolder.getContext().getAuthentication() == null){
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
}
现在已经获取到了userDetails和Jwt,此时就可以判断Jwt是否合法,如果合法,那么就更新安全上下文并将请求发送到调度程序DispatcherServlet
梳理流程: 如果获取到了userEmail并且SecurityContextHolder中没有认证信息,那么就通过UserDetailsService从数据库中获得当前userEmail对应用户信息userDetails,如果用户和令牌有效,创建UsernamePasswordAuthenticationToken对象,将用户详细信息,凭据和权限作为参数传递,然后通过请求拓展或更新此身份验证令牌,然后更新。
需要注意的是,Jwt过滤完成后,仍然需要其它过滤器执行。所以不要忘记filterChain.doFilter(request,response);
userEmail=jwtService.extractUsername(jwt);
// 如果userEmail存在,并且未被认证,
if(userEmail !=null && SecurityContextHolder.getContext().getAuthentication() == null){
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
if(jwtService.isTokenValid(jwt,userDetails)){
//使用UsernamePasswordAuthenticationToken对象更新安全上下文
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
//更新SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request,response);
此时,我们的Jwt过滤器类就已经编写完成,下面是完整的代码
package com.example.security.config;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
// 生成一个构造函数,初始化所有标注了 final 或 @NonNull 的字段。
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
//继承OncePerRequestFilter的方法
/**
*
* @param request http请求
* @param response 响应
* @param filterChain 过滤器链
*/
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
// 该字段通常携带用户的认证信息
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String userEmail;
//如果不存在Authorization字段,则客户端请求没有携带认证信息
//Bearer 是 JWT 的标准认证头格式,用于标识后续的内容是一个 Token。
//如果不符合该格式,则说明请求头中没有合法的 JWT。
//如果 authHeader 为空或格式不正确,直接调用过滤器链的 doFilter 方法,将请求传递给下一个过滤器。
//返回结束当前过滤器的执行,不再处理当前请求的 Token 验证逻辑。
if(authHeader == null || !authHeader.startsWith("Bearer ")){
filterChain.doFilter(request,response);
return;
}
jwt = authHeader.substring(7);
userEmail=jwtService.extractUsername(jwt);
// 如果userEmail存在,并且未被认证,
if(userEmail !=null && SecurityContextHolder.getContext().getAuthentication() == null){
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
if(jwtService.isTokenValid(jwt,userDetails)){
//使用UsernamePasswordAuthenticationToken对象更新安全上下文
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
//更新SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request,response);
}
}
配置SpringSecurity
配置url鉴权配置
已经创建了JwtAuthenticationFilter.java,但此时这个过滤器类尚未生效。
在编写SpringSecurity的配置类的时候,除了可以配置验证规则,也可以使我们的Jwt过滤器类生效。
我们可以指定一些url,这些路径不需要任何的认证,也就是说无论是否有token,都可以正常访问,通常用于登录等不需要保护的url。
除此之外的url,则都需要认证,并且还可以根据用户权限来决定用户是否可以请求这些url。
package com.example.security.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
// 启用Spring Security的默认配置
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
http
.csrf(csrf -> csrf.disable()) //禁用csrf
// 配置请求授权规则
.authorizeHttpRequests(auth->auth
.requestMatchers("").permitAll() //白名单url不需要验证
.anyRequest().authenticated() //其它路径需要认证
)
//配置会话管理策略为无状态
.sessionManagement(session ->session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
//配置自定义认证提供者
.authenticationProvider(authenticationProvider)
//添加自定义的Jwt认证过滤器,并放在UsernamePasswordAuthenticationFilter之前
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
完整的配置
package com.example.security.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
http
.csrf(csrf -> csrf.disable()) //禁用csrf
// 配置请求授权规则
.authorizeHttpRequests(auth->auth
.requestMatchers("/api/v1/auth/**").permitAll() //白名单url不需要验证
// .requestMatchers("/api/v1/demo-controller").hasRole("admin") //此路径会先验证Jwt Token的合法性,然后再判断用户是否有指定的权限。
.requestMatchers("/api/v1/demo-controller").hasRole("admin") //此路径会先验证Jwt Token的合法性,然后再判断用户是否有指定的权限。
.anyRequest().authenticated() //其它路径需要认证
)
//配置会话管理策略为无状态
.sessionManagement(session ->session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
//配置自定义认证提供者
.authenticationProvider(authenticationProvider)
//添加自定义的Jwt认证过滤器,并放在UsernamePasswordAuthenticationFilter之前
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
在配置Spring Security的时候,我们需要再向Spring容器中加入两个Bean,分别是AuthenticationProvider和AuthenticationManager。
AuthenticationProvider:SpringSecurity内置的身份验证提供者,使用userDetailService加载用户信息后并验证用户的密码
AuthenticationManager:SpringSecurity内置的身份验证管理者,管理和协调多个 AuthenticationProvider的身份验证过程。
需要在ApplicationConfig.java中注入这两个Bean
完整的ApplicationConfig.java
package com.example.security.config;
import com.example.security.user.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@RequiredArgsConstructor
public class ApplicationConfig {
private final UserRepository userRepository;
@Bean
public UserDetailsService userDetailsService (){
// return username -> userRepository.findByEmail(username)
// .orElseThrow(() -> new UsernameNotFoundException("User not found with email: " + username));
return username -> {
System.out.println("Loading user by username: " + username); // 打印请求的用户名
return userRepository.findByEmail(username)
.orElseThrow(() -> {
System.out.println("User not found in database for username: " + username);
return new UsernameNotFoundException("User not found with email: " + username);
});
};
}
// 身份验证提供者,使用 UserDetailsService 加载用户信息,并用 PasswordEncoder 验证用户的密码
@Bean
public AuthenticationProvider authenticationProvider(){
// SpringSecurity内置的身份验证提供者
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
//身份验证管理者,管理和协调多个 AuthenticationProvider 的身份验证过程。
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
//负责对密码进行加密和验证匹配。
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
配置方法级别的鉴权
Spring Security可以针对方法级别得到访问权限控制而不依赖URL,在控制器或服务方法层次进行权限校验。
工作机制:
1,SecurityContext 中存储 Authentication 对象
-
每个请求经过认证后,Spring Security 会将认证结果存储在当前线程的 SecurityContext 中。
-
Authentication 对象中包含当前用户的认证信息(如用户名、角色、权限等),具体存储在 Authentication.getAuthorities() 方法中。
2,方法调用时检查权限 -
当带有 @PreAuthorize("hasRole('ROLE_ADMIN')") 的方法被调用时,Spring Security 会检查 SecurityContextHolder.getContext().getAuthentication() 中的 Authentication 对象。
-
它会从 Authentication.getAuthorities() 中获取用户的所有权限,并与 ROLE_ADMIN 进行匹配。
-
hasRole('ROLE_ADMIN') 等价于检查 GrantedAuthority 是否包含 "ROLE_ADMIN"。
-
Spring Security 的 GrantedAuthority 接口是用来表示用户的权限(通常是角色字符串,如 ROLE_ADMIN 或 ROLE_USER)。
-
如果 Authentication.getAuthorities() 中包含 ROLE_ADMIN,方法调用被允许。
-
如果不包含,则抛出 AccessDeniedException,方法调用被拒绝。
eg
@Service
public class DemoService {
@PreAuthorize("hasRole('ADMIN')")
public void adminOnlyMethod() {
System.out.println("This method can only be accessed by ADMIN role");
}
@PreAuthorize("hasRole('USER')")
public void userMethod() {
System.out.println("This method can be accessed by USER role");
}
}
启用SpringSecurity的方法级别的权限控制,需要使用 **@EnableGlobalMethodSecurity **注解,
url权限控制 vs 方法级别权限控制
相关权限控制注解
@PreAuthorize
功能:
在方法执行 之前 检查权限。
使用 Spring EL 表达式来定义复杂的权限逻辑。
用法:
可以基于角色、权限或其他表达式进行访问控制。
@Service
public class DemoService {
@PreAuthorize("hasRole('ADMIN')") // 用户必须具有 ADMIN 角色
public void adminOnly() {
System.out.println("Admin only method");
}
@PreAuthorize("hasAuthority('READ_PRIVILEGE')") // 用户必须具有特定权限
public void readPrivilegeOnly() {
System.out.println("Read privilege only method");
}
@PreAuthorize("#id == authentication.principal.id") // 用户只能访问自己的资源
public void accessOwnResource(Long id) {
System.out.println("Accessing own resource");
}
}
- @PostAuthorize
功能:
在方法执行 之后 检查权限。
通常用于检查返回值是否符合权限要求。
用法:
返回值可以作为权限检查的一部分。
@Service
public class DemoService {
@PostAuthorize("returnObject.owner == authentication.name") // 返回对象的 owner 必须是当前用户
public Resource getResource(Long id) {
Resource resource = resourceRepository.findById(id);
return resource;
}
}
@Secured
功能:
限制方法访问,仅允许具有指定角色的用户访问。
使用简单,直接指定角色列表。
用法:
配置静态角色列表,不支持 Spring EL 表达式。
@Service
public class DemoService {
@Secured("ROLE_ADMIN") // 仅具有 ROLE_ADMIN 的用户可以访问
public void adminOnly() {
System.out.println("Admin only method");
}
@Secured({"ROLE_USER", "ROLE_ADMIN"}) // 具有 ROLE_USER 或 ROLE_ADMIN 的用户可以访问
public void userOrAdmin() {
System.out.println("User or Admin method");
}
}
@RolesAllowed
功能:
类似于 @Secured,用于限制访问的角色。
基于 JSR-250 标准,推荐使用。
用法:
指定角色列表,不支持复杂表达式。
@Service
public class DemoService {
@RolesAllowed("ROLE_ADMIN") // 仅 ROLE_ADMIN 的用户可以访问
public void adminOnly() {
System.out.println("Admin only method");
}
@RolesAllowed({"ROLE_USER", "ROLE_ADMIN"}) // 具有 ROLE_USER 或 ROLE_ADMIN 的用户可以访问
public void userOrAdmin() {
System.out.println("User or Admin method");
}
}
@EnableGlobalMethodSecurity
要使用上述注解,需要在 Spring Security 配置中启用方法级别的权限控制:
//prePostEnabled:启用 @PreAuthorize 和 @PostAuthorize。
//securedEnabled:启用 @Secured。
//jsr250Enabled:启用 @RolesAllowed。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig {
// 配置 SecurityFilterChain 等
}
测试
package com.example.security.auth;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
public class AuthenticationController {
private final AuthenticationService authenticationService;
// 注册
@PostMapping("/register")
public ResponseEntity<AuthenticationResponse> register(
@RequestBody RegisterRequest request
){
return ResponseEntity.ok(authenticationService.register(request));
}
// 身份验证
@PostMapping("/authenticate")
public ResponseEntity<AuthenticationResponse> authenticate(
@RequestBody AuthenticationRequest request
){
return ResponseEntity.ok(authenticationService.authenticate(request));
}
}
package com.example.security.auth;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NonNull;
@Data
@AllArgsConstructor
@NonNull
@Builder
public class AuthenticationRequest {
String email;
String password;
}
package com.example.security.auth;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NonNull;
@Data
@AllArgsConstructor
@NonNull
@Builder
public class AuthenticationResponse {
private String token;
}
package com.example.security.auth;
import com.example.security.config.JwtService;
import com.example.security.user.Role;
import com.example.security.user.User;
import com.example.security.user.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class AuthenticationService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtService jwtService;
private final AuthenticationManager authenticationManager;
public AuthenticationResponse register(RegisterRequest request) {
var encodedPassword = passwordEncoder.encode(request.getPassword());
System.out.println("Encoded password: " + encodedPassword); // 打印加密后的密码
var user = User.builder()
.firstname(request.getFirstname())
.lastname(request.getLastname())
.email(request.getEmail())
.password(encodedPassword) //加密密码
.role(Role.user)
.build();
System.out.println("Before save: " + user);
userRepository.save(user);
System.out.println("After save: " + user);
var jwtToken = jwtService.generateToken(user);
return AuthenticationResponse.builder()
.token(jwtToken)
.build();
}
public AuthenticationResponse authenticate(AuthenticationRequest request) {
System.out.println("Authentication request: email=" + request.getEmail() + ", password=" + request.getPassword());
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getEmail(),
request.getPassword()
)
);
var user=userRepository.findByEmail(request.getEmail())
.orElseThrow(() -> {
System.out.println("User not found for email: " + request.getEmail());
return new RuntimeException("User not found");});
System.out.println("User found: " + user); // 打印从数据库中找到的用户信息
System.out.println("Encoded password in database: " + user.getPassword()); // 打印数据库中加密的密码
var jwtToken = jwtService.generateToken(user);
return AuthenticationResponse.builder()
.token(jwtToken)
.build();
}
}
package com.example.security.auth;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NonNull;
@Data
@AllArgsConstructor
@NonNull
@Builder
public class RegisterRequest {
private String firstname;
private String lastname;
private String email;
private String password;
}
当我们直接访问http://localhost:8080/api/v1/demo-controller
和 http://localhost:8080/api/v1/auth/authenticate
的时候,都会报403错误,因为第一个接口需要认证而请求没有带jwt token,而第二个是因为数据库中还尚未存在用户。
当我们http://localhost:8080/api/v1/auth/register
会得到一个jwt token,此时将这个token 加入到http://localhost:8080/api/v1/demo-controller
请求头中便可以正常得到内容。
使用注册的角色信息请求http://localhost:8080/api/v1/auth/authenticate
的时候也可以正常访问。