1. 传统Web项目开发添加认证验证码
01. 项目依赖
<!--引入Springsecurity-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--验证码-->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
02. 登录页面 login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户登录</title>
</head>
<body>
<h1>用户登录</h1>
<form method="post" th:action="@{/doLogin}">
用户名: <input name="uname" type="text"> <br>
密码: <input name="passwd" type="text"> <br>
验证码: <input name="kaptcha" type="text"> <img alt="" th:src="@{/vc.jpg}"> <br>
<input type="submit" value="登录">
</form>
</body>
</html>
03. 欢迎页面 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>系统主页</title>
</head>
<body>
<h1>欢迎进入我的主页</h1>
</body>
</html>
04. 配置访问页面的控制器
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/index.html").setViewName("index");
registry.addViewController("/login.html").setViewName("login");
}
}
05. 验证码配置类
@Configuration
public class KaptchaConfig {
@Bean
public Producer kaptcha() {
Properties properties = new Properties();
//1.验证码宽度
properties.setProperty("kaptcha.image.width", "150");
//2.验证码高度
properties.setProperty("kaptcha.image.height", "50");
//3.验证码字符串
properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
//4.验证码长度
properties.setProperty("kaptcha.textproducer.char.length", "4");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
06. 生成验证码
@Controller
public class VerifyCodeController {
private final Producer producer;
@Autowired
public VerifyCodeController(Producer producer) {
this.producer = producer;
}
@RequestMapping("/vc.jpg")
public void verifyCode(HttpServletResponse response, HttpSession session) throws IOException {
//1.生成验证码
String verifyCode = producer.createText();
//2.保存到中 session
session.setAttribute("kaptcha", verifyCode);
//3.生成图片
BufferedImage bi = producer.createImage(verifyCode);
//4.响应图片
response.setContentType("image/png");
ServletOutputStream os = response.getOutputStream();
ImageIO.write(bi, "jpg", os);
}
}
07. 配置 SpringSecurity
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());
return inMemoryUserDetailsManager;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 开启请求的权限管理
http.authorizeRequests()
// 访问登录页面放行
.mvcMatchers("/login.html").permitAll()
// 访问验证码放行
.mvcMatchers("/vc.jpg").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
.usernameParameter("uname")
.passwordParameter("passwd")
.defaultSuccessUrl("/index.html",true)
.failureUrl("/login.html")
.and()
.csrf().disable();
}
}
访问项目,跳转到登录页面:
08. 自定义过滤器KaptchaFilter 实现验证码的验证
在UsernamePasswordAuthenticationFilter过滤器并没有实现验证码的认证:
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
因此如果想要在登录请求时验证验证码,需要自定义过滤器KaptchaFilter实现验证码验证逻辑实现验证码的验证,放在UsernamePasswordAuthenticationFilter的位置:
public class KaptchaFilter extends UsernamePasswordAuthenticationFilter {
private static final String FORM_KAPTCHA_KEY = "kaptcha";
private String kaptchaParameter = FORM_KAPTCHA_KEY;
public void setKaptchaParameter(String kaptchaParameter){
this.kaptchaParameter = kaptchaParameter;
}
public String getKaptchaParameter(){
return kaptchaParameter;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 1. 从请求中获取验证码
String verifyCode = request.getParameter(getKaptchaParameter());
// 2. 从session中获取验证码
String sessionVerifyCode = (String)request.getSession().getAttribute("kaptcha");
// 3. 比较
if(!ObjectUtils.isEmpty(verifyCode) && !ObjectUtils.isEmpty(sessionVerifyCode)
&& verifyCode.equalsIgnoreCase(sessionVerifyCode)){
return super.attemptAuthentication(request,response);
}
throw new KaptchaNotMatchException("验证码认证异常");
}
}
//自定义验证码认证异常
public class KaptchaNotMatchException extends AuthenticationException {
public KaptchaNotMatchException(String msg, Throwable cause) {
super(msg, cause);
}
public KaptchaNotMatchException(String msg) {
super(msg);
}
}
09. 配置 SpringSecurity
修改SpringSecurity配置,将KaptchaFilter放在UsernamePasswordAuthenticationFilter位置:
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());
return inMemoryUserDetailsManager;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
// 暴露自定义的authenticationManager
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
public KaptchaFilter kaptchaFilter() throws Exception {
KaptchaFilter kaptchaFilter = new KaptchaFilter();
kaptchaFilter.setFilterProcessesUrl("/doLogin");
kaptchaFilter.setUsernameParameter("uname");
kaptchaFilter.setKaptchaParameter("passwd");
kaptchaFilter.setKaptchaParameter("kaptcha");
// 认证管理器
kaptchaFilter.setAuthenticationManager(authenticationManagerBean());
// 指定认证成功后的处理
kaptchaFilter.setAuthenticationSuccessHandler(((request, response, authentication) -> {
response.sendRedirect("/index.html");
}));
// 指定认证失败后的处理
kaptchaFilter.setAuthenticationFailureHandler(((request, response, exception) -> {
response.sendRedirect("/login.html");
}));
return kaptchaFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 开启请求的权限管理
http.authorizeRequests()
.mvcMatchers("/login.html").permitAll()
.mvcMatchers("/vc.jpg").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
.usernameParameter("uname")
.passwordParameter("passwd")
.defaultSuccessUrl("/index.html",true)
.failureUrl("/login.html")
.and()
.csrf().disable();
http.addFilterAt(kaptchaFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
10. 源码分析
①直接访问:localhost:8080/doLogin
②请求首先进入认证处理入口 AbstractAuthenticationProcessingFilter 过滤器判断是否需要认证并尝试认证:
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 判断是否需要是配置的请求:kaptchaFilter.setFilterProcessesUrl("/doLogin");
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
// 调用子类的方法尝试认证
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
// Authentication failed
unsuccessfulAuthentication(request, response, ex);
}
}
}
②然后进入自定义过滤器 KaptchaFilter 进行验证码的认证:
public class KaptchaFilter extends UsernamePasswordAuthenticationFilter {
private static final String FORM_KAPTCHA_KEY = "kaptcha";
private String kaptchaParameter = FORM_KAPTCHA_KEY;
public void setKaptchaParameter(String kaptchaParameter){
this.kaptchaParameter = kaptchaParameter;
}
public String getKaptchaParameter(){
return kaptchaParameter;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 1. 从请求中获取验证码
String verifyCode = request.getParameter(getKaptchaParameter());
// 2. 从session中获取验证码
String sessionVerifyCode = (String)request.getSession().getAttribute("kaptcha");
// 3. 比较
if(!ObjectUtils.isEmpty(verifyCode) && !ObjectUtils.isEmpty(sessionVerifyCode)
&& verifyCode.equalsIgnoreCase(sessionVerifyCode)){
return super.attemptAuthentication(request,response);
}
throw new KaptchaNotMatchException("验证码认证异常");
}
}
③验证码验证通过后,进入 UsernamePasswordAuthenticationFilter 过滤器进行用户名和密码的认证:
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
2. 前后端分离开发添加验证码的认证
01. 项目依赖
<!--引入Springsecurity-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--验证码-->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
02. 验证码配置类
@Configuration
public class KaptchaConfig {
@Bean
public Producer kaptcha() {
Properties properties = new Properties();
//1.验证码宽度
properties.setProperty("kaptcha.image.width", "150");
//2.验证码高度
properties.setProperty("kaptcha.image.height", "50");
//3.验证码字符串
properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
//4.验证码长度
properties.setProperty("kaptcha.textproducer.char.length", "4");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
03. 生成验证码
@RestController
public class VerifyCodeController {
private final Producer producer;
@Autowired
public VerifyCodeController(Producer producer) {
this.producer = producer;
}
@GetMapping("/vc.jpg")
public String getVerifyCode(HttpSession session) throws IOException {
//1.生成验证码
String text = producer.createText();
//2.放入 session redis 实现
session.setAttribute("kaptcha", text);
//3.生成图片
BufferedImage bi = producer.createImage(text);
FastByteArrayOutputStream fos = new FastByteArrayOutputStream();
ImageIO.write(bi, "jpg", fos);
//4.返回 base64
return Base64.encodeBase64String(fos.toByteArray());
}
}
04. 配置 SpringSecurity
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());
return inMemoryUserDetailsManager;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 开启请求的权限管理
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
// 认证失败异常处理
.exceptionHandling()
.authenticationEntryPoint(new MyAuthenticationEntryPoint())
.and()
.csrf().disable();
}
}
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().println("请认证之后再去处理");
}
}
05. 自定义过滤器 LoginKaptchaFilter 实现验证码的验证
在UsernamePasswordAuthenticationFilter过滤器并没有实现验证码的认证:
public class LoginKaptchaFilter extends UsernamePasswordAuthenticationFilter {
public static final String FORM_KAPTCHA_KEY = "kaptcha";
private String kaptchaParameter = FORM_KAPTCHA_KEY;
public String getKaptchaParameter() {
return kaptchaParameter;
}
public void setKaptchaParameter(String kaptchaParameter) {
this.kaptchaParameter = kaptchaParameter;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
if(request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)){
try {
// 1.获取请求jsonString数据,并转为Map对象
Map<String,String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
// 获取请求中验证码
String kaptcha = userInfo.get(getKaptchaParameter());
// 获取请求中用户名
String username = userInfo.get(getUsernameParameter());
// 获取请求中密码
String password = userInfo.get(getPasswordParameter());
// 2.获取session中验证码
String sessionKaptcha = (String)request.getSession().getAttribute("kaptcha");
if(!ObjectUtils.isEmpty(sessionKaptcha) && !ObjectUtils.isEmpty(kaptcha)
&& kaptcha.equalsIgnoreCase(sessionKaptcha)){
// 3. 获取用户名和密码认证
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username,password);
setDetails(request,authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
throw new KaptchaNotMatchException("验证码不正确");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 如果请求不是json数据,调用父类的方法尝试认证
return super.attemptAuthentication(request,response);
}
}
06. 配置 SpringSecurity
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());
return inMemoryUserDetailsManager;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
// 暴露自定义的authenticationManager
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
public LoginKaptchaFilter loginKaptchaFilter() throws Exception {
LoginKaptchaFilter loginKaptchaFilter = new LoginKaptchaFilter();
loginKaptchaFilter.setFilterProcessesUrl("/doLogin");
loginKaptchaFilter.setKaptchaParameter("kaptcha");
loginKaptchaFilter.setUsernameParameter("uname");
loginKaptchaFilter.setPasswordParameter("passwd");
loginKaptchaFilter.setAuthenticationManager(authenticationManagerBean());
loginKaptchaFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
loginKaptchaFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
return loginKaptchaFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 开启请求的权限管理
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.exceptionHandling()
.authenticationEntryPoint(new MyAuthenticationEntryPoint())
.and()
.csrf().disable();
http.addFilterAt(loginKaptchaFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
测试: