先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Golang全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
如果你需要这些资料,可以添加V获取:vip1024b (备注go)
正文
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页面
(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)给 BrowserSecurityConfig 的 configure(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)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事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)给 BrowserSecurityConfig 的 configure(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行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!