Spring Security提供了对身份验证的全面支持。
下面描述Spring Security在Servlet身份验证中使用的主要架构组件。
Spring Security的身份验证模型的核心是SecurityContextHolder。它包含SecurityContext。
SecurityContextHolder介绍
SecurityContextHolder是Spring Security存储身份验证的详细信息的地方。Spring Security并不关心SecurityContextHolder是如何填充的。如果它包含一个值,那么它就被用作当前经过身份验证的用户。
指定用户身份验证的最简单方法是直接设置SecurityContextHolder。
示例:设置SecurityContextHolder
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER");
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
我们首先创建一个空的SecurityContext。
重要的是要创建一个新的SecurityContext实例,而不是使用
SecurityContextHolder.getContext().setAuthentication(authentication),以避免多线程之间的竞争条件。
接下来,我们创建一个新的Authentication对象。
Spring Security并不关心在SecurityContext上设置了什么类型的Authentication实现。
这里我们使用
TestingAuthenticationToken,因为它非常简单。更常见的生产场景是UsernamePasswordAuthenticationToken(userDetails, password, authorities)。
最后,我们在SecurityContextHolder上设置SecurityContext。
Spring Security将使用此信息进行授权。
如果希望获得关于经过身份验证的主体的信息,可以通过访问SecurityContextHolder来实现。
示例:访问当前认证用户
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
默认情况下,SecurityContext使用ThreadLocal来存储这些详细信息,这意味着SecurityContext对于同一个线程中的方法总是可用的,即使SecurityContext没有显式地作为参数传递给这些方法。以这种方式使用ThreadLocal是相当安全的,如果在处理当前主体的请求后小心地清除线程。Spring Security的FilterChainProxy确保SecurityContext总是被清除。
有些应用程序并不完全适合使用ThreadLocal,因为它们处理线程的特定方式。例如,Swing客户机可能希望Java虚拟机中的所有线程都使用相同的安全上下文。
SecurityContextHolder可以在启动时配置一个策略,以指定您希望如何存储上下文。对于独立的应用程序,您将使用
SecurityContextHolder.MODE_GLOBAL策略。其他应用程序可能希望安全线程生成的线程也采用相同的安全标识。这是通过使用SecurityContextHolder.MODE_INHERITABLETHREADLOCAL实现的。您可以通过两种方式更改默认SecurityContextHolder.MODE_THREADLOCAL的模式。第一个是设置系统属性,第二个是调用SecurityContextHolder上的静态方法。大多数应用程序不需要改变默认设置,但如果需要,请查看SecurityContextHolder的JavaDoc以了解更多信息。
SecurityContext从SecurityContextHolder中获取。SecurityContext包含一个Authentication对象。
在Spring Security中,Authentication有两个主要目的:
Authentication包含如下内容:
GrantedAuthority为用户被授予的高级权限。一些例子是角色或作用域。
GrantedAuthority可以从
Authentication.getAuthorities()方法获得。此方法提供了一个GrantedAuthority对象集合。GrantedAuthority是授予主体的权限,这并不奇怪。这样的权限通常是“角色”,例如ROLE_ADMINISTRATOR或ROLE_HR_SUPERVISOR。稍后将为web授权、方法授权和域对象授权配置这些角色。Spring Security的其他部分能够解释这些权限,并期望它们存在。当使用基于用户名/密码的身份验证时,GrantedAuthority通常由UserDetailsService加载。
通常,GrantedAuthority对象是应用程序范围的权限。它们不是特定于给定的域对象。因此,您不太可能拥有一个GrantedAuthority来表示对Employee对象编号54的权限,因为如果有数千个这样的权限,您将很快耗尽内存(或者至少导致应用程序花很长时间来验证用户)。当然,Spring Security是专门设计来处理这一常见需求的,但是您可以使用项目的域对象安全功能来实现这一目的。
AuthenticationManager是定义Spring Security的过滤器如何执行身份验证的API。然后,调用AuthenticationManager的控制器(即Spring Security的Filterss)在SecurityContextHolder上设置返回的Authentication。如果你没有集成Spring Security的过滤器,你可以直接设置SecurityContextHolder,而不需要使用AuthenticationManager。
虽然AuthenticationManager的实现可以是任何内容,但最常见的实现是ProviderManager。
ProviderManager是AuthenticationManager最常用的实现。ProviderManager委托给AuthenticationProviders列表。每个AuthenticationProvider都有机会表明身份验证应该是成功的或失败的,或者表明它不能做出决定,并允许下游的AuthenticationProvider来做出决定。如果配置的AuthenticationProviders中没有一个可以进行身份验证,那么身份验证将失败,因为ProviderNotFoundException是特殊的AuthenticationException,表示ProviderManager没有配置成支持传递给它的身份验证类型。
ProvviderManager 介绍
实际上,每个AuthenticationProvider都知道如何执行特定类型的身份验证。例如,一个AuthenticationProvider可能能够验证用户名/密码,而另一个AuthenticationProvider可能能够验证SAML断言。这允许每个AuthenticationProvider执行特定类型的身份验证,同时支持多种类型的身份验证,并且只公开一个AuthenticationManager bean。
ProviderManager还允许配置一个可选的父AuthenticationManager,当AuthenticationProvider不能执行身份验证时,会咨询该父AuthenticationManager。父类可以是任何类型的AuthenticationManager,但它通常是ProviderManager的一个实例。
ProviderManager Parent介绍
事实上,多个ProviderManager实例可能共享相同的父AuthenticationManager。这在多个SecurityFilterChain实例具有某些共同身份验证(共享的父类AuthenticationManager)和不同身份验证机制(不同的ProviderManager实例)的场景中有些常见。
多个ProviderManager 同一个Parent 介绍
默认情况下,ProviderManager将尝试从成功的身份验证请求返回的Authentication对象中清除任何敏感凭证信息。这可以防止密码等信息在HttpSession中保留的时间超过必要时间
当您使用用户对象的缓存(例如,在无状态应用程序中提高性能)时,这可能会导致问题。如果Authentication包含对缓存中的对象(如UserDetails实例)的引用,并且该引用已删除其凭证,那么将不再能够根据缓存的值进行身份验证。如果您正在使用缓存,则需要考虑到这一点。一个明显的解决方案是,首先在缓存实现中或在创建返回的Authentication对象的AuthenticationProvider中复制一个对象。或者,您可以禁用ProviderManager上的
eraseCredentialsAfterAuthentication属性
多个AuthenticationProviders可以被注入到ProviderManager中。每个AuthenticationProvider执行特定类型的身份验证。例如,DaoAuthenticationProvider支持基于用户名/密码的身份验证,而JwtAuthenticationProvider支持验证JWT令牌。
AuthenticationEntryPoint用于从客户端发送一个HTTP响应请求凭证。有时客户端将主动包括凭证,例如请求资源的用户名/密码。在这些情况下,Spring Security不需要提供从客户端请求凭证的HTTP响应,因为它们已经包含在其中了。
在其他情况下,客户端将向未被授权访问的资源发出未经身份验证的请求。在本例中,AuthenticationEntryPoint的实现用于客户端的请求凭证。AuthenticationEntryPoint实现可能执行重定向到一个登录页面,响应一个WWW-Authenticate头,等等。
AbstractAuthenticationProcessingFilter被用作验证用户凭据的基本过滤器。在验证凭证之前,Spring Security通常使用AuthenticationEntryPoint请求凭证。接下来,AbstractAuthenticationProcessingFilter可以验证提交给它的任何身份验证请求。
AbstractAuthenticationProcessingFilter 介绍
①当用户提交他们的凭证时,
AbstractAuthenticationProcessingFilter从HttpServletRequest创建一个Authentication来进行身份验证。创建的身份验证类型依赖于AbstractAuthenticationProcessingFilter的子类。例如,UsernamePasswordAuthenticationFilter从HttpServletRequest中提交的用户名和密码创建UsernamePasswordAuthenticationToken。
②接下来,将Authentication传递给AuthenticationManager进行身份验证。
③如果身份验证失败,则失败:
④如果身份验证成功,则成功。
验证用户身份的最常见方法之一是验证用户名和密码。因此,Spring Security提供了对使用用户名和密码进行身份验证的全面支持。
读取用户名和密码
Spring Security提供了以下内置机制来从HttpServletRequest读取用户名和密码:
存储机制
每种受支持的读取用户名和密码的机制都可以利用任何受支持的存储机制:
Spring Security支持通过html表单提供用户名和密码。下面详细介绍基于表单的身份验证如何在Spring Security中工作。
让我们看看基于表单的登录是如何在Spring Security中工作的。首先,我们将看到如何将用户重定向到登录表单页面。
重定向到登录页面
该图构建了我们的SecurityFilterChain图解。
①首先,用户向未授权的资源/private发出未经身份验证的请求。
②Spring Security的FilterSecurityInterceptor通过抛出AccessDeniedException来拒绝未经身份验证的请求。
③由于用户没有经过身份验证,
ExceptionTranslationFilter将启动Start Authentication并发送一个重定向到配置了AuthenticationEntryPoint的登录页面。在大多数情况下,AuthenticationEntryPoint是LoginUrlAuthenticationEntryPoint的一个实例。
④然后,浏览器将请求重定向到登录页面。
⑤应用程序内的某些东西必须呈现登录页面。
提交用户名和密码后,
UsernamePasswordAuthenticationFilter将对用户名和密码进行验证。UsernamePasswordAuthenticationFilter扩展了AbstractAuthenticationProcessingFilter,所以下面的流程图看起来应该很相似。
验证用户名和密码
该图构建了我们的SecurityFilterChain图解。
①当用户提交他们的用户名和密码时,
UsernamePasswordAuthenticationFilter通过从HttpServletRequest中提取用户名和密码创建UsernamePasswordAuthenticationToken,这是一种Authentication类型。
②接下来,将
UsernamePasswordAuthenticationToken传递到AuthenticationManager中进行身份验证。AuthenticationManager的详细信息取决于用户信息的存储方式。
③如果身份验证失败,则失败
④如果身份验证成功,则成功。
Spring Security表单登录在默认情况下是启用的。但是,只要提供了任何基于servlet的配置,就必须显式地提供基于表单的登录。
一个最小的、显式的Java配置如下所示:
protected void configure(HttpSecurity http) {
http
// ...
.formLogin(withDefaults());
}
在这个配置中,Spring Security将呈现一个默认的登录页面。大多数生产应用程序都需要一个自定义的登录表单。
下面的配置演示了如何在表单中提供自定义登录页面。
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.formLogin(form -> form
.loginPage("/login")
.permitAll()
);
}
当在Spring Security配置中指定登录页面时,您将负责呈现该页面。下面是一个Thymeleaf模板,它生成一个符合/login登录页面的HTML登录表单:
示例:登录表单
路径:
src/main/resources/templates/login.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<title>Please Log In</title>
</head>
<body>
<h1>Please Log In</h1>
<div th:if="${param.error}">
Invalid username and password.</div>
<div th:if="${param.logout}">
You have been logged out.</div>
<form th:action="@{/login}" method="post">
<div>
<input type="text" name="username" placeholder="Username"/>
</div>
<div>
<input type="password" name="password" placeholder="Password"/>
</div>
<input type="submit" value="Log in" />
</form>
</body>
</html>
关于默认HTML表单有几个关键点:
许多用户只需要定制登录页面。但是,如果需要的话,上面的一切都可以通过额外的配置进行定制。
如果您正在使用Spring MVC,您将需要一个控制器,将GET /login映射到我们创建的登录模板。LoginController的最小示例如下:
@Controller
class LoginController {
@GetMapping("/login")
String login() {
return "login";
}
}
下面详细介绍Spring Security如何为基于servlet的应用程序提供对基本HTTP身份验证的支持。
让我们看看HTTP基本身份验证是如何在Spring Security中工作的。首先,我们看到WWW-Authenticate头被发回给一个未经过身份验证的客户端。
发送WWW-Authenticate头
该图构建了我们的SecurityFilterChain图解。
①首先,用户向未授权的资源/private发出未经身份验证的请求。
②Spring Security的FilterSecurityInterceptor通过抛出AccessDeniedException来拒绝未经身份验证的请求。
③由于用户没有经过身份验证,
ExceptionTranslationFilter将启动启动身份验证。配置的AuthenticationEntryPoint是一个BasicAuthenticationEntryPoint的实例,它发送一个WWW-Authenticate头。RequestCache通常是一个不保存请求的NullRequestCache,因为客户端能够重复它最初请求的请求。
当客户端接收到WWW-Authenticate报头时,它知道应该用用户名和密码重试。下面是正在处理的用户名和密码的流程。
验证用户名和密码
该图构建了我们的SecurityFilterChain图解。
①当用户提交他们的用户名和密码时,BasicAuthenticationFilter通过从HttpServletRequest中提取用户名和密码来创建
UsernamePasswordAuthenticationToken,这是一种身份验证类型。
②接下来,将
UsernamePasswordAuthenticationToken传递到AuthenticationManager中进行身份验证。AuthenticationManager的详细信息取决于用户信息的存储方式。
③如果身份验证失败,则失败
④如果身份验证成功,则成功。
默认情况下,Spring Security的HTTP基本身份验证支持是启用的。但是,只要提供了任何基于servlet的配置,就必须显式地提供HTTP Basic。
一个最小的,显式的配置如下所示:
protected void configure(HttpSecurity http) {
http
// ...
.httpBasic(withDefaults());
}
下面详细介绍Spring Security如何提供摘要身份验证支持,摘要身份验证是由
DigestAuthenticationFilter提供的。
您不应该在现代应用程序中使用摘要身份验证,因为它被认为不安全。最明显的问题是必须以明文、加密或MD5格式存储密码。所有这些存储格式都是不安全的。相反,您应该使用单向自适配密码哈希(即bCrypt, PBKDF2, SCrypt等)存储凭证,这是摘要认证不支持的。
摘要身份验证试图解决基本身份验证的许多弱点,特别是通过确保凭证永远不会通过网络以明文发送。许多浏览器支持摘要身份验证。
管理HTTP摘要身份验证的标准由RFC 2617定义,它更新了RFC 2069规定的摘要身份验证标准的早期版本。大多数用户代理实现RFC 2617。Spring Security的摘要认证支持与RFC 2617规定的“auth”质量保护(qop)兼容,它还提供了与RFC 2069的向后兼容性。摘要身份验证被认为是一个更有吸引力的选择,如果你需要使用未加密的HTTP(即没有TLS/HTTPS),并希望最大限度地安全性的身份验证过程。无论如何,每个人都应该使用HTTPS。
摘要中心身份验证是一个“nonce”。这是服务器生成的值。Spring Security的nonce采用以下格式:
示例:摘要语法
base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
expirationTime: The date and time when the nonce expires, expressed in milliseconds
key: A private key to prevent modification of the nonce token
你需要确保你配置了不安全的明文密码存储使用NoOpPasswordEncoder 。以下是使用Java配置配置摘要认证的示例:
@Autowired
UserDetailsService userDetailsService;
DigestAuthenticationEntryPoint entryPoint() {
DigestAuthenticationEntryPoint result = new DigestAuthenticationEntryPoint();
result.setRealmName("My App Relam");
result.setKey("3028472b-da34-4501-bfd8-a355c42bdf92");
}
DigestAuthenticationFilter digestAuthenticationFilter() {
DigestAuthenticationFilter result = new DigestAuthenticationFilter();
result.setUserDetailsService(userDetailsService);
result.setAuthenticationEntryPoint(entryPoint());
}
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.exceptionHandling(e -> e.authenticationEntryPoint(authenticationEntryPoint()))
.addFilterBefore(digestFilter());
}
Spring Security的
InMemoryUserDetailsManager实现了UserDetailsService,以支持在内存中检索的基于用户名/密码的身份验证。InMemoryUserDetailsManager通过实现UserDetailsManager接口来提供对UserDetails的管理。当Spring Security配置为接受用户名/密码进行身份验证时,将使用基于UserDetails的身份验证。
在这个示例中,我们使用Spring Boot CLI对password的密码进行编码,并获得编码后的密码{bcrypt}$2a$10$
GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW。
示例:
InMemoryUserDetailsManager的java配置示例
@Bean
public UserDetailsService users() {
UserDetails user = User.builder()
.username("user")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
上面的示例以安全的格式存储密码,但是在开始体验方面还有很多不足之处。在下面的示例中,我们利用了
User.withDefaultPasswordEncoder来确保存储在内存中的密码是受保护的。但是,它不能通过反编译源代码来防止获得密码。出于这个原因,User.withDefaultPasswordEncoder应该只用于“入门”,而不是用于生产。
示例:使用
User.withDefaultPasswordEncoder的InMemoryUserDetailsManager
@Bean
public UserDetailsService users() {
// The builder will ensure the passwords are encoded before saving in memory
UserBuilder users = User.withDefaultPasswordEncoder();
UserDetails user = users
.username("user")
.password("password")
.roles("USER")
.build();
UserDetails admin = users
.username("admin")
.password("password")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
Spring Security的JdbcDaoImpl实现了UserDetailsService来提供对使用JDBC检索的基于用户名/密码的身份验证的支持。JdbcUserDetailsManager扩展了JdbcDaoImpl,通过UserDetailsManager接口提供对UserDetails的管理。当Spring Security配置为接受用户名/密码进行身份验证时,将使用基于UserDetails的身份验证。
在下面的内容中,我们将讨论:
默认模式
Spring Security为基于JDBC的身份验证提供默认查询。本节提供与默认查询相对应的默认模式。您将需要调整模式,以匹配与您正在使用的查询和数据库方言相匹配的定制。
用户模式
JdbcDaoImpl需要表来加载用户的密码、帐户状态(启用或禁用)和权限(角色)列表。需要的默认模式可以在下面找到。
默认模式也公开为一个名为
org/springframework/security/core/userdetails/jdbc/users.ddl的类路径资源。
示例:默认用户模式
create table users(
username varchar_ignorecase(50) not null primary key,
password varchar_ignorecase(500) not null,
enabled boolean not null
);
create table authorities (
username varchar_ignorecase(50) not null,
authority varchar_ignorecase(50) not null,
constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username,authority);
Oracle是一种流行的数据库选择,但是需要略微不同的模式。您可以在下面找到用于用户的默认Oracle模式。
针对Oracle的默认用户模式
CREATE TABLE USERS (
USERNAME NVARCHAR2(128) PRIMARY KEY,
PASSWORD NVARCHAR2(128) NOT NULL,
ENABLED CHAR(1) CHECK (ENABLED IN ('Y','N') ) NOT NULL
);
CREATE TABLE AUTHORITIES (
USERNAME NVARCHAR2(128) NOT NULL,
AUTHORITY NVARCHAR2(128) NOT NULL
);
ALTER TABLE AUTHORITIES ADD CONSTRAINT AUTHORITIES_UNIQUE UNIQUE (USERNAME, AUTHORITY);
ALTER TABLE AUTHORITIES ADD CONSTRAINT AUTHORITIES_FK1 FOREIGN KEY (USERNAME) REFERENCES USERS (USERNAME) ENABLE;
组模式
如果您的应用程序需要组,您将需要提供组模式。组的默认模式可以在下面找到。
默认组模式:
create table groups (
id bigint generated by default as identity(start with 0) primary key,
group_name varchar_ignorecase(50) not null
);
create table group_authorities (
group_id bigint not null,
authority varchar(50) not null,
constraint fk_group_authorities_group foreign key(group_id) references groups(id)
);
create table group_members (
id bigint generated by default as identity(start with 0) primary key,
username varchar(50) not null,
group_id bigint not null,
constraint fk_group_members_group foreign key(group_id) references groups(id)
);
设置数据源
在配置JdbcUserDetailsManager之前,必须创建一个数据源。在我们的示例中,我们将设置一个使用默认用户模式初始化的嵌入式数据源。
示例:嵌入式数据来源
@Bean
DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(H2)
.addScript("classpath:org/springframework/security/core/userdetails/jdbc/users.ddl")
.build();
}
在生产环境中,您将希望确保建立到外部数据库的连接。
JdbcUserDetailsManager Bean
在这个示例中,我们使用Spring Boot CLI对password的密码进行编码,并获得编码后的密码{bcrypt}$2a$10$
GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW。
示例:JdbcUserDetailsManager
@Bean
UserDetailsManager users(DataSource dataSource) {
UserDetails user = User.builder()
.username("user")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER", "ADMIN")
.build();
JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
users.createUser(user);
users.createUser(admin);
}
UserDetails
UserDetails由UserDetailsService返回。DaoAuthenticationProvider验证UserDetails,然后返回一个Authentication,该Authentication有一个主体,该主体是由已配置的UserDetailsService返回的UserDetails。
UserDetailsService
DaoAuthenticationProvider使用UserDetailsService检索用户名、密码和其他属性,以验证用户名和密码。Spring Security提供了UserDetailsService的内存和JDBC实现。
您可以通过将自定义UserDetailsService公开为bean来定义自定义身份验证。例如,以下将自定义身份验证,假设CustomUserDetailsService实现了UserDetailsService:
只有当
AuthenticationManagerBuilder没有被填充并且AuthenticationProviderBean没有被定义时才会使用。
@Bean
CustomUserDetailsService customUserDetailsService() {
return new CustomUserDetailsService();
}
PasswordEncoder
Spring Security的servlet通过与PasswordEncoder集成来支持安全存储密码。定制Spring Security使用的PasswordEncoder实现可以通过公开PasswordEncoder Bean来完成。
DaoAuthenticationProvider
DaoAuthenticationProvider是一个AuthenticationProvider实现,它利用UserDetailsService和PasswordEncoder来验证用户名和密码。让我们看看DaoAuthenticationProvider是如何在Spring Security中工作的。图中解释了读取用户名和密码中的AuthenticationManager如何工作的细节。
使用DaoAuthenticationProvider
①读取用户名和密码的身份验证过滤器将
UsernamePasswordAuthenticationToken传递给AuthenticationManager,这是由ProviderManager实现的。
②ProviderManager被配置为使用DaoAuthenticationProvider类型的AuthenticationProvider。
③DaoAuthenticationProvider从UserDetailsService中查找UserDetails。
④然后,DaoAuthenticationProvider使用PasswordEncoder验证上一步返回的UserDetails上的密码。
⑤当身份验证成功时,返回的身份验证类型为
UsernamePasswordAuthenticationToken,并且具有一个主体,该主体是由已配置的UserDetailsService返回的UserDetails。最终,返回的UsernamePasswordAuthenticationToken将由身份验证过滤器在SecurityContextHolder上设置。
LDAP经常被企业用作用户信息的中心存储库和身份验证服务。它还可以用于存储应用程序用户的角色信息。
当Spring Security被配置为接受用户名/密码进行身份验证时,Spring Security将使用基于LDAP的身份验证。但是,尽管利用用户名/密码进行身份验证,但它并没有使用UserDetailsService集成,因为在绑定身份验证中,LDAP服务器没有返回密码,因此应用程序不能执行密码验证。
对于如何配置LDAP服务器,有许多不同的场景,因此Spring Security的LDAP提供者是完全可配置的。它使用单独的策略接口进行身份验证和角色检索,并提供可以配置为处理各种情况的缺省实现。
先决条件
在尝试将LDAP与Spring Security一起使用之前,您应该熟悉LDAP。下面的链接很好地介绍了相关的概念,并提供了使用免费LDAP服务器OpenLDAP设置目录的指南
:https://www.zytrax.com/books/ldap/。熟悉一些用于从Java访问LDAP的JNDI api可能也很有用。我们在LDAP提供程序中没有使用任何第三方LDAP库(Mozilla、JLDAP等),但是Spring LDAP得到了广泛的使用,所以如果您计划添加自己的自定义,对该项目有所了解可能会有所帮助。
在使用LDAP身份验证时,一定要确保正确配置LDAP连接池。如果您不熟悉如何做到这一点,可以参考Java LDAP文档(
https://docs.oracle.com/javase/jndi/tutorial/ldap/connect/config.html)。
设置嵌入式LDAP服务器
您需要做的第一件事是确保有一个LDAP Server来指向您的配置。为简单起见,最好从嵌入式LDAP Server开始。Spring Security支持使用以下任意一种:
在下面的示例中,我们将下面的users.ldif作为类路径资源来初始化嵌入的LDAP服务器,其中用户user和admin的密码都是password。
users.ldif的内容
dn: ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: groups
dn: ou=people,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: people
dn: uid=admin,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: .NETOrgPerson
cn: Rod Johnson
sn: Johnson
uid: admin
userPassword: password
dn: uid=user,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Dianne Emu
sn: Emu
uid: user
userPassword: password
dn: cn=user,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: user
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org
uniqueMember: uid=user,ou=people,dc=springframework,dc=org
dn: cn=admin,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: admin
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org
嵌入式UnboundID服务器
如果你想使用UnboundID,请指定以下依赖项:
UnboundID依赖项Maven:
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>4.0.14</version>
<scope>runtime</scope>
</dependency>
然后可以配置嵌入式LDAP服务器
示例:嵌入式LDAP服务器配置
@Bean
UnboundIdContainer ldapContainer() {
return new UnboundIdContainer("dc=springframework,dc=org",
"classpath:users.ldif");
}
嵌入式ApacheDS服务器
Spring Security使用不再维护的ApacheDS 1.x。不幸的是ApacheDS 2.x只发布了里程碑版本,没有稳定的版本。一旦ApacheDS 2.x稳定版本发布了,我们会考虑更新。
如果你想使用Apache DS,那么指定以下依赖项:
ApacheDS的Maven依赖项:
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-core</artifactId>
<version>1.5.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-server-jndi</artifactId>
<version>1.5.5</version>
<scope>runtime</scope>
</dependency>
然后可以配置嵌入式LDAP服务器:
示例:嵌入式LDAP服务器配置
@Bean
ApacheDSContainer ldapContainer() {
return new ApacheDSContainer("dc=springframework,dc=org",
"classpath:users.ldif");
}
LDAP ContextSource
一旦LDAP服务器指向您的配置,您需要将Spring Security配置为指向应该用于对用户进行身份验证的LDAP服务器。这是通过创建LDAP ContextSource来完成的,它相当于JDBC数据源。
示例:LDAP Context Source
ContextSource contextSource(UnboundIdContainer container) {
return new DefaultSpringSecurityContextSource("ldap://localhost:53389/dc=springframework,dc=org");
}
Authentication
Spring Security的LDAP支持不使用UserDetailsService,因为LDAP绑定身份验证不允许客户端读取密码,甚至是密码的哈希版本。这意味着Spring Security无法读取密码并对其进行身份验证。
因此,LDAP支持是使用LdapAuthenticator接口实现的。LdapAuthenticator还负责检索任何必需的用户属性。这是因为属性上的权限可能取决于所使用的身份验证类型。例如,如果绑定为用户,可能需要使用用户自己的权限读取它们。
Spring Security提供了两个LdapAuthenticator实现:
使用绑定验证
绑定身份验证是使用LDAP对用户进行身份验证的最常用机制。在绑定身份验证中,用户凭证(即用户名/密码)被提交给LDAP服务器,由LDAP服务器对用户进行身份验证。使用绑定身份验证的好处是,用户的秘密信息(即密码)不需要暴露给客户端,这有助于防止它们泄露。
下面是绑定身份验证配置的示例。
@Bean
BindAuthenticator authenticator(BaseLdapPathContextSource contextSource) {
BindAuthenticator authenticator = new BindAuthenticator(contextSource);
authenticator.setUserDnPatterns(new String[] { "uid={0},ou=people" });
return authenticator;
}
@Bean
LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator) {
return new LdapAuthenticationProvider(authenticator);
}
这个简单的示例通过在提供的模式中替换用户登录名并尝试将登录密码绑定为该用户来获得用户DN。如果您的所有用户都存储在目录中的单个节点下,那么这是可以的。如果你想要配置一个LDAP搜索过滤器来定位用户,你可以使用以下方法:
@Bean
BindAuthenticator authenticator(BaseLdapPathContextSource contextSource) {
String searchBase = "ou=people";
String filter = "(uid={0})";
FilterBasedLdapUserSearch search =
new FilterBasedLdapUserSearch(searchBase, filter, contextSource);
BindAuthenticator authenticator = new BindAuthenticator(contextSource);
authenticator.setUserSearch(search);
return authenticator;
}
@Bean
LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator) {
return new LdapAuthenticationProvider(authenticator);
}
如果与上面的ContextSource定义一起使用,这将使用(uid={0})作为过滤器在DN ou=people,dc=springframework,dc=org下执行搜索。同样,用户登录名被替换为筛选器名称中的参数,因此它将搜索uid属性等于用户名的条目。如果没有提供用户搜索库,则从根目录执行搜索。
使用密码身份验证
密码比较是将用户提供的密码与存储在存储库中的密码进行比较。这可以通过检索密码属性的值并在本地进行检查来完成,也可以通过执行LDAP“比较”操作来完成,在该操作中,将提供的密码传递给服务器进行比较,而永远不会检索真正的密码值。如果密码用随机的盐值正确哈希,则无法进行LDAP比较。
示例:最小密码比较配置
@Bean
PasswordComparisonAuthenticator authenticator(BaseLdapPathContextSource contextSource) {
return new PasswordComparisonAuthenticator(contextSource);
}
@Bean
LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator) {
return new LdapAuthenticationProvider(authenticator);
}
下面是一个更高级的配置,包含一些自定义。
示例:密码比较配置
@Bean
PasswordComparisonAuthenticator authenticator(BaseLdapPathContextSource contextSource) {
PasswordComparisonAuthenticator authenticator =
new PasswordComparisonAuthenticator(contextSource);
authenticator.setPasswordAttributeName("pwd");
authenticator.setPasswordEncoder(new BCryptPasswordEncoder());
return authenticator;
}
@Bean
LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator) {
return new LdapAuthenticationProvider(authenticator);
}
①指定密码属性为pwd
②使用BCryptPasswordEncoder
LdapAuthoritiesPopulator
Spring Security的ldapauthortiespopulator用于确定为用户返回什么权限。
示例:LdapAuthoritiesPopulator配置
@Bean
LdapAuthoritiesPopulator authorities(BaseLdapPathContextSource contextSource) {
String groupSearchBase = "";
DefaultLdapAuthoritiesPopulator authorities =
new DefaultLdapAuthoritiesPopulator(contextSource, groupSearchBase);
authorities.setGroupSearchFilter("member={0}");
return authorities;
}
@Bean
LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator, LdapAuthoritiesPopulator authorities) {
return new LdapAuthenticationProvider(authenticator, authorities);
}
Active Directory
Active Directory支持它自己的非标准身份验证选项,并且正常的使用模式不太适合标准
LdapAuthenticationProvider。通常使用域用户名(格式为user@domain)执行身份验证,而不是使用LDAP专有名称。为了简化这一点,Spring Security提供了一个为典型的Active Directory设置定制的身份验证提供程序。
配置
ActiveDirectoryLdapAuthenticationProvider非常简单。您只需要提供域名和提供服务器地址的LDAP URL。下面是一个配置示例:
@Bean
ActiveDirectoryLdapAuthenticationProvider authenticationProvider() {
return new ActiveDirectoryLdapAuthenticationProvider("example.com", "ldap://company.example.com/");
}
HTTP会话相关的功能是通过SessionManagementFilter和
SessionAuthenticationStrategy接口的组合来处理的,该接口由过滤器委托给它。典型的应用包括会话固定保护、攻击预防、会话超时检测和限制通过身份验证的用户可以同时打开的会话数量。
您可以配置Spring Security来检测无效会话ID的提交,并将用户重定向到适当的URL。这是通过会话管理元素实现的:
protected void configure(HttpSecurity http) {
http.sessionManagement().invalidSessionUrl("/invalidSession.htm");
}
注意,如果您使用这种机制来检测会话超时,如果用户登出,然后在没有关闭浏览器的情况下重新登录,它可能会错误地报告错误。这是因为当您使会话失效时,会话cookie不会被清除,即使用户已经注销,它也会被重新提交。你可以在登出时显式地删除JSESSIONID cookie,例如在登出处理程序中使用以下语法:
protected void configure(HttpSecurity http) {
http.logout().deleteCookies("JSESSIONID");
}
不幸的是,这不能保证对每个servlet容器都适用,所以您需要在您的环境中测试它。
如果您在代理服务器后运行应用程序,您还可以通过配置代理服务器来删除会话cookie。例如,使用Apache HTTPD的mod_headers,下面的指令会在登出请求的响应中使JSESSIONID过期,从而删除JSESSIONID cookie(假设应用程序部署在路径为 /tutorial下):
<LocationMatch "/tutorial/logout">
Header always set Set-Cookie "JSESSIONID=;Path=/tutorial;Expires=Thu, 01 Jan 1970 00:00:00 GMT"
</LocationMatch>
如果您希望对单个用户登录到您的应用程序的能力进行限制,Spring Security通过以下简单的附加功能提供了开箱即用的支持。首先,你需要将以下侦听器添加到你的web.xml文件中,以保持Spring Security关于会话生命周期事件的更新:
<listener>
<listener-class>
org.springframework.security.web.session.HttpSessionEventPublisher
</listener-class>
</listener>
然后将以下几行添加到你的应用程序上下文中:
<http>
...
<session-management>
<concurrency-control max-sessions="1" />
</session-management>
</http>
这将防止用户多次登录,第二次登录将导致第一次登录无效。通常您希望防止第二次登录,在这种情况下您可以使用:
<http>
...
<session-management>
<concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />
</session-management>
</http>
第二次登录将被拒绝。通过“拒绝”,我们的意思是,如果使用基于表单的登录,用户将被发送到
authentication-failure-url。如果第二次身份验证是通过另一种非交互机制进行的,比如“remember-me”,一个“unauthorized”(401)错误将被发送给客户端。如果希望使用错误页面,可以将属性session-authentication-error-url添加到会话管理元素。
如果您正在为基于表单的登录使用自定义身份验证过滤器,那么您必须显式地配置并发会话控制支持。
会话固定攻击是一个潜在的风险,在这种情况下,恶意攻击者可能通过访问一个站点来创建一个会话,然后说服另一个用户使用相同的会话登录(例如,通过向他们发送一个包含会话标识符作为参数的链接)。Spring Security通过在用户登录时创建新会话或更改会话ID来自动防止这种情况发生。如果您不需要这种保护,或者它与其他一些需求冲突,您可以使用<session-management>上的
session-fixation-protection属性来控制行为,该属性有四个选项:
当会话固定保护发生时,它会导致在应用程序上下文中发布
SessionFixationProtectionEvent。如果您使用changeSessionId,此保护也将导致任何javax.servlet.http. httpessionidlistener被通知,所以如果您的代码侦听这两个事件,请谨慎使用。
SessionManagementFilter检查SecurityContextRepository的内容和反对的当前SecurityContextHolder内容是否在当前请求用户已经通过身份验证,通常由非交互式验证机制,如pre-authentication或remember-me。如果存储库包含安全上下文,则过滤器不执行任何操作。如果没有,并且本地线程SecurityContext包含一个(非匿名的)Authentication对象,那么过滤器假定它们已经通过堆栈中先前的过滤器进行了身份验证。然后它将调用已配置的
SessionAuthenticationStrategy。
如果用户当前没有经过身份验证,该过滤器将检查是否请求了一个无效的会话ID(例如,由于超时),并将调用配置的InvalidSessionStrategy(如果设置了一个)。最常见的行为就是重定向到一个固定的URL,这被封装在标准实现
SimpleRedirectInvalidSessionStrategy中。
SessionAuthenticationStrategy被SessionManagementFilter和AbstractAuthenticationProcessingFilter使用,所以如果你使用一个定制的表单登录类,例如,你将需要将它注入到这两个类中。在这种情况下,结合命名空间和自定义bean的典型配置可能是这样的:
<http>
<custom-filter position="FORM_LOGIN_FILTER" ref="myAuthFilter" />
<session-management session-authentication-strategy-ref="sas"/>
</http>
<beans:bean id="myAuthFilter" class=
"org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<beans:property name="sessionAuthenticationStrategy" ref="sas" />
...
</beans:bean>
<beans:bean id="sas" class=
"org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy" />
请注意,如果您将bean存储在实现httpessionbindinglistener的会话中(包括Spring会话范围内的bean),使用缺省的
SessionFixationProtectionStrategy可能会导致问题。有关这个类的更多信息,请参阅Javadoc。
Spring Security能够防止主体对同一应用程序的并发身份验证次数超过指定的次数。许多isv利用这一点来实施许可,而网络管理员喜欢这个特性,因为它有助于防止人们共享登录名。例如,您可以阻止用户“Batman”从两个不同的会话登录到web应用程序。您可以使他们之前的登录失效,也可以在他们试图再次登录时报告错误,以防止第二次登录。注意,如果您使用第二种方法,没有显式登出的用户(例如,刚刚关闭浏览器的用户)将不能再次登录,直到原始会话过期。
命名空间支持并发控制,因此请检查前面的命名空间章节以获得最简单的配置。但有时你需要定制一些东西。
该实现使用了
SessionAuthenticationStrategy的一个特殊版本,称为ConcurrentSessionControlAuthenticationStrategy。
以前,并发身份验证检查是由ProviderManager进行的,它可以被注入一个
ConcurrentSessionController。后者将检查用户是否试图超过允许的会话数。但是,这种方法要求预先创建HTTP会话,这是不可取的。在Spring Security 3中,用户首先通过AuthenticationManager进行身份验证,一旦验证成功,将创建一个会话,并检查是否允许打开另一个会话。