SpringOauth初级开发三 自定义认证页面中增加图像验证码

因为不但自定义认证页面中需要图像验证码认证,手机APP图像验证码认证,所以在QIQIHAL-SECURITY-BROWSER和QIQIHAL-SECURITY-APP项目的模块,QIQIHAL-SECURITY-CORE中实现自定义认证页面中增加图像验证码功能,在QIQIHAL-SECURITY-CORE\src\main\java\com\qiqihal路径下创建validatecode文件夹,在该文件夹下创建AbstractValidateCodeGenerator.java文件、ValidateCode.java文件、ValidateCodeException.java文件、ValidateCodeConfig.java文件、ValidateCodeController.java文件和ValidateCodeGenerator.java文件这六个文件

SpringOauth初级开发三 自定义认证页面中增加图像验证码_第1张图片
image.png

ValidateCode.java文件的作用是抽取图像验证码实体中的重复代码,以便短信验证码实体重用

package com.qiqihal.validatecode;

import lombok.Data;
import lombok.experimental.Accessors;

import java.time.LocalDateTime;

@SuppressWarnings("serial")
@Data
@Accessors(chain = true)
public class ValidateCode {

    //验证码
    private String validateCode;

    //过期时间
    private LocalDateTime expireTime;

    //在构造方法中传入过期秒数,LocalDateTime.now()获取现在时间,与过期秒数相加,生成过期时间
    public ValidateCode(String validateCode, int expireSecond) {
        this.validateCode = validateCode;
        this.expireTime = expireTime.now().plusSeconds(expireSecond);
    }

    //如果过期时间在当前日期之前,则验证码过期
    public boolean isExpried(){
        return LocalDateTime.now().isAfter(expireTime);
    }
}

AbstractValidateCodeGenerator.java文件的作用是实现ValidateCodeGenerator接口,使用抽象类实现接口,再使用其他的类继承抽象类,这样就可以选择性实现接口中的某一个方法

QIQIHAL-SECURITY-CORE\src\main\java\com\qiqihal\validatecode\AbstractValidateCodeGenerator.java文件内容如下

package com.qiqihal.validatecode;

import com.qiqihal.validatecode.imagevalidatecode.ImageValidateCode;
import com.qiqihal.validatecode.smsvalidatecode.SmsValidateCode;
import org.springframework.web.context.request.ServletWebRequest;

public abstract class AbstractValidateCodeGenerator implements ValidateCodeGenerator{
    
    @Override
    public ImageValidateCode imageValidateCodeGenerate(ServletWebRequest request) {
        return null;
    }

    @Override
    public SmsValidateCode smsValidateCodeGenerate(ServletWebRequest request){
        return null;
    }
}

ValidateCodeException.java文件的作用是处理图像验证码、短信验证码在验证码校验不成功时抛出的异常,ValidateCodeException类继承AuthenticationException类,AuthenticationException是SpringSecurity中的抽象异常,是所有认证过程中抛出异常的基类,继承AuthenticationException必须实现该类中的一个构造方法,ValidateCodeException只有继承AuthenticationException类,才能作为AuthenticationFailureHandler接口的onAuthenticationFailure方法的参数,在QIQIHAL-SECURITY-BROWSER\src\main\java\com\qiqihal\config\securityconfig\QiqihalSecurityConfig类的configure方法中,将ImageValidateCodeFilter过滤器加入到SpringSecurity过滤链中并放在UsernamePasswordAuthenticationFilter过滤器前面时,传入实现AuthenticationFailureHandler接口的认证失败后的自定义处理类,一旦validate方法抛出异常,将ValidateCodeException异常传入实现AuthenticationFailureHandler接口的认证失败后的自定义处理类的onAuthenticationFailure方法中,执行相关逻辑

QIQIHAL-SECURITY-CORE\src\main\java\com\qiqihal\validatecode\ValidateCodeException.java文件内容如下

package com.qiqihal.validatecode;

import org.springframework.security.core.AuthenticationException;

/*AuthenticationException是SpringSecurity中的抽象异常,是所有认证过程中抛出异常的基类,
继承AuthenticationException必须实现该类中的一个构造方法,ValidateCodeException只有继承AuthenticationException类,
才能作为AuthenticationFailureHandler接口的onAuthenticationFailure方法的参数,
在QIQIHAL-SECURITY-BROWSER\src\main\java\com\qiqihal\config\securityconfig\QiqihalSecurityConfig类的configure方法中,
将ImageValidateCodeFilter过滤器加入到SpringSecurity过滤链中并放在UsernamePasswordAuthenticationFilter过滤器前面时,
传入实现AuthenticationFailureHandler接口的认证失败后的自定义处理类,
一旦validate方法抛出异常,
将ValidateCodeException异常传入实现AuthenticationFailureHandler接口的认证失败后的自定义处理类,
执行相关逻辑*/
public class ValidateCodeException extends AuthenticationException {

    //在Java序列化与反序列化时,使用serialVersionUID来验证版本一致性
    private static final long serialVersionUID = 1L;

    public ValidateCodeException(String msg) {
        super(msg);
    }
}

