35. X.509 认证

35.1 概述

X.509 证书认证最常见的用途是在使用 SSL 时验证服务器的身份,最常见的是在从浏览器使用 HTTPS 时。浏览器将自动检查服务器提供的证书是否由它所维护的可信证书颁发机构列表之一颁发(即数字签名)。

你还可以使用 "mutual authentication" 的SSL;然后,服务器将请求来自客户端的有效证书作为 SSL 握手的一部分。服务器将通过检查其证书由可接受的权限签名来验证客户端。如果已提供有效证书,则可以通过应用程序中的 servlet API 获得。Spring Security X.509 模块使用过滤器提取证书。它将证书映射到应用程序用户,并加载该用户的授权权限集,以便与标准 Spring Security 基础设施一起使用。

在尝试将 servlet 容器与 Spring Security 一起使用之前,你应该熟悉使用证书并为其设置客户端认证。大部分工作是创建和安装合适的证书和密钥。例如,如果使用 Tomcat,则在这里阅读 http://tomcat.apache.org/tomcat-6.0-doc/ssl-howto.html 的说明。重要的是你要先做这个工作然后再试用 Spring Security

35.2 添加 X.509 认证到你的 Web 应用程序

启用 X.509 客户端认证非常简单。只需将 <x509/> 元素添加到 HTTP 安全命名空间配置中即可。

<http>
...
	<x509 subject-principal-regex="CN=(.*?)," user-service-ref="userService"/>;
</http>

这个元素有两个可选属性:

  • subject-principal-regex。用于从证书的主体名称中提取用户名的正则表达式。默认值显示在上面。这是用户名,该用户名将传递给 UserDetailsService,以便为用户加载权限。
  • user-service-ref。这是与 X.509 一起使用的 UserDetailsService 的 bean ID。如果在应用程序上下文中只定义了一个,则不需要它。

subject-principal-regex 应该包含单个组。例如默认表达式 "CN=(.*?)," “匹配公共名称字段。因此,如果证书中的主题名是 "CN=Jimi Hendrix, OU=…​",这将给出一个 "Jimi Hendrix" 的用户名。匹配是不区分大小写的。那么,"emailAddress=(.?)," 将匹配 "EMAILADDRESS=jimi@hendrix.org,CN=…​" 给出用户名 "jimi@hendrix.org"。如果客户端呈现证书并且成功提取了有效的用户名,那么在安全上下文中应该有一个有效的 Authentication 对象。如果没有找到证书,或者找不到相应的用户,则安全上下文将保持空。这意味着你可以轻松地使用 X.509 认证与其他选项,如基于表单的登录。

35.3 在 Tomcat 中设置 SSL

Spring Security 项目中的 samples/certificate 目录中有一些预生成的证书。如果不想生成自己的 SSL,可以使用这些方法来测试。文件 server.jks 包含服务器证书、私钥和颁发证书颁发机构证书。也有一些来自示例应用程序的用户的客户端证书文件。你可以在浏览器中安装这些文件以启用 SSL 客户端认证。

要运行使用 SSL 支持的 tomcat,请将 server.jks 文件放到 tomcat conf 目录中,并向 server.xml 文件添加以下连接器

<Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true" scheme="https" secure="true"
			clientAuth="true" sslProtocol="TLS"
			keystoreFile="${catalina.home}/conf/server.jks"
			keystoreType="JKS" keystorePass="password"
			truststoreFile="${catalina.home}/conf/server.jks"
			truststoreType="JKS" truststorePass="password"
/>

如果你仍然希望 SSL 连接成功,即使客户端不提供证书,也可以将 clientAuth 设置为 want。除非使用非 X.509 认证机制,如表单认证,否则不提供证书的客户端将无法访问由 Spring Security 保护的任何对象。

36. 作为认证替换运行

36.1 概述

AbstractSecurityInterceptor 能够在安全对象回调阶段临时替换 SecurityContext 和 SecurityContextHolder 中的 Authentication 对象。只有在原始 Authentication 对象被 AuthenticationManager 和 AccessDecisionManager 成功处理时才会发生这种情况。RunAsManager 将指示在 SecurityInterceptorCallback 期间应该使用的替换 Authentication 对象(如果有的话)。

通过在安全对象回调阶段临时替换 Authentication 对象,安全调用将能够调用需要不同认证和授权凭证的其他对象。它还能够为特定的 GrantedAuthority 对象执行任何内部安全检查。因为 Spring Security 提供了许多帮助类,这些帮助类根据 SecurityContextHolder 的内容自动配置远程协议,所以这些 run-as 替换在调用远程 web 服务时特别有用

36.2 配置

RunAsManager 接口由 Spring Security 提供:

Authentication buildRunAs(Authentication authentication, Object object,
	List<ConfigAttribute> config);

boolean supports(ConfigAttribute attribute);

boolean supports(Class clazz);

第一个方法返回 Authentication 对象,该对象应该在方法调用期间替换现有的 Authentication 对象。如果方法返回 null,则指示不应进行替换。第二个方法被 AbstractSecurityInterceptor 用作其配置属性的启动验证的一部分。supports(Class) 方法由安全拦截器实现调用,以确保配置的 RunAsManager 支持安全拦截器将呈现的安全对象类型。

RunAsManager 的一个具体实现是用 Spring Security 提供的。如果任何 ConfigAttribute 以 RUN_AS_ 开头,RunAsManagerImpl 类将返回一个替换 RunAsUserToken。如果找到任何这样的 ConfigAttribute,替换的 RunAsUserToken 将包含与原始 Authentication 对象相同的主体、凭证和授予的权限,以及每个 RUN_AS_ ConfigAttribute 的新的 SimpleGrantedAuthority。每个新的 SimpleGrantedAuthority 将用 ROLE_ 前缀,接着是 RUN_AS ConfigAttribute。例如,RUN_AS_SERVER 将导致包含 ROLE_RUN_AS_SERVER 授予权限的替换 RunAsUserToken。

替换 RunAsUserToken 就像任何其他 Authentication 对象一样。它需要通过 AuthenticationManager 进行认证,可能是通过委派给合适的 AuthenticationProvider。RunAsImplAuthenticationProvider 执行这样的认证。它只接受有效的任何 RunAsUserToken 呈现。

为了确保恶意代码不创建 RunAsUserToken 并呈现它以便 RunAsImplAuthenticationProvider 保证接受,密钥的散列存储在所有生成的令牌中。RunAsManagerImpl 和 RunAsImplAuthenticationProvider 在 bean 上下文中用相同的密钥创建:

<bean id="runAsManager"
	class="org.springframework.security.access.intercept.RunAsManagerImpl">
<property name="key" value="my_run_as_password"/>
</bean>

<bean id="runAsAuthenticationProvider"
	class="org.springframework.security.access.intercept.RunAsImplAuthenticationProvider">
<property name="key" value="my_run_as_password"/>
</bean>

通过使用相同的密钥,每个 RunAsUserToken 都可以被验证,它是由一个已批准的 RunAsManagerImpl 创建的。由于安全原因,RunAsUserToken 在创建之后是不可变的。

37. Spring Security Crypto 模块

37.1 介绍

Spring Security Crypto 模块为对称加密、密钥生成和密码编码提供支持。该代码作为核心模块的一部分分发,但不依赖于任何其他 Spring Security(或 Spring)代码。

37.2 Encryptors

Encryptors 类提供了用于构建对称加密器的工厂方法。使用这个类,你可以创建 ByteEncryptors 来加密原始 byte[] 表单中的数据。还可以构造 TextEncryptors 来加密文本字符串。Encryptors 是线程安全的。

37.2.1 BytesEncryptor

使用 Encryptors.standard 工厂方法来构造一个标准 BytesEncryptor:

Encryptors.standard("password", "salt");

标准加密方法是使用 PKCS #5 的 PBKDF2(基于密码的密钥推导函数 #2)的 256-bit AES。此方法需要 Java 6。用于生成密钥的密码应该保持在安全的位置,而不是共享的。如果你的加密数据被破坏,则使用盐来防止对密钥的字典攻击。还应用了 16-byte 的随机初始化向量,因此每个加密的消息都是唯一的。

所提供的盐应该是十六进制编码的字符串形式,是随机的,并且长度至少为8字节。可以使用密钥生成器生成这样的盐:

String salt = KeyGenerators.string().generateKey(); // generates a random 8-byte salt that is then hex-encoded

37.2.2 TextEncryptor

使用 Encryptors.text 工厂方法构建标准 TextEncryptor:

Encryptors.text("password", "salt");

TextEncryptor 使用标准的字节加密器来加密文本数据。加密结果作为十六进制编码字符串返回,以便于文件系统或数据库中的存储。

使用 Encryptors.queryableText 工厂方法构造一个可查询的 TextEncryptor:

Encryptors.queryableText("password", "salt");

可查询 TextEncryptor 和标准 TextEncryptor 之间的差异与初始化向量(iv)处理有关。在可查询 TextEncryptor#encrypt 操作中使用的 iv 是共享的或不变的,并且不是随机生成的。这意味着多次加密相同的文本总是产生相同的加密结果。这是不太安全的,但是对于需要被查询的加密数据来说是必要的。可查询加密文本的一个例子是 OAuth apiKey。

37.3 密钥生成器

KeyGenerators 类为构建不同类型的密钥生成器提供了许多便利的工厂方法。使用这个类,你可以创建 BytesKeyGenerator 来生成 byte[] 密钥。还可以构造 StringKeyGenerator 来生成 string 密钥。KeyGenerators 是线程安全的。

37.3.1 BytesKeyGenerator

使用 KeyGenerators.secureRandom 工厂方法生成一个由 SecureRandom 实例支持的 BytesKeyGenerator:

BytesKeyGenerator generator = KeyGenerators.secureRandom();
byte[] key = generator.generateKey();

默认密钥长度为8字节。还有一个 KeyGenerators.secureRandom 变量,提供对密钥长度的控制:

KeyGenerators.secureRandom(16);

使用 KeyGenerators.shared 工厂方法构造一个 BytesKeyGenerator,它总是在每次调用时返回相同的密钥:

KeyGenerators.shared(16);

37.3.2 StringKeyGenerator

使用 KeyGenerators.string 工厂方法构造一个8字节的 SecureRandom KeyGenerator,hex 将每个密钥编码为字符串:

KeyGenerators.string();

37.4 密码编码

spring-security-crypto 模块的密码包提供了对密码进行编码的支持。PasswordEncoder 是中央服务接口,并具有以下签名:

public interface PasswordEncoder {

String encode(String rawPassword);

boolean matches(String rawPassword, String encodedPassword);
}

如果 rawPassword 一旦编码,等于编码密码,则匹配方法返回 true。该方法被设计为支持基于口令的认证模式。

BCryptPasswordEncoder 实现使用广泛支持的 "bcrypt" 算法来散列密码。Bcrypt 使用随机16字节的盐值,是一种故意缓慢的算法,以阻止密码破解。可以使用 "strength" 参数来调节它所做的工作量,该参数取4到31的值。值越高,计算哈希所需的工作量就越大。默认值为10。可以在部署的系统中更改此值而不影响现有密码,因为该值也存储在编码的散列中。

// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

Pbkdf2PasswordEncoder 实现使用 PBKDF2 算法来对密码进行散列。为了打败密码破解,PBKDF2 是一个故意降低速度的算法,应该调整到大约 .5 秒来验证系统上的密码。

// Create an encoder with all the defaults
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));