【SpringSecurity 6.X新版】自定义过滤器登录方式

SpringSecurity自定义登录

适用于5.7.5及6.X版本,以密码账号登录方式通过过滤器验证方式实现

先看看大致流程:

    1. 请求经过用户名密码认证的过滤器,在过滤器中获取用户名,密码,验证码等参数并封装成Authentication对象,然后调用安全管理器进行认证(需要我们写自己的过滤器和Authentication实现类);
    1. 安全管理器会遍历配置好的认证逻辑集合(AuthenticationProvider),通过AuthenticationProvider的supports方法找到合适的AuthenticationProvider进行认证的操作(SpringSecurity管理,不需要我们写)
    1. 安全管理器找到合适的认证AuthenticationProvider后会执行他的authenticate方法,这个方法就是用来判断是否通过认证的,通过则返回Authentication对象即可(需要实现AuthenticationProvider接口,重写里面的方法)
1. 重写UserDetails(相当于用户实体类)
/**
 * 自定义UserDetails,用于Spring Security的认证
 * @author tanglang
 * @Date 2024-04-02
 */
@Data
public class LoginUser implements UserDetails {

    private User user;

    private Collection<? extends GrantedAuthority> authorities;

    private String token;

    public LoginUser(User user, Collection<? extends GrantedAuthority> authorities) {
        this.user = user;
        this.authorities = authorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @JSONField(serialize = false)
    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @JSONField(serialize = false)
    @Override
    public boolean isAccountNonExpired() {
        return !user.getIsExpired();
    }

    @JSONField(serialize = false)
    @Override
    public boolean isAccountNonLocked() {
        return !user.getIsLocked();
    }

    @JSONField(serialize = false)
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @JSONField(serialize = false)
    @Override
    public boolean isEnabled() {
        return user.getIsActive();
    }

}

2. 重写AbstractAuthenticationToken

主要是用来存储认证信息的(全部认证都会用到它),也要根据他的当前子类型判断用哪个AuthenticationProvider类来进行认证 列如:UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)

/**
 * 注意我这个类名与SpringSecurity自带的有重名,后续不要引入错包了
 */
public class UsernamePasswordAuthenticationToken  extends AbstractAuthenticationToken {

    private final Object principal;

    private Object credentials;

    /**
     * 验证码
     */
    private String captcha;

    public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }

    public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(false);
    }

    public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) {
        return new UsernamePasswordAuthenticationToken(principal, credentials);
    }

    public static UsernamePasswordAuthenticationToken authenticated(Object principal, Object credentials,
                                                               Collection<? extends GrantedAuthority> authorities) {
        return new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
    }

    @Override
    public Object getCredentials() {
        return credentials;
    }

    @Override
    public Object getPrincipal() {
        return principal;
    }

    public String getCaptcha() {
        return captcha;
    }

    public void setCaptcha(String captcha) {
        this.captcha = captcha;
    }
    
    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException("无法将此令牌设置为受信任,只能使用构造函数,该构造函数接受GrantedAuthority列表。");
        }
        super.setAuthenticated(false);
    }
}
3. 重写AuthenticationProvider

重写里面两个方法,authenticate方法就是用来验证用户是否正确的,supports方法是用来给认证管理器(ProviderManager)来决定使用你这个AuthenticationProvider类来进行认证的,返回true这表示使用这个类来处理认证

/**
 * 用户名密码认证提供者
 * @author tanglang
 * @date 2024-04-16
 */
@Component
public class UsernamePasswordAuthenticationProvider  implements AuthenticationProvider {

    @Autowired
    private RedisCache redisCache;

    /**
     * 这个是查询用户信息的服务类,自行实现
     */
    @Autowired
    private SecurityService securityService;

    /**
     * 密码加密器,用于对密码进行加密和密码验证
     */
    private PasswordEncoder bCryptPasswordEncoder;

    /**
     * 用户状态检查器,用于判断用户是否锁定、过期等,抛出AccountStatusException的子类异常,自行实现
     */
    private final UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();

    /**
     * 尝试使用用户名和密码对用户进行身份验证。
     * @param authentication the authentication request object.
     * @return 包含凭据的完全经过身份验证的对象。
     * @throws AuthenticationException 认证失败异常。
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
        // TODO: 编写自定义业务逻辑,验证用户名和密码是否正确(大致写的,仅供参考)
        // 这里写验证码、用户名、密码验证的逻辑,都知道怎么写吧
//        if (StringUtils.isEmpty(token.getCaptcha())
//                || !token.getCaptcha().equals(redisCache.getCacheObject(CacheConstants.LOGIN_CAPTCHA_KEY + "预留key"))) { //TODO 暂时没加本次验证码在redis中的key,自己加上哦
//            throw new BadCredentialsException("验证码错误");
//        }
        // 验证码验证成功后删除验证码
//        redisCache.deleteObject(CacheConstants.LOGIN_CAPTCHA_KEY + "预留key");
        if (token.getName() == null || token.getCredentials() == null) {
            throw new BadCredentialsException("用户名或密码为空");
        }
        // 调用业务逻辑,查询用户信息
        UserDetails userDetails = securityService.loadUserByUsername(token.getName());
        // 验证密码是否正确
        if (!bCryptPasswordEncoder.matches(token.getCredentials().toString(), userDetails.getPassword())) {
            throw new BadCredentialsException("用户名或密码错误");
        }
        // 验证用户状态 如是否锁定,是否过期等
        userDetailsChecker.check(userDetails);
        // 认证成功,返回Authentication对象
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, token.getCredentials(), userDetails.getAuthorities());
        authenticationToken.setDetails(token.getDetails());
        return authenticationToken;
    }

    /**
     * ProviderManager会调用这个方法判断是否使用此Provider进行认证,返回true则使用此Provider进行认证,false则跳过此Provider。
     * 此处表示UsernamePasswordAuthenticationToken类型的认证请求对象才会使用此Provider进行认证。
     * @param authentication 身份验证请求对象。
     * @return true则使用此Provider进行认证,false则跳过此Provider。
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public void setBCryptPasswordEncoder(PasswordEncoder passwordEncoder) {
        this.bCryptPasswordEncoder = passwordEncoder;
    }
}
4. 重写AbstractAuthenticationProcessingFilter(可选项)

继承此过滤器,我们就只需实现attemptAuthentication方法即可,他会帮我们实现管理安全管理器,路径验证,捕获认证异常并调用认证成功或失败的后续操作,这里可以实现自定义登录接口和获取登录相关数据如username,password等,该功能也可以直接写个controller(此方法别忘了需要配置放行url)实现

/**
 * 用于处理用户名密码登录请求的过滤器。
 * @author tanglang
 * @date 2024-04-16
 */
public class UsernamePasswordAuthenticationFilter  extends AbstractAuthenticationProcessingFilter {

    /**
     * 此筛选器的默认请求匹配器。
     */
    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/user/login", "POST");

    /**
     * 请求参数名,用于在请求对象中获取用户名。
     */
    public static final String USERNAME_PARAM = "username";

    /**
     * 请求参数名,用于在请求对象中获取密码。
     */
    public static final String PASSWORD_PARAM = "password";

    /**
     * 请求参数名,用于在请求对象中获取验证码。
     */
    public static final String CAPTCHA_PARAM = "captcha";

    /**
     * 是否仅支持POST请求
     */
    private boolean postOnly = true;

    public UsernamePasswordAuthenticationFilter() {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    }
    
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        // 如果仅支持POST请求,并且不是POST请求,则抛出异常
        if (postOnly && !"POST".equals(request.getMethod())) {
            throw new AuthenticationServiceException("不支持身份验证方法: " + request.getMethod());
        }
        // 获取用户名、密码、验证码
        String username = obtainUsername(request);
        username = (username != null) ? username.trim() : "";
        String password = obtainPassword(request);
        password = (password != null) ? password : "";
        String captcha = obtainCaptcha(request);
        captcha = (captcha != null) ? captcha : "";
        // 创建UsernamePasswordAuthenticationToken,存入用户名、密码并设置验证码
        UsernamePasswordAuthenticationToken authenticationToken = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
        authenticationToken.setCaptcha(captcha);
        // 存储细节,可以是IP地址、设备信息等
        authenticationToken.setDetails(this.authenticationDetailsSource.buildDetails(request));
        // 调用AuthenticationManager进行身份验证
        // AuthenticationManager就会根据他管理的provider的supports方法来判断是否支持该UsernamePasswordAuthenticationToken
        return this.getAuthenticationManager().authenticate(authenticationToken);
    }

