文章目录

  • 一、添加验证码
    • 1.1 验证servlet
    • 1.2 修改 login.html
    • 1.3 添加匿名访问 Url
  • 二、AJAX 验证
  • 三、过滤器验证
    • 3.1 编写验证码过滤器
    • 3.2 注入过滤器
    • 3.3 运行程序
  • 四、Spring Security 验证
    • 4.1 WebAuthenticationDetails
    • 4.2 AuthenticationDetailsSource
    • 4.3 AuthenticationProvider
    • 4.4 运行程序

通过前面三篇文章,你应该大致了解了 Spring Security 的流程。你应该发现了,真正的 login 请求是由 Spring Security 帮我们处理的,那么我们如何实现自定义表单登录呢,比如添加一个验证码…

一、添加验证码

1.1 验证servlet

public class VerifyServlet extends HttpServlet {/*** 验证码图片的宽度。*/private int width = 100;/***  验证码图片的高度。*/private int height = 30;/*** 验证码字符个数*/private int codeCount = 4;/*** 字体高度*/private int fontHeight;/*** 干扰线数量*/private int interLine = 16;/*** 第一个字符的x轴值,因为后面的字符坐标依次递增,所以它们的x轴值是codeX的倍数*/private int codeX;/*** codeY ,验证字符的y轴值,因为并行所以值一样*/private int codeY;/*** codeSequence 表示字符允许出现的序列值*/char[] codeSequence = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J','K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W','X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };/*** 初始化验证图片属性*/@Overridepublic void init() throws ServletException {// 从web.xml中获取初始信息// 宽度String strWidth = this.getInitParameter("width");// 高度String strHeight = this.getInitParameter("height");// 字符个数String strCodeCount = this.getInitParameter("codeCount");// 将配置的信息转换成数值try {if (strWidth != null && strWidth.length() != 0) {width = Integer.parseInt(strWidth);}if (strHeight != null && strHeight.length() != 0) {height = Integer.parseInt(strHeight);}if (strCodeCount != null && strCodeCount.length() != 0) {codeCount = Integer.parseInt(strCodeCount);}} catch (NumberFormatException e) {e.printStackTrace();}//width-4 除去左右多余的位置,使验证码更加集中显示,减得越多越集中。//codeCount+1     //等比分配显示的宽度,包括左右两边的空格codeX = (width-4) / (codeCount+1);//height - 10 集中显示验证码fontHeight = height - 10;codeY = height - 7;}/*** @param request* @param response* @throws ServletException* @throws java.io.IOException*/@Overrideprotected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, java.io.IOException {// 定义图像bufferBufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);Graphics2D gd = buffImg.createGraphics();// 创建一个随机数生成器类Random random = new Random();// 将图像填充为白色gd.setColor(Color.LIGHT_GRAY);gd.fillRect(0, 0, width, height);// 创建字体,字体的大小应该根据图片的高度来定。Font font = new Font("Times New Roman", Font.PLAIN, fontHeight);// 设置字体。gd.setFont(font);// 画边框。gd.setColor(Color.BLACK);gd.drawRect(0, 0, width - 1, height - 1);// 随机产生16条干扰线,使图象中的认证码不易被其它程序探测到。gd.setColor(Color.gray);for (int i = 0; i < interLine; i++) {int x = random.nextInt(width);int y = random.nextInt(height);int xl = random.nextInt(12);int yl = random.nextInt(12);gd.drawLine(x, y, x + xl, y + yl);}// randomCode用于保存随机产生的验证码,以便用户登录后进行验证。StringBuffer randomCode = new StringBuffer();int red = 0, green = 0, blue = 0;// 随机产生codeCount数字的验证码。for (int i = 0; i < codeCount; i++) {// 得到随机产生的验证码数字。String strRand = String.valueOf(codeSequence[random.nextInt(36)]);// 产生随机的颜色分量来构造颜色值,这样输出的每位数字的颜色值都将不同。red = random.nextInt(255);green = random.nextInt(255);blue = random.nextInt(255);// 用随机产生的颜色将验证码绘制到图像中。gd.setColor(new Color(red,green,blue));gd.drawString(strRand, (i + 1) * codeX, codeY);// 将产生的四个随机数组合在一起。randomCode.append(strRand);}// 将四位数字的验证码保存到Session中。HttpSession session = request.getSession();session.setAttribute("validateCode", randomCode.toString());// 禁止图像缓存。response.setHeader("Pragma", "no-cache");response.setHeader("Cache-Control", "no-cache");response.setDateHeader("Expires", 0);response.setContentType("image/jpeg");// 将图像输出到Servlet输出流中。ServletOutputStream sos = response.getOutputStream();ImageIO.write(buffImg, "jpeg", sos);sos.close();}}

然后在 Application 中注入该 Servlet:

@SpringBootApplication
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}@Beanpublic ServletRegistrationBean servletRegistrationBean() {ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new VerifyServlet());servletRegistrationBean.addUrlMappings("/getVerifyCode");return servletRegistrationBean;}
}

1.2 修改 login.html

在原本的 Login 页面基础上加上验证码字段

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>登陆</title>
</head>
<body>
<h1>登陆</h1>
<form method="post" action="/login"><div>用户名:<input type="text" name="username"></div><div>密码:<input type="password" name="password"></div><div><input type="text" class="form-control" name="verifyCode" required="required" placeholder="验证码" /><img src="getVerifyCode" title="看不清,请点我" onclick="refresh(this)" onmouseover="mouseover(this)"  /></div><div><label><input type="checkbox" name="remember-me"/>自动登录</label><button type="submit">立即登陆</button></div>
</form>
<script>function refresh(obj) { obj.src = "getVerifyCode?" + Math.random(); }function mouseover(obj) { obj.style.cursor = "pointer"; }
</script>
</body>
</html>

1.3 添加匿名访问 Url

在 WebSecurityConfig 中允许该 Url 的匿名访问,不然没有登录是没有办法访问的:

  @Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests()// 如果有允许匿名的url,填在下面.antMatchers("/getVerifyCode").permitAll().anyRequest().authenticated().and().formLogin().loginPage("/login")// 设置登陆成功页.defaultSuccessUrl("/").permitAll().failureHandler(new SimpleUrlAuthenticationFailureHandler())//登录失败url.failureForwardUrl("/login/error").authenticationDetailsSource(authenticationDetailsSource).and()// 自定义登陆用户名和密码参数,默认为username和password// .usernameParameter("username")//  .passwordParameter("password").logout().permitAll()//基于内存自动登录.and().rememberMe().tokenRepository(persistentTokenRepository()).tokenValiditySeconds(60)   ;http.csrf().disable();}

这样验证码就加好了,运行下程序:


下面才算是这篇文章真正的部分。我们如何才能实现验证码验证呢,思考一下,应该有以下几种实现方式:

  1. 登录表单提交前发送 AJAX 验证验证码
  2. 使用自定义过滤器(Filter),在 Spring security 校验前验证验证码合法性
  3. 和用户名、密码一起发送到后台,在 Spring security 中进行验证

二、AJAX 验证

使用 AJAX 方式验证和我们 Spring Security 框架就没有任何关系了,其实就是表单提交前先发个 HTTP 请求验证验证码,本篇不再赘述。

三、过滤器验证

使用过滤器的思路是:在 Spring Security 处理登录验证请求前,验证验证码,如果正确,放行;如果不正确,调到异常。

3.1 编写验证码过滤器

自定义一个过滤器,实现 OncePerRequestFilter (该 Filter 保证每次请求一定会过滤),在 isProtectedUrl() 方法中拦截了 POST 方式的 /login 请求。

在逻辑处理中从 request 中取出验证码,并进行验证,如果验证成功,放行;验证失败,手动生成异常。
//获取当前线程绑定的request对象
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String validateCode = ((String)request.getSession().getAttribute(“validateCode”)).toUpperCase();

