SpringSecurity 核心组件及认证介绍

819 阅读9分钟

核心组件

在深入了解Spring Security之前我们要了解主要组件有哪些,不管后面版本如何变化和如何封装,其实底层都是围绕下面这些核心组件来操作。

1、SecurityContextHolder, SecurityContext和Authentication对象

最基本的对象是SecurityContextHolder。我们在这里存储应用程序当前SecurityContext的详细信息,其中包括当前使用应用程序的主体的详细信息。默认情况下,SecurityContextHolder使用ThreadLocal来存储这些细节,这意味着SecurityContext总是对同一执行线程中的方法可用,即使SecurityContext没有显式地作为参数传递给那些方法。如果在当前主体的请求被处理后小心地清除线程,以这种方式使用ThreadLocal是非常安全的。当然,Spring Security会自动为您处理这个问题,因此您不必为此担心。

一些应用程序并不完全适合使用ThreadLocal,因为它们处理线程有特定方式。例如,Swing客户端可能希望Java Virtual Machine中的所有线程都使用相同的安全上下文中。SecurityContextHolder可以在启动时配置一个策略来指定你想要如何存储上下文。对于一个独立的应用程序,你可以使用SecurityContextHolder.MODE_GLOBAL策略。其他应用程序可能希望由安全线程派生的线程也采用相同的安全标识。这是通过使用SecurityContextHolder.MODE_INHERITABLETHREADLOCAL实现的。你可以改变默认的模式SecurityContextHolder.MODE_THREADLOCAL有两种实现方式。第一个是设置一个系统属性,第二个是调用SecurityContextHolder的静态方法。大多数应用程序不需要更改默认值,但如果你需要更改,请查看SecurityContextHolder的 JavaDoc 来了解更多。

2、获取当前用户信息

SecurityContextHolder中,我们存储了当前与应用程序交互的主体的细节。Spring Security 使用Authentication对象来表示该信息。通常,您不需要自己创建Authentication对象,但用户查询Authentication对象是相当常见的。您可以在应用程序的任何地方使用以下代码块——以获取目前已经身份验证的用户的名称,例如:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
​
if (principal instanceof UserDetails) {
    String username = ((UserDetails)principal).getUsername();
} else {
    String username = principal.toString();
}

调用getContext()返回的对象是SecurityContext接口的一个实例。这是保存在线程本地存储中的对象。正如我们下面将看到的,Spring Security 中的大多数身份验证机制返回一个UserDetails实例作为主体。

3、UserDetailsService

上述代码片段中需要注意的另一项是,您可以从Authentication对象获得一个主体。主体只是一个对象。大多数时候,这可以转换为一个UserDetails对象。UserDetails是 Spring Security 中的一个核心接口。它表示主体,但是以可扩展和特定于应用程序的方式。把UserDetails看作是你自己的用户数据库和SecurityContextHolder中 Spring Security 需要的东西之间的适配器。作为来自您自己的用户数据库的内容的表示,您通常会将UserDetails转换为应用程序提供的原始对象,这样就可以调用特定于业务的方法(如getEmail()getEmployeeNumber()等)。

现在你可能想知道,我什么时候提供一个UserDetails对象?怎么做呢?我记得你说过这个东西是声明性的,我不需要写任何 Java 代码——这是怎么回事?简单地说,有一个特殊的接口叫做UserDetailsService。这个接口上唯一的方法接受基于字符串的用户名参数并返回UserDetails:

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

这是 Spring Security 中为用户加载信息的最常见方法,当需要用户信息时,您将看到它在整个框架中使用。

在成功的身份验证中,UserDetails被用来构建存储在SecurityContextHolder中的Authentication对象(下文将详细介绍)。好消息是我们提供了许多UserDetailsService实现,其中一个使用了内存映射(InMemoryDaoImpl),另一个使用了 JDBC (JdbcDaoImpl)。不过,大多数用户倾向于编写自己的代码,其实现通常位于表示其雇员、客户或应用程序的其他用户的现有数据访问对象(Data Access Object, DAO)之上。记住,无论UserDetailsService返回什么,都可以使用上面的代码片段从SecurityContextHolder中获得。

❗关于UserDetailsService经常有一些混淆。它纯粹是一个用于用户数据的DAO,除了向框架内的其他组件提供数据外,不执行其他功能。特别是,它不验证用户,这是由AuthenticationManager完成的。在许多情况下,如果需要自定义身份验证过程,直接implement AuthenticationProvider更有意义。

4、GrantedAuthority

除了主体之外,Authentication提供的另一个重要方法是getAuthorities()。此方法提供一个GrantedAuthority对象数组。被授予的权限是授予主体的权限,这并不奇怪。这些权限通常是“角色”,如ROLE_ADMINISTRATORROLE_HR_SUPERVISOR。稍后将为web授权、方法授权和域对象授权配置这些角色。Spring Security 的其他部分能够解释这些权限,并期望它们出现。授权权限对象通常由UserDetailsService加载。

通常,GrantedAuthority对象是应用程序范围的权限。它们不是特定于给定的域对象。因此,你就不可能有GrantedAuthority代表许可Employee54号对象,因为如果有成千上万的这种权限,你会很快耗尽内存(或者,至少导致应用程序需要很长时间才能验证一个用户)。当然,Spring Security 是专门设计来处理这个常见需求的,但是您应该使用项目的域对象安全功能来实现这个目的。

5、总结

回顾一下,我们目前看到的Spring Security的主要构建模块是:

  • SecurityContextHolder, 提供对SecurityContext的访问
  • SecurityContext, 保存身份验证以及可能的特定请求的安全信息
  • Authentication, 以特定于Spring Security的方式表示主体
  • GrantedAuthority, 反映授予主体的应用程序范围权限
  • UserDetails, 提供从应用程序的 DAOs 或其他安全数据来源构建身份验证对象所需的信息
  • UserDetailsService, 当传入基于字符串的用户名(或证书ID或类似的)时,创建UserDetails

通过上面的介绍我们已经了解了这些组件各代表什么意思,后面让我们进一步来了解身份验证的具体过程。

认证

我再来简单介绍下Authentication

Spring Security 可以参与许多不同的身份验证环境。虽然我们建议人们使用 Spring Security 进行身份验证,而不是与现有的容器管理身份验证集成,但它仍然是受支持的——就像与你自己专有的身份验证系统集成一样。

1、Spring Security 中的认证是什么?

让我们考虑一个大家都熟悉的标准身份验证场景。

1、提示用户使用用户名和密码登录。

2、系统(成功)验证用户名的密码是否正确。

3、获得该用户的上下文信息(其角色列表等)。

4、为用户建立一个安全上下文。

5、用户继续执行某些操作,这些操作可能受到访问控制机制的保护,该机制根据当前安全上下文信息检查操作所需的权限。

前四项构成了身份验证过程,因此我们将看看这些在 Spring Security 中是如何发生的。

1、获取用户名和密码并将其组合到UsernamePasswordAuthenticationToken的实例中(一个Authentication接口的实例,我们在前面看到过)。

2、令牌被传递给AuthenticationManager的一个实例进行验证。

3、AuthenticationManager在身份验证成功时返回一个完全填充的Authentication实例。

4、安全上下文通过调用SecurityContextHolder.getContext().setAuthentication(…)来建立,并传入返回的认证对象。

至此,用户就被认为通过了身份验证。让我们看一些代码作为示例。

package com.hz.ss.authentication;
​
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
​
/**
 * @author Dong
 * @version 1.0
 * @date 2022/5/16
 */
public class AuthenticationMain {
​
    private static AuthenticationManager am = new SimpleAuthenticationManager();
    private static AuthenticationProvider ap = new SimpleAuthenticationProvider();
​
    public static void main(String[] args) {
        // String name = "root";
        String name = "admin";
        String pass = "123";
​
        // 1 通过用户名和密码封装一个 UsernamePasswordAuthenticationToken 对象
        Authentication authentication = new UsernamePasswordAuthenticationToken(name, pass);
​
        // 2/3 初始化一个 AuthenticationManager 实例,并验证上面的 Authentication 对象,之后返回 Authentication 实例
        AuthenticationManager authenticationManager = new MyAuthenticationManager();
        Authentication auth = authenticationManager.authenticate(authentication);
​
        // 4 传入并设置认证对象
        SecurityContextHolder.getContext().setAuthentication(auth);
​
        // 打印认证后的安全上下文
        System.out.println(SecurityContextHolder.getContext());
    }
​
    // AuthenticationManager 实现
    static class MyAuthenticationManager implements AuthenticationManager {
​
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            // 测试认证,只要用户名为 root 就认为认证成功
            if ("root".equals(authentication.getName())) {
                return new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), authentication.getCredentials());
            }
            throw new UsernameNotFoundException("用户不存在...");
        }
    }
}

通过切换不同的用户来验证返回的信息:

org.springframework.security.core.context.SecurityContextImpl@1f: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@1f: Principal: root; Credentials: [PROTECTED]; Authenticated: false; Details: null; Not granted any authorities
​
# 或者
​
D:\softs\JDK\jdk1.8.0_221\bin\java.exe -...com.hz.ss.authentication.AuthenticationMain
Exception in thread "main" org.springframework.security.core.userdetails.UsernameNotFoundException: 用户不存在...
    at com.hz.ss.authentication.AuthenticationMain$MyAuthenticationManager.authenticate(AuthenticationMain.java:62)
    at com.hz.ss.authentication.AuthenticationMain.main(AuthenticationMain.java:45)

❤️请注意,我们通常不需要编写任何这样的代码。该过程通常在内部进行,例如在 web 认证过滤器中。我们在这里包含的代码表明,Spring Security 中究竟是什么构成了身份验证的问题有一个相当简单的答案。当SecurityContextHolder包含一个完全填充的 Authentication 对象时,用户被验证。

2、直接设置 SecurityContextHolder 内容

事实上,Spring Security 并不介意你如何将Authentication对象放在SecurityContextHolder中。唯一的关键需求是SecurityContextHolder包含一个在AbstractSecurityInterceptor(我们将在后面看到更多关于它的信息)需要授权用户操作之前表示主体的身份验证。

您可以(许多用户也这样做)编写自己的过滤器或 MVC 控制器,以提供与不基于 Spring Security 的认证系统的互操作性。例如,您可能使用容器管理身份验证,使当前用户可以从 ThreadLocal 或 JNDI 位置访问。或者,您可能为一家拥有遗留的专有身份验证系统的公司工作,该系统是您几乎无法控制的企业“标准”。在这种情况下,很容易让Spring Security 工作,并且仍然提供授权功能。你所需要做的就是编写一个过滤器(或等效的东西),它从一个位置读取第三方用户信息,构建一个 Spring Security 具体Authentication对象,并将其放入SecurityContextHolder。在这种情况下,您还需要考虑通常由内置身份验证基础设施自动处理的事情。例如,在将响应写入客户端之前,您可能需要预先创建一个HTTP会话来缓存请求之间的上下文。