Spring-security-一架构框架ComponentServiceFilter分析

56次阅读

共计 12880 个字符,预计需要花费 33 分钟才能阅读完成。

  想要深入 spring security 的 authentication(身份验证)和 access-control(访问权限控制)工作流程,必须清楚 spring security 的主要技术点包括关键接口、类以及抽象类如何协同工作进行 authentication 和 access-control 的实现。

1.spring security 认证和授权流程

常见认证和授权流程可以分成:

  1. A user is prompted to log in with a username and password(用户用账密码登录)
  2. The system (successfully) verifies that the password is correct for the username(校验密码正确性)
  3. The context information for that user is obtained (their list of roles and so on).(获取用户信息 context,如权限)
  4. A security context is established for the user(为用户创建 security context)
  5. The user proceeds, potentially to perform some operation which is potentially protected by an access control mechanism which checks the required permissions for the operation against the current security context information.(访问权限控制,是否具有访问权限)

1.1 spring security 认证

上述前三点为 spring security 认证验证环节:

  1. 通常通过 AbstractAuthenticationProcessingFilter 过滤器将账号密码组装成 Authentication 实现类 UsernamePasswordAuthenticationToken;
  2. 将 token 传递给 AuthenticationManager 验证是否有效,而 AuthenticationManager 通常使用 ProviderManager 实现类来检验;
  3. AuthenticationManager 认证成功后将返回一个拥有详细信息的 Authentication object(包括权限信息,身份信息,细节信息,但密码通常会被移除);
  4. 通过 SecurityContextHolder.getContext().getAuthentication().getPrincipal() 将 Authentication 设置到 security context 中。

1.2 spring security 访问授权

  1. 通过 FilterSecurityInterceptor 过滤器入口进入;
  2. FilterSecurityInterceptor 通过其继承的抽象类的 AbstractSecurityInterceptor.beforeInvocation(Object object) 方法进行访问授权,其中涉及了类 AuthenticationManager、AccessDecisionManager、SecurityMetadataSource 等。

根据上述描述的过程,我们接下来主要去分析其中涉及的一下 Component、Service、Filter。

2. 核心组件(Core Component)

2.1 SecurityContextHolder

  SecurityContextHolder 提供对 SecurityContext 的访问,存储 security context(用户信息、角色权限等),而且其具有下列储存策略即工作模式:

  1. SecurityContextHolder.MODE_THREADLOCAL(默认):使用 ThreadLocal,信息可供此线程下的所有的方法使用,一种与线程绑定的策略,此天然很适合 Servlet Web 应用。
  2. SecurityContextHolder.MODE_GLOBAL:使用于独立应用
  3. SecurityContextHolder.MODE_INHERITABLETHREADLOCAL:具有相同安全标示的线程

修改 SecurityContextHolder 的工作模式有两种方法 :

  1. 设置一个系统属性 (system.properties) : spring.security.strategy;
  2. 调用 SecurityContextHolder 静态方法 setStrategyName()

在默认 ThreadLocal 策略中,SecurityContextHolder 为静态方法获取用户信息为:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {String username = ((UserDetails)principal).getUsername();} else {String username = principal.toString();
}

但是一般不需要自身去获取。
其中 getAuthentication() 返回一个 Authentication 认证主体,接下来分析 Authentication、UserDetails 细节。

2.2 Authentication

  Spring Security 使用一个 Authentication 对象来描述当前用户的相关信息, 其包含用户拥有的权限信息列表、用户细节信息(身份信息、认证信息)。Authentication 为认证主体在 spring security 中时最高级别身份 / 认证的抽象,常见的实现类 UsernamePasswordAuthenticationToken。Authentication 接口源码:

public interface Authentication extends Principal, Serializable {
// 权限信息列表, 默认 GrantedAuthority 接口的一些实现类
Collection<? extends GrantedAuthority> getAuthorities();
// 密码信息
Object getCredentials();
// 细节信息,web 应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的 ip 地址和 sessionId 的值
Object getDetails();
// 通常返回值为 UserDetails 实现类
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

前面两个组件都涉及了 UserDetails,以及 GrantedAuthority 其到底是什么呢?2.3 小节分析。

2.3 UserDetails&GrantedAuthority

