Spring Security开发安全的REST服务-学习笔记(4)

Spring Security开发安全的REST服务-学习笔记(4)

欢迎

本篇博文仅记录该学习过程。

4.2 Spring Security基本原理

基本原理
绿色部分: 都是项目中根据实际项目需求会进行添加的。
蓝色部分: Exception Translation Filter在后面的黄色部分抛出权限相关的异常之后会被拦截到,做相应的处理。
黄色部分: 是对Rest请求服务的是否通过认证的最终决定。

处理逻辑:当有一个非登陆请求过来的时候,会直接进到黄色部分,在黄色部分验证是否登陆,如果登陆则放行请求;如果未登陆则抛出异常,被蓝色部分拦截后会重定向到登陆页面要求用户登陆。在此时如果用户填入用户名和密码点击登陆后,请求会被相应的绿色部分的Filter拦截,在Filter中进行用户登陆,如果用户登陆成功,则会把第一次的请求重定向倒后面的Interceptor中继续判断是否可以访问REST API

4.3 自定义用户认证逻辑

Ps:由于SpringBoot2.x和Spring security的版本的问题,在此处的自定义用户认证逻辑部分和SpringBoot1.x版本的有不同

配置自定义用户认证逻辑

1、SecurityConfig部分代码

package com.lwj.securitybrowser.config;

import com.lwj.securitybrowser.security.MyUserDetailService;
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.authentication.builders.AuthenticationManagerBuilder;
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;

/**
 * @author Administrator
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 注入用户认证逻辑处理类
     */
    @Autowired
    private MyUserDetailService myUserDetailService;

    /**
     * 注入密码加密工具
     * @return
     */
    @Bean
    public static BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 配置安全规则
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .and()
                .authorizeRequests()
                .anyRequest()
                .authenticated();
    }

    /**
     * 配置自定义的用户认证逻辑
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //  配置用户认证的逻辑处理类和密码加密的工具
        auth.userDetailsService(myUserDetailService).passwordEncoder(bCryptPasswordEncoder());
    }
}

2、编写自定义用户认证的逻辑代码

package com.lwj.securitybrowser.security;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.BadCredentialsException;
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.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class MyUserDetailService implements UserDetailsService {
    private Logger logger = LoggerFactory.getLogger(getClass());

    private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        logger.info("登陆的用户名为:" + userName);
        logger.info("密码:" + this.passwordEncoder.encode("123456"));

        if (this.passwordEncoder.matches("123456", "$2a$10$DBuDUrd9A/c8GqJJ6jZuGuqv/Ut7WLF6/lDGs8ayexUp8dcxhnydq")) {
            return new User(userName, this.passwordEncoder.encode("123456"), AuthorityUtils.createAuthorityList("admin"));
        }
        throw new BadCredentialsException("登陆失败!");
    }
}

Ps:上面代码中已包含了关于自定义认证配置和加密算法配置部分的代码

4.4 个性化用户认证流程

一、自定义登陆页面

1.在SecurityConfig类的configure方法中添加登陆页面的配置

/**
 * 配置安全规则
 * @param http
 * @throws Exception
 */
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.formLogin()
            .loginPage("/signIn.html")//添加自定义登陆页面
            .and()
            .authorizeRequests()
            .anyRequest()
            .authenticated();
}

打开浏览器访问项目时会报下面这个错误

异常截图
PS:以上配置了所有请求都需要登陆校验,所以在这种情况下启动项目访问,路由会被重定向到signIn.html但是由于该路由又是需要登陆校验的,所以又会被重定向到signIn.html,导致死循环。

针对上面的问题,需要在配置中增加

/**
 * 配置安全规则
 * @param http
 * @throws Exception
 */
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.formLogin()
            .loginPage("/signIn.html")//配置自定义登陆页面
            .and()
            .authorizeRequests()
            .antMatchers("/signIn.html").permitAll()//配置不拦截登陆页面
            .anyRequest()
            .authenticated();
}

Ps:注意点
在上面配置好后,重新刷新页面点击登陆时还会出现302的错误是因为crsf的问题,登陆连接会报302。需要在配置中继续添加http.csrf().disable();

2.处理不同类型的请求

