📑本篇内容:SpringSecurity的用户认证扩展配置——囊括登陆成功或失败处理器的相关配置原理及示例——从0到1教会方法自行学习
📘 文章专栏:前后端分离项目(Vue + SpringBoot)
🎬最近更新:2022年2月18日 如何理解SpringSecurity的用户登录所需信息以及配置原理——源码学习——从0到1教会方法自行学习
🙊个人简介:一只二本院校在读的大三程序猿,本着注重基础,打卡算法,分享技术作为个人的经验总结性的博文博主,虽然可能有时会犯懒,但是还是会坚持下去的,如果你很喜欢博文的话,建议看下面一行~(疯狂暗示QwQ)
🌇点赞 👍 收藏 ⭐留言 📝 一键三连 关爱程序猿,从你我做起
📖本文目录
📝SpringSecurity的用户认证扩展配置
⭐Security中的身份认证成功与失败之后工作流程⭐
在表单提交信息时,参与认证过后,Security帮我们做了些什么呢?
为什么信息在登录成功之后跳转到我们之前访问的受限资源?
为什么信息在登录失败之后会出现对应的错误信息重定向回我们的登录页面呢?
小付一开始也是带着这三个疑问去学习的,希望能对各位有用~
先来解决第一个问题——Security在认证结束之后,他干了什么?
还不了解Security默认用户登录信息生成的缘由,以及Security自动配置原理的uu们请移步至小付写的这两篇文章。
既然有了上述基础,后面学习就简单容易了~
读过Security自动配置类应该知道,Security默认会将 defaultSecurityFilterChain 注入到Spring容器当中,对应开启默认的相关配置。
看其源码SpringBootWebSecurityConfiguration.java:
@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
return http.build();
}
之前小付提到过这个封装的HttpSecurity中有很多配置,这才是对我们自定义配置的前提基础。
对于用户登录所需信息以及配置与原理中
我们都可以知道,当用户登录的时候肯定需要提交表单,进行对应的身份认证,对应在该http中对应的就是formLogin()
,究其根本这玩意是在 AbstractAuthenticationProcessingFilter这个抽象类中 调用了doFilter()方法 而我们实际运行的是在继承了这个抽象方法的 UsernamePasswordAuthenticationFilter 执行了其中的 attemptAuthentication()方法, 至于为什么上面的第二篇文章讲过了哦
~
这样一来咱们就正儿八经的进入了对应的认证环节了
~
下面小付用简单的话语总结认证部分,咱们快速回忆认证部分,进入到认证部分后的工作:
总结:UsernamePasswordAuthenticationFilter 中的 attemptAuthentication()方法 会去获取认证管理器进行认证操作
—— 通过调用接口AuthenticationManager中的 authenticate()方法区进行认证,但实际我们操作
的是 ProviderManager这个接口实现类去帮助我们获取 AuthenticationProvider来进行认证,默认当中只有一个DaoAuthenticationProvider的接口实现类,故交给了这个类去执行校验检测认证等相关工作。
我们可以DEBUG追踪其运行轨迹~
我们在UsernamePasswordAuthenticationFilter 中的 attemptAuthentication()方法开始进行断点测试~
这一块我们之前都是理解过得就快速到达通过DaoAuthenticationProvider进行认证之后的工作流程就好了~
如上图所示我们通过ProviderManager这个类调用到了DaoAuthenticationProvider这个唯一接口实现类去默认实现认证管理。
当认证获取信息之后,认证之前会对其进行一系列的检查
//用于认证之前先检查该用户是否被锁定、 用户是否被禁用、以及用户账户是否过期
this.preAuthenticationChecks.check(user);
//对身份认证类型校验核对密码 如果当前用户输入的密码错误则会抛出BadCredentialsException这个异常
//这个方法是先去尝试获取密码,然后对该密码通过编码匹配是否一致,如果不一直才会报错,反之继续运行
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
//该方法用于认证当前用户密码是否过期的
this.postAuthenticationChecks.check(user);
做完上述相关认证之后会将相应的用户数据置于缓存当中 这里默认的会放置在NullUserCache中实际上这个也是个空缓存类摆设用的~
成功之后我们就会到达最后这一步骤了~
return this.createSuccessAuthentication(principalToReturn, authentication, user);
创建一个成功认证的返回值这是调用了DaoAuthenticationProvider中的createSuccessAuthentication()方法实现的~
随后经过漫长的参数返回以及一系列内部认证,成功认证的事件分发之后,我们最终回来到 AbstractAuthenticationProcessingFilter类中的successfulAuthentication()方法
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
SecurityContextHolder.setContext(context);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
最后这里:
this.successHandler.onAuthenticationSuccess(request, response, authResult);
实际又是调用 实现了AuthenticationSuccessHandler接口的 继承了SimpleUrlAuthenticationSuccessHandler类的SavedRequestAwareAuthenticationSuccessHandler类。
结构如图所示:
这也就是为什么登陆成功之后会重定向访问到我们的受限资源了~同时这也就是为什么我们可以通过实现 AuthenticationSuccessHandler接口中的onAuthenticationSuccess()方法来实现咱们的自定义登录成功后的返回JSON数据实现认证登录的工作原理了~
那么用户身份认证登录失败的工作流程就靠自己去理解了,其实按按照DEBUG的顺序就可以轻松理解工作流程。
⭐示例——自定义登录成功与失败流程配置⭐
通过前面的学习我们知道如果想要接管并且扩展Security的相关配置就需要创建一个配置类去继承WebSecurityConfigurerAdapter并且将该类注入到Spring容器当中。
这里我们看到源码中的相关注释来理解
/**
* The default configuration for web security. It relies on Spring Security's
* content-negotiation strategy to determine what sort of authentication to use. If the
* user specifies their own {@link WebSecurityConfigurerAdapter} or
* {@link SecurityFilterChain} bean, this will back-off completely and the users should
* specify all the bits that they want to configure as part of the custom security
* configuration.
*
* @author Madhura Bhave
*/
上面注释简单理解就是 如果我们用户需要客制化自定义我们的WebSecurityConfigurerAdapter或者SecurityFilterChain组件时,我们应当在客制化Security中重新配置所有想要的配置。
既然它都这么说了,那我们就按照她说的来进行配置就好啦,切记是她哦~
com.alascanfu.config.WebSecurityConfigurer.java
配置之前我们需要大致了解一下我们需要在configure中配置些什么有关于认证登录成功与失败的?哪些最常用~
我们从源码入手逐一添加:
// 这是源码中默认的 认证 和 授权 的配置。
// 注意这里是23种设计模式种的建造者模式 但又有些许不同 读源码就知道了
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()// 请求授权
.anyRequest().authenticated()//所有请求都需要进行认证
.and()// 返还HttpSecurityBuilder继续建造。
.formLogin()// 字如其意 表单登录
.and()
.httpBasic();// 或者httpBasic认证
}
步骤1:第一步扩展 针对于我们不想添加保护的一些静态资源 我们是无需认证就可以直接访问
例如:我们想要对我们的博客首页无需登录直接访问的设置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/index").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable();
}
这样一来针对于我们的/index访问请求,就会被直接放行无需认证,任何访问该请求的人可以直接看到我们的/index下的资源。
注意一点:就是针对于放行的资源务必写在.anyRequest().authenticated()之前
,否则服务器会报错,原因点是命名你都设置了当前全局请求都要认证,但又多出来一个请求路径,计算机就会认为发生了冲突就不行了
步骤2:第二步扩展 针对于我们的表单认证,通常情况都是自定义表单而非默认表单,同时如果表单登录成功/失败我们想做出自定义的相关配置,能否自定义成功/失败相关的自定义配置,比如说当我们登录失败我们只需要返回一个包含了错误信息的JSON数据统一格式进行返回,或者登陆成功就直接将成功的数据返回并且加载对应成功后的数据界面。
在第一部分的时候小付阐述了一下Security中的身份认证成功与失败之后的工作流程,作为前后端分离项目当中,我们更加侧重自定义相关配置来实现我们的数据传输
,正如第一部分如果登陆身份认证成功我们只需要创建一个对应的实现了AuthenticationSuccessHandler
的配置类并且使用就可以了,那么登录失败的相关认证我们又可以使用
创建一个对应实现了 AuthenticationFailureHandler
的配置类添加到Configure中使用就好
啦~
form表单中常用的Security配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginProcessingUrl("/doLogin") // 指定登录中转的地址也就是修改DEFAULT_ANT_PATH_REQUEST_MATCHER的默认请求中转拦截的地址的属性
.successHandler(successAuthenticateHandler)// 指定成功之后的自定义处理
.failureHandler(failAuthenticateHandler)// 指定失败后的自定义处理器
.usernameParameter("username")// 指定接收用户名的数据参数名
.passwordParameter("password")// 指定接收密码的数据参数名
.and()
.csrf().disable();// 关闭跨域请求访问攻击防护
}
⭐SuccessAuthenticateHandler
com.alascanfu.handler.security.SuccessAuthenticateHandler.java
/***
* @author: Alascanfu
* @date : Created in 2022/2/18 15:59
* @description: SuccessAuthenticateHandler配置
* @modified By: Alascanfu
**/
@Configuration
public class SuccessAuthenticateHandler implements AuthenticationSuccessHandler {
// 用于转化为JSON数据进行传输
@Autowired
ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 设置统一字符编码响应
response.setContentType("application/json;charset=utf-8");
// 获取认证成功的用户数据 这里用到了统一格式数据返回 ResultData是统一格式返回的数据,其中包含了状态码,状态信息,以及数据信息
String returnJson = objectMapper.writeValueAsString(ResultData.success(authentication.getPrincipal()));
// 通过响应将我们想要的JSON数据返回给浏览器
response.getWriter().println(returnJson);
}
}
这里推荐去看缥缈Jam的何其美化统一数据格式返回
这里面的ReturnCode是一个枚举状态类,包含了状态码以及状态信息~这里就不再赘述了。
/***
* @author: 缥缈Jam Alascanfu
* @date : Created in 2022/2/10 16:37
* @description: 用于统一返回数据格式
* @modified By: Alascanfu
**/
@Data
public class ResultData<T> {
/** 状态码 */
private int status ;
/** 返回的操作信息 */
private String message ;
/** 返回的数据信息 */
private T data ;
/** 记录当前操作时间 */
private long timestamp ;
public ResultData() {
this.timestamp = System.currentTimeMillis();
}
/**统一结果返回 成功时 */
public static <T> ResultData<T> success(T data){
ResultData<T> resultData = new ResultData<>();
resultData.setStatus(ReturnCode.RC200.getCode());
resultData.setMessage(ReturnCode.RC200.getMessage());
resultData.setData(data);
return resultData;
}
/**统一结果返回 失败时 */
public static <T> ResultData<T> fail(int code,String message){
ResultData<T> resultData = new ResultData<>();
resultData.setStatus(code);
resultData.setMessage(message);
return resultData;
}
}
如图所示:我们就基本完成了一个最简单的登录认证请求的访问了,至于为什么password不会传输回来,学过信息安全技术的人员都知道JSON数据如果被拦截了那是不是用户的个人隐私就被泄露了,所以数据传输回来的时候我们是不会将重要数据存放在JSON中一并传输回来的
,这也就从根源上避免
了用户可能出现安全的相关问题。
⭐AuthenticationFailureHandler
有了上述的相关配置,那就是举一反三配置AuthenticationFailureHandler配置类了
com.alascanfu.handler.FailAuthenticateHandler
/***
* @author: Alascanfu
* @date : Created in 2022/2/18 16:02
* @description: FailAuthenticateHandler配置
* @modified By: Alascanfu
**/
@Configuration
public class FailAuthenticateHandler implements AuthenticationFailureHandler {
// 用于返回JSON数据
@Autowired
public ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
ResultData resultData = null;
// 这里是针对Security的不同类型的不同返回状态码 以及对应的 状态返回信息
if (exception instanceof BadCredentialsException){
resultData = ResultData.fail(ReturnCode.USERNAME_OR_PASSWORD.getCode(),ReturnCode.USERNAME_OR_PASSWORD.getMessage());
}else if (exception instanceof LockedException){
resultData = ResultData.fail(ReturnCode.LOCKED_USERNAME.getCode(), ReturnCode.LOCKED_USERNAME.getMessage());
}else if (exception instanceof CredentialsExpiredException){
resultData = ResultData.fail(ReturnCode.PASSWORD_EXPIRED.getCode(), ReturnCode.PASSWORD_EXPIRED.getMessage());
}else if (exception instanceof AccountExpiredException){
resultData = ResultData.fail(ReturnCode.ACCOUNT_EXPIRED.getCode(), ReturnCode.ACCOUNT_EXPIRED.getMessage());
}else if (exception instanceof DisabledException){
resultData = ResultData.fail(ReturnCode.DISABLED_ACCOUNT.getCode(),ReturnCode.DISABLED_ACCOUNT.getMessage());
}else {
resultData = ResultData.fail(ReturnCode.CLIENT_AUTHENTICATION_FAILED.getCode(),ReturnCode.CLIENT_AUTHENTICATION_FAILED.getMessage());
}
response.getWriter().println(objectMapper.writeValueAsString(resultData));
}
}
ReturnCode
/***
* @author: Alascanfu
* @date : Created in 2022/2/10 17:09
* @description: 状态码枚举类
* @modified By: Alascanfu
**/
public enum ReturnCode {
/**操作成功**/
RC200(200,"操作成功"),
/**操作失败**/
RC999(999,"操作失败"),
/**服务限流**/
RC100(100,"服务器开启限流保护,请稍后再试!"),
/**服务降级**/
RC201(201,"服务器开启降级保护,请稍后再试!"),
/**热点参数限流**/
RC202(202,"热点参数限流,请稍后再试!"),
/**系统规则不满足要求**/
RC203(203,"系统规则不满足要求,请稍后再试!"),
/**授权规则不通过**/
RC204(204,"授权规则不通过,请稍后再试!"),
/**access_denied**/
RC403(403,"无访问权限,请联系管理员授予权限"),
/**Not,Found~**/
RC404(404,"没有找到您所要访问的页面,请重试"),
/**access_denied**/
RC401(401,"匿名用户访问无权限资源时的异常"),
/**服务器异常**/
RC500(500,"系统异常,请稍后重试~"),
/**访问令牌不合法**/
INVALID_TOKEN(2001,"访问令牌不合法"),
/**访问令牌不合法**/
ACCESS_DENIED(2003,"没有权限访问该资源"),
/**客户端认证出错**/
CLIENT_AUTHENTICATION_FAILED(401,"客户端认证失败"),
/**用户名或者密码错误**/
USERNAME_OR_PASSWORD(401,"用户名或者密码错误"),
/**不支持的用户认证模式**/
UNSUPPORTED_GRANT_TYPE(401,"不支持的认证模式"),
/**Security认证中用户被锁定*/
LOCKED_USERNAME(401,"账户已经被锁定,请联系工作人员~"),
/**Security认证中用户密码过期*/
PASSWORD_EXPIRED(401,"账户密码过期,请联系工作人员~"),
/**Security认证中用户账号过期*/
ACCOUNT_EXPIRED(401,"用户账户过期,请联系工作人员~"),
/**Security认证中用户账号封禁*/
DISABLED_ACCOUNT(401,"用户已被封禁,请联系工作人员~")
;
/** 自定义状态码 **/
private final int code;
/**自定义描述**/
private final String message;
ReturnCode(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode(){
return code ;
}
public String getMessage (){
return message;
}
}
随后我们就可以进行测试了~
如上图所示:此时我们也正确成功的配置好了有关于认证失败后的工作流程配置,也是我们所期待那样直接返回包含了错误的状态码和状态信息的JSON数据。
🙊总结
今天小付的项目已经配置好了相关于的安全认证信息了,这块的笔记也写的差不多,这里就慢慢更吧,毕竟一篇文章好几k字呢
,慢下来,就好了
,不用想那么多,一天一点点总能学完的~ 冲冲冲!! 今天是双周赛,明天是周赛,所以明天可能会更新下周赛的解题题解,就先酱紫吧,干饭~