SpringBoot интегрирует Security (2) для реализации входа с кодом подтверждения.

Spring Boot задняя часть

На основе предыдущего раздела мы добавим в проект модуль проверочного кода.Защита не сразу реализует эту часть функции за нас, поэтому для ее реализации нам понадобится написанный от руки фильтр. Название этого раздела, кажется, не имеет ничего общего с первым разделом, но идеи схожи. Надеюсь, вы сможете терпеливо прочитать его. В конце концов, я старался изо всех сил, чтобы написать его ясно. Вы можете чувствовать себя хаотично после прочтения, что нормально из-за упаковки, но я считаю, что технология будет значительно улучшена после терпеливого прочтения.

Глава 1 Автостоп:SpringBoot интегрирует Security (1) для реализации аутентификации пользователя и определения того, следует ли возвращать json или просматривать
Глава 2 Автостоп:SpringBoot интегрирует Security (2) для реализации входа с кодом подтверждения.

Хорошо, давайте начнем текст.

На основе первого туториала мы добавили новый

И добавил некоторый код в основной класс конфигурации BrowserSecurityConfig.

Как всегда, я дам общее представление о каждом классе:

  • ImageCodeProperties:
  • Валидатекодепропертиес:
  • Эти два класса находятся в пакете свойств, поскольку они используются для получения конфигурации в файле конфигурации приложения.
  • ImageCodeGenerator: Генерация класса реализации кода проверки
  • ValidateCode BeanConfig: внедрить Image CodeGenerator в контейнер Spring. Почему бы не добавить инъекцию @Component непосредственно в ImageCodeGenerator? См. подробное объяснение ниже.
  • ValidateCodeGenerator: создайте интерфейс кода подтверждения
  • ImageCode: класс сущности кода подтверждения
  • ValidateCodeController: класс контроллера, используемый для возврата кода подтверждения пользователю.
  • ValidateCodeException: пользовательское исключение
  • ValidateCodeFilter: фильтр кодов подтверждения

1. Сначала нам нужно написать фильтр вручную

Так вот вопрос, какой фильтр пишется от руки, и на каком этапе запуска проекта фильтр должен вызываться? Как вы можете видеть в первом разделе, на самом деле при возврате объекта UserDetail все следующие операции являются операциями черного ящика безопасности (проверка основной информации), поэтому мы должны обработать проверочный код перед этим фильтром (генерация и суждение), если проверочный код не соответствует требованиям, он напрямую передается в обработчик неудачных входов в систему, и наоборот.

1.1 Нам нужно добавить фрагмент кода в основной класс конфигурации BrowserSecurityConfig.
        /**
         * 创建 验证码 过滤器 ,并将该过滤器的Handler 设置成自定义登录失败处理器
         */
        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        validateCodeFilter.setFailureHandler(myFailHandler);
        //将 securityproperties 设置进去
        validateCodeFilter.setSecurityProperties(securityProperties);
        //调用 装配 需要图片验证码的 url 的初始化方法
        validateCodeFilter.afterPropertiesSet();

        http
                //在UsernamePasswordAuthenticationFilter 过滤器前 加一个过滤器 来搞验证码
                .addFilterBefore(validateCodeFilter,UsernamePasswordAuthenticationFilter.class)
                .formLogin()
                ....

Сначала я создал объект ValidateCodeFilter, задал его обработчик ошибок и класс конфигурации securityProperties и вызвал его метод afterPropertiesSet(). Как правило, вызываются три метода объекта. Что касается того, что должен делать этот метод, медленно посмотрите вниз.

Во-вторых, добавьте фильтр перед фильтром UsernamePasswordAuthenticationFilter, чтобы получить код подтверждения. который:.addFilterBefore(validateCodeFilter,UsernamePasswordAuthenticationFilter.class)

1.2 ValidateCodeFilter .java
package com.fantJ.core.validate;

/**
 * 验证码 过滤器
 * Created by Fant.J.
 */
@Component
public class ValidateCodeFilter  extends OncePerRequestFilter implements InitializingBean{

    private Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * 登录失败处理器
     */
    @Autowired
    private AuthenticationFailureHandler failureHandler;

