一文搞定 Spring Security 异常处理机制!_spring security exceptionhandling__江南一点雨的博客-程序员宝宝

技术标签: spring  java  关于Spring Boot  

今天来和小伙伴们聊一聊 Spring Security 中的异常处理机制。

在 Spring Security 的过滤器链中,ExceptionTranslationFilter 过滤器专门用来处理异常,在 ExceptionTranslationFilter 中,我们可以看到,异常被分为了两大类:认证异常和授权异常,两种异常分别由不同的回调函数来处理,今天松哥就来和大家分享一下这里的条条框框。

1.异常分类

Spring Security 中的异常可以分为两大类,一种是认证异常,一种是授权异常。

认证异常就是 AuthenticationException,它有众多的实现类:

可以看到,这里的异常实现类还是蛮多的,都是都是认证相关的异常,也就是登录失败的异常。这些异常,有的松哥在之前的文章中都和大家介绍过了,例如下面这段代码(节选自:Spring Security 做前后端分离,咱就别做页面跳转了!统统 JSON 交互):

resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
RespBean respBean = RespBean.error(e.getMessage());
if (e instanceof LockedException) {
    
    respBean.setMsg("账户被锁定,请联系管理员!");
} else if (e instanceof CredentialsExpiredException) {
    
    respBean.setMsg("密码过期,请联系管理员!");
} else if (e instanceof AccountExpiredException) {
    
    respBean.setMsg("账户过期,请联系管理员!");
} else if (e instanceof DisabledException) {
    
    respBean.setMsg("账户被禁用,请联系管理员!");
} else if (e instanceof BadCredentialsException) {
    
    respBean.setMsg("用户名或者密码输入错误,请重新输入!");
}
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();

另一类就是授权异常 AccessDeniedException,授权异常的实现类比较少,因为授权失败的可能原因比较少。

2.ExceptionTranslationFilter

ExceptionTranslationFilter 是 Spring Security 中专门负责处理异常的过滤器,默认情况下,这个过滤器已经被自动加载到过滤器链中。

有的小伙伴可能不清楚是怎么被加载的,我这里和大家稍微说一下。

当我们使用 Spring Security 的时候,如果需要自定义实现逻辑,都是继承自 WebSecurityConfigurerAdapter 进行扩展,WebSecurityConfigurerAdapter 中本身就进行了一部分的初始化操作,我们来看下它里边 HttpSecurity 的初始化过程:

protected final HttpSecurity getHttp() throws Exception {
    
	if (http != null) {
    
		return http;
	}
	AuthenticationEventPublisher eventPublisher = getAuthenticationEventPublisher();
	localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);
	AuthenticationManager authenticationManager = authenticationManager();
	authenticationBuilder.parentAuthenticationManager(authenticationManager);
	Map<Class<?>, Object> sharedObjects = createSharedObjects();
	http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
			sharedObjects);
	if (!disableDefaults) {
    
		http
			.csrf().and()
			.addFilter(new WebAsyncManagerIntegrationFilter())
			.exceptionHandling().and()
			.headers().and()
			.sessionManagement().and()
			.securityContext().and()
			.requestCache().and()
			.anonymous().and()
			.servletApi().and()
			.apply(new DefaultLoginPageConfigurer<>()).and()
			.logout();
		ClassLoader classLoader = this.context.getClassLoader();
		List<AbstractHttpConfigurer> defaultHttpConfigurers =
				SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);
		for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
    
			http.apply(configurer);
		}
	}
	configure(http);
	return http;
}

可以看到,在 getHttp 方法的最后,调用了 configure(http);,我们在使用 Spring Security 时,自定义配置类继承自 WebSecurityConfigurerAdapter 并重写的 configure(HttpSecurity http) 方法就是在这里调用的,换句话说,当我们去配置 HttpSecurity 时,其实它已经完成了一波初始化了。

在默认的 HttpSecurity 初始化的过程中,调用了 exceptionHandling 方法,这个方法会将 ExceptionHandlingConfigurer 配置进来,最终调用 ExceptionHandlingConfigurer#configure 方法将 ExceptionTranslationFilter 添加到 Spring Security 过滤器链中。

我们来看下 ExceptionHandlingConfigurer#configure 方法源码:

@Override
public void configure(H http) {
    
	AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http);
	ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(
			entryPoint, getRequestCache(http));
	AccessDeniedHandler deniedHandler = getAccessDeniedHandler(http);
	exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler);
	exceptionTranslationFilter = postProcess(exceptionTranslationFilter);
	http.addFilter(exceptionTranslationFilter);
}

可以看到,这里构造了两个对象传入到 ExceptionTranslationFilter 中:

  • AuthenticationEntryPoint 这个用来处理认证异常。
  • AccessDeniedHandler 这个用来处理授权异常。

具体的处理逻辑则在 ExceptionTranslationFilter 中,我们来看一下:

public class ExceptionTranslationFilter extends GenericFilterBean {
    
	public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint,
			RequestCache requestCache) {
    
		this.authenticationEntryPoint = authenticationEntryPoint;
		this.requestCache = requestCache;
	}
	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);
		}
		catch (IOException ex) {
    
			throw ex;
		}
		catch (Exception ex) {
    
			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 {
    
				if (ex instanceof ServletException) {
    
					throw (ServletException) ex;
				}
				else if (ex instanceof RuntimeException) {
    
					throw (RuntimeException) ex;
				}
				throw new RuntimeException(ex);
			}
		}
	}
	private void handleSpringSecurityException(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, RuntimeException exception)
			throws IOException, ServletException {
    
		if (exception instanceof AuthenticationException) {
    
			sendStartAuthentication(request, response, chain,
					(AuthenticationException) exception);
		}
		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")));
			}
			else {
    
				accessDeniedHandler.handle(request, response,
						(AccessDeniedException) exception);
			}
		}
	}
	protected void sendStartAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain,
			AuthenticationException reason) throws ServletException, IOException {
    
		SecurityContextHolder.getContext().setAuthentication(null);
		requestCache.saveRequest(request, response);
		logger.debug("Calling Authentication entry point.");
		authenticationEntryPoint.commence(request, response, reason);
	}
}

ExceptionTranslationFilter 的源码比较长,我这里列出来核心的部分和大家分析:

  1. 过滤器最核心的当然是 doFilter 方法,我们就从 doFilter 方法看起。这里的 doFilter 方法中过滤器链继续向下执行,ExceptionTranslationFilter 处于 Spring Security 过滤器链的倒数第二个,最后一个是 FilterSecurityInterceptor,FilterSecurityInterceptor 专门处理授权问题,在处理授权问题时,就会发现用户未登录、未授权等,进而抛出异常,抛出的异常,最终会被 ExceptionTranslationFilter#doFilter 方法捕获。
  2. 当捕获到异常之后,接下来通过调用 throwableAnalyzer.getFirstThrowableOfType 方法来判断是认证异常还是授权异常,判断出异常类型之后,进入到 handleSpringSecurityException 方法进行处理;如果不是 Spring Security 中的异常类型,则走 ServletException 异常类型的处理逻辑。
  3. 进入到 handleSpringSecurityException 方法之后,还是根据异常类型判断,如果是认证相关的异常,就走 sendStartAuthentication 方法,最终被 authenticationEntryPoint.commence 方法处理;如果是授权相关的异常,就走 accessDeniedHandler.handle 方法进行处理。

AuthenticationEntryPoint 的默认实现类是 LoginUrlAuthenticationEntryPoint,因此默认的认证异常处理逻辑就是 LoginUrlAuthenticationEntryPoint#commence 方法,如下:

public void commence(HttpServletRequest request, HttpServletResponse response,
		AuthenticationException authException) throws IOException, ServletException {
    
	String redirectUrl = null;
	if (useForward) {
    
		if (forceHttps && "http".equals(request.getScheme())) {
    
			redirectUrl = buildHttpsRedirectUrlForRequest(request);
		}
		if (redirectUrl == null) {
    
			String loginForm = determineUrlToUseForThisRequest(request, response,
					authException);
			RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
			dispatcher.forward(request, response);
			return;
		}
	}
	else {
    
		redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
	}
	redirectStrategy.sendRedirect(request, response, redirectUrl);
}

可以看到,就是重定向,重定向到登录页面(即当我们未登录就去访问一个需要登录才能访问的资源时,会自动重定向到登录页面)。

AccessDeniedHandler 的默认实现类则是 AccessDeniedHandlerImpl,所以授权异常默认是在 AccessDeniedHandlerImpl#handle 方法中处理的:

public void handle(HttpServletRequest request, HttpServletResponse response,
		AccessDeniedException accessDeniedException) throws IOException,
		ServletException {
    
	if (!response.isCommitted()) {
    
		if (errorPage != null) {
    
			request.setAttribute(WebAttributes.ACCESS_DENIED_403,
					accessDeniedException);
			response.setStatus(HttpStatus.FORBIDDEN.value());
			RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);
			dispatcher.forward(request, response);
		}
		else {
    
			response.sendError(HttpStatus.FORBIDDEN.value(),
				HttpStatus.FORBIDDEN.getReasonPhrase());
		}
	}
}

可以看到,这里就是服务端跳转返回 403。

3.自定义处理

前面和大家介绍了 Spring Security 中默认的处理逻辑,实际开发中,我们可以需要做一些调整,很简单,在 exceptionHandling 上进行配置即可。

首先自定义认证异常处理类和授权异常处理类:

@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
    
        response.getWriter().write("login failed:" + authException.getMessage());
    }
}
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
    
        response.setStatus(403);
        response.getWriter().write("Forbidden:" + accessDeniedException.getMessage());
    }
}

然后在 SecurityConfig 中进行配置,如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
        http.authorizeRequests()
                ...
                ...
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(myAuthenticationEntryPoint)
                .accessDeniedHandler(myAccessDeniedHandler)
                .and()
                ...
                ...
    }
}

配置完成后,重启项目,认证异常和授权异常就会走我们自定义的逻辑了。

4.小结

好啦,今天主要和小伙伴们分享了 Spring Security 中的异常处理机制,感兴趣的小伙伴可以试一试哦~

文中代码下载地址:https://github.com/lenve/spring-security-samples

公众号【江南一点雨】后台回复 springsecurity,获取Spring Security系列 40+ 篇完整文章~

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/u012702547/article/details/107033269

智能推荐

周鸿祎闭关3个月之后发言:我没老,我要做互联网圈鲨鱼!_虚坏叔叔的博客-程序员宝宝

来源:《财经》杂志2014年初,周鸿祎闭关三个月。在这三个月内,他思考了一些很本质很哲学的问题:360是什么?360要成为一家怎样的公司?结果,他的选择既不像百度、阿里,也不像腾讯、小米,再次出人意料。多数互联网企业家都是横向思维,而周鸿祎是纵向思维。对于BAT而言,他们做社交、做电商、做搜索,切入各个细分领域,横向扩展自己的业务。而周鸿祎则选择把安全这道壁垒建的越来越高、

递归函数的调用次数计算_递归函数子函数调用次数_blackjacki的博客-程序员宝宝

递归函数的调用次数计算def fibonacci(n): if n &lt;= 1: return n else: return fibonacci(n-2) + fibonacci(n-1)fibonacci(7)fibonacci(7)fibonacci(7)调用次数计算过程:C0 = 1C1 = 1C2 = 1+ C0 + C1 = 1 + 1 + 1 = 3C3 ...

python文本信息对比_迷茫小码农的博客-程序员宝宝

