Spring Security 中重要对象汇总

本文正在参加「金石计划 . 瓜分6万现金大奖」

日积月累,水滴石穿 😄

前言

已经写了好几篇关于 Spring Security 的文章了,相信很多读者还是对 Spring Security 的云里雾里的。这是因为对 Spring Security 中的对象还不了解。本文就来介绍介绍一下常用对象。

认证流程

SecurityContextHolder

用户认证通过后,为了避免用户的每次操作都进行认证,可将用户的信息保存在会话中。Spring Security 提供会话管理,认证通过后将身份信息放入 SecurityContextHolder 上下文,SecurityContext 与当前线程进行绑定,方便获取用户身份。

// 获取当前登录的用户信息
Authentication authentication = 
SecurityContextHolder.getContext().getAuthentication();
复制代码

AuthenticationManager

认证管理器,AuthenticationManager 是认证相关的核心接口,是发起认证的入口,用于处理认证请求。接口只提供了一个认证方法,方法接收一个未通过认证 Authentication 对象,返回一个通过认证的 Authentication 对象。最常见的实现是ProviderManager

public interface AuthenticationManager {
    Authentication authenticate(Authentication var1) throws AuthenticationException;
}
复制代码

ProviderManager

提供商管理器,ProviderManagerAuthenticationManager 的一个实现类,提供了基本的认证逻辑和方法。它其中包含了一个 List 的 AuthenticationProvider 的属性,该属性存放多种认证方式!为什么需要这个属性呢?当Spring Security默认提供的认证方式不能满足需求时,就可以通过 AuthenticationProvider 接口来扩展出其他认证方式,比如邮箱+验证码,手机号码+验证码登录。

AuthenticationProvider

AuthenticationProvider(身份验证提供者),可以将多个AuthenticationProvider实例添加到ProviderManager中。其每个AuthenticationProvider可以执行特定的 Authentication (身份验证)类型。例如:DaoAuthenticationProvider支持基于用户名+密码的 UsernamePasswordAuthenticationToken 身份验证。也可以自定义认证方式,比如自定义EmailVerificationCodeAuthenticationProvider支持邮箱 + 验证码的 EmailVerificationCodeAuthenticationToken 身份验证。

public interface AuthenticationProvider {

    Authentication authenticate(Authentication authentication) throws AuthenticationException;

    boolean supports(Class<?> authentication);
}
复制代码

该接口中有两个方法,如下:

  • authenticate() 方法接收一个未通过认证 Authentication 对象,返回一个通过认证的 Authentication 对象。可以实现 authenticate() 方法来自定义身份验证逻辑。

  • supports(Class<?> authentication) 方法接收一个 Authentication(身份验证) 对象,如果 AuthenticationProvider 支持指定的身份验证对象,则返回 true。 但是返回 true 并不保证 AuthenticationProvider 能够对提供的 Authentization 类实例进行身份验证。它只是表明它可以支持对其进行更深入的验证。AuthenticationProvider 仍可以从 authenticate() 方法返回 null,以尝试其他的 AuthentitationProvider 进行验证。

Authentication

Authentication(身份验证) 接口是 Spring Security 中身份验证流程的顶级接口,该接口定义了如下方法:

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

    Object getCredentials();

    Object getDetails();

    Object getPrincipal();

    boolean isAuthenticated();

    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
复制代码

方法含义如下:

方法描述
getAuthorities获取登录用户的权限列表
getCredentials获取凭据。用户密码登录,这个字段就是密码信息,在认证过后通常会被移除,用于保障安全。如果是手机号验证码登录,那这个字段存的就是验证码
getDetails包含了一些认证时的信息,默认的实现为 WebAuthenticationDetails,记录了访问者的远程地址和sessionId的值。
getPrincipal身份信息,默认情况下返回的是 UserDetails的实例
isAuthenticated是否通过认证,通过认证为 true
setAuthenticated设置是否已认证
getName用户名

具体响应内容可以参考如下:

{
	"authorities": [
		{
			"authority": "ROLE_admin"
		},
		{
			"authority": "ROLE_user"
		}
	],
	"details": {
		"remoteAddress": "0:0:0:0:0:0:0:1",
		"sessionId": "D77AF630A476DEE7A2A75B1D751C4CF1"
	},
	"authenticated": true,
	"principal": {
		"password": null,
		"username": "cxyxj",
		"authorities": [
			{
				"authority": "ROLE_admin"
			},
			{
				"authority": "ROLE_user"
			}
		],
		"accountNonExpired": true,
		"accountNonLocked": true,
		"credentialsNonExpired": true,
		"enabled": true
	},
	"credentials": null,
	"name": "cxyxj"
}
复制代码

Authentication 本身是一个接口,它有很多实现类:

image.png 在众多的实现类中,我们最常用的就是 UsernamePasswordAuthenticationToken(用户名密码身份验证令牌),但是这个类就只有简单的50行左右的代码,其中有两个属性,principal 代表用户名,credentials 代表密码。还有两个构造方法,一个是代表未认证的,一个是代表已认证的;三个set、get方法,一个擦除凭据方法。该类继承了 AbstractAuthenticationToken,其大部分逻辑在父类中,当然父类的逻辑也非常简单。 所以 UsernamePasswordAuthenticationToken(用户名密码身份验证令牌)的作用就是将用户输入的用户名和密码进行封装,并供给 AuthenticationManager 进行验证。

UserDetails

这个接口定义了用户的核心信息,比如用户名、密码、账号是否过期、是否锁定等!默认实现类org.springframework.security.core.userdetails.User。在 Spring Security 中,如果自定义认证逻辑时,需要实现该接口进行扩展,来保存自己系统的用户信息。接口定义如下方法:

方法描述
getAuthorities获取用户权限
getPassword获取用户密码
getUsername获取用户名
isAccountNonExpired账户是否未过期,true:未过期,false:过期
isAccountNonLocked账户是否未锁定,true:未锁定,false:锁定
isCredentialsNonExpired凭证(密码)是否未过期,true:未过期,false:过期
isEnabled账户是否启用,true:启用,false:禁用

一个正常能登录的账号,四个状态都是为 true 的。

UserDetailsService

在 Spring Security 中,什么也不进行配置时,账号和密码是由 Spring Security 自动生成的。 但在实际的项目中账号、密码是从数据库中查询出来的。所以我们需要自定义认证逻辑。 此时需要实现 UserDetailsService 接口。而 UserDetailsService 接口中只定义了一个方法,作用是根据用户名加载用户,获得 UserDetails 对象。

public interface UserDetailsService {

        // 按用户名加载用户
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
复制代码

可以参考自定义逻辑如下:

@Override
public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
    LambdaQueryWrapper<SysUser> queryWrapper = Wrappers.lambdaQuery();
    queryWrapper.eq(SysUser::getAccount, account);
    // 根据用户名查询用户
    SysUser sysUsers = sysUserMapper.selectOne(queryWrapper);
    if (Objects.isNull(sysUsers)) {
        Assert.isTrue(true,"用户名或者密码错误");
    }
    // 获得用户角色信息
    List<String> roles = sysUserMapper.selectByUserId(sysUsers.getUserId());
    // 构建 SimpleGrantedAuthority 对象
    List<SimpleGrantedAuthority> authorities = roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
    return new SysUserDetails(sysUsers, authorities);
}
复制代码

除了需要手动实现 UserDetailsService 接口的方式外,Spring Security 也内置了几种方式。 我们来看下 UserDetailsService 都有哪些实现类:

image.png

  • InMemoryUserDetailsManager:内存用户,这种方式在学习 Spring Security 的时候,用的非常多。

  • JdbcUserDetailsManager:通过 JDBC 的方式将数据库和 Spring Security 连接起来。它自己提供了一个数据库脚本,脚本路径如下:org/springframework/security/core/userdetails/jdbc/users.ddl。脚本的内容呢,应该是不符合项目实际开发的,所以这个也是在学习的时候可以使用使用。

PasswordEncoder

PasswordEncoder 接口用于执行密码的单向转换,以便安全地存储密码。

InteractiveAuthenticationSuccessEvent

身份验证成功后,发布一个名为InteractiveAuthenticationSuccessEvent的事件通知给到应用上下文,用于告知身份验证已经成功。

FilterChainProxy

在 Spring Security 的默认配置中,将创建一个名为 springSecurityFilterChain 的 servlet 过滤器作为bean。默认情况下,Spring Security 内置了一个过滤链,链中有 15 个过滤器。 image.png 想要了解更多请前往: 深入理解 FilterChainProxy【源码篇】

ExceptionTranslationFilter

ExceptionTranslationFilter 异常转换过滤器位于整个 springSecurityFilterChain 的后方,用来转换整个链路中出现的异常。此过滤器本身不处理异常,而是将认证过程中出现的异常交给内部维护的一些类去处理,一般处理两大类异常:AccessDeniedException 已登录无权限访问异常和 AuthenticationException 未认证访问异常。

HeaderWriterFilter

用来给http响应添加一些Header,比如X-Frame-Options, X-XSS- Protection*,X-Content-Type-Options.

CsrfFilter

用于防止csrf攻击(跨站点请求伪造(Cross- site request forgery))。

LogoutFilter

处理注销的过滤器。

RequestCacheAwareFilter

内部维护了一个RequestCache,用于缓存request请求。

SecurityContextHolderAwareRequestFilter

对ServletRequest进行了一次包装,使得request 具有更加丰富的API。

SessionManagementFilter

和session相关的过滤器,内部维护了一个 SessionAuthenticationStrategy,两者组合使用,常用来防止会话固定攻击保护( session- fixation protection attack ),以及限制同一用户开启多个会话的数量。

AnonymousAuthenticationFilter

匿名身份过滤器,spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。

UsernamePasswordAuthenticationFilter

表单提交了username和password参数,被封装成token进行一系列的认证,便是主要通过这个过滤器完成的。在表单认证的流程中,这是最最关键的过滤器。

AbstractAuthenticationProcessingFilter

翻译为:抽象身份验证处理过滤器,这是一个抽象类,定义了认证处理的过程。是一个模板类。默认的实现为 UsernamePasswordAuthenticationFilter,根据用户名、密码进行身份验证,如果需要自定义身份验证,比如手机验证码登录,就需要继承该类。

授权

当用户访问 Spring Security 中一个受保护的资源时,需要使用投票器和表决机制,投票器根据用户的角色投出赞成或者反对票,表决方式则根据投票器的结果进行表决。

AccessDecisionManager

访问决策管理器,AccessDecisionManagerAbstractSecurityInterceptor调用。AccessDecisionManager 采用投票的方式来确定是否能够访问受保护资源。 AccessDecisionManager 中包含的多个 AccessDecisionVoter,Voter 将会被用来对Authentication是否有权访问受保护对象进行投票, AccessDecisionManager 根据投票结果,做出最终决策。

image.png AccessDecisionManager 访问决策管理器还有三个子类决策器,分别是:

  • AffirmativeBased:存在多个投票器时,有一个投票器同意,则请求就允许访问,也就是一票通过;默认使用的决策器。
  • UnanimousBased:存在多个投票器时,如果有投票器拒绝,则请求不允许访问,也就是一票否决。
  • ConsensusBased:存在多个投票器时,大多数投票器同意。则请求就允许访问,也就是少数服从多数。如果是平局,则看 allowIfEqualGrantedDeniedDecisions 的值来判断是否通过,在默认情况下allowIfEqualGrantedDeniedDecisions值是true,也就是说平票的情况下,请求允许访问。

注意:不论是哪个决策器,如果所有投票器全部弃权则表示通过。

AccessDecisionVoter

翻译为:投票机制/投票器。在 Spring Security 中,投票机制是由 AccessDecisionVoter 接口来定义的,它有许多的实现:

image.png 实现有好多种,我们可以选择其中一种或多种投票机制,也可以自定义投票机制,默认的投票机制是 WebExpressionVoter。

public interface AccessDecisionVoter<S> {
	
	int ACCESS_GRANTED = 1;
	int ACCESS_ABSTAIN = 0;
	int ACCESS_DENIED = -1;


	boolean supports(ConfigAttribute attribute);

	boolean supports(Class<?> clazz);

	
	int vote(Authentication authentication, S object,
			Collection<ConfigAttribute> attributes);
}
复制代码
  • 三个常量含义分别为:1 表示同意;0 表示弃权;-1 表示拒绝。
  • 两个 supports 方法用来判断投票器是否支持当前请求。
    1. vote 则是具体的投票方法;在不同的实现类中实现。authentication 表示当前登录主体;object 表示正在调用的受保护接口;attributes 表示当前所访问的接口所需要的角色集合。

