章节索引 :

Spring Security 用户名密码认证实例

1. 前言

上一节,我们介绍了 Spring Security 的基本认证组件,本节我们介绍最常见的认证方式「密码认证」的实现方法。

用户名、密码认证被广泛应用于 PC 端的 Web 应用和客户端应用,比如登陆网站,又比如 QQ 桌面客户端。

图片描述

图 1 慕课登录页面
Spring Security 提供了成熟的用户名密码认证解决方案。 针对用户名、密码认证,从认证请求方式的角度,可以将认证过程分为三类:
  1. 表单登录;
  2. 基本认证;
  3. 数字认证。

从认证数据源角度分类,也可以将认证分为:

  1. 使用内存存储;
  2. 使用关系型数据库存储;
  3. 自定义存储;
  4. LDAP 存储。

本小节实例开发环境

本小节所使用的实例代码是基于 Spring 官网中提供的最小化 HelloWorld 模板创建,请点此下载完整的 HelloWorld 模板压缩包

2. 实例讲解

2.1 创建 Spring Security 项目

  1. 修改 Hello World 模板工程的目录名称为 UsernamePasswordSample;
  2. 修改 pom.xml 文件,将基础信息部分改为如下形式:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
略...
	<groupId>imooc.springsecurity</groupId>
	<artifactId>UsernamePasswordSample</artifactId>
	<version>0.0.1-SNAPSHOT</version>
略...
</project>
  1. 修改启动类,修改其包名为 imooc.springsecurity.usernamepassword,修改类名为 UsernamePasswordSample

  2. 创建测试页面,返回登录用户信息

新建 src/main/java/imooc/springsecurity/usernamepassword/controller/UserController.java

package imooc.springsecurity.usernamepassword.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

@RestController
@RequestMapping("user")
public class UserController {
    @RequestMapping("me")
    private String showMe(Principal principal) {
        return String.format("当前登录用户为:「%s」", principal.getName());
    }
}
  1. 在项目根目录控制台输入命令 mvn spring-boot:run,如看到以下输出代表配置正确。

图片描述

2.2 表单认证

2.2.1 表单认证的过程说明

Spring Security 支持从 HTML 的 Form 表单形式提交登录用户信息。

表单认证可分为以下步骤:

  1. 用户请求受保护资源;
  2. Spring Security 的 FilterSecurityInterceptor 对象,检测到当前用户认证未通过,应予以拒绝,并抛出 AccessDeniedException
  3. AccessDeniedExceptionExceptionTranslationFilter 接收后,其认定需要发起认证流程,此时用户被要求登录,认证服务器将登录地址(默认由 LoginUrlAuthenticationEntryPoint)返回给客户端;
  4. 客户端浏览重定向到登录页面;
  5. 登录页面有服务端渲染生成。

图片描述

图 2 表单登录流程

当用户提交登录信息,认证服务器端的 UsernamePasswordAuthenticationFilter 就会被执行。

此过程的具体执行过程如下:

  1. UsernamePasswordAuthenticationFilter 产生 UsernamePasswordAuthenticationToken,并存入从请求中获取的用户名、密码等信息;
  2. 创建出的 Token 被传递给 AuthenticationManager 用于认证;
  3. 认证成功或失败的后续流程同上一小节中关于 AbstractAuthenticationProcessingFilter 的执行过程一致。

2.2.2 表单认证的开启

默认情况下,Spring Security 开启了表单认证功能。如果我们需要显式配置,可用如下方式实现。

创建 Security 配置文件: src/main/java/imooc/springsecurity/usernamepassword/config/WebSecurityConfig.java,并在其中添加 http.formLogin(withDefaults()) 的配置,完整代码如下:

package imooc.springsecurity.usernamepassword.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin(Customizer.withDefaults());
    }

}

访问 http://localhost:8080/user/me ,网页会自动跳转到登录页面。

图片描述

登录页面
输入默认生成的用户名 「user」, 默认生成密码可在控制台日志中找到。如下图:

图片描述

提交登录后,通过认证,我们将在浏览器看到当前登录的用户名。

当前登录用户为:「user」

2.2.3 表单认证的配置

默认情况下,表单登录的跳转地址是 /login,登录参数中用户名变量名为 username,密码变量名为 password。如果我们需要修改这些配置信息,可以通过如下方式实现:

configure(HttpSecurity http) 方法中,为 httpformLogin 项修改配置。

    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login").permitAll() // 表单认证页面不需要权限
                .anyRequest().authenticated(); // 其他页面需要登录用户才能访问

        http.formLogin()
                .loginPage("/login") // 自定义表单认证页面地址
                .usernameParameter("user")
                .passwordParameter("pass");

        http.csrf().disable(); // 关闭 csrf 以通过认证,注意,这不是最好的做法,后续章节会有介绍。
    }