ValidateCodeConfig.java文件是配置类,ValidateCodeConfig配置类将ImageValidateCodeGenerator注入到Spring容器中,与直接在ImageValidateCodeGenerator类上加入@Compomet注解效果是一样的,但ValidateCodeConfig配置类的好处是可以使用@ConditionalOnMissingBean,在Spring容器初始化之前,会寻找是否已经有"名为@ConditionalOnMissingBean注解参数的实体",注入到Spring容器中,如果有就使用这个"名为@ConditionalOnMissingBean注解参数的实体",如果没有就执行@ConditionalOnMissingBean修饰的方法,把方法中创建的实体注入到spring容器中,@ConditionalOnMissingBean注解实现"增量方式去适应变化"的思想,当逻辑出现变化,处理方式不是改原来的代码,而是去增加新的代码代替旧的代码
QIQIHAL-SECURITY-CORE\src\main\java\com\qiqihal\validatecode\ValidateCodeConfig.java内容如下

package com.qiqihal.validatecode;

import com.qiqihal.properties.SecurityProperties;
import com.qiqihal.validatecode.imagevalidatecode.ImageValidateCodeGenerator;
import com.qiqihal.validatecode.imagevalidatecode.ValidateCodeGenerator;
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;

@Configuration
/*创建ValidateCodeConfig配置类将ImageValidateCodeGenerator注入到Spring容器中,
与直接在ImageValidateCodeGenerator类上加入@Compomet注解效果是一样的,
但ValidateCodeConfig配置类的好处是可以使用@ConditionalOnMissingBean,
在Spring容器初始化之前,会寻找是否已经有"名为@ConditionalOnMissingBean注解参数的实体",
注入到Spring容器中,如果有就使用这个"名为@ConditionalOnMissingBean注解参数的实体",
如果没有就执行@ConditionalOnMissingBean修饰的方法,把方法中创建的实体注入到spring容器中,
@ConditionalOnMissingBean注解实现"增量方式去适应变化"的思想,当逻辑出现变化,处理方式不是改原来的代码,
而是去增加新的代码代替旧的代码
*/
public class ValidateCodeConfig {

    @Autowired
    SecurityProperties securityProperties;

    @Bean
    @ConditionalOnMissingBean(name = "imageValidateCodeGenerator")
    public ValidateCodeGenerator imageValidateCodeGenerator(){
        ImageValidateCodeGenerator imageValidateCodeGenerator = new ImageValidateCodeGenerator();
        imageValidateCodeGenerator.setSecurityProperties(securityProperties);
        return imageValidateCodeGenerator;
    }
}

ValidateCodeController.java文件的作用是获取图像验证码,通过自定义认证页面login.html中的的img标签的src属性请求该对象的"/code/image"获取图像验证码

QIQIHAL-SECURITY-CORE\src\main\java\com\qiqihal\validatecode\imagevalidatecode\ValidateCodeController.java文件内容如下

package com.qiqihal.validatecode;

import com.qiqihal.validatecode.ValidateCodeGenerator;
import org.springframework.beans.factory.annotation.Autowired;
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.ServletWebRequest;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RestController
public class ValidateCodeController {

    @Autowired
    ValidateCodeGenerator imageValidateCodeGenerator;

    //HttpSessionSessionStrategy,操作session
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    //将保存图像的图像缓冲区保存到session,SESSION_KEY作为key
    public final static String SESSION_KEY = "SESSION_KEY_VALIDATE_CODE_";

    @GetMapping("/code/image")
    public void createCodeImage(HttpServletRequest request, HttpServletResponse response)throws Exception{
        //获得保存图像的图像缓冲区
        ImageValidateCode imageValidateCode = imageValidateCodeGenerator.generate(new ServletWebRequest(request));
        //以SESSION_KEY作为key,ImageCode作为value保存到session中
        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY +"IMAGE", imageValidateCode);
        //从BufferedImage图像缓冲区中取出保存的图像,用输出流生成jpeg图片
        ImageIO.write(imageValidateCode.getImage(),"JPEG",response.getOutputStream());
    }

}

ValidateCodeGenerator.java文件的作用是为了使验证码生成逻辑可配置,将生成验证码的方法抽象在接口中,可切换多种生成图像验证码的方法,使验证码生成逻辑可扩展、未来增加生成短信验证码额方法也会抽象再这个接口中

QIQIHAL-SECURITY-CORE\src\main\java\com\qiqihal\validatecode\ValidateCodeGenerator.java文件内容如下

package com.qiqihal.validatecode;

import com.qiqihal.validatecode.imagevalidatecode.ImageValidateCode;
import org.springframework.web.context.request.ServletWebRequest;

/*为了使验证码生成逻辑可配置,将生成验证码的方法抽象在接口中*/
public interface ValidateCodeGenerator {

    public ImageValidateCode generate(ServletWebRequest request);
}

在QIQIHAL-SECURITY-CORE\src\main\java\com\qiqihal\validatecode路径下创建imagevalidatecode文件夹,在该文件夹下创建ImageValidateCode.java文件、ImageValidateCodeFilter.java文件、ImageValidateCodeGenerator.java文件这三个文件

