为了能够早日攻克JWT验证,要好好学一下Spring Security。
- Spring Security认证原理
- Authentication
- 如何验证用户身份
- AuthenticationManager
- DaoAuthenticationProvider
- UserDetails
- 认证流程图
Spring Security认证原理
已经打算要通读文档的我突然发现了一篇介绍Spring Security结构的博客,顿感柳暗花明,赶快打开IDE来对着做一番。
无论是之前的Spring in Aciton 4还是5,对于Spring Security真的是非常实战的讲法,直接上代码告诉你如何使用,对于结构讲的很少。
所以现在需要自己实现JWT认证的我,如果看两本书就远远不够了,必须更深入的了解Spring Security才能来编写属于自己的认证。
核心组件
SecurityContextHolder
SecurityContextHolder
,这个可以理解成类似Spring IOC容器一样的安全容器。这个容器使用ThreadLocal来保存信息,一个线程一个,从其中可以取出所有的安全相关的内容,比如用户信息,权限等。
在所有Spring内部代码,类似获取容器上下文,也可以获取这个安全上下文来取得其中的内容:
SecurityContextHolder.getContext().getAuthentication().getPrincipal()
.getContext()
拿到上下文,之后的Authentication
和Principal
是什么
Authentication
这是一个接口:
package org.springframework.security.core;
import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;
public interface Authentication extends Principal, Serializable {
//获取权限
Collection<? extends GrantedAuthority> getAuthorities();
//获取密码,一般在认证后会被移除
Object getCredentials();
//得到WebAuthenticationDetails的实现,记录了访问者的ip地址和sessionid,如果是JWT,其中的内容就无用了
Object getDetails();
//非常关键的一个方法,返回Principal,什么是Principal已经知道了,在Spring Security里就是Authenticate的实现类
Object getPrincipal();
//判断当前身份已经通过认证了吗?
boolean isAuthenticated();
//设置当前身份为已经通过认证
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
可以看到继承了Principal
,点进去java.security.Principal
看源码的解释:
This interface represents the abstract notion of a principal, which
can be used to represent any entity, such as an individual, a
corporation, and a login id.
很显然,这表示一个认证的实体,可以对应一个用户,一个公司,一个登陆ID等,实际上就是一个身份。
所以Authentication
是一个身份的抽象。
这个接口是Spring Security提供的,所以实现了这个接口的类,都相当于一个身份。
现在可以开始创建一个只带有Web和Spring Security依赖的Spring Boot项目来尝试一下了。先配置好所有URL都可以任意访问。
在任何一个REST控制器内输出这个对象:
@RestController
@RequestMapping("/objects")
public class BaseController {
@GetMapping("/authentication")
public Authentication getMessage() {
return SecurityContextHolder.getContext().getAuthentication();
}
}
可以看到结果是:
{
"authorities": [
{
"authority": "ROLE_ANONYMOUS"
}
],
"details": {
"remoteAddress": "0:0:0:0:0:0:0:1",
"sessionId": null
},
"authenticated": true,
"principal": "anonymousUser",
"keyHash": -2031280993,
"credentials": "",
"name": "anonymousUser"
}
能看到其中的一些属性,其中的角色是一个匿名用户。
接口中还有一个方法:
void setAuthenticated(boolean var1) throws IllegalArgumentException;
这个方法很重要,是设置这个身份对象,是否通过认证。Spring Security对于通过认证的身份对象,才会放行。
如何验证用户身份
在继续看其他类之前,就有必要看一下完整的流程了,这里我就根据自己的研究整理了一下:
- 过滤器
UsernamePasswordAuthenticationFilter
拿到POST请求中的用户名和密码,然后把用户名和密码封到Authentication
的实现类中,这个实现类一般是UsernamePasswordAuthenticationToken
。
- 把
Authentication
交给AuthenticationManager
进行验证
AuthenticationManager
会调用其中的各种AuthenticationProvider
去进行验证,只要验证通过,就会新建一个带有Authority的Authentication
对象,之后最关键的是会进行super.setAuthenticated(true);
,表示认证通过。
- 过滤器检查是否认证通过,如果通过,就调用安全上下文容器的
.getContext().setAuthentication(…)
方法,把Authentication
设置到其中。(这个是在)AbstractAuthenticationProcessingFilter
的successfulAuthentication
方法中。
- 所有过滤器都会检查安全上下文中的
Authentication
对象,如果检测到isAuthenticated()==true
,就放行(当然还有权限的问题,这里先简化)。验证不通过,就会抛出各种异常然后返回。
- 返回异常则没有通过验证。如果
Authentication
一路被放行到控制器,控制器就成功接受响应,这样就完成了一次成功的安全验证。
可以看到,AuthenticationManager
也是一个很关键的类。其他的主要就是过滤器了。
AuthenticationManager
这个就是一个提供认证服务的接口,即我把用户身份给你,你来告诉我是否通过认证,怎么告诉我,就是调用用户身份上的setAuthenticated(true)
。
接口的源码很简单:
public interface AuthenticationManager {
Authentication authenticate(Authentication var1) throws AuthenticationException;
}
只有一个方法,接收一个Authentication
对象,返回一个Authentication
对象。
抛的这个异常也是一个根异常,有一堆各种验证异常继承这个异常。
既然是接口,常用的实现类就很重要了,其实Spring Security自己只有一个实现类,就是ProviderManager
,部分源码:
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
private List<AuthenticationProvider> providers;
public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) {
this.eventPublisher = new ProviderManager.NullEventPublisher();
this.providers = Collections.emptyList();
this.messages = SpringSecurityMessageSource.getAccessor();
this.eraseCredentialsAfterAuthentication = true;
Assert.notNull(providers, "providers list cannot be null");
this.providers = providers;
this.parent = parent;
this.checkState();
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
Iterator var8 = this.getProviders().iterator();
while(var8.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var8.next();
if (provider.supports(toTest)) {
if (debug) {
logger.debug("Authentication attempt using " + provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
if (result != null) {
this.copyDetails(authentication, result);
break;
}
} catch (AccountStatusException var13) {
this.prepareException(var13, authentication);
throw var13;
} catch (InternalAuthenticationServiceException var14) {
this.prepareException(var14, authentication);
throw var14;
} catch (AuthenticationException var15) {
lastException = var15;
}
}
}
......
}
}
首先可以看到这个类继承了AuthenticationManager
和InitializingBean
,说明这个Bean会在SpringBoot启动的时候被组装。
从源码里可以看到,这其中有一个属性是一个认证提供器的列表:List<AuthenticationProvider>
。
使用这个列表进行构造之后,重写了接口的认证方法,其中var8代表着所有认证提供器的迭代器,然后尝试一个一个的拿出来先检测是否能够认证Authentication
对象,如果不能认证就继续,能认证就调用认证提供器的方法进行认证。如果结果不是null,表名认证通过,就把结果复制到Authentication
对象上,认证不通过,认证器会抛异常,抓住异常然后返回。
这里为什么要有这么多认证提供器,是因为对应的Authentication
对象的实现类也很多,在官网查看API文档可以发现其实现类有好多:
AbstractAuthenticationToken, AbstractOAuth2TokenAuthenticationToken, AnonymousAuthenticationToken,
BearerTokenAuthenticationToken, CasAssertionAuthenticationToken, CasAuthenticationToken,
JaasAuthenticationToken, JwtAuthenticationToken, OAuth2AuthenticationToken,
OAuth2AuthorizationCodeAuthenticationToken, OAuth2LoginAuthenticationToken,
OpenIDAuthenticationToken, PreAuthenticatedAuthenticationToken, RememberMeAuthenticationToken,
RunAsUserToken, TestingAuthenticationToken, UsernamePasswordAuthenticationToken
这里的每一个实现类内部都不同,所以需要不同的AuthenticationProvider
。
AuthenticationProvider
再去看看AuthenticationProvider
实现类,有这么一大批:
AbstractJaasAuthenticationProvider, AbstractLdapAuthenticationProvider, AbstractUserDetailsAuthenticationProvider,
ActiveDirectoryLdapAuthenticationProvider, AnonymousAuthenticationProvider,
AuthenticationManagerBeanDefinitionParser.NullAuthenticationProvider, CasAuthenticationProvider,
DaoAuthenticationProvider, DefaultJaasAuthenticationProvider, JaasAuthenticationProvider,
JwtAuthenticationProvider, LdapAuthenticationProvider, OAuth2AuthorizationCodeAuthenticationProvider,
OAuth2LoginAuthenticationProvider, OidcAuthorizationCodeAuthenticationProvider, OpenIDAuthenticationProvider,
PreAuthenticatedAuthenticationProvider, RememberMeAuthenticationProvider, RemoteAuthenticationProvider,
RunAsImplAuthenticationProvider, TestingAuthenticationProvider
在初始化的时候,Spring会根据配置,自动给ProviderManager
放入不同的AuthenticationProvider
的实现类列表。
比如我们默认情况或者在配置文件中使用.formLogin()
的时候,认证提供器中就会有一个DaoAuthenticationProvider
来验证用户名和密码。
实际上我们在一层层深入:过滤器--->调用AuthenticationManager
实现类--》调用AuthenticationProvider
的实现类验证,这个过程中全部都是在操作Authentication
对象,最后将其返回给过滤器。
现在只差最后一层了,就是具体怎么验证的,这里就挑一个只要是用户名和密码就会进行验证的DaoAuthenticationProvider
类来看看怎么工作的:
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
//密码编码器
private PasswordEncoder passwordEncoder;
private volatile String userNotFoundEncodedPassword;
//UserDetailsService对象,关键
private UserDetailsService userDetailsService;
private UserDetailsPasswordService userDetailsPasswordService;
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
} catch (InternalAuthenticationServiceException var5) {
throw var5;
} catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
boolean upgradeEncoding = this.userDetailsPasswordService != null && this.passwordEncoder.upgradeEncoding(user.getPassword());
if (upgradeEncoding) {
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
return super.createSuccessAuthentication(principal, authentication, user);
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
protected UserDetailsService getUserDetailsService() {
return this.userDetailsService;
}
public void setUserDetailsPasswordService(UserDetailsPasswordService userDetailsPasswordService) {
this.userDetailsPasswordService = userDetailsPasswordService;
}
}
这个DAO验证器其实名字已经暗含了,就是要通过DAO去拿用户信息。先是这个方法:
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
拿着Authentication
对象和用户名,到里边通过UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username)
获取一个UserDetails
对象,然后通过additionalAuthenticationChecks
去校验密码。
之后通过protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user)
来创建一个成功验证后的Authentication
对象。
这其中提供了类似DAO服务的就是UserDetailsService
类。
验证器的整体流程是-->得到用户名和密码-->数据库里取出用户对象-->比较密码是否相同-->相同,创建成功认证的对象,返回去。
UserDetails
这也是一个接口:
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
这其实就代表一个从数据库取出来的用户对象,除了用户名和密码之外,还有四个布尔值表示四种验证,可以自行设置true或者false。还有权限。
可能看上去,其中的内容不是和Authentication
对象一样么,其实不然,这里一定要注意:
Authentication
对象是用户POST进来的数据外加其他逻辑组成
UserDetails
对象是由数据库取出来的对象外加其他逻辑组成
两者的联系是,如果验证成功,UserDetails
的权限属性也就是getAuthorities()
的结果,会被复制到Authentication
对象上。
每次请求进来,过滤器使用Authentication
对象,而UserDetails
只在校验的时候使用,使用完了就结束了。
在配置SS的时候,configure(AuthenticationManagerBuilder auth)
这个配置方法现在就应该明白了,实际上是创建不同类型的AuthenticationManager
。
如果写了inMemoryAuthentication()
,那么DaoAuthenticationProvider
就不会出现在认证器的列表里,出现的是其他实现。
认证流程图
看到原作者有图,我也手绘一张:
把架构搞清楚了,写JWT验证就心里有数了。看来主要就是基于过滤器来展开代码了。