详解SpringSecurity认证流程
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
详解SpringSecurity认证流程
前⾔
Spring Seuciry相关的内容看了实在是太多了,但总觉得还是理解地不够巩固,还是需要靠知识输出做巩固。
相关版本:
java: jdk 8
spring-boot: 2.1.6.RELEASE
过滤器链和认证过程
⼀个认证过程,其实就是过滤器链上的⼀个绿⾊矩形Filter所要执⾏的过程。
基本的认证过程有三步骤:
1. Filter拦截请求,⽣成⼀个未认证的Authentication,交由AuthenticationManager进⾏认证;
2. AuthenticationManager的默认实现ProviderManager会通过AuthenticationProvider对Authentication进⾏认证,其本⾝不做认证处理;
3. 如果认证通过,则创建⼀个认证通过的Authentication返回;否则抛出异常,以表⽰认证不通过。
要理解这个过程,可以从类UsernamePasswordAuthenticationFilter,ProviderManager,DaoAuthenticationProvider和InMemoryUserDetailsManager(UserDetailsService实现类,
由UserDetailsServiceAutoConfiguration默认配置提供)进⾏了解。
只要创建⼀个含有spring-boot-starter-security的springboot项⽬,在适当地打上断点接⼝看到这个流程。
⽤认证部门进⾏讲解
)
请求到前台之后,负责该请求的前台会将请求的内容封装为⼀个Authentication对象交给认证管理部门,认证管理部门仅管理认证部门,不做具体的认证操作,具体的操作由与该前台相关的认证部门进⾏处理。
当然,每个认证部门需要判断Authentication是否为该部门负责,是则由该部门负责处理,否则交给下⼀个部门处理。
认证部门认证成功之后会创建⼀个认证通过的Authentication返回。
否则要么抛出异常表⽰认证不通过,要么交给下⼀个部门处理。
如果需要新增认证类型,只要增加相应的前台(Filter)和与该前台(Filter)想对应的认证部门(AuthenticationProvider)就即可,当然也可以增加⼀个与已有前台对应的认证部门。
认证部门会通过前台⽣成的Authentication来判断该认证是否由该部门负责,因⽽也许提供⼀个两者相互认同的Authentication.
认证部门需要⼈员资料时,则可以从⼈员资料部门获取。
不同的系统有不同的⼈员资料部门,需要我们提供该⼈员资料部门,否则将拿到空⽩档案。
当然,⼈员资料部门不⼀定是唯⼀的,认证部门可以有⾃⼰的专属资料部门。
上图还可以有如下的画法:
这个画法可能会和FilterChain更加符合。
每⼀个前台其实就是FilterChain中的⼀个,客户拿着请求逐个前台请求认证,找到正确的前台之后进⾏认证判断。
前台(Filter)
这⾥的前台Filter仅仅指实现认证的Filter,Spring Security Filter Chain中处理这些Filter还有其他的Filter,⽐如CsrfFilter。
如果⾮要给⾓⾊给他们,那么就当他们是保安⼈员吧。
Spring Security为我们提供了3个已经实现的Filter。
UsernamePasswordAuthenticationFilter,BasicAuthenticationFilter和RememberMeAuthenticationFilter。
如果不做任何个性化的配置,UsernamePasswordAuthenticationFilter和BasicAuthenticationFilter会在默认的过滤器链中。
这两种认证⽅式也就是默认的认证⽅式。
UsernamePasswordAuthenticationFilter仅仅会对/login路径⽣效,也就是说UsernamePasswordAuthenticationFilter负责发布认证,发布认证的接⼝为/login。
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
...
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
...
}
UsernamePasswordAuthenticationFilter为抽象类AbstractAuthenticationProcessingFilter的⼀个实现,⽽BasicAuthenticationFilter为抽象类BasicAuthenticationFilter的⼀个实现。
这四个类的源码提供了不错的前台(Filter)实现思路。
AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter提供了认证前后需要做的事情,其⼦类只需要提供实现完成认证的抽象⽅法attemptAuthentication(HttpServletRequest, HttpServletResponse)即可。
使⽤AbstractAuthenticationProcessingFilter时,需要提供⼀个拦截路径(使⽤AntPathMatcher进⾏匹配)来拦截对应的特定的路径。
UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter作为实际的前台,会将客户端提交的username和password封装成⼀个UsernamePasswordAuthenticationToken交给认证管理部门(AuthenticationManager)进⾏认证。
如此,她的任务就完成了。
BasicAuthenticationFilter
该前台(Filter)只会处理含有Authorization的Header,且⼩写化后的值以basic开头的请求,否则该前台(Filter)不负责处理。
该Filter会从header中获取Base64编码之后的username和password,创建UsernamePasswordAuthenticationToken提供给认证管理部门(AuthenticationMananager)进⾏认证。
认证资料(Authentication)
前台接到请求之后,会从请求中获取所需的信息,创建⾃家认证部门(AuthenticationProvider)所认识的认证资料(Authentication),认证部门(AuthenticationProvider)则主要是通过认证资料(Authentication)的类型判断是否由该部门处理。
public interface Authentication extends Principal, Serializable {
// 该principal具有的权限。
AuthorityUtils⼯具类提供了⼀些⽅便的⽅法。
Collection<? extends GrantedAuthority> getAuthorities();
// 证明Principal的⾝份的证书,⽐如密码。
Object getCredentials();
// authentication request的附加信息,⽐如ip。
Object getDetails();
// 当事⼈。
在username+password模式中为username,在有userDetails之后可以为userDetails。
Object getPrincipal();
// 是否已经通过认证。
boolean isAuthenticated();
// 设置通过认证。
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
在Authentication被认证之后,会保存到⼀个thread-local的SecurityContext中。
// 设置
SecurityContextHolder.getContext().setAuthentication(anAuthentication);
// 获取
Authentication existingAuth = SecurityContextHolder.getContext()
.getAuthentication();
在写前台Filter的时候,可以先检查SecurityContextHolder.getContext()中是否已经存在通过认证的Authentication了,如果存在,则可以直接跳过该Filter。
已经通过验证
的Authentication建议设置为⼀个不可修改的实例。
⽬前从Authentication的类图中看到的实现类,均为Authentication的抽象⼦类AbstractAuthenticationToken的实现类。
实现类有好⼏个,与前⾯的讲到的Filter相关的
有UsernamePasswordAuthenticationToken和RememberMeAuthenticationToken。
AbstractAuthenticationToken为CredentialsContainer和Authentication的⼦类。
实现了⼀些简单的⽅法,但主要的⽅法还需要实现。
该类的getName()⽅法的实现可以看到常⽤的principal类为UserDetails、AuthenticationPrincipal和Princial。
如果有需要将对象设置为principal,可以考虑继承这三个类中的⼀个。
public String getName() {
if (this.getPrincipal() instanceof UserDetails) {
return ((UserDetails) this.getPrincipal()).getUsername();
}
if (this.getPrincipal() instanceof AuthenticatedPrincipal) {
return ((AuthenticatedPrincipal) this.getPrincipal()).getName();
}
if (this.getPrincipal() instanceof Principal) {
return ((Principal) this.getPrincipal()).getName();
}
return (this.getPrincipal() == null) ? "" : this.getPrincipal().toString();
}
认证管理部门(AuthenticationManager)
AuthenticationManager是⼀个接⼝,认证Authentication,如果认证通过之后,返回的Authentication应该带上该principal所具有的GrantedAuthority。
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
该接⼝的注释中说明,必须按照如下的异常顺序进⾏检查和抛出:
DisabledException:账号不可⽤
LockedException:账号被锁
BadCredentialsException:证书不正确
Spring Security提供⼀个默认的实现ProviderManager。
认证管理部门(ProviderManager)仅执⾏管理职能,具体的认证职能由认证部门(AuthenticationProvider)执⾏。
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
...
public ProviderManager(List<AuthenticationProvider> providers) {
this(providers, null);
}
public ProviderManager(List<AuthenticationProvider> providers,
AuthenticationManager parent) {
Assert.notNull(providers, "providers list cannot be null");
this.providers = providers;
this.parent = parent;
checkState();
}
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();
for (AuthenticationProvider provider : getProviders()) {
// #1, 检查是否由该认证部门进⾏认证`AuthenticationProvider`
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
// #2, 认证部门进⾏认证
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
// #3,认证通过则不再进⾏下⼀个认证部门的认证,否则抛出的异常被捕获,执⾏下⼀个认证部门(AuthenticationProvider)
break;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = parentException = e;
}
}
// #4, 如果认证通过,执⾏认证通过之后的操作
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
// #5,如果认证不通过,必然有抛出异常,否则表⽰没有配置相应的认证部门(AuthenticationProvider)
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
...
}
遍历所有的认证部门(AuthenticationProvider),找到⽀持的认证部门进⾏认证认证部门进⾏认证认证通过则不再进⾏下⼀个认证部门的认证,否则抛出的异常被捕获,执⾏下⼀个认证部门(AuthenticationProvider)如果认证通过,执⾏认证通过之后的操作如果认证不通过,必然有抛出异常,否则表⽰没有配置相应的认证部门(AuthenticationProvider)
当使⽤到Spring Security OAuth2的时候,会看到另⼀个实现OAuth2AuthenticationManager。
认证部门(AuthenticationProvider)
认证部门(AuthenticationProvider)负责实际的认证⼯作,与认证管理部门(ProvderManager)协同⼯作。
也许其他的认证管理部门(AuthenticationManager)并不需要认证部门(AuthenticationProvider)的协作。
public interface AuthenticationProvider {
// 进⾏认证
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
// 是否由该AuthenticationProvider进⾏认证
boolean supports(Class<?> authentication);
}
该接⼝有很多的实现类,其中包含了RememberMeAuthenticationProvider(直接AuthenticationProvider)和DaoAuthenticationProvider(通
过AbastractUserDetailsAuthenticationProvider简介继承)。
这⾥重点讲讲AbastractUserDetailsAuthenticationProvider和DaoAuthenticationProvider。
AbastractUserDetailsAuthenticationProvider
顾名思义,AbastractUserDetailsAuthenticationProvider是对UserDetails⽀持的Provider,其他的Provider,如RememberMeAuthenticationProvider就不需要⽤到UserDetails。
该抽象类有两个抽象⽅法需要实现类完成:
// 获取 UserDetails
protected abstract UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;
protected abstract void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;
retrieveUser()⽅法为校验提供UserDetails。
先看下UserDetails:
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
// 账号是否过期
boolean isAccountNonExpired();
// 账号是否被锁
boolean isAccountNonLocked();
// 证书(password)是否过期
boolean isCredentialsNonExpired();
// 账号是否可⽤
boolean isEnabled();
}
AbastractUserDetailsAuthenticationProvider#authentication(Authentication)分为三步验证:
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
postAuthenticationChecks.check(user);
preAuthenticationChecks的默认实现为DefaultPreAuthenticationChecks,负责完成校验:
1. UserDetails#isAccountNonLocked()
2. UserDetails#isEnabled()
3. UserDetails#isAccountNonExpired()
postAuthenticationChecks的默认实现为DefaultPostAuthenticationChecks,负责完成校验:
UserDetails#user.isCredentialsNonExpired()
additionalAuthenticationChecks需要由实现类完成。
校验成功之后,AbstractUserDetailsAuthenticationProvider会创建并返回⼀个通过认证的Authentication。
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
// Ensure we return the original credentials the user supplied,
// so subsequent attempts are successful even with encoded passwords.
// Also ensure we return the original getDetails(), so that future
// authentication events after cache expiry contain the details
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
DaoAuthenticationProvider
如下为DaoAuthenticationProvider对AbstractUserDetailsAuthenticationProvider抽象⽅法的实现。
// 检查密码是否正确
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();
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"));
}
}
// 通过资料室(UserDetailsService)获取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;
}
...
}
在以上的代码中,需要提供UserDetailsService和PasswordEncoder实例。
只要实例化这两个类,并放⼊到Spring容器中即可。
资料部门(UserDetailsService)
UserDetailsService接⼝提供认证过程所需的UserDetails的类,如DaoAuthenticationProvider需要⼀个UserDetailsService实例。
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
Spring Security提供了两个UserDetailsService的实现:InMemoryUserDetailsManager和JdbcUserDetailsManager。
InMemoryUserDetailsManager为默认配置,
从UserDetailsServiceAutoConfiguration的配置中可以看出。
当然也不容易理解,基于数据库的实现需要增加数据库的配置,不适合做默认实现。
这两个类均
为UserDetailsManager的实现类,UserDetailsManager定义了UserDetails的CRUD操作。
InMemoryUserDetailsManager使⽤Map<String, MutableUserDetails>做存储。
public interface UserDetailsManager extends UserDetailsService {
void createUser(UserDetails user);
void updateUser(UserDetails user);
void deleteUser(String username);
void changePassword(String oldPassword, String newPassword);
boolean userExists(String username);
}
如果我们需要增加⼀个UserDetailsService,可以考虑实现UserDetailsService或者UserDetailsManager。
增加⼀个认证流程
到这⾥,我们已经知道Spring Security的流程了。
从上⾯的内容可以知道,如要增加⼀个新的认证⽅式,只要增加⼀个[前台(Filter) + 认证部门(AuthenticationProvider) + 资料室(UserDetailsService)]组合即可。
事实上,资料室(UserDetailsService)不是必须的,可根据认证部门(AuthenticationProvider)需要实现。
我会在另⼀篇⽂章中以⼿机号码+验证码登录为例进⾏讲解。
以上就是本⽂的全部内容,希望对⼤家的学习有所帮助,也希望⼤家多多⽀持。