SpringOauth初级开发三 自定义认证页面中增加图像验证码_第2张图片
image.png

ImageValidateCode.java文件中BufferedImage image属性的作用是保存生成图像数据,String code属性的作用是保存验证码,LocalDateTime expireTime属性的作用是保存过期时间,并将保存这些数据的ImageValidateCode实体保存到Session中,在将ImageValidateCode实体保存到Session之后,还会使用输出流将BufferedImage image属性中保存的图像数据生成jpeg图片,在构造方法实例化ImageValidateCode实体时,传入过期时间的秒数作为参数,会在ValidateCode父类的构造方法上将LocalDateTime expireTime属性的时间增加传入过期时间的秒数,最终ImageValidateCodeFilter.java文件中的validate方法上从Session中取出保存的ImageValidateCode实体中的expireTime过期时间,与当前时间相比,如果当前时间超过保存的ImageValidateCode实体中的expireTime过期时间,则抛出ValidateCodeException异常类,将ValidateCodeException异常传入实现AuthenticationFailureHandler接口的认证失败后的自定义处理类QiqihalAuthenticationFailureHandler的onAuthenticationFailure方法中,证失败后的自定义处理类,将认证失败信息,和自定义认证页面请求路径,封装成Json数据返回前端SpringSecurity过滤链就会终止,不会进入,包括SpringSecurity过滤链上的过滤器

QIQIHAL-SECURITY-CORE\src\main\java\com\qiqihal\validatecode\imagevalidatecode\ImageValidateCode.java文件内容如下

package com.qiqihal.validatecode.imagevalidatecode;

import com.qiqihal.validatecode.ValidateCode;
import lombok.Data;
import lombok.experimental.Accessors;
import java.awt.image.BufferedImage;

@SuppressWarnings("serial")
@Data
@Accessors(chain = true)
public class ImageValidateCode extends ValidateCode{

    /*保存生成图像数据*/
    private BufferedImage image;

    //在构造方法中传入过期秒数,LocalDateTime.now()获取现在时间,与过期秒数相加,生成过期时间
    public ImageValidateCode(BufferedImage image, String imageValidateCode, int expireSecond) {

        super(imageValidateCode,expireSecond);
        this.image = image;
    }

}

ImageValidateCodeFilter.java文件的作用是过滤从配置文件中读取的提交username、password、validateCode的自定义认证请求,但只从自定义认证请求中读取validateCode验证码的值,与从Session中取出的保存的ImageValidateCode实体中的code验证码进行对比
如果不匹配则抛出将ValidateCodeException异常传入,将ValidateCodeException异常传入实现AuthenticationFailureHandler接口的认证失败后的自定义处理类QiqihalAuthenticationFailureHandler的onAuthenticationFailure方法中,认证失败后的自定义处理类,将认证失败信息,和自定义认证页面请求路径,封装成Json数据返回前端SpringSecurity过滤链就会终止,不会进入,包括SpringSecurity过滤链上的过滤器
因为在QIQIHAL-SECURITY-BROWSER\src\main\java\com\qiqihal\config\securityconfig\QiqihalSecurityConfig类的configure方法中,添加.addFilterBefore(ImageValidateCodeFilter, UsernamePasswordAuthenticationFilter.class),将ImageValidateCodeFilter自定义校验图像验证码过滤器放在,SpringSecurity过滤器链中UsernamePasswordAuthenticationFilter过滤器之前,所以如果匹配则,继续将提交username、password、validateCode的自定义认证请求,传入SpringSecurity过滤器链中ImageValidateCodeFilter自定义校验图像验证码过滤器后面的UsernamePasswordAuthenticationFilter过滤器中,从提交username、password、validateCode的自定义认证请求读取username、password的值保存到UsernamePasswordAuthenticationToken类中,DaoAuthenticationProvider类中从UsernamePasswordAuthenticationToken类取出username传入
UserDetailsService接口的实现类QIQIHAL-SECURITY-BROWSER\src\main\java\com\qiqihal\config\securityconfig\QiqihalUserDetailsService类的loadUserByUsername方法中,在该方法中根据username从数据库中查询出包含加密的password的记录并封装到实现UserDetails接口的User类中,最后执行DaoAuthenticationProvider类的additionalAuthenticationChecks方法,该方法将"保存数据库中加密password字段值的User类"和"保存从自定义认证请求提交的username、password值的UsernamePasswordAuthenticationToken类"作为参数,然后从UsernamePasswordAuthenticationToken类中取出password值也进行加密,加密后的结果与User类加密的password值进行对比以此判断自定义认证请求提交的username、password是否正确

QIQIHAL-SECURITY-CORE\src\main\java\com\qiqihal\validatecode\imagevalidatecode\ImageValidateCodeFilter.java文件如下

package com.qiqihal.validatecode.imagevalidatecode;

import com.qiqihal.validatecode.ValidateCodeException;
import org.apache.commons.lang.StringUtils;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
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;

/*ImageValidateCodeFilter自定义校验图像验证码过滤器,OncePerRequestFilter过滤器Spring提供的过滤器,只执行一次,
 ImageValidateCodeFilter自定义校验图像验证码过滤器,的作用是过滤从配置文件中读取的提交username、password、validateCode的自定义认证请求,
 但只从自定义认证请求中读取validateCode验证码的值,与从Session中取出的保存的ImageValidateCode实体中的code验证码进行对比
如果不匹配则抛出将ValidateCodeException异常传入,
将ValidateCodeException异常传入实现AuthenticationFailureHandler接口的认证失败后的自定义处理类QiqihalAuthenticationFailureHandler的onAuthenticationFailure方法中,
认证失败后的自定义处理类,将认证失败信息,和自定义认证页面请求路径,封装成Json数据返回前端SpringSecurity过滤链就会终止,
不会进入,包括SpringSecurity过滤链上的过滤器
因为在QIQIHAL-SECURITY-BROWSER\src\main\java\com\qiqihal\config\securityconfig\QiqihalSecurityConfig类的configure方法中,
添加.addFilterBefore(ImageValidateCodeFilter, UsernamePasswordAuthenticationFilter.class),
将ImageValidateCodeFilter自定义校验图像验证码过滤器放在,SpringSecurity过滤器链中UsernamePasswordAuthenticationFilter过滤器之前,
所以如果匹配则,继续将提交username、password、validateCode的自定义认证请求,
传入SpringSecurity过滤器链中ImageValidateCodeFilter自定义校验图像验证码过滤器后面的UsernamePasswordAuthenticationFilter过滤器中,
从提交username、password、validateCode的自定义认证请求读取username、password的值保存到UsernamePasswordAuthenticationToken类中,
DaoAuthenticationProvider类中从UsernamePasswordAuthenticationToken类取出username传入UserDetailsService接口的实现类QIQIHAL-SECURITY-BROWSER\src\main\java\com\qiqihal\config\securityconfig\QiqihalUserDetailsService类的loadUserByUsername方法中,
在该方法中根据username从数据库中查询出包含加密的password的记录并封装到实现UserDetails接口的User类中,
最后执行DaoAuthenticationProvider类的additionalAuthenticationChecks方法,
该方法将"保存数据库中加密password字段值的User类"和"保存从自定义认证请求提交的username、password值的UsernamePasswordAuthenticationToken类"作为参数,
然后从UsernamePasswordAuthenticationToken类中取出password值也进行加密,
加密后的结果与User类加密的password值进行对比以此判断自定义认证请求提交的username、password是否正确 */
public class ImageValidateCodeFilter extends OncePerRequestFilter{

    public static final String SPRING_SECURITY_VALIDATE_CODE_KEY = "validateCode";

    private String validateCodeParameter = SPRING_SECURITY_VALIDATE_CODE_KEY;

    /*ImageValidateCodeFilter自定义校验验证码认证过滤器是否只处理post请求*/
    private boolean postOnly = true;

    /*提交username、password、validateCode的认证请求路径*/
    private String authenticationUrl;

    /*是否开启图片认证*/
    private Boolean isOpenValidateCode;

    private AuthenticationFailureHandler authenticationFailureHandler;

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    public ImageValidateCodeFilter(String authenticationUrl, Boolean isOpenValidateCode, AuthenticationFailureHandler authenticationFailureHandler) {
        this.authenticationUrl = authenticationUrl;
        this.isOpenValidateCode = isOpenValidateCode;
        this.authenticationFailureHandler = authenticationFailureHandler;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        if (postOnly && request.getMethod().equalsIgnoreCase("POST") && authenticationUrl.equals(request.getServletPath())) {
            if (isOpenValidateCode) {
                try {
                    //将表单提交的图像验证码和session中保存的图像验证码进行校验
                    validate(new ServletWebRequest(request));
                } catch (ValidateCodeException e) {
                    /*AuthenticationException是SpringSecurity中的抽象异常,是所有认证过程中抛出异常的基类,
                      继承AuthenticationException必须实现该类中的一个构造方法,
                      ValidateCodeException只有继承AuthenticationException类,
                      才能作为AuthenticationFailureHandler接口的onAuthenticationFailure方法的参数,
                      在QIQIHAL-SECURITY-BROWSER\src\main\java\com\qiqihal\config\securityconfig\QiqihalSecurityConfig类的configure方法中,
                      将ImageValidateCodeFilter过滤器加入到SpringSecurity过滤链中并放在UsernamePasswordAuthenticationFilter过滤器前面时,
                      传入实现AuthenticationFailureHandler接口的认证失败后的自定义处理类,
                      一旦validate方法抛出异常,
                      将ValidateCodeException异常传入实现AuthenticationFailureHandler接口的认证失败后的自定义处理类QiqihalAuthenticationFailureHandler的onAuthenticationFailure方法中,
                      证失败后的自定义处理类,将认证失败信息,
                      和自定义认证页面请求路径,封装成Json数据返回前端,
                      SpringSecurity过滤链就会终止,不会进入,包括SpringSecurity过滤链上的过滤器*/
                    authenticationFailureHandler.onAuthenticationFailure(request, response, e);
                    /*执行完实现AuthenticationFailureHandler接口的认证失败后的自定义处理类后,
                      执行return null终止SpringSecurity过滤链中执行下一个过滤器,
                      避免多个错误用户名错误、密码错误和验证码错误等认证失败后都进入认证失败后的自定义处理类,
                      返回两个结果*/
                    return;
                }
            }
        }
        //继续执行其他过滤器,包括SpringSecurity过滤链上的过滤器
        filterChain.doFilter(request,response);
    }

    private void validate(ServletWebRequest request) throws ServletRequestBindingException {
        //从session中取出验证码
        ImageValidateCode codeInSession = (ImageValidateCode) sessionStrategy.getAttribute(request,ValidateCodeController.SESSION_KEY +"IMAGE");
        //从request请求中根据表单提交的参数"imageCode"取出的值就是提交的验证码,
        String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(),validateCodeParameter);

        if (StringUtils.isBlank(codeInRequest)){
            throw new ValidateCodeException("验证码不能为空");
        }
        if (codeInSession == null){
            throw new ValidateCodeException("验证码不存在");
        }
        if (codeInSession.isExpried()){
            //过期时间expireTime,是在ImageValidateCodeGenerator类中设置的,验证码已过期,把session中将保存图像的图像缓冲区删掉
            sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY +"IMAGE");
            throw new ValidateCodeException("验证码已过期");
        }
        //校验表单提交的验证码和session中取出的验证码
        if (!StringUtils.equals(codeInSession.getImageValidateCode(),codeInRequest)){
            throw new ValidateCodeException("验证码不匹配");
        }
        //验证码校验完毕,则把session中将保存图像的图像缓冲区删掉
        sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY +"IMAGE");
    }
}

要想将我们自定义的过滤器ImageValidateCodeFilter加入到SpringSecurity的过滤器链中,并放在UsernamePasswordAuthenticationFilter过滤器前面,以便使用ImageValidateCodeFilter过滤器验证image图像验证码,使用UsernamePasswordAuthenticationFilter验证username、password是否正确,需要在QIQIHAL-SECURITY-BROWSER\src\main\java\com\qiqihal\config\securityconfig\QiqihalSecurityConfig类的configure方法中,添加.addFilterBefore(ImageValidateCodeFilter, UsernamePasswordAuthenticationFilter.class),将ImageValidateCodeFilter自定义校验图像验证码过滤器放在,SpringSecurity过滤器链中UsernamePasswordAuthenticationFilter过滤器前同时还要修改QIQIHAL-
SECURITY-BROWSER\src\main\java\com\qiqihal\config\securityconfig\QiqihalSecurityConfig.java文件

package com.qiqihal.config.securityconfig;

