SpringBoot3整合SpringSecurity,实现自定义接口权限过滤(二)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: SpringBoot3整合SpringSecurity,实现自定义接口权限过滤

4.3 判断账户是否被禁用

管理系统一般支持账户禁用功能,即把 status 值设定为某个状态,如 -1

接着判断是否是账户禁用异常 DisabledException如果属于账户禁用,则给与提示,代码如下。

if (exception instanceof DisabledException) {
    ResponseUtil.out(response, ResponseUtil.resultMap(false,500,"账户处于禁用状态,无法登录"));
}

用户是否禁用通常是实体类的一个字段,如 status

4.4 其他登录失败处理

如果不属于上面的三种情况,则给与通用的报错提示,代码如下。

else {
    ResponseUtil.out(response, ResponseUtil.resultMap(RESPONSE_FAIL_FLAG,RESPONSE_FAIL_CODE,"系统当前不能登录,请稍后再试"));
}

完整代码如下。

@ApiOperation(value = "登录失败回调")
@Slf4j
@Component
public class AuthenticationFailHandler extends SimpleUrlAuthenticationFailureHandler {
    @Autowired
    private ZwzLoginProperties tokenProperties;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    private static final String LOGIN_FAIL_TIMES_PRE = "LOGIN_FAIL_TIMES_PRE:";
    private static final String REQUEST_PARAMETER_USERNAME = "username:";
    private static final boolean RESPONSE_FAIL_FLAG = false;
    private static final int RESPONSE_FAIL_CODE = 500;
    @ApiOperation(value = "查询登录失败的次数")
    public boolean recordLoginTime(String username) {
        String loginFailTimeStr = stringRedisTemplate.opsForValue().get(LOGIN_FAIL_TIMES_PRE + username);
        int loginFailTime = 0;
        // 已错误次数
        if(!ZwzNullUtils.isNull(loginFailTimeStr)){
            loginFailTime = Integer.parseInt(loginFailTimeStr) + 1;
        }
        stringRedisTemplate.opsForValue().set(LOGIN_FAIL_TIMES_PRE + username, loginFailTime + "", tokenProperties.getLoginFailMaxThenLockTimes(), TimeUnit.MINUTES);
        if(loginFailTime >= tokenProperties.getMaxLoginFailTimes()){
            stringRedisTemplate.opsForValue().set("userLoginDisableFlag:"+username, "fail", tokenProperties.getLoginFailMaxThenLockTimes(), TimeUnit.MINUTES);
            return false;
        }
        return true;
    }
    @Override
    @ApiOperation(value = "登录失败回调")
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) {
        if (exception instanceof BadCredentialsException || exception instanceof UsernameNotFoundException) {
            recordLoginTime(request.getParameter(REQUEST_PARAMETER_USERNAME));
            String failTimesStr = stringRedisTemplate.opsForValue().get(LOGIN_FAIL_TIMES_PRE + request.getParameter(REQUEST_PARAMETER_USERNAME));
            //已错误的次数
            int userFailTimes = 0;
            if(!ZwzNullUtils.isNull(failTimesStr)){
                userFailTimes = Integer.parseInt(failTimesStr);
            }
            int restLoginTime = tokenProperties.getMaxLoginFailTimes() - userFailTimes;
            if(restLoginTime < 5 && restLoginTime > 0){
                ResponseUtil.out(response, ResponseUtil.resultMap(RESPONSE_FAIL_FLAG,RESPONSE_FAIL_CODE,"账号密码不正确,还能尝试登录" + restLoginTime + "次"));
            } else if(restLoginTime < 1) {
                ResponseUtil.out(response, ResponseUtil.resultMap(RESPONSE_FAIL_FLAG,RESPONSE_FAIL_CODE,"重试超限,请您" + tokenProperties.getLoginFailMaxThenLockTimes() + "分后再登录"));
            } else {
                ResponseUtil.out(response, ResponseUtil.resultMap(RESPONSE_FAIL_FLAG,RESPONSE_FAIL_CODE,"账号密码不正确"));
            }
        } else if (exception instanceof ZwzAuthException){
            ResponseUtil.out(response, ResponseUtil.resultMap(RESPONSE_FAIL_FLAG,RESPONSE_FAIL_CODE,((ZwzAuthException) exception).getMsg()));
        } else if (exception instanceof DisabledException) {
            ResponseUtil.out(response, ResponseUtil.resultMap(RESPONSE_FAIL_FLAG,RESPONSE_FAIL_CODE,"账户处于禁用状态,无法登录"));
        } else {
            ResponseUtil.out(response, ResponseUtil.resultMap(RESPONSE_FAIL_FLAG,RESPONSE_FAIL_CODE,"系统当前不能登录,请稍后再试"));
        }
    }
}

