SpringSecurity结构梳理

整体结构

Spring Security整体是基于Servlet的Filter机制。简单来说,其就是将一系列的Filter组合起来,然后建立一定的机制来管理这些Filter;并且为特定的Filter提供一定的配套类来达成一定的功能,比如认证的Filter,CSRF的Filter等。

下面是对其管理机制的一些简单介绍

FilterChain

Servlet的Filter的执行过程,类似于函数的调用过程。

即当一个请求从客户端发送到Server的时候,在到达对应的Servlet处理之前,都会调用提前的设定的对应Filter。并且这些Filter会按照调用栈执行,并且会按照调用的顺序再返回,由第一个Filter来执行response。

其流程图如下:

Filter流程

值得注意的是,SpringMVC就是基于Servelt的,其基本原理为,建立一个名为DispatcherServlet的servlet来接受所有的请求,然后再在SpringMVC应用内为其映射对应的Controller请求函数。

而上面整个过程中用的Filter就构成了一个链式的结构,其又被称为Filter链(FilterChain)。

DelegatingFilterProxy

这个类实际上是为Spring上下文和Servlet之间提供了一个桥梁。即一般来说,我们为Servlet注册上下文的时候,Spring上下文并不一定启动了,所以在Filter中无法感知到Spring应用,并且这些Filter也是用户自己new出来的,可以说是违背了Spring应用中IOC的核心,即把类的构造权交由Spring容器来做,而不是自己手动新建。

所以该类就是为了解决这个问题,从名字就可以看出,这是一个具有代理功能的类。即一个DelegatingFilterProxy代理一个真实的Filter,并且被代理的这个类是在Spring容器中的。代理类中提供了从容器中查找的对应真实Filter的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected void initFilterBean() throws ServletException {
synchronized(this.delegateMonitor) {
if (this.delegate == null) {
if (this.targetBeanName == null) {
this.targetBeanName = this.getFilterName();
}

WebApplicationContext wac = this.findWebApplicationContext();
if (wac != null) {
this.delegate = this.initDelegate(wac);
}
}

}
}

和调用真实Filter的方法:

1
2
3
protected void invokeDelegate(Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
delegate.doFilter(request, response, filterChain);
}

所以一般的用法就是,在Spring容器中定义对应的Filter,然后再向Servlet容器中注册一个自己新建的DelegatingFilterProxy类实例,该实例就会自动查找并调用Spring容器中的Filter。

如新建的DelegatingFilterProxy的doFilter方法:

1
2
3
4
5
6
7
8
9
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// Lazily get Filter that was registered as a Spring Bean
// For the example in DelegatingFilterProxy
delegate
is an instance of Bean Filter0
Filter delegate = getFilterBean(someBeanName);
// delegate work to the Spring Bean
delegate.doFilter(request, response);
}

使用这种代理机制的另一个好处就是延迟加载,实际上还是克服了无法将容器对象注册为标准Filter的缺点。因为Filter需要在应用启动的时候就注册,但此时Spring容器可能并没有初始化好,那么就是无法进行注册的。则借用代理,在需要时加载,那么在需要的时候一般就已经初始化好了(因为请求都发送过来了)。

反思

实际上该类很简单,一共就180行代码。

这里其实也为我们经常遇到的一个问题提供了解决思路,即:在Filter中如果需要使用Spring容器中的对象,该怎么拿到?或者更宽泛一点:在Spring容器可能还没有初始化完成时(还比如interceptor种),我们又需要使用容器中的对象,该怎么使用?一个典型的场景就是:在Filter中需要使用redis,除非自己手写一个RedisTemplate,但Spring-Redis实际上已经新建好并且注入到Spring容器中,只需要我们取出来就好了,这种情况下,自己再去新建就显得没有必要了。

那么这里我们就可以使用其中的API,通过request来获取对应的Spring上下文,并以此来获取容器的对象来进行使用。值得注意的是,这里的使用也是延迟的,即获取和使用都是在请求到来时才进行,因为容器的初始化在之后才进行的。(例如,如果将上下文的获取,容器中对象中的获取放到Filter的构造器中,那就肯定得不到对应的对象,因为那时容器还没有初始化,获取的必定为null。)