import com.qiqihal.properties.SecurityProperties;
import com.qiqihal.validatecode.imagevalidatecode.ImageValidateCodeFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
//@EnableWebSecurity注解以及WebSecurityConfigurerAdapter一起配合提供基于web的security
@EnableWebSecurity
public class QiqihalSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    QiqihalAuthenticationSuccessHandler qiqihalAuthenticationSuccessHandler;

    @Autowired
    QiqihalAuthenticationFailureHandler qiqihalAuthenticationFailureHandler;

    @Autowired
    SecurityProperties securityProperties;

    @Override
    //HttpSecurity对每个请求进行细粒度安全性控制
    protected void configure(HttpSecurity http) throws Exception {

        ImageValidateCodeFilter imageValidateCodeFilter = new ImageValidateCodeFilter(
                securityProperties.getValidateCode().getImageValidateCode().getAuthenticationUrl(),
                securityProperties.getValidateCode().getImageValidateCode().getIsOpenValidateCode(),
                qiqihalAuthenticationFailureHandler);

        http //认证方法具有顺序,顺序不同效果不一样

             /*将ImageValidateCodeFilter自定义校验图像验证码过滤器放在,
             SpringSecurity过滤器链中UsernamePasswordAuthenticationFilter过滤器前*/
             .addFilterBefore(imageValidateCodeFilter , UsernamePasswordAuthenticationFilter.class)

             //####################认证方式####################
             //httpBasic()使用弹窗认证,formLogin()使用SpringSecurity默认认证页面认证
             //使用swagger测试接口时使用弹窗认证更加灵活,swagger中无法法跳转到认证页面认证
             //.httpBasic()
             //为了使用自定义认证页面,只能使用.formLogin()
             .formLogin()

             /*将SpringSecurity默认认证页面请求路径改为自定义认证页面请求路径"/login.html",
             同时把login.html放在static文件夹中,让SpringBoot自动读取到该页面,
             还要将"/login.html"请求路径放入ant风格的访问请求路径白名单,
             否则进入自定义认证页面也需要认证,项目启动后就可以直接访问localhost:8000/login.html
             .loginPage("/login.html")*/

             /*将SpringSecurity默认认证请页面求路径改为请求判断接口请求路径"/authentication/require",
             如果未认证的请求需要认证时,原本需要跳转到默认认证页面的求情路径变成了"/authentication/require"映射的SecurityController类的requireAuthentication方法,
             根据是否是html请求判断(请求路径是否以.html后缀结尾),如果是html请求则直接跳转到认证页面,认证通过则放行,
             如果不是html请求,则返回状态码为401的错误信息,还要将"/authentication/require"请求路径放入ant风格的访问请求路径白名单*/
             .loginPage("/authentication/require")

             /*修改UsernamePasswordAuthenticationFilter过滤器的默认过滤的提交username、password的认证请求路径,
             当我们使用httpBasic()弹窗认证或formLogin()默认页面或自定义页面,提交username、password进行认证时,
             提交username、password的认证请求由UsernamePasswordAuthenticationFilter过滤器过滤,
             UsernamePasswordAuthenticationFilter过滤器默认过滤的提交username、password的认证请求为请求路径是"/login"的"POST"请求,
             使用loginProcessingUrl作用是将UsernamePasswordAuthenticationFilter过滤器的默认过滤的提交username、password的认证路径修改为,
             自定义认证页面中的提交username、password的自定义认证请求的请求路径,
             这样当自定义认证页面login.html提交username、password的自定义认证请求提交的username和password的值,
             就会被UserDetailsService的实现类中loadUserByUsername方法认证,
             自定义认证页面的认证功能才会生效,
             从配置文件中获取提交username、password的自定义认证请求*/
             .loginProcessingUrl(securityProperties.getValidateCode().getImageValidateCode().getAuthenticationUrl())

             /*认证成功后调用注入Spring容器中的QiqihalAuthenticationSuccessHandler类中的,
             AuthenticationSuccess方法*/
             .successHandler(qiqihalAuthenticationSuccessHandler)

             /*认证成功后调用注入Spring容器中的QiqihalAuthenticationFailureHandler类中的,
             onAuthenticationFailure方法*/
             .failureHandler(qiqihalAuthenticationFailureHandler)

             //####################认证请求白名单####################
             .and()

             //允许基于使用HttpServletRequest限制请求
             .authorizeRequests()

             //ant风格的访问请求路径
             .antMatchers(new String[]{
                    //静态资源
                    "/css/**",
                    "/js/**",
                    "/fonts/**",
                    //swagger ui
                    "/v2/api-docs",
                    "/swagger-resources",
                    "/swagger-resources/**",
                    "/configuration/ui",
                    "/configuration/security",
                    "/swagger-ui.html",
                    "/webjars/**",
                    //创建用户账户请求
                    "/user/",
                    //从配置文件中读取自定义认证页面请求
                    securityProperties.getBrowser().getLonginPage(),
                    //html请求判断接口请求
                    "/authentication/require",
                    //生成图像验证码请求
                    "/code/image"
                 }
             )

             //全部放行
             .permitAll()

             //####################认证所有请求####################
             .and()

             //允许基于使用HttpServletRequest限制请求
             .authorizeRequests()

             //拦截任何访问
             .anyRequest()

             //允许认证通过的用户访问
             .authenticated()

             //####################关闭csrf####################
             .and()

             /*SpringSecurity默认开启了防止跨域攻击的功能,
             任何提交到后台的POST请求要验证是否带有_csrf参数,
             一旦传来的_csrf参数不正确,服务器便返回403错误,
             由于使用swagger测试,目前暂时不能设置_csrf参数,所以暂时关闭*/
             .csrf().disable()
        ;
    }

    @Bean
    //passwordEncoder 处理密码加密和解密,不处理报错There is no PasswordEncoder mapped for the id "null"
    public PasswordEncoder passwordEncoder(){
        //使用BCryptPasswordEncoder类进行加密和解密,BCryptPasswordEncoder类实现PasswordEncoder接口,使用BCrypt强哈希方法来加密密码
        return new BCryptPasswordEncoder();
        /*BCryptPasswordEncoder中的encode方法每次加密后生成的加密字符串都不同,
        SpringSecurity每次执行encode方法时,会随机生成盐,加入到密码中生成加密字符串,
        BCryptPasswordEncoder中的在matches方法根据随机生成的盐从加密字符串中解密出密码

        如果想要自定义加密方式,只需实现PasswordEncoder接口,重写encode和matches方法,
        在encode(CharSequence rawPassword)方法中传入原始密码进行自定义加密,
        CharSequence rawPassword参数为原始密码

        在matches(CharSequence rawPassword, String encodedPassword)方法中将用户输入的密码和加密后的密码进行匹配,
        CharSequence rawPassword参数为用户输入的密码,encodedPassword为加密后的密码*/
    }
}

因为我们将提交username、password的认证请求路径、生成图像验证码图片的高度、宽度、验证码个数等信息全部写入application.yml中,所以我们也要加入读取application.yml配置文件的类,在QIQIHAL-SECURITY-CORE\src\main\java\com\qiqihal路径下创建properties文件夹,在该文件夹下创建BrowserProperties.java文件、ImageValidateCodeProperties.java文件、SecurityProperties.java文件和ValidateCodeProperties.java文件

SpringOauth初级开发三 自定义认证页面中增加图像验证码_第3张图片
image.png

BrowserProperties.java文件作用是封装从application.yml配置文件中读取Browser项目相关的配置

QIQIHAL-SECURITY-CORE\src\main\java\com\qiqihal\properties\BrowserProperties.java文件内容如下

package com.qiqihal.properties;

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

@SuppressWarnings("serial")
@NoArgsConstructor
@Data
@Accessors(chain = true)
//用来封装从application.yml配置文件中读取的Browser项目相关功能的参数
public class BrowserProperties {

    //自定义认证页面请求路径
    private String longinPage;
}

SecurityProperties.java文件作用是读取配置文件中的值,并多个key、value键值对封装到对应的类中,因为@ConfigurationProperties注解的参数prefix不支持驼峰命名和蛇形命名(下划线),所以配置文件的key由多个单子组合而成并使用驼峰命名和蛇形命名(下划线),perfix的值只能匹配配置文件中单个单词的key,将使用驼峰命名和蛇形命名(下划线)的key的值映射到名字为key的bean中,并给名字为key的bean添加get/set方法

QIQIHAL-SECURITY-CORE\src\main\java\com\qiqihal\properties\SecurityProperties.java文件内容如下

package com.qiqihal.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "qiqihal.security")
@Data
/*因为@ConfigurationProperties注解的参数prefix不支持驼峰命名和蛇形命名(下划线),
所以配置文件的key由多个单子组合而成并使用驼峰命名和蛇形命名(下划线),
perfix的值只能匹配配置文件中单个单词的key,
将使用驼峰命名和蛇形命名(下划线)的key的值映射到名字为key的bean中,
并给名字为key的bean添加get/set方法*/
public class SecurityProperties {

    //从配置文件读取验证码相关配置
    private ValidateCodeProperties validateCode = new ValidateCodeProperties();

    //从配置文件读取Browser项目相关配置
    private BrowserProperties browser = new BrowserProperties();
}

ValidateCodeProperties.java文件作用是封装ImageValidateCodeProperties对象

QIQIHAL-SECURITY-CORE\src\main\java\com\qiqihal\properties\ValidateCodeProperties.java文件内容如下

package com.qiqihal.properties;

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

@SuppressWarnings("serial")
@NoArgsConstructor
@Data
@Accessors(chain = true)

public class ValidateCodeProperties {

    private ImageValidateCodeProperties imageValidateCode = new ImageValidateCodeProperties();

}

ImageValidateCodeProperties.java文件作用是封装从application.yml配置文件中读取的图像验证相关功能的参数
QIQIHAL-SECURITY-CORE\src\main\java\com\qiqihal\properties\ImageValidateCodeProperties.java文件内容如下

package com.qiqihal.properties;

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

@SuppressWarnings("serial")
@NoArgsConstructor
@Data
@Accessors(chain = true)
//用来封装从application.yml配置文件中读取的图像验证相关功能的参数
public class ImageValidateCodeProperties {

    private int width = 67;

    private int height = 23;

    private int validateCodelength = 4;

    private int expireTime = 60;

    private Boolean isOpenValidateCode;

    private String authenticationUrl;
}

修改QIQIHAL-SECURITY-BROWSER\src\main\resources\application-dev.yml配置文件,增加验证码相关配置

server:
    port: 8000
###################以下为logback增加的配置###########################
logging:
    config: classpath:logback-boot.xml
     #控制台打印sql
    level:
      com.qiqihal.mapper: debug
debug: true
###################以下为srpingboot增加的配置###########################
spring:
  session:
    store-type: none #禁用spring-session
  application:
    #服务提供方注册在eureka中的微服务名称(Instances currently registered with Eureka中的Application),
    #将服务提供方作为作为微服务交由eureka管理,服务消费方获得服务提供方地址由eureka提供
    #ribbon整合服务消费方后可以直接调用服务提供方,不用在关心地址和端口号,
    #当服务提供方创建集群时,所有集群下的服务提供方注册在eureka中的微服务名称不能改变,
    #因为使用ribbon做负载均衡时才让服务消费方负载时均衡调用所有服务提供方
    name: QIQIHAL-SECURITY-BROWSER
  ###################以下为mysql数据库增加的配置###########################
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.43.234:3306/security?useUnicode=true&characterEncoding=utf8&autoReconnect=true
    type: com.alibaba.druid.pool.DruidDataSource
    username: root
    password: 123456
    ###################以下为druid增加的配置###########################
    # 下面为连接池的补充设置,应用到上面所有数据源中
    # 初始化大小,最小,最大
    initialSize: 5
    minIdle: 5
    maxActive: 20
    # 配置获取连接等待超时的时间
    maxWait: 60000
    # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
    timeBetweenEvictionRunsMillis: 60000
    # 配置一个连接在池中最小生存的时间,单位是毫秒
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    # 打开PSCache,并且指定每个连接上PSCache的大小
    poolPreparedStatements: true
    maxPoolPreparedStatementPerConnectionSize: 20
  ###################以下为Redis增加的配置###########################
  redis:
      #单机配置
      #host: 192.168.43.188
      #port: 6379
    timeout: 7000
    password: 123456
    ###################以下为redis集群增加的配置###########################
    sentinel:
      nodes: 192.168.43.234:26379,192.168.43.234:26380,192.168.43.234:26381
      master: mymaster
    ###################以下为lettuce连接池增加的配置###########################
    lettuce:
      pool:
        max-active: 100 # 连接池最大连接数(使用负值表示没有限制)
        max-idle: 100 # 连接池中的最大空闲连接
        min-idle: 50 # 连接池中的最小空闲连接
        max-wait: 6000 # 连接池最大阻塞等待时间(使用负值表示没有限制
  ###################以下为springcache增加的配置###########################
  cache:
    redis:
      use-key-prefix: true
      key-prefix: dev
      cache-null-values: false
      time-to-live: 20s
