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

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Golang全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注go)
img

正文

log.info(“密码:”+password);
//这个User类实现了UserDetails
//密码应该是数据库查询出的密码
//authorities:用户权限的集合,即用来给用户授权
User user = new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList(“admin”));

return user;
}

2、处理用户校验逻辑(UserDetails)

(1)密码是否匹配校验:是由SpringSecurity来做,我们只需要取出用户的密码给SpringSecurity,它会拿后台数据库的密码和前端输入的密码进行校验。
(2)其他校验:用户是否冻结、密码是否过期等等。

①UserDetails详解:

package org.springframework.security.core.userdetails;

import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;

public interface UserDetails extends Serializable {

//用户的权限信息
Collection<? extends GrantedAuthority> getAuthorities();

//用户的密码
String getPassword();

//用户的用户名
String getUsername();

//账户是否过期(true=没有过期,false=已过期)
boolean isAccountNonExpired();

//账户是否锁定(true=没有锁定,false=已锁定)
boolean isAccountNonLocked();

//密码是否过期(true=没有过期,false=已过期)
boolean isCredentialsNonExpired();

//账户是否可用(true=可用,false=不可用),即用户是否被删除了
boolean isEnabled();
}

3、处理密码的加密和解密(PasswordEncoder)

四、SpringSecurity开发基于表单的认证(4-4-1内容)

1、自定义登录页面

(1)在继承了WebSecurityConfigurerAdapter类的子类的configure(HttpSecurity http)方法里的http.formLogin()方法后面加loginPage()方法和.loginProcessingUrl()还有antMatchers(“/signIn.html”).permitAll()。

http.formLogin()//开启表单登录(即对表单登录进行身份认证)
.loginPage(“/signIn.html”)//指定登录页面
.loginProcessingUrl(“/authentication/form”)//让UsernamePasswordAuthenticationFilter能够处理提交的登录请求
// http.httpBasic()//开启SpringSecurity原生的表单登录
.and()
.authorizeRequests()//对请求进行授权(即登录后需要授权)
.antMatchers(“/signIn.html”).permitAll()//允许signIn.html请求进来,不进行拦截
.anyRequest()//对任何请求
.authenticated();//开启认证

(2)在resources文件夹下新建templates文件夹,然后在templates文件夹里新增signIn.html登录页面。
登录

表单登录

用户名:
密码:
登录
(3)新建一个负责页面跳转的Controller类,增加对登录页面跳转的方法。

package security.browser.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class PageController {

@GetMapping(“/signIn.html”)
public String loginPage(){
return “signIn.html”;
}

}

(4)测试:如果测试登录时能够跳转登录页面,但是提交登录却没有成功,可能是没有对跨域进行处理导致的。

解决方法:在继承了WebSecurityConfigurerAdapter类的子类的configure(HttpSecurity http)方法里的.authenticated()方法后面加上对跨域的处理即可。

http.formLogin()//开启表单登录(即对表单登录进行身份认证)
.loginPage(“/signIn.html”)//指定登录页面
.loginProcessingUrl(“/authentication/form”)//处理提交登录的请求
// http.httpBasic()//开启SpringSecurity原生的表单登录
.and()
.authorizeRequests()//对请求进行授权(即登录后需要授权)
.antMatchers(“/signIn.html”).permitAll()//允许signIn.html请求进来,不进行拦截
.anyRequest()//对任何请求
.authenticated()//开启认证
.and()
.csrf() //跨域请求伪造
.disable();//关闭

2、自定义登陆成功处理

(1)处理流程示意图

无

SpringSecurity会首先来处理请求,如果请求需要身份认证,SpringSecurity会让请求跳转到认证页面(登录页面),但是在跳转之前SpringSecurity会用HttpSessionRequestCache类把请求的信息缓存到session中,这样在认证页面就能获取到原始的请求信息。

(2)对原来的登录跳转及拦截进行修改

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 security.core.properties.SecurityProperties;

/**

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

@Autowired
private SecurityProperties securityProperties;

/**
*

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

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

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

}

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

}

(3)新建一个处理登录跳转的RestController

package security.browser.controller;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
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.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import security.core.properties.SecurityProperties;
import security.core.support.SimpleResponse;

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

@RestController
@Slf4j
public class BrowserSecurityController {

@Autowired
private SecurityProperties securityProperties;

@GetMapping(“/properties”)
public String properties(){
String properties = securityProperties.getBrowser().getLoginPage();
System.out.println(“properties:” + properties);
return properties;
}

/**

  • 用来获取请求缓存里的信息,后续用来判断是浏览器端请求还是APP端的请求
    */
    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页面中加入

图形验证码:

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

完整的signIn.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 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 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 中的代码

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Go)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
删除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 中的代码

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Go)
[外链图片转存中…(img-VSpD8j5S-1713162426140)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值