前言
使用SpringSecurity默认的登录是没有验证码的,但是如果没有验证码,被暴力破解还是挺危险的,所以很有必要为SpringSecurity加一层验证码保护。
实现
可以通过给SpringSecurity加一个前置过滤器,来校验验证码来实现。
配置文件如下SecurityConfiguration.java:
主要看configure方法里的:http.addFilterBefore(new CaptchaAuthenticationFilter(loginProcessingUrl, "login?code=-2"), UsernamePasswordAuthenticationFilter.class)
其他的按需要复制
@EnableWebSecurity public class SecurityConfiguration extends WebSecurityConfigurerAdapter { private static final String REMEMBER_KEY = "xxx-web"; @Autowired private MySecurityFilter mySecurityFilter; @Autowired private MyAuthenticationProvider myAuthenticationProvider; @Autowired private MyAccountDetailServiceImpl myAccountDetailService; @Autowired private DataSource dataSource; @Autowired private UserDetailsService userDetailsService; @Bean public PersistentTokenRepository persistentTokenRepository() { MyJdbcTokenRepositoryImpl jdbcTokenRepository = new MyJdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); jdbcTokenRepository.setCreateTableOnStartup(false); return jdbcTokenRepository; } @Bean public PersistentTokenBasedRememberMeServices rememberMeServices() { return new PersistentTokenBasedRememberMeServices(REMEMBER_KEY, userDetailsService, persistentTokenRepository()); } @Bean public MyLogoutHandler myLogoutHandler() { return new MyLogoutHandler("/login?code=1"); } @Bean public AuthenticationSuccessHandler myAuthenticationSuccessHandler() { //也可以根据实际情况来决定首页跳转到哪里,写到MyAuthenticationSuccessHandler的逻辑里去,注入service参考MyLogoutHandler return new MyAuthenticationSuccessHandler(); } @Override public void configure(HttpSecurity http) throws Exception { //防止用户名输入了中文,后面获取到了乱码 CharacterEncodingFilter filter = new CharacterEncodingFilter(); filter.setEncoding("UTF-8"); filter.setForceEncoding(true); http.addFilterBefore(filter, CsrfFilter.class); String loginProcessingUrl = "/j_spring_security_check"; http.headers().frameOptions().disable();//可以嵌套到frame中去 // 增加验证码校验的前置过滤器 http .addFilterBefore(new CaptchaAuthenticationFilter(loginProcessingUrl, "login?code=-2"), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(mySecurityFilter, FilterSecurityInterceptor.class) .authorizeRequests() .antMatchers(loginProcessingUrl, "/main/**", "/", "/logout_index", "/login_index").permitAll() //任何人都可以访问,需要把自定义的登录、登出的链接加到这里来,要不然会调到原生的 login 界面 .anyRequest().authenticated() //其他所有资源都需要认证,登陆后访问 .and().formLogin().loginPage("/login").failureHandler(new MyAuthFailHandler("/login?code=-1")).loginProcessingUrl(loginProcessingUrl).successHandler(myAuthenticationSuccessHandler()).authenticationDetailsSource(new MyWebAuthenticationDetailsSource()) // .and().rememberMe().rememberMeServices(rememberMeServices()) .and().rememberMe().key(REMEMBER_KEY).tokenRepository(persistentTokenRepository()).userDetailsService(myAccountDetailService) .and().exceptionHandling().accessDeniedPage("/login?code=-3") .and().logout().logoutUrl("/logout").logoutSuccessHandler(myLogoutHandler()).permitAll() .and().csrf().disable() ; //UserDetails实现类Account那边要 重写equals()和hashCode() http.sessionManagement().maximumSessions(5).expiredUrl("/login?code=-5").sessionRegistry(sessionRegistry()); } @Override public void configure(AuthenticationManagerBuilder auth) { auth.authenticationProvider(myAuthenticationProvider); } @Override public void configure(WebSecurity web) { //解决静态资源被拦截的问题 web.ignoring().antMatchers("/assets/**", "/dashboard/**"); web.ignoring().antMatchers("/**/fav.ico", "/favicon.ico", "/robots.txt"); web.ignoring().antMatchers("/**/captcha.jpg"); web.ignoring().antMatchers("/login", "/authweb"); web.ignoring().antMatchers("/test", "/404_error", "/generic_error", "/about"); } @Bean public SessionRegistry sessionRegistry() { SessionRegistry sessionRegistry = new SessionRegistryImpl(); return sessionRegistry; } @Bean public HttpSessionEventPublisher httpSessionEventPublisher() { return new HttpSessionEventPublisher(); } }
CaptchaAuthenticationFilter.java 如下:
主要就是对比request里的captcha和session里生成的captcha是否一致,不一致的话,跑出SessionAuthenticationException
public class CaptchaAuthenticationFilter extends GenericFilterBean { //请求路径匹配 private RequestMatcher requiresAuthenticationRequestMatcher; private String defaultFailureUrl; public CaptchaAuthenticationFilter(String filterUrl, String defaultFailureUrl) { this.requiresAuthenticationRequestMatcher = new AntPathRequestMatcher(filterUrl, "POST"); this.defaultFailureUrl = defaultFailureUrl; } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; // 不是需要过滤的路径,执行下一个过滤器 if (!requiresAuthentication(request)) { filterChain.doFilter(request, response); return; } HttpSession session = request.getSession(); String requestCode = request.getParameter("captcha"); String sessionCode = (String) session.getAttribute(Constants.SESSION_GENERATED_CAPTCHA_KEY); session.removeAttribute(Constants.SESSION_GENERATED_CAPTCHA_KEY); if (sessionCode == null || !sessionCode.equals(requestCode)) { String username = request.getParameter(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY); // String password = request.getParameter("password"); session.setAttribute(MyAuthFailHandler.LAST_USERNAME_KEY, username); // session.setAttribute(MyAuthFailHandler.LAST_AUTH_FAIL_MSG, "验证码错误"); response.sendRedirect(defaultFailureUrl); return; // throw new SessionAuthenticationException("验证码错误"); } filterChain.doFilter(request, response); } private boolean requiresAuthentication(HttpServletRequest request) { return requiresAuthenticationRequestMatcher.matches(request); } }
前端登录的页面的表单里,增加类似captcha就可以了:
<input class="" name="captcha" placeholder="输入验证码" /> <%--生成验证码--%> <img :src="captchasrc" @click="refreshCaptcha" class="img-responsive" style="min-width:120px;height: 38px;"/>
关于如何生成验证码,详见我之前的博客:[Java]Spring/SpringBoot实现验证码(Kaptcha的多种自定义使用)
验证码校验,也可以用在Spring Seciruty的自定义的登录,详见我之前的博客:[Java]SpringSecurity5实现自定义的登录和退出,包括 remember-me下次自动登录
文章评论