###################以下为mybatis增加的配置###########################
mybatis:
  configuration:
    #自动转换驼峰标识
    map-underscore-to-camel-case: true
###################以下为Browser项目增加的配置###########################
qiqihal:
  security:
    ###################以下为验证码增加的配置###########################
    validateCode:
      imageValidateCode:
        #验证码图像长度和验证码长度成正比
        width: 75
        height: 23
        validateCodelength: 4
        expireTime: 60
        isOpenValidateCode: true
        authenticationUrl: /authentication/html
    ###################以下为Browser项目增加的配置###########################
    browser:
      #自定义认证页面请求路径
      longinPage: /login.html

ImageValidateCodeGenerator.java文件的作用是根据application.yml中关于验证码的配置生成图像和验证码

QIQIHAL-SECURITY-CORE\src\main\java\com\qiqihal\validatecode\imagevalidatecode\ImageValidateCodeGenerator.java文件内容如下

package com.qiqihal.validatecode.imagevalidatecode;

import com.qiqihal.properties.SecurityProperties;
import com.qiqihal.validatecode.ValidateCodeGenerator;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;
/*生成图像和验证码*/
public class ImageValidateCodeGenerator implements ValidateCodeGenerator {

    SecurityProperties securityProperties;

    public SecurityProperties getSecurityProperties() {
        return securityProperties;
    }

    public void setSecurityProperties(SecurityProperties securityProperties) {
        this.securityProperties = securityProperties;
    }

    //生成保存验证码的图像在图像缓冲区,生成验证码,生成过期时间
    @Override
    public ImageValidateCode generate(ServletWebRequest request) {

        /*从请求中获图像验证码宽度
          ServletRequestUtils.getIntParameter(),从请求中获取int类型参数的值,
          从请求中获取名为"width"的int类型参数的值,如果名为"width"的int类型参数的值不存在,
          则从配置文件中读取图像验证码宽度*/
        int width = ServletRequestUtils.getIntParameter(request.getRequest(),"width",securityProperties.getValidateCode().getImageValidateCode().getWidth());

        /*从请求中获图像验证码高度
          从请求中获取名为"height"的int类型参数的值,如果名为"height"的int类型参数的值不存在,
          则从配置文件中读取图像验证码高度*/
        int height = ServletRequestUtils.getIntParameter(request.getRequest(),"height",securityProperties.getValidateCode().getImageValidateCode().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(1,1,width-1,height-1);
        g.setColor(new Color(102,102,102));
        g.drawRect(0,0, width-1, height-1);
        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-1);
            int y=random.nextInt(height-1);
            int x1=random.nextInt(6)+1;
            int y1=random.nextInt(12)+1;
            g.drawLine(x,y,x+x1,y+y1);
        }

        String code="";

        /*从请求中获图像验证码随机数个数
          从请求中获取名为"validateCodelength"的int类型参数的值,
          如果名为"validateCodelength"的int类型参数的值不存在,
          则从配置文件中读取图像验证码随机数个数*/
        int validateCodelength = ServletRequestUtils.getIntParameter(request.getRequest(),"validateCodelength",securityProperties.getValidateCode().getImageValidateCode().getValidateCodelength());

        for(int i=0;i255){
            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);
    }
    
}

修改QIQIHAL-SECURITY-BROWSER\src\main\resources\static\login.html文件,新增图像验证码功能




    
    HTML自定义认证页面



普通登录

用户名:
密码:
图片验证码:

右键Debug运行\QIQIHAL-SECURITY-BROWSER\src\main\java\com\qiqihal\QiqihalSecurityBrowserApplication.java主启动类,启动QIQIHAL-SECURITY-BROWSER项目,打开浏览器在地址栏输入http://localhost:8000/login.html

SpringOauth初级开发三 自定义认证页面中增加图像验证码_第4张图片
image.png

输入正确的username和password,输入错误的图像验证码,可以看到我们的ImageValidateCodeFilter自定义校验图像验证码过滤器成功过滤了错误的验证码

SpringOauth初级开发三 自定义认证页面中增加图像验证码_第5张图片
image.png

输入正确的username和图像验证码,输入错误的password

SpringOauth初级开发三 自定义认证页面中增加图像验证码_第6张图片
image.png

可以看到我们的ImageValidateCodeFilter自定义校验图像验证码过滤器中图像验证码校验通过,UsernamePasswordAuthenticationFilter过滤器成功过滤了错误的密码

SpringOauth初级开发三 自定义认证页面中增加图像验证码_第7张图片
image.png

你可能感兴趣的:(SpringOauth初级开发三 自定义认证页面中增加图像验证码)