处理不同类型的请求
Security配置类中的方法修改

    /**
     * 配置安全规则
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                //配置自定义登陆页面
                .loginPage("/authentication/require")
                .loginProcessingUrl("/authentication/form")
                .successHandler(myAuthenticationSuccessHandler)
//                .defaultSuccessUrl("/test")
                .and()
                .authorizeRequests()
                //配置不拦截登陆页面
                .antMatchers("/authentication/require", securityProperties.getBrowser().getLoginPage()).permitAll()
                .anyRequest()
                .authenticated();
        http.csrf().disable();
    }

主要代码逻辑代码BrowserSecurityController

package com.moss.securitybrowser.controller;

import cn.hutool.core.util.StrUtil;
import com.moss.securitybrowser.support.SimpleResponse;
import com.moss.securitycore.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.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author lwj
 */
@RestController
public class BrowserSecurityController {

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

    private RequestCache requestCache = new HttpSessionRequestCache();

    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Autowired
    private SecurityProperties securityProperties;

    /**
     * 当需要身份认证时跳转到这个控制器中
     * @param request
     * @param response
     * @return
     */
    @RequestMapping("/authentication/require")
    @ResponseStatus(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 (StrUtil.endWithIgnoreCase(targetUrl, ".html")) {
                redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage());
            }
        }

        return new SimpleResponse("访问的服务需要身份认证,请引导用户到登录页");
    }
}

二、自定义登陆成功处理

实现AuthenticationSuccessHandler接口

package com.moss.securitybrowser.security;

import com.fasterxml.jackson.databind.ObjectMapper;
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.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 登陆成功处理器
 *
 * @author lwj
 */
@Component("myAuthenticationSuccessHandler")
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        logger.info("登陆成功");
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        httpServletResponse.getWriter().write(objectMapper.writeValueAsString(authentication));
    }
}

三、自定义登陆失败处理

实现AuthenticationFailureHandler接口

package com.moss.securitybrowser.security;

import com.fasterxml.jackson.databind.ObjectMapper;
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.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 登陆失败处理器
 *
 * @author lwj
 */
@Component("myAuthenticationFailureHandler")
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

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

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        logger.info("登陆失败");
        httpServletResponse.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        httpServletResponse.getWriter().write(objectMapper.writeValueAsString(e));
    }
}

四.配置路由跳转或者JSON的登陆返回方式

package com.moss.securitybrowser.security;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.moss.securitycore.properties.LoginType;
import com.moss.securitycore.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 lwj
 */
@Component("myAuthenticationSuccessHandler")
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        logger.info("登陆成功");
        if (LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())) {
            httpServletResponse.setContentType("application/json;charset=UTF-8");
            httpServletResponse.getWriter().write(objectMapper.writeValueAsString(authentication));
        } else {
            super.onAuthenticationSuccess(httpServletRequest, httpServletResponse, authentication);
        }
    }
}

4.6 认证流程源码级讲解

一、认证流程图解

认证流程图
针对之前的配置的用户登陆过程,在用户登陆请求过来的时候,会被UsernamePasswordAuthenticationFilter拦截到,在这个过滤器中会将登录名和密码封装到一个Authentication(未认证)中,然后调用AutnenticationManager(该类中并不包含处理逻辑,仅是对AuthenticationProvider的管理)中的authenticat方法。根据当前传入的authentication的类型决定调用哪个AuthenticationProvider来处理具体的登录逻辑。本例子中的是一个authentication是一个UsernamePasswordAuthenticationToken,对应的一个provider是一个DaoAuthenticationProvider,该类中将会去调用UserDetailsService中的loadUserByUsername()方法获取一个UserDetails对象。如果拿到了这个UserDetails对象,则还会进行三个预检查。当都检查通过的时候,则会创建一个登录成功的Authentication(已认证)。并将authentication放到SpringContextHolder中。
在这里插入图片描述
SecurityContextPersistenceFilter会校验线程中是否存在authentication

4.7 图形验证码

一、编写图形验证码服务

1.创建ImageCode类

package com.moss.securitycore.validate.code.image;

import lombok.Getter;
import lombok.Setter;

import java.awt.image.BufferedImage;
import java.time.LocalDateTime;

/**
 * 图形验证码
 *
 * @author lwj
 */
@Getter
@Setter
public class ImageCode {

