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,实现了自定义接口权限过滤。