    @Nullable
    private String obtainUsername(HttpServletRequest request) {
        return request.getParameter(USERNAME_PARAM);
    }

    @Nullable
    private String obtainPassword(HttpServletRequest request) {
        return request.getParameter(PASSWORD_PARAM);
    }

    @Nullable
    private String obtainCaptcha(HttpServletRequest request) {
        return request.getParameter(CAPTCHA_PARAM);
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }
}

5. 配置SpringSecurity相关配置
/**
 * 安全配置类
 * @author tanglang
 * @Date 2024-04-16
 */
@Configuration
public class SecurityConfigTest {

    /**
     * 注入AuthenticationManagerBuilder,用于配置认证提供者
     */
    @Autowired
    private AuthenticationManagerBuilder authenticationManagerBuilder;

    /**
     * 注入UsernamePasswordAuthenticationProvider,用于提供用户密码认证
     */
    @Autowired
    private UsernamePasswordAuthenticationProvider usernamePasswordAuthenticationProvider;

    /**
     * 注入自定义的AuthenticationSuccessHandler
     */
    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;

    /**
     * 注入自定义的AuthenticationFailureHandler
     */
    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    /**
     * 配置密码加密方式
     * 定义SpringSecurity的密码加密方式为BCryptPasswordEncoder
     * 比如在UsernamePasswordAuthenticationProvider就有用到他来对密码进行加密验证
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 配置认证管理器
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration, BCryptPasswordEncoder bCryptPasswordEncoder) throws Exception {
        //为安全管理器配置认证提供者,加入自定义的UsernamePasswordAuthenticationProvider,他使用list保存provider的所以可以添加多个provider
       	//为usernamePasswordAuthenticationProvider设置密码验证器
        usernamePasswordAuthenticationProvider.setBCryptPasswordEncoder(bCryptPasswordEncoder);
        authenticationManagerBuilder.authenticationProvider(usernamePasswordAuthenticationProvider);
        //可以继续添加其他的provider
//        authenticationManagerBuilder.authenticationProvider(phoneNumberAuthenticationProvider);
        return authenticationConfiguration.getAuthenticationManager();
    }

    /**
     * 配置认证过滤器
     */
    @Bean
    public UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
        UsernamePasswordAuthenticationFilter authenticationFilter = new UsernamePasswordAuthenticationFilter();
        //设置认证管理器
        authenticationFilter.setAuthenticationManager(authenticationManager);
        //设置登录成功和失败的处理器,分别对应AuthenticationSuccessHandler和AuthenticationFailureHandler,自行实现里面的方法就行,认证成功或失败时AbstractAuthenticationProcessingFilter会帮我们调用这两个处理器
        authenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
        authenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
        //改变登录页面的url,过滤器写了默认的,就可以不用加
//        authenticationFilter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/user/login", "POST"));
        //必须是post请求
        authenticationFilter.setPostOnly(true);
        return authenticationFilter;
    }

    /**
     * 配置安全过滤器
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter, AuthenticationManager authenticationManager) throws Exception {
        httpSecurity
                .authorizeHttpRequests(
                        registry ->
                                registry
                                        //放行的请求
                                        .requestMatchers(new AntPathRequestMatcher("/user/captchaImage", "GET"), new AntPathRequestMatcher("/favicon.ico", "GET")).permitAll()
                                        .anyRequest()
                                        .authenticated()
                )
                // 将我们自己的usernamePasswordAuthenticationFilter添加到SpringSecurity的过滤器链中
                .addFilterBefore(usernamePasswordAuthenticationFilter, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.class)
                // 自定义的认证管理器
                .authenticationManager(authenticationManager)
                // 禁用表单登录
                .formLogin(AbstractHttpConfigurer::disable)
                // 禁用httpBasic登录
                .httpBasic(AbstractHttpConfigurer::disable)
                // 禁用rememberMe
                .rememberMe(AbstractHttpConfigurer::disable)
                // 关闭csrf
                .csrf(AbstractHttpConfigurer::disable)
                // 允许跨域请求
                .cors(withDefaults())
        ;
        return httpSecurity.build();
    }
}

以上便实现了SpringSecurity的自定义登录方式,通过发起post /user/login 请求进行登录测试,可以发现登录成功之后依然不能访问受保护的资源。

一般情况,我们需要将用户信息存入redis中并返回jwt给前端,之后前端的每次请求都携带这个token,后端在过滤器中解析jwt,从redis中获取用户信息并交个SpringSecurity管理。

生成jwt以及解析

1. 生成jwt

我们选择在登陆成功之后执行的handler中生成jwt,即实现上文有提到的AuthenticationSuccessHandler接口

/**
 * 自定义登录成功处理器
 * @author tanglang
 * @Date 2024-04-02
 */
