什么是Spring Security验证?
让我们考虑一个大家都很熟悉的标准的验证场景。
- 提示用户输入用户名和密码进行登录。
- 该系统 (成功) 验证该用户名的密码正确。
- 获取该用户的环境信息 (他们的角色列表等).
- 为用户建立安全的环境。
- 用户进行,可能执行一些操作,这是潜在的保护的访问控制机制,检查所需权限,对当前的安全的环境信息的操作。
而对于Spring Security来说,假设是用户名密码登陆,那执行顺序就是:
下面来逐个分析一下每个类
这一节主要介绍一些Spring Security中核心的java类,他们之间的依赖,构建起了整个框架。
SecurityContextHolder 是最基本的对象,它负责存储当前安全上下文信息。即保存着当前用户是什么,是否已经通过认证,拥有哪些权限。。。等等。SecurityContextHolder默认使用ThreadLocal策略来存储认证信息,意味着这是一种与线程绑定的策略。在Web场景下的使用Spring Security,在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。
获取有关当前用户的信息
Spring Security使用一个Authentication对象来表示这些信息。通常不需要自己创建一个Authentication对象,但可以在程序的任何一个地方获取到用户信息,下面时官方提供的获取用户信息:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
首先看一下源码:
package org.springframework.security.core;// <1>
public interface Authentication extends Principal, Serializable { // <1>
Collection<? extends GrantedAuthority> getAuthorities(); // <2>
Object getCredentials();// <2>
Object getDetails();// <2>
Object getPrincipal();// <2>
boolean isAuthenticated();// <2>
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
<1> Authentication继承自来自于java.security包的Principal类,而本身又是spring.security包中的接口。也就是说,Authentication是Spring Security中最高级别的认证。
<2> 从这个接口中,我们可以得到用户身份信息,密码,细节信息,认证信息,以及权限列表,具体的详细解读如下:
官方文档里说过,当用户提交登陆信息时,会将用户名和密码进行组合成一个实例UsernamePasswordAuthenticationToken,而这个类是Authentication的一个常用的实现类,用来进行用户名和密码的认证,类似的还有RememberMeAuthenticationToken,它用于记住我功能。
上文提到了UserDetails接口,它代表了最详细的用户信息,这个接口涵盖了一些必要的用户信息字段,具体的实现类对它进行了扩展,首先来看一下源码:
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
它和Authentication接口类似,都包含了用户名,密码以及权限信息,而区别就是Authentication中的getCredentials来源于用户提交的密码凭证,而UserDetails中的getPassword取到的则是用户正确的密码信息,认证的第一步就是比较两者是否相同,除此之外,Authentication中的getAuthorities是认证用户名和密码成功之后,由UserDetails中的getAuthorities传递而来。而Authentication中的getDetails信息是经过了AuthenticationProvider认证之后填充的。
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetailsService 只有一个方法,就是从特定的地方(一般是从数据库中)加载用户信息,仅此而已。常用的实现类由JdbcDaoImpl和InMemoryUserDetailsManager,前者从数据库中加载用户,后者从内存中。还可以自己实现UserDetailsService。
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
AuthenticationManager接口只包含一个方法,那就是认证,它是认证相关的核心接口,也是发起认证的出发点。实际业务中可能根据不同的信息进行认证,所以Spring推荐通过实现AuthenticationManager接口来自定义自己的认证方式.Spring提供了一个默认的实现,ProviderManager。
ProviderManager与认证相关的源码:
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
// 维护一个AuthenticationProvider列表
private List<AuthenticationProvider> providers = Collections.emptyList();
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
// 依次认证
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
...
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列表,按照顺序进行依次认证,每个provider都会尝试认证,或者通过简单地返回null来跳过验证。如果所有实现都返回null,那么ProviderManager将抛出一个ProviderNotFoundException。
到这里,认证相关的核心类其实都已经基本介绍完毕了:身份信息的存放容器SecurityContextHolder,身份信息的抽象Authentication,身份认证器AuthenticationManager及其认证流程。下面来介绍下AuthenticationProvider接口的具体实现。
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> authentication);
}
AuthenticationProvider接口提供了两个方法,一个是真正的认证,另一个是满足什么样的身份信息才进行如上认证。
Spring 提供了几种AuthenticationProvider的实现:
当然也可以自己实现AuthenticationProvider接口来自定义认证。
这里我们基于最常用的DaoAuthenticationProvider来详细解释一下:
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
//密码加解密算法
private PasswordEncoder passwordEncoder;
//用户信息dao
private UserDetailsService userDetailsService;
//检查用户名和密码是否匹配
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
//用户提交的密码凭证
String presentedPassword = authentication.getCredentials().toString();
//比较两个密码
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
//获取用户信息
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
}
}
在Spring Security中。提交的用户名和密码,被封装成了UsernamePasswordAuthenticationToken,而根据用户名加载用户的任务则是交给了UserDetailsService,在DaoAuthenticationProvider中,对应的方法便是retrieveUser,返回一个UserDetails。然后再将UsernamePasswordAuthenticationToken和UserDetails密码的比对,这便是交给additionalAuthenticationChecks方法完成的,如果这个void方法没有抛异常,则认为比对成功。
参考: https://www.cnkirito.moe/spring-security-1/