  UserDetails 提供从应用程序的 DAO 或其他安全数据源构建 Authentication 对象所需的信息,包含 GrantedAuthority。其官方实现类为 User,开发者可以实现其接口自定义 UserDetails 实现类。其接口源码:

public interface UserDetails extends Serializable {Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();}

  UserDetails 与 Authentication 接口功能类似,其实含义即是 Authentication 为用户提交的认证凭证(账号密码),UserDetails 为系统中用户正确认证凭证,在 UserDetailsService 中的 loadUserByUsername 方法获取正确的认证凭证。
  其中在 getAuthorities() 方法中获取到 GrantedAuthority 列表是代表用户访问应用程序权限范围,此类权限通常是“role( 角色)”,例如 ROLE_ADMINISTRATOR 或 ROLE_HR_SUPERVISOR。GrantedAuthority 接口常见的实现类 SimpleGrantedAuthority。

3. 核心服务类(Core Services)

3.1 AuthenticationManager、ProviderManager 以及 AuthenticationProvider

  AuthenticationManager 是认证相关的核心接口,是认证一切的起点。但常见的认证流程都是 AuthenticationManager 实现类 ProviderManager 处理,而且 ProviderManager 实现类基于委托者模式维护 AuthenticationProvider 列表用于不同的认证方式。例如:

  1. 使用账号密码认证方式 DaoAuthenticationProvider 实现类(继承了 AbstractUserDetailsAuthenticationProvide 抽象类),其为默认认证方式,进行数据库库获取认证数据信息。
  2. 游客身份登录认证方式 AnonymousAuthenticationProvider 实现类
  3. 从 cookies 获取认证方式 RememberMeAuthenticationProvider 实现类

  AuthenticationProvider 为

ProviderManager 源码分析:

public Authentication authenticate(Authentication authentication)
throws AuthenticationException {Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
//AuthenticationProvider 列表依次认证
for (AuthenticationProvider provider : getProviders()) {if (!provider.supports(toTest)) {continue;}
try {
// 每个 AuthenticationProvider 进行认证
result = provider.authenticate(authentication)
if (result != null) {copyDetails(authentication, result);
break;
}
}
....
catch (AuthenticationException e) {lastException = e;}
}
// 进行父类 AuthenticationProvider 进行认证
if (result == null && parent != null) {
// Allow the parent to try.
try {result = parent.authenticate(authentication);
}
catch (AuthenticationException e) {lastException = e;}
}
// 如果有 Authentication 信息,则直接返回
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// 清除密码
((CredentialsContainer) result).eraseCredentials();}
// 发布登录成功事件
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
// 如果都没认证成功,抛出异常
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}

  ProviderManager 中的 AuthenticationProvider 列表,会依照次序去认证,默认策略下,只需要通过一个 AuthenticationProvider 的认证,即可被认为是登录成功,而且 AuthenticationProvider 认证成功后返回一个 Authentication 实体,并为了安全会进行清除密码。如果所有认证器都无法认证成功,则 ProviderManager 会抛出一个 ProviderNotFoundException 异常。

3.2 UserDetailsService

  UserDetailsService 接口作用是从特定的地方获取认证的数据源(账号、密码)。如何获取到系统中正确的认证凭证,通过 loadUserByUsername(String username) 获取认证信息,而且其只有一个方法:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;  

其常见的实现类从数据获取的 JdbcDaoImpl 实现类,从内存中获取的 InMemoryUserDetailsManager 实现类,不过我们可以实现其接口自定义 UserDetailsService 实现类,如下:

public class CustomUserService implements UserDetailsService {
@Autowired
// 用户 mapper
private UserInfoMapper userInfoMapper;
@Autowired
// 用户权限 mapper
private PermissionInfoMapper permissionInfoMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {UserInfoDTO userInfo = userInfoMapper.getUserInfoByUserName(username);
if (userInfo != null) {List<PermissionInfoDTO> permissionInfoDTOS = permissionInfoMapper.findByAdminUserId(userInfo.getId());
List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
// 组装权限 GrantedAuthority object
for (PermissionInfoDTO permissionInfoDTO : permissionInfoDTOS) {if (permissionInfoDTO != null && permissionInfoDTO.getPermissionName() != null) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permissionInfoDTO.getPermissionName());
grantedAuthorityList.add(grantedAuthority);
}
}
// 返回用户信息
return new User(userInfo.getUserName(), userInfo.getPasswaord(), grantedAuthorityList);
}else {
// 抛出用户不存在异常
throw new UsernameNotFoundException("admin" + username + "do not exist");
}
}
}

3.3 AccessDecisionManager&SecurityMetadataSource

  AccessDecisionManager 是由 AbstractSecurityInterceptor 调用,负责做出最终的访问控制决策。

AccessDecisionManager 接口源码:

// 访问控制决策
void decide(Authentication authentication, Object secureObject,Collection<ConfigAttribute> attrs)
throws AccessDeniedException;
// 是否支持处理传递的 ConfigAttribute
boolean supports(ConfigAttribute attribute);
// 确认 class 是否为 AccessDecisionManager
boolean supports(Class clazz);

  SecurityMetadataSource 包含着 AbstractSecurityInterceptor 访问授权所需的元数据(动态 url、动态授权所需的数据),在 AbstractSecurityInterceptor 授权模块中结合 AccessDecisionManager 进行访问授权。其涉及了 ConfigAttribute。
SecurityMetadataSource 接口:

Collection<ConfigAttribute> getAttributes(Object object)
throws IllegalArgumentException;
Collection<ConfigAttribute> getAllConfigAttributes();
boolean supports(Class<?> clazz);

我们还可以自定义 SecurityMetadataSource 数据源,实现接口 FilterInvocationSecurityMetadataSource。例:

public class MyFilterSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {public List<ConfigAttribute> getAttributes(Object object) {FilterInvocation fi = (FilterInvocation) object;
String url = fi.getRequestUrl();
String httpMethod = fi.getRequest().getMethod();
List<ConfigAttribute> attributes = new ArrayList<ConfigAttribute>();
// Lookup your database (or other source) using this information and populate the
// list of attributes
return attributes;
}
public Collection<ConfigAttribute> getAllConfigAttributes() {return null;}
public boolean supports(Class<?> clazz) {return FilterInvocation.class.isAssignableFrom(clazz);
}
}

3.4 PasswordEncoder

  为了存储安全,一般要对密码进行算法加密,而 spring security 提供了加密 PasswordEncoder 接口。其实现类有使用 BCrypt hash 算法实现的 BCryptPasswordEncoder,SCrypt hashing 算法实现的 SCryptPasswordEncoder 实现类,实现类内部实现可看源码分析。而 PasswordEncoder 接口只有两个方法:

public interface PasswordEncoder {
// 密码加密
String encode(CharSequence rawPassword);
// 密码配对
boolean matches(CharSequence rawPassword, String encodedPassword);
}

4 核心 Security 过滤器(Core Security Filters)

4.1 FilterSecurityInterceptor

  FilterSecurityInterceptor 是 Spring security 授权模块入口,该类根据访问的用户的角色,权限授权访问那些资源(访问特定路径应该具备的权限)。
  FilterSecurityInterceptor 封装 FilterInvocation 对象进行操作,所有的请求到了这一个 filter,如果这个 filter 之前没有执行过的话,那么首先执行其父类 AbstractSecurityInterceptor 提供的 InterceptorStatusToken token = super.beforeInvocation(fi),在此方法中使用 AuthenticationManager 获取 Authentication 中用户详情,使用 ConfigAttribute 封装已定义好访问权限详情,并使用 AccessDecisionManager.decide() 方法进行访问权限控制。
