1 github: 源码地址 2 security09 子工程访问登录页面时,显示图片验证码
登录提交前,需要输入验证码,登录认证流程校验验证码正确性,校验失败提示验证码错误
4.0.0 com.yzm security 0.0.1-SNAPSHOT ../pom.xml security09 0.0.1-SNAPSHOT jar security09 Demo project for Spring Boot com.yzm common 0.0.1-SNAPSHOT org.springframework.boot spring-boot-maven-plugin
项目结构
application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test04?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
username: root
password: root
main:
allow-bean-definition-overriding: true
mybatis-plus:
mapper-locations: classpath:/mapper
@Slf4j
public class VerifyServlet extends HttpServlet {
private static final long serialVersionUID = -5051097528828603895L;
private final int width = 100;
private final int height = 30;
private final int codeCount = 4;
private int codeX;
private int codeY;
private int fontHeight;
private final int interLine = 12;
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'};
@Override
public void init() throws ServletException {
//width-4 除去左右多余的位置,使验证码更加集中显示,减得越多越集中。
//codeCount+1 等比分配显示的宽度,包括左右两边的空格
codeX = (width - 4) / (codeCount + 1);
//height - 10 集中显示验证码
fontHeight = height - 10;
codeY = height - 7;
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, java.io.IOException {
// 定义图像buffer
BufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// 获取Graphics对象,便于对图像进行各种绘制操作
Graphics2D gd = buffImg.createGraphics();
// 背景白色
gd.setColor(Color.LIGHT_GRAY);
gd.fillRect(0, 0, width, height);
// 设置字体,字体的大小应该根据图片的高度来定。
gd.setFont(new Font("Times New Roman", Font.PLAIN, fontHeight));
// 画边框。
gd.setColor(Color.BLACK);
gd.drawRect(0, 0, width - 1, height - 1);
// 随机产生干扰线,使图象中的认证码不易被其它程序探测到。
gd.setColor(Color.gray);
Random random = new Random();
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用于保存随机产生的验证码,以便用户登录后进行验证。
StringBuilder randomCode = new StringBuilder();
int red, green, blue;
// 随机产生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());
log.info("验证码:" + randomCode);
// 禁止图像缓存。
response.setContentType("image/jpeg");
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
// 将图像输出到Servlet输出流中。
ServletOutputStream sos = response.getOutputStream();
ImageIO.write(buffImg, "jpeg", sos);
sos.close();
}
}
注入Servlet,设置对应的请求地址
package com.yzm.security09.config;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("home");
registry.addViewController("/home").setViewName("home");
registry.addViewController("/auth/login").setViewName("login");
registry.addViewController("/401").setViewName("401");
}
@Bean
public ServletRegistrationBean initServletRegistrationBean() {
return new ServletRegistrationBean<>(new VerifyServlet(),"/auth/getVerifyCode");
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("
@Slf4j
public class UsernamePasswordCodeAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public UsernamePasswordCodeAuthenticationFilter() {
super();
}
public UsernamePasswordCodeAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String inputVerify = request.getParameter("verifyCode");
log.info("用户输入:" + inputVerify);
//这个validateCode是在servlet中存入session的名字
String validateCode = (String) request.getSession().getAttribute("validateCode");
if (!validateCode.equalsIgnoreCase(inputVerify)) {
throw new AuthenticationServiceException("验证码不一致");
}
String username = obtainUsername(request);
String password = obtainPassword(request);
if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
throw new AuthenticationServiceException("用户名密码错误");
}
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authToken);
return this.getAuthenticationManager().authenticate(authToken);
}
}
6 SecurityConfig 配置
将自定义的UsernamePasswordCodeAuthenticationFilter注入Security框架并代替默认的UsernamePasswordAuthenticationFilter
自定义的UsernamePasswordCodeAuthenticationFilter构造器需要AuthenticationManager
package com.yzm.security09.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Slf4j
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
public SecurityConfig(@Qualifier("secUserDetailsServiceImpl") UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 从数据库读取用户、并使用密码编码器解密
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//配置资源权限规则
@Override
protected void configure(HttpSecurity http) throws Exception {
UsernamePasswordCodeAuthenticationFilter codeAuthenticationFilter = new UsernamePasswordCodeAuthenticationFilter(authenticationManagerBean());
codeAuthenticationFilter.setAuthenticationSuccessHandler(new LoginSuccessHandler());
codeAuthenticationFilter.setAuthenticationFailureHandler(new LoginFailureHandler());
http
// 关闭CSRF跨域
.csrf().disable()
.addFilterAt(codeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
// 登录
.formLogin()
.loginPage("/auth/login") //指定登录页的路径,默认/login
.loginProcessingUrl("/login") //指定自定义form表单请求的路径(必须跟login.html中的form action=“url”一致)
.permitAll()
.and()
.exceptionHandling()
.accessDeniedPage("/401") // 拒接访问跳转页面
.and()
// 退出登录
.logout()
.permitAll()
.and()
// 访问路径URL的授权策略,如注册、登录免登录认证等
.authorizeRequests()
.antMatchers("/", "/home", "/register", "/auth/login").permitAll() //指定url放行
.antMatchers("/auth/getVerifyCode").permitAll() //放行验证码请求
.anyRequest().authenticated() //其他任何请求都需要身份认证
;
}
}
上面登录成功处理和登录失败处理,如下
package com.yzm.security09.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
public class LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
log.info("登录成功");
response.sendRedirect(request.getContextPath() + "/");
}
}
package com.yzm.security09.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.info("登录失败:" + exception.getMessage());
response.sendRedirect(request.getContextPath() + "/auth/login?verify");
}
}
7 测试
启动项目,访问登录页
输入错误验证码
再次输入正确的验证码