/*** 基于filter做的验证码* 该 Filter 保证每次请求一定会过滤* @author shuliangzhao* @Title: VerifyFilter* @ProjectName spring-boot-learn* @Description: TODO* @date 2019/8/4 16:58*/
public class VerifyFilter extends OncePerRequestFilter {private static final PathMatcher pathMatcher = new AntPathMatcher();@Overrideprotected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp, FilterChain filterChain) throws ServletException, IOException {if (isProtectedUrl(req)) {String verifyCode = req.getParameter("verifyCode");if (!validateVerify(verifyCode)) {//手动设置异常req.setAttribute("SPRING_SECURITY_LAST_EXCEPTION",new DisabledException("验证码输入错误"));// 转发到错误Urlreq.getRequestDispatcher("/login/error").forward(req,resp);} else {filterChain.doFilter(req,resp);}} else {filterChain.doFilter(req,resp);}}private boolean validateVerify(String verifyCode) {//获取当前线程绑定的request对象HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();String validateCode = ((String)request.getSession().getAttribute("validateCode")).toUpperCase();verifyCode = verifyCode.toUpperCase();System.out.println("验证码:" + validateCode + "用户输入:" + verifyCode);return validateCode.equals(verifyCode);}// 拦截 /login的POST请求private boolean isProtectedUrl(HttpServletRequest req) {return "POST".equals(req.getMethod()) && pathMatcher.match("/login",req.getServletPath());}
}

3.2 注入过滤器

修改 WebSecurityConfig 的 configure 方法,添加一个 addFilterBefore() ,具有两个参数,作用是在参数二之前执行参数一设置的过滤器。

Spring Security 对于用户名/密码登录方式是通过 UsernamePasswordAuthenticationFilter 处理的,我们在它之前执行验证码过滤器即可。

@Override
protected void configure(HttpSecurity http) throws Exception {http.authorizeRequests()// 如果有允许匿名的url,填在下面.antMatchers("/getVerifyCode").permitAll().anyRequest().authenticated().and()// 设置登陆页.formLogin().loginPage("/login")// 设置登陆成功页.defaultSuccessUrl("/").permitAll()// 登录失败Url.failureUrl("/login/error")// 自定义登陆用户名和密码参数,默认为username和password
//                .usernameParameter("username")
//                .passwordParameter("password").and().addFilterBefore(new VerifyFilter(),UsernamePasswordAuthenticationFilter.class).logout().permitAll()// 自动登录.and().rememberMe().tokenRepository(persistentTokenRepository())// 有效时间:单位s.tokenValiditySeconds(60).userDetailsService(userDetailsService);// 关闭CSRF跨域http.csrf().disable();
}

3.3 运行程序

四、Spring Security 验证

使用过滤器就已经实现了验证码功能,但其实它和 AJAX 验证差别不大。

  • AJAX 是在提交前发一个请求,请求返回成功就提交,否则不提交
  • 过滤器是先验证验证码,验证成功就让 Spring Security 验证用户名和密码;验证失败,则产生异常·。

如果我们要做的需求是用户登录是需要多个验证字段,不单单是用户名和密码,那么使用过滤器会让逻辑变得复杂,这时候可以考虑自定义 Spring Security 的验证逻辑了…

4.1 WebAuthenticationDetails

我们知道 Spring security 默认只会处理用户名和密码信息。这时候就要请出我们的主角——WebAuthenticationDetails

WebAuthenticationDetails: 该类提供了获取用户登录时携带的额外信息的功能,默认提供了 remoteAddress 与 sessionId 信息

我们需要实现自定义的 WebAuthenticationDetails,并在其中加入我们的验证码:

/*** 获取用户登录时携带的额外信息* @author shuliangzhao* @Title: CustomWebAuthenticationDetails* @ProjectName spring-boot-learn* @Description: TODO* @date 2019/8/4 17:14*/
public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {private final String verifyCode;public CustomWebAuthenticationDetails(HttpServletRequest request) {super(request);// verifyCode为页面中验证码的nameverifyCode = request.getParameter("verifyCode");}public String getVerifyCode() {return this.verifyCode;}@Overridepublic String toString() {StringBuilder sb = new StringBuilder();sb.append(super.toString()).append("; VerifyCode: ").append(this.getVerifyCode());return sb.toString();}
}

在这个方法中,我们将前台 form 表单中的 verifyCode 获取到,并通过 get 方法方便被调用。

4.2 AuthenticationDetailsSource

自定义了WebAuthenticationDetails,我i们还需要将其放入 AuthenticationDetailsSource 中来替换原本的 WebAuthenticationDetails ,因此还得实现自定义 AuthenticationDetailsSource :

/*** 该接口用于在Spring Security登录过程中对用户的登录信息的详细信息进行填充* @author shuliangzhao* @Title: CustomAuthenticationDetailsSource* @ProjectName spring-boot-learn* @Description: TODO* @date 2019/8/4 17:17*/
@Component("authenticationDetailsSource")
public class CustomAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {@Overridepublic WebAuthenticationDetails buildDetails(HttpServletRequest request) {return new CustomWebAuthenticationDetails(request);}
}

该类内容将原本的 WebAuthenticationDetails 替换为了我们的 CustomWebAuthenticationDetails。

然后我们将 CustomAuthenticationDetailsSource 注入Spring Security中,替换掉默认的 AuthenticationDetailsSource。

修改 WebSecurityConfig,将其注入,然后在config()中使用 authenticationDetailsSource(authenticationDetailsSource)方法来指定它。

private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource;@Override
protected void configure(HttpSecurity http) throws Exception {http.authorizeRequests()// 如果有允许匿名的url,填在下面.antMatchers("/getVerifyCode").permitAll().anyRequest().authenticated().and()// 设置登陆页.formLogin().loginPage("/login")// 设置登陆成功页.defaultSuccessUrl("/").permitAll()// 登录失败Url.failureUrl("/login/error")// 自定义登陆用户名和密码参数,默认为username和password
//                .usernameParameter("username")
//                .passwordParameter("password")// 指定authenticationDetailsSource.authenticationDetailsSource(authenticationDetailsSource).and().logout().permitAll()// 自动登录.and().rememberMe().tokenRepository(persistentTokenRepository())// 有效时间:单位s.tokenValiditySeconds(60).userDetailsService(userDetailsService);// 关闭CSRF跨域http.csrf().disable();

4.3 AuthenticationProvider

至此我们通过自定义WebAuthenticationDetails和AuthenticationDetailsSource将验证码和用户名、密码一起带入了Spring Security中,下面我们需要将它取出来。

这里需要我们自定义AuthenticationProvider,需要注意的是,如果是我们自己实现AuthenticationProvider,那么我们就需要自己做密码校验了

/*** @author shuliangzhao* @Title: CustomAuthenticationProvider* @ProjectName spring-boot-learn* @Description: TODO* @date 2019/8/4 17:20*/
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {@Autowiredprivate CustomUserDetailsService userDetailsService;@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {// 获取用户输入的用户名和密码String name = authentication.getName();String inputPassword = authentication.getCredentials().toString();CustomWebAuthenticationDetails details = (CustomWebAuthenticationDetails) authentication.getDetails();String verifyCode = details.getVerifyCode();if(!validateVerify(verifyCode)) {throw new DisabledException("验证码输入错误");}//userDetails为数据库中查询到的用户信息UserDetails userDetails = userDetailsService.loadUserByUsername(name);if (!userDetails.getPassword().equals(inputPassword)) {throw new BadCredentialsException("密码错误");}return new UsernamePasswordAuthenticationToken(name, inputPassword, userDetails.getAuthorities());}private boolean validateVerify(String verifyCode) {//获取当前线程绑定的request对象HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();String validateCode = ((String)request.getSession().getAttribute("validateCode")).toUpperCase();verifyCode = verifyCode.toUpperCase();System.out.println("验证码:" + validateCode + "用户输入:" + verifyCode);return validateCode.equals(verifyCode);}@Overridepublic boolean supports(Class<?> authentication) {// 这里不要忘记,和UsernamePasswordAuthenticationToken比较return authentication.equals(UsernamePasswordAuthenticationToken.class);}}

最后在 WebSecurityConfig 中将其注入,并在 config 方法中通过 auth.authenticationProvider() 指定使用。

 @Autowiredprivate CustomAuthenticationProvider customAuthenticationProvider;@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.authenticationProvider(customAuthenticationProvider);}

4.4 运行程序

是不是比较复杂,为了实现该需求自定义了 WebAuthenticationDetails、AuthenticationDetailsSource、AuthenticationProvider,让我们运行一下程序,当输入错误验证码时:

SpringBoot 整合Security——自定义表单登录相关推荐

  1. SpringBoot整合activiti5-业务表单

    系列文章目录(springboot整合activiti5) 在实际的开发当中,除了简单的业务逻辑之外,还有更为复杂的业务,例如常见的主从表单,总之采用Activiti的内置表单和外置表单方式无法满足所 ...

  2. shiro表单登录认证及退出(自定义form认证器)

    博主地址:http://blog.csdn.net/zcl_love_wx 注意:此文是基于springMVC框架的,所以关于springMVC的配置这里不说,后面有时间专门写一个shiro整合spr ...

  3. springboot整合security,mybatisPlus,thymeleaf实现登录认证及用户,菜单,角色权限管理

    介绍 本系统为springboot整合security,mybatisPlus,thymeleaf实现登录认证及用户,菜单,角色权限管理.页面为极简模式,没有任何渲染. 源码:https://gite ...

  4. 基于springboot+vue的开源自定义表单问卷系统

    一.项目简介 基于springboot+vue的开源自定义表单问卷系统 二.实现功能 支持表单拖拽 支持各种控件操作(基础组件.进阶组件等) 基础组件包含文本.多行文本.图片.图形.日历控件 支持拖拽 ...

  5. springsecurity不拦截某个接口_SpringSecurity 默认表单登录页展示流程源码

    SpringSecurity 默认表单登录页展示流程源码 本篇主要讲解 SpringSecurity提供的默认表单登录页 它是如何展示的的流程,涉及1.FilterSecurityIntercepto ...

  6. springboot整合security实现权限控制

    1.建表,五张表,如下: 1.1.用户表 CREATE TABLE `t_sys_user` (`user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT ...

  7. html表单输入框添加验证码,织梦Dedecms为自定义表单添加验证码功能

    使用织梦Dedecms自定义表单的时候,即使你做了字段的验证,也很有可能被人刷很多垃圾的内容,更加安全的一个方法是为自定义表单添加上验证码功能.今天我就来为大家分享一下怎样给自定义表单添加验证码! 一 ...

  8. DedeCMS实现自定义表单提交后发送指定QQ邮箱法

    https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&tn=monline_3_dg&wd=dedecms 邮箱&oq=d ...

  9. Angular19 自定义表单控件

    1 需求 当开发者需要一个特定的表单控件时就需要自己开发一个和默认提供的表单控件用法相似的控件来作为表单控件:自定义的表单控件必须考虑模型和视图之间的数据怎么进行交互 2 官方文档 -> 点击前 ...

最新文章

  1. 爱了!Intellij IDEA 自带的 Vim 插件真心不错
  2. linux之ps命令详解
  3. ASP.NET Core Docker部署
  4. 货车运输(洛谷P1967)(倍增)
  5. 基于Java+SpringBoot+vue+node.js等疫情网课管理系统详细设计和实现
  6. 开源身份证识别_新的开源:金钱,公司和身份
  7. Android5.0 netd架构流程
  8. 存储过程和存储函数和触发器示例
  9. Json字符串和Java对象互相转换
  10. 圆周移位是怎么移的_【装修干货】马桶移位改造全攻略,总有一天你能用得上!赶紧收藏...
  11. stm32神舟I号开发板下的六子棋开发
  12. Verilog语言乒乓球机8段译码器
  13. Java—P1957口算练习题
  14. linux考试中的7654_Excel工作表G8单元格的值为7654.375,执行某些操作之后,在G8单元格中显示一串“”符号,说明G8单元格的()...
  15. 电阻电容电感二极管三极管在电路中的作用
  16. Windows 10快速删除大量回收站文件以及由此引起的回收站右键清空反应慢问题的解决
  17. 怎么把html表格转化为excel,导出html到excel表格数据格式-如何将html转换成excel
  18. 伪装游戏软件变成计算机,注意!Steam上这款游戏是伪装的病毒:把玩家PC变成矿机...
  19. mac笔记本常用快捷键
  20. 新一代垃圾回收器—ZGC

热门文章

  1. AI理论知识整理(10)-向量空间与矩阵(1)
  2. rust(16)-数组
  3. wxWidgets随笔(2)-hello,world
  4. 【深度学习】神经网络基础:反向传播算法
  5. 【推荐系统入门】一窥推荐系统的原理
  6. 【Python】五种Pandas图表美化样式汇总
  7. 【机器学习基础】一文归纳AI调参炼丹之法
  8. 【机器学习基础】数学推导+纯Python实现机器学习算法23:kmeans聚类
  9. 爸,这下你还敢抽烟么?
  10. 技术干货 | JavaScript 之事件循环(Event Loop)