    /**
     * Session 对象
     */
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    /**
     * 创建一个Set 集合 存放 需要验证码的 urls
     */
    private Set<String> urls = new HashSet<>();

    /**
     * security applicaiton  配置属性
     */
    @Autowired
    private SecurityProperties securityProperties;
    /**
     * spring的一个工具类:用来判断 两字符串 是否匹配
     */
    private AntPathMatcher pathMatcher = new AntPathMatcher();

    /**
     * 这个方法是 InitializingBean 接口下的一个方法, 在初始化配置完成后 运行此方法
     */
    @Override
    public void afterPropertiesSet() throws ServletException {
        super.afterPropertiesSet();
        ValidateCodeProperties code = securityProperties.getCode();
        logger.info(String.valueOf(code));
        //将 application 配置中的 url 属性进行 切割
        String[] configUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getImage().getUrl(), ",");
        //添加到 Set 集合里
        urls.addAll(Arrays.asList(configUrls));
        //因为登录请求一定要有验证码 ,所以直接 add 到set 集合中
        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 和 配置中的url 相匹配
            if (pathMatcher.match(url,request.getRequestURI())){
                action = true;
            }
        }

        //拦截请求
        if (action){
            logger.info("拦截成功"+request.getRequestURI());
            //如果是登录请求
            try {
                validate(new ServletWebRequest(request));
            }catch (ValidateCodeException exception){
                //返回错误信息给 失败处理器
                failureHandler.onAuthenticationFailure(request,response,exception);
                return;
            }

        }else {
            //不做任何处理,调用后面的 过滤器
            filterChain.doFilter(request,response);
        }
    }

    private void validate(ServletWebRequest request) throws ServletRequestBindingException {
        //从session中取出 验证码
        ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request,ValidateCodeController.SESSION_KEY);
        //从request 请求中 取出 验证码
        String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(),"imageCode");

        if (StringUtils.isBlank(codeInRequest)){
            logger.info("验证码不能为空");
            throw new ValidateCodeException("验证码不能为空");
        }
        if (codeInSession == null){
            logger.info("验证码不存在");
            throw new ValidateCodeException("验证码不存在");
        }
        if (codeInSession.isExpried()){
            logger.info("验证码已过期");
            sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY);
            throw new ValidateCodeException("验证码已过期");
        }
        if (!StringUtils.equals(codeInSession.getCode(),codeInRequest)){
            logger.info("验证码不匹配"+"codeInSession:"+codeInSession.getCode() +", codeInRequest:"+codeInRequest);
            throw new ValidateCodeException("验证码不匹配");
        }
        //把对应 的 session信息  删掉
        sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY);
    }

    /**
     * 失败 过滤器 getter and setter 方法
     */
    public AuthenticationFailureHandler getFailureHandler() {
        return failureHandler;
    }

    public void setFailureHandler(AuthenticationFailureHandler failureHandler) {
        this.failureHandler = failureHandler;
    }
    /**
     * SecurityProperties 属性类 getter and setter 方法
     */
    public SecurityProperties getSecurityProperties() {
        return securityProperties;
    }

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

В соответствии с порядком вызова трех методов в классе ValidateCodeFilter в версии 1.1.

  1. validateCodeFilter.setFailureHandler(myFailHandler);

Вызовите методы getter и setter обработчика ошибок входа.

Итак, вопрос в том, что мы внедрили этот класс обработчика ошибок входа в первый раздел, почему мы снова устанавливаем его здесь?

Ответ прост, потому что его вызывает новый фильтр. Кроме того, этот фильтр ValidateCode будет выполняться перед проверкой пользователя.

  1. validateCodeFilter.setSecurityProperties(securityProperties);

На самом деле это метод получения и установки свойства конфигурации приложения безопасности.

Этот код ни о чем не говорит, причина та же, что и выше. Потому что в основном классе конфигурации BrowserSecurityConfig мы внедрили объект, а затем передали его. Также должна быть возможность не передавать его, потому что объект также внедряется в класс, но он определенно не сообщит об ошибке, если он будет передан.

  1. afterPropertiesSet()метод
    Этот метод представляет собой метод в рамках интерфейса InitializingBean. Этот метод запускается после завершения настройки инициализации. Цель этого метода — сделать коллекцию URL-адресов, которые нам нужны для проверки кода проверки в бизнес-логике, а затем перехватить (в методе doFilterInternal() это перехват URL-адреса и последующая его обработка)
    Затем я разместил конфигурацию приложения
