首页 > 数据库 >Spring Security6 实现数据库自定义验证和jwt校验

Spring Security6 实现数据库自定义验证和jwt校验

时间:2024-12-13 12:29:47浏览次数:11  
标签:return String 自定义 Spring jwt param userDetails public

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();
}
  1. 为什么要关闭csrf?:csrf是依据浏览器的自动附加凭证行为(如Cookie)来进行攻击,在传统会话认证中,用户登录后,服务器会生成一个 Session ID 并存储在 Cookie 中。之后的请求会自动携带这个 Cookie,这就有可能会被人恶意利用伪造用户的操作,但是因为我们使用的是jwt是我们自己发布的,所以csrf 攻击无法利用浏览器的行为来发起攻击
  2. 为什么要禁用表单?:因为我们要自定义认证,即使用controller等来进行,这时候就不需要其提供的表单登录了
  3. 为什么会话设置为无状态?:jwt中包含了必要的信息如用户信息,权限,过期时间等,这意味着服务器不需要存储任何与会话相关的状态信息,只需验证令牌的签名即可确认用户的身份和权限
  4. 为什么自定义的过滤器放在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();
}
  1. 为什么要使用http.getSharedObject()方法?:Spring Security 的配置是模块化的,多个组件可能会共享同一个 AuthenticationManager。通过 http.getSharedObject(AuthenticationManagerBuilder.class),可以确保获取的是 Spring Security 内部管理的共享 AuthenticationManagerBuilder,而不是每次都创建一个新的实例,保证 AuthenticationManagerBuilder 的共享性和一致性,使得 AuthenticationManager 的配置与 HttpSecurity 配置逻辑统一管理
  2. 添加自己定义的userDetailsService:Spring Security 会检查你的配置中是否存在 UserDetailsService(无论是默认实现还是自定义实现)。如果你提供了 UserDetailsService,Spring Security 会自动使用它来进行身份验证,调用 UserDetailsService 实现类的 loadUserByUsername() 方法来加载用户信息
  3. 添加密码加密器:数据库中用户密码不能明文存储,故在存储密码的时候要使用密码加密器来加密以保证安全,在验证时PasswordEncoder 提供了一个统一的接口,可以将密码加密和验证过程封装在一起,简化了代码的实现和维护。
2.UserPrincipal和UserDetailsServiceImpl

