Spring Security 用戶名密碼認證實例
1. 前言
上一節(jié),我們介紹了 Spring Security 的基本認證組件,本節(jié)我們介紹最常見的認證方式「密碼認證」的實現(xiàn)方法。
用戶名、密碼認證被廣泛應用于 PC 端的 Web 應用和客戶端應用,比如登陸網站,又比如 QQ 桌面客戶端。
- 表單登錄;
- 基本認證;
- 數字認證。
從認證數據源角度分類,也可以將認證分為:
- 使用內存存儲;
- 使用關系型數據庫存儲;
- 自定義存儲;
- LDAP 存儲。
本小節(jié)實例開發(fā)環(huán)境:
本小節(jié)所使用的實例代碼是基于 Spring 官網中提供的最小化 HelloWorld 模板創(chuàng)建,請點此下載完整的 HelloWorld 模板壓縮包。
2. 實例講解
2.1 創(chuàng)建 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
; -
創(chuàng)建測試頁面,返回登錄用戶信息
新建 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
接收后,其認定需要發(fā)起認證流程,此時用戶被要求登錄,認證服務器將登錄地址(默認由LoginUrlAuthenticationEntryPoint
)返回給客戶端; - 客戶端瀏覽重定向到登錄頁面;
- 登錄頁面有服務端渲染生成。
當用戶提交登錄信息,認證服務器端的 UsernamePasswordAuthenticationFilter
就會被執(zhí)行。
此過程的具體執(zhí)行過程如下:
UsernamePasswordAuthenticationFilter
產生UsernamePasswordAuthenticationToken
,并存入從請求中獲取的用戶名、密碼等信息;- 創(chuàng)建出的 Token 被傳遞給
AuthenticationManager
用于認證; - 認證成功或失敗的后續(xù)流程同上一小節(jié)中關于
AbstractAuthenticationProcessingFilter
的執(zhí)行過程一致。
2.2.2 表單認證的開啟
默認情況下,Spring Security 開啟了表單認證功能。如果我們需要顯式配置,可用如下方式實現(xiàn)。
創(chuàng)建 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
。如果我們需要修改這些配置信息,可以通過如下方式實現(xiàn):
在 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 以通過認證,注意,這不是最好的做法,后續(xù)章節(jié)會有介紹。
}
當然這一步中配置 /login
頁面需要我們自己去實現(xiàn)。這里有幾個需要注意的地方:
- 自定義表單提交地址為
/login
,提交方法僅支持POST
; - 表單需要支持 CSRF 票據,即附帶
_csrf
參數; - 用戶名字段需要命名為
user
; - 密碼字段需要命名為
pass
; - 當認證失敗時,表單頁面會收到
error
參數; - 當用戶退出成功時,表單頁面會收到
logout
參數。
為了測試上述配置,我們創(chuàng)建一個測試登錄頁:
- 新建
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 基本認證的流程
基本認證也是常用的認證方式?;菊J證分兩種場景:
- 如果直接在瀏覽器里訪問頁面,瀏覽器會彈出登錄窗口,如下圖:
- 如果發(fā)送未經認證的 http 請求,服務端會返回 401 錯誤。
實現(xiàn)基本認證有兩種方式:
- 在請求頭中添加
Authorization: "Basic Base64(用戶名+密碼)"
; - 在請求參數中增加用戶名和密碼。
在 Spring Security 中,具體的認證過程如下:
- 用戶請求受保護資源;(與表單認證相同)
- Spring Security 的
FilterSecurityInterceptor
對象,檢測到當前用戶認證未通過,應予以拒絕,并拋出AccessDeniedException
;(與表單認證相同) - 當
AccessDeniedException
被ExceptionTranslationFilter
接收后,其認定需要發(fā)起認證流程,此時用戶被要求登錄,認證服務器將認證頭WWW-Authenticate
(默認由BasicAuthenticationEntryPoint
提供)返回給客戶端。
當客戶端收到 WWW-Authenticate
頭后,客戶端提供用戶名和密碼參數用于認證。
2.3.2 基本認證的配置
默認情況下,Spring Security 開啟了基本認證功能。如果我們需要顯式配置,可用如下方式實現(xiàn)。
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);
}
注意,其中的密碼字段需要符合系統(tǒng)加密規(guī)則。比較簡單的生成方式是通過 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);
使用此數據庫描述文本,在我們的數據庫中創(chuàng)建「用戶表」和「權限表」,并在 Spring Security 項目中配置 JDBC 數據源。
2.6.2 配置 JDBC 數據源
@Autowired
private DataSource dataSource;
@Autowired
public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication().dataSource(dataSource);
}
如此,我們便可以用數據庫中存儲的用戶名和密碼進行登錄校驗了。
3. 小結
本節(jié)我們介紹了用戶名密碼認證的實現(xiàn)方式,主要知識點如下:
Spring Security 用戶名密碼認證有多種實現(xiàn)方式,從認證方式角度看,可以分為表單認證、基本認證和數字證書認證,其中數字證書認證已經不適用現(xiàn)代的互聯(lián)網技術。從認證數據源角度,也可以分為內存認證、JDBC 數據庫認證和 LDAP 認證。
用戶名密碼認證是 Spring Security 框架中最基本的認證形式。下節(jié)開始我們將討論一種在移動應用被廣泛應用的認證方式:OAuth2.0 認證。