@Component
@Slf4j
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
	/**
	 * jwt的工具类,自行实现,主要就是将用户存入redis中并生成uuid作为缓存的key,将key存入jwt中
	 */
    @Autowired
    private TokenService tokenService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("登录成功:{}", authentication.getName());
        // 保存用户权限
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        loginUser.setAuthorities(authentication.getAuthorities());
        // 生成JWT Token
        String token = tokenService.createToken(loginUser);
        log.info("生成JWT Token:{}", token);
        ServletUtils.writerResponse(response, R.success("登录成功", token));
    }
}
2. 自定义JWT认证过滤器
/**
 * 自定义JWT认证过滤器
 * @author tanglang
 * @date 2024-04-08
 */
@Component
public class CustomJwtAuthenticationFilter extends OncePerRequestFilter {
   /**
	* 自行实现,主要就是解析jwt,将用户从redis中获取出来
	*/
    @Autowired
    private TokenService tokenService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        LoginUser loginUser = tokenService.getLoginUser(request);
        if(loginUser != null && SecurityContextHolder.getContext().getAuthentication() == null){
            //TODO 可以加个续期token的逻辑 暂时没写
            //...
            //认证
            UsernamePasswordAuthenticationToken authenticationToken = UsernamePasswordAuthenticationToken.authenticated(loginUser, null, loginUser.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        filterChain.doFilter(request, response);
    }
}
3. 将CustomJwtAuthenticationFilter 加入到SpringSecurity的过滤器链中

在这里插入图片描述

再次登录后,携带jwt的请求就能访问受保护的资源了。至此结束。

Spring Security 6.x系列中,默认过滤器是一组预定义的过滤器链,用于处理Web请求的安全性。默认过滤器的功能主要包括认证和授权。 首先是认证过滤器。在默认过滤器链中,最重要的认证过滤器是UsernamePasswordAuthenticationFilter,用于处理基于用户名密码的认证方式。该过滤器通过拦截登录请求并获取用户名和密码,然后验证用户的身份。 另一个常见的认证过滤器是BasicAuthenticationFilter,用于处理基本身份验证方式。它从请求的头部获取认证信息,并与存储的用户名和密码进行比较以验证用户身份。 接下来是授权过滤器。默认过滤器链中的主要授权过滤器是FilterSecurityInterceptor,它是Spring Security的核心授权过滤器。该过滤器通过使用AccessDecisionManager和SecurityMetadataSource来判断用户是否有权限访问受保护的资源。 默认过滤器链中还包括其他过滤器,如LogoutFilter用于处理注销请求,ExceptionTranslationFilter用于处理异常和错误,SessionManagementFilter用于处理会话管理等。 在Spring Security 6.x系列中,可以根据需要对默认过滤器进行自定义配置。通过自定义配置,我们可以添加、删除或修改默认过滤器,并指定它们的顺序和URL匹配规则。 总的来说,Spring Security 6.x系列中的默认过滤器提供了一组常用的过滤器链,用于处理认证和授权。通过对默认过滤器自定义配置,我们可以根据具体需求来实现更精细的安全控制。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值