用户认证的最常见方式之一是用户名和密码认证。因此,Spring Security为使用用户名和密码进行验证提供了全面的支持。

Reading the Username & Password

Spring Security为从HttpServletRequest中读取用户名和密码提供了以下几种内置机制:

Form Login(推荐)

Spring Security提供了获取html表单中用户名和密码的支持。本节提供了基于表单的认证在Spring Security中如何工作的细节。

让我们来看看基于表单的登录在Spring Security中是如何工作的。首先,我们看到用户是如何被重定向到登录表单的:
Username/Password Authentication - 图1
该图建立在SecurityFilterChain图上。

  1. 首先,一个用户向其未被授权的资源/private提出了一个未经认证的请求。
  2. Spring Security的FilterSecurityInterceptor通过抛出一个AccessDeniedException异常来表明未经认证的请求被拒绝了。
  3. 由于用户没有被认证,ExceptionTranslationFilter启动了开始认证,并通过配置的AuthenticationEntryPoint发送一个重定向到登录页面。在大多数情况下,AuthenticationEntryPoint是LoginUrlAuthenticationEntryPoint的一个实例。
  4. 然后,浏览器将请求它被重定向到的登录页面。
  5. 呈现登录页面。

当用户名和密码被提交后,UsernamePasswordAuthenticationFilter会对用户名和密码进行认证。UsernamePasswordAuthenticationFilter扩展了AbstractAuthenticationProcessingFilter,所以这张图看起来应该很相似。
Username/Password Authentication - 图2
该图建立在SecurityFilterChain图上。

  1. 当用户提交他们的用户名和密码时,UsernamePasswordAuthenticationFilter通过从HttpServletRequest中提取用户名和密码来创建一个UsernamePasswordAuthenticationToken,这是一种认证类型。
  2. 接下来,UsernamePasswordAuthenticationToken被传递到AuthenticationManager中进行认证。AuthenticationManager的细节取决于用户信息的存储方式。
  3. 如果认证失败:
    1. 清空SecurityContextHolder
    2. 执行RememberMeServices.loginFail,如果配置了remember me的话
    3. 执行AuthenticationFailureHandler
  4. 如果认证成功:
    1. SessionAuthenticationStrategy被通知有新的登录
    2. 一个包含完整信息的Authentication将被设置到SecurityContextHolder,之后将SecurityContextPersistenceFilter设置到HttpSession中
    3. 执行RememberMeServices.loginSuccess,如果配置了 remeber me的话
    4. ApplicationEventPublisher发布了一个InteractiveAuthenticationSuccessEvent
    5. 执行AuthenticationSuccessHandler。通常这是一个SimpleUrlAuthenticationSuccessHandler,当我们重定向到登录页面时,它将重定向到一个由ExceptionTranslationFilter保存的请求。

Spring Security的表单登录是默认启用的。然而,一旦提供任何基于Servlet的配置,就必须明确提供基于表单的登录。一个最小的、明确的Java配置如下:

  1. protected void configure(HttpSecurity http) {
  2. http
  3. // ...
  4. .formLogin(withDefaults());
  5. }

在这种配置下,Spring Security将呈现一个默认的登录页面。然而大多数生产应用将需要一个自定义的登录表单,下面的配置演示了如何提供一个自定义的登录表单:

  1. protected void configure(HttpSecurity http) throws Exception {
  2. http
  3. // ...
  4. .formLogin(form -> form
  5. .loginPage("/login")
  6. .permitAll()
  7. );
  8. }

当登录页面在Spring Security配置中被指定时,你要负责渲染该页面。下面是一个Thymeleaf模板,它生成了一个符合/login的登录页面的HTML登录表单。

  1. <!DOCTYPE html>
  2. <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
  3. <head>
  4. <title>Please Log In</title>
  5. </head>
  6. <body>
  7. <h1>Please Log In</h1>
  8. <div th:if="${param.error}">
  9. Invalid username and password.</div>
  10. <div th:if="${param.logout}">
  11. You have been logged out.</div>
  12. <form th:action="@{/login}" method="post">
  13. <div>
  14. <input type="text" name="username" placeholder="Username"/>
  15. </div>
  16. <div>
  17. <input type="password" name="password" placeholder="Password"/>
  18. </div>
  19. <input type="submit" value="Log in" />
  20. </form>
  21. </body>
  22. </html>

