9.图形验证码
图形验证码
图形验证码一般是防止恶意,人眼看起来都费劲,何况是机器。不少网站为了防止用户利用机器人自动注册、登录、灌水,都采用了验证码技术。所谓验证码,就是将一串随机产生的数字或符号,生成一幅图片, 图片里加上一些干扰, 也有目前需要手动滑动的图形验证码. 这种可以有专门去做的第三方平台. 比如极验(https://www.geetest.com/), 那么本次课程讲解主要针对图形验证码.
spring security添加验证码大致可以分为三个步骤:
1. 根据随机数生成验证码图片;
2. 将验证码图片显示到登录页面;
3. 认证流程中加入验证码校验。
Spring Security的认证校验是由UsernamePasswordAuthenticationFilter过滤器完成的,所以我们的验证码校验逻辑应该在这个过滤器之前。验证码通过后才能到后续的操作. 流程如下:
代码实现:
验证码生成类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | package com.po.controller; import com.po.domain.ImageCode; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.imageio.ImageIO; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.awt.*; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.Random; import java.util.concurrent.TimeUnit; /** * 处理生成验证码的请求 */ @RestController @RequestMapping( "/code" ) public class ValidateCodeController { public final static String REDIS_KEY_IMAGE_CODE = "REDIS_KEY_IMAGE_CODE" ; public final static int expireIn = 60; // 验证码有效时间 60s //使用sessionStrategy将生成的验证码对象存储到Session中,并通过IO流将生成的图片输出到登录页面上。 @Autowired public StringRedisTemplate stringRedisTemplate; @RequestMapping( "/image" ) public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException { //获取访问IP String remoteAddr = request.getRemoteAddr(); //生成验证码对象 ImageCode imageCode = createImageCode(); //生成的验证码对象存储到redis中 KEY为REDIS_KEY_IMAGE_CODE+IP地址 stringRedisTemplate.boundValueOps(REDIS_KEY_IMAGE_CODE + "-" + remoteAddr) .set(imageCode.getCode(), expireIn, TimeUnit.SECONDS); //通过IO流将生成的图片输出到登录页面上 ImageIO.write(imageCode.getImage(), "jpeg" , response.getOutputStream()); } /** * 用于生成验证码对象 * * @return */ private ImageCode createImageCode() { int width = 100; // 验证码图片宽度 int height = 36; // 验证码图片长度 int length = 4; // 验证码位数 //创建一个带缓冲区图像对象 BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); //获得在图像上绘图的Graphics对象 Graphics g = image.getGraphics(); Random random = new Random(); //设置颜色、并随机绘制直线 g.setColor(getRandColor(200, 250)); g.fillRect(0, 0, width, height); g.setFont( new Font( "Times New Roman" , Font.ITALIC, 20)); g.setColor(getRandColor(160, 200)); for ( int i = 0; i < 155; i++) { int x = random.nextInt(width); int y = random.nextInt(height); int xl = random.nextInt(12); int yl = random.nextInt(12); g.drawLine(x, y, x + xl, y + yl); } //生成随机数 并绘制 StringBuilder sRand = new StringBuilder(); for ( int i = 0; i < length; i++) { String rand = String.valueOf(random.nextInt(10)); sRand.append( rand ); g.setColor( new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110))); g.drawString( rand , 13 * i + 6, 16); } g.dispose(); return new ImageCode(image, sRand.toString()); } /** * 获取随机演示 * * @param fc * @param bc * @return */ private Color getRandColor( int fc, int bc) { Random random = new Random(); if (fc > 255) { fc = 255; } if (bc > 255) { bc = 255; } int r = fc + random.nextInt(bc - fc); int g = fc + random.nextInt(bc - fc); int b = fc + random.nextInt(bc - fc); return new Color(r, g, b); } } |
自定义验证码过滤器ValidateCodeFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | package com.po.filter; import com.po.controller.ValidateCodeController; import com.po.exception.ValidateCodeException; import com.po.service.impl.MyAuthenticationService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * 自定义验证码过滤器,OncePerRequestFilter 一次请求只会经过一次过滤器 */ @Service public class ValidateCodeFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 判断是否是登录请求 if (request.getRequestURI().equals( "/login" ) && request.getMethod().equalsIgnoreCase( "post" )) { String imageCode = request.getParameter( "imageCode" ); System.out.println(imageCode); // 具体的验证流程 try { validate(request, imageCode); } catch (ValidateCodeException e) { myAuthenticationService.onAuthenticationFailure(request, response, e); return ; } } // 如果不是登录请求,直接放行 filterChain.doFilter(request, response); } @Autowired StringRedisTemplate stringRedisTemplate; @Autowired MyAuthenticationService myAuthenticationService; private void validate(HttpServletRequest request, String imageCode) { // 从redis中获取验证码 String redisKey = ValidateCodeController.REDIS_KEY_IMAGE_CODE + "-" + request.getRemoteAddr(); String redisImageCode = stringRedisTemplate.boundValueOps(redisKey).get(); // 验证码的判断 if (!StringUtils.hasText(redisImageCode)) { throw new ValidateCodeException( "验证码的值不能为空" ); } if (redisImageCode == null) { throw new ValidateCodeException( "验证码已过期" ); } if (!redisImageCode.equals(imageCode)) { throw new ValidateCodeException( "验证码不正确" ); } // 从redis中删除验证码 stringRedisTemplate. delete (redisKey); } } |
自定义验证码异常类
1 2 3 4 5 6 7 8 9 | package com.po.exception; import org.springframework.security.core.AuthenticationException; public class ValidateCodeException extends AuthenticationException { public ValidateCodeException(String msg) { super(msg); } } |
security配置类(里面添加我们自定义过滤器的顺序)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | package com.po.config; import com.po.filter.ValidateCodeFilter; import com.po.service.impl.MyAuthenticationService; import com.po.service.impl.MyUserDetailsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import javax.sql.DataSource; @Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired private MyUserDetailsService myUserDetailsService; @Autowired ValidateCodeFilter validateCodeFilter; /** * http请求方法 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { /** http.httpBasic() //开启httpBasic认证 .and().authorizeRequests().anyRequest().authenticated(); //所有请求都需要认证之后访问 */ // 将验证码过滤器添加在UsernamePasswordAuthenticationFilter过滤器的前面 http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter. class ); /* http.formLogin().loginPage("/login.html")//开启表单认证 // .and().authorizeRequests() //放行登录页面 // .anyRequest().authenticated(); // .and().authorizeRequests().antMatchers("/login.html").permitAll() //放行登录页面 .and().authorizeRequests().antMatchers("/toLoginPage").permitAll() //放行登录页面 .anyRequest().authenticated();*/ http.formLogin() //开启表单认证 .loginPage( "/toLoginPage" ) // 自定义登陆页面 .loginProcessingUrl( "/login" ) //表单提交路径 .usernameParameter( "username" ).passwordParameter( "password" ) //自定义input额name值和password .successForwardUrl( "/" ) //登录成功之后跳转的路径 .successHandler(myAuthenticationService) // 登录成功处理 .failureHandler(myAuthenticationService) //登录失败处理 .and().logout().logoutUrl( "/logout" ) //退出 .logoutSuccessHandler(myAuthenticationService) //退出后处理 .and().authorizeRequests().antMatchers( "/toLoginPage" ).permitAll() //放行登录页面 .anyRequest().authenticated() .and().rememberMe() //开启记住我功能 .tokenValiditySeconds(1209600) //token失效时间,默认失效时间是两周 .rememberMeParameter( "remember-me" ) // 自定义表单name值 .tokenRepository(getPersistentTokenRepository()) //设置PersistentTokenRepository .and().headers().frameOptions().sameOrigin() //加载同源域名下iframe页面 .and().csrf().disable(); //关闭csrf防护 } @Override public void configure(WebSecurity web) throws Exception { //解决静态资源被拦截的问题 web.ignoring().antMatchers( "/css/**" , "/images/**" , "/js/**" , "/code/**" ); } /** *身份安全管理器 * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(myUserDetailsService); } @Autowired DataSource dataSource; /** * 负责token与数据库之间的操作 * @return */ @Bean public PersistentTokenRepository getPersistentTokenRepository(){ JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl(); tokenRepository.setDataSource(dataSource); //设置数据源 tokenRepository.setCreateTableOnStartup( false ); //启动时帮助我们自动创建一张表,第一次启动设置为true,第二次启动程序的时候设置false或者注释掉; return tokenRepository; } @Autowired private MyAuthenticationService myAuthenticationService; } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY