解决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中AuthenticationEntryPoint不生效相关问题

与我所设置的认证异常返回值不一致。

在继续讲解前,我先简单说下我当前的Spring Security配置,我是将不同的登录方式整合在一起,并模仿Spring Security中的UsernamePasswordAuthenticationFilter实现了不同登录方式的过滤器。   

设想通过邮件、短信、验证码和微信等登录方式登录(这里暂时只实现了验证码登录的模板)。

解决Spring Security中AuthenticationEntryPoint不生效相关问题   

以下是配置信息


/**
* @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的 * 链

解决Spring Security中AuthenticationEntryPoint不生效相关问题

  

很明显可以发现,我们自定义的过滤器在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后面:

解决Spring Security中AuthenticationEntryPoint不生效相关问题   

此外,我们也可以通过实现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

标签:Spring,Security,AuthenticationEntryPoint
0
投稿

猜你喜欢

  • Java俄罗斯方块小游戏

    2021-12-01 04:36:49
  • Java代码精简之道(推荐)

    2023-07-28 02:00:05
  • TCP协议详解_动力节点Java学院整理

    2022-09-22 07:55:14
  • maven仓库中心mirrors配置多个下载中心(执行最快的镜像)

    2023-05-04 22:08:33
  • Spring如何利用@Value注解读取yml中的map配置

    2023-07-24 21:18:00
  • java实现文件变化监控的方法(推荐)

    2023-11-08 01:18:26
  • 深入解析Java的Hibernate框架中的一对一关联映射

    2022-08-07 22:23:39
  • Java Date时间类型的操作实现

    2023-11-25 06:44:31
  • Java源码解析之详解ImmutableMap

    2023-11-23 08:06:07
  • java查找图中两点之间所有路径

    2022-10-04 03:08:11
  • Java中的SuppressWarnings注解使用

    2023-08-18 17:31:19
  • C#11新特性使用案例详解

    2023-11-26 03:19:15
  • Java线程的start方法回调run方法的操作技巧

    2023-11-11 06:02:00
  • 浅谈Java(SpringBoot)基于zookeeper的分布式锁实现

    2023-11-16 08:14:56
  • Java 如何实现时间控制

    2023-02-20 06:19:23
  • Android自定义View实现渐变色进度条

    2022-11-25 08:27:17
  • spring定义和装配bean详解

    2023-08-23 00:33:18
  • java调用微信现金红包接口的心得与体会总结

    2022-12-22 19:55:12
  • 关于国际化、OGNL表达式语言

    2023-09-04 15:20:45
  • spring boot 加载web容器tomcat流程源码分析

    2021-12-05 14:48:38
  • asp之家 软件编程 m.aspxhome.com