在 Spring Security 中,``UserDetailsUserDetailsService` 是用来处理用户认证和授权的两个核心接口。

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

过程分析

自定义登录流程

我们先来看一下正常表单登录的流程:

  1. 用户通过登录表单提交用户名和密码

  2. UsernamePasswordAuthenticationFilter 拦截请求,创建一个未认证的 Authentication 对象(例如 UsernamePasswordAuthenticationToken

  3. 将该对象传递给 AuthenticationManagerauthenticate()方法进行认证

  4. AuthenticationManager 的实现类ProviderManager调用一个或多个 AuthenticationProvider

  5. AuthenticationProvider

    • 调用 UserDetailsService 加载用户信息

    • 使用 PasswordEncoder 验证密码

    • 如果认证成功,返回一个已认证的 Authentication 对象

  6. 认证成功后,SecurityContextHolder 保存认证信息

那么由上可知,如果我们要实现自定义登录就需要:

  1. 用户提交用户名和密码

  2. 在AuthController进行格式校验后进入AuthService的方法中,再次进行格式校验(双重保险)后直接使用AuthenticationManagerauthenticate()方法,这就意味着我们会跳过所有的过滤器,即没有**UsernamePasswordAuthenticationFilter 拦截请求,创建一个未认证的 Authentication 对象**,此时我们需要自己来创建这个对象,这就是为什么要new一个UsernamePasswordAuthenticationToken对象

  3. 调用authenticate()方法,在 AuthenticationManagerBuilder 中,调用了 .userDetailsService(userDetailsService).passwordEncoder(passwordEncoder())。这些配置的作用是向 AuthenticationManagerBuilder 提供 AuthenticationProvider(通常是 DaoAuthenticationProvider),并通过 ProviderManager来调用Provider来进行验证,可以这样理解,这些Provider是实际干活的,ProviderManager是分配活的

    AuthenticationManagerBuilder 最终会构建一个 ProviderManager,这是默认行为,所以我们调用authenticate()方法实际是ProviderManagerauthenticate()方法

  4. authenticate()方法会调用Provider的认证方法这里我们提供了 DaoAuthenticationProvider故会调用DaoAuthenticationProviderauthenticate()方法,但是DaoAuthenticationProvider并没有重写authenticate()方法,所以实际上我们使用的是DaoAuthenticationProvider的父类AbstractUserDetailsAuthenticationProviderauthenticate()方法

  5. AbstractUserDetailsAuthenticationProviderauthenticate()方法会调用user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);来获取用户信息此时我们会使用 DaoAuthenticationProviderretrieveUser()方法获取到用户信息并返回,这里获取的信息是使用我们自己创建的UserDetailsServiceImpl中的loadUserByUsername()方法加载的用户信息

  6. 在这之后,使用我们自定义的PasswordEncoder来比对输入密码和数据库中的密码,认证成功后,此时如果是第一次登录即没有缓存时,会将用户存入到缓存中减少下一次查数据库的开销,提高性能,认证成功后Spring Security会自动帮我们把认证信息存入Sercurity Context,最终返回一个已认证的 Authentication 对象

  7. 这时候我们调用JwtService来颁发一个jwt给用户

    之后用户在访问其他路径的时候会在请求头携带上这个jwt,以上即为自定义登录的全过程


以上为笔者拙见,如有错误望指正,非常感谢!

标签:return,String,自定义,Spring,jwt,param,userDetails,public
From: https://blog.csdn.net/m0_74184535/article/details/144446948

相关文章

  • 自定义资源支持:K8s Device Plugin 从原理到实现
    本文主要分析k8s中的device-plugin机制工作原理,并通过实现一个简单的device-plugin来加深理解。1.背景默认情况下,k8s中的Pod只能申请CPU和Memory这两种资源,就像下面这样:resources:requests:memory:"1024Mi"cpu:"100m"limits:memory:"2......
  • springboot毕设 停车场管理系统 程序+论文
    本系统(程序+源码)带文档lw万字以上文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着城市化进程的加速,汽车数量急剧增加,停车难问题已成为现代城市交通管理的一大挑战。传统的停车场管理模式大多依赖于人工操作,效率低下且容易出错,无......
  • springboot毕设 企业出纳系统的设计与实现 程序+论文
    本系统(程序+源码)带文档lw万字以上文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景在当今信息化高速发展的时代,企业财务管理作为企业运营的核心环节,其效率与准确性直接关系到企业的竞争力和持续发展能力。传统的出纳管理方式往往依赖......
  • Springboot+maven+druid+mybatis-plus多数据源
    Springboot版本:2.3.12.RELEASE1.maven依赖<!--druid连接池--><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.22&l......
  • springboot线下培训机构集中管理和推荐平台-计算机毕业设计源码48919
    线下培训机构集中管理和推荐平台的实现摘 要该论文研究了一种线下培训机构集中管理和推荐平台的设计与实现。该平台旨在解决传统线下培训机构管理和推荐过程中存在的诸多问题,包括信息不对称、资源分散、推荐不精准等。通过系统性的需求分析和技术调研,设计了一套基于Sprin......
  • Spring、SpringBoot、SpringCloud关系是什么?你真的需要SpringCloud吗?
    目录Spring核心能力:控制反转(IOC):依赖注入(DI):面向切面编程(AOP):SpringBoot核心能力约定优于配置:嵌入式容器:自动配置:丰富的starter:SpringCloud的特点三者关系你真的需要SpringCloud吗?spring的第一个版本发布于2002年,他出现的使命就是替换EJB(J2EE规范,理论上很先进,......
  • 计算机毕业设计 | SpringBoot+vue毕业设计系统 毕业论文设计项目答辩管理(附源码+论文
    1,绪论1.1选题背景目前整个社会发展的速度,严重依赖于互联网,如果没有了互联网的存在,市场可能会一蹶不振,严重影响经济的发展水平,影响人们的生活质量。计算机的发展,不管是从硬件还是软件,都有很多技术储备,每年都有很多的技术和软件产生,纵观各个领域,无一不用互联网软件,办公用的......
  • 计算机毕业设计 | SpringBoot+vue医疗挂号管理系统 医院就诊业务办理平台(附源码+论文
    1,绪论1.1选题背景目前整个社会发展的速度,严重依赖于互联网,如果没有了互联网的存在,市场可能会一蹶不振,严重影响经济的发展水平,影响人们的生活质量。计算机的发展,不管是从硬件还是软件,都有很多技术储备,每年都有很多的技术和软件产生,纵观各个领域,无一不用互联网软件,办公用的......
  • Java-25 深入浅出 Spring - 实现简易Ioc-01 Servlet介绍 基本代码编写
    点一下关注吧!!!非常感谢!!持续更新!!!大数据篇正在更新!https://blog.csdn.net/w776341482/category_12713819.html案例思路参考来源来自网络视频,这里的案例是转账的案例。这里我们直接使用接口的方式,就不实现具体的页面了,我们直接通过接口调用的方式来模拟这一块。最终将实......
  • 132Java基于SpringBoot的西山区家政服务网站设计与开发-java vue.js idea
    所需该项目可以在最下面查看联系方式,为防止迷路可以收藏文章,以防后期找不到项目介绍132Java基于SpringBoot的西山区家政服务网站设计与开发-javavue.jsidea系统实现截图技术栈介绍JDK版本:jdk1.8+编程语言:java框架支持:springboot数据库:mysql......