    /** 图形验证码 */
    private BufferedImage image;
    /** 验证码 */
    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 ImageCode(BufferedImage image, String code, LocalDateTime expireTime) {
        this.image = image;
        this.code = code;
        this.expireTime = expireTime;
    }

}

2.创建验证码服务部分代码

package com.moss.securitycore.validate.code;

import com.moss.securitycore.validate.code.image.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.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;

/**
 * 图形验证码服务
 *
 * @author lwj
 */
@RestController
public class ValidateCodeController {

    /** ImageCode在session中的key */
    private static final String SESSION_KEY_IMAGE_CODE = "SESSION_KEY_IMAGE_CODE";

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @GetMapping("/code/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        //  生成ImageCode
        ImageCode imageCode = createImageCode(request);
        //  将ImageCode放到session中
        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_IMAGE_CODE, imageCode);
        //  将图形验证码写到响应输出流中
        ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
    }

    /**
     * 创建图形验证码
     * @param request
     * @return
     */
    private ImageCode createImageCode(HttpServletRequest request) {
        // 在内存中创建图象
        int width = 65, height = 20;
        BufferedImage image = new BufferedImage(width, height,
                BufferedImage.TYPE_INT_RGB);
        // 获取图形上下文
        Graphics g = image.getGraphics();
        // 生成随机类
        Random random = new Random();
        // 设定背景色
        g.setColor(getRandColor(230, 255));
        g.fillRect(0, 0, 100, 25);
        // 设定字体
        g.setFont(new Font("Arial", Font.CENTER_BASELINE | Font.ITALIC, 18));
        // 产生0条干扰线,
        g.drawLine(0, 0, 0, 0);
        // 取随机产生的认证码(4位数字)
        String sRand = "";
        for (int i = 0; i < 4; i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand += rand;
            // 将认证码显示到图象中
            g.setColor(getRandColor(100, 150));// 调用函数出来的颜色相同,可能是因为种子太接近,所以只能直接生成
            g.drawString(rand, 15 * i + 6, 16);
        }
        g.dispose();
        return new ImageCode(image, sRand, 60);
    }

    /**
     * 给定范围获得随机颜色
     *
     * @param fc
     * @param bc
     * @return
     */
    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);
    }

}

3.将验证码的服务路由配置到permitAll()中防止被权限拦截。

.antMatchers("/authentication/require", securityProperties.getBrowser().getLoginPage(), "/code/image").permitAll()

4.登录界面的html代码

<!DOCTYPE html>
<html lang="en">
<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>

二、编写图形验证码校验部分代码

根据之前的SpringSecurity的基本原理只需要编写一个过滤器,并将该过滤器添加到UsernamePasswordAuthenticationFilter前就可以了。(图见上面)

1.编写图形验证码的Filter

package com.moss.securitycore.validate.code;

import cn.hutool.core.util.StrUtil;
import com.moss.securitycore.validate.code.image.ImageCode;
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;

/**
 * 图形验证码过滤器
 *
 * @author lwj
 */
public class ValidateCodeFilter extends OncePerRequestFilter {

    private AuthenticationFailureHandler authenticationFailureHandler;

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        if (StrUtil.equals("/authentication/form", httpServletRequest.getRequestURI())
                && StrUtil.endWithIgnoreCase(httpServletRequest.getMethod(), "POST")) {
            try {
                validate(new ServletWebRequest(httpServletRequest));
            } catch (ValidateCodeException e) {
                authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
                return;
            }
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }

    private void validate(ServletWebRequest servletWebRequest) throws ServletRequestBindingException {
        ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(servletWebRequest, ValidateCodeController.SESSION_KEY_IMAGE_CODE);
        String codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "imageCode");
        if (StrUtil.isBlank(codeInRequest)) {
            throw new ValidateCodeException("验证码的值不能为空");
        }
        if (codeInSession == null) {
            throw new ValidateCodeException("验证码不存在");
        }
        if (codeInSession.isExpried()) {
            sessionStrategy.removeAttribute(servletWebRequest, ValidateCodeController.SESSION_KEY_IMAGE_CODE);
            throw new ValidateCodeException("验证码已过期");
        }
        if (!StrUtil.equals(codeInSession.getCode(), codeInRequest)) {
            throw new ValidateCodeException("验证码不匹配");
        }
        sessionStrategy.removeAttribute(servletWebRequest, ValidateCodeController.SESSION_KEY_IMAGE_CODE);
    }

    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.编写ValidateCodeException

