解决Spring Security中AuthenticationEntryPoint不生效相关问题
作者:冲鸭hhh 发布时间:2022-11-29 06:53:09
之前由于项目需要比较详细地学习了Spring Security的相关知识,并打算实现一个较为通用的权限管理模块。由于项目是前后端分离的,所以当认证或授权失败后不应该使用formLogin()的重定向,而是返回一个json形式的对象来提示没有授权或认证。
这时,我们可以使用AuthenticationEntryPoint对认证失败异常提供处理入口,而通过AccessDeniedHandler对用户无授权异常提供处理入口
在这里我的代码如下
/**
* 对已认证用户无权限的处理
*/
@Component
public class JsonAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.setContentType("application/json;charset=utf-8");
// 提示无权限
httpServletResponse.getWriter().print(JSONObject.toJSONString(new BaseResult<String>(NO_PERMISSION, false, null)));
}
}
/**
* 对匿名用户无权限的处理
*/
@Component
public class JsonAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.setContentType("application/json;charset=utf-8");
// 认证失败
httpServletResponse.getWriter().print(JSONObject.toJSONString(new BaseResult<String>(e.getMessage(), false, null)));
}
}
在这样的设置下,如果认证失败的话会提示具体认证失败的原因;而用户进行无权限访问的时候会返回无权限的提示。
用不存在的用户名密码登录后会出现以下返回数据
与我所设置的认证异常返回值不一致。
在继续讲解前,我先简单说下我当前的Spring Security配置,我是将不同的登录方式整合在一起,并模仿Spring Security中的UsernamePasswordAuthenticationFilter实现了不同登录方式的过滤器。
设想通过邮件、短信、验证码和微信等登录方式登录(这里暂时只实现了验证码登录的模板)。
以下是配置信息
/**
* @Author chongyahhh
* 验证码登录配置
*/
@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class VerificationLoginConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private final VerificationAuthenticationProvider verificationAuthenticationProvider;
@Qualifier("tokenAuthenticationDetailsSource")
private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource;
@Override
public void configure(HttpSecurity http) throws Exception {
VerificationAuthenticationFilter verificationAuthenticationFilter = new VerificationAuthenticationFilter();
verificationAuthenticationFilter.setAuthenticationManager(http.getSharedObject((AuthenticationManager.class)));
http
.authenticationProvider(verificationAuthenticationProvider)
.addFilterAfter(verificationAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); // 将VerificationAuthenticationFilter加到UsernamePasswordAuthenticationFilter后面
}
}
/**
* @Author chongyahhh
* Spring Security 配置
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final AuthenticationEntryPoint jsonAuthenticationEntryPoint;
private final AccessDeniedHandler jsonAccessDeniedHandler;
private final VerificationLoginConfig verificationLoginConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.apply(verificationLoginConfig) // 用户名密码验证码登录配置导入
.and()
.exceptionHandling()
.authenticationEntryPoint(jsonAuthenticationEntryPoint) // 注册自定义认证异常入口
.accessDeniedHandler(jsonAccessDeniedHandler) // 注册自定义授权异常入口
.and()
.anonymous()
.and()
.formLogin()
.and()
.csrf().disable(); // 关闭 csrf,防止首次的 POST 请求被拦截
}
@Bean("customSecurityExpressionHandler")
public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler(){
DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
handler.setPermissionEvaluator(new CustomPermissionEvaluator());
return handler;
}
}
以下是实现的验证码登录过滤器
模仿UsernamePasswordAuthenticationFilter继承AbstractAuthenticationProcessingFilter实现。
/**
* @Author chongyahhh
* 验证码登录过滤器
*/
public class VerificationAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final String USERNAME = "username";
private static final String PASSWORD = "password";
private static final String VERIFICATION_CODE = "verificationCode";
private boolean postOnly = true;
public VerificationAuthenticationFilter() {
super(new AntPathRequestMatcher(SECURITY_VERIFICATION_CODE_LOGIN, "POST"));
// 继续执行 * 链,执行被拦截的 url 对应的接口
super.setContinueChainBeforeSuccessfulAuthentication(true);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String verificationCode = this.obtainVerificationCode(request);
System.out.println("验证中...");
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
username = (username == null) ? "" : username;
password = (password == null) ? "" : password;
username = username.trim();
VerificationAuthenticationToken authRequest = new VerificationAuthenticationToken(username, password);
//this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
private String obtainPassword(HttpServletRequest request) {
return request.getParameter(PASSWORD);
}
private String obtainUsername(HttpServletRequest request) {
return request.getParameter(USERNAME);
}
private String obtainVerificationCode(HttpServletRequest request) {
return request.getParameter(VERIFICATION_CODE);
}
private void setDetails(HttpServletRequest request, VerificationAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
private boolean validate(String verificationCode) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
HttpSession session = request.getSession();
Object validateCode = session.getAttribute(VERIFICATION_CODE);
if(validateCode == null) {
return false;
}
// 不分区大小写
return StringUtils.equalsIgnoreCase((String)validateCode, verificationCode);
}
}
其它的设置与本问题无关,就先不放出来了。
首先我们要知道,AuthenticationEntryPoint和AccessDeniedHandler是过滤器ExceptionTranslationFilter中的一部分,当ExceptionTranslationFilter捕获到之后过滤器的执行异常后,会调用AuthenticationEntryPoint和AccessDeniedHandler中的对应方法来进行异常处理。
以下是对应的源码
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {
if (exception instanceof AuthenticationException) { // 认证异常
...
sendStartAuthentication(request, response, chain,
(AuthenticationException) exception); // 在这里调用 AuthenticationEntryPoint 的 commence 方法
} else if (exception instanceof AccessDeniedException) { // 无权限
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
...
sendStartAuthentication(
request,
response,
chain,
new InsufficientAuthenticationException(
messages.getMessage(
"ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource"))); // 在这里调用 AuthenticationEntryPoint 的 commence 方法
} else {
...
accessDeniedHandler.handle(request, response,
(AccessDeniedException) exception); // 在这里调用 AccessDeniedHandler 的 handle 方法
}
}
}
在ExceptionTranslationFilter抓到之后的 * 抛出的异常后就进行以上判断:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
try {
chain.doFilter(request, response);
logger.debug("Chain processed normally");
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
RuntimeException ase = (AuthenticationException) throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (ase == null) {
ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
AccessDeniedException.class, causeChain);
}
if (ase != null) {
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
}
// 这里进入上面的方法!!!
handleSpringSecurityException(request, response, chain, ase);
}
else {
// Rethrow ServletExceptions and RuntimeExceptions as-is
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
else if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
// Wrap other Exceptions. This shouldn't actually happen
// as we've already covered all the possibilities for doFilter
throw new RuntimeException(ex);
}
}
}
综上,我们考虑 * 链没有到达ExceptionTranslationFilter便抛出异常并结束处理;或是经过了ExceptionTranslationFilter,但之后的异常没被其抓取便处理结束。
我们首先看一下当前Security的 * 链
很明显可以发现,我们自定义的过滤器在ExceptionTranslationFilter之前,所以在抛出异常后,应该会处理后直接终止执行链。
由于篇幅原因,这里不具体给出debug过程,直接给出结果。
我们查看VerificationAuthenticationFilter继承的AbstractAuthenticationProcessingFilter中的doFilter方法:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 在此处进行 url 匹配,如果不是该 * 拦截的 url,就直接执行下一个 * 的拦截
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
// 调用我们实现的 VerificationAuthenticationFilter 中的 attemptAuthentication 方法,进行登录逻辑验证
authResult = attemptAuthentication(request, response);
if (authResult == null) {
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
} catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);
return;
} catch (AuthenticationException failed) {
//
// 注意这里,如果登录失败,我们抛出的异常会在这里被抓取,然后通过 unsuccessfulAuthentication 进行处理
// 翻阅 unsuccessfulAuthentication 中的代码我们可以发现,如果我们没有设置认证失败后的重定向url,就会封装一个401的响应,也就是我们上面出现的情况
//
unsuccessfulAuthentication(request, response, failed);
// 执行完成后直接中断 * 链的执行
return;
}
// 如果登录成功就继续执行,我们设置的 continueChainBeforeSuccessfulAuthentication 为 true
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authResult);
}
通过这段代码的分析,原因就一目了然了,如果我们继承AbstractAuthenticationProcessingFilter来实现我们的登录验证逻辑,无论该过滤器在ExceptionTranslationFilter的前面或后面,都无法顺利触发ExceptionTranslationFilter中的异常处理逻辑,因为AbstractAuthenticationProcessingFilter会对认证异常进行自我消化并中断 * 链的进行,所以我们只能通过其他的Filter来封装我们的登录逻辑 * ,如:GenericFilterBean。
为了保证 * 链能顺利到达ExceptionTranslationFilter
我们需要满足两个条件:
1、自定义的认证过滤器不能通过继承AbstractAuthenticationProcessingFilter实现;
2、自定义的认证过滤器应在ExceptionTranslationFilter后面:
此外,我们也可以通过实现AuthenticationFailureHandler的方式来处理认证异常。
public class JsonAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSONObject.toJSONString(new BaseResult<String>(exception.getMessage(), false, null)));
}
}
public class VerificationAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final String USERNAME = "username";
private static final String PASSWORD = "password";
private static final String VERIFICATION_CODE = "verificationCode";
private boolean postOnly = true;
public VerificationAuthenticationFilter() {
super(new AntPathRequestMatcher(SECURITY_VERIFICATION_CODE_LOGIN, "POST"));
// 继续执行 * 链,执行被拦截的 url 对应的接口
super.setContinueChainBeforeSuccessfulAuthentication(true);
// 设置认证失败处理入口
setAuthenticationFailureHandler(new JsonAuthenticationFailureHandler());
}
...
}
来源:https://blog.csdn.net/qq_44753451/article/details/112185142


猜你喜欢
- 1 自定义类加载器自定义类加载器的代码很简单,只需要继承ClassLoader类,覆写findClass方法即可其默认实现是会抛出一个异常:
- Metro UI For JavaFX!这是一个Windows设计风格的UI库,使用非常简单,只要一行代码就可以实现整体UI风格的替换!ne
- 本文实例讲述了C#生成单页静态页简单实现方法。分享给大家供大家参考。具体方法如下:protected void BtGroup_Server
- 一、背景在我们编写程序的过程中,程序中可能随时发生各种异常,那么我们如何优雅的处理各种异常呢?二、需求1、拦截系统中部分异常,返回自定义的响
- 本文实例讲述了Android开发之开关按钮控件ToggleButton简单用法。分享给大家供大家参考,具体如下:先来看看运行效果:具体代码如
- 由于老师说如果拿MATLAB制作出游戏或者有趣的动画的话。。平时成绩可以拿满分于是。。开始尝试制作各种matlab小游戏最初通过Alex的贪
- 平时用到的库仓库名地址备注mavenCentralhttps://repo1.maven.org/maven2/
- 持久层的那些事什么是 JDBCJDBC(JavaDataBase Connectivity)就是 Java 数据库连接, 说的直白点就是 使
- 本文实例讲述了Android实现便于批量操作可多选的图片ListView。分享给大家供大家参考,具体如下:之前项目需要实现一个可多选的图片列
- 本文实例讲述了C#遍历指定目录下所有文件的方法。分享给大家供大家参考。具体分析如下:先通过DirectoryInfo打开指定的目录,然后通过
- 本文提供了基于MD5加密16位和32位的方法,具体内容如下import java.io.IOException;import java.ma
- 现在,C#创建不规则窗体不是一件难事,下面总结一下:一、自定义窗体一般为规则的图形,如圆、椭圆等。做法:重写Form1_Paint事件(Fo
- 本文实例为大家分享了java实现邮箱群发的具体代码,供大家参考,具体内容如下近来无事,在网上看了一些大牛文章,其中看到一篇比较好的,分享给大
- 自定义封装StringUtils常用工具类,供大家参考,具体内容如下package com.demo.utils;import java.u
- 本文实例讲述了Java实现文件和base64流的相互转换功能。分享给大家供大家参考,具体如下:import java.io.FileInpu
- 实例引入在家庭影院中,有灯光,屏幕,投影机,功放机,DVD 播放器这几个基本的工具:灯光,可以关闭灯光和打开灯光。投影机,可以打开和关闭投影
- 哈希表(HashMap)hash查询的时间复杂度是O(1)按值传递Character,Short,Integer,Long, Float,D
- 本文实例为大家分享了java实现学生成绩档案管理系统的具体代码,供大家参考,具体内容如下实验要求• 学生信息录入,信息包括学号、姓名、专业、
- 有经验的开发人员都知道在开发.NET应用时可以利用配置文件保存一些常用并且有可能变化的信息,例如日志文件的保存路径、数据库连接信息等等,这样
- 本文实例为大家分享了Unity实现全屏截图、Unity实现QQ截图,供大家参考,具体内容如下全屏截图:要实现的是点击鼠标左键,就实现截图,并