五、编写过滤器

5.1 基于 Token 的权限过滤

请同学们新建 JwtTokenOncePerRequestFilter 过滤器,继承于 OncePerRequestFilter,重写 doFilterInternal 过滤方法,代码如下。

@ApiOperation(value = "自定义权限过滤")
@Slf4j
public class JwtTokenOncePerRequestFilter extends OncePerRequestFilter {
    private SecurityUtil securityUtil;
    @Autowired
    private RedisTemplateHelper redisTemplate;
    private ZwzLoginProperties zwzLoginProperties;
    private static final boolean RESPONSE_FAIL_FLAG = false;
    private static final int RESPONSE_NO_ROLE_CODE = 401;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String tokenHeader = request.getHeader(ZwzLoginProperties.HTTP_HEADER);
        if(ZwzNullUtils.isNull(tokenHeader)){
            tokenHeader = request.getParameter(ZwzLoginProperties.HTTP_HEADER);
        }
        if (ZwzNullUtils.isNull(tokenHeader)) {
            filterChain.doFilter(request, response);
            return;
        }
        try {
            UsernamePasswordAuthenticationToken token = getUsernamePasswordAuthenticationToken(tokenHeader, response);
            SecurityContextHolder.getContext().setAuthentication(token);
        }catch (Exception e){
            log.warn("自定义权限过滤失败" + e);
        }
        filterChain.doFilter(request, response);
    }
    @ApiOperation(value = "判断登录是否失效")
    private UsernamePasswordAuthenticationToken getUsernamePasswordAuthenticationToken(String header, HttpServletResponse response) {
        String userName = null;
        String tokenInRedis = redisTemplate.get(ZwzLoginProperties.HTTP_TOKEN_PRE + header);
        if(ZwzNullUtils.isNull(tokenInRedis)){
            ResponseUtil.out(response, ResponseUtil.resultMap(RESPONSE_FAIL_FLAG,RESPONSE_NO_ROLE_CODE,"登录状态失效,需要重登!"));
            return null;
        }
        TokenUser tokenUser = JSONObject.parseObject(tokenInRedis,TokenUser.class);
        userName = tokenUser.getUsername();
        List<GrantedAuthority> permissionList = new ArrayList<>();
        if(zwzLoginProperties.getSaveRoleFlag()){
            for(String permission : tokenUser.getPermissions()){
                permissionList.add(new SimpleGrantedAuthority(permission));
            }
        } else{
            permissionList = securityUtil.getCurrUserPerms(userName);
        }
        if(!tokenUser.getSaveLogin()){
            redisTemplate.set(ZwzLoginProperties.USER_TOKEN_PRE + userName, header, zwzLoginProperties.getUserTokenInvalidDays(), TimeUnit.MINUTES);
            redisTemplate.set(ZwzLoginProperties.HTTP_TOKEN_PRE + header, tokenInRedis, zwzLoginProperties.getUserTokenInvalidDays(), TimeUnit.MINUTES);
        }
        if(!ZwzNullUtils.isNull(userName)) {
            User user = new User(userName, "", permissionList);
            return new UsernamePasswordAuthenticationToken(user, null, permissionList);
        }
        return null;
    }
    public JwtTokenOncePerRequestFilter(RedisTemplateHelper redis, SecurityUtil securityUtil,ZwzLoginProperties zwzLoginProperties) {
        this.redisTemplate = redis;
        this.securityUtil = securityUtil;
        this.zwzLoginProperties = zwzLoginProperties;
    }
}

以上代码用于判断用户的 Token 状态,依次判断用户登录是否还处于有效状态

5.2 图形验证码过滤

请同学们新建 ImageValidateFilter 过滤器,继承于 OncePerRequestFilter,重写 doFilterInternal 过滤方法。

过滤器首先需要读取需要验证的接口,如果无需验证则放行,代码如下。

Boolean filterFlag = false;
for(String requestURI : captchaProperties.getVerification()){
    if(pathMatcher.match(requestURI, request.getRequestURI())){
        filterFlag = true;
        break;
    }
}
if(!filterFlag) {
    filterChain.doFilter(request, response);
    return;
}

如果确定需要进行验证码过滤,则尝试读取验证码 ID captchaId 和输入的验证码值 code,在 Redis 进行取值判断。

过滤器需要判断验证码是否为空、是否过期、是否正确,最后给与处理,代码如下。