一个简单的实例如下(从request获取存储在Spring上下文中的RedisUtils对象):

1
2
3
4
public static RedisUtils getInstance(HttpServletRequest request){
WebApplicationContext webApplicationContext = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
return webApplicationContext.getBean(RedisUtils.class);
}

这里主要是使用WebApplicationContextUtils工具类。

再看一下其是如何实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Nullable
public static WebApplicationContext getWebApplicationContext(ServletContext sc) {
return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
}

@Nullable
public static WebApplicationContext getWebApplicationContext(ServletContext sc, String attrName) {
Assert.notNull(sc, "ServletContext must not be null");
Object attr = sc.getAttribute(attrName);
if (attr == null) {
return null;
} else if (attr instanceof RuntimeException) {
throw (RuntimeException)attr;
} else if (attr instanceof Error) {
throw (Error)attr;
} else if (attr instanceof Exception) {
throw new IllegalStateException((Exception)attr);
} else if (!(attr instanceof WebApplicationContext)) {
throw new IllegalStateException("Context attribute is not of type WebApplicationContext: " + attr);
} else {
return (WebApplicationContext)attr;
}
}

可以看到其实逻辑很简单,就是从ServletContext通过名字获取WebApplicationContext对象。而且个对象是在应用启动的时候由Spring放进去的。

FilterChainProxy

FilterChainProxy实际上相当于Spring Security的DelegatingFilterProxy。即它也是一个代理类,但它与DelegatingFilterProxy不同的点在于:

  • FilterChainProxy是所有经过Spring Security请求的入口位置,所以其代理的是多个Filter,而不是像DelegatingFilterProxy一样,仅仅代理一个Filter。
  • FilterChainProxy作为Spring Security的核心Filter,其可以用来做一些其他任务,比如清除SecurityContext防止内存泄露。
  • FilterChainProxy提供了更灵活的Filter匹配机制,即哪些Filter将在哪些路径中触发。

简单来说,就是DelegatingFilterProxy代理了FilterChainProxy,而FilterChainProxy又代理一系列的Filter(这些代理就是普通的Spring容器内的对象了)。

下面是一个简单的例子:

  1. Servlet内看到的Filter,可以看到第四个就是一个DelegatingFilterProxy,这个类就是Spring Security注册的。

Servlet内看到的Filter

  1. 可以看到,这个DelegatingFilterProxy代理了一个FilterChainProxy对象。

DelegatingFilterProxy代理了FilterChainProxy

  1. FilterChainProxy又代理了一系列的Filter。

FilterChainProxy代理的Filter

所以实际上就是一个套娃的过程。

Spring Security中的默认拦截器顺序

  • ForceEagerSessionCreationFilter
  • ChannelProcessingFilter
  • WebAsyncManagerIntegrationFilter
  • SecurityContextPersistenceFilter
  • HeaderWriterFilter
  • CorsFilter
  • CsrfFilter
  • LogoutFilter
  • OAuth2AuthorizationRequestRedirectFilter
  • Saml2WebSsoAuthenticationRequestFilter
  • X509AuthenticationFilter
  • AbstractPreAuthenticatedProcessingFilter
  • CasAuthenticationFilter
  • OAuth2LoginAuthenticationFilter
  • Saml2WebSsoAuthenticationFilter
  • UsernamePasswordAuthenticationFilter
  • DefaultLoginPageGeneratingFilter
  • DefaultLogoutPageGeneratingFilter
  • ConcurrentSessionFilter
  • DigestAuthenticationFilter
  • BearerTokenAuthenticationFilter
  • BasicAuthenticationFilter
  • RequestCacheAwareFilter
  • SecurityContextHolderAwareRequestFilter
  • JaasApiIntegrationFilter
  • RememberMeAuthenticationFilter
  • AnonymousAuthenticationFilter
  • OAuth2AuthorizationCodeGrantFilter
  • SessionManagementFilter
  • ExceptionTranslationFilter
  • FilterSecurityInterceptor
  • SwitchUserFilter