关于默认的HTML表格,有几个关键点:

  • 该表单发送一个post请求到/login
  • 该表单需要一个CSRF令牌,该令牌由Thymeleaf自动包含
  • 该表单提供一个用户名username参数
  • 该表单提供一个密码password参数
  • 如果发现HTTP参数error,表明用户未能提供有效的用户名/密码
  • 如果发现HTTP参数logout,表明用户已经成功退出登录

如果你使用的是Spring MVC,你将需要一个控制器,将GET /login映射到我们创建的登录模板。下面是一个最小的LoginController示例:

  1. @Controller
  2. class LoginController {
  3. @GetMapping("/login")
  4. String login() {
  5. return "login";
  6. }
  7. }

Basic Authentication(不推荐)

本节详细介绍了Spring Security如何为基于servlet的应用程序提供对基本HTTP认证的支持。 让我们来看看HTTP基本认证是如何在Spring Security中工作的。首先,我们看到WWW-Authenticate头被送回给未认证的客户端。

Username/Password Authentication - 图3
该图建立在SecurityFilterChain图上。

  1. 首先,一个用户向其未被授权的资源/private提出了一个未经认证的请求。
  2. Spring Security的FilterSecurityInterceptor通过抛出一个AccessDeniedException异常来表明未经认证的请求被拒绝了。
  3. 由于用户没有经过认证,ExceptionTranslationFilter启动了开始认证。配置的AuthenticationEntryPoint是BasicAuthenticationEntryPoint的一个实例,它发送一个 WWW-Authentication:Basic realm=”Realm”的头的响应。RequestCache通常是一个NullRequestCache,它不保存请求,因为客户端有能力重新请求。

当客户端收到WWW-Authenticate头时,它知道它应该用一个用户名和密码重试。下面是正在处理的用户名和密码的流程:
Username/Password Authentication - 图4
该图建立在SecurityFilterChain图上。

  1. 当用户提交他们的用户名和密码时,BasicAuthenticationFilter通过从HttpServletRequest中提取用户名和密码,创建一个UsernamePasswordAuthenticationToken,这是一种认证类型。
  2. 接下来,UsernamePasswordAuthenticationToken被传递到AuthenticationManager中进行认证。AuthenticationManager的细节取决于用户信息的存储方式。
  3. 如果认证失败:
    1. 清空SecurityContextHolder
    2. 执行RememberMeServices.loginFail,如果配置了remember me的话
    3. 调用AuthenticationEntryPoint发送一个 WWW-Authentication:Basic realm=”Realm”的头的响应
  4. 如果认证成功:
    1. 一个包含完整信息的Authentication将被设置到SecurityContextHolder
    2. 执行RememberMeServices.loginSuccess,如果配置了 remeber me的话
    3. BasicAuthenticationFilter调用FilterChain.doFilter(request,response)来继续执行其余的应用逻辑

Spring Security的HTTP Basic认证支持在默认情况下是启用的。然而,只要提供任何基于Servlet的配置,就必须明确提供HTTP Basic。一个最小的、明确的Java配置如下:

  1. protected void configure(HttpSecurity http) {
  2. http
  3. // ...
  4. .httpBasic(withDefaults());
  5. }

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的加密密码。虽然使用安全的格式存储密码,但是不利于入门时学习。

  1. @Bean
  2. public UserDetailsService users() {
  3. UserDetails user = User.builder()
  4. .username("user")
  5. .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
  6. .roles("USER")
  7. .build();
  8. UserDetails admin = User.builder()
  9. .username("admin")
  10. .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
  11. .roles("USER", "ADMIN")
  12. .build();
  13. return new InMemoryUserDetailsManager(user, admin);
  14. }

在下面的例子中,我们利用User.withDefaultPasswordEncoder来确保存储在内存中的密码得到保护。然而,它不能防止通过反编译源代码获得密码。由于这个原因,User.withDefaultPasswordEncoder只能用于 “入门”,不适合用于生产。

  1. @Bean
  2. public UserDetailsService users() {
  3. // The builder will ensure the passwords are encoded before saving in memory
  4. UserBuilder users = User.withDefaultPasswordEncoder();
  5. UserDetails user = users
  6. .username("user")
  7. .password("password")
  8. .roles("USER")
  9. .build();
  10. UserDetails admin = users
  11. .username("admin")
  12. .password("password")
  13. .roles("USER", "ADMIN")
  14. .build();
  15. return new InMemoryUserDetailsManager(user, admin);
  16. }

你可以选择在密码前加上{noop}来表示密码不加密存储。

  1. <user-service>
  2. <user name="user"
  3. password="{noop}password"
  4. authorities="ROLE_USER" />
  5. <user name="admin"
  6. password="{noop}password"
  7. authorities="ROLE_USER,ROLE_ADMIN" />
  8. </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. ::: image.png

  1. create table users
  2. (
  3. username varchar_ignorecase(50) not null primary key,
  4. password varchar_ignorecase(500) not null,
  5. enabled boolean not null
  6. );
  7. create table authorities
  8. (
  9. username varchar_ignorecase(50) not null,
  10. authority varchar_ignorecase(50) not null,
  11. constraint fk_authorities_users foreign key (username) references users (username)
  12. );
  13. create unique index ix_auth_username on authorities (username, authority);

Group Schema

Spring Security同样提供了默认的群组数据库模型。

  1. create table groups (
  2. id bigint generated by default as identity(start with 0) primary key,
  3. group_name varchar_ignorecase(50) not null
  4. );
  5. create table group_authorities (
  6. group_id bigint not null,
  7. authority varchar(50) not null,
  8. constraint fk_group_authorities_group foreign key(group_id) references groups(id)
  9. );
  10. create table group_members (
  11. id bigint generated by default as identity(start with 0) primary key,
  12. username varchar(50) not null,
  13. group_id bigint not null,
  14. constraint fk_group_members_group foreign key(group_id) references groups(id)
  15. );

Setting up a DataSource

在我们配置使用JdbcUserDetailsManager之前,我们必须创建一个数据源。在我们的例子中,我们将设置一个嵌入式的数据源,用默认的用户数据库模型进行初始化。

  1. @Bean
  2. DataSource dataSource() {
  3. return new EmbeddedDatabaseBuilder()
  4. .setType(H2)
  5. .addScript("classpath:org/springframework/security/core/userdetails/jdbc/users.ddl")
  6. .build();
  7. }

在生产环境中,你要确保你设置了一个与外部数据库的连接。

JdbcUserDetailsManager Bean

在下面的例子中,我们使用Spring Boot CLI对密码进行编码,得到的编码密码为{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW。

  1. @Bean
  2. UserDetailsManager users(DataSource dataSource) {
  3. UserDetails user = User.builder()
  4. .username("user")
  5. .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
  6. .roles("USER")
  7. .build();
  8. UserDetails admin = User.builder()
  9. .username("admin")
  10. .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
  11. .roles("USER", "ADMIN")
  12. .build();
  13. JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
  14. users.createUser(user);
  15. users.createUser(admin);
  16. return users;
  17. }

UserDetails

UserDetails由UserDetailsService返回。DaoAuthenticationProvider将验证后的UserDetails放入Authentication的principle字段中。

UserDetailsService

UserDetailsService被DaoAuthenticationProvider用来检索用户名、密码和其他属性,以便用用户名和密码进行认证。Spring Security提供了UserDetailsService的内存和JDBC实现。 你可以通过暴露一个自定义的UserDetailsService作为一个bean来定义自定义认证。例如,假设CustomUserDetailsService实现了UserDetailsService,下面将自定义认证:

:::info 只有在AuthenticationManagerBuilder没有被填充并且没有定义AuthenticationProviderBean的情况下才会使用。 :::

  1. @Bean
  2. CustomUserDetailsService customUserDetailsService() {
  3. return new CustomUserDetailsService();
  4. }

PasswordEncoder

Spring Security的Servlet支持通过与PasswordEncoder集成来安全地存储密码。定制Spring Security使用的PasswordEncoder实现可以通过暴露PasswordEncoder Bean来完成。

DaoAuthenticationProvider

DaoAuthenticationProvider是一个AuthenticationProvider的实现,它利用UserDetailsService和PasswordEncoder来验证一个用户名和密码。

让我们来看看DaoAuthenticationProvider在Spring Security中是如何工作的。
Username/Password Authentication - 图6

  1. 读取用户名和密码的认证过滤器将一个UsernamePasswordAuthenticationToken传递给AuthenticationManager的实现ProviderManager
  2. ProviderManager包含众多的AuthenticationProvider,不同的AuthenticationProvider应用于不同场景下的认证,比如此处的DaoAuthenticationProvider就是用于username、password认证
  3. DaoAuthenticationProvider通过UserDetailsService从系统中加载UserDetails
  4. 然后,DaoAuthenticationProvider使用PasswordEncoder来与上一步返回的UserDetails上的密码进行比对
  5. 当认证成功时,返回的UsernamePasswordAuthenticationToken(principal字段=userDetails(由userDetailService返回))将被认证过滤器设置在SecurityContextHolder上