FilterSecurityInterceptor 源码分析:

public void invoke(FilterInvocation fi) throws IOException, ServletException {if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {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);
}
// 回调其继承的抽象类 AbstractSecurityInterceptor 的方法
InterceptorStatusToken token = super.beforeInvocation(fi);
try {fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}

AbstractSecurityInterceptor 源码分析:

protected InterceptorStatusToken beforeInvocation(Object object) {
....
// 获取所有访问权限(url-role)属性列表(已定义在数据库或者其他地方)Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
....
// 获取该用户访问信息(包括 url,访问权限)Authentication authenticated = authenticateIfRequired();
// Attempt authorization
try {
// 进行授权访问
this.accessDecisionManager.decide(authenticated, object, attributes);
}catch
....
}

4.2 UsernamePasswordAuthenticationFilter

  UsernamePasswordAuthenticationFilter 使用 username 和 password 表单登录使用的过滤器,也是最为常用的过滤器。其源码:

public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 获取表单中的用户名和密码
String username = obtainUsername(request);
String password = obtainPassword(request);
...
username = username.trim();
// 组装成 username+password 形式的 token
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
// 交给内部的 AuthenticationManager 去认证,并返回认证信息
return this.getAuthenticationManager().authenticate(authRequest);
}

  其主要代码为创建 UsernamePasswordAuthenticationToken 的 Authentication 实体以及调用 AuthenticationManager 进行 authenticate 认证,根据认证结果执行 successfulAuthentication 或者 unsuccessfulAuthentication,无论成功失败,一般的实现都是转发或者重定向等处理,不再细究 AuthenticationSuccessHandler 和 AuthenticationFailureHandle。兴趣的可以研究一下其父类 AbstractAuthenticationProcessingFilter 过滤器。

4.3 AnonymousAuthenticationFilter

AnonymousAuthenticationFilter 是匿名登录过滤器,它位于常用的身份认证过滤器(如 UsernamePasswordAuthenticationFilter、BasicAuthenticationFilter、RememberMeAuthenticationFilter)之后,意味着只有在上述身份过滤器执行完毕后,SecurityContext 依旧没有用户信息,AnonymousAuthenticationFilter 该过滤器才会有意义——基于用户一个匿名身份。
AnonymousAuthenticationFilter 源码分析:

public class AnonymousAuthenticationFilter extends GenericFilterBean implements
InitializingBean {
...
public AnonymousAuthenticationFilter(String key) {this(key, "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
}
...
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {if (SecurityContextHolder.getContext().getAuthentication() == null) {
// 创建匿名登录 Authentication 的信息
SecurityContextHolder.getContext().setAuthentication(createAuthentication((HttpServletRequest) req));
...
}
chain.doFilter(req, res);
}
// 创建匿名登录 Authentication 的信息方法
protected Authentication createAuthentication(HttpServletRequest request) {
AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
principal, authorities);
auth.setDetails(authenticationDetailsSource.buildDetails(request));
return auth;
}
}

4.4 SecurityContextPersistenceFilter

  SecurityContextPersistenceFilter 的两个主要作用便是 request 来临时,创建 SecurityContext 安全上下文信息和 request 结束时清空 SecurityContextHolder。源码后续分析。

小节总结:

. AbstractAuthenticationProcessingFilter: 主要处理登录
. FilterSecurityInterceptor: 主要处理鉴权

总结

  经过上面对核心的 Component、Service、Filter 分析,初步了解了 Spring Security 工作原理以及认证和授权工作流程。Spring Security 认证和授权还有很多负责的过程需要深入了解,所以下次会对认证模块和授权模块进行更具体工作流程分析以及案例呈现。最后以上纯粹个人结合博客和官方文档总结,如有错请指出!

最后可关注公众号【Ccww 笔记】,一起学习。加群,每天会分享干货,还有学习视频领取!

正文完
 0