用户认证的最常见方式之一是用户名和密码认证。因此,Spring Security为使用用户名和密码进行验证提供了全面的支持。
Reading the Username & Password
Spring Security为从HttpServletRequest中读取用户名和密码提供了以下几种内置机制:
Form Login(推荐)
Spring Security提供了获取html表单中用户名和密码的支持。本节提供了基于表单的认证在Spring Security中如何工作的细节。
让我们来看看基于表单的登录在Spring Security中是如何工作的。首先,我们看到用户是如何被重定向到登录表单的:
该图建立在SecurityFilterChain图上。
- 首先,一个用户向其未被授权的资源/private提出了一个未经认证的请求。
- Spring Security的FilterSecurityInterceptor通过抛出一个AccessDeniedException异常来表明未经认证的请求被拒绝了。
- 由于用户没有被认证,ExceptionTranslationFilter启动了开始认证,并通过配置的AuthenticationEntryPoint发送一个重定向到登录页面。在大多数情况下,AuthenticationEntryPoint是LoginUrlAuthenticationEntryPoint的一个实例。
- 然后,浏览器将请求它被重定向到的登录页面。
- 呈现登录页面。
当用户名和密码被提交后,UsernamePasswordAuthenticationFilter会对用户名和密码进行认证。UsernamePasswordAuthenticationFilter扩展了AbstractAuthenticationProcessingFilter,所以这张图看起来应该很相似。
该图建立在SecurityFilterChain图上。
- 当用户提交他们的用户名和密码时,UsernamePasswordAuthenticationFilter通过从HttpServletRequest中提取用户名和密码来创建一个UsernamePasswordAuthenticationToken,这是一种认证类型。
- 接下来,UsernamePasswordAuthenticationToken被传递到AuthenticationManager中进行认证。AuthenticationManager的细节取决于用户信息的存储方式。
- 如果认证失败:
- 清空SecurityContextHolder
- 执行RememberMeServices.loginFail,如果配置了remember me的话
- 执行AuthenticationFailureHandler
- 如果认证成功:
- SessionAuthenticationStrategy被通知有新的登录
- 一个包含完整信息的Authentication将被设置到SecurityContextHolder,之后将SecurityContextPersistenceFilter设置到HttpSession中
- 执行RememberMeServices.loginSuccess,如果配置了 remeber me的话
- ApplicationEventPublisher发布了一个InteractiveAuthenticationSuccessEvent
- 执行AuthenticationSuccessHandler。通常这是一个SimpleUrlAuthenticationSuccessHandler,当我们重定向到登录页面时,它将重定向到一个由ExceptionTranslationFilter保存的请求。
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登录表单。
<!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表格,有几个关键点:
- 该表单发送一个post请求到/login
- 该表单需要一个CSRF令牌,该令牌由Thymeleaf自动包含
- 该表单提供一个用户名username参数
- 该表单提供一个密码password参数
- 如果发现HTTP参数error,表明用户未能提供有效的用户名/密码
- 如果发现HTTP参数logout,表明用户已经成功退出登录
如果你使用的是Spring MVC,你将需要一个控制器,将GET /login映射到我们创建的登录模板。下面是一个最小的LoginController示例:
@Controllerclass LoginController {@GetMapping("/login")String login() {return "login";}}
Basic Authentication(不推荐)
本节详细介绍了Spring Security如何为基于servlet的应用程序提供对基本HTTP认证的支持。 让我们来看看HTTP基本认证是如何在Spring Security中工作的。首先,我们看到WWW-Authenticate头被送回给未认证的客户端。

该图建立在SecurityFilterChain图上。
- 首先,一个用户向其未被授权的资源/private提出了一个未经认证的请求。
- Spring Security的FilterSecurityInterceptor通过抛出一个AccessDeniedException异常来表明未经认证的请求被拒绝了。
- 由于用户没有经过认证,ExceptionTranslationFilter启动了开始认证。配置的AuthenticationEntryPoint是BasicAuthenticationEntryPoint的一个实例,它发送一个 WWW-Authentication:Basic realm=”Realm”的头的响应。RequestCache通常是一个NullRequestCache,它不保存请求,因为客户端有能力重新请求。
当客户端收到WWW-Authenticate头时,它知道它应该用一个用户名和密码重试。下面是正在处理的用户名和密码的流程:
该图建立在SecurityFilterChain图上。
- 当用户提交他们的用户名和密码时,BasicAuthenticationFilter通过从HttpServletRequest中提取用户名和密码,创建一个UsernamePasswordAuthenticationToken,这是一种认证类型。
- 接下来,UsernamePasswordAuthenticationToken被传递到AuthenticationManager中进行认证。AuthenticationManager的细节取决于用户信息的存储方式。
- 如果认证失败:
- 清空SecurityContextHolder
- 执行RememberMeServices.loginFail,如果配置了remember me的话
- 调用AuthenticationEntryPoint发送一个 WWW-Authentication:Basic realm=”Realm”的头的响应
- 如果认证成功:
- 一个包含完整信息的Authentication将被设置到SecurityContextHolder
- 执行RememberMeServices.loginSuccess,如果配置了 remeber me的话
- BasicAuthenticationFilter调用FilterChain.doFilter(request,response)来继续执行其余的应用逻辑
Spring Security的HTTP Basic认证支持在默认情况下是启用的。然而,只要提供任何基于Servlet的配置,就必须明确提供HTTP Basic。一个最小的、明确的Java配置如下:
protected void configure(HttpSecurity http) {http// ....httpBasic(withDefaults());}
Password Storage
几种存储密码的方式:
- Simple Storage with In-Memory Authentication
- Relational Databases with JDBC Authentication
- Custom data stores with UserDetailsService
- LDAP storage with LDAP Authentication
In-Memory Authentication(基于内存的认证)
Spring Security使InMemoryUserDetailsManager实现UserDetailsManager(继承自UserDetailsService),为使用基于内存检索username/password的认证提供支持。InMemoryUserDetailsManager提供对UserDetails的管理。
在下面例子中,我们使用Spring Boot CLI对密码进行编码,得到{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW的加密密码。虽然使用安全的格式存储密码,但是不利于入门时学习。
@Beanpublic 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只能用于 “入门”,不适合用于生产。
@Beanpublic UserDetailsService users() {// The builder will ensure the passwords are encoded before saving in memoryUserBuilder 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);}
你可以选择在密码前加上{noop}来表示密码不加密存储。
<user-service><user name="user"password="{noop}password"authorities="ROLE_USER" /><user name="admin"password="{noop}password"authorities="ROLE_USER,ROLE_ADMIN" /></user-service>
JDBC Authentication(基于数据库的认证)
Spring Security的JdbcDaoImpl实现了UserDetailsService,为使用基于JDBC检索用户名/密码的认证提供支持。JdbcUserDetailsManager扩展了JdbcDaoImpl,通过UserDetailsManager接口提供对UserDetails的管理。当Spring Security被配置为接受用户名/密码的认证时,它就会使用基于UserDetails的认证。
Default Schema
Spring Security为基于JDBC的认证提供了默认的sql查询语句。本节提供了默认sql查询语句所对应的默认用户数据库模型,你可以手动调整以匹配任何定制的查询和你使用的数据库方言。
User Schema
JdbcDaoImpl需要一些表来加载用户的密码、账户状态(启用或禁用)和权限(角色)列表。
:::info
The default schema is also exposed as a classpath resource named 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);
Group Schema
Spring Security同样提供了默认的群组数据库模型。
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));
Setting up a DataSource
在我们配置使用JdbcUserDetailsManager之前,我们必须创建一个数据源。在我们的例子中,我们将设置一个嵌入式的数据源,用默认的用户数据库模型进行初始化。
@BeanDataSource dataSource() {return new EmbeddedDatabaseBuilder().setType(H2).addScript("classpath:org/springframework/security/core/userdetails/jdbc/users.ddl").build();}
JdbcUserDetailsManager Bean
在下面的例子中,我们使用Spring Boot CLI对密码进行编码,得到的编码密码为{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW。
@BeanUserDetailsManager 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);return users;}
UserDetails
UserDetails由UserDetailsService返回。DaoAuthenticationProvider将验证后的UserDetails放入Authentication的principle字段中。
UserDetailsService
UserDetailsService被DaoAuthenticationProvider用来检索用户名、密码和其他属性,以便用用户名和密码进行认证。Spring Security提供了UserDetailsService的内存和JDBC实现。 你可以通过暴露一个自定义的UserDetailsService作为一个bean来定义自定义认证。例如,假设CustomUserDetailsService实现了UserDetailsService,下面将自定义认证:
:::info 只有在AuthenticationManagerBuilder没有被填充并且没有定义AuthenticationProviderBean的情况下才会使用。 :::
@BeanCustomUserDetailsService customUserDetailsService() {return new CustomUserDetailsService();}
PasswordEncoder
Spring Security的Servlet支持通过与PasswordEncoder集成来安全地存储密码。定制Spring Security使用的PasswordEncoder实现可以通过暴露PasswordEncoder Bean来完成。
DaoAuthenticationProvider
DaoAuthenticationProvider是一个AuthenticationProvider的实现,它利用UserDetailsService和PasswordEncoder来验证一个用户名和密码。
让我们来看看DaoAuthenticationProvider在Spring Security中是如何工作的。
- 读取用户名和密码的认证过滤器将一个UsernamePasswordAuthenticationToken传递给AuthenticationManager的实现ProviderManager
- ProviderManager包含众多的AuthenticationProvider,不同的AuthenticationProvider应用于不同场景下的认证,比如此处的DaoAuthenticationProvider就是用于username、password认证
- DaoAuthenticationProvider通过UserDetailsService从系统中加载UserDetails
- 然后,DaoAuthenticationProvider使用PasswordEncoder来与上一步返回的UserDetails上的密码进行比对
- 当认证成功时,返回的UsernamePasswordAuthenticationToken(principal字段=userDetails(由userDetailService返回))将被认证过滤器设置在SecurityContextHolder上