String verificationCodeId = request.getParameter("captchaId");
String userInputCode = request.getParameter("code");
if(ZwzNullUtils.isNull(userInputCode) || ZwzNullUtils.isNull(verificationCodeId)){
    ResponseUtil.out(response, ResponseUtil.resultMap(RESPONSE_FAIL_FLAG,RESPONSE_CODE_FAIL_CODE,"验证码为空"));
    return;
}
String codeAnsInRedis = redisTemplate.opsForValue().get(verificationCodeId);
if(ZwzNullUtils.isNull(codeAnsInRedis)){
    ResponseUtil.out(response, ResponseUtil.resultMap(RESPONSE_FAIL_FLAG,RESPONSE_CODE_FAIL_CODE,"已过期的验证码,需要重新填写"));
    return;
}
if(!Objects.equals(codeAnsInRedis.toLowerCase(),userInputCode.toLowerCase())) {
    ResponseUtil.out(response, ResponseUtil.resultMap(RESPONSE_FAIL_FLAG,RESPONSE_CODE_FAIL_CODE,"验证码不正确"));
    return;
}
redisTemplate.delete(verificationCodeId);
filterChain.doFilter(request, response);

六、WebSecurityConfig 类整合

6.1 WebSecurityConfig 类配置

请同学们创建 WebSecurityConfig 类,增加 @Configuration 注解,定义为配置类,代码如下。

@ApiOperation(value = "SpringSecurity配置类")
@Configuration
@EnableMethodSecurity
public class WebSecurityConfig {
}

接着创建 securityFilterChain 方法,配置 Spring Security。

首先是配置白名单,即配置不需要拦截的接口明细,代码如下。

.requestMatchers("/zwz/dictData/getByType/**","/zwz/file/view/**","/zwz/user/regist","/zwz/common/**","/*/*.js","/*/*.css","/*/*.png","/*/*.ico", "/swagger-ui.html")

接着配置提示登录页面和登录接口,代码如下。

.formLogin().loginPage("/zwz/common/needLogin").loginProcessingUrl("/zwz/login").permitAll()

接着配置登录成功回调处理类,代码如下。

.successHandler(authenticationSuccessHandler)

接着配置登录失败回调处理类,代码如下。

.failureHandler(authenticationFailHandler)

最后配置过滤器,包括自定义权限过滤器和图形验证码过滤器,代码如下。

.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(imageValidateFilter, UsernamePasswordAuthenticationFilter.class);

完整代码如下所示。