package com.moss.securitycore.validate.code;

import org.springframework.security.core.AuthenticationException;

public class ValidateCodeException extends AuthenticationException {

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

3.修改SecurityConfig配置类

    /**
     * 配置安全规则
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        validateCodeFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);

        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
                //  配置自定义登陆页面请求
                .loginPage("/authentication/require")
                //  配置登陆请求
                .loginProcessingUrl("/authentication/form")
                //  配置登陆成功处理器
                .successHandler(myAuthenticationSuccessHandler)
                //  配置登陆失败处理器
                .failureHandler(myAuthenticationFailureHandler)
//                .defaultSuccessUrl("/test")
                .and()
                .authorizeRequests()
                //  配置不拦截登陆页面
                .antMatchers("/authentication/require", securityProperties.getBrowser().getLoginPage(), "/code/image", "/error").permitAll()
                .anyRequest()
                .authenticated();
        http.csrf().disable();
    }

4.8 图形验证码代码重构

一、验证码基本参数可配置

参数的重构

1.创建ImageCodeProperties,ValidateCodePeoperties配置类

package com.moss.securitycore.properties;

import lombok.Data;

/**
 * 图形验证码配置类
 *
 * @author lwj
 */
@Data
public class ImageCodeProperties {

    /** 宽度 */
    private int width = 65;
    /** 长度 */
    private int height = 23;
    /** 验证码长度 */
    private int length = 4;
    /** 过期时间 */
    private int expireIn = 60;

}
package com.moss.securitycore.properties;

import lombok.Data;

/**
 * 验证配置类
 *
 * @author lwj
 */
@Data
public class ValidateCodeProperties {

    private ImageCodeProperties image = new ImageCodeProperties();
}

在总配置类中添加验证部分的配置

package com.moss.securitycore.properties;

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

/**
 * @author lwj
 */
@Data
@ConfigurationProperties(prefix = "moss.security")
public class SecurityProperties {

    private BrowserProperties browser = new BrowserProperties();

    private ValidateCodeProperties code = new ValidateCodeProperties();

}

2.修改图形验证码创建方法,将固定值替换成参数值变量

/**
 * 创建图形验证码
 * @param request
 * @return
 */
private ImageCode createImageCode(ServletWebRequest 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(230, 255));
    g.fillRect(0, 0, 100, 25);
    // 设定字体
    g.setFont(new Font("Arial", Font.CENTER_BASELINE | Font.ITALIC, 18));
    // 产生0条干扰线,
    g.drawLine(0, 0, 0, 0);
    // 取随机产生的认证码(4位数字)
    String sRand = "";
    for (int i = 0; i < securityProperties.getCode().getImage().getLength(); i++) {
        String rand = String.valueOf(random.nextInt(10));
        sRand += rand;
        // 将认证码显示到图象中
        g.setColor(getRandColor(100, 150));// 调用函数出来的颜色相同,可能是因为种子太接近,所以只能直接生成
        g.drawString(rand, 15 * i + 6, 16);
    }
    g.dispose();
    return new ImageCode(image, sRand, securityProperties.getCode().getImage().getExpireIn());
}

3.修改默认登陆界面中的图形验证码的请求

<img src="/code/image?width=200">

二、验证码拦截的接口可配置

1.在ImageCodeProperties类中添加请求属性

/** 需要校验的请求 */
private String url;

2.修改ValidateCodeFilter中的代码

将ValidateCodeFilter实现InitializingBean接口

public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean

实现afterPropertiesSet接口

@Override
public void afterPropertiesSet() throws ServletException {
    super.afterPropertiesSet();
    String[] configUrls = StrUtil.split(securityProperties.getCode().getImage().getUrl(), ",");
    for (String configUrl : configUrls) {
        urls.add(configUrl);
    }
    urls.add("/authentication/form");
}

修改doFilter方法

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

    boolean action = false;
    for (String url : urls) {
        if (antPathMatcher.match(url, httpServletRequest.getRequestURI())) {
            action = true;
        }
    }

    if (action) {
        try {
            validate(new ServletWebRequest(httpServletRequest));
        } catch (ValidateCodeException e) {
            authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
            return;
        }
    }
    filterChain.doFilter(httpServletRequest, httpServletResponse);
}

