Spring Security 验证流程

Spring Security 验证流程

为了能够早日攻克JWT验证,要好好学一下Spring Security。 Spring Security认证原理 Authentication 如何验证用户身份 AuthenticationManager DaoAuthenticationProvider UserDetails 认证流程图 Spr

为了能够早日攻克JWT验证,要好好学一下Spring Security。
  1. Spring Security认证原理
  2. Authentication
  3. 如何验证用户身份
  4. AuthenticationManager
  5. DaoAuthenticationProvider
  6. UserDetails
  7. 认证流程图

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()拿到上下文,之后的AuthenticationPrincipal是什么

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对于通过认证的身份对象,才会放行。

如何验证用户身份

在继续看其他类之前,就有必要看一下完整的流程了,这里我就根据自己的研究整理了一下:
  1. 过滤器UsernamePasswordAuthenticationFilter拿到POST请求中的用户名和密码,然后把用户名和密码封到Authentication的实现类中,这个实现类一般是UsernamePasswordAuthenticationToken
  2. Authentication交给AuthenticationManager进行验证
  3. AuthenticationManager会调用其中的各种AuthenticationProvider去进行验证,只要验证通过,就会新建一个带有Authority的Authentication对象,之后最关键的是会进行super.setAuthenticated(true);,表示认证通过。
  4. 过滤器检查是否认证通过,如果通过,就调用安全上下文容器的.getContext().setAuthentication(…)方法,把Authentication设置到其中。(这个是在)AbstractAuthenticationProcessingFiltersuccessfulAuthentication方法中。
  5. 所有过滤器都会检查安全上下文中的Authentication对象,如果检测到isAuthenticated()==true,就放行(当然还有权限的问题,这里先简化)。验证不通过,就会抛出各种异常然后返回。
  6. 返回异常则没有通过验证。如果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;
                }
            }
        }

        ......
    }
}
首先可以看到这个类继承了AuthenticationManagerInitializingBean,说明这个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对象一样么,其实不然,这里一定要注意:
  1. Authentication对象是用户POST进来的数据外加其他逻辑组成
  2. UserDetails对象是由数据库取出来的对象外加其他逻辑组成
两者的联系是,如果验证成功,UserDetails的权限属性也就是getAuthorities()的结果,会被复制到Authentication对象上。 每次请求进来,过滤器使用Authentication对象,而UserDetails只在校验的时候使用,使用完了就结束了。 在配置SS的时候,configure(AuthenticationManagerBuilder auth)这个配置方法现在就应该明白了,实际上是创建不同类型的AuthenticationManager。 如果写了inMemoryAuthentication(),那么DaoAuthenticationProvider就不会出现在认证器的列表里,出现的是其他实现。

认证流程图

看到原作者有图,我也手绘一张: Spring认证流程 把架构搞清楚了,写JWT验证就心里有数了。看来主要就是基于过滤器来展开代码了。
LICENSED UNDER CC BY-NC-SA 4.0
Comment