前言
实现图形验证码:
- 开发生成图形验证码接口
- 在认证流程中加入图形验证码校验
- 重构代码
内容
1.开发生成图形验证码接口
根据随机数生成图片
将随机数存到session中
将生成图片写到接口的响应中
1.1 验证码对象封装
放到公用模块core中,用于app,web端公用。
public class ImageCode {
private BufferedImage image;
/**
* code是一个随机数,图片是根据随机数生成的,
* 存放到session里面,后面用户提交登录请求时候要去验证的
*/
private String code;
/**
* 过期时间
*/
private LocalDateTime expireTime;
public ImageCode(BufferedImage image,String code,int expireIn){
this.image=image;
this.code=code;
/**
* 过期时间传递的参数应该是一个秒数:根据这个秒数去计算过期时间
*/
this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
}
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;
}
}
1.2.验证码逻辑处理接口
@RestController
public class ValidateCodeController {
private 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 {
/**
* 1.根据随机数生成图片
* 2.将随机数存到session中
* 3.将生成图片写到接口的响应中
*/
ImageCode imageCode = createImageCode(request);
sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);
ImageIO.write(imageCode.getImage(),"JPEG",response.getOutputStream());
}
private ImageCode createImageCode(HttpServletRequest request) {
int width = 67;
int height = 23;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
Random random = new Random();
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
g.setColor(getRandColor(160, 200));
for (int i = 0; i < 155; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
String sRand = "";
for (int i = 0; i < 4; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 13 * i + 6, 16);
}
g.dispose();
return new ImageCode(image, sRand, 60);
}
/**
* 生成随机背景条纹
*
* @param fc
* @param bc
* @return
*/
private Color getRandColor(int fc, int bc) {
Random random = new Random();
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
}
1.3.前端获取验证码
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h2>标准登录页面</h2>
<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>图形验证码</td>
<td>
<input type="text" name="imageCode">
<img src="/code/image">
</td>
</tr>
<tr>
<td colspan="2"><button type="submit">登录</button></td>
</tr>
</table>
</form>
</body>
</html>
1.4 在授权模块添加:允许验证码生成请求permitAll()
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 定义web安全配置类:覆盖config方法
* 1.参数为HttpSecurity
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
/**
* 定义了任何请求都需要表单认证
*/
http.formLogin()//表单登录---指定了身份认证方式
// .loginPage("/login.html")
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")//配置UsernamePasswordAuthenticationFilter需要拦截的请求
.successHandler(myAuthenticationSuccessHandler)//表单登录成功之后用自带的处理器
.failureHandler(myAuthenticationFailureHandler)//表单登录失败之后用自带的处理器
// http.httpBasic()//http的basic登录
.and()
.authorizeRequests()//对请求进行授权
.antMatchers("/authentication/require",securityProperties.getBrowser().getLoginPage(),"/code/image").permitAll()//对匹配login.html的请求允许访问
.anyRequest()//任何请求
.authenticated()
.and()
.csrf().disable();//都需要认证
}
}
2.在认证流程中加入图形验证码校验 过滤器
我们在实现登录请求时候 都是在实现Spring提供的接口;并且加密解密实现都是Spring已经提供给我们自己的,但是spring并没有给我们提供图形验证码,因为spring security他的基本原理就是一个过滤器链。在这个链上我们可以加入自己写的过滤器。我们在UsernamePasswordAuthticationFilter前加一个自定义的过滤器。 extends OncePerRequestFilter。实现:doFilterInternal方法。
public class ValidateCodeFilter extends OncePerRequestFilter {
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private SessionStrategy sessionStrategy;
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
logger.info("验证码过滤器:doFilterInternal: requestURI:[{}] requestMethod:[{}]",request.getRequestURI(),request.getMethod());
/**
* 如果是需要认证请求,我们进行家宴
* 如果校验失败,使用我们自定义的校验失败处理类处理
* 如果不需要认证,我们放行进入下一个Filter
*/
if(StringUtils.equals("/authentication/form",request.getRequestURI()) && StringUtils.endsWithIgnoreCase(request.getMethod(),"post")){
try{
validate(new ServletWebRequest(request));
}catch (ValidateCodeException e){
authenticationFailureHandler.onAuthenticationFailure(request,response,e);
}
}
filterChain.doFilter(request,response);
}
private void validate(ServletWebRequest servletWebRequest) throws ServletRequestBindingException {
//1.获取存放到session中的验证码
ImageCode codeInSession = (ImageCode)sessionStrategy.getAttribute(servletWebRequest, ValidateCodeController.SESSION_KEY);
//2.获取请求中的验证码
String codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "imageCode");
if(StringUtils.isBlank(codeInRequest)){
throw new ValidateCodeException("验证码的值不能为空");
}
if(codeInSession == null){
throw new ValidateCodeException("验证码不存在")
}
if(codeInSession.isExpried()){
sessionStrategy.removeAttribute(servletWebRequest,ValidateCodeController.SESSION_KEY);
throw new ValidateCodeException("验证码已过期");
}
if(StringUtils.equals(codeInSession.getCode(),codeInRequest)){
throw new ValidateCodeException("验证码不匹配")
}
sessionStrategy.removeAttribute(servletWebRequest,ValidateCodeController.SESSION_KEY);
}
}
需要把自定义的过滤器加到UsernamePasswordAuthenticationFilter前面去。
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 定义web安全配置类:覆盖config方法
* 1.参数为HttpSecurity
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
/**
* 定义了任何请求都需要表单认证
*/
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()//表单登录---指定了身份认证方式
// .loginPage("/login.html")
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")//配置UsernamePasswordAuthenticationFilter需要拦截的请求
.successHandler(myAuthenticationSuccessHandler)//表单登录成功之后用自带的处理器
.failureHandler(myAuthenticationFailureHandler)//表单登录失败之后用自带的处理器
// http.httpBasic()//http的basic登录
.and()
.authorizeRequests()//对请求进行授权
.antMatchers("/authentication/require",securityProperties.getBrowser().getLoginPage(),"/code/image").permitAll()//对匹配login.html的请求允许访问
.anyRequest()//任何请求
.authenticated()
.and()
.csrf().disable();//都需要认证
}
}
我们重启服务,测试:
1.web端输入:(http://127.0.0.1:8088/login.html)
输入用户名/密码;但是不输入验证码时候;
后台报了验证码异常:
然后走了我们自己失败处理:由于项目配置的不是json格式:
所以排除异常:跳转到SpringBoot提供的页面:
我们修改下demo的配置:
登录类型改为:JSON
然后重启服务尝试登录:此时报出的异常为:
缺点:1.打印出了堆栈信息
2.将认证的信息:用户名/密码;说明我们调用到了后端的UsernamePasswordFilter过滤器了(按理我们应该不能调用到)
针对第一个问题:在自定义错误处理器中,我们返回给前端的返回异常消息即可。
@Component("myAuthenticationFailureHandler")
public class MyAuthenticationFailureHandler 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 {
/**
* 1.第三个参数不是Authentication了,因为是登录失败抛异常了,所以是:AuthenticationException
* 2.因为是登录失败,所以我们返回的时候状态码不再是200,而是500
*/
logger.info("登录失败");
if(LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())){//JSON
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);
}
}
}
针对第二个问题:在ValidateCodeFilter校验失败后立马结束。
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
logger.info("验证码过滤器:doFilterInternal: requestURI:[{}] requestMethod:[{}]",request.getRequestURI(),request.getMethod());
/**
* 如果是需要认证请求,我们进行家宴
* 如果校验失败,使用我们自定义的校验失败处理类处理
* 如果不需要认证,我们放行进入下一个Filter
*/
if(StringUtils.equals("/authentication/form",request.getRequestURI()) && StringUtils.endsWithIgnoreCase(request.getMethod(),"post")){
try{
validate(new ServletWebRequest(request));
}catch (ValidateCodeException e){
authenticationFailureHandler.onAuthenticationFailure(request,response,e);
//抛出异常校验失败,不再走小面过滤器执行链
return;
}
}
filterChain.doFilter(request,response);
}
正常正度得到如下结果:
3. 重构代码
重构图形验证码主要是下面3个方面:
- 验证码基本参数可配置
- 验证码拦截的接口可配置
- 验证码的生成逻辑可配置
3.1 验证码基本参数可配置:
- 验证码长宽可配置
- 验证码位数可配置
- 验证码有效时间可配置
针对于基本参数配置,我们做成3级配置。
默认配置写在spring-security-core里面,
应用级别配置是自己应用的配置。
请求级配置:调用接口时传递参数配置,在各个请求时候图形验证码大小可能不一致的。
3.1.1 默认配置
将验证码长宽可配置、验证码位数可配置、验证码有效时间可配置作为属性参数写到配置中;
ImageCodeProperties:
public class ImageCodeProperties {
private int width = 67;
private int height = 23;
private int length = 4;
private int expireIn = 60;
//getter setter
}
我们后面还会讲解短信验证码,所以我们的配置做成多个类级别配置:ValidateCodeProperties;
public class ValidateCodeProperties {
private ImageCodeProperties image = new ImageCodeProperties();
public ImageCodeProperties getImage() {
return image;
}
public void setImage(ImageCodeProperties image) {
this.image = image;
}
}
最后封装到:
//此类读取配置文件里所有以yxm.security开头的配置
@ConfigurationProperties(prefix = "yxm.security")
public class SecurityProperties {
//其中yxm.security.browser开头的配置否会读取到BrowserProperties中
private BrowserProperties browser = new BrowserProperties();
private ValidateCodeProperties code = new ValidateCodeProperties();
//getter setter
}
3.1.2 应用级配置
我们在spring-security-demo应用下自己在配置文件里面配置:
yxm.security.code.image.length=6
3.1.3 请求级配置
请求级别配置就是在ValidateCodeController里面验证码的生成:从request前端获取不到值 就用默认配置,能获取到值就用前端传递过来的配置。
public ImageCode generate(ServletWebRequest request) {
//从request前端获取不到值 就用默认配置,能获取到值就用前端传递过来的配置
int width = ServletRequestUtils.getIntParameter(request.getRequest(),"width",securityProperties.getCode().getImage().getWidth());
int height = ServletRequestUtils.getIntParameter(request.getRequest(),"height",securityProperties.getCode().getImage().getHeight());
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
Random random = new Random();
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
g.setColor(getRandColor(160, 200));
for (int i = 0; i < 155; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
String sRand = "";
for (int i = 0; i < securityProperties.getCode().getImage().getLength(); i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 13 * i + 6, 16);
}
g.dispose();
return new ImageCode(image, sRand, securityProperties.getCode().getImage().getExpireIn());
}
我们将页面请求参数设置为:200,应用demo配置中改为100;
<tr>
<td>图形验证码</td>
<td>
<input type="text" name="imageCode">
<img src="/code/image?width=200">
</td>
</tr>
yxm.security.code.image.width=100
我们启动应用,发现:我们请求的url中width=200的值会覆盖掉:spring-security-demo里面的width=100的值;spring-security-demo里面的length=6的值覆盖掉系统默认的4。
3.2 验证码拦截接口可配置
目前我们验证码过滤器拦截的接口是写死的:/authentication/form
但是我们这个校验的逻辑是可以运用在多个应用上的。比如我们的用户访问接口:"/user"也需要验证码校验的,那么这个时候,我们可以把其配置到验证码拦截接口url上去。应用使用验证码时候可以指定哪些服务需要校验验证码。
我们在ImageCodeProperties里面添加:url
在ValidateCodeFilter中,我们实现InitializingBean,目的是:我门覆盖其方法:afterPropertiesSet(),做初始化操作:private Set<String> urls = new HashSet<>();初始化我们的url(我们通过读取配置的url,将其封装到urls里面);然后在doFilter中校验每一个请求是否含有上面的urls,有的话就返回true。
/**
* 验证码过滤器
* @author yexinming
* @date 2020/2/24
**/
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
private AuthenticationFailureHandler authenticationFailureHandler;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
private Logger logger = LoggerFactory.getLogger(getClass());
//需要校验的url都在这里面添加
private Set<String> urls = new HashSet<>();
private AntPathMatcher antPathMatcher = new AntPathMatcher();
//此处不用注解@Autowire 而是使用setter方法将在WebSecurityConfig设置
private SecurityProperties securityProperties;
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
//我们获取配置的ImageCodeProperties里面的url,转化为数据,添加到urls里面去
String[] configUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getImage().getUrl(), ",");
for (String configUrl:configUrls) {
urls.add(configUrl);
}
//"/authentication/form"一定会校验验证码的
urls.add("/authentication/form");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
logger.info("验证码过滤器:doFilterInternal: requestURI:[{}] requestMethod:[{}]",request.getRequestURI(),request.getMethod());
/**
* 如果是需要认证请求,我们进行家宴
* 如果校验失败,使用我们自定义的校验失败处理类处理
* 如果不需要认证,我们放行进入下一个Filter
*/
//在afterPropertiesSet执行之后,url初始化完毕之后,但是此时我们判断不能用StringUtils.equals,我们我们urls里面有 url: /user,/user/* 带星号的配置
// 用户请求有可能是/user/1、/user/2 我们需要使用Spring的 AntPathMatcher
boolean action = false;
for (String url:urls) {
//如果配置的url和请求的url相同时候,需要校验
if(antPathMatcher.match(url,request.getRequestURI())){
action = true;
}
}
if(action){
try{
validate(new ServletWebRequest(request));
}catch (ValidateCodeException e){
authenticationFailureHandler.onAuthenticationFailure(request,response,e);
//抛出异常校验失败,不再走小面过滤器执行链
return;
}
}
filterChain.doFilter(request,response);
}
private void validate(ServletWebRequest servletWebRequest) throws ServletRequestBindingException {
//1.获取存放到session中的验证码
ImageCode codeInSession = (ImageCode)sessionStrategy.getAttribute(servletWebRequest, ValidateCodeController.SESSION_KEY);
//2.获取请求中的验证码
String codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "imageCode");
if(StringUtils.isBlank(codeInRequest)){
throw new ValidateCodeException("验证码的值不能为空");
}
if(codeInSession == null){
throw new ValidateCodeException("验证码不存在");
}
if(codeInSession.isExpried()){
sessionStrategy.removeAttribute(servletWebRequest,ValidateCodeController.SESSION_KEY);
throw new ValidateCodeException("验证码已过期");
}
if(!StringUtils.equals(codeInSession.getCode(),codeInRequest)){
throw new ValidateCodeException("验证码不匹配");
}
sessionStrategy.removeAttribute(servletWebRequest,ValidateCodeController.SESSION_KEY);
}
public AuthenticationFailureHandler getAuthenticationFailureHandler() {
return authenticationFailureHandler;
}
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
public SecurityProperties getSecurityProperties() {
return securityProperties;
}
public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
}
然后我们在全局配置中:
此时我们的spring-security-demo项目的配置里面为:
yxm.security.code.image.length=6
yxm.security.code.image.width=100
yxm.security.code.image.url=/user,/user/*
启动服务:
我们不输入验证码后返回:
我们再次输入正确验证码,登录进去:
然后访问:http://127.0.0.1:8088/user得出以下结论:
再次访问:http://127.0.0.1:8088/user/1得出以下结论:
说明都被拦截住了。
3.3 验证码生成逻辑可配置
我们现在图形验证码逻辑是写死的,是一个相对简单的逻辑,有些项目可能需要更复杂的验证码,我们通过配置来替换这一段验证码。
要把一段逻辑做成可配置的,我们需要把这段逻辑封装到接口后面;
就像我们之前扩展Spring功能时候,我们要扩展SpringSecurity的一些东西,都是要去实现SpringSecurity指定的某一个接口。
所以现在我们自己也需要写一个接口,这样的话别人才好覆盖掉我们自己的逻辑。
3.3.1 声明接口
首先我们声明一个接口(校验码生成器):ValidateCodeGenerator;将Controller里面生成的方法挪动到接口里面去。
public interface ValidateCodeGenerator {
/**
* 生成验证码
* @param request
* @return
*/
ImageCode generate(ServletWebRequest request);
}
然后再写一个实现类:ImageCodeGenerator;然后将Controller里面的实现逻辑封装进去。
@Service
public class ImageCodeGenerator implements ValidateCodeGenerator {
private SecurityProperties securityProperties;
@Override
public ImageCode generate(ServletWebRequest request) {
//从request前端获取不到值 就用默认配置,能获取到值就用前端传递过来的配置
int width = ServletRequestUtils.getIntParameter(request.getRequest(),"width",securityProperties.getCode().getImage().getWidth());
int height = ServletRequestUtils.getIntParameter(request.getRequest(),"height",securityProperties.getCode().getImage().getHeight());
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
Random random = new Random();
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
g.setColor(getRandColor(160, 200));
for (int i = 0; i < 155; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
String sRand = "";
for (int i = 0; i < securityProperties.getCode().getImage().getLength(); i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 13 * i + 6, 16);
}
g.dispose();
return new ImageCode(image, sRand, securityProperties.getCode().getImage().getExpireIn());
}
/**
* 生成随机背景条纹
* @param fc
* @param bc
* @return
*/
private Color getRandColor(int fc, int bc) {
Random random = new Random();
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
public SecurityProperties getSecurityProperties() {
return securityProperties;
}
public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
}
把之前控制器里面的逻辑都删除掉,然后将ValidateCodeGenerator里面的实现逻辑注入到ValidateCodeController里面去。
@RestController
public class ValidateCodeController {
public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Autowired
private ValidateCodeGenerator imageCodeGenerator;
@GetMapping("/code/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
/**
* 1.根据随机数生成图片
* 2.将随机数存到session中
* 3.将生成图片写到接口的响应中
*/
ImageCode imageCode = imageCodeGenerator.generate(new ServletWebRequest(request));
sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);
ImageIO.write(imageCode.getImage(),"JPEG",response.getOutputStream());
}
}
我们如何把生成验证码接口的实现做成可配置的呢?我们创建一个配置Bean--->ValidateCodeBeanConfig
@Configuration
public class ValidateCodeBeanConfig {
@Autowired
private SecurityProperties securityProperties;
/*
* 这个配置与我们在ImageCodeGenerator上面加一个注解是类似的,但是这样配置灵活,
* 可以添加注解:@ConditionalOnMissingBean 作用是:在初始化这个bean的时候,
* 先到spring容器去查找imageCodeGenerator,如果有一个imageCodeGenerator时候,
* 就不会再用下面代码去创建
**/
@Bean @ConditionalOnMissingBean(name="imageCodeGenerator")
public ValidateCodeGenerator imageCodeGenerator(){//方法的名字就是放到Spring容器里bean的名字
ImageCodeGenerator imageCodeGenerator = new ImageCodeGenerator();
imageCodeGenerator.setSecurityProperties(securityProperties);
return imageCodeGenerator;
}
}
启动工程测试:我们现在没有做额外的配置。 会使用我们默认的生成器,效果就是
我们在spring-security-demo里面添加一个:DemoImageCodeGenrator,去实现ValidateCodeGenerator;Bean的名字和ValidateCodeBeanConfig里面配置的一直,这样的话在spring-security-web和spring-security-demo里面都有。
@Component("imageCodeGenrator")
public class DemoImageCodeGenrator implements ValidateCodeGenerator {
@Override
public ImageCode generate(ServletWebRequest request) {
System.out.println("更高级的图形验证码逻辑");
return null;
}
}
我们启动spring-security-demo去验证:
我们发现图形验证码没有出来,并且后台报错了:
说明走了我们demo项目里面自己的接口。
这个是高级开发人员必须掌握的技巧,这里体现了一个设计思想:以增量的方式去适应变化(当我们出现变化时候,比如图形验证码逻辑改变了,之前的逻辑不满足了,我处理的方式不是去改原来代码,而是新加了一个代码)。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。