SpringSecurity 添加验证码的两种方式
一 验证码生产
<dependency> <groupId>com.github.penggle</groupId> <artifactId>kaptcha</artifactId> <version>2.3.2</version> </dependency>
@Configuration public class KaptchaConfig { @Bean Producer kaptcha() { Properties properties = new Properties(); properties.setProperty("kaptcha.image.width", "150"); properties.setProperty("kaptcha.image.height", "50"); properties.setProperty("kaptcha.textproducer.char.string", "0123456789"); properties.setProperty("kaptcha.textproducer.char.length", "4"); Config config = new Config(properties); DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); defaultKaptcha.setConfig(config); return defaultKaptcha; } }
@RestController public class LoginController { @Autowired Producer producer; @GetMapping("/vc.jpg") public void getVerifyCode(HttpServletResponse resp, HttpSession session) throws IOException { resp.setContentType("image/jpeg"); String text = producer.createText(); session.setAttribute("kaptcha", text); BufferedImage image = producer.createImage(text); try(ServletOutputStream out = resp.getOutputStream()) { ImageIO.write(image, "jpg", out); } } }
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>用户登陆</title> </head> <body> <h2>登录页面</h2> <!--如果没认证--> <div sec:authorize="!isAuthenticated()">显示没认证的内容</div> <!--如果认证了--> <div sec:authorize="isAuthenticated()">显示认证的内容</div> <!-- ${session?.SPRING_SECURITY_LAST_EXCEPTION?.message} security自带的错误提示信息 --> <p th:if="${param.error}" th:text="${session?.SPRING_SECURITY_LAST_EXCEPTION?.message}" ></p> <form action="/login/doLogin" method="post"> <table> <tr> <td>用户名:</td> <td><input type="text" name="uname" value="zhangsan"></td> </tr> <tr> <td>密码:</td> <td><input type="password" name="pwd" value="123456"></td> </tr> <td>验证码:</td> <td><input type="text" name="code"> <img src="/vc.jpg" style="height:33px;cursor:pointer;" onclick="this.src=this.src"> <span th:text="${session.errorMsg}" style="color: #FF0000;"></span> </td> <tr> <td colspan="2"> <button type="submit">登录</button> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"> </td> </tr> </table> </form> </body>
tips:
1.页面地址传参 login?error=true ,接收 param
<!-- ${session?.SPRING_SECURITY_LAST_EXCEPTION?.message} security自带的错误提示信息 -->
<p th:if="${param.error}" th:text="${session?.SPRING_SECURITY_LAST_EXCEPTION?.message}" ></p>
2.session传递参数
java: request.getSession().setAttribute("errorMsg", "验证码错误");
页面:<h4 th:if="${session.errorMsg}" style="color: #c41f1f;" th:text="${session.errorMsg}"></h4>
3.正常页面对象数据
java: model.addAttribute("data","SpringBoot 成功集成 Thymeleaf 模版!");
页面:<span th:text="${data}"></span>
二 添加过滤器认证
UsernamePasswordAuthenticationFilter收集用户名和密码,之前添加一个校验验证码的过滤器,通过配置加入到过滤器链。
@Component public class ValidateCodeFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { //得到请求地址 String requestURI = request.getRequestURI(); System.out.println("requestURL" + requestURI); //判断是否是登录请求 if (requestURI.equals("/login/doLogin")) { //说明当前请求为登陆 //1,得到登陆时用户输入的验证码 String code1 = request.getSession().getAttribute("kaptcha").toString(); String code = request.getParameter("kaptcha"); System.out.println("用户输入的验证码:" + code); if (StringUtils.hasText(code)) { if (code.equalsIgnoreCase(code1)) { //说明验证码正确 直接放行 request.getSession().removeAttribute("errorMSg"); filterChain.doFilter(request, response); return; } else { //说明验证码不正确,返回登陆页面 request.getSession().setAttribute("errorMsg", "验证码错误"); response.sendRedirect("/index/toLogin"); return; } } else { //用户没有输出验证码重定向到登陆页面 request.getSession().setAttribute("errorMsg", "验证码不能为空"); response.sendRedirect("/index/toLogin"); return; } } else { //说明不是登陆 直接放行到下一个过滤器 filterChain.doFilter(request, response); return; } } }
@Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { /*注入 登录成功处理器*/ @Autowired private AppAuthenticationSuccessHandler appAuthenticationSuccessHandler; /*注入 登录 失败处理器*/ @Autowired private AppAuthenticationFailureHandler appAuthenticationFailureHandler; /*注入 没有权限处理器*/ @Autowired private AppAccessDeniedHandler appAccessDeniedHandler; /*注入 登出成功处理器*/ @Autowired private AppLogoutSuccessHandler appLogoutSuccessHandler; @Autowired private AppUserDetailsService appUserDetailsService; //验证码拦截器注入 @Autowired private ValidateCodeFilter validateCodeFilter; /*配置多用户*/ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(appUserDetailsService); } /*http请求配置*/ @Override protected void configure(HttpSecurity http) throws Exception { // super.configure(http);// 不使用父类的 方法, 需要 提供登录配置 /*没权权限的处理*/ // http.exceptionHandling().accessDeniedHandler(appAccessDeniedHandler); /*登录*/ // 配置登录之前添加一个验证码的过滤器 http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class); http.formLogin() .usernameParameter("uname")//页面表单账号的参数名 默认 为 username .passwordParameter("pwd")//页面表单密码的参数名 默认 为 password .loginPage("/index/toLogin")//定义登录页面的 请求 地址(转发到登录页面) .loginProcessingUrl("/login/doLogin")// 表单提交的 地址(不需要提供),登录验证..... .successForwardUrl("/index/toIndex")//登录成功 跳转的路径 .failureForwardUrl("/index/toLogin")//登录失败 跳转的路径 // .successHandler(appAuthenticationSuccessHandler)//登录成功处理器 // .failureHandler(appAuthenticationFailureHandler)//登录失败处理器 .permitAll(); ; /*登出*/ http.logout() .logoutUrl("/logout")//登出的 请求地址 .logoutSuccessUrl("/index/toLogin")//登出成功后 访问的路径 // .logoutSuccessHandler(appLogoutSuccessHandler)//登出成功处理器 .permitAll() ; /*设置 资源所需要的 权限 (好比 门上锁)*/ http.authorizeRequests() // .mvcMatchers("/index/toLogin", "/index.html","/code/img").permitAll()//不需要认证就可以访问 .antMatchers("/code/img") // 放行验证码的路径 .permitAll() .anyRequest().authenticated()//所有请求都需要登录认证 才能进行 ; /*禁用csrf跨域请求攻击 如果不禁用 自定义的登录页面无法登录*/ http.csrf().disable(); } /*资源服务匹配放行:静态资源*/ @Override public void configure(WebSecurity web) throws Exception { // super.configure(web); web.ignoring().antMatchers("/css/**"); } /*强制要求配置 密码加密器*/ @Bean// 将对象 交给 spring容器 管理 public PasswordEncoder passwordEncoder() { // return NoOpPasswordEncoder.getInstance();//不加密 return new BCryptPasswordEncoder(); } }
三 自定义认证
UsernamePasswordAuthenticationFilter收集用户名和密码后,要往ProviderManager--DaoAuthenticationProvider--InMemoryUserDetailsManager/UserDetailsService传递。
我们可以继承原有DaoAuthenticationProvider类重写其方法,让在调用UserDetailsService之前,校验验证码。
身份认证是 AuthenticationProvider 的 authenticate 方法完成,因此验证码可以在此之前完成:
public class KaptchaAuthenticationProvider extends DaoAuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String kaptcha = req.getParameter("kaptcha"); String sessionKaptcha = (String) req.getSession().getAttribute("kaptcha"); if (kaptcha != null && sessionKaptcha != null && kaptcha.equalsIgnoreCase(sessionKaptcha)) { return super.authenticate(authentication); } throw new AuthenticationServiceException("验证码输入错误"); } }
配置 AuthenticationManager:(旧方式继承WebSecurityConfigurerAdapter )
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean AuthenticationProvider kaptchaAuthenticationProvider() { InMemoryUserDetailsManager users = new InMemoryUserDetailsManager(User.builder() .username("xiepanapn").password("{noop}123").roles("admin").build()); KaptchaAuthenticationProvider provider = new KaptchaAuthenticationProvider(); provider.setUserDetailsService(users); return provider; } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { ProviderManager manager = new ProviderManager(kaptchaAuthenticationProvider()); return manager; } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/vc.jpg").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/mylogin.html") .loginProcessingUrl("/doLogin") .defaultSuccessUrl("/index.html") .failureForwardUrl("/mylogin.html") .usernameParameter("uname") .passwordParameter("passwd") .permitAll() .and() .csrf().disable(); } }
新方式过滤器链
@Configuration @EnableWebSecurity // 添加 security 过滤器 @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) // 启用方法级别的权限认证 public class SecurityWebConfig { @Autowired(required=true) public UserDetailsService userDetailsServiceImpl; @Bean AuthenticationProvider kaptchaAuthenticationProvider() { //内存认证 //InMemoryUserDetailsManager users = new InMemoryUserDetailsManager(User.builder().username("xiepanapn").password("{noop}123").roles("admin").build()); //provider.setUserDetailsService(users); //userDetailsService认证 KaptchaAuthenticationProvider provider = new KaptchaAuthenticationProvider(); provider.setUserDetailsService(userDetailsServiceImpl); return provider; } /** * 获取AuthenticationManager(认证管理器),登录时认证使用 * @param authenticationConfiguration * @return * @throws Exception */ @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { //return authenticationConfiguration.getAuthenticationManager(); //方式一 ProviderManager manager = new ProviderManager(kaptchaAuthenticationProvider()); //方式二,(验证码) return manager; } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // 注解标记允许匿名访问的url //ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests(); //permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll()); http // 基于 web,需要 csrf (其实不用配置,默认支持) .csrf().and() // 基于 web,需要 session (其实不用配置,默认支持) .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS).and() // 下面开始设置权限过滤请求 .authorizeRequests(authorize -> authorize // 请求放开 .antMatchers("/index","/index1","/index2","/index3").permitAll() // 对于登录login 注册register 验证码captchaImage 允许匿名访问 .antMatchers("/login", "/register", "/captchaImage").permitAll() // 静态资源,可匿名访问 .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll() // 其他地址的访问均需验证权限 .anyRequest().authenticated() ); // 认证用户时用户信息加载配置,注入springAuthUserService (其实不用配置,默认实现接口即可调用) //.userDetailsService(userDetailsServiceImpl); //请求过滤规则 //指定登录表单的规则 http.formLogin() //这个路径必须和登录表单的提交路径一致。 .loginProcessingUrl("/login") //设置自定义登录界面 .loginPage("/login.html") //登录成功后转发的路径 .successForwardUrl("/main") //登录失败跳转地址 //.failureForwardUrl("/fail") .failureUrl("/login?error=true") .permitAll(); //登出配置 http.logout().logoutUrl("/logout").logoutSuccessUrl("/").clearAuthentication(true); //记住我功能 //没有权限时跳转的路径 http.exceptionHandling().accessDeniedPage("/403.html"); return http.build(); } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Bean public WebSecurityCustomizer webSecurityCustomizer() { return (web) -> web.ignoring().antMatchers("/css/**","/js/**","/img/**","/uploads/**","**/favicon.ico"); } /** * 配置跨源访问(CORS) * @return */ @Bean public CorsConfigurationSource corsConfigurationSource() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues()); return source; } }
-
配置 UserDetailsService 提供的数据源
-
提供 AuthenticationProvider 实例,并配置 UserDetailsService
-
重写 authenticationManagerBean 方法提供一个自己的 ProviderManager 并自定义 AuthenticationManager 实例。
四 在web形式的验证下,过滤器中异常的处理
上面二的例子在ValidateCodeFilter出现验证码错误的时候,直接重定向到登录页,并把错误信息返回,这样有些不太好,能不能在过滤器中出现错误了抛异常,最后由security的认证错误处理器来处理,这样是不是更好一点.
这里说security两个配置
.failureUrl("/login1?error=true1") 和 .failureHandler(customAuthenticationFailureHandler) 如果自定义登录失败跳转的地址不满足要求,可以使用登录失败处理器
@Component public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { System.out.println(222); response.setContentType("text/html; charset=UTF-8"); request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception); request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception); response.sendRedirect("/login1?error=true2"); //request.getRequestDispatcher("/login1?error=true2").forward(request, response); // response.setContentType("application/json;charset=UTF-8"); // response.getWriter().write("111"); } }
但如果是验证码的过滤器ValidateCodeFilter,这个过滤器在UsernamePasswordAuthenticationFilter之前,它抛出异常,似乎这个处理器处理不到.
现在ValidateCodeFilter这个验证码过滤器的异常,有两种方式
方式一:ValidateCodeFilter过滤器中就抛异常,由认证失败处理器 处理。
public class VerificationCodeException extends AuthenticationException { public VerificationCodeException(String msg) { super(msg); } public VerificationCodeException(String msg, Throwable cause) { super(msg, cause); } }
public class ValidateCodeFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, AuthenticationException { //得到请求地址 String requestURI = request.getRequestURI(); System.out.println("requestURL" + requestURI); System.out.println(request.getMethod()); //对比验证略 new VerificationCodeException("验证码输入错误..") } }
这种异常,可以尝试配置 认证失败处理器 处理 http.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint()); ---可以捕获到,但是异常信息估计被修改了,可以再试试
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { System.out.println(333); //response.sendRedirect("/login1?error=true2"); System.out.println(authException.toString()); response.setContentType("text/html; charset=UTF-8"); request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, authException); request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, authException); response.sendRedirect("/login1?error=true3"); //request.getRequestDispatcher("/login1?error=true3").forward(request, response); } }
方式二:ValidateCodeFilter过滤器中就抛异常,直接交由自定义的处理器处理 (这种就不配置认证失败处理器了 http.exceptionHandling().authenticationEntryPoint,登录失败处理器最好配置上(不配置也没关系,只是捕获其他的异常和验证码过滤器无关).failureHandler(customAuthenticationFailureHandler))
public class ValidateCodeFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, AuthenticationException { //得到请求地址 String requestURI = request.getRequestURI(); System.out.println("requestURL" + requestURI); System.out.println(request.getMethod()); //对比验证略 new CustomAuthenticationFailureHandler().onAuthenticationFailure(request, response, new VerificationCodeException("验证码输入错误..")); return; } }
@Component public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { System.out.println(222); response.setContentType("text/html; charset=UTF-8"); request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception); request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception); response.sendRedirect("/login1?error=true2"); //request.getRequestDispatcher("/login1?error=true2").forward(request, response); // response.setContentType("application/json;charset=UTF-8"); // response.getWriter().write("111"); } }
这里重点说明,重定向或跳转的路径要配置在SecurityConfig中用模糊匹配,如
.antMatchers("/login","/doLogin","/logout","/register", "/captchaImage").permitAll()改为
.antMatchers("/login","/login**","/toLogin","/doLogin","/logout","/register", "/captchaImage").permitAll()等.
转: https://xie.infoq.cn/article/a5614c477aa97be61ae2a0ee6
https://blog.csdn.net/qq_51307593/article/details/127561058
https://www.cnblogs.com/dalianpai/p/14744704.html