Spring Security 用户名密码认证实例
1. 前言
上一节,我们介绍了 Spring Security 的基本认证组件,本节我们介绍最常见的认证方式「密码认证」的实现方法。
用户名、密码认证被广泛应用于 PC 端的 Web 应用和客户端应用,比如登陆网站,又比如 QQ 桌面客户端。
- 表单登录;
- 基本认证;
- 数字认证。
从认证数据源角度分类,也可以将认证分为:
- 使用内存存储;
- 使用关系型数据库存储;
- 自定义存储;
- LDAP 存储。
本小节实例开发环境:
本小节所使用的实例代码是基于 Spring 官网中提供的最小化 HelloWorld 模板创建,请点此下载完整的 HelloWorld 模板压缩包。
2. 实例讲解
2.1 创建 Spring Security 项目
- 修改 Hello World 模板工程的目录名称为 UsernamePasswordSample;
- 修改 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>
-
修改启动类,修改其包名为
imooc.springsecurity.usernamepassword
,修改类名为UsernamePasswordSample
; -
创建测试页面,返回登录用户信息
新建 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());
}
}
- 在项目根目录控制台输入命令
mvn spring-boot:run
,如看到以下输出代表配置正确。
2.2 表单认证
2.2.1 表单认证的过程说明
Spring Security 支持从 HTML 的 Form 表单形式提交登录用户信息。
表单认证可分为以下步骤:
- 用户请求受保护资源;
- Spring Security 的
FilterSecurityInterceptor
对象,检测到当前用户认证未通过,应予以拒绝,并抛出AccessDeniedException
; - 当
AccessDeniedException
被ExceptionTranslationFilter
接收后,其认定需要发起认证流程,此时用户被要求登录,认证服务器将登录地址(默认由LoginUrlAuthenticationEntryPoint
)返回给客户端; - 客户端浏览重定向到登录页面;
- 登录页面有服务端渲染生成。
当用户提交登录信息,认证服务器端的 UsernamePasswordAuthenticationFilter
就会被执行。
此过程的具体执行过程如下:
UsernamePasswordAuthenticationFilter
产生UsernamePasswordAuthenticationToken
,并存入从请求中获取的用户名、密码等信息;- 创建出的 Token 被传递给
AuthenticationManager
用于认证; - 认证成功或失败的后续流程同上一小节中关于
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」
2.2.3 表单认证的配置
默认情况下,表单登录的跳转地址是 /login
,登录参数中用户名变量名为 username
,密码变量名为 password
。如果我们需要修改这些配置信息,可以通过如下方式实现:
在 configure(HttpSecurity http)
方法中,为 http
的 formLogin
项修改配置。
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
页面需要我们自己去实现。这里有几个需要注意的地方:
- 自定义表单提交地址为
/login
,提交方法仅支持POST
; - 表单需要支持 CSRF 票据,即附带
_csrf
参数; - 用户名字段需要命名为
user
; - 密码字段需要命名为
pass
; - 当认证失败时,表单页面会收到
error
参数; - 当用户退出成功时,表单页面会收到
logout
参数。
为了测试上述配置,我们创建一个测试登录页:
- 新建
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";
}
}
- 新建 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 基本认证的流程
基本认证也是常用的认证方式。基本认证分两种场景:
- 如果直接在浏览器里访问页面,浏览器会弹出登录窗口,如下图:
- 如果发送未经认证的 http 请求,服务端会返回 401 错误。
实现基本认证有两种方式:
- 在请求头中添加
Authorization: "Basic Base64(用户名+密码)"
; - 在请求参数中增加用户名和密码。
在 Spring Security 中,具体的认证过程如下:
- 用户请求受保护资源;(与表单认证相同)
- Spring Security 的
FilterSecurityInterceptor
对象,检测到当前用户认证未通过,应予以拒绝,并抛出AccessDeniedException
;(与表单认证相同) - 当
AccessDeniedException
被ExceptionTranslationFilter
接收后,其认定需要发起认证流程,此时用户被要求登录,认证服务器将认证头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 认证。