@ApiOperation(value = "SpringSecurity配置类")
@Configuration
@EnableMethodSecurity
public class WebSecurityConfig {
    @Autowired
    private ZwzLoginProperties zwzLoginProperties;
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;
    @Autowired
    private AuthenticationFailHandler authenticationFailHandler;
    @Autowired
    private ZwzAccessDeniedHandler zwzAccessDeniedHandler;
    @Autowired
    private ImageValidateFilter imageValidateFilter;
    @Autowired
    private RedisTemplateHelper redisTemplate;
    @Autowired
    private SecurityUtil securityUtil;
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests().requestMatchers("/zwz/dictData/getByType/**","/zwz/file/view/**","/zwz/user/regist","/zwz/common/**","/*/*.js","/*/*.css","/*/*.png","/*/*.ico", "/swagger-ui.html").permitAll()
                .and().formLogin().loginPage("/zwz/common/needLogin").loginProcessingUrl("/zwz/login").permitAll()
                .successHandler(authenticationSuccessHandler).failureHandler(authenticationFailHandler).and()
                .headers().frameOptions().disable().and()
                .logout()
                .permitAll()
                .and()
                .authorizeHttpRequests()
                .anyRequest()
                .authenticated()
                .and()
                .cors().and()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .exceptionHandling().accessDeniedHandler(zwzAccessDeniedHandler)
                .and()
                .authenticationProvider(authenticationProvider())
                .addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(imageValidateFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
    @Bean
    public UserDetailsService userDetailsService() {
        return username -> userDetailsService.loadUserByUsername(username);
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService());
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
    @Bean
    public JwtTokenOncePerRequestFilter authenticationJwtTokenFilter() throws Exception {
        return new JwtTokenOncePerRequestFilter(redisTemplate, securityUtil, zwzLoginProperties);
    }
}

6.2 测试

请同学们运行项目后端,首先使用浏览器访问 http://localhost:8081/,系统自动重定向到登录提示页面,如下图所示。

接着访问免登接口 http://localhost:8081/zwz/dictData/getByType/sex,发现可以正常读取数据。

最终,本文成功将 SpringBoot3 整合了 SpringSecurity,实现了自定义接口权限过滤。

相关文章
|
2月前
|
安全 NoSQL Java
SpringBoot接口安全:限流、重放攻击、签名机制分析
本文介绍如何在Spring Boot中实现API安全机制,涵盖签名验证、防重放攻击和限流三大核心。通过自定义注解与拦截器,结合Redis,构建轻量级、可扩展的安全防护方案,适用于B2B接口与系统集成。
490 3
|
5月前
|
算法 网络协议 Java
Spring Boot 的接口限流算法
本文介绍了高并发系统中流量控制的重要性及常见的限流算法。首先讲解了简单的计数器法,其通过设置时间窗口内的请求数限制来控制流量,但存在临界问题。接着介绍了滑动窗口算法,通过将时间窗口划分为多个格子,提高了统计精度并缓解了临界问题。随后详细描述了漏桶算法和令牌桶算法,前者以固定速率处理请求,后者允许一定程度的流量突发,更符合实际需求。最后对比了各算法的特点与适用场景,指出选择合适的算法需根据具体情况进行分析。
484 56
Spring Boot 的接口限流算法
|
8月前
|
安全 Java Apache
微服务——SpringBoot使用归纳——Spring Boot中集成 Shiro——Shiro 身份和权限认证
本文介绍了 Apache Shiro 的身份认证与权限认证机制。在身份认证部分,分析了 Shiro 的认证流程,包括应用程序调用 `Subject.login(token)` 方法、SecurityManager 接管认证以及通过 Realm 进行具体的安全验证。权限认证部分阐述了权限(permission)、角色(role)和用户(user)三者的关系,其中用户可拥有多个角色,角色则对应不同的权限组合,例如普通用户仅能查看或添加信息,而管理员可执行所有操作。
436 0
|
5月前
|
Java API 网络架构
基于 Spring Boot 框架开发 REST API 接口实践指南
本文详解基于Spring Boot 3.x构建REST API的完整开发流程,涵盖环境搭建、领域建模、响应式编程、安全控制、容器化部署及性能优化等关键环节,助力开发者打造高效稳定的后端服务。
778 1
|
9月前
|
Java Maven 开发者
编写SpringBoot的自定义starter包
通过本文的介绍,我们详细讲解了如何创建一个Spring Boot自定义Starter包,包括自动配置类、配置属性类、`spring.factories`文件的创建和配置。通过自定义Starter,可以有效地复用公共配置和组件,提高开发效率。希望本文能帮助您更好地理解和应用Spring Boot自定义Starter,在实际项目中灵活使用这一强大的功能。
769 17
|
9月前
|
监控 Java Spring
SpringBoot:SpringBoot通过注解监测Controller接口
本文详细介绍了如何通过Spring Boot注解监测Controller接口,包括自定义注解、AOP切面的创建和使用以及具体的示例代码。通过这种方式,可以方便地在Controller方法执行前后添加日志记录、性能监控和异常处理逻辑,而无需修改方法本身的代码。这种方法不仅提高了代码的可维护性,还增强了系统的监控能力。希望本文能帮助您更好地理解和应用Spring Boot中的注解监测技术。
336 16
|
8月前
|
JSON Java 数据格式
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——拦截自定义异常
本文介绍了在实际项目中如何拦截自定义异常。首先,通过定义异常信息枚举类 `BusinessMsgEnum`,统一管理业务异常的代码和消息。接着,创建自定义业务异常类 `BusinessErrorException`,并在其构造方法中传入枚举类以实现异常信息的封装。最后,利用 `GlobalExceptionHandler` 拦截并处理自定义异常,返回标准的 JSON 响应格式。文章还提供了示例代码和测试方法,展示了全局异常处理在 Spring Boot 项目中的应用价值。
412 0
|
10月前
|
XML JavaScript Java
SpringBoot集成Shiro权限+Jwt认证
本文主要描述如何快速基于SpringBoot 2.5.X版本集成Shiro+JWT框架,让大家快速实现无状态登陆和接口权限认证主体框架,具体业务细节未实现,大家按照实际项目补充。
719 11
|
11月前
|
NoSQL Java Redis
Spring Boot 自动配置机制:从原理到自定义
Spring Boot 的自动配置机制通过 `spring.factories` 文件和 `@EnableAutoConfiguration` 注解,根据类路径中的依赖和条件注解自动配置所需的 Bean,大大简化了开发过程。本文深入探讨了自动配置的原理、条件化配置、自定义自动配置以及实际应用案例,帮助开发者更好地理解和利用这一强大特性。
1836 15
|
12月前
|
安全 Java 应用服务中间件
如何将Spring Boot应用程序运行到自定义端口
如何将Spring Boot应用程序运行到自定义端口
990 0