修改SecurityConfig中的config方法

validateCodeFilter.afterPropertiesSet();

三、验证码的生成逻辑可配置

以增量的方式去适应变化
这个地方的代码重构就需要注意了,通过接口的方式,去重写默认实现方式,再通过注解@ConditionalOnMissingBean(name = “imageCodeGenerator”) 的方式去配置默认的实现类。在demo项目中可以通过编写配置类的方式去编写一个对应的imageCodeGenerator的方式去替换默认实现类

/**
 * 图形校验配置类
 *
 * @author lwj
 */
@Configuration
public class ValidateCodeBeanConfig {

    @Autowired
    private SecurityProperties securityProperties;

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

demo项目配置类代码

/**
 * 图形校验配置类
 *
 * @author lwj
 */
@Configuration
public class ValidateCodeBeanConfig {

    @Autowired
    private SecurityProperties securityProperties;

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

4.9 记住我功能

基本原理

1.添加记住我功能的超时时间 BrowserProperties

/** 记住我时限 */
private int rememberMeSeconds = 3600;

2.在登陆页中添加记住我checkbox

注意==name=“remember-me” ==

<tr>
    <td colspan="2"><input name="remember-me" type="checkbox" value="true"/>记住我</td>
</tr>

3.在SecurityConfig中添加记住我部分的配置

//	注入数据源的配置
@Autowired
private DataSource dataSource;

@Bean
public PersistentTokenRepository persistentTokenRepository() {
    JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
    jdbcTokenRepository.setDataSource(dataSource);
    //  在项目启动时自动创建表
    jdbcTokenRepository.setCreateTableOnStartup(true);
    return jdbcTokenRepository;
}

在configure配置方法中添加

.rememberMe()
.tokenRepository(persistentTokenRepository())
.userDetailsService(myUserDetailService)
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
.and()

4.10 短信验证码接口开发

一、获取短信验证码接口开发

1.重构验证码配置类

拆分之前的ImageCodeProperties类中的属性,创建SmsCodeProperties 类作为验证码配置类的父类,将原先ImageCodeProperties类中的属性抽取到该类中

package com.moss.securitycore.properties;

import lombok.Data;

/**
 * 图形验证码配置类
 *
 * @author lwj
 */
@Data
public class SmsCodeProperties {
    /** 验证码长度 */
    private int length = 6;
    /** 过期时间 */
    private int expireIn = 60;
    /** 需要校验的请求 */
    private String url;
}

修改ImageCodeproperties类,继承SmsCodeProperties类

package com.moss.securitycore.properties;

import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * 图形验证码配置类
 *
 * @author lwj
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class ImageCodeProperties extends SmsCodeProperties {

    /**
     * 当实例化ImageCodeProperties时初始化图形验证码默认的length值
     */
    public ImageCodeProperties() {
        setLength(4);
    }

    /** 宽度 */
    private int width = 65;
    /** 长度 */
    private int height = 23;

}

将短信验证码类添加到验证码的基类配置类中

package com.moss.securitycore.properties;

import lombok.Data;

/**
 * 验证配置类
 *
 * @author lwj
 */
@Data
public class ValidateCodeProperties {

    private ImageCodeProperties image = new ImageCodeProperties();

    private SmsCodeProperties sms = new SmsCodeProperties();
}

2.重构验证码类

添加ValidateCode类作为验证码封装类的基类

package com.moss.securitycore.validate.code;

import lombok.Data;

import java.time.LocalDateTime;

/**
 * 验证码
 *
 * @author lwj
 */
@Data
public class ValidateCode {

    /** 验证码 */
    private String code;
    /** 过期时间 */
    private LocalDateTime expireTime;

    public ValidateCode(String code, int expireIn) {
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
    }

    public ValidateCode(String code, LocalDateTime expireTime) {
        this.code = code;
        this.expireTime = expireTime;
    }

