1、功能实现
访问登录页面时,显示图片验证码
登录提交前,需要输入验证码,登录认证流程校验验证码正确性,校验失败提示验证码错误
2、security09 子工程
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.yzm</groupId>
<artifactId>security</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <!-- lookup parent from repository -->
</parent>
<artifactId>security09</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>security09</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>com.yzm</groupId>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.192.128:3306/testdb?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
username: root
password: 1234
main:
allow-bean-definition-overriding: true
mybatis-plus:
mapper-locations: classpath:/mapper/*Mapper.xml
type-aliases-package: com.yzm.security09.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
3、生成验证码,存储到session中
package com.yzm.security09.config;
import lombok.extern.slf4j.Slf4j;
import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;
/**
* 生成验证码并存储在Session中
*/
@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;
/**
* 第一个字符的x轴值,因为后面的字符坐标依次递增,所以它们的x轴值是codeX的倍数
*/
private int codeX;
/**
* codeY ,验证字符的y轴值,因为并行所以值一样
*/
private int codeY;
/**
* 字体高度
*/
private int fontHeight;
/**
* 干扰线数量
*/
private final int interLine = 12;
/**
* 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'};
/**
* 初始化验证图片属性
*/
@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();
}
}
在SecurityConfig中注入Servlet,设置对应的请求地址
/**
* 注入验证码servlet
* servlet可拦截指定url路径,添加自定义操作
*/
@Bean
public ServletRegistrationBean<VerifyServlet> initServletRegistrationBean() {
return new ServletRegistrationBean<>(new VerifyServlet(),"/auth/getVerifyCode");
}
.antMatchers("/auth/getVerifyCode").permitAll() //放行验证码请求
4、登录页显示验证码,刷新验证码
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录页</title>
</head>
<body>
<h1 th:if="${param.logout}">You have been logged out.</h1>
<h1 th:if="${param.error}">You username or password is wrong.</h1>
<h1 th:if="${param.verify}">Verification codes are inconsistent</h1>
<h2>用户名密码登录</h2>
<form th:action="@{/login}" method="post">
<div><label> username : <input type="text" name="username"/> </label></div>
<div><label> password : <input type="password" name="password"/> </label></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> Remember me on this computer.</div>
<div><input type="submit" value="Sign In"/></div>
</form>
<script>
function refresh(obj) {
obj.src = "getVerifyCode?" + Math.random();
}
function mouseover(obj) {
obj.style.cursor = "pointer";
}
</script>
</body>
</html>
5、拦截验证码
在获取请求中的用户名密码时,校验验证码,那么需要定义Filter继承UsernamePasswordAuthenticationFilter重写attemptAuthentication()方法,具体如下
package com.yzm.security09.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 校验验证码
*/
@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、登录成功、登录失败处理
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、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;
}
/**
* 密码编码器
* passwordEncoder.encode是用来加密的,passwordEncoder.matches是用来解密的
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 配置用户
* 指定默认从哪里获取认证用户的信息,即指定一个UserDetailsService接口的实现类
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 从数据库读取用户、并使用密码编码器解密
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
/**
* 注入验证码servlet
* servlet可拦截指定url路径,添加自定义操作
*/
@Bean
public ServletRegistrationBean<VerifyServlet> initServletRegistrationBean() {
return new ServletRegistrationBean<>(new VerifyServlet(),"/auth/getVerifyCode");
}
@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() //其他任何请求都需要身份认证
;
}
}
8、测试
启动项目,访问登录页
输入错误验证码
输入正确的验证码