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

目录
  • 在这里我的代码如下
  • 用不存在的用户名密码登录后会出现以下返回数据
  • 以下是配置信息
  • 以下是实现的验证码登录过滤器
  • 以下是对应的源码
  • 我们首先看一下当前Security的拦截器链
  • 为了保证拦截器链能顺利到达ExceptionTranslationFilter

之前由于项目需要比较详细地学习了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());
    }
    ...
}

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • 详解Spring Boot 使用Spring security 集成CAS

    1.创建工程 创建Maven工程:springboot-security-cas 2.加入依赖 创建工程后,打开pom.xml,在pom.xml中加入以下内容: <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.3.RELEASE</versio

  • 深入浅析 Spring Security 缓存请求问题

    为什么要缓存? 为了更好的描述问题,我们拿使用表单认证的网站举例,简化后的认证过程分为7步: 用户访问网站,打开了一个链接(origin url). 请求发送给服务器,服务器判断用户请求了受保护的资源. 由于用户没有登录,服务器重定向到登录页面 填写表单,点击登录 浏览器将用户名密码以表单形式发送给服务器 服务器验证用户名密码.成功,进入到下一步.否则要求用户重新认证(第三步) 服务器对用户拥有的权限(角色)判定: 有权限,重定向到origin url; 权限不足,返回状态码403("forbi

  • SpringBoot集成Spring Security用JWT令牌实现登录和鉴权的方法

    最近在做项目的过程中 需要用JWT做登录和鉴权 查了很多资料 都不甚详细 有的是需要在application.yml里进行jwt的配置 但我在导包后并没有相应的配置项 因而并不适用 在踩过很多坑之后 稍微整理了一下 做个笔记 一.概念 1.什么是JWT Json Web Token (JWT)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519) 该token被设计为紧凑且安全的 特别适用于分布式站点的单点登录(SSO)场景 随着JWT的出现 使得校验方式更加简单便

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

    目录 在这里我的代码如下 用不存在的用户名密码登录后会出现以下返回数据 以下是配置信息 以下是实现的验证码登录过滤器 以下是对应的源码 我们首先看一下当前Security的拦截器链 为了保证拦截器链能顺利到达ExceptionTranslationFilter 之前由于项目需要比较详细地学习了Spring Security的相关知识,并打算实现一个较为通用的权限管理模块.由于项目是前后端分离的,所以当认证或授权失败后不应该使用formLogin()的重定向,而是返回一个json形式的对象来提示没

  • 解决Spring Security的权限配置不生效问题

    目录 SpringSecurity权限配置不生效 1.不生效的例子 2.解决办法 SpringSecurity动态配置权限 导入依赖 相关配置 创建UserMapper类&&UserMapper.xml 创建UserServiceMenuService 创建CustomFilterInvocationSecurityMetadataSource 创建CustomAccessDecisionManager 创建WebSecurityConfig配置类 Spring Security权限配置不

  • spring security中的csrf防御原理(跨域请求伪造)

    什么是csrf? csrf又称跨域请求伪造,攻击方通过伪造用户请求访问受信任站点.CSRF这种攻击方式在2000年已经被国外的安全人员提出,但在国内,直到06年才开始被关注,08年,国内外的多个大型社区和交互网站分别爆出CSRF漏洞,如:NYTimes.com(纽约时报).Metafilter(一个大型的BLOG网站),YouTube和百度HI......而现在,互联网上的许多站点仍对此毫无防备,以至于安全业界称CSRF为"沉睡的巨人". 举个例子,用户通过表单发送请求到银行网站,银行

  • Spring Security 中细化权限粒度的方法

    有小伙伴表示微人事(https://github.com/lenve/vhr)的权限粒度不够细.不过松哥想说的是,技术都是相通的,明白了 vhr 中权限管理的原理,在此基础上就可以去细化权限管理粒度,细化过程和还是用的 vhr 中用的技术,只不过设计层面重新规划而已. 当然今天我想说的并不是这个话题,主要是想和大家聊一聊 Spring Security 中权限管理粒度细化的问题.因为这个问题会涉及到不同的权限管理模型,今天和小伙伴们聊一聊- 1.权限管理模型 要想将细化权限粒度,我们不可避免会涉

  • Spring Security中的Servlet过滤器体系代码分析

    1. 前言 我在Spring Security 实战干货:内置 Filter 全解析对Spring Security的内置过滤器进行了罗列,但是Spring Security真正的过滤器体系才是我们了解它是如何进行"认证"."授权"."防止利用漏洞"的关键. 2. Servlet Filter体系 这里我们以Servlet Web为讨论目标,Reactive Web暂不讨论.我们先来看下最基础的Servlet体系,在Servlet体系中客户端发起

  • 图解Spring Security 中用户是如何实现登录的

    1. 前言 欢迎阅读Spring Security 实战干货系列文章,在集成Spring Security安全框架的时候我们最先处理的可能就是根据我们项目的实际需要来定制注册登录了,尤其是Http登录认证.根据以前的相关文章介绍,Http登录认证由过滤器UsernamePasswordAuthenticationFilter 进行处理.我们只有把这个过滤器搞清楚才能做一些定制化.今天我们就简单分析它的源码和工作流程. 2. UsernamePasswordAuthenticationFilter

  • 详解Spring Security 中的四种权限控制方式

    Spring Security 中对于权限控制默认已经提供了很多了,但是,一个优秀的框架必须具备良好的扩展性,恰好,Spring Security 的扩展性就非常棒,我们既可以使用 Spring Security 提供的方式做授权,也可以自定义授权逻辑.一句话,你想怎么玩都可以! 今天松哥来和大家介绍一下 Spring Security 中四种常见的权限控制方式. 表达式控制 URL 路径权限 表达式控制方法权限 使用过滤注解 动态权限 四种方式,我们分别来看.  1.表达式控制 URL 路径权

  • 解决Spring Security 用户帐号已被锁定问题

    1.问题描述 主要就是org.springframework.security.authentication.LockedException: 用户帐号已被锁定这个异常,完整异常如下: [2020-05-09 16:07:00 下午]:DEBUG org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider$DefaultPreAuthenticationChecks.check

  • Spring Security 中如何让上级拥有下级的所有权限(案例分析)

    答案是能! 松哥之前写过类似的文章,但是主要是讲了用法,今天我们来看看原理! 本文基于当前 Spring Security 5.3.4 来分析,为什么要强调最新版呢?因为在在 5.0.11 版中,角色继承配置和现在不一样.旧版的方案我们现在不讨论了,直接来看当前最新版是怎么处理的. 1.角色继承案例 我们先来一个简单的权限案例. 创建一个 Spring Boot 项目,添加 Spring Security 依赖,并创建两个测试用户,如下: @Override protected void con

  • 详解Spring Security中获取当前登录用户的详细信息的几种方法

    目录 在Bean中获取用户信息 在Controller中获取用户信息 通过 Interface 获取用户信息 在JSP页面中获取用户信息 在Bean中获取用户信息 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (!(authentication instanceof AnonymousAuthenticationToken)) { String currentU

随机推荐