#图形验证码配置
fantJ.security.code.image.length = 6
fantJ.security.code.image.width = 100
fantJ.security.code.image.url=/user,/user/*

согласно сsecurityProperties.getCode().getImage().getUrl()Мы видим, что я добавил объект Code в SecurityProperties, объект Image в Code и объект Url в Image. (полностью выложу код позже), чтобы получить url в конфигурации, обрезать его запятыми и поместить в коллекцию Set.

  1. Знакомство с идеей doFilterInternal() в классе ValidateCodeFilter
    После вызова метода afterPropertiesSet() мы получили набор урлов, которые нужно перехватить, и далее я сужу, есть ли uri в запросе запроса в наборе, если да, то перехватываем запрос и вызываемvalidate();метод, метод validate() извлечет код подтверждения из сеанса и код подтверждения из запроса запроса. Подробности для сравнения и проверки см. в примечаниях. ImageCode — это класс сущности капчи.
3. Класс контроллера генерирует проверочный код

Вышеупомянутое извлечение кода подтверждения из запроса запроса. Перед этим нам нужно сгенерировать код подтверждения в контроллере и вернуть его пользователю. Пользователь заполняет и отправляет код подтверждения, чтобы получить его из запроса запроса.

package com.fantJ.core.validate;

/**
 * Created by Fant.J.
 */
@RestController
public class ValidateCodeController {

    public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
    /**
     * 引入 session
     */
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Autowired
    private ValidateCodeGenerator imageCodeGenerator;

    @GetMapping("/code/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ImageCode imageCode = imageCodeGenerator.createCode(new ServletWebRequest(request));
        //将随机数 放到Session中
        sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);
        //写给response 响应
        ImageIO.write(imageCode.getImage(),"JPEG",response.getOutputStream());
    }
}

Мы случайным образом генерируем проверочный код, помещаем его в сеанс и возвращаем ответному клиенту.

4. Создайте класс кода подтверждения

ImageCode .java

package com.fantJ.core.validate;

/**
 * 验证码信息类
 * Created by Fant.J.
 */
public class ImageCode {

    /**
     * 图片
     */
    private BufferedImage image;
    /**
     * 随机数
     */
    private String code;
    /**
     * 过期时间
     */
    private LocalDateTime expireTime;

    public ImageCode(BufferedImage image, String code, LocalDateTime expireTime) {
        this.image = image;
        this.code = code;
        this.expireTime = expireTime;
    }
    public ImageCode(BufferedImage image, String code, int  expireIn) {
        this.image = image;
        this.code = code;
        //当前时间  加上  设置过期的时间
        this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
    }

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

...getter and setter 
}

Обратите внимание, что есть обработка времени истечения.Мы загружаем параметр int expireIn, используем текущее время плюс этот параметр, а затем в методе isExpried() сравниваем текущее время со временем после плюса, чтобы определить, истек ли срок действия проверочного кода.

Класс интерфейса ValidateCodeGenerator.java

/**
 * 验证码生成器
 * Created by Fant.J.
 */
public interface ValidateCodeGenerator {
    /**
     * 创建验证码
     */
    ImageCode  createCode(ServletWebRequest request);
}

Зачем делать интерфейс?Ради инкапсуляции, если мы хотим написать более мощный генератор кода проверки в будущем, мы можем напрямую наследовать метод реализации интерфейса, не изменяя исходный код.

ImageCodeGenerator.java

package com.fantJ.core.validate.code;

/**
 * Created by Fant.J.
 */
public class ImageCodeGenerator implements ValidateCodeGenerator {
    /**
     * 引入 Security 配置属性类
     */
    private SecurityProperties securityProperties;


