验证码(CAPTCHA)的全称是Complete Automated Public Turing test to tell Computers And Humans Apart,翻译过来就是“全自动区分计算机和人类的图灵测试”。通俗地讲,验证码就是为了防止恶意用户暴力重试而设置的。不管是用户注册,用户登录,还是论坛发帖,如果不加以限制,一旦某些恶意用户利用计算机发起无限重试,就容易导致系统遭到破坏。
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jdbcartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.2.0version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>com.github.pengglegroupId>
<artifactId>kaptchaartifactId>
<version>2.3.2version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-testartifactId>
<scope>testscope>
dependency>
dependencies>
@SpringBootApplication
@MapperScan("com.example.securitymybatis.dao")
public class SecurityMybatisApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityMybatisApplication.class, args);
}
@Bean
public Producer producer() { // 配置验证码参数
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");
DefaultKaptcha kaptcha = new DefaultKaptcha();
kaptcha.setConfig(new Config(properties));
return kaptcha;
}
}
@Controller
public class CaptchaController {
@Autowired
private Producer producer;
@GetMapping("/captcha.jpg")
public void getCaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("image/jpeg");
String captchaText = producer.createText();
request.getSession().setAttribute("captcha", captchaText);
BufferedImage image = producer.createImage(captchaText);
try(ServletOutputStream outputStream = response.getOutputStream()) {
ImageIO.write(image, "jpg", outputStream);
outputStream.flush();
}
}
}
这个对验证码的过滤器需放在UsernamePasswordAuthenticationFilter之前,可继承OncePerRequestFilter确保一次请求只会通过一次该过滤器
/**
* 验证码验证
*/
public class VerificationCodeFilter extends OncePerRequestFilter {
private AuthenticationFailureHandler failureHandler;
public VerificationCodeFilter(AuthenticationFailureHandler failureHandler) {
this.failureHandler = failureHandler;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 登录请求不校验验证码
if (Objects.equals(request.getRequestURI(), "/login")) {
String captcha = request.getParameter("captcha");
HttpSession session = request.getSession();
String expect = (String) session.getAttribute("captcha");
if (!StringUtils.isEmpty(expect)) {
// 随手清除验证码,无论是失败,还是成功。客户端应在登录失败时刷新验证码
session.removeAttribute("captcha");
}
if (!Objects.equals(captcha, expect)) {
try {
throw new VerifationCodeException("验证码错误!");
} catch (VerifationCodeException e) {
failureHandler.onAuthenticationFailure(request, response, e);
}
} else {
filterChain.doFilter(request, response);
}
} else {
filterChain.doFilter(request, response);
}
}
}
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/api/**").hasRole("ADMIN")
.antMatchers("/user/api/**").hasRole("USER")
.antMatchers("/app/api/**", "/captcha.jpg", "/login.html").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/login")
.failureHandler(authenticationFailureHandler())
.and().sessionManagement().maximumSessions(1)
.and().and()
.csrf().disable();
// 将过滤器配置在UsernamePasswordAuthenticationFilter之前
http.addFilterBefore(new VerificationCodeFilter(authenticationFailureHandler()), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new MessageDigestPasswordEncoder("MD5");
}
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
@Bean
public AuthenticationFailureHandler authenticationFailureHandler() {//验证失败返回JSON格式信息
return (request, response, exception) -> {
Map<String, Object> map = new HashMap<>();
map.put("code", 401);
map.put("message", "验证码错误");
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
};
}
}
启动springboot,打开http://localhost:8080/admin/api/hello访问
点击login,返回{“code”:401,“message”:“验证码错误”}
改为填写正确的账号/密码和验证码
点击login,返回hello, admin!