SecurityContextHolder持有安全上下文(security context)的信息,可以通过SecurityContextHolder.getContext静态方法获取。
当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权等等,这些都被保存在SecurityContextHolder中。
SecurityContextHolder默认使用ThreadLocal 策略来存储认证信息。看到ThreadLocal 也就意味着,这是一种与线程绑定的策略。在web环境下,Spring Security在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。
在Spring Security中,在请求之间存储SecurityContext的责任落在SecurityContextPersistenceFilter上,默认情况下,该上下文将上下文存储为HTTP请求之间的HttpSession属性。它会为每个请求恢复上下文SecurityContextHolder,并且最重要的是,在请求完成时清除SecurityContextHolder。SecurityContextHolder是一个类,他的功能方法都是静态的(static)。
SecurityContextHolder可以设置指定JVM策略(SecurityContext的存储策略),这个策略有三种:
MODE_THREADLOCAL:SecurityContext 存储在线程中。
MODE_INHERITABLETHREADLOCAL:SecurityContext 存储在线程中,但子线程可以获取到父线程中的 SecurityContext。
MODE_GLOBAL:SecurityContext 在所有线程中都相同。
SecurityContextHolder默认使用MODE_THREADLOCAL模式,即存储在当前线程中。
安全上下文信息接口,用户通过Spring Security 的校验之后,验证信息存储在SecurityContext中。
SecurityContext接口只定义了两个方法,实际上其主要作用就是获取Authentication对象(getAuthentication()方法),如果用户未鉴权,那Authentication对象将会是空的。
Authentication是一个接口,用来表示用户认证信息。
该对象主要包含了用户的详细信息(UserDetails)和用户鉴权时所需要的信息,如用户提交的用户名密码、Remember-me Token,或者digest hash值等。按不同鉴权方式使用不同的Authentication实现。
在用户登录认证之前相关信息会封装为一个Authentication具体实现类的对象,在登录认证成功之后又会生成一个信息更全面,包含用户权限等信息的Authentication对象,然后把它保存在 SecurityContextHolder所持有的SecurityContext中,供后续的程序进行调用,如访问权限的鉴定等。
接口中的方法:
从这个接口中,我们可以得到用户身份信息,密码,细节信息,认证信息,以及权限列表,具体的详细解读如下:
getAuthorities(): 用户权限信息(权限列表),通常是代表权限的字符串列表;
getCredentials(): 用户认证信息(密码信息),由用户输入的密码凭证,认证之后会移出,来保证安全性;
getDetails(): 细节信息,Web应用中一般是访问者的ip地址和sessionId;
getPrincipal(): 用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails (UserDetails也是一个接口,里边的方法有getUsername,getPassword等);
isAuthenticated: 获取当前 Authentication 是否已认证;
setAuthenticated: 设置当前 Authentication 是否已认证(true or false)。
官方文档里说过,当用户提交登陆信息时,会将用户名和密码进行组合成一个实例UsernamePasswordAuthenticationToken,而这个类是Authentication的一个常用的实现类,用来进行用户名和密码的认证,类似的还有RememberMeAuthenticationToken,它用于记住我功能。
Authentication的getAuthorities()方法返回一个 GrantedAuthority 对象数组。
GrantedAuthority该接口表示了当前用户所拥有的权限(或者角色)信息,用于配置 web授权、方法授权、域对象授权等。该属性通常由UserDetailsService 加载给 UserDetails。这些信息由授权负责对象AccessDecisionManager来使用,并决定最终用户是否可以访问某资源(URL或方法调用或域对象)。鉴权时并不会使用到该对象。
如果一个用户有几千个这种权限,内存的消耗将会是非常巨大的。
UserDetails存储的就是用户信息,它和Authentication接口类似,都包含了用户名,密码以及权限信息。
而区别就是Authentication中的getCredentials来源于用户提交的密码凭证,而UserDetails中的getPassword取到的则是用户正确的密码信息,认证的第一步就是比较两者是否相同,除此之外,Authentication#getAuthorities是认证用户名和密码成功之后,由UserDetails#getAuthorities传递而来。而Authentication中的getDetails信息是经过了AuthenticationProvider认证之后填充的。
其接口方法含义如下:
getAuthorites:获取用户权限,本质上是用户的角色信息。
getPassword: 获取密码。
getUserName: 获取用户名。
isAccountNonExpired: 账户是否过期。
isAccountNonLocked: 账户是否被锁定。
isCredentialsNonExpired: 密码是否过期。
isEnabled: 账户是否可用。
提到了UserDetails就必须得提到UserDetailsService, UserDetailsService也是一个接口。
接口只有一个方法
loadUserByUsername:用来获取UserDetails。
通常在spring security应用中,我们会自定义一个CustomUserDetailsService来实现UserDetailsService接口,并实现其public UserDetails loadUserByUsername(final String login);方法。我们在实现loadUserByUsername方法的时候,就可以通过查询数据库(或者是缓存、或者是其他的存储形式)来获取用户信息,然后组装成一个UserDetails,(通常是一个org.springframework.security.core.userdetails.User,它继承自UserDetails) 并返回。
用户登陆时传递的用户名和密码也是通过这里这查找出来的用户名和密码进行校验,但是真正的校验不在这里,而是由AuthenticationManager以及AuthenticationProvider负责的,需要强调的是,如果用户不存在,不应返回NULL,而要抛出异常org.springframework.security.core.userdetails.UsernameNotFoundException。
UserDetails这个接口,它代表了最详细的用户信息,这个接口涵盖了一些必要的用户信息字段,我们一般都需要对它进行必要的扩展。它和Authentication接口很类似,比如它们都拥有username,authorities,区分他们也是本文的重点内容之一。Authentication的getCredentials()与UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户正确的密码,认证器其实就是对这两者的比对。Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而形成的。还记得Authentication接口中的getUserDetails()方法吗?其中的UserDetails用户详细信息便是经过了AuthenticationProvider之后被填充的
UserDetailsService和AuthenticationProvider两者的职责常常被人们搞混,UserDetailsService只负责从特定的地方加载用户信息,可以是数据库、redis缓存、接口等。
AuthenticationManager(接口)是认证相关的核心接口,它的作用就是校验Authentication,如果验证失败会抛出AuthenticationException异常。AuthenticationException是一个抽象类,因此代码逻辑并不能实例化一个AuthenticationException异常并抛出,实际上抛出的异常通常是其实现类,如DisabledException,LockedException,BadCredentialsException等。BadCredentialsException可能会比较常见,即密码错误的时候。
这个接口只有一个方法:
authenticate:认证。
方法运行后可能会有三种情况:
验证成功,返回一个带有用户信息的Authentication。
验证失败,抛出一个AuthenticationException异常。
无法判断,返回null。
AuthenticationManager是发起认证的出发点,因为在实际需求中,我们可能会允许用户使用用户名+密码登录,同时允许用户使用邮箱+密码,手机号码+密码登录,甚至,可能允许用户使用指纹登录(还有这样的操作?没想到吧),所以说AuthenticationManager一般不直接认证,AuthenticationManager接口的常用实现类ProviderManager 内部会维护一个List列表,存放多种认证方式,实际上这是委托者模式的应用(Delegate)。也就是说,核心的认证入口始终只有一个:AuthenticationManager,不同的认证方式:用户名+密码(UsernamePasswordAuthenticationToken),邮箱+密码,手机号码+密码登录则对应了三个AuthenticationProvider。其中有一个重要的实现类是ProviderManager。
ProviderManager是上面的AuthenticationManager最常见的实现,它也不自己处理验证,而是将验证委托给其所配置的AuthenticationProvider列表,然后会依次调用每一个 AuthenticationProvider进行认证,或者通过简单地返回null来跳过验证。如果所有实现都返回null,那么ProviderManager将抛出一个ProviderNotFoundException。这个过程中只要有一个AuthenticationProvider验证成功,就不会再继续做更多验证,会直接以该认证结果作为ProviderManager的认证结果。
AuthenticationProvider接口提供了两个方法,一个是真正的认证,另一个是满足什么样的身份信息才进行如上认证。
Spring 提供了几种AuthenticationProvider的实现:
当然也可以自己实现AuthenticationProvider接口来自定义认证。
这里我们基于最常用的DaoAuthenticationProvider来详细解释一下:
Dao正是数据访问层的缩写,也暗示了这个身份认证器的实现思路。主要作用:它获取用户提交的用户名和密码,比对其正确性,如果正确,返回一个数据库中的用户信息(假设用户信息被保存在数据库中)。
在Spring Security中。提交的用户名和密码,被封装成了UsernamePasswordAuthenticationToken,而根据用户名加载用户的任务则是交给了UserDetailsService,在DaoAuthenticationProvider中,对应的方法便是retrieveUser,虽然有两个参数,但是retrieveUser只有第一个参数起主要作用,返回一个UserDetails。还需要完成UsernamePasswordAuthenticationToken和UserDetails密码的比对,这便是交给additionalAuthenticationChecks方法完成的,如果这个void方法没有抛异常,则认为比对成功。
Spring security 定义了一个过滤器链, 当认证请求到达这个链时, 该请求将会穿过这个链条用于认证和授权. 这个链上的可以定义1…N个过滤器, 过滤器的用途是获取请求中的认证信息, 根据认证方式进行路由, 把认证信息传递给对应的认证处理程序进行处理. 下面的示例图显示了Spring security中常用的认证过滤器
不同的过滤器处理不同的认证信息. 例如:
这里我们以最常用表单登录为例子, 用户在登录表单中输入用户名和密码, 并点击确定, 浏览器提交POST请求到服务器, 穿过过滤器链, 被 UsernamePasswordAuthenticationFilter 识别, UsernamePasswordAuthenticationFilter 提取请求中的用户名和密码来创建 UsernamePasswordAuthenticationToken 对象.
组装好的 UsernamePasswordAuthenticationToken 对象被传递给 AuthenticationManagager 的 authenticate 方法进行认证决策.AuthenticationManager 只是一个接口, 实际的实现是 ProviderManager
ProviderManager 有一个配置好的认证提供者列表(AuthenticationProvider), ProviderManager 会把收到的 UsernamePasswordAuthenticationToken 对象传递给列表中的每一个 AuthenticationProvider 进行认证.
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class> authentication);
}
AuthenticationProvider提供了以下的实现类:
CasAuthenticationProvider
JaasAuthenticationProvider
DaoAuthenticationProvider
OpenIDAuthenticationProvider
RememberMeAuthenticationProvider
LdapAuthenticationProvider
上面我们说了, ProviderManager 会把收到的 UsernamePasswordAuthenticationToken 对象传递给列表中的每一个 AuthenticationProvider 进行认证.那到底 UsernamePasswordAuthenticationToken 会被哪一个接收和处理呢?是由supports方法来决定的。
UserDetailsService 获取的对象是一个 UserDetails. 框架中自带一个 User 实现, 但是一般我们需要对 UserDetails 进行定制, 内置的 User 太过简单实际项目无法满足需要.案例说明(基于jpa实现):
@Service
public class JpaReactiveUserDetailsService implements ReactiveUserDetailsService {
private UserRepository userRepository;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
/**
* @param s 用户名
* @return Mono
*/
@Override
public Mono findByUsername(String s) {
// 从用户Repository中获取一个User Jpa实体对象
Optional optionalUser = userRepository.findByUsername(s);
if (!optionalUser.isPresent()) {
return Mono.empty();
}
User user = optionalUser.get();
// 填充权限
List authorities = new ArrayList<>();
for (Role role : user.getRoles()) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
// 返回 UserDetails
return Mono.just(new org.springframework.security.core.userdetails.User(
user.getUsername(), user.getPassword(), authorities
));
}
}
@Repository
public interface UserRepository extends JpaRepository {
User findByEmail(String email);
@Override
void delete(User user);
Optional findByUsername(String username);
}
如果认证成功(用户名,密码完全正确), AuthenticationProvider 将会返回一个完全有效的 Authentication 对象(UsernamePasswordAuthenticationToken). 否则抛出 AuthenticationException 异常.完全有效的 Authentication 对象定义如下:
authenticated属性为 true
已授权的权限列表(GrantedAuthority列表)
用户凭证(仅用户名)
认证完成后, AuthenticationManager 将会返回该认证对象(UsernamePasswordAuthenticationToken)返回给过滤器
相关的过滤器获得一个认证对象后, 把它存储在安全上下文中(SecurityContext) 用于后续的授权判断(比如查询,修改等操作).
SecurityContextHolder.getContext().setAuthentication(authentication);
实际上真正来做验证操作的是一个个的AuthenticationProvider,所以如果要自定义验证方法,只需要实现一个自己的AuthenticationProvider然后再将其添加进ProviderManager里就行了。
@Component
public class CustomAuthenticationProvider
implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String name = authentication.getName();
String password = authentication.getCredentials().toString();
if (shouldAuthenticateAgainstThirdPartySystem()) {
// use the credentials
// and authenticate against the third-party system
return new UsernamePasswordAuthenticationToken(
name, password, new ArrayList<>());
} else {
return null;
}
}
@Override
public boolean supports(Class> authentication) {
return authentication.equals(
UsernamePasswordAuthenticationToken.class);
}
}
其中的supports()方法接受一个authentication参数,用来判断传进来的authentication是不是该AuthenticationProvider能够处理的类型。
现在再将刚创建的AuthenticationProvider在与ProviderManager里注册,所有操作就完成了。
@Configuration
@EnableWebSecurity
@ComponentScan("org.baeldung.security")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomAuthenticationProvider authProvider;
@Override
protected void configure(
AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and()
.httpBasic();
}
}
一旦认证成功,我们可以继续进行授权,授权是通过AccessDecisionManager来实现的。框架有三种实现,默认是AffirmativeBased,通过AccessDecisionVoter决策,有点像ProviderManager委托给AuthenticationProviders来认证。
用户可以享受哪一种权限可以自己配置或者读取数据库设置
参考文章
Spring Security验证流程剖析及自定义验证方法
Spring Security详解(一)认证之核心组件和服务
spring security系列一:核心组件