SpringSecurity02-基本原理

在上一节中,我们发现SpringBoot会自动配置Security,让我们跳转到表单登录页面,输入默认的用户名和随机生成的密码。

其实在springboot1.x版本中,并不是跳转到表单登录页,而是弹出一个对话框,在对话框里输入信息,我们可以修改代码尝试一下。

两种认证方式

首先我们在browser模块创建一个BrowserSecurityConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
// 以http方式认证 springboot1.x时默认
// 以form表单方式认证 springboot2.x默认
http.httpBasic()
.and()
.authorizeRequests() // 下面是认证信息
.anyRequest() // 所有请求
.authenticated(); // 都需要认证
}
}

这个类继承了WebSecurityConfigurerAdapter,安全配置适配器类,并重写configure方法,这里configure有三种重载方法,我们这里先使用Http形式的。

通过代码很好理解,即通过一系列配置,使用了httpBasic的方式进行身份认证的拦截。

重启Application,查看认证方式(注意需要把之前关闭自动配置的代码给注释掉)

1578279559426

登录方式成为了弹出框的形式

图解基本原理

对于SpringSecurity的基本原理,这里先放一张图,根据这张图我们进行简单了解

img

这里首先分为Security的过滤器链,由多个过滤器组成,和REST API即我们编写的服务需要被保护的服务。

再进行API请求时,会先走一遍过滤器链,成功认证后才会调用,返回时会再走一遍

绿色过滤器

这里有两个UsernamePasswordAuthenticationFilterBasicAuthenticationFilter,通过名字我们可以联想到刚刚实现的两种认证方式,第一个就是表单认证过滤器,第二个是弹出登录框的基础Http认证过滤器。

这里还能看到有第三个绿色框,其实还有很多种认证方式,比如短信验证,第三方登录等等。

请求通过绿色过滤器之后如果认证成功会加一个标记,然后进入橙色过滤器FilterSecurityInterceptor里,如果没有认证成功也会进去,只不过没有认证成功的标记。

橙色过滤器

FilterSecurityInterceptor是非常重要的过滤器,会根据我们的配置进行拦截判断,比如我们刚刚的代码:

1
2
3
4
5
6
7
8
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic()
.and()
.authorizeRequests() // 下面是认证信息
.anyRequest() // 所有请求
.authenticated(); // 都需要认证
}

这里的配置都会被它读取,从绿色过滤器过来之后会判断是否有成功标记,如果没有抛出异常,如果有再判断是否有其他的一些认证配置,比如vip用户认证,判断用户是否为vip用户,不是还是抛出异常,如果全部认证通过会放行请求API接口

蓝色过滤器

ExceptionTranslationFilter,看名字也知道是异常捕获的过滤器,即当FilterSecurityInterceptor抛出异常后,会被此过滤器拦截。

比如Interceptor抛出没有身份认证的异常,Exception Translation Filter会看看前面究竟配置的是什么样的Filter,然后做出相应的处理。比如说前面配置的是Username Password Authentication Filter,那么就会跳转到带表单的登录页面。但如果说前面配置的是BasicAuthenticationFilter,那么就会弹出登录框等待用户输入信息。

再比如用户没有输入用户名密码,Interceptor抛出认证失败异常,此过滤器就会抛出401异常

1578280446054

源码查看

首先是FilterSecurityInterceptor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
// first time this request being called, so perform security checking
if (fi.getRequest() != null && observeOncePerRequest) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}

InterceptorStatusToken token = super.beforeInvocation(fi);

try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}

super.afterInvocation(token, null);
}
}

这里简单的看一下,主要是else里的代码,获取请求信息,然后调用super.beforeInvocation()来判断是否有权限调用API,如果有权限放行,如果没有权限抛出异常,这里代码就不放出来了,可以自己去看,我们主要目的是查看调用链的执行过程。

然后查看ExceptionTranslationFilter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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);
}
}
}

因为是捕获异常的过滤器,重要代码都在catch里,对不同异常进行不同处理

最后查看UsernamePasswordAuthenticationFilter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}

String username = obtainUsername(request);
String password = obtainPassword(request);

if (username == null) {
username = "";
}

if (password == null) {
password = "";
}

username = username.trim();

UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);

// Allow subclasses to set the "details" property
setDetails(request, authRequest);

return this.getAuthenticationManager().authenticate(authRequest);
}

提取请求中的用户名密码,进行校验,成功则设置认证成功。

断点调试

接着我们对这几个类进行打断点,并DEBUG启动测试,看一下执行流程

1578290080591

首先进入Interceptor,这是因为第一次访问需要验证过滤

1578290207784

接着进入Exception过滤器,捕获到Interceptor抛出的AccessDeniedException: Access is denied异常,这里可以深入的跟一下

1578290449886

首先调用handleSpringSecurityException()方法

1578290478199

因为异常是AccessDeniedException,所以调用sendStartAuthentication()方法

1578290509511

最后进入authenticationEntryPoint.commence()方法,因为我们使用表单方式,所以进入LoginUrlAuthenticationEntryPoint

1578290621414

调用这个方法,通过注释也可知道是转发到login界面了

1578290698974

接着我们输入用户名密码,再次查看调用拦截链路

1578290757863

这里就会进入到第一个绿色框了即表单登录拦截器,这里判断用户名密码正确后会在request中添加token

1578290855189

最后到Interceptor的时候判断请求头具有token认证,即放行,成功请求到API方法

1578290897243

总结一下,拦截链路为:

Interceptor -> ExceptionFilter -> /login页面 -> 表单验证Filter -> Interceptor -> API接口