使用python实现对两个文本信息的对比,查看文本更新、差异import difflibtext1 = &quot;&quot;&quot; &quot;&quot;&quot;text2 = &quot;&quot;&quot; &quot;&quot;&quot;text1_lines = text1.splitlines()text2_lines = text2.splitlines()d = difflib.Differ()diff = d.compare(text1_lines

Realm异步查询的三种方式_android realm 异步查询_浅浅清风的博客-程序员宝宝

阅读过Realm文档的童鞋们应该都知道Realm、RealmObject 和RealmResults 实例都是不可以跨线程使用的。 虽然Realm查询数据的速度非常快,但有些时候我们还是不得不用上异步查询。在Realm中,从子线程查询到的数据到主线程中是不可以使用的,会报Realm accessed from incorrect thread. 所以我们如果希望通过我们熟悉的方式去创建异步

Matlab使用gtsam_toolbox_gtsam matlab代码_苏碧落的博客-程序员宝宝

看了一圈博客,感觉很多人都不看官方文档的。。。算了,自己写一篇博客吧首先说一下我自己的平台和版本:MATLAB2017bUbuntu18.04gtsam-4.0.31 编译你的gstam设定好编译的安装目录,打开matlab工具箱编译功能,配置mex文件目录,正常用cmake-gui就可以直接设定,在这里我简单写一下cmake -DMEX_COMMAND=/XXX/Matlab/bin/mex \ -DMATLAB_ROOT=/XXX/Matlab \ ..

BrokenPipeError: [Errno 32] Broken pipe解决方法_条件反射104的博客-程序员宝宝

使用pytorch运行代码报错:BrokenPipeError: [Errno 32] Broken pipe解决方案:令 torch.utils.data.DataLoader() 函数的 num_workers = 0

随便推点

mysql重置数据表主键id_qq_39161501的博客-程序员宝宝

假设表名为(logs)truncate table logs;

hdu-1078-FatMouse and Cheese(dfs+记忆化)_fatmouse and cheese hdu - 1078题意_永远鲜红の幼月的博客-程序员宝宝

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1078 Problem Description FatMouse has stored some cheese in a city. The city can be considered as a square grid of dimension n: each grid locati...

01:统计数字字符个数_萌同学_Yeah的博客-程序员宝宝

描述输入一行字符,统计出其中数字字符的个数。输入一行字符串,总长度不超过255。输出输出为1行,输出字符串里面数字字符的个数。样例输入Peking University is set up at 1898.样例输出4源码#include&amp;lt;stdio.h&amp;gt;int main(){ int n = 0; char str[256];...

编译原理复习笔记 第四章 语法分析_RabbitCotton的博客-程序员宝宝

编译原理第四章4-1 自顶向下分析概述从文法的开始符号S推导出词串w的过程最左推导:总是选择每个句型的最左非终结符进行替换最左句型:最右推导:总是选择每个句型的最右非终结符进行替换最左推导和最右推导具有唯一性扫描器从左向右扫描,故自顶向下的分析采用最左推导自顶向下语法分析的通用形式:递归下降分析:由一组过程组成,每个过程对应一个非终结符预测分析:一种确定的自顶向下分析方法,不需要回溯4-2 文法转换(不是所有的文法都适合自顶向下的分析)问

号外!首发!折腾无限!VMware Workstation 7.0 虚拟机安装雪豹snow leopard 10.6_无聊的络客的博客-程序员宝宝

VMware Workstation 7.0 虚拟机安装雪豹snow leopard 10.6!(操作系统windows 7)===原创在远景,转载注明,多谢!== 下载最新版本虚拟机VMware workstation 7.0.0 203739,安装好,重启。还以为在window7的虚拟机里装雪豹会很慢,没想到,它比以前装的豹还流畅许多,声音也不迟缓,

关于NGUI中uiscrollview拖动问题_夏日微风5的博客-程序员宝宝

有时候会发现,列表中就一个,或者一眼就看出展示的所有子物体,照样可以拖动然后拖动完之后,就定死在哪个地方这个是因为可点击的区域比UiPanel区域大,所以会发生这个情况, 把这个list大小调整好,就可以了。 图中绿色的线,是代表缓存一个子物体,里面的内容根据配表自动填充。而红色线代表PanelList的大小PanelList大小要大于子物体。即是保证子物体在Panel...

推荐文章

热门文章

相关标签