認識Spring Security
Spring Security 是為基於 Spring 的應用程序提供聲明式安全保護的安全性框架。Spring Security 提供了完整的安全性解決方案,它能夠在 Web 請求級別和方法調用級別處理身份認證和授權。因為基於 Spring 框架,所以 Spring Security 充分利用了依賴注入(dependency injection, DI)和麪向切面的技術。
核心功能
對於一個權限管理框架而言,無論是 Shiro 還是 Spring Security,最最核心的功能,無非就是兩方面:
認證
授權
通俗點説,認證就是我們常説的登錄,授權就是權限鑑別,看看請求是否具備相應的權限。
認證(Authentication)
Spring Security 支持多種不同的認證方式,這些認證方式有的是 Spring Security 自己提供的認證功能,有的是第三方標準組織制訂的,主要有如下一些:
一些比較常見的認證方式:
HTTP BASIC authentication headers:基於IETF RFC 標準。
HTTP Digest authentication headers:基於IETF RFC 標準。
HTTP X.509 client certificate exchange:基於IETF RFC 標準。
LDAP:跨平台身份驗證。
Form-based authentication:基於表單的身份驗證。
Run-as authentication:用户用户臨時以某一個身份登錄。
OpenID authentication:去中心化認證。
除了這些常見的認證方式之外,一些比較冷門的認證方式,Spring Security 也提供了支持。
Jasig Central Authentication Service:單點登錄。
Automatic "remember-me" authentication:記住我登錄(允許一些非敏感操作)。
Anonymous authentication:匿名登錄。
......
作為一個開放的平台,Spring Security 提供的認證機制不僅僅是上面這些。如果上面這些認證機制依然無法滿足你的需求,我們也可以自己定製認證邏輯。當我們需要和一些“老破舊”的系統進行集成時,自定義認證邏輯就顯得非常重要了。
授權(Authorization)
無論採用了上面哪種認證方式,都不影響在 Spring Security 中使用授權功能。Spring Security 支持基於 URL 的請求授權、支持方法訪問授權、支持 SpEL 訪問控制、支持域對象安全(ACL),同時也支持動態權限配置、支持 RBAC 權限模型等,總之,我們常見的權限管理需求,Spring Security 基本上都是支持的。
項目實踐
創建 maven 工程
項目依賴如下:
<dependencies>
<!-- 以下是>spring boot依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 以下是>spring security依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
複製代碼
提供一個簡單的測試接口,如下:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello,hresh";
}
@GetMapping("/hresh")
public String sayHello() {
return "hello,world";
}
}
複製代碼
再創建一個啓動類,如下:
@SpringBootApplication
public class SecurityInMemoryApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityInMemoryApplication.class, args);
}
}
複製代碼
在 Spring Security 中,默認情況下,只要添加了依賴,我們項目的所有接口就已經被統統保護起來了,現在啓動項目,訪問 /hello 接口,就需要登錄之後才可以訪問,登錄的用户名是 user,密碼則是隨機生成的,在項目的啓動日誌中,如下所示:
Using generated security password: 21596f81-e185-4b6a-a8ff-1b21e2a60c6f
複製代碼
我們嘗試訪問 /hello 接口,因為該接口被 Spring Security 保護起來了,重定向到 /login 接口,如下圖所示:
輸入賬號和密碼後,即可訪問 /hello 接口。
那麼如何自定義登錄用户信息呢?以及 Spring Security 如何知道我們想要支持基於表單的身份驗證?
認證
啓用web安全性功能
Spring Security 提供了用户名密碼登錄、退出、會話管理等認證功能,只需要配置即可使用。
在 Spring Security 5.7版本之前,或者 SpringBoot2.7 之前,我們都是繼承 WebSecurityConfigurerAdapter 來配置。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//定義用户信息服務(查詢用户信息)
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
return manager;
}
//密碼編碼器,不加密,字符串直接比較
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/hello");
}
//安全攔截機制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}
}
複製代碼
Spring Security 提供了這種鏈式的方法調用。上面配置指定了認證方式為 HTTP Basic 登錄,並且所有請求都需要進行認證。
這裏有一點需要注意,我沒並沒有在 Spring Security 配置類上使用@EnableWebSecurity 註解。這是因為在非 Spring Boot 的 Spring Web MVC 應用中,註解@EnableWebSecurity 需要開發人員自己引入以啓用 Web 安全。而在基於 Spring Boot 的 Spring Web MVC 應用中,開發人員沒有必要再次引用該註解,Spring Boot 的自動配置機制 WebSecurityEnablerConfiguration 已經引入了該註解,如下所示:
package org.springframework.boot.autoconfigure.security.servlet;
// 省略 imports 行
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnMissingBean(
name = {"springSecurityFilterChain"}
)
@ConditionalOnClass({EnableWebSecurity.class})
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@EnableWebSecurity
class WebSecurityEnablerConfiguration {
WebSecurityEnablerConfiguration() {
}
}
複製代碼
實際上,一個 Spring Web 應用中,WebSecurityConfigurerAdapter 可能有多個 , @EnableWebSecurity 可以不用在任何一個WebSecurityConfigurerAdapter 上,可以用在每個 WebSecurityConfigurerAdapter 上,也可以只用在某一個WebSecurityConfigurerAdapter 上。多處使用@EnableWebSecurity 註解並不會導致問題,其最終運行時效果跟使用@EnableWebSecurity 一次效果是一樣的。
在 userDetailsService()方法中,我們返回了一個 UserDetailsService 給 Spring 容器,Spring Security 會使用它來獲取用户信息。我們暫時使用 InMemoryUserDetailsManager 實現類,並在其中分別創建了zhangsan、lisi兩個用户,並設置密碼和權限。
在configure(HttpSecurity http)方法中進入如下配置:
確保對我們的應用程序的任何請求都要求用户進行身份驗證
允許用户使用基於表單的登錄進行身份驗證
允許用户使用HTTP基本身份驗證進行身份驗證
注意上述還有一個 passwordEncoder()方法,在 IDEA 中會提示 NoOpPasswordEncoder 已過期。這是因為 Spring Security 5對 PasswordEncoder 做了相關的重構,原先默認配置的 PlainTextPasswordEncoder(明文密碼)被移除了,想要做到明文存儲密碼,只能使用一個過期的類來過渡。
//加入
//已過期
@Bean
PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
複製代碼
Spring Security 提供了多種類來進行密碼編碼,並作為了相關配置的默認配置,只不過沒有暴露為全局的 Bean。在實際應用中使用明文校驗密碼肯定是存在風險的,NoOpPasswordEncoder 只能存在於 demo 中。
//實際應用
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//加密方式與對應的類
bcrypt - BCryptPasswordEncoder (Also used for encoding)
ldap - LdapShaPasswordEncoder
MD4 - Md4PasswordEncoder
MD5 - new MessageDigestPasswordEncoder("MD5")
noop - NoOpPasswordEncoder
pbkdf2 - Pbkdf2PasswordEncoder
scrypt - SCryptPasswordEncoder
SHA-1 - new MessageDigestPasswordEncoder("SHA-1")
SHA-256 - new MessageDigestPasswordEncoder("SHA-256")
sha256 - StandardPasswordEncoder
複製代碼
但是在 Spring Security 5.7版本之後(包括5.7版本),或者 SpringBoot2.7 之後,WebSecurityConfigurerAdapter 就過期了,雖然可以繼續使用,但看着比較彆扭。
看 5.7版本官方文檔是如何解釋的:
以前我們自定義類繼承自 WebSecurityConfigurerAdapter 來配置我們的 Spring Security,我們主要是配置兩個東西:
configure(HttpSecurity)
configure(WebSecurity)
前者主要是配置 Spring Security 中的過濾器鏈,後者則主要是配置一些路徑放行規則。
現在在 WebSecurityConfigurerAdapter 的註釋中,人家已經把意思説的很明白了:
以後如果想要配置過濾器鏈,可以通過自定義 SecurityFilterChain Bean 來實現。
以後如果想要配置 WebSecurity,可以通過 WebSecurityCustomizer Bean 來實現。
我們對上文中的 SecurityConfig 文件做一下改動,試試新版中該如何配置。
@Configuration
public class SecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
return manager;
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring().antMatchers("/hello");
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.and()
.csrf().disable();
return http.build();
}
}
複製代碼
此時重啓項目,你會發現 /hello 也是可以直接訪問的,就是因為這個路徑不經過任何過濾器。
個人覺得新寫法更加直觀,可以清楚的看到 SecurityFilterChain 是關於過濾器鏈配置的,與我們理論知識提到的過濾器知識是一致的。
測試
訪問 http://localhost:8086/hello,可以直接看到頁面內容,不需要輸入賬號密碼。
訪問 http://localhost:8086/hresh,則需要賬號密碼,即我們配置的 zhangsan 和 lisi 用户。
在測試過程中,你可能會發現這樣幾個問題:
1、直接訪問 http://localhost:8086,默認會跳轉到 /login 頁面,該配置位於 UsernamePasswordAuthenticationFilter 類文件中,如果你想自定義登錄頁面,可以這樣修改:
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
// .loginPage("/login.html")
.loginProcessingUrl("/login")
複製代碼
2、表單登錄時,賬號密碼默認字段為 username 和 password。
3、按理來説,登錄成功之後是跳到/頁面,失敗跳轉到登錄頁,但因為我們這是 SpringBoot 項目,我們可以讓它登錄成功時返回json數據,而不是重定向到某個頁面。默認情況下,賬號密碼輸入錯誤會自動返回登錄頁面,所以此處我們就不處理失敗的情況。
@Component
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private static ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(objectMapper.writeValueAsString("登錄成功"));
}
}
複製代碼
接着修改 securityFilterChain()方法
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.successHandler(myAuthenticationSuccessHandler)
.and()
.csrf().disable();
複製代碼
再次重啓項目,在登錄頁面輸入賬號密碼後,返回結果如下所示:
4、自定義登錄頁面,在 resource 目錄下新建 static 目錄,裏面添加 login.html 文件,暫時未添加樣式
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登錄</title>
</head>
<body>
<form action="/doLogin" method="post">
<div class="input">
<label for="name">用户名</label>
<input type="text" name="name" id="name">
<span class="spin"></span>
</div>
<div class="input">
<label for="pass">密碼</label>
<input type="password" name="passwd" id="pass">
<span class="spin"></span>
</div>
<div class="button login">
<button type="submit">
<span>登錄</span>
<i class="fa fa-check"></i>
</button>
</div>
</form>
</body>
</html>
複製代碼
修改 securityFilterChain()方法
http.authorizeRequests() //表示開啓權限配置
.antMatchers("/login.html").permitAll()
.anyRequest().authenticated() //表示所有的請求都要經過認證之後才能訪問
.and() // 鏈式編程寫法
.formLogin() //開啓表單登錄配置
.loginPage("/login.html") // 配置登錄頁面地址
.loginProcessingUrl("/doLogin")
.permitAll()
.and()
.csrf().disable();
複製代碼
重啓項目後, 再次訪問 http://localhost:8086/,會重定向到 http://localhost:8086/login.html。
最後,總結一下 HttpSecurity 的配置,示例如下:
http.authorizeRequests() //表示開啓權限配置
.anyRequest().authenticated() //表示所有的請求都要經過認證之後才能訪問
.and() // 鏈式編程寫法
.formLogin() //開啓表單登錄配置
.loginPage("/login.html") // 配置自定義登錄頁面地址
.loginProcessingUrl("/login") //配置登錄接口地址
// .defaultSuccessUrl() //登錄成功後的跳轉頁面
// .failureUrl() //登錄失敗後的跳轉頁面
// .usernameParameter("username") //登錄用户名的參數名稱
// .passwordParameter("password") // 登錄密碼的參數名稱
// .successHandler(
// myAuthenticationSuccessHandler) //前後端分離的情況,並不想通過defaultSuccessUrl進行頁面跳轉,只需要返回一個json數據來告知前端
// .failureHandler(myAuthenticationFailureHandler) // 同理,替代failureUrl
// .permitAll()
.and()
.csrf().disable();// 禁用CSRF防禦功能,測試可以先關閉
複製代碼
表單驗證時,loginPage 與 loginProcessingUrl 區別:
loginPage 配置自定義登錄頁面地址,loginProcessingUrl 默認與表單 action 地址一致;
如果只配置 loginPage 而不配置 loginProcessingUrl,那麼 loginProcessingUrl 默認就是 loginPage;
如果只配置 loginProcessUrl,就會用不了自定義登陸頁面,Security 會使用自帶的默認登陸頁面;
如果 loginProcessingUrl 默認與表單 action 地址不一致,那麼它需要指向一個有效的地址,比如説 /doLogin.html,這要求我們在 static 目錄下創建一個 doLogin.html 頁面,此外,還需要在 controller 文件中增加如下方法:
@PostMapping("/doLogin")
public String doLogin() {
return "我登錄成功了";
}
複製代碼
但是登錄成功後並不會顯示 doLogin.html 頁面的內容,而是顯示 /doLogin 的返回結果。同理,不配置 loginProcessingUrl,那麼 loginProcessingUrl 默認就是 loginPage,即 loginProcessingUrl=login.html,與 doLogin.html 效果一樣。
另外再介紹一下 Spring Security 中 defaultSuccessUrl 和 successForwardUrl 的區別:
假定在 defaultSuccessUrl 中指定登錄成功的跳轉頁面為 /index,那麼存在兩種情況:
① 瀏覽器中輸入的是登錄地址,登錄成功後,則直接跳轉到 /index;
② 如果瀏覽器中輸入了其他地址,例如 http://localhost:8080/elseUrl,若登錄成功,就不會跳轉到 /index,而是來到 /elseUrl 頁面。
defaultSuccessUrl 就是説,它會默認跳轉到 Referer 來源頁面,如果 Referer 為空,沒有來源頁,則跳轉到默認設置的頁面。
successForwardUrl 表示不管 Referer 從何而來,登錄成功後一律跳轉到指定的地址。
認證方式選擇
在 WebSecurityConfigurerAdapter 類中有很多 configure()方法,除了上文提到的 HttpSecurity 和 WebSecurity 參數,還有一個 AuthenticationManagerBuilder 參數,源碼如下:
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
this.disableLocalConfigureAuthenticationBldr = true;
}
protected AuthenticationManager authenticationManager() throws Exception {
if (!this.authenticationManagerInitialized) {
this.configure(this.localConfigureAuthenticationBldr);
if (this.disableLocalConfigureAuthenticationBldr) {
this.authenticationManager = this.authenticationConfiguration.getAuthenticationManager();
} else {
this.authenticationManager = (AuthenticationManager)this.localConfigureAuthenticationBldr.build();
}
this.authenticationManagerInitialized = true;
}
return this.authenticationManager;
}
複製代碼
該類用於設置各種用户想用的認證方式,設置用户認證數據庫查詢服務 UserDetailsService 類以及添加自定義 AuthenticationProvider 類實例等
Spring Security 為配置用户存儲提供了多個可選解決方案,包括:
基於內存的用户存儲
基於 JDBC 的用户存儲
以 LDAP 作為後端的用户存儲
自定義用户詳情服務
關於這四種方式就不詳細介紹了,可以重點關注方案二和方案四,而在本項目中,我們直接在 SecurityConfig 中重寫 userDetailsService 方法,並將 UserDetailsService 對象注入到 Spring 容器中。
授權
1、首先在 HelloController 中增加 r1 和 r2 資源。
@GetMapping(value = "/r/r1")
public String r1() {
return " 訪問資源1";
}
@GetMapping(value = "/r/r2")
public String r2() {
return " 訪問資源2";
}
複製代碼
2、修改 SecurityConfig 文件中的 securityFilterChain()方法
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/r/r1").hasAuthority("p1")
.antMatchers("/r/r2").hasAuthority("p2")
.anyRequest().authenticated()
.and()
.formLogin()
.successHandler(myAuthenticationSuccessHandler)
.permitAll()
.and()
.csrf().disable();
return http.build();
}
複製代碼
訪問 r1、r2 資源,需要對應的權限,而且其他接口則只需要認證,並不需要授權。
3、測試
訪問 http://localhost:8086 ,進入登錄頁面,輸入正確的賬號密碼,提交後頁面返回“登錄成功”,如果是 zhangsan,則可以訪問 r1資源,訪問 r2則會報錯,我們暫時未處理錯誤如下:
總結
關於 Spring Security 的學習先到這裏,基本瞭解如何使用即可,我們繼續後面的學習。
如果想要深入學習 Spring Security,推薦閲讀《深入淺出Spring Security》,還包括配套的代碼示例。