- 认证(你是谁)
- 授权(你能干什么)
- 攻击防护(防止伪造身份)
第一章、 Spring Security 原理介绍
- 基于过滤器链实现认证、授权以及攻击防护。
- 绿色的过滤器,可通过配置来决定是否生效。
1. 简单示例
package com.seapp.security.browser;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @author seapp
* @date 2020/8/6 15:35
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
/**
* httpBasic():basic登录方式
* formLogin():指定登录方式为表单认证
* and():添加以下授权规则
* authorizeRequests():认证权限
* anyRequest():任何请求
* authenticated():认证后才能访问
*/
// http.httpBasic()
http.formLogin()
.and()
.authorizeRequests()
.anyRequest()
.authenticated();
}
}
对于任何请求都需认证后才能访问:
第二章、 基于Spring Security的默认实现开发“用户名+密码”认证
- 处理用户信息获取逻辑 : UserDetailsService
- 处理用户校验逻辑 :UserDetails
- 处理密码加密解密 : PasswordEncoder
package com.seapp.security.browser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* @author seapp
* @date 2020/8/6 16:30
*/
@Component
public class MyUserDetailsService implements UserDetailsService {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
public PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("username:" + username);
//根据用户名称查找用户信息
return new User(username,passwordEncoder.encode("123456")
//写死权限,工具类将字符串封装为需要的权限校验需要的对象
, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
1. 处理用户信息获取逻辑
实现UserDetailsService接口,在loadUserByUsername方法中可获取用户信息
2. 处理用户校验逻辑
package org.springframework.security.core.userdetails;
public interface UserDetails extends Serializable {
//用户权限信息
Collection<? extends GrantedAuthority> getAuthorities();
//用户名
String getUsername();
//账户是在有效期
boolean isAccountNonExpired();
//账户是否锁定
boolean isAccountNonLocked();
//密码是否在有效期
boolean isCredentialsNonExpired();
//账户是否被删除
boolean isEnabled();
}
3.处理密码加密解密
默认密码加密方式的配置
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
第四章、 个性化用户认证流程
1. 自定义登录页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录页面</title>
</head>
<body>
<h3>登录页面</h3>
<form action="/authentication/form" method="post">
<table>
<tr>
<td>用户名</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密码</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td colspan="2">
<button type="submit">登录</button>
</td>
</tr>
</table>
</form>
</body>
</html>
2. 处理不同登录的请求
- 来自PC端,针对html的请求,当认证失败时,返回html登录页面信息。
- 来自移动端(Android/Ios),当认证失败时,返回json响应数据
当访问到达时,根据请求方式的不同,响应不同的结果
package com.seapp.security.browser;
/**
* @author seapp
* @date 2020/8/6 22:39
*/
@RestController
public class BrowserSecurityController {
@Autowired
private SecurityProperties securityProperties;
private Logger logger = LoggerFactory.getLogger(getClass());
private RequestCache requestCache = new HttpSessionRequestCache();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
/**
* 当需要认证的时候,调用该方法
* @param request
* @param response
* @return
*/
@RequestMapping("/authentication/require")
@ResponseStatus(code = HttpStatus.UNAUTHORIZED)
public SimpleResponse requireAuthentication(HttpServletRequest request,
HttpServletResponse response) throws IOException {
SavedRequest savedRequest = requestCache.getRequest(request,response);
if(savedRequest != null){
String targetUrl = savedRequest.getRedirectUrl();
logger.info("引发跳转的请求是:" + targetUrl);
if(StringUtils.endsWithIgnoreCase(targetUrl,".html")){
redirectStrategy.sendRedirect(request,response,securityProperties.getBrowser().getLoginPage());
}
}
return new SimpleResponse("访问的服务需要身份认证,请引导用户到登录页面");
}
}
对响应json结果的封装
package com.seapp.security.browser.support;
/**定义封装好的返回对象
* @author seapp
* @date 2020/8/6 22:53
*/
public class SimpleResponse {
public SimpleResponse(Object content) {
this.content = content;
}
private Object content;
public Object getContent() {
return content;
}
public void setContent(Object content) {
this.content = content;
}
}
3. 作为基类,当第三方调用该封装框架是,传入自定义的登录页面实现
- 通过在application.properties中配置seapp.security.browser.loginPage来实现自定义界面
seapp.security.browser.loginPage=/demo-signin.html
- Security-core中对配置属性的获取
①:core中目录结构
②:对BrowserProperties的定义
package com.seapp.security.core.properties;
/**
* @author seapp
* @date 2020/8/6 23:03
*/
public class BrowserProperties {
//当第三方引用未设定自定义登录页时,使用默认的登录页
private String loginPage = "/seapp-signin.html";
public String getLoginPage() {
return loginPage;
}
public void setLoginPage(String loginPage) {
this.loginPage = loginPage;
}
}
③:通过SecurityProperties对配置属性的获取
package com.seapp.security.core.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author seapp
* @date 2020/8/6 23:03
*/
@ConfigurationProperties(prefix = "seapp.security")
public class SecurityProperties {
private BrowserProperties browser = new BrowserProperties();
public BrowserProperties getBrowser() {
return browser;
}
public void setBrowser(BrowserProperties browser) {
this.browser = browser;
}
}
④:配置读取器
package com.seapp.security.core;
import com.seapp.security.core.properties.SecurityProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @author seapp
* @date 2020/8/6 23:07
*/
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)//让设置的配置读取器生效
public class SecurityConfig {
}
结合以下 两个类配置属性的引用,实现对自定义登录页面的加载
4. 自定义登录成功处理
AuthenticationSuccessHandler
package com.seapp.security.browser.authentication;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.seapp.security.core.properties.LoginResponseType;
import com.seapp.security.core.properties.SecurityProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author seapp
* @date 2020/8/8 10:21
*/
@Component("seappAuthenticationSuccessHandler")
public class SeappAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private ObjectMapper objectMapper;
@Autowired
private SecurityProperties securityProperties;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response
, Authentication authentication) throws ServletException, IOException {
logger.info("登录成功");
if(String.valueOf(LoginResponseType.JSON).equals(securityProperties.getBrowser().getLoginType())){
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}else {
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
5. 自定义登录失败处理
AuthenticationFailureHandler
package com.seapp.security.browser.authentication;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.seapp.security.browser.support.SimpleResponse;
import com.seapp.security.core.properties.LoginResponseType;
import com.seapp.security.core.properties.SecurityProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author seapp
* @date 2020/8/7 23:42
*/
@Component("seappAuthenticationFailureHandler")
public class SeappAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private ObjectMapper objectMapper;
@Autowired
private SecurityProperties securityProperties;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
logger.info("登录失败");
if(LoginResponseType.JSON.equals(securityProperties.getBrowser().getLoginType())){
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse(exception.getMessage())));
}else {
super.onAuthenticationFailure(request, response, exception);
}
}
}
6. 自定义成功及识别处理的配置:
第五章、认证流程源码级详解
- 认证处理流程说明
- 认证结果如何在多个请求之间共享
- 获取认证用户信息
第六章 实现图形验证码功能
1. 开发生成图形验证码接口
- 根据随机数生成图片
- 将随机数存到Session中
- 将生成的图片写到接口的响应中
- 在认证流程中加入图形验证码校验
package com.seapp.security.core.validate;
import com.seapp.security.core.validate.code.ImageCode;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.request.ServletWebRequest;
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;
/**
* 调用Controller中/code/image接口,生成随机图片,并将图片信息存到session中。
* @author seapp
* @date 2020/8/7 11:32
*/
@RestController
public class ValidateCodeController {
public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@GetMapping("/code/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
ImageCode imageCode = creaetImageCode(request);
sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
}
/**
* 生成随机验证图片
* @param request
* @return
*/
private ImageCode creaetImageCode(HttpServletRequest request) {
int width = 120;// 定义图片的width
int height = 45;// 定义图片的height
int codeCount = 4;// 定义图片上显示验证码的个数
int xx = 22;
int fontHeight = 35;
int codeY = 35;
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', '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',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
// 定义图像buffer
BufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics gd = buffImg.getGraphics();
// 创建一个随机数生成器类
Random random = new Random();
// 将图像填充为白色
gd.setColor(Color.WHITE);
gd.fillRect(0, 0, width, height);
// 创建字体,字体的大小应该根据图片的高度来定。
Font font = new Font("Fixedsys", Font.BOLD, fontHeight);
// 设置字体。
gd.setFont(font);
// 画边框。
gd.setColor(Color.BLACK);
gd.drawRect(0, 0, width - 1, height - 1);
// 随机产生50条干扰线,使图象中的认证码不易被其它程序探测到。
gd.setColor(Color.BLACK);
for (int i = 0; i < 50; 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用于保存随机产生的验证码,以便用户登录后进行验证。
StringBuffer randomCode = new StringBuffer();
int red = 0, green = 0, blue = 0;
// 随机产生codeCount数字的验证码。
for (int i = 0; i < codeCount; i++) {
// 得到随机产生的验证码数字。
String code = String.valueOf(codeSequence[random.nextInt(codeSequence.length)]);
// 产生随机的颜色分量来构造颜色值,这样输出的每位数字的颜色值都将不同。
red = random.nextInt(255);
green = random.nextInt(255);
blue = random.nextInt(255);
// 用随机产生的颜色将验证码绘制到图像中。
gd.setColor(new Color(red, green, blue));
gd.drawString(code, (i + 1) * xx, codeY);
// 将产生的四个随机数组合在一起。
randomCode.append(code);
}
ImageCode imageCode = new ImageCode(buffImg,randomCode.toString(),60);
return imageCode;
}
}
- ImageCode类实现对图片信息的封装
package com.seapp.security.core.validate.code;
import java.awt.image.BufferedImage;
import java.time.LocalDateTime;
/**
* @author seapp
* @date 2020/8/7 11:19
*/
public class ImageCode {
private BufferedImage image;
private String code;
private LocalDateTime expireTime;
/**
* 当传递过期时间参数时,获取具体的过期时间
* @param image
* @param code
* @param expireIn
*/
public ImageCode(BufferedImage image, String code, int expireIn) {
this.image = image;
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
}
public ImageCode(BufferedImage image, String code, LocalDateTime expireTime) {
this.image = image;
this.code = code;
this.expireTime = expireTime;
}
public boolean isExpired(){
return LocalDateTime.now().isAfter(expireTime);
}
public BufferedImage getImage() {
return image;
}
public void setImage(BufferedImage image) {
this.image = image;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public LocalDateTime getExpireTime() {
return expireTime;
}
public void setExpireTime(LocalDateTime expireTime) {
this.expireTime = expireTime;
}
}
- 请求中发送获取到随机图片的code值,在用户名密码校验之前添加图片校验的过滤器,当图片校验成功后再执行用户名与密码的校验过滤
package com.seapp.security.core.validate.code;
import com.seapp.security.core.validate.ValidateCodeController;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
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;
/**
* @author seapp
* @date 2020/8/7 23:08
*
* 继承 OncePerRequestFilter过滤器,保障该过滤器只执行一次。
* 实现对图片码的校验,在该类中可引入自定义的失败处理器。
*
*/
@Component
public class ValidateCodeFilter extends OncePerRequestFilter {
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if(StringUtils.equals("/authentication/form",request.getRequestURI())
&& StringUtils.equals("POST",request.getMethod())){
//登录请求,执行图片校验
try{
validate(new ServletWebRequest(request));
}catch (ValidateCodeException exception){
authenticationFailureHandler.onAuthenticationFailure(request,response,exception);
return;
}
}
//若不满足上述条件,则不是登录请求,则执行后续的请求
filterChain.doFilter(request,response);
}
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
ImageCode codeInSession = (ImageCode) sessionStrategy
.getAttribute(request, ValidateCodeController.SESSION_KEY);
String codeInRequest = ServletRequestUtils
.getStringParameter(request.getRequest(),"imageCode");
if(StringUtils.isBlank(codeInRequest)){
throw new ValidateCodeException("验证码的值不能为空");
}
if(codeInSession == null){
throw new ValidateCodeException("验证码不存在");
}
if(codeInSession.isExpired()){
//将过期的验证码在session中移除
sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY);
throw new ValidateCodeException("验证码已过期");
}
if(StringUtils.equals(codeInSession.getCode(),codeInRequest)){
throw new ValidateCodeException("验证码不匹配");
}
//校验全部通过后,移除在session中校验码
sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY);
}
public AuthenticationFailureHandler getAuthenticationFailureHandler() {
return authenticationFailureHandler;
}
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
public SessionStrategy getSessionStrategy() {
return sessionStrategy;
}
public void setSessionStrategy(SessionStrategy sessionStrategy) {
this.sessionStrategy = sessionStrategy;
}
}
- 图片认证码校验过滤器的配置:
2. 图片校验代码重构,支持第三方应用以及配置
- 图片验证码的基本参数可配置(图片大小、验证码长度、有效期时间等)
- 验证码拦截的接口可配置(可定义那些接口拦截、那些接口放行)
- 验证码的生成逻辑可配置
定义ImageCodeProperties配置接收类,参数包含图片宽度、高度、校验码长度、以及拦截接口
package com.seapp.security.core.properties;
/**
* @author seapp
* @date 2020/8/8 16:05
*/
public class ImageCodeProperties {
private int width = 120;// 定义图片的width
private int height = 45;// 定义图片的height
private int codeCount = 4;// 定义图片上显示验证码的个数
private int expireIn = 60;//过期时间
private String url;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int getCodeCount() {
return codeCount;
}
public void setCodeCount(int codeCount) {
this.codeCount = codeCount;
}
public int getExpireIn() {
return expireIn;
}
public void setExpireIn(int expireIn) {
this.expireIn = expireIn;
}
}
*对其进行封装,最后至于SecurityProperties配置类中
@ConfigurationProperties(prefix = "seapp.security")//该配置定义application.properties配置类中的前缀
package com.seapp.security.core.properties;
/**
* @author seapp
* @date 2020/8/6 23:03
*/
public class ValidateCodeProperties {
ImageCodeProperties image = new ImageCodeProperties();
public ImageCodeProperties getImage() {
return image;
}
public void setImage(ImageCodeProperties image) {
this.image = image;
}
}
package com.seapp.security.core.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author seapp
* @date 2020/8/6 23:03
*/
@ConfigurationProperties(prefix = "seapp.security")
public class SecurityProperties {
private BrowserProperties browser = new BrowserProperties();
private ValidateCodeProperties code = new ValidateCodeProperties();
public ValidateCodeProperties getCode() {
return code;
}
public void setCode(ValidateCodeProperties code) {
this.code = code;
}
public BrowserProperties getBrowser() {
return browser;
}
public void setBrowser(BrowserProperties browser) {
this.browser = browser;
}
}
配置示例如图所示
参数的获取,以及使用。参数获取中包括对接口请求中传递的宽、高参数的获取。
package com.seapp.security.core.validate.code;
import com.seapp.security.core.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.ServletRequestUtils;
import javax.servlet.http.HttpServletRequest;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;
/**
* @author seapp
* @date 2020/8/8 16:48
*/
public class ImageCodeGenerator implements ValidateCodeGenerator {
private SecurityProperties securityProperties;
@Override
public ImageCode creaetImageCode(HttpServletRequest request) {
//从请求参数中获取width与height的值,若没有使用配置项中的。配置项也那不到的话,使用默认配置。
int width = ServletRequestUtils.getIntParameter(request,"width"
,securityProperties.getCode().getImage().getWidth());// 定义图片的width
int height = ServletRequestUtils.getIntParameter(request,"height"
,securityProperties.getCode().getImage().getHeight());// 定义图片的height
int codeCount = securityProperties.getCode().getImage().getCodeCount();// 定义图片上显示验证码的个数
int xx = 22;
int fontHeight = 35;
int codeY = 35;
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', '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',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
// 定义图像buffer
BufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics gd = buffImg.getGraphics();
// 创建一个随机数生成器类
Random random = new Random();
// 将图像填充为白色
gd.setColor(Color.WHITE);
gd.fillRect(0, 0, width, height);
// 创建字体,字体的大小应该根据图片的高度来定。
Font font = new Font("Fixedsys", Font.BOLD, fontHeight);
// 设置字体。
gd.setFont(font);
// 画边框。
gd.setColor(Color.BLACK);
gd.drawRect(0, 0, width - 1, height - 1);
// 随机产生50条干扰线,使图象中的认证码不易被其它程序探测到。
gd.setColor(Color.BLACK);
for (int i = 0; i < 50; 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用于保存随机产生的验证码,以便用户登录后进行验证。
StringBuffer randomCode = new StringBuffer();
int red = 0, green = 0, blue = 0;
// 随机产生codeCount数字的验证码。
for (int i = 0; i < codeCount; i++) {
// 得到随机产生的验证码数字。
String code = String.valueOf(codeSequence[random.nextInt(codeSequence.length)]);
// 产生随机的颜色分量来构造颜色值,这样输出的每位数字的颜色值都将不同。
red = random.nextInt(255);
green = random.nextInt(255);
blue = random.nextInt(255);
// 用随机产生的颜色将验证码绘制到图像中。
gd.setColor(new Color(red, green, blue));
gd.drawString(code, (i + 1) * xx, codeY);
// 将产生的四个随机数组合在一起。
randomCode.append(code);
}
//通过securityProperties对象获取配置的过期时间
ImageCode imageCode = new ImageCode(buffImg,randomCode.toString()
,securityProperties.getCode().getImage().getExpireIn());
return imageCode;
}
public SecurityProperties getSecurityProperties() {
return securityProperties;
}
public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
}
** 可拦截接口的配置,这个需在ValidateFilter过滤器中实现**
/**
* @author seapp
* @date 2020/8/7 23:08
*
* 继承 OncePerRequestFilter过滤器,保障该过滤器只执行一次。
* 实现接口InitializingBean,是可以在最后去组装urls参数
*
*/
@Component
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Autowired
private SecurityProperties securityProperties;
//该工具类实现对正则字符串的匹配
private AntPathMatcher pathMatcher = new AntPathMatcher();
/**
* 拦截器中接收传递过来的需要过滤的url参数
*/
private Set<String> urls = new HashSet<>();
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
String[] configUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(
securityProperties.getCode().getImage().getUrl(),",");
for (String configUrl:configUrls) {
urls.add(configUrl);
}
//登录接口是必须校验的,所以在读取完配置后,默认将该接口参数添加上。
urls.add("/authentication/form");
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
boolean action = false;
for (String url:urls){
//当传递的url符合匹配参数时,将action值置位true,进行图片校验
if(pathMatcher.match(url,request.getRequestURI())){
action = true;
}
}
if(action){
//登录请求,执行图片校验
try{
validate(new ServletWebRequest(request));
}catch (ValidateCodeException exception){
authenticationFailureHandler.onAuthenticationFailure(request,response,exception);
return;
}
}
//若不满足上述条件,则不是登录请求,则执行后续的请求
filterChain.doFilter(request,response);
}
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
ImageCode codeInSession = (ImageCode) sessionStrategy
.getAttribute(request, ValidateCodeController.SESSION_KEY);
String codeInRequest = ServletRequestUtils
.getStringParameter(request.getRequest(),"imageCode");
if(StringUtils.isBlank(codeInRequest)){
throw new ValidateCodeException("验证码的值不能为空");
}
if(codeInSession == null){
throw new ValidateCodeException("验证码不存在");
}
if(codeInSession.isExpired()){
//将过期的验证码在session中移除
sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY);
throw new ValidateCodeException("验证码已过期");
}
if(StringUtils.equals(codeInSession.getCode(),codeInRequest)){
throw new ValidateCodeException("验证码不匹配");
}
//校验全部通过后,移除在session中校验码
sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY);
}
public AuthenticationFailureHandler getAuthenticationFailureHandler() {
return authenticationFailureHandler;
}
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
public SessionStrategy getSessionStrategy() {
return sessionStrategy;
}
public void setSessionStrategy(SessionStrategy sessionStrategy) {
this.sessionStrategy = sessionStrategy;
}
public SecurityProperties getSecurityProperties() {
return securityProperties;
}
public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
}
注意:验证码逻辑的可配置
- 实现接口
package com.seapp.security.core.validate.code;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/** 供外部实现校验码图片的生成逻辑
* @author seapp
* @date 2020/8/8 16:47
*/
public interface ValidateCodeGenerator {
ImageCode creaetImageCode(HttpServletRequest request);
}
- 提供默认实现
ImageCodeGenerator实现类(文档上方有) - 通过JavaBean类配置实现可替换逻辑
package com.seapp.security.core.validate.code;
import com.seapp.security.core.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author seapp
* @date 2020/8/8 17:01
*/
@Configuration
public class ValidateCodeConfig {
@Autowired
private SecurityProperties securityProperties;
@Bean
@ConditionalOnMissingBean(name = "imageCodeGenerator")
//在加载该注册bean时,若容器中已经有了该名称命名的bean类,则不再加加载。
public ValidateCodeGenerator imageCodeGenerator(){
ImageCodeGenerator imageCodeGenerator = new ImageCodeGenerator();
imageCodeGenerator.setSecurityProperties(securityProperties);
return imageCodeGenerator;
}
}
- 测试类实现,需要实现ValidateCodeGenerator接口,并在Ioc中注册名称指定为"imageCodeGenerator"(在配置类中有标注),即可使用图片检验码自定义
package com.seapp.validator;
import com.seapp.security.core.validate.code.ImageCode;
import com.seapp.security.core.validate.code.ValidateCodeGenerator;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* @author seapp
* @date 2020/8/8 17:13
*/
@Component("imageCodeGenerator")
public class DemoValidateCodeGenerator implements ValidateCodeGenerator {
@Override
public ImageCode creaetImageCode(HttpServletRequest request) {
System.out.println("更高级的图形校验码生成工具类");
return null;
}
}