认证流程

SecurityContextHolder

SecurityContextHolder是Spring认证的核心。它存储了当前被认证的用户信息,其存储的是一个SecurityContext对象。

Spring Security并不关心如何存储该信息,但是它包含一个值,它被用作当前通过身份验证的用户。

其结构如下:

securitycontextholder

表示用户通过身份验证的最简单方法是直接设置SecurityContextHolder:

1
2
3
4
SecurityContext context = SecurityContextHolder.createEmptyContext(); 
Authentication authentication = new TestingAuthenticationToken("username", "password", "ROLE_USER");
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);

获取存储的信息:

1
2
3
4
5
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

默认情况下,SecurityContextHolder使用ThreadLocal来存储这些细节,这意味着SecurityContext总是对同一个线程中的方法可用,即使SecurityContext没有显式地作为参数传递给那些方法。如果在处理当前主体的请求后清除线程,那么以这种方式使用ThreadLocal是非常安全的。Spring Security的FilterChainProxy确保SecurityContext可以被清除掉。

在其他的场景中,可能并不适合使用ThreadLocal,比如Swing客户端中,可能希望Java虚拟机中的所有线程使用相同的SecurityContextHolder

那么就可以设定SecurityContextHolder的策略,即设置SecurityContextHolder.strategyName。其有4种模式:

  • public static final String MODE_THREADLOCAL = “MODE_THREADLOCAL”:默认,即ThreadLocal方式。
  • public static final String MODE_INHERITABLETHREADLOCAL = “MODE_INHERITABLETHREADLOCAL”:其所有派生线程共享。
  • public static final String MODE_GLOBAL = “MODE_GLOBAL”:所有线程共享上下文。

SecurityContext

其是一个接口,只定义了基本的设置Authentication的方法。但其有一个实现类SecurityContextImpl,但是实现也很简单,只是定义了最基本的Authentication成员变量的get和set方法。该类是实际存储用户数据的类,其将会被存储到SecurityContextHolder中。

Authentication

这是一个接口,其含义是真正存储用户认证信息的类,其源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.security.core;

import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;

public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();

Object getCredentials();

Object getDetails();

Object getPrincipal();

boolean isAuthenticated();

void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

可以看到很简单,其中的getCredentials是定义的获取用户的的密钥,getPrincipal用来获取用户的名字,而getDetails用于获取其他附加的信息(可由自己定义)。

由于其是一个接口,必须要有其实现类才能工作,所以在Spring Security内部先定义了一个AbstractAuthenticationToken抽象类实现了一定的功能。

然后又为不同场景中定义了很多不同的Authentication,这些Authentication都被命名为xxxToken。

最常见的就是UsernamePasswordAuthenticationTokenAnonymousAuthenticationToken

其中AnonymousAuthenticationToken是用来存储账户密码的对象。

其实现很简单,即四十多行代码,其中principal就是用户名,credentials就是密码:

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
46
47
48
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.security.authentication;

import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.Assert;

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 560L;
private final Object principal;
private Object credentials;

public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}

public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}

public Object getCredentials() {
return this.credentials;
}

public Object getPrincipal() {
return this.principal;
}

public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
super.setAuthenticated(false);
}

public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}

该对象会交给对应类型的ProviderManager来进行认证,并且是更具这个类的类型来选择ProviderManager的类型。

GrantedAuthority

这个类是用来存储用户的权限实例。比如角色和权限。

一般是从Authentication.getAuthorities() 获取。这个对象是对应于特定角色的权限。一般来说是在UserDetailService中装配User对象时一起装配的(比如是从数据库中的RBAC模型获取的)。

AuthenticationManager

AuthenticationManager是一个接口,它规定了Spring Security如何进行验证用户。但实际上其只定义了一个authenticate方法,这个方法也是整个认证过程的关键。

我们可以自己实现AuthenticationManager,但一般都是使用Spring Security自己实现的ProviderManager

ProviderManager