    /**
     * 创建验证码
     */
    @Override
    public ImageCode createCode(ServletWebRequest request) {
        //如果请求中有 width 参数,则用请求中的,否则用 配置属性中的
        int width = ServletRequestUtils.getIntParameter(request.getRequest(),"width",securityProperties.getCode().getImage().getWidth());
        //高度(宽度)
        int height = ServletRequestUtils.getIntParameter(request.getRequest(),"height",securityProperties.getCode().getImage().getHeight());
        //图片验证码字符个数
        int length = securityProperties.getCode().getImage().getLength();
        //过期时间
        int expireIn = securityProperties.getCode().getImage().getExpireIn();

        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 < length; 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, expireIn);
    }
    /**
     * 生成随机背景条纹
     */
    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;
    }
}

Наконец, введите этот класс в контейнер Spring. ValidateCodeBeanConfig.java

package com.fantJ.core.validate.code;

/**
 * 验证码 实体类设置 类
 * Created by Fant.J.
 */
@Configuration
public class ValidateCodeBeanConfig {

    @Autowired
    private SecurityProperties securityProperties;

    @Bean
    @ConditionalOnMissingBean(name = "imageCodeGenerator")
    /**
     * 
     * 
     *
     * 在触发 ValidateCodeGenerator 之前会检测有没有imageCodeGenerator这个bean。
     */
    public ValidateCodeGenerator imageCodeGenerator(){
        ImageCodeGenerator codeGenerator = new ImageCodeGenerator();
        codeGenerator.setSecurityProperties(securityProperties);
        return codeGenerator;
    }

}

@ConditionalOnMissingBean Эта аннотация фактически эквивалентна добавлению аннотации @Component к классу ImageCodeGenerator. Отличие между ними в том, что эта аннотация является условной аннотацией, а это значит, что если в контейнере нет класса ImageCodeGenerator, я создам этот класс, а если есть, то операцию делать не буду.

Почему должно быть так? Если в будущем мы перепишем лучше сгенерированный класс кода проверки, мы можем напрямую добавить к нему аннотацию @Component для внедрения, и нам не нужно заботиться об исходном коде, а также нам не нужно учитывать конфликт имен компонентов. .

Это класс, используемый для генерации кода подтверждения. Эффект следующий


5. Класс исключений и класс свойств

ValidateCodeException .java

package com.fantJ.core.validate;
import org.springframework.security.core.AuthenticationException;

/**
 * 自定义 验证码异常类
 * Created by Fant.J.
 */
public class ValidateCodeException extends AuthenticationException {
    public ValidateCodeException(String msg) {
        super(msg);
    }
}

SecurityProperties.java

package com.fantJ.core.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * Security 属性 类
 * Created by Fant.J.
 */
@ConfigurationProperties(prefix = "fantJ.security")
public class SecurityProperties {
    /**
     * 浏览器 属性类
     */
    private BrowserProperties browser = new BrowserProperties();

    /**
     * 验证码 属性类
     */
    private ValidateCodeProperties code = new ValidateCodeProperties();

    getter and setter...
}

ValidateCodeProperties.java

package com.fantJ.core.properties;

/**
 * 验证码 配置类
 * Created by Fant.J.
 */
public class ValidateCodeProperties {
    /**
     * 图形验证码 配置属性
     */
    private ImageCodeProperties image = new ImageCodeProperties();
  
    getter and setter...
}

ImageCodeProperties.java

package com.fantJ.core.properties;

/**
 * 图形验证码  配置读取类
 * Created by Fant.J.
 */
public class ImageCodeProperties {

    /**
     * 验证码宽度
     */
    private int width = 67;
    /**
     * 高度
     */
    private int height = 23;
    /**
     * 长度(几个数字)
     */
    private int length = 4;
    /**
     * 过期时间
     */
    private int expireIn = 60;

    /**
     * 需要图形验证码的 url
     */
    private String url;
    
  getter and setter ...
}

Наконец, я дам вам демонстрацию страницы входа.

<!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?width=100">
            </td>
        </tr>
        <tr>
            <td colspan="2"><button type="submit">登录</button></td>
        </tr>
    </table>
</form>

</body>
</html>

Показать результаты

страница авторизации Код подтверждения пуст Ошибка кода подтверждения
Категории