现在我们对 Spring Security 体系结构及其核心类有了一个高层次的概述,让我们更仔细地看一两个核心接口及其实现,特别是AuthenticationManager
、UserDetailsService
和AccessDecisionManager
。在本文档的其余部分中,这些内容会定期出现,所以了解它们是如何配置和操作的非常重要。
一、AuthenticationManager, ProviderManager 和 AuthenticationProvider
AuthenticationManager
只是一个接口,因此实现可以是我们选择的任何东西,但它在实践中是如何工作的呢?如果我们需要检查多个身份验证数据库或不同身份验证服务(如数据库和LDAP服务器)的组合,该怎么办?
Spring Security 中的默认实现称为ProviderManager
,它不是处理身份验证请求本身,而是委托给一个配置的AuthenticationProvider
列表,依次查询每个AuthenticationProvider
,看看它是否可以执行身份验证。每个 provider [提供者]都将抛出一个异常或返回一个完全填充的Authentication
对象。还记得我们的好朋友UserDetails
和UserDetailsService
吗?如果没有,回到上一章,刷新你的记忆。验证身份验证请求最常用的方法是加载相应的UserDetails
,并根据用户输入的密码检查加载的密码。这是DaoAuthenticationProvider
使用的方法(见下文)。加载的UserDetails
对象——特别是它包含的授权权限——将在构建完整的Authentication
对象时使用,该对象从成功的身份验证中返回,并存储在SecurityContext
中。
如果你正在使用命名空间,ProviderManager
的一个实例将在内部创建和维护,你可以通过使用命名空间身份验证提供者元素将提供者添加到它中(参见命名空间一章)。在这种情况下,您不应该在应用程序上下文中声明ProviderManager
bean。但是,如果你不使用命名空间,你可以这样声明:
<bean id="authenticationManager" class="org.springframework.security.authentication.ProviderManager">
<constructor-arg>
<list>
<ref local="daoAuthenticationProvider"/>
<ref local="anonymousAuthenticationProvider"/>
<ref local="ldapAuthenticationProvider"/>
</list>
</constructor-arg>
</bean>
在上面的示例中,我们有三个提供程序。按照显示的顺序(通过使用List隐含了这一点)尝试它们,每个提供者都能够尝试身份验证,或者通过返回null
跳过身份验证。如果所有实现都返回null
,则ProviderManager
将抛出ProviderNotFoundException
异常。如果您有兴趣了解更多关于链接提供程序的知识,请参考ProviderManager
Javadoc。
身份验证机制(如web表单登录处理过滤器)通过对ProviderManager
的引用注入,并调用它来处理身份验证请求。您需要的提供者有时可以与身份验证机制互换,而在其他时候,它们将依赖于特定的身份验证机制。例如,DaoAuthenticationProvider
和LdapAuthenticationProvider
与任何提交简单用户名/密码身份验证请求的机制兼容,因此将用于基于表单的登录或HTTP基本身份验证。另一方面,一些身份验证机制创建的身份验证请求对象只能由单一类型的AuthenticationProvider
解释。这方面的一个例子是JA-SIG CAS,它使用了服务票据的概念,因此只能由CasAuthenticationProvider
进行身份验证。您不必过于关注这一点,因为如果您忘记注册合适的提供者,在尝试进行身份验证时,您将只会收到ProviderNotFoundException
。
1.1 在身份验证成功后清除凭据
默认情况下(从Spring Security 3.1开始),ProviderManager
将尝试从成功的认证请求返回的认证对象中清除任何敏感的凭据信息。这可以防止像密码这样的信息被保留得超过必要的时间。
当您使用用户对象的缓存(例如,在无状态应用程序中提高性能)时,这可能会导致问题。如果身份验证包含对缓存中的对象(例如UserDetails
实例)的引用,并且该对象的凭证已被删除,那么将不再可能根据缓存的值进行身份验证。如果您正在使用缓存,则需要考虑到这一点。一个明显的解决方案是首先复制对象,要么在缓存实现中,要么在创建返回的Authentication
对象的AuthenticationProvider
中。或者,您可以禁用ProviderManager
上的eraseCredentialsAfterAuthentication
属性。有关更多信息,请参阅Javadoc。
1.2 DaoAuthenticationProvider
Spring Security 实现的最简单的认证提供者是DaoAuthenticationProvider
,它也是 Spring Security 最早支持的认证提供者之一。它利用UserDetailsService
(作为一个DAO)来查找用户名、密码和授权权限。它通过将UsernamePasswordAuthenticationToken
中提交的密码与UserDetailsService
加载的密码进行比较来验证用户。配置提供商非常简单:
<bean id="daoAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="inMemoryDaoImpl"/>
<property name="passwordEncoder" ref="passwordEncoder"/>
</bean>
PasswordEncoder
是可选的。PasswordEncoder
提供了从配置的UserDetailsService
返回的UserDetails
对象中显示的密码的编码和解码。这将在下面详细讨论。
二、UserDetailsService 实现
正如本参考指南前面提到的,大多数身份验证提供者都利用了UserDetails
和UserDetailsService
接口。回想一下UserDetailsService
的契约是一个单一方法:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
返回的UserDetails
是一个接口,它提供了 getters,保证提供非空的身份验证信息,例如用户名、密码、授予的权限以及用户帐户是启用还是禁用。大多数身份验证提供者将使用UserDetailsService
,即使用户名和密码实际上没有用作身份验证决策的一部分。他们可能只使用返回的UserDetails
对象作为其授权权限信息,因为其他一些系统(如LDAP或X.509或CAS等)已经承担了实际验证凭据的责任。
由于UserDetailsService
的实现非常简单,因此用户应该很容易使用自己选择的持久性策略检索身份验证信息。话虽如此,Spring Security 确实包括了几个有用的基础实现,我们将在下面讨论。
2.1 In-Memory Authentication 内存中的身份验证
创建自定义UserDetailsService
实现,从所选的持久性引擎提取信息,这很容易使用,但许多应用程序不需要这样的复杂性。如果您正在构建一个原型应用程序或刚刚开始集成 Spring Security,而又不想花时间配置数据库或编写UserDetailsService
实现,则尤其如此。对于这种情况,一个简单的选择是使用安全名称空间中的user-service
元素:
<user-service id="userDetailsService">
<!-- 密码前缀为{noop},以指示DelegatingPasswordEncoder应该使用NoOpPasswordEncoder。这对于生产是不安全的,但可以使读取样品更容易。通常密码应该使用BCrypt进行散列 -->
<user name="jimi" password="{noop}jimispassword" authorities="ROLE_USER, ROLE_ADMIN" />
<user name="bob" password="{noop}bobspassword" authorities="ROLE_USER" />
</user-service>
它还支持使用外部属性文件:
<user-service id="userDetailsService" properties="users.properties"/>
属性文件应该包含表单中的条目
username=password,grantedAuthority[,grantedAuthority][,enabled|disabled]
例如
jimi=jimispassword,ROLE_USER,ROLE_ADMIN,enabled
bob=bobspassword,ROLE_USER,enabled
2.2 JdbcDaoImpl
Spring Security 还包括一个UserDetailsService
,它可以从 JDBC 数据源获得身份验证信息。在内部使用了Spring JDBC,因此它避免了仅用于存储用户详细信息的全功能对象关系映射器(ORM)的复杂性。如果您的应用程序确实使用 ORM 工具,您可能更喜欢编写一个自定义UserDetailsService
来重用您可能已经创建的映射文件。返回到JdbcDaoImpl
,配置示例如下所示:
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
<property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>
<bean id="userDetailsService" class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
<property name="dataSource" ref="dataSource"/>
</bean>
你可以通过修改上面显示的DriverManagerDataSource
来使用不同的关系数据库管理系统。您还可以使用从 JNDI 获得的全局数据源,就像使用任何其他 Spring 配置一样。
2.3 Authority Groups 权限组
默认情况下,JdbcDaoImpl
在假定权限直接映射到用户的情况下加载单个用户的权限(请参阅数据库模式附录)。另一种方法是将权限划分为组,并将组分配给用户。有些人喜欢用这种方法来管理用户权限。有关如何启用组权限的更多信息,请参阅JdbcDaoImpl
Javadoc。组模式也包含在附录中。
三、案例
3.1 环境
spring=5.2.12
spring-security=5.3.6
tomcat=8.5
jdk=1.8
说明:这里 Tomcat 版本一定要求为 8.5 以上,由于我这里使用的 Spring 版本为 5.x,若使用低版本的 Tomcat 项目会报以下异常:
java.lang.NoSuchMethodError: javax.servlet.http.HttpServletRequest.changeSessionId()Ljava/lang/String
后续为了更好的理解 Spring Security,我们项目不使用 Spring Boot,Spring Boot 会把一些细节给封装和处理,对我们理解技术或框架本身不太好。
3.2 创建项目
在 Idea 中使用 Maven 创建一个普通的 JavaWeb 项目,引入相关依赖:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
</dependency>
</dependencies>
3.3 配置
3.3.1 web.xml
<!-- 3.配置 servlet -->
<servlet>
<servlet-name>springMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:application-context.xml</param-value>
</context-param>
<!-- 2.配置过滤器 在 IOC 中查找 -->
<filter>
<filter-name>webSecurityFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetBeanName</param-name>
<param-value>springSecurityFilterChain</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>webSecurityFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 1. 初始化,启动 Spring IOC 容器 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
3.3.2 springMVC-servlet.xml
说明:把这个文件放在 项目的 webapp/WEB-INF
目录下。文件名必须为 上面的web.xml
中{sevlet-name}-servlet.xml
。如此配置的好处就是我们无需在web.xml
中去引入配置文件。否则我们就需要在web.xml
中的 servlet
标签里面再配置<init-param><param-name>contextConfigLocation</param-name><param-value>classpath:myspringmvc.xml</param-value></init-param>
。
<!-- 开启扫描 controller -->
<context:component-scan base-package="com.hz.ss.controller" />
<!-- 配置 jsp 解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/" />
<property name="suffix" value=".jsp" />
</bean>
3.3.3 application-context.xml
<!-- 启动注解配置 -->
<context:annotation-config />
<!-- 配置注解扫描基础包 -->
<context:component-scan base-package="com.hz.ss" />
3.3.4 配置 UserDetailsService 实现
package com.hz.ss.config;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@EnableWebSecurity
public class WebSecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(
User.withDefaultPasswordEncoder()
.username("root")
.password("123")
.roles("ADMIN")
.build()
);
return manager;
}
}
3.3.5 创建资源
package com.hz.ss.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class IndexController {
@GetMapping("/index")
public String index() {
return "index";
}
}
3.4 验证及总结
当我们访问资源http://127.0.0.1:8080/hz_ss09/index
时,由于未做身份验证自动跳转到默认的登录页。
输入我们上面配置的验证信息,会自动跳转到我们登录前访问的页面。
这里我们来说明下 web.xml 中的 filter 配置 DelegatingFilterProxy
。
在前文 Spring Security 结构
中,我们了解类DelegatingFilterProxy
是用来代理javaee和spring的bean,所以在配置 filter 的时候,我们就不能单单配置某一个filter。当我们配置好了这个代理 Filter 之后,当项目启动的时候,会自动调用里面的两个方法,我们来看看源码。
public class DelegatingFilterProxy extends GenericFilterBean {
@Override
protected void initFilterBean() throws ServletException {
synchronized (this.delegateMonitor) {
if (this.delegate == null) {
// 如果没有指定目标bean名称,则使用过滤器名称
if (this.targetBeanName == null) {
this.targetBeanName = getFilterName();
}
// 获取Spring根应用上下文,如果可能的话,尽早初始化委托。
// 如果根应用程序上下文将在这个过滤器代理之后启动,我们将不得不求助于惰性初始化。
WebApplicationContext wac = findWebApplicationContext();
if (wac != null) {
this.delegate = initDelegate(wac);
}
}
}
}
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
String targetBeanName = getTargetBeanName();
Assert.state(targetBeanName != null, "No target bean name set");
Filter delegate = wac.getBean(targetBeanName, Filter.class);
if (isTargetFilterLifecycle()) {
delegate.init(getFilterConfig());
}
return delegate;
}
}
从方法initFilterBean()
,我们了解到了,为什么在web.xml
中,需要配置初始化参数targetBeanName
。若我们不配置该参数,项目运行时,会去IOC中查找filter的bean,此时会查找失败包异常,No bean named 'webSecurityFilter' available
。