当然这一步中配置 /login 页面需要我们自己去实现。这里有几个需要注意的地方:

  1. 自定义表单提交地址为 /login ,提交方法仅支持 POST
  2. 表单需要支持 CSRF 票据,即附带 _csrf 参数;
  3. 用户名字段需要命名为 user
  4. 密码字段需要命名为 pass
  5. 当认证失败时,表单页面会收到 error 参数;
  6. 当用户退出成功时,表单页面会收到 logout 参数。

为了测试上述配置,我们创建一个测试登录页:

  1. 新建 src/main/java/imooc/springsecurity/usernamepassword/controller/LoginController.java
package imooc.springsecurity.usernamepassword.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class LoginController {
    @RequestMapping("/login")
    public String viewLogin2() {
        return "/login.html";
    }
}
  1. 新建 src/main/resources/templates/login.html
<form method="post" action="/login">
    <input type="text" name="user">
    <input type="password" name="pass">
    <input type="submit" value="登录">
</form>

访问测试:http://localhost:8080/user/me ,此时跳转到我们新建的登录页面。

图片描述

登录页面
输入用户名密码后可看到用户信息。

2.3 基本认证

2.3.1 基本认证的流程

基本认证也是常用的认证方式。基本认证分两种场景:

  1. 如果直接在浏览器里访问页面,浏览器会弹出登录窗口,如下图:
  2. 如果发送未经认证的 http 请求,服务端会返回 401 错误。

实现基本认证有两种方式:

  1. 在请求头中添加 Authorization: "Basic Base64(用户名+密码)"
  2. 在请求参数中增加用户名和密码。

在 Spring Security 中,具体的认证过程如下:

  1. 用户请求受保护资源;(与表单认证相同)
  2. Spring Security 的 FilterSecurityInterceptor 对象,检测到当前用户认证未通过,应予以拒绝,并抛出 AccessDeniedException;(与表单认证相同)
  3. AccessDeniedExceptionExceptionTranslationFilter 接收后,其认定需要发起认证流程,此时用户被要求登录,认证服务器将认证头 WWW-Authenticate (默认由 BasicAuthenticationEntryPoint 提供)返回给客户端。

当客户端收到 WWW-Authenticate 头后,客户端提供用户名和密码参数用于认证。

2.3.2 基本认证的配置

默认情况下,Spring Security 开启了基本认证功能。如果我们需要显式配置,可用如下方式实现。

protected void configure(HttpSecurity http) {
    http
        // ...
        .httpBasic(withDefaults());
}

2.4 数字认证

数字认证在新型应用中已不建议使用,因为这种方式下,用户的敏感信息,比如密码等都需要以明文形式存在,因此数字认证方式并不安全。

数字认证对应的认证过滤器为:DigestAuthenticationFilter

2.5 在内存中配置用户名密码

内存认证是将用户名密码信息存储在内存之中,通过 InMemoryUserDetailsManager 方式完成认证。

内存认证添加用户的方式如下,在 WebSecurityConfig.java 类(非必须)中添加以下 Bean 定义。

@Bean
public UserDetailsService users() {
  	// 用户1 user 用户
    UserDetails user = User.builder()
        .username("user")
        .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
        .roles("USER")
        .build();

    // 用户2 admin 用户
    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);
}

注意,其中的密码字段需要符合系统加密规则。比较简单的生成方式是通过 Spring Boot CLI 工具,在控制台将密码转换为密文。

然后我们可以在登录表单中,用这里配置的用户信息完成认证。

2.6 使用数据库管理用户名密码

Spring Security 支持使用数据库作为认证数据源,并且提供了默认数据模型。

2.6.1 默认的数据模型

使用 JDBC 数据源最简单直接的方法就是使用 Spring Security 提供的默认数据模型「users.ddl」构建认证数据库。

users.ddl 的定义如下:

create table users(
    username varchar_ignorecase(50) not null primary key,
    password varchar_ignorecase(50) 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);

使用此数据库描述文本,在我们的数据库中创建「用户表」和「权限表」,并在 Spring Security 项目中配置 JDBC 数据源。

2.6.2 配置 JDBC 数据源

    @Autowired
    private DataSource dataSource;
    @Autowired
    public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication().dataSource(dataSource);
    }

如此,我们便可以用数据库中存储的用户名和密码进行登录校验了。

3. 小结

本节我们介绍了用户名密码认证的实现方式,主要知识点如下:

Spring Security 用户名密码认证有多种实现方式,从认证方式角度看,可以分为表单认证、基本认证和数字证书认证,其中数字证书认证已经不适用现代的互联网技术。从认证数据源角度,也可以分为内存认证、JDBC 数据库认证和 LDAP 认证。

用户名密码认证是 Spring Security 框架中最基本的认证形式。下节开始我们将讨论一种在移动应用被广泛应用的认证方式:OAuth2.0 认证。