    /**
     * 判读当前验证码是否过期
     *
     * @return
     */
    public boolean isExpired() {
        return LocalDateTime.now().isAfter(expireTime);
    }
}

将ImageCode类继承ValidateCode基类

package com.moss.securitycore.validate.code.image;

import com.moss.securitycore.validate.code.ValidateCode;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.awt.image.BufferedImage;
import java.time.LocalDateTime;

/**
 * 图形验证码
 *
 * @author lwj
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class ImageCode extends ValidateCode {

    /** 图形验证码 */
    private BufferedImage image;

    public ImageCode(BufferedImage image, String code, int expireIn) {
        super(code, expireIn);
        this.image = image;
    }

    public ImageCode(String code, LocalDateTime expireTime, BufferedImage image) {
        super(code, expireTime);
        this.image = image;
    }
}

3.添加SmsCodeGenerator类

package com.moss.securitycore.validate.code;

import cn.hutool.core.util.RandomUtil;
import com.moss.securitycore.properties.SecurityProperties;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.ServletWebRequest;

/**
 * 短信验证码生成器实现类
 *
 * @author lwj
 */
@Data
@Component("smsCodeGenerator")
public class SmsCodeGenerator implements ValidateCodeGenerator {

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public ValidateCode generate(ServletWebRequest request) {
        String sRand = RandomUtil.randomNumbers(securityProperties.getCode().getSms().getLength());
        return new ValidateCode(sRand, securityProperties.getCode().getSms().getExpireIn());
    }
}

4.修改ValidateCodeGenerator

此处将返回类型改成了父类,将这个接口重用了(不知道理解对不对)

package com.moss.securitycore.validate.code;

import org.springframework.web.context.request.ServletWebRequest;

/**
 * 验证码生成器
 *
 * @author lwj
 */
public interface ValidateCodeGenerator {

    /**
     * 生成验证码
     *
     * @param request
     * @return
     */
    ValidateCode generate(ServletWebRequest request);
}

5.编写默认短信发送接口和实现

5.1创建DefaultSmsCodeSender实现类
package com.moss.securitycore.validate.code.sms;

/**
 * 默认短信验证接口实现类
 *
 * @author lwj
 */
public class DefaultSmsCodeSender implements SmsCodeSender {

    @Override
    public void send(String mobile, String code) {
        System.out.println("手机号:" + mobile + "短信验证码:" + code);
    }

}
5.2创建SmsCodeSender接口
package com.moss.securitycore.validate.code.sms;

/**
 * 短信验证码发送接口
 *
 * @author lwj
 */
public interface SmsCodeSender {

    /**
     * 发送验证码
     *
     * @param mobile 手机号
     * @param code  验证码
     */
    void send(String mobile, String code);
}

6.修改ValidateCodeController类

package com.moss.securitycore.validate.code;

import com.moss.securitycore.validate.code.image.ImageCode;
import com.moss.securitycore.validate.code.sms.SmsCodeSender;
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.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
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;
import java.io.IOException;

/**
 * 图形验证码服务
 *
 * @author lwj
 */
@RestController
public class ValidateCodeController {

    /** ImageCode在session中的key */
    public static final String SESSION_KEY_IMAGE_CODE = "SESSION_KEY_IMAGE_CODE";

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Autowired(required = false)
    private ValidateCodeGenerator imageCodeGenerator;

    @Autowired(required = false)
    private ValidateCodeGenerator smsCodeGenerator;

    @Autowired(required = false)
    private SmsCodeSender smsCodeSender;

    @GetMapping("/code/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        //  生成ImageCode
        ImageCode imageCode = (ImageCode) imageCodeGenerator.generate(new ServletWebRequest(request));
        //  将ImageCode放到session中
        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_IMAGE_CODE, imageCode);
        //  将图形验证码写到响应输出流中
        ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
    }

    @GetMapping("/code/sms")
    public void createSmsCode(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletRequestBindingException {
        //  生成验证码
        ValidateCode smsCode = smsCodeGenerator.generate(new ServletWebRequest(request));
        //  将验证码放到session中
        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_IMAGE_CODE, smsCode);
        String mobile = ServletRequestUtils.getRequiredStringParameter(request, "mobile");
        smsCodeSender.send(mobile, smsCode.getCode());
    }
}

7.重构验证码部分代码

重构部分不在此详述
重构代码图示

4.11 短信登陆开发

短信登陆逻辑

猜你喜欢

转载自blog.csdn.net/weixin_38657051/article/details/88690032
今日推荐