ProviderManager是Spring Security认证的核心类。它是AuthenticationManager的最常见实现类。

它代理一系列的AuthenticationProviderAuthenticationProvider是实际进行用户认证的类。每个AuthenticationProvider都有机会表明身份验证应该成功、失败,或者表明它不能做出决定并允许下游的AuthenticationProvider做出决定。如果没有配置AuthenticationProvider实例可以验证,与ProviderNotFoundException认证失败,这是一个特殊AuthenticationException表明ProviderManager没有配置为支持传递给它的身份验证类型(Authentication类型)。

简单来说就是由ProviderManager代理一系列的AuthenticationProvider,然后当认证请求到来的时候(通常是在Filter中),来调用对应类型的AuthenticationProvider来进行用户的验证。

其结构如下:

providermanager

另外,ProviderManager还允许配置一个可选的父类AuthenticationManager(当然一般也是ProviderManager)。在当前ProviderManager找不到对应AuthenticationProvider的情况下就会调用父类进行验证。

AuthenticationProvider

这个类也是一个十分重要的类,它提供了对不同类型Authentication的具体验证方案。Spring Security也提供了很多具体的验证方案,在Spring Security中,用户密码验证的默认AuthenticationProviderDaoAuthenticationProvider,它实现了AbstractUserDetailsAuthenticationProvider类。

其中主要规定了密码加密方式setPasswordEncoderUserDetailService的获取等方法。

而我们要自定义自己的验证方式时,也可以自定义AuthenticationProvider。主要就是要实现authenticate方法。

例如,下面是一个实现邮箱登录的AuthenticationProvider

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
46
47
48
49
50
51
52
53
54
55
56
public class EmailCodeAuthenticationProvider implements AuthenticationProvider {

private XPanUserEmailDetailsService UuerEmailDetailsService;

private DefaultPreAuthenticationChecks preAuthenticationChecks = new DefaultPreAuthenticationChecks();

private HttpServletRequest request;

public EmailCodeAuthenticationProvider(XPanUserEmailDetailsService userDetailsService) {
this.UuerEmailDetailsService = userDetailsService;
}

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
supports(authentication.getClass());
EmailCodeAuthenticationToken token = (EmailCodeAuthenticationToken) authentication;
UserDetails user = UuerEmailDetailsService.loadUserByUserEmail((String) token.getPrincipal());
HttpServletRequest request = (HttpServletRequest)token.getDetails();
RedisUtils instance = RedisUtils.getInstance(request);
String realCode = instance.get((String)authentication.getPrincipal());
if(realCode == null){
throw new BadCredentialsException("验证码已过期!");
}
// 验证码是否相同
if(!realCode.equals((String) authentication.getCredentials())){
throw new BadCredentialsException("验证码错误!");
}
if(user == null){
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
preAuthenticationChecks.check(user);
EmailCodeAuthenticationToken res = new EmailCodeAuthenticationToken(user, user.getAuthorities());
res.setDetails(token.getDetails());
return res;
}

@Override
public boolean supports(Class<?> authentication) {
return EmailCodeAuthenticationToken.class.isAssignableFrom(authentication);
}

private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
private DefaultPreAuthenticationChecks() {
}

public void check(UserDetails user) {
if (!user.isAccountNonLocked()) {
throw new LockedException("User account is locked");
} else if (!user.isEnabled()) {
throw new DisabledException("User is disabled");
} else if (!user.isAccountNonExpired()) {
throw new AccountExpiredException(" User account has expired");
}
}
}
}

UserDetailsService

该接口一般用在AuthenticationProvider类中用来获取用户的完整信息,该接口定义了loadUserByUsername方法用来获取用户的信息。但是如果在自定义的AuthenticationProvider中,就不一定要用实现UserDetailsService的方式来获取用户信息了,也可以直接定义自己的类。(基本的问题就是比如邮箱登录的时候,我们应当是通过邮箱来获取用户,而不是用户名)。

Powered by Hexo and Hexo-theme-hiker

Copyright © 2019 - 2024 My Wonderland All Rights Reserved.

UV : | PV :