Spring Security6 数据库自定义验证和jwt校验的简单实现以及个人解读
版本
- spring boot 3.4.0
- mybatis-plus 3.5.7
- jjwt 0.12.6
在使用jjwt的时候需要导入三个依赖分别是jjwt-api,jjwt-impl和jjwt-jackson,导入三个有点麻烦,所以可以直接导入jjwt依赖,这个依赖包含前面三个
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.12.6</version>
</dependency>
快速实现
1.SecurityConfig
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Resource
private UserDetailsServiceImpl userDetailsService;
@Resource
private JwtAuthorizeFilter jwtAuthorizeFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
// 关闭csrf防护
.csrf(AbstractHttpConfigurer::disable)
// 除了白名单其他路径都需要认证
.authorizeHttpRequests(conf -> conf
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
// 禁用表单
.formLogin(AbstractHttpConfigurer::disable)
// 会话设为无状态
.sessionManagement(conf -> conf
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// 配置自定义认证管理器
.authenticationManager(authenticationManagerBean(http))
// 将自定义的过滤器添加到UsernamePasswordAuthenticationFilter之前
.addFilterBefore(jwtAuthorizeFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
/**
* 自定义认证管理器
* @param http http
* @return 自定义的认证管理器
* @throws Exception 异常
*/
@Bean
public AuthenticationManager authenticationManagerBean(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder =
http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
return authenticationManagerBuilder.build();
}
/**
* 密码加密器
* @return 密码加密器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
2.UserPrincipal
@RequiredArgsConstructor
public class UserPrincipal implements UserDetails {
private final User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return UserDetails.super.isAccountNonExpired();
}
@Override
public boolean isAccountNonLocked() {
return UserDetails.super.isAccountNonLocked();
}
@Override
public boolean isCredentialsNonExpired() {
return UserDetails.super.isCredentialsNonExpired();
}
@Override
public boolean isEnabled() {
return UserDetails.super.isEnabled();
}
}
3.UserDetailsServiceImpl
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.selectOne(new QueryWrapper<User>()
.eq("Backend_username", username));
if (user == null) {
throw new UsernameNotFoundException(username);
}
return new UserPrincipal(user);
}
}
4.JwtService
@Service
public class JwtService {
@Value("${spring.security.jwt.key}")
private String key;
/**
* 生成jwt
* @param userDetails 用户信息
* @param claims 声明
* @return jwt
*/
public String generateJwt(
UserDetails userDetails,
Map<String, Object> claims
) {
return Jwts.builder()
.header()
.type("JWT")
.and()
.claims(claims)
.id(UUID.randomUUID().toString())
.subject(userDetails.getUsername())
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION_TIME))
.signWith(getSecretKey(),ALGORITHM)
.compact();
}
/**
* 生成jwt
* @param userDetails 用户信息
* @return jwt
*/
public String generateJwt(UserDetails userDetails) {
return generateJwt(userDetails,new HashMap<>());
}
/**
* 获取密钥
* @return 密钥
*/
private SecretKey getSecretKey() {
byte[] decode = Base64.getDecoder().decode(key);
return Keys.hmacShaKeyFor(decode);
}
/**
* 返回全部声明
* @param jwt jwt
* @return 声明
* @throws JwtException 验证失败异常
*/
public Claims extractAllClaims(String jwt) throws JwtException {
return Jwts
.parser()
.verifyWith(getSecretKey())
.build()
.parseSignedClaims(jwt)
.getPayload();
}
/**
* 从jwt提取指定声明
* @param jwt 需要解析的jwt字符串
* @param claimsResolver 从声明中提取和转换需要的数据
* @return claimsResolver的处理结果
* @param <T> 泛型
*/
public <T> T extractClaims(String jwt, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(jwt);
return claimsResolver.apply(claims);
}
/**
* 从jwt提取指定声明
* @param claims 声明
* @param claimsResolver 从声明中提取和转换需要的数据
* @return claimsResolver的处理结果
* @param <T> 泛型
*/
public <T> T extractClaims(Claims claims, Function<Claims, T> claimsResolver) {
return claimsResolver.apply(claims);
}
/**
* 提取用户名
* @param jwt 需要解析的jwt字符串
* @return 用户名
*/
public String extractUsername(String jwt) {
try {
return extractClaims(jwt, Claims::getSubject);
} catch (JwtException e) {
return null;
}
}
/**
* jwt是否有效
* @param jwt jwt
* @param userDetails 用户信息
* @return 验证结果
*/
public Boolean isJwtValid(String jwt,UserDetails userDetails) {
try {
Claims claims = extractAllClaims(jwt);
String username = claims.getSubject();
Date expiration = claims.getExpiration();
return (username.equals(userDetails.getUsername())) && isJwtNotExpired(expiration);
} catch (JwtException e) {
return false;
}
}
/**
* jwt是否有效
* @param claims 声明的集合
* @param userDetails 用户信息
* @return 验证结果-有效:<code>true</code>,无效:<code>false</code>
*/
public Boolean isJwtValid(Claims claims,UserDetails userDetails) {
String username = claims.getSubject();
Date expiration = claims.getExpiration();
return (username.equals(userDetails.getUsername())) && isJwtNotExpired(expiration);
}
/**
* jwt是否没有过期
* @param expiration 过期时间
* @return 验证结果-未过期:<code>true</code>,过期:<code>false</code>
*/
private boolean isJwtNotExpired(Date expiration) {
return expiration == null || !expiration.before(new Date());
}
}
5.JwtAuthorizeFilter
@Component
@Slf4j
public class JwtAuthorizeFilter extends OncePerRequestFilter {
@Resource
private JwtService jwtService;
@Resource
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
String servletPath = request.getServletPath();
// 检查路径是否在排除列表中
if (isExcludedPath(servletPath)) {
filterChain.doFilter(request, response);
return;
}
String jwt = extractJwtFromRequest(request);
if (jwt != null) {
try {
processJwtAuthentication(jwt, request);
} catch (JwtException e) {
log.error("jwt解析失败:{}", e.getMessage());
handleJwtError(response, e);
return; // 中断过滤链,返回错误响应
}
}
filterChain.doFilter(request, response);
}
/**
* 提取jwt
* @param request 请求
* @return jwt
*/
private String extractJwtFromRequest(HttpServletRequest request) {
String header = request.getHeader(AUTH_HEADER);
if (header != null && header.startsWith(BEARER_TOKEN_PREFIX)) {
return header.substring(BEARER_TOKEN_PREFIX.length());
}
return null;
}
/**
* 解析jwt,在验证jwt有效后将认证信息存入到SecurityContext
* @param jwt jwt
* @param request 请求
* @throws JwtException 解析jwt的报错
*/
private void processJwtAuthentication(String jwt, HttpServletRequest request) throws JwtException{
Claims claims = jwtService.extractAllClaims(jwt);
String username = claims.getSubject();
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtService.isJwtValid(claims, userDetails)) {
setAuthentication(userDetails, request);
}
}
}
/**
* 设置认证
* @param userDetails 用户信息
* @param request 请求
*/
private void setAuthentication(UserDetails userDetails, HttpServletRequest request) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
/**
* 处理jwt异常
* @param response 响应
* @param e 异常
* @throws IOException 异常
*/
private void handleJwtError(HttpServletResponse response, JwtException e) throws IOException {
// 构建通用响应对象
BaseResponse<String> errorResponse = BaseResponse.forbidden("JWT 解析失败: " + e.getMessage());
// 设置内容类型
response.setContentType("application/json;charset=UTF-8");
// 使用 Jackson 将对象序列化为 JSON 字符串
ObjectMapper objectMapper = new ObjectMapper();
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}
/**
* 检查是否是排除路径
* @param path 请求的路径
* @return 是-<code>true</code>,不是-<code>false</code>
*/
private boolean isExcludedPath(String path) {
for (String excluded : EXCLUDED_PATHS) {
if (pathMatches(excluded, path)) {
return true;
}
}
return false;
}
/**
* 比对路径
* @param pattern 排除路径
* @param path 请求路径
* @return 一样-<code>true</code>,不一样-<code>false</code>
*/
private boolean pathMatches(String pattern, String path) {
if (pattern.endsWith("/*")) {
String basePath = pattern.substring(0, pattern.length() - 2);
return path.startsWith(basePath);
}
return pattern.equals(path);
}
}
6.AuthConstants
这里的用户名和密码格式校验为用户名由大小写字母,数字和下划线,密码为大小写字母,数字,下划线和破折号,自己的程序可以按照自己的要求来自定义
public class AuthConstants {
/**
* 认证请求头
*/
public static final String AUTH_HEADER = "Authorization";
/**
* jwt的前缀
*/
public static final String BEARER_TOKEN_PREFIX = "Bearer ";
/**
* 用户名格式
*/
public static final String USERNAME_MATCHING = "^[a-zA-Z0-9]+$";
/**
* 密码格式
*/
public static final String PASSWORD_MATCHING = "^[a-zA-Z0-9_-]+$";
/**
* jwt过期时间
*/
public static final Integer JWT_EXPIRATION_TIME = 1000 * 60 * 60 * 24;
/**
* jwt签名算法
*/
public final static SecureDigestAlgorithm<SecretKey, SecretKey> ALGORITHM = Jwts.SIG.HS256;
/**
* 排除的路径,这些路径不经过JwtAuthenticationFilter
*/
public static final List<String> EXCLUDED_PATHS = Arrays.asList(
"/api/auth/login",
"/api/auth/register"
);
}
7.AuthUtils
public class AuthUtils {
/**
* 校验用户名和密码格式
* @param username 用户名
* @param password 密码
* @return 校验结果-合法:<code>true</code>,不合法:<code>false</code>
*/
public static Boolean formatValidation(String username, String password) {
return (Pattern.matches(AuthConstants.USERNAME_MATCHING, username)
&& Pattern.matches(AuthConstants.PASSWORD_MATCHING, password));
}
}
8.AuthController
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Resource
private AuthService authService;
@PostMapping("/login")
public BaseResponse<String> login(@RequestBody @Valid LoginRequest loginRequest) {
String username = loginRequest.getUsername();
String password = loginRequest.getPassword();
// 格式校验
if (!AuthUtils.formatValidation(username, password)) {
return BaseResponse.forbidden("账号或密码格式不正确");
}
return authService.loginVerification(username, password);
}
}
9.AuthService
public interface AuthService {
/**
* 登陆验证
* @param userName 用户名
* @param password 密码
* @return 响应结果
*/
public BaseResponse<String> loginVerification(String userName, String password);
}
10.AuthServiceImpl
@Service
public class AuthServiceImpl implements AuthService {
@Resource
private AuthenticationManager authenticationManager;
@Resource
private JwtService jwtService;
@Resource
private UserDetailsServiceImpl userDetailsService;
@Override
public BaseResponse<String> loginVerification(String username, String password) {
// 格式校验
if (!AuthUtils.formatValidation(username, password)) {
return BaseResponse.forbidden("账号或密码格式不正确");
}
// 创建认证对象
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(username, password);
try {
// 使用 AuthenticationManager 进行认证
Authentication authentication = authenticationManager.authenticate(authenticationToken);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
String jwt = jwtService.generateJwt(userDetails);
// 如果认证通过,返回自定义响应
return BaseResponse.success(jwt);
} catch (AuthenticationException e) {
// 如果认证失败,返回失败的自定义响应
return BaseResponse.forbidden("登录失败: " + e.getMessage());
}
}
}
个人解读
1.SecurityConfig
@EnableSecurity注解:
开启spring security支持,虽然不加上这个注解也可以运行,因为spring boot会自动装配,但是有时可能会莫名其妙报错无法自动装配。找不到 'HttpSecurity' 类型的 Bean
这时候可以加上该注解解决,spring security也是推荐显式添加 @EnableWebSecurity
注解,以确保安全配置被正确启用并且你能够更好地控制配置过程。
securityFilterChain方法
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
// 关闭csrf防护
.csrf(AbstractHttpConfigurer::disable)
// 除了白名单其他路径都需要认证
.authorizeHttpRequests(conf -> conf
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
// 禁用表单
.formLogin(AbstractHttpConfigurer::disable)
// 会话设为无状态
.sessionManagement(conf -> conf
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// 配置自定义认证管理器
.authenticationManager(authenticationManagerBean(http))
// 将自定义的过滤器添加到UsernamePasswordAuthenticationFilter之前
.addFilterBefore(jwtAuthorizeFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
- 为什么要关闭csrf?:csrf是依据浏览器的自动附加凭证行为(如Cookie)来进行攻击,在传统会话认证中,用户登录后,服务器会生成一个 Session ID 并存储在 Cookie 中。之后的请求会自动携带这个 Cookie,这就有可能会被人恶意利用伪造用户的操作,但是因为我们使用的是jwt是我们自己发布的,所以csrf 攻击无法利用浏览器的行为来发起攻击
- 为什么要禁用表单?:因为我们要自定义认证,即使用controller等来进行,这时候就不需要其提供的表单登录了
- 为什么会话设置为无状态?:jwt中包含了必要的信息如用户信息,权限,过期时间等,这意味着服务器不需要存储任何与会话相关的状态信息,只需验证令牌的签名即可确认用户的身份和权限
- 为什么自定义的过滤器放在UsernamePasswordAuthenticationFilter之前?:是为了确保在处理任何请求时,优先尝试通过 JWT 进行认证,而不是触发传统的用户名和密码认证逻辑,这样减少不必要的验证逻辑,确保无状态认证的优先级以及保证安全性(避免被伪造请求绕过jwt检查)
自定义的认证管理器
@Resource
private UserDetailsServiceImpl userDetailsService;
/**
* 自定义认证管理器
* @param http http
* @return 自定义的认证管理器
* @throws Exception 异常
*/
@Bean
public AuthenticationManager authenticationManagerBean(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder =
http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
return authenticationManagerBuilder.build();
}
/**
* 密码加密器
* @return 密码加密器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
- 为什么要使用http.getSharedObject()方法?:Spring Security 的配置是模块化的,多个组件可能会共享同一个
AuthenticationManager
。通过http.getSharedObject(AuthenticationManagerBuilder.class)
,可以确保获取的是 Spring Security 内部管理的共享AuthenticationManagerBuilder
,而不是每次都创建一个新的实例,保证AuthenticationManagerBuilder
的共享性和一致性,使得AuthenticationManager
的配置与HttpSecurity
配置逻辑统一管理 - 添加自己定义的userDetailsService:Spring Security 会检查你的配置中是否存在
UserDetailsService
(无论是默认实现还是自定义实现)。如果你提供了 UserDetailsService,Spring Security 会自动使用它来进行身份验证,调用 UserDetailsService 实现类的loadUserByUsername()
方法来加载用户信息 - 添加密码加密器:数据库中用户密码不能明文存储,故在存储密码的时候要使用密码加密器来加密以保证安全,在验证时
PasswordEncoder
提供了一个统一的接口,可以将密码加密和验证过程封装在一起,简化了代码的实现和维护。
2.UserPrincipal和UserDetailsServiceImpl
在 Spring Security 中,``UserDetails和
UserDetailsService` 是用来处理用户认证和授权的两个核心接口。
UserDetails
就是我们用来封装用户信息的接口,比如用户名、密码、权限等。我们通常会实现它,创建一个自定义的用户信息模型,像 UserPrincipal 这样的类,可以让我们根据需要自定义用户数据。通过实现 UserDetails,我们能更灵活地处理用户信息。
UserDetailsService 则是用来从数据库或其他地方加载用户信息的接口。我们实现 UserDetailsService 后,重写 loadUserByUsername
方法,这样就可以根据用户名从数据库中获取用户数据,并将其封装成 UserDetails
对象返回。这样 Spring Security 就能够用这些信息来进行用户认证和权限验证。
通过实现这两个接口,我们可以更加灵活地处理用户认证的流程,特别是当需要从数据库或者其他外部系统获取用户信息时。
3.JwtService
提供了jwt的生成和解析
jwt
jwt共有三部分,分别为头部(header),负载(payload)和签名(signature)
头部记录签名算法和token类型
负载用于存储用户信息,权限,jwt有效期等
签名用于验证信息没有被修改
生成jwt
public String generateJwt(
UserDetails userDetails,
Map<String, Object> claims
) {
return Jwts.builder()
.header()
.type("JWT")
.and()
.claims(claims)
.id(UUID.randomUUID().toString())
.subject(userDetails.getUsername())
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION_TIME))
.signWith(getSecretKey(),ALGORITHM)
.compact();
}
private SecretKey getSecretKey() {
byte[] decode = Base64.getDecoder().decode(key);
return Keys.hmacShaKeyFor(decode);
}
.header()
表示构建头部
.type("JWT")
表示token类型是JWT
and()
返回jwt构建器,用and()
让整体结构更清晰
claims()
负载中添加声明
.id()
为jwt生成一个随机不重复的id用来标识一个jwt,后续如果需要设置登出操作的时候可以依据这个来设置白/黑名单来登出
.subject()
记录jwt的使用者
.issuedAt()
jwt签发时间
.expiration()
jwt过期时间
.signWith()
密钥和签名算法
getSecretKey
:获取密钥,密钥推荐使用Base64进行编码,所以在获取密钥的时候,需要先使用Base64解码,再告诉签名算法密钥是什么,这里并非是加密密钥,而是告诉要用到的签名算法解密密钥是什么,就像是指纹锁一样,你得先告诉它,谁的指纹能解开这个锁,这也是为什么,这里获取密钥使用的算法标签要和签名算法一致,因为你总不可能在这个指纹锁录入你的指纹后,去用你的指纹解锁其他的锁吧
解析jwt
/**
* 返回全部声明
* @param jwt jwt
* @return 声明
* @throws JwtException 验证失败异常
*/
public Claims extractAllClaims(String jwt) throws JwtException {
return Jwts
.parser()
.verifyWith(getSecretKey())
.build()
.parseSignedClaims(jwt)
.getPayload();
}
/**
* jwt是否有效
* @param claims 声明的集合
* @param userDetails 用户信息
* @return 验证结果-有效:<code>true</code>,无效:<code>false</code>
*/
public Boolean isJwtValid(Claims claims,UserDetails userDetails) {
String username = claims.getSubject();
Date expiration = claims.getExpiration();
return (username.equals(userDetails.getUsername())) && isJwtNotExpired(expiration);
}
/**
* jwt是否没有过期
* @param expiration 过期时间
* @return 验证结果-未过期:<code>true</code>,过期:<code>false</code>
*/
private boolean isJwtNotExpired(Date expiration) {
return expiration == null || !expiration.before(new Date());
}
.extractAllClaims()
-
.parser()
获取jwt解析构造器 -
.verifyWith()
:解密密钥 -
.build()
获得jwt解析器 -
.parseSignedClaims()
:添加要解析的jwt,解析jwt参数 -
.getPayload()
获得负载的内容
isJwtValid()
- 比对数据库中的信息判断jwt是否被篡改
- 与当前时间比较判断jwt是否过期
4.JwtAuthorizeFilter
@Component
@Slf4j
public class JwtAuthorizeFilter extends OncePerRequestFilter {
@Resource
private JwtService jwtService;
@Resource
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
String servletPath = request.getServletPath();
// 检查路径是否在排除列表中
if (isExcludedPath(servletPath)) {
filterChain.doFilter(request, response);
return;
}
String jwt = extractJwtFromRequest(request);
if (jwt != null) {
try {
processJwtAuthentication(jwt, request);
} catch (JwtException e) {
log.error("jwt解析失败:{}", e.getMessage());
handleJwtError(response, e);
return; // 中断过滤链,返回错误响应
}
}
filterChain.doFilter(request, response);
}
/**
* 提取jwt
* @param request 请求
* @return jwt
*/
private String extractJwtFromRequest(HttpServletRequest request) {
String header = request.getHeader(AUTH_HEADER);
if (header != null && header.startsWith(BEARER_TOKEN_PREFIX)) {
return header.substring(BEARER_TOKEN_PREFIX.length());
}
return null;
}
/**
* 解析jwt,在验证jwt有效后将认证信息存入到SecurityContext
* @param jwt jwt
* @param request 请求
* @throws JwtException 解析jwt的报错
*/
private void processJwtAuthentication(String jwt, HttpServletRequest request) throws JwtException{
Claims claims = jwtService.extractAllClaims(jwt);
String username = claims.getSubject();
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtService.isJwtValid(claims, userDetails)) {
setAuthentication(userDetails, request);
}
}
}
/**
* 设置认证
* @param userDetails 用户信息
* @param request 请求
*/
private void setAuthentication(UserDetails userDetails, HttpServletRequest request) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
/**
* 处理jwt异常
* @param response 响应
* @param e 异常
* @throws IOException 异常
*/
private void handleJwtError(HttpServletResponse response, JwtException e) throws IOException {
// 构建通用响应对象
BaseResponse<String> errorResponse = BaseResponse.forbidden("JWT 解析失败: " + e.getMessage());
// 设置内容类型
response.setContentType("application/json;charset=UTF-8");
// 使用 Jackson 将对象序列化为 JSON 字符串
ObjectMapper objectMapper = new ObjectMapper();
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}
/**
* 检查是否是排除路径
* @param path 请求的路径
* @return 是-<code>true</code>,不是-<code>false</code>
*/
private boolean isExcludedPath(String path) {
for (String excluded : EXCLUDED_PATHS) {
if (pathMatches(excluded, path)) {
return true;
}
}
return false;
}
/**
* 比对路径
* @param pattern 排除路径
* @param path 请求路径
* @return 一样-<code>true</code>,不一样-<code>false</code>
*/
private boolean pathMatches(String pattern, String path) {
if (pattern.endsWith("/*")) {
String basePath = pattern.substring(0, pattern.length() - 2);
return path.startsWith(basePath);
}
return pattern.equals(path);
}
}
1.为什么要实现OncePerRequestFilter?
首先我们要处理所有的jwt验证逻辑, OncePerRequestFilter
确保每个请求仅执行一次过滤器逻辑
OncePerRequestFilter
是 Spring Security 推荐的过滤器基类之一,其设计使得过滤器更好地融入 Spring Security 的过滤器链
2.验证流程
首先,我们要获取当前的请求路径,看看是否在要排除掉的路径名单中(因为我们登录注册要使用UsernamePasswordAuthenticationFilter
,此时不需要解析jwt),如果在就退出这个过滤器,继续过滤器链,如果不在则继续
其次我们要提取出来我们生成的jwt,因为在获取请求头的Authentication
的时候,会发现添加上了Bearer
的前缀,这是默认的,所以我们需要将这个前缀去除,取出生成的jwt
然后我们要解析jwt并在验证jwt无问题后保存到SecurityContext中
解析验证jwt
private void processJwtAuthentication(String jwt, HttpServletRequest request) throws JwtException{
Claims claims = jwtService.extractAllClaims(jwt);
String username = claims.getSubject();
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtService.isJwtValid(claims, userDetails)) {
setAuthentication(userDetails, request);
}
}
}
首先我们解析jwt获取到声明,其次我们要确保获取到的声明有用户信息且该用户并没有认证过,然后我们要确保当前这个jwt是有效的再去设置认证
jwt解析失败
当jwt解析失败后,会抛出JwtException,此时我们统一处理该报错,切断过滤器链,直接返回错误信息
if (jwt != null) {
try {
processJwtAuthentication(jwt, request);
} catch (JwtException e) {
log.error("jwt解析失败:{}", e.getMessage());
handleJwtError(response, e);
return; // 中断过滤链,返回错误响应
}
}
private void handleJwtError(HttpServletResponse response, JwtException e) throws IOException {
// 构建通用响应对象
BaseResponse<String> errorResponse = BaseResponse.forbidden("JWT 解析失败: " + e.getMessage());
// 设置内容类型
response.setContentType("application/json;charset=UTF-8");
// 使用 Jackson 将对象序列化为 JSON 字符串
ObjectMapper objectMapper = new ObjectMapper();
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}
AuthUtils
对输入的用户名和密码进行格式校验
public class AuthUtils {
/**
* 校验用户名和密码格式
* @param username 用户名
* @param password 密码
* @return 校验结果-合法:<code>true</code>,不合法:<code>false</code>
*/
public static Boolean formatValidation(String username, String password) {
return (Pattern.matches(AuthConstants.USERNAME_MATCHING, username)
&& Pattern.matches(AuthConstants.PASSWORD_MATCHING, password));
}
}
AuthServiceImpl
@Override
public BaseResponse<String> loginVerification(String username, String password) {
// 格式校验
if (!AuthUtils.formatValidation(username, password)) {
return BaseResponse.forbidden("账号或密码格式不正确");
}
// 创建认证对象
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(username, password);
try {
// 使用 AuthenticationManager 进行认证
Authentication authentication = authenticationManager.authenticate(authenticationToken);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
String jwt = jwtService.generateJwt(userDetails);
// 如果认证通过,返回自定义响应
return BaseResponse.success(jwt);
} catch (AuthenticationException e) {
// 如果认证失败,返回失败的自定义响应
return BaseResponse.forbidden("登录失败: " + e.getMessage());
}
}
为什么要new一个UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken
表示用户名和密码的认证凭据
使用 UsernamePasswordAuthenticationToken
可以直接利用 Spring Security 提供的默认实现,又因为我们使用的是Spring Security所以最好遵守其规则
方法authenticate()
是认证的核心方法,其需要接收一个Authentication
的参数,UsernamePasswordAuthenticationToken
继承了AbstractAuthenticationToken
,而AbstractAuthenticationToken
实现了Authentication
过程分析
自定义登录流程
我们先来看一下正常表单登录的流程:
-
用户通过登录表单提交用户名和密码
-
UsernamePasswordAuthenticationFilter
拦截请求,创建一个未认证的Authentication
对象(例如UsernamePasswordAuthenticationToken
) -
将该对象传递给
AuthenticationManager
的authenticate()
方法进行认证 -
AuthenticationManager
的实现类ProviderManager
调用一个或多个AuthenticationProvider
-
AuthenticationProvider
-
调用
UserDetailsService
加载用户信息 -
使用
PasswordEncoder
验证密码 -
如果认证成功,返回一个已认证的
Authentication
对象
-
-
认证成功后,
SecurityContextHolder
保存认证信息
那么由上可知,如果我们要实现自定义登录就需要:
-
用户提交用户名和密码
-
在AuthController进行格式校验后进入AuthService的方法中,再次进行格式校验(双重保险)后直接使用
AuthenticationManager
的authenticate()
方法,这就意味着我们会跳过所有的过滤器,即没有**UsernamePasswordAuthenticationFilter
拦截请求,创建一个未认证的Authentication
对象**,此时我们需要自己来创建这个对象,这就是为什么要new一个UsernamePasswordAuthenticationToken
对象 -
调用
authenticate()
方法,在AuthenticationManagerBuilder
中,调用了.userDetailsService(userDetailsService)
和.passwordEncoder(passwordEncoder())
。这些配置的作用是向AuthenticationManagerBuilder
提供AuthenticationProvider
(通常是DaoAuthenticationProvider
),并通过ProviderManager
来调用Provider来进行验证,可以这样理解,这些Provider是实际干活的,ProviderManager是分配活的AuthenticationManagerBuilder
最终会构建一个ProviderManager
,这是默认行为,所以我们调用authenticate()
方法实际是ProviderManager
的authenticate()
方法 -
authenticate()
方法会调用Provider的认证方法这里我们提供了DaoAuthenticationProvider
故会调用DaoAuthenticationProvider
的authenticate()
方法,但是DaoAuthenticationProvider
并没有重写authenticate()
方法,所以实际上我们使用的是DaoAuthenticationProvider
的父类AbstractUserDetailsAuthenticationProvider
的authenticate()
方法 -
AbstractUserDetailsAuthenticationProvider
的authenticate()
方法会调用user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
来获取用户信息此时我们会使用DaoAuthenticationProvider
的retrieveUser()
方法获取到用户信息并返回,这里获取的信息是使用我们自己创建的UserDetailsServiceImpl
中的loadUserByUsername()
方法加载的用户信息 -
在这之后,使用我们自定义的
PasswordEncoder
来比对输入密码和数据库中的密码,认证成功后,此时如果是第一次登录即没有缓存时,会将用户存入到缓存中减少下一次查数据库的开销,提高性能,认证成功后Spring Security会自动帮我们把认证信息存入Sercurity Context,最终返回一个已认证的Authentication
对象 -
这时候我们调用JwtService来颁发一个jwt给用户
之后用户在访问其他路径的时候会在请求头携带上这个jwt,以上即为自定义登录的全过程
以上为笔者拙见,如有错误望指正,非常感谢!
标签:return,String,自定义,Spring,jwt,param,userDetails,public From: https://blog.csdn.net/m0_74184535/article/details/144446948