image.png

  • RoleVoter:判断当前请求是否具备该接口所需要的角色。
  • RoleHierarchyVoter 是 RoleVoter 的一个子类,在 RoleVoter 角色判断的基础上,引入了角色分层管理,也就是角色继承。
  • WebExpressionVoter:基于表达式权限控制
  • Jsr250Voter:处理 Jsr-250 权限注解的投票器,如 @PermitAll@DenyAll 等
  • PreInvocationAuthorizationAdviceVoter:使用 @PreFilter 和 @PreAuthorize 注解处理的权限,通过 PreInvocationAuthorizationAdvice 来授权。

AbstractSecurityInterceptor

根据注释翻译了一下:为安全对象实现安全拦截的抽象类,干的事情说白点就是对未放行的资源,根据用户的权限来控制是否能访问的拦截器。由于这是一个抽象类,所以只定义了一些逻辑方法,具体执行都是子类去调用的。

image.png

FilterSecurityInterceptor

从 FilterChainProxy 章节中的截图来看,FilterSecurityInterceptor 位于 Spring Security Filter Chain 中的最后一个 Filter。这是一个过滤器,它会拦截HTTP请求,进行鉴权处理。

MethodSecurityInterceptor

它还实现了 MethodInterceptor,所以这是一个方法拦截器,基于 Spring AOP 实现了方法拦截,对方法进行鉴权处理。比如在方法上标注了@RolesAllowed@PermitAll@PreAuthorize等等注解。

AspectJMethodSecurityInterceptor

继承了 MethodSecurityInterceptor,基于 Aspectj 实现方法拦截。

区别

  • 一个是过滤器,一个是方法拦截器,所以他们拦截对象是不同,一个是拦截Http请求,一个是拦截方法。
  • 两者都调用了父类的beforeInvocation方法,但是传入的参数是不一样的,FilterSecurityInterceptor传入的是FilterInvocation,MethodInterceptor传入的是MethodInvocation。
  • 两者维护的 SecurityMetadataSource 不一样,MethodSecurityInterceptor 中维护的是MethodSecurityMetadataSource,FilterSecurityInterceptor维护的是 FilterInvocationSecurityMetadataSource。

SecurityMetadataSource

获取授权配置的接口。可以自定义实现该接口,比如从数据库中加载ConfigAttribute。在 Spring Security中,给该接口提供了两个子类,继承图如下: image.png

  • MethodSecurityMetadataSource:由Spring Security Core定义,用于表示安全对象是方法调用(MethodInvocation)的安全元数据源;存放的是所有类和方法,会根据当前执行的类和方法,去内存中遍历,查询到当前执行方法配置的权限注解,然后对其进行授权判断。

image.png

  • FilterInvocationSecurityMetadataSource:由Spring Security Web定义,用于表示安全对象是Web请求(FilterInvocation)的安全元数据源;存放的是 HttpSecurity 配置类中配置的授权规则。

image.png 配置如下:

http.authorizeRequests()
    // 如果用户具备 admin 权限,就允许访问。
    .antMatchers("/cxyxj/**").hasAuthority("ROLE_admin")
// 如果用户具备给定权限中某一个,就允许访问。
    .antMatchers("/admin/demo").hasAnyAuthority("ROLE_admin", "ROLE_System")
// 如果用户具备 user 权限,就允许访问。注意不需要手动写 ROLE_ 前缀,写了会报错
    .antMatchers("/security/**").hasRole("user")
//如果请求是指定的 IP 就允许访问。
    .antMatchers("/admin/demo").hasIpAddress("192.168.64.5")
    .anyRequest()   //其他请求
    .authenticated(); //需要认证才能访问
复制代码

参考文献

Spring Security 权限管理的投票器与表决机制


  • 如你对本文有疑问或本文有错误之处,欢迎评论留言指出。如觉得本文对你有所帮助,欢迎点赞 + 收藏。
来源:https://juejin.cn/post/7171342453138522120
posted @ 2022-12-18 23:42  程序员小明1024  阅读(82)  评论(0编辑  收藏  举报