SpringSecurity认证流程分析
重要组件
SecurityContext
上下文对象,Authentication(认证)对象会放在里面SecurityContextHolder
用于拿到上下文对象的静态工具类Authentication
认证接口,定义了认证对象的数据形式(数据格式)AuthenticationManager
用于校验Authentication,返回一个认证后的Authentication对象
SecurityContext
对象负责存放认证后的对象数据
public interface SecurityContext extends Serializable {
// 获取Authentication对象
Authentication getAuthentication();
// 放入Authentication对象
void setAuthentication(Authentication authentication);
}
主要的作用就是get and set
SecurityContextHolder
用于获取上下文对象的静态工具类。用于存储上下文对象的信息。包括当前的用户是谁,是否认证、角色权限.... 都被保存在SecurityContextHolder
中
public class SecurityContextHolder {
//清除上下文数据
public static void clearContext() {
strategy.clearContext();
}
//获取上下文数据
public static SecurityContext getContext() {
return strategy.getContext();
}
//放入上下文数据
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
}
主要的作用就是获取和放入SecurityContext
Authentication
认证接口,定义了认证对象的数据形式
public interface Authentication extends Principal, Serializable {
//获取用户权限
Collection<? extends GrantedAuthority> getAuthorities();
//获取证明用户的信息(密码等...)
Object getCredentials();
//用户额外信息(用户表信息)
Object getDetails();
//获取用户身份信息,未认证获取的是用户名,认证后获取的是UserDetails
Object getPrincipal();
//获取当前Authentication是否已认证
boolean isAuthenticated();
//设置当前Authentication是否已认证(true or false)
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
Authentication
只是定义了一种在SpringSecurity
进行认证的数据的数据形式应该是怎么样的。
AuthenticationManager
【接口】是认证相关的核心接口,它定义了一个认证方法,它将一个未认证的Authentication
传入,返回一个已认证的Authentication
,它的默认实现类是ProviderManager
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
ProviderManager
是 AuthenticationManager
的默认实现类,其实很大一部分工具类都是围绕着这个 ProviderManager
实现类来的,由他衍生出来的 AuthenticationProvider
接口
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
// 这里维护着一个 AuthenticationProvider 列表,存放多种认证方式,实际上这是委托者模式的应用(Delegate)。
// 也就是说,核心的认证入口始终只有一个:AuthenticationManager
// 例如下面不同的认证方式,对应了三个 AuthenticationProvider:
// 1、用户名 + 密码(UsernamePasswordAuthenticationToken),
// 2、邮箱 + 密码,
// 3、手机号码 + 密码登录
// 在默认策略下,只需要通过一个 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;
// ProviderManager 中的 List(providers),会依照次序去认证,认证成功则立即返回,
// 若认证失败则返回 null,下一个 AuthenticationProvider 会继续尝试认证,如果所有
// 认证器都无法认证成功,则 ProviderManager 会抛出一个 ProviderNotFoundException 异常。
for (AuthenticationProvider provider : getProviders()) {
// 这个 supports 方法用来判断此AuthenticationProvider 是否支持当前的 Authentication 对象
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;
}
}
AuthenticationProvider
接口内部有两个实现类
public interface AuthenticationProvider {
//负责认证
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
//负责判断此AuthenticationProvider是否支持当前的 Authentication 对象。
boolean supports(Class<?> authentication);
}
就是输入一个凭证,输出一个Principal,输入输出都是封装在Authenticaiton
里面的。
输入(credentials) ---->
AuthenticationProvider.authenticate()
输出(principal)<-------
而且可以有多种类型的认证方式。
那这些 AuthenticationProvider
需要做什么工作呢?首先通过username
取得数据库里面的用户数据,所以这时就可以使用这个UserDetailService.loadUserByUserName()
来取得数据。这个UserDetailService
会将用户数据填充到Authentication
里面去【Authentication.getCredentials()
】。
最后的最后,为了让其他的过滤器也能够获取到这个认证的Authentication
,SpringSecurity
会将其存储到上下文对象中。
DaoAuthenticationProvider
就是AuthenticationProvider
的实现类。
Dao
正是数据访问层的缩写,也暗示了这个身份认证器的实现思路,用户前台提交了用户名和密码,而数据库中保存了用户名和密码,认证便是负责比对同一个用户名,提交的密码和保存的密码是否相同便是了。
在SpringSecurity
中。提交的用户名和密码被封装成了UsernamePasswordAuthenticationToken
,而根据用户名价值用户的认为则是提交给了UserDetailsService
。
如何取得用户?
在DaoAuthenticationProvider
中,对应的根据用户名加载用户的方法便是retrieveUser
。
// 虽然有两个参数,但是 retrieveUser 只有第一个参数(username)起主要作用,返回一个 UserDetails。
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
对比密码
还需要完成UsernamePasswordAuthenticationToken
和UserDetails
密码的比对,这便是交给 additionalAuthenticationChecks
方法完成的。
这个方法是在父类 AbstractUserDetailsAuthenticationProvider 的 authenticate 方法中被调用的,如果这个 void 方法没有抛异常,则认为比对成功。
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
//比对密码的过程,用到了 PasswordEncoder 和 SaltSource
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
总之就是 DaoAuthenticationProvider
它获取用户提交的用户名和密码,比对其正确性,如果正确,返回一个数据库中的用户信息(假设用户信息被保存在数据库中)。
UserDetails
它代表了最详细的用户信息,这个接口涵盖了一些必要的用户信息字段,具体的实现类对它进行了扩展。
public interface UserDetails extends Serializable {
//访问权限信息
Collection<? extends GrantedAuthority> getAuthorities();
//获取密码
String getPassword();
//获取用户名
String getUsername();
//判断是没过期
boolean isAccountNonExpired();
//是否没有被锁定
boolean isAccountNonLocked();
//是否没有超时
boolean isCredentialsNonExpired();
//用户是否可用
boolean isEnabled();
}
Authentication 的 getCredentials()
与 UserDetails
中的 getPassword()
需要被区分对待,前者是用户提交的密码凭证,后者是用户正确的密码,认证器其实就是对这两者的比对。
Authentication 中的 getAuthorities()
实际是由 UserDetails
的 getAuthorities()
传递而形成的。
UserDetailsService
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetailsService
它纯粹是一个用于用户数据的 DAO
,除了向框架内的其他组件提供该数据之外,没有其他功能。常见的类型有,从数据库加载,从内存中加载。
总体认证流程:
- 首先是一个请求带着身份信息进来,用户名和密码被过滤器获取到,封装成
Authentication
默认情况下是UsernamePasswordAuthenticationToken
这个实现类。 - 这个
Authentication
经过AuthenticationManager
的认证成功后,AuthenticationManager
身份管理器返回一个被填充满了信息的Authentication
实例。 SecurityContextHolder
安全上下文容器将上面充满信息的Authentication
通过SecurityContextHolder.getContext().setAuthentication(...)
方法设置到SecurityContext
上下文对象中。- 从
Authentication
对象中拿到我们的UserDetails
对象,之前我们说过,认证后的Authentication
对象调用它的getPrincipal()
方法就可以拿到我们先前数据库查询后组装出来的UserDetails
对象,然后创建 token。 - 把
UserDetails
对象放入缓存中,方便后面过滤器使用。
这样的话就算完成了,,因为主要认证操作都会由 AuthenticationManager.authenticate()
帮我们完成。
authenticate
方法
这个 authenticate
方法就是认证的核心方法,它位于 AbstractUserDetailsAuthenticationProvider
这个抽象类里面,而大部分具体的 AuthenticationProvider
都是先继承自这个抽象类。
// AbstractUserDetailsAuthenticationProvider
// 注意这个 AbstractUserDetailsAuthenticationProvider 类是上面的 DaoAuthenticationProvider 的父类,
// 认证部分都是在这个父类做的
public Authentication authenticate(Authentication authentication) {
// 校验未认证的Authentication对象里面有没有用户名
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
// 从缓存中去查用户名为XXX的对象
UserDetails user = this.userCache.getUserFromCache(username);
// 如果没有就进入到这个方法
if (user == null) {
cacheWasUsed = false;
try {
// 调用我们重写 UserDetailsService 的 loadUserByUsername 方法
// 拿到我们自己组装好的 UserDetails 对象
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
} catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
} else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
// 校验账号是否禁用
preAuthenticationChecks.check(user);
// 校验数据库查出来的密码,和我们传入的密码是否一致
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
}
逻辑模型
public static void main(String[] args) throws Exception {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
AuthenticationManager am = new SampleAuthenticationManager();
while(true) {
System.out.println("请输入您的用户名:");
String name = in.readLine();
System.out.println("请输入您的密码:");
String password = in.readLine();
try {
// 1、封装一个 UsernamePasswordAuthenticationToken 对象
Authentication request = new UsernamePasswordAuthenticationToken(name, password);
// 2、经过 AuthenticationManager 的认证,如果认证失败会抛出一个 AuthenticationException 错误
Authentication result = am.authenticate(request);
// 3、将这个认证过的 Authentication 填入 SecurityContext 里面
SecurityContextHolder.getContext().setAuthentication(result);
break;
} catch(AuthenticationException e) {
System.out.println("认证失败:" + e.getMessage());
}
}
System.out.println("认证成功: Security context contains:\n" +
SecurityContextHolder.getContext().getAuthentication());
}
// 关键认证部分
@Override
public Authentication authenticate(Authentication auth) throws AuthenticationException {
// getCredentials 返回的是密码,这里随便写了,直接用户名和密码一致就算登陆成功
if (auth.getName().equals(auth.getCredentials())) {
// 认证成功返回一个已经认证的 UsernamePasswordAuthenticationToken 的对象,并把这个用户的权限填入
return new UsernamePasswordAuthenticationToken(auth.getName(),
auth.getCredentials(), AUTHORITIES);
}
throw new BadCredentialsException("Bad Credentials");
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性