spring security(一)

折月煮酒 提交于 2020-03-23 23:08:49

3 月,跳不动了?>>>

Spring、Spring Boot 和 Spring Security 三者的关系如下图所示 :

一、spring security 的整体架构

二、核心组件

SecurityContextHolder,SecurityContext 和 Authentication

如图所示,用户在走完认证授权流程后,最终信息都将保存在SecurityContexHolder中, 存储当前应用程序安全上下文的详细信息,其中包括当前使用应用程序的主体的详细信息。如当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限等。

默认情况下,SecurityContextHolder 使用 ThreadLocal 来存储这些详细信息,这意味着 Security Context 始终可用于同一执行线程中的方法,即使 Security Context 未作为这些方法的参数显式传递。

  • 获取当前用户的信息
Object principal = SecurityContextHolder.getContext()
  .getAuthentication().getPrincipal();

if (principal instanceof UserDetails) {
  String username = ((UserDetails)principal).getUsername();
} else {
  String username = principal.toString();
}
  • securityContext

上面代码中,SecurityContextHolder.getContext()中即为securityContext

// org/springframework/security/core/context/SecurityContext.java
public interface SecurityContext extends Serializable {
    Authentication getAuthentication();
    void setAuthentication(Authentication authentication);
}

其内部定义了身份验证的Authentication的get/set方法;

  • 身份验证Authentication

使用getAuthentication时,会返回一个Authentication类型的对象,其内部包含的内容如下:

// org/springframework/security/core/Authentication.java
public interface Authentication extends Principal, Serializable {
  // 权限信息列表,默认是GrantedAuthority接口的一些实现类,通常是代表权限信息的一系列字符串。
    Collection<? extends GrantedAuthority> getAuthorities();
  // 密码信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
    Object getCredentials();
    Object getDetails();
  // 最重要的身份信息,大部分情况下返回的是UserDetails接口的实现类,也是框架中的常用接口之一。
    Object getPrincipal();
    boolean isAuthenticated();
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

1.权限信息列表
2.用户输入的密码串
3.UserDetails接口的实现类
4.身份验证结果

 三、身份验证

常见流程

  1. 登录页提示用户输入用户名和密码
  2. 系统验证用户名和密码是否与注册的信息一致
  3. 验证后得到用户的信息、权限信息等
  4. 框架将信息保存至该用户上下文中
  5. 用户后续请求操作,根据权限信息进行安全验证

1~3步骤,身份验证流程、权限等信息如前面所说将保存至SecutiyContextHoder中。

  • 通过 UsernamePasswordAuthenticationToken  获取用户请求中的用户名和密码,并做简单的非空检查;(由框架处理)
  • 令牌将传递给 AuthenticationManager 的实例以进行验证。 主要是使用username去数据源中获取用户密码,并交由AuthenticationManager 进行校验,校验部分由框架处理。
  • AuthenticationManager 在成功验证时返回完全填充的 Authentication 实例。 (由框架处理)
  • SecurityContext 对象是通过调用 SecurityContextHolder.getContext().setAuthentication(…) 创建的,传入返回的身份验证 Authentication 对象。 (由框架处理)

四、核心服务

AuthenticationManager,ProviderManager 和 AuthenticationProvider

AuthenticationManager(接口)是认证相关的核心接口,也是发起认证的出发点,因为在实际需求中,我们可能会允许用户使用用户名 + 密码登录,同时允许用户使用邮箱 + 密码,手机号码 + 密码登录,甚至,可能允许用户使用指纹登录,所以要求认证系统要支持多种认证方式。

Spring Security 中 AuthenticationManager 接口的默认实现是 ProviderManager,但它本身并不直接处理身份验证请求,它会委托给已配置的 AuthenticationProvider 列表,每个列表依次被查询以查看它是否可以执行身份验证。每个 Provider 验证程序将抛出异常或返回一个完全填充的 Authentication 对象。

也就是说,Spring Security 中核心的认证入口始终只有一个:AuthenticationManager,不同的认证方式:用户名 + 密码(UsernamePasswordAuthenticationToken),邮箱 + 密码,手机号码 + 密码登录则对应了三个 AuthenticationProvider。

ProviderManager  中维护了AuthenticationProvider列表, 在 ProviderManager 进行认证的过程中,会遍历 providers 列表,判断是否支持当前 authentication 对象的认证方式,若支持该认证方式时,就会调用所匹配 provider(AuthenticationProvider)对象的 authenticate 方法进行认证操作。若认证失败则返回 null,下一个 AuthenticationProvider 会继续尝试认证,如果所有认证器都无法认证成功,则 ProviderManager 会抛出一个 ProviderNotFoundException 异常。

下面将介绍常用的AuthenticationProvide。

DaoAuthenticaitonProvide

Dao正是数据访问层的缩写,也暗示了这个身份认证器的实现思路。 在实际项目中,最常见的认证方式是使用用户名和密码。用户在登录表单中提交了用户名和密码,而对于已注册的用户,在数据库中已保存了正确的用户名和密码,认证便是负责比对同一个用户名,提交的密码和数据库中所保存的密码是否相同便是了。

在 Spring Security 中,对于使用用户名和密码进行认证的场景,用户在登录表单中提交的用户名和密码,被封装成了 UsernamePasswordAuthenticationToken,而根据用户名加载用户的任务则是交给了 UserDetailsService,在 DaoAuthenticationProvider 中,对应的方法就是 retrieveUser,虽然有两个参数,但是 retrieveUser 只有第一个参数起主要作用,返回一个 UserDetails。retrieveUser 方法的具体实现如下:

// spring-security-core-5.2.0.RELEASE-sources.jar
// org/springframework/security/authentication/dao/DaoAuthenticationProvider.java
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);
        }
}

在 DaoAuthenticationProvider 类的 retrieveUser 方法中,会以传入的 username 作为参数,调用 UserDetailsService 对象的 loadUserByUsername 方法加载用户。

 Authentication 中的 getAuthorities() 实际是由 UserDetails 的 getAuthorities() 传递而形成的。还记得 Authentication 接口中的 getUserDetails() 方法吗?其中的 UserDetails 用户详细信息就是经过了 provider (AuthenticationProvider) 认证之后被填充的。

UserDetailsService 接口

大多数身份验证提供程序都利用了 UserDetails  UserDetailsService 接口。

在 UserDetailsService 接口中,只有一个 loadUserByUsername 方法,用于通过 username 来加载匹配的用户。当找不到 username 对应用户时,会抛出 UsernameNotFoundException 异常。UserDetailsService 和 AuthenticationProvider 两者的职责常常被人们搞混,记住一点即可,UserDetailsService 只负责从特定的地方(通常是数据库)加载用户信息,仅此而已。

UserDetailsService 常见的实现类有 JdbcDaoImpl,InMemoryUserDetailsManager,前者从数据库加载用户,后者从内存中加载用户,当然你也可以自己实现 UserDetailsService。 ​​​​​​​

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!