SpringSecurity学习总结-3 使用SpringSecurity开发基于表单的登录_springsecurity会检查账号可用状态吗

 */
private RequestCache requestCache = new HttpSessionRequestCache();

/**
 * 负责进行跳转
 */
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

/**
 * 当需要身份认证时跳转到这里,状态码是未授权的,即没有登陆
 * @return
 */
@GetMapping("/authentication/require")
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {

    //SavedRequest:具体存有请求信息的类
    SavedRequest savedRequest = requestCache.getRequest(request,response);

    //如果有请求信息
    if(savedRequest != null){
        //获取引发请求的url地址
        String targetUrl = savedRequest.getRedirectUrl();
        log.info("引发跳转的url:"+targetUrl);

        String properties = securityProperties.getBrowser().getLoginPage();
        log.info("properties:" + properties);
        //判断targetUrl是否以.html结尾
        if(StringUtils.endsWithIgnoreCase(targetUrl,".html")){
            redirectStrategy.sendRedirect(request,response,properties);
        }else{

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

}


(4)在core模块的新建一个application-browser.properties中添加配置



#自定义配置用于处理不同客户端的登录请求
imooc.security.browser.loginPage = /login/browser-login.html


(5)在browser模块新建一个login文件夹,增加一个 browser-login.html



browser页面

browser页面

```
(6)在core模块新建配置相关的类,用途不同配置项也不同。

无

①BrowserProperties:封装和浏览器相关的配置项。

②ValidateCodeProperties:封装和验证码相关的配置项。

③Oauth2Properties:Oauth相关的配置项。

④SocialProperties:会话相关的配置项。

⑤SecurityProperties:父配置项,里面封装了针对不同类型的配置子项。

(7)在core模块新建配置类

它(demo模块)读取的是core模块里的application-browser.properties配置。不采用视频中讲解的使用demo模块的配置文件是因为发现core模块里的配置类无法读取demo模块的配置,因此将各个模块的配置文件统一放在了core模块里,目的是方便core模块读取配置文件里的配置信息,而且core是基础模块,其他模块都直接或间接的依赖core模块,所以其他模块在需要使用自己所需的配置文件时,只需要在本模块的application,properties文件里加入  spring.profiles.active= browser来激活不同配置,这样就能在本模块使用core模块读取的配置信息。

①SecurityProperties配置类

package security.core.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

@Component
@PropertySource("classpath:application-browser.properties")
//@PropertySource(value= {"classpath:application-demo.properties","classpath:application-browser.properties"})
@ConfigurationProperties("imooc.security")
public class SecurityProperties {

    private BrowserProperties browser = new BrowserProperties();

    public BrowserProperties getBrowser() {
        return browser;
    }

    public void setBrowser(BrowserProperties browser) {
        this.browser = browser;
    }
}

②BrowserProperties配置类

package security.core.properties;

public class BrowserProperties {

    /**
     * 设置默认的登录页面
     */
    private String loginPage = "/signIn.html";

    public String getLoginPage() {
        return loginPage;
    }

    public void setLoginPage(String loginPage) {
        this.loginPage = loginPage;
    }
}

③在browser模块的启动类上加**@EnableConfigurationProperties(SecurityProperties.class)**

提示:哪一个模块需要使用core模块里的配置类,就需要在那一个模块的启动类上加@EnableConfigurationProperties(SecurityProperties.class)

package security.browser;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import security.core.properties.SecurityProperties;

@SpringBootApplication
@EnableConfigurationProperties(SecurityProperties.class)
public class BrowserApplication {

	public static void main(String[] args) {
		SpringApplication.run(BrowserApplication.class, args);
	}

}

(8)登陆成功的处理(4-5节内容)

①新建处理登录成功的处理器类并实现AuthenticationSuccessHandler

package security.browser.authentication;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
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 security.core.support.SimpleResponse;

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

/**
 * 自定义登陆成功的处理器
 */
@Component("imoocAuthenticationSuccessHandler")
@Slf4j
public class ImoocAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    /**
     * 用于将对象转为json类型
     */
    @Autowired
    private ObjectMapper objectMapper;

    /**
     * @param request
     * @param response
     * @param authentication   封装了认证信息,包括请求时的ip、session以及认证通过后UserDeatilsService放回的UserDetails
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("登陆成功!");

        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(authentication));
    }
}


②在browser模块的配置类中加入处理登陆成功的方法

    /**
     * 自定义的登陆成功的处理器
     */
    @Autowired
    private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;       

------------------------------------------------------------------------------------------------

 http.formLogin()//开启表单登录(即对表单登录进行身份认证)
                .loginPage("/authentication/require")//指定登录页面
                .loginProcessingUrl("/authentication/form")//让UsernamePasswordAuthenticationFilter能够处理提交的登录请求
                .successHandler(imoocAuthenticationSuccessHandler)

③修改core模块application-browser.properties中的

#注释掉即可恢复使用原来默认的登录页面
#imooc.security.browser.loginPage = /login/browser-login.html

3、登录失败的处理

与2中的(8)类似

(1)新建处理登录成功的处理器类并实现AuthenticationFailureHandler

package security.browser.authentication;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
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;

/**
 * 自定义登录失败的处理器
 */
@Component("imoocAuthenticationFailureHandler")
@Slf4j
public class ImoocAuthenticationFailureHandler implements AuthenticationFailureHandler {

    /**
     * 用于将对象转为json类型
     */
    @Autowired
    private ObjectMapper objectMapper;

    /**
     * @param request
     * @param response
     * @param exception 包含认证过程中出现的异常信息
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        log.info("登陆失败!");

        //登陆失败时返回服务器内部异常
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(exception));

    }
}

(2)在browser模块的配置类中加入处理登陆失败的方法

    /**
     * 自定义的登陆失败的处理器
     */
    @Autowired
    private AuthenticationFailureHandler imoocAuthenticationFailureHandler;

----------------------------------------------------------------------------------------------------       

 http.formLogin()//开启表单登录(即对表单登录进行身份认证)
                .loginPage("/authentication/require")//指定登录页面
                .loginProcessingUrl("/authentication/form")//让UsernamePasswordAuthenticationFilter能够处理提交的登录请求
                .successHandler(imoocAuthenticationSuccessHandler)
                .failureHandler(imoocAuthenticationFailureHandler)
4、对登陆成功和登陆失败进行改造,使它能处理浏览器和APP端请求

(1)在core模块的properties包中新建一个返回类型的枚举类

package security.core.properties;

/**
 * 返回类型的枚举类
 */
public enum LoginType {

    /**
     * 跳转
     */
    REDIRECT,
    /**
     *返回JSON
     */
    JSON
}

(2)在BrowserProperties类加入

    /**
     * 设置默认返回JSON
     */
    private LoginType loginType = LoginType.JSON;

    public LoginType getLoginType() {
        return loginType;
    }

    public void setLoginType(LoginType loginType) {
        this.loginType = loginType;
    }

完整代码:

package security.core.properties;

public class BrowserProperties {

    /**
     * 设置默认的登录页面
     */
    private String loginPage = "/signIn.html";

    /**
     * 设置默认返回JSON
     */
    private LoginType loginType = LoginType.JSON;

    public String getLoginPage() {
        return loginPage;
    }

    public void setLoginPage(String loginPage) {
        this.loginPage = loginPage;
    }

    public LoginType getLoginType() {
        return loginType;
    }

    public void setLoginType(LoginType loginType) {
        this.loginType = loginType;
    }
}

(3)改造登录成功的处理器

①让它继承SavedRequestAwareAuthenticationSuccessHandler

(查SavedRequestAwareAuthenticationSuccessHandler继承的父类,一直往上查看,可以发现最终也是实现了AuthenticationSuccessHandler接口。)

package security.browser.authentication;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import security.core.properties.LoginType;
import security.core.properties.SecurityProperties;
import security.core.support.SimpleResponse;

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

/**
 * 自定义登陆成功的处理器
 */
@Component("imoocAuthenticationSuccessHandler")
@Slf4j
public class ImoocAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

}

②加入SecurityProperties类读取配置

    /**
     * 用户判断需要返回的类型
     */
    @Autowired
    private SecurityProperties securityProperties;

③在方法里加入判断逻辑

        //如果配置的登录方式是JSON
        if(LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())){
            response.setContentType("application/json;charset=UTF-8");
//        String type = authentication.getClass().getSimpleName();
            response.getWriter().write(objectMapper.writeValueAsString(authentication));
        }else{//调用父类的方法,跳转到index.html页面
            super.onAuthenticationSuccess(request,response,authentication);
        }

完整的代码:

package security.browser.authentication;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import security.core.properties.LoginType;
import security.core.properties.SecurityProperties;
import security.core.support.SimpleResponse;

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

/**
 * 自定义登陆成功的处理器
 */
@Component("imoocAuthenticationSuccessHandler")
@Slf4j
public class ImoocAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    /**
     * 用于将对象转为json类型
     */
    @Autowired
    private ObjectMapper objectMapper;

    /**
     * 用户判断需要返回的类型
     */
    @Autowired
    private SecurityProperties securityProperties;

    /**
     * @param request
     * @param response
     * @param authentication   封装了认证信息,包括请求时的ip、session以及认证通过后UserDeatilsService放回的UserDetails
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("登陆成功!");

        //如果配置的返回类型是JSON
        if(LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())){
            response.setContentType("application/json;charset=UTF-8");
//        String type = authentication.getClass().getSimpleName();
            response.getWriter().write(objectMapper.writeValueAsString(authentication));
        }else{//调用父类的方法,跳转到index.html页面
            super.onAuthenticationSuccess(request,response,authentication);
        }
    }
}

(4)在core模块的 application-browser.properties 配置文件中加入配置

imooc.security.browser.loginType = REDIRECT

则启用REDIRECT类型,即重定向到index.html页面;如果不配置则使用JSON类型。需要特别注意的是使用时默认是JSON类型,如果在配置文件中改成了REDIRECT,则返回的信息会不同。

(5)新建在templates文件夹下index.html页面

(6)新建一个负责页面跳转的控制器类,加入处理url为index.html的跳转请求。


    @GetMapping("/index.html")
    public String index(){
        return "/index.html";
    }

以上就完成了对登陆成功的处理根据类型的返回情况。

登录失败的处理:

(1)登录失败的处理器继承SimpleUrlAuthenticationFailureHandler

(查看SimpleUrlAuthenticationFailureHandler的信息,可以发现它也实现了****AuthenticationFailureHandler。)

package security.browser.authentication;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
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.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import security.core.properties.LoginType;
import security.core.properties.SecurityProperties;

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

/**
 * 自定义登录失败的处理器
 */
@Component("imoocAuthenticationFailureHandler")
@Slf4j
public class ImoocAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

}

(2)在方法里加入判断类型的逻辑

        //如果配置的返回类型是JSON
        if(LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())){
            //登陆失败时返回服务器内部异常
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.setContentType("application/json;charset=UTF-8");
            //改为只返回错误信息
            response.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse(exception.getMessage())));
        }else{
            super.onAuthenticationFailure(request,response,exception);
        }

完整代码:

package security.browser.authentication;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
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.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import security.core.properties.LoginType;
import security.core.properties.SecurityProperties;

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

/**
 * 自定义登录失败的处理器
 */
@Component("imoocAuthenticationFailureHandler")
@Slf4j
public class ImoocAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    /**
     * 用于将对象转为json类型
     */
    @Autowired
    private ObjectMapper objectMapper;

    /**
     * 用户判断需要返回的类型
     */
    @Autowired
    private SecurityProperties securityProperties;

    /**
     * @param request
     * @param response
     * @param exception 包含认证过程中出现的异常信息
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        log.info("登陆失败!");

        //如果配置的返回类型是JSON
        if(LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())){
            //登陆失败时返回服务器内部异常
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(exception));
        }else{
            super.onAuthenticationFailure(request,response,exception);
        }
        
    }
}

五、认证流程源码级详解(4-6节内容)")

六、图片验证码

需要注意各个类所在的模块。

1、开发生成图形验证码的接口

(1)根据随机数生成一个图片,下面是工具类 (祖传代码,亲测可用,哈哈哈)
package security.core.validateCode;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

/**
 * 随机生成验证码的工具类
 */
public class ImageCodeUtil {
	
	//CodeUtils mCodeUtils 属性 和 CodeUtils getInstance()方法可去除,若要去除,则generateCodeAndPic()应该声明成静态方,即用static修饰,调用的时候通过类名直接调用

	private static ImageCodeUtil imageCodeUtils;
	public static ImageCodeUtil getInstance() {
        if(imageCodeUtils == null) {
			imageCodeUtils = new ImageCodeUtil();
        }
        return imageCodeUtils;
    }

	/**
	 * 定义图片的width
	 */
	private static int width = 115;
	/**
	 * 定义图片的height
	 */
	private static int height = 34;
	/**
	 * 验证码的长度  这里是6位
	 */
	private static final int DEFAULT_CODE_LENGTH = 6;

	/**
	 * 生成的验证码
	 */
	private String randomString;

	private Random random;

	/**
	 * 随机字符字典   去掉了[0,1,I,O,o]这几个容易混淆的字符
	 */
	private static final char[] CHARS = { '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
			'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
			'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' };

    /**
     * 生成验证码
     * @return
     */
    public String getRandomString() {
		StringBuilder mBuilder = new StringBuilder();
    	random = new Random();
		//使用之前首先清空内容
        mBuilder.delete(0, mBuilder.length());

        for (int i = 0; i < DEFAULT_CODE_LENGTH; i++) {
            mBuilder.append(CHARS[random.nextInt(CHARS.length)]);
        }

        return mBuilder.toString();
    }

	/**
	 * 获取随机数颜色
	 * 
	 * @return
	 */
	private Color getRandomColor() {
		return new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255));
	}

	/**
	 * 返回某颜色的反色
	 * 
	 * @param color
	 * @return
	 */
	private Color getReverseColor(Color color) {
		return new Color(255 - color.getRed(), 255 - color.getGreen(), 255 - color.getBlue());
	}

	/**
	 * 生成一个map集合 code为生成的验证码 codePic为生成的验证码BufferedImage对象
	 * 
	 * @return
	 */
	public Map<String, Object> generateCodeAndPic() {
		// 定义图像buffer
		BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
		Graphics2D graphics = bufferedImage.createGraphics();
		//生成验证码字符
	    randomString = getRandomString();
		for (int i = 0; i < randomString.length(); i++) {
			Color color = getRandomColor();
			Color reverse = getReverseColor(color);
			// 设置字体颜色
			graphics.setColor(color);
			// 设置字体样式
			graphics.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 25));
			//设置验证码图片原点以及验证码图片大小,来画矩形
			graphics.fillRect(0, 0, width, height);
			graphics.setColor(reverse);
			//10:是验证码在验证码图片中左边第一个字符距左边框的距离 ,25:是所有验证码的底部距离验证码图片上边框的距离
			graphics.drawString(randomString, 10, 25);
		}
		// 随机生成一些点
		for (int i = 0, n = random.nextInt(100); i < n; i++) {
			graphics.drawRect(random.nextInt(width), random.nextInt(height), 1, 1);
		}
		// 随机产生干扰线,使图象中的认证码不易被其它程序探测到
		for (int i = 0; i < 10; i++) {
			graphics.setColor(getRandomColor());
			// 保证画在边框之内
			final int x = random.nextInt(width - 1);
			final int y = random.nextInt(height - 1);
			final int xl = random.nextInt(width);
			final int yl = random.nextInt(height);
			graphics.drawLine(x, y, x + xl, y + yl);
		}
		// 图像生效
		graphics.dispose();
		Map<String, Object> map = new HashMap<String, Object>();
		// 存放验证码
		map.put("imageCode", randomString);

		// 存放生成的验证码BufferedImage对象
		map.put("codePic", bufferedImage);
		return map;
	}
}

(2)开发处理前台获取验证码请求的处理器,并把随机数存到Session中
package security.browser.controller;

import lombok.extern.slf4j.Slf4j;
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 security.core.validateCode.ImageCodeUtil;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.util.Map;

@RestController
@Slf4j
public class ValidateCodeController {

    /**
     * 图形验证码的key
     */
    private static final String SESSION_IMAGE_CODE_KEY = "SESSION_IMAGE_CODE_KEY";

    /**
     * social工具,用于存储Session信息
     */
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    /**
     * 提供图片验证码
     * @param request
     * @param response
     * @throws IOException
     */
    @GetMapping("/code/image")
    public void createImageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        //1、生成验证码
        Map<String, Object> imageCodeMap = ImageCodeUtil.getInstance().generateCodeAndPic();

        log.info("图形验证码:"+imageCodeMap.get("imageCode"));
        //2、存入到Session中
        sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_IMAGE_CODE_KEY,imageCodeMap.get("imageCode"));

        //3、写入到响应中(response)
        ImageIO.write((RenderedImage) imageCodeMap.get("codePic"),"JPEG",response.getOutputStream());
    }
}

(3)将生成的图片写到接口的响应中,显示到页面上,即(2)中的步骤3。
(4)在配置类中加入允许访问图片验证吗的url
                .antMatchers("/authentication/require",securityProperties.getBrowser().getLoginPage()
                ,"/code/image").permitAll()//允许signIn.html请求进来,不进行拦截

(5)在负责登录的HTML页面中加入

            <tr>
                <td>图形验证码:</td>
                <td>
                    <input type="text" name="imageCode">
                    <img src="/code/image">
                </td>
            </tr>

输入框中name=“imageCode”,是验证码的参数名称,后台就是使用imageCode来从request中获取前台输入的验证码。

完整的signIn.html代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
    <h3>表单登录</h3>
<!--    <form action="/security-browser/authentication/form" method="post">-->
    <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>

2、在认证流程中加入图形验证码校验。

①新建一个验证码的过滤器类 ValidateCodeFilter并继承 OncePerRequestFilter,OncePerRequestFilter类是在所有过滤器前执行的过滤器,即过滤器链上的前置过滤器。

package security.core.validateCode;


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;


/**
 * 在处理请求之前过滤,且只过滤一次
 */
public class ValidateCodeFilter extends OncePerRequestFilter {

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

在 doFilterInternal() 方法中对提交的登录请求进行过滤,主要的目的是为了校验验证码。特别需要注意的是 filterChain.doFilter(request,response); 不要写错位置,它是在if()代码块后面,如果if()代码块里有异常则不会再执行filterChain.doFilter(request,response); ,直接返回异常信息;如果没有出现异常需要继续调用过滤器链上的其他过滤器,主要是调用后面的UsernamePasswordAuthenticationFilter过滤器。

完整代码:

package security.core.validateCode;


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.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;

/**
 * 在处理请求之前过滤,且只过滤一次
 */
public class ValidateCodeFilter extends OncePerRequestFilter {

    /**
     * 登陆失败的处理器
     */
    private AuthenticationFailureHandler authenticationFailureHandler;

    /**
     * 图形验证码的key
     */
    private static final String SESSION_IMAGE_CODE_KEY = "SESSION_IMAGE_CODE_KEY";

    /**
     * 存储了Session信息
     */
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    /**
     * 对图形验证码进行校验
     * @param request
     * @param response
     * @param filterChain
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        //如果提交的请求url是/authentication/form,且是POST请求
        if(StringUtils.equals("/authentication/form",request.getRequestURI())
                && StringUtils.equalsIgnoreCase(request.getMethod(),"post")){
            try {
                //从Session中获取参数,需要以ServletWebRequest为参数
                validate(new ServletWebRequest(request));
            }catch (ValidateCodeException exception){
                //如果捕获异常就使用authenticationFailureHandler把错误信息返回回去
                authenticationFailureHandler.onAuthenticationFailure(request,response,exception);
                return;//如果抛出异常了就不再继续走下面的过滤器了
            }
        }
        //校验完图形验证码后调用下一个过滤器
        filterChain.doFilter(request,response);
    }

    /**
     * 图形验证码校验的具体方法
     * @param request
     */
    public void validate(ServletWebRequest request) throws ServletException{
        //Session中取出,即后台存储的验证码
        String sessionImageCode = (String)sessionStrategy.getAttribute(request,SESSION_IMAGE_CODE_KEY);

        //从请求中取出
        String requestImageCode = ServletRequestUtils.getStringParameter(request.getRequest(),"imageCode");
        if(StringUtils.isBlank(requestImageCode)){
            throw new ValidateCodeException("验证码不能为空");
        }
        if(StringUtils.isBlank(sessionImageCode)){
            throw new ValidateCodeException("验证码不存在");
        }
        if(!StringUtils.equalsIgnoreCase(sessionImageCode,requestImageCode)){
            throw new ValidateCodeException("验证码不匹配");
        }

        //如果没有出现以上的异常则验证完后删除session中存储的验证码
        sessionStrategy.removeAttribute(request,SESSION_IMAGE_CODE_KEY);
    }

    public AuthenticationFailureHandler getAuthenticationFailureHandler() {
        return authenticationFailureHandler;
    }

    public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
        this.authenticationFailureHandler = authenticationFailureHandler;
    }
}

③验证码校验中异常的处理。

package security.core.validateCode;


import org.springframework.security.core.AuthenticationException;

import java.io.Serializable;

/**
 * AuthenticationException:是security校验过程中出现异常的父类
 */
public class ValidateCodeException extends AuthenticationException implements Serializable {

    private static final long serialVersionUID = -2799288346535627988L;

    /**
     * @param detail A possibly null string containing details of the exception.
     * @see Throwable#getMessage
     */
    public ValidateCodeException(String detail) {
        super(detail);
    }
}

④将验证码的过滤器加到过滤器链上。在 BrowserSecurityConfig 配置类的 configure(HttpSecurity http)方法中加入验证码的过滤器,放到 UsernamePasswordAuthenticationFilter

以下为部分代码:

    /**
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        //加入图片验证码的前置校验过滤器
        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        validateCodeFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);

        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()//开启表单登录(即对表单登录进行身份认证)
//    http.formLogin()//开启表单登录(即对表单登录进行身份认证)
                .loginPage("/authentication/require")//指定登录页面
                .loginProcessingUrl("/authentication/form")//让UsernamePasswordAuthenticationFilter能够处理提交的登录请求
                .successHandler(imoocAuthenticationSuccessHandler)
                .failureHandler(imoocAuthenticationFailureHandler)

完整代码:

package security.browser.config;

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.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import security.core.properties.SecurityProperties;
import security.core.validateCode.ValidateCodeFilter;

/**
 * WebSecurityConfigurerAdapter是SpringSecurity提供的安全适配器类
 */
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 读取配置信息
     */
    @Autowired
    private SecurityProperties securityProperties;

    /**
     * 自定义的登陆成功的处理器
     */
    @Autowired
    private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;

    /**
     * 自定义的登陆失败的处理器
     */
    @Autowired
    private AuthenticationFailureHandler imoocAuthenticationFailureHandler;

    /**
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        //加入图片验证码的前置校验过滤器
        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        validateCodeFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);

        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()//开启表单登录(即对表单登录进行身份认证)
//    http.formLogin()//开启表单登录(即对表单登录进行身份认证)
                .loginPage("/authentication/require")//指定登录页面
                .loginProcessingUrl("/authentication/form")//让UsernamePasswordAuthenticationFilter能够处理提交的登录请求
                .successHandler(imoocAuthenticationSuccessHandler)
                .failureHandler(imoocAuthenticationFailureHandler)
//                http.httpBasic()//开启SpringSecurity原生的表单登录
                .and()
                .authorizeRequests()//对请求进行授权(即登录后需要授权)
                .antMatchers("/authentication/require",securityProperties.getBrowser().getLoginPage(),"/code/image").permitAll()//允许signIn.html请求进来,不进行拦截
                .anyRequest()//对任何请求
                .authenticated()//开启认证
                .and()
                .csrf() //跨域请求伪造
                .disable();//关闭

//                .anyRequest()
//                .authenticated();
//            上面两个方法的意思:对任何请求都需要认证

    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

}

完成验证码所有代码。

再次强调要注意application-browser.properties配置文件中 imooc.security.browser.loginType = JSON 还是 REDIRECT,它会影响返回的结果。

3、重构图形验证码接口(目的是为了可重用)

(1)验证码基本参数可配置

即验证码和验证码图片之间的宽度、高度,验证码的长度等可配置

无

这里只有两级,即应用级配置和默认配置,对于请求级 的配置暂时舍弃。

① 在core模块的 properties包中新建 ImageCodeProperties 类,里面的数值即为默认配置。

package security.core.properties;

/**
 * 图形验证码的默认配置类
 */
public class ImageCodeProperties {

    /**
     * 验证码图片的宽度  115位默宽度
     */
    private int width = 115;

    /**
     * 验证码图片的高度  34为默认高度
     */
    private int height = 34;

    /**
     * 验证码的长度 6为默认长度
     */
    private int DEFAULT_CODE_LENGTH = 6;

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getDEFAULT_CODE_LENGTH() {
        return DEFAULT_CODE_LENGTH;
    }

    public void setDEFAULT_CODE_LENGTH(int DEFAULT_CODE_LENGTH) {
        this.DEFAULT_CODE_LENGTH = DEFAULT_CODE_LENGTH;
    }
}


② 在core模块的 properties包中新建 ValidateCodeProperties 类 (专门负责验证码相关的配置)

package security.core.validateCode;

import security.core.properties.ImageCodeProperties;

/**
 * 验证码相关的配置类(包含图片验证码、短信验证码等)
 */
public class ValidateCodeProperties {
    
    private ImageCodeProperties  image = new ImageCodeProperties();


    public ImageCodeProperties getImage() {
        return image;
    }

    public void setImage(ImageCodeProperties image) {
        this.image = image;
    }
}

③ 在 SecurityProperties 中加入 ValidateCodeProperties

package security.core.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
import security.core.validateCode.ValidateCodeProperties;

@Component
@PropertySource("classpath:application-browser.properties")
//@PropertySource(value= {"classpath:application-demo.properties","classpath:application-browser.properties"})
@ConfigurationProperties("imooc.security")
public class SecurityProperties {

    /**
     * 浏览器相关的配置
     */
    private BrowserProperties browser = new BrowserProperties();

    /**
     * 验证码相关的配置
     */
    private ValidateCodeProperties code = new ValidateCodeProperties();
    
    public BrowserProperties getBrowser() {
        return browser;
    }

    public void setBrowser(BrowserProperties browser) {
        this.browser = browser;
    }

    public ValidateCodeProperties getCode() {
        return code;
    }

    public void setCode(ValidateCodeProperties code) {
        this.code = code;
    }
}

④ 在application-browser.properties中加入应用级的配置,只要在 application-browser.properties 配置了验证码的参数信息,在使用时就会覆盖 ImageCodeProperties 默认的配置。

imooc.security.code.image.width = 120
imooc.security.code.image.height = 40
imooc.security.code.image.DEFAULT_CODE_LENGTH = 5

⑤修改 ImageCodeUtil 中的配置,让它通过读取参数信息来动态配置验证码

需要修改的地方

(1) 注释掉原有的 width、height、DEFAULT_CODE_LENGTH配置

(2) 将generateCodeAndPic() 改为 generateCodeAndPic(int width,int height,int DEFAULT_CODE_LENGTH) 来接收参数

(3) 将getRandomString() 改为getRandomString(int DEFAULT_CODE_LENGTH)

package security.core.validateCode;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

/**
 * 随机生成验证码
 * @author chenliucheng
 */
public class ImageCodeUtil {

	//CodeUtils mCodeUtils 属性 和 CodeUtils getInstance()方法可去除,若要去除,则generateCodeAndPic()应该声明成静态方,即用static修饰,调用的时候通过类名直接调用

	private static ImageCodeUtil imageCodeUtils;
	public static ImageCodeUtil getInstance() {
        if(imageCodeUtils == null) {
			imageCodeUtils = new ImageCodeUtil();
        }
        return imageCodeUtils;
    }

	/**
	 * 定义图片的width
	 */
//	private static int width = 115;
	/**
	 * 定义图片的height
	 */
//	private static int height = 34;
	/**
	 * 验证码的长度  这里是6位
	 */
//	private static final int DEFAULT_CODE_LENGTH = 6;

	/**
	 * 生成的验证码
	 */
	private String randomString;

	private Random random;

	/**
	 * 随机字符字典   去掉了[0,1,I,O,o]这几个容易混淆的字符
	 */
	private static final char[] CHARS = { '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
			'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
			'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' };

    /**
     * 生成验证码
     * @return
     */
    public String getRandomString(int DEFAULT_CODE_LENGTH) {
		StringBuilder mBuilder = new StringBuilder();
    	random = new Random();
		//使用之前首先清空内容
        mBuilder.delete(0, mBuilder.length());

        for (int i = 0; i < DEFAULT_CODE_LENGTH; i++) {
            mBuilder.append(CHARS[random.nextInt(CHARS.length)]);
        }

        return mBuilder.toString();
    }

	/**
	 * 获取随机数颜色
	 * 
	 * @return
	 */
	private Color getRandomColor() {
		return new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255));
	}

	/**
	 * 返回某颜色的反色
	 * 
	 * @param color
	 * @return
	 */
	private Color getReverseColor(Color color) {
		return new Color(255 - color.getRed(), 255 - color.getGreen(), 255 - color.getBlue());
	}

	/**
	 * 生成一个map集合 code为生成的验证码 codePic为生成的验证码BufferedImage对象
	 * 
	 * @return
	 */
	public Map<String, Object> generateCodeAndPic(int width,int height,int DEFAULT_CODE_LENGTH) {
		// 定义图像buffer
		BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
		Graphics2D graphics = bufferedImage.createGraphics();
		//生成验证码字符
	    randomString = getRandomString(DEFAULT_CODE_LENGTH);
		for (int i = 0; i < randomString.length(); i++) {
			Color color = getRandomColor();
			Color reverse = getReverseColor(color);
			// 设置字体颜色
			graphics.setColor(color);
			// 设置字体样式
			graphics.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 25));
			//设置验证码图片原点以及验证码图片大小,来画矩形
			graphics.fillRect(0, 0, width, height);
			graphics.setColor(reverse);
			//10:是验证码在验证码图片中左边第一个字符距左边框的距离 ,25:是所有验证码的底部距离验证码图片上边框的距离
			graphics.drawString(randomString, 10, 25);
		}
		// 随机生成一些点
		for (int i = 0, n = random.nextInt(100); i < n; i++) {
			graphics.drawRect(random.nextInt(width), random.nextInt(height), 1, 1);
		}
		// 随机产生干扰线,使图象中的认证码不易被其它程序探测到
		for (int i = 0; i < 10; i++) {
			graphics.setColor(getRandomColor());
			// 保证画在边框之内
			final int x = random.nextInt(width - 1);
			final int y = random.nextInt(height - 1);
			final int xl = random.nextInt(width);
			final int yl = random.nextInt(height);
			graphics.drawLine(x, y, x + xl, y + yl);
		}
		// 图像生效
		graphics.dispose();
		Map<String, Object> map = new HashMap<String, Object>();
		// 存放验证码
		map.put("imageCode", randomString);

		// 存放生成的验证码BufferedImage对象
		map.put("codePic", bufferedImage);
		return map;
	}
}

ImageCodeUtil 类的完整代码:

package security.core.validateCode;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

/**
 * 随机生成验证码
 * @author chenliucheng
 */
public class ImageCodeUtil {

	//CodeUtils mCodeUtils 属性 和 CodeUtils getInstance()方法可去除,若要去除,则generateCodeAndPic()应该声明成静态方,即用static修饰,调用的时候通过类名直接调用

	private static ImageCodeUtil imageCodeUtils;
	public static ImageCodeUtil getInstance() {
        if(imageCodeUtils == null) {
			imageCodeUtils = new ImageCodeUtil();
        }
        return imageCodeUtils;
    }

	/**
	 * 定义图片的width
	 */
//	private static int width = 115;
	/**
	 * 定义图片的height
	 */
//	private static int height = 34;
	/**
	 * 验证码的长度  这里是6位
	 */
//	private static final int DEFAULT_CODE_LENGTH = 6;

	/**
	 * 生成的验证码
	 */
	private String randomString;

	private Random random;

	/**
	 * 随机字符字典   去掉了[0,1,I,O,o]这几个容易混淆的字符
	 */
	private static final char[] CHARS = { '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
			'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
			'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' };

    /**
     * 生成验证码
     * @return
     */
    public String getRandomString(int DEFAULT_CODE_LENGTH) {
		StringBuilder mBuilder = new StringBuilder();
    	random = new Random();
		//使用之前首先清空内容
        mBuilder.delete(0, mBuilder.length());

        for (int i = 0; i < DEFAULT_CODE_LENGTH; i++) {
            mBuilder.append(CHARS[random.nextInt(CHARS.length)]);
        }

        return mBuilder.toString();
    }

	/**
	 * 获取随机数颜色
	 * 
	 * @return
	 */
	private Color getRandomColor() {
		return new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255));
	}

	/**
	 * 返回某颜色的反色
	 * 
	 * @param color
	 * @return
	 */
	private Color getReverseColor(Color color) {
		return new Color(255 - color.getRed(), 255 - color.getGreen(), 255 - color.getBlue());
	}

	/**
	 * 生成一个map集合 code为生成的验证码 codePic为生成的验证码BufferedImage对象
	 * 
	 * @return
	 */
	public Map<String, Object> generateCodeAndPic(int width,int height,int DEFAULT_CODE_LENGTH) {
		// 定义图像buffer
		BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
		Graphics2D graphics = bufferedImage.createGraphics();
		//生成验证码字符
	    randomString = getRandomString(DEFAULT_CODE_LENGTH);
		for (int i = 0; i < randomString.length(); i++) {
			Color color = getRandomColor();
			Color reverse = getReverseColor(color);
			// 设置字体颜色
			graphics.setColor(color);
			// 设置字体样式
			graphics.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 25));
			//设置验证码图片原点以及验证码图片大小,来画矩形
			graphics.fillRect(0, 0, width, height);
			graphics.setColor(reverse);
			//10:是验证码在验证码图片中左边第一个字符距左边框的距离 ,25:是所有验证码的底部距离验证码图片上边框的距离
			graphics.drawString(randomString, 10, 25);
		}
		// 随机生成一些点
		for (int i = 0, n = random.nextInt(100); i < n; i++) {
			graphics.drawRect(random.nextInt(width), random.nextInt(height), 1, 1);
		}
		// 随机产生干扰线,使图象中的认证码不易被其它程序探测到
		for (int i = 0; i < 10; i++) {
			graphics.setColor(getRandomColor());
			// 保证画在边框之内
			final int x = random.nextInt(width - 1);
			final int y = random.nextInt(height - 1);
			final int xl = random.nextInt(width);
			final int yl = random.nextInt(height);
			graphics.drawLine(x, y, x + xl, y + yl);
		}
		// 图像生效
		graphics.dispose();
		Map<String, Object> map = new HashMap<String, Object>();
		// 存放验证码
		map.put("imageCode", randomString);

		// 存放生成的验证码BufferedImage对象
		map.put("codePic", bufferedImage);
		return map;
	}
}

⑥ 改造ValidateCodeController

(1)加入SecurityProperties配置类

	/**
	 * 使用配置文件里的验证码参数配置
	 */
	@Autowired
	private SecurityProperties securityProperties;

(2)将读入的配置传到ImageCodeUtil的 generateCodeAndPic(int width,int height,int DEFAULT_CODE_LENGTH)中。

		int width = securityProperties.getCode().getImage().getWidth();
		int height = securityProperties.getCode().getImage().getHeight();
		int codeLength = securityProperties.getCode().getImage().getDEFAULT_CODE_LENGTH();

		//1、生成验证码
		Map<String, Object> codeMap = ImageCodeUtil.getInstance().generateCodeAndPic(width,height,codeLength);
ValidateCodeController完整代码:
package security.browser.controller;

import lombok.extern.slf4j.Slf4j;
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 security.core.properties.SecurityProperties;
import security.core.validateCode.ImageCodeUtil;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.RenderedImage;
import java.util.Map;

/**
 * 生成校验码的请求处理器
 * 
 * @author zhailiang
 *
 */
@RestController
@Slf4j
public class ValidateCodeController {

	/**
	 * 图形验证码的key
	 */
	private static final String SESSION_KEY = "SESSION_IMAGE_CODE_KEY";

	/**
	 * social工具,用于存储Session信息
	 */
	private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

	/**
	 * 使用配置文件里的验证码参数配置
	 */
	@Autowired
	private SecurityProperties securityProperties;

	/**
	 * 提供图片验证码
	 * @param request
	 * @param response
	 * @throws Exception
	 */
	@GetMapping("/code/image")
	public void createImageCode(HttpServletRequest request, HttpServletResponse response) throws Exception{
		int width = securityProperties.getCode().getImage().getWidth();
		int height = securityProperties.getCode().getImage().getHeight();
		int codeLength = securityProperties.getCode().getImage().getDEFAULT_CODE_LENGTH();

		//1、生成验证码
		Map<String, Object> codeMap = ImageCodeUtil.getInstance().generateCodeAndPic(width,height,codeLength);

		log.info("验证码:"+codeMap.get("imageCode"));
		//2、存入到Session中
		sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,codeMap.get("imageCode"));
		//3、写入到响应中(response)
		ImageIO.write((RenderedImage) codeMap.get("codePic"),"JPEG",response.getOutputStream());
	}

}

(2)验证码拦截的接口可配置

即拦截图形验证码的过滤器中表单提交的url地址可配置

① 在 ImageCodeProperties 中加入 url 的参数

    /**
     * 验证码拦截的接口可配置,用于对逗号隔开的url进行拦截
     */
    private String url;

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

② 在 application-browser.properties配置文件中加入配置 url 集合

#验证码拦截的接口可配置(用于对图形验证码的校验,即遇到该url才进行图形验证码校验)
imooc.security.code.image.url = /user,/user/*

③ 对 ValidateCodeFilter 进行改造

(1)再加上实现 InitializingBean 主要是使用  InitializingBean 类中的 afterPropertiesSet() 方法

这里对InitializingBean 增加一些个人的理解,实现 InitializingBean 的目的是使用 afterPropertiesSet()在 ValidateCodeFilter 类初始化的时候就进行 url 的添加此处使用并没有特别其他的含义。

(2)加入一些相关属性

    /**
     * 用于存储需要拦截的url地址集合
     */
    private Set<String> urls = new HashSet<>();

    /**
     * 使用配置文件里配置的拦截url地址
     */
    private SecurityProperties securityProperties;

    /**
     * 用于对url地址进行匹配判断
     */
    private AntPathMatcher antPathMatcher = new AntPathMatcher();



    public SecurityProperties getSecurityProperties() {
        return securityProperties;
    }

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

(3)afterPropertiesSet() 方法中的处理逻辑

    /**
     * 用户将配置文件中配置的url集合遍历后放到urls集合中
     * @throws ServletException
     */
    @Override
    public void afterPropertiesSet() throws ServletException {
        super.afterPropertiesSet();
        String[] configUrls = StringUtils.split(securityProperties.getCode().getImage().getUrl(),",");
        for(String configUrl : configUrls){
            urls.add(configUrl);
        }
        //把提交表单的登录请求也加到url集合中
        urls.add("/authentication/form");
    }

(4)修改 doFilterInternal() 方法

    /**
     * 对图形验证码进行校验
     * @param request
     * @param response
     * @param filterChain
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        //如果提交的请求url是/authentication/form,且是POST请求
//        if(StringUtils.equals("/authentication/form",request.getRequestURI())
//                && StringUtils.equalsIgnoreCase(request.getMethod(),"post")){

        //用于对urls集合中所有的url进行遍历判断
        boolean action = false;
        for(String url : urls){
            //如果请求中的url和我们配置拦截的url一致,action = true;
           if(antPathMatcher.match(url,request.getRequestURI())){
               action = true;
           }
        }

        if(action){
            try {
                //从Session中获取参数,需要以ServletWebRequest为参数
                validate(new ServletWebRequest(request));
            }catch (ValidateCodeException exception){
                //如果捕获异常就使用authenticationFailureHandler把错误信息返回回去
                authenticationFailureHandler.onAuthenticationFailure(request,response,exception);
                return;//如果抛出异常了就不再继续走下面的过滤器了
            }
        }
        //校验完图形验证码后调用下一个过滤器
        filterChain.doFilter(request,response);
    }

完整代码:

package security.core.validateCode;


import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;
import security.core.properties.SecurityProperties;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

/**
 * 在处理请求之前过滤,且只过滤一次
 * 实现InitializingBean的目的是为了在其他参数都组装完毕后,再初始化urls的值
 */
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {

    /**
     * 登陆失败的处理器
     */
    private AuthenticationFailureHandler authenticationFailureHandler;

    /**
     * 图形验证码的key
     */
    private static final String SESSION_IMAGE_CODE_KEY = "SESSION_IMAGE_CODE_KEY";

    /**
     * 存储了Session信息
     */
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    /**
     * 用于存储需要拦截的url地址集合
     */
    private Set<String> urls = new HashSet<>();

    /**
     * 使用配置文件里配置的拦截url地址
     */
    private SecurityProperties securityProperties;

    /**
     * 用于对url地址进行匹配判断
     */
    private AntPathMatcher antPathMatcher = new AntPathMatcher();


    /**
     * 用户将配置文件中配置的url集合遍历后放到urls集合中
     * @throws ServletException
     */
    @Override
    public void afterPropertiesSet() throws ServletException {
        super.afterPropertiesSet();
        String[] configUrls = StringUtils.split(securityProperties.getCode().getImage().getUrl(),",");
        for(String configUrl : configUrls){
            urls.add(configUrl);
        }
        //把提交表单的登录请求也加到url集合中
        urls.add("/authentication/form");
    }

    /**
     * 对图形验证码进行校验
     * @param request
     * @param response
     * @param filterChain
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        //如果提交的请求url是/authentication/form,且是POST请求
//        if(StringUtils.equals("/authentication/form",request.getRequestURI())
//                && StringUtils.equalsIgnoreCase(request.getMethod(),"post")){

        //用于对urls集合中所有的url进行遍历判断
        boolean action = false;
        for(String url : urls){
            //如果请求中的url和我们配置拦截的url一致,action = true;
           if(antPathMatcher.match(url,request.getRequestURI())){
               action = true;
           }
        }

        if(action){
            try {
                //从Session中获取参数,需要以ServletWebRequest为参数
                validate(new ServletWebRequest(request));
            }catch (ValidateCodeException exception){
                //如果捕获异常就使用authenticationFailureHandler把错误信息返回回去
                authenticationFailureHandler.onAuthenticationFailure(request,response,exception);
                return;//如果抛出异常了就不再继续走下面的过滤器了
            }
        }
        //校验完图形验证码后调用下一个过滤器
        filterChain.doFilter(request,response);
    }

    /**
     * 图形验证码校验的具体方法
     * @param request
     */
    public void validate(ServletWebRequest request) throws ServletException{
        //Session中取出,即后台存储的验证码
        String sessionImageCode = (String)sessionStrategy.getAttribute(request,SESSION_IMAGE_CODE_KEY);

        //从请求中取出
        String requestImageCode = ServletRequestUtils.getStringParameter(request.getRequest(),"imageCode");
        if(StringUtils.isBlank(requestImageCode)){
            throw new ValidateCodeException("验证码不能为空");
        }
        if(StringUtils.isBlank(sessionImageCode)){
            throw new ValidateCodeException("验证码不存在");
        }
        if(!StringUtils.equalsIgnoreCase(sessionImageCode,requestImageCode)){
            throw new ValidateCodeException("验证码不匹配");
        }

        //如果没有出现以上的异常则验证完后删除session中存储的验证码
        sessionStrategy.removeAttribute(request,SESSION_IMAGE_CODE_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;
    }
}

(5)给 BrowserSecurityConfigconfigure(HttpSecurity http) 方法中的 ValidateCodeFilter 对象增加设置

        //加入图片验证码的前置校验过滤器
        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        validateCodeFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);
        //设置可配置的拦截url
        validateCodeFilter.setSecurityProperties(securityProperties);
        validateCodeFilter.afterPropertiesSet();
(3)验证码的生成逻辑可配置

① 新建一个 ValidateCodeGenerator 接口类,里面添加生成验证码的抽象方法

package security.core.validateCode;

import java.util.Map;

/**
 * 负责生成验证码的接口
 */
public interface ValidateCodeGenerator {

    /**
     * 生成图形验证码的接口方法
     * @param width
     * @param height
     * @param DEFAULT_CODE_LENGTH
     * @return
     */
    Map<String, Object> generateCodeAndPic(int width,int height,int DEFAULT_CODE_LENGTH);

}

②新建一个 ImageCodeGenerator 来实现 ValidateCodeGenerator 接口里的生成图形验证码的方法

package security.core.validateCode;


import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * 负责生成图形验证码的实现类
 */
public class ImageCodeGenerator implements ValidateCodeGenerator{

    /**
     * 生成图形验证码的方法
     * @param width
     * @param height
     * @param DEFAULT_CODE_LENGTH
     * @return
     */
    @Override
    public Map<String, Object> generateCodeAndPic(int width, int height, int DEFAULT_CODE_LENGTH) {
        Map<String, Object> codeMap = ImageCodeUtil.getInstance().generateCodeAndPic(width,height,DEFAULT_CODE_LENGTH);
        return codeMap;
    }
}

③ 在 BrowserSecurityConfig 加入ValidateCodeGenerator 接口的 Bean

    @Bean
    public ValidateCodeGenerator imageCodeGenerator(){
        return new ImageCodeGenerator();
    }

④ 修改 ValidateCodeController 中的代码

(1)加入ValidateCodeGenerator

	@Autowired
	private ValidateCodeGenerator imageCodeGenerator;

(2)修改生成验证码的接口方法

		//1、生成验证码
//		Map<String, Object> codeMap = ImageCodeUtil.getInstance().generateCodeAndPic(width,height,codeLength);
		//动态配置的验证码生成逻辑
		Map<String, Object> codeMap = imageCodeGenerator.generateCodeAndPic(width,height,codeLength);

完整的代码:

package security.browser.controller;

import lombok.extern.slf4j.Slf4j;
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 security.core.properties.SecurityProperties;
import security.core.validateCode.ImageCodeUtil;
import security.core.validateCode.ValidateCodeGenerator;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.RenderedImage;
import java.util.Map;

/**
 * 生成校验码的请求处理器
 * 
 * @author zhailiang
 *
 */
@RestController
@Slf4j
public class ValidateCodeController {

	/**
	 * 图形验证码的key
	 */
	private static final String SESSION_KEY = "SESSION_IMAGE_CODE_KEY";

	/**
	 * social工具,用于存储Session信息
	 */
	private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

	/**
	 * 使用配置文件里的验证码参数配置
	 */
	@Autowired
	private SecurityProperties securityProperties;

	@Autowired
	private ValidateCodeGenerator imageCodeGenerator;

	/**
	 * 提供图片验证码
	 * @param request
	 * @param response
	 * @throws Exception
	 */
	@GetMapping("/code/image")
	public void createImageCode(HttpServletRequest request, HttpServletResponse response) throws Exception{
		int width = securityProperties.getCode().getImage().getWidth();
		int height = securityProperties.getCode().getImage().getHeight();
		int codeLength = securityProperties.getCode().getImage().getDEFAULT_CODE_LENGTH();

		//1、生成验证码
//		Map<String, Object> codeMap = ImageCodeUtil.getInstance().generateCodeAndPic(width,height,codeLength);
		//动态配置的验证码生成逻辑
		Map<String, Object> codeMap = imageCodeGenerator.generateCodeAndPic(width,height,codeLength);


		log.info("验证码:"+codeMap.get("imageCode"));
		//2、存入到Session中
		sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,codeMap.get("imageCode"));
		//3、写入到响应中(response)
		ImageIO.write((RenderedImage) codeMap.get("codePic"),"JPEG",response.getOutputStream());
	}

}

七、SpringSecurity的记住我功能

1、记住我功能的基本原理

t

(1)、表单请求提交后经过UsernamePasswordAuthenticationFilter后,如果验证通过(即验证成功),会去调用RemeberMeService服务,在RemeberMeService类里面有一个TokenRepository()方法。TokenRepository()方法会生成一个token,将这个token存入到浏览器的Cookie中,同时TokenRepository()方法还会将这个Token写入到数据库中,因为记住我功能是在通过UsernamePasswordAuthenticationFilter认证成功之后调用的RemeberMeService服务,所以在存入数据库的时候会将用户名和token存入进去。

(2)、当下次同一个用户再次访问系统的时候,如果系统配置了记住我功能,访问请求会先经过RememberMeAuthenticationFilter过滤器,这个过滤器会去读取cookie中的token,然后交给RemeberMeServiceRemeberMeService会用TokenRepository()方法到数据库中去查询这个token在数据库中有没有记录,如果有记录会将username取出来,取出来之后再调用UserDetailsService去获取用户信息,然后将用户信息存入到SecurityContext中去,这样就实现了记住我的功能。

(3)、RemeberMeService的过滤器所处的过滤器链位置

2

2、记住我功能的具体实现

(1)、signIn.html登录页面添加记住我复选框

注意:name只能设置为:remember-me

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

signIn.html页面完整代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
    <h3>表单登录</h3>
<!--    <form action="/security-browser/authentication/form" method="post">-->
    <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"><input name="remember-me" type="checkbox" value="true"/>记住我</td>
            </tr>
            
            <tr>
                <td colspan="2"><button type="submit">登录</button></td>
            </tr>
        </table>
    </form>

</body>
</html>
(2)在配置文件中配置使用的数据库,我是配置在了application-browser.properties配置文件中

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springsecurity?useUnicode=true&characterEncoding=utf8&useSSL=false&useTimezone=true&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root

(3)在BrowserSecurityConfig配置类中配置PersistentTokenRepository
    @Autowired
    private DataSource dataSource;

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
//        tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }

①、在PersistentTokenRepository类的对象中注入dataSource配置(数据库配置);

②、开启自动新建存储token和用户名的数据表,**tokenRepository.setCreateTableOnStartup(true);**需要注意的是这个方法只负责创建数据表,如果数据库里已经有相应的数据表了,再在项目中开启自动建表功能会报错。

或者手动在数据库建好相应的数据表,建表语句在JdbcTokenRepositoryImpl类中。

(4)在BrowserProperties类中设置一个默认的记住我的时间,单位是秒(s),这个也是可以在application-browser.properties配置文件中去配置的。默认我写了360秒(即一小时)

    /**
     * 记住我
     */
    private int rememberMeSecond = 3600;


    public int getRememberMeSecond() {
        return rememberMeSecond;
    }

    public void setRememberMeSecond(int rememberMeSecond) {
        this.rememberMeSecond = rememberMeSecond;
    }

(5)在BrowserSecurityConfig配置类中的configure(HttpSecurity http)中配置记住我

 http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
                    .loginPage("/authentication/require")
                    .loginProcessingUrl("/authentication/form")
                    .successHandler(tinnerAuthentivationSuccessHandler)
                    .failureHandler(tinnerAuthentivationFailureHandler)
//记住我功能
                .and()
                    .rememberMe()
                    .tokenRepository(persistentTokenRepository())
                    .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSecond())
                    .userDetailsService(userDetailsService)

3、记住我功能SpringSecurity源码解析(略)

最后

很多程序员,整天沉浸在业务代码的 CRUD 中,业务中没有大量数据做并发,缺少实战经验,对并发仅仅停留在了解,做不到精通,所以总是与大厂擦肩而过。

我把私藏的这套并发体系的笔记和思维脑图分享出来,理论知识与项目实战的结合,我觉得只要你肯花时间用心学完这些,一定可以快速掌握并发编程。

不管是查缺补漏还是深度学习都能有非常不错的成效,需要的话记得帮忙点个赞支持一下

整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 26
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值