06.06 「spring boot 系列」spring security 實踐 + 源碼分析

前言

本文將從示例、原理、應用3個方面介紹 spring data jpa。

以下分析基於spring boot 2.0 + spring 5.0.4版本源碼

概述

Spring Security 是一個能夠為基於 Spring 的企業應用系統提供聲明式的安全訪問控制解決方案的安全框架。它提供了一組可以在 Spring 應用上下文中配置的 Bean,充分利用了Spring IoC,DI(控制反轉Inversion of Control ,DI:Dependency Injection 依賴注入)和 AOP(面向切面編程)功能,為應用系統提供聲明式的安全訪問控制功能,減少了為企業系統安全控制編寫大量重複代碼的工作。當前版本為 5.0.5。

Spring Security 5 相比 4,主要有以下幾點升級:

  • 支持 OAuth 2.0
  • 支持 Spring WebFlux
  • 可以使用 Reactor 的 StepVerifier 進行測試

示例

pom配置

 <dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-security/<artifactid>
/<dependency>
<dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-web/<artifactid>
/<dependency>
<dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-thymeleaf/<artifactid>
/<dependency>

配置非常簡單,和 spring security 有關的就是 spring-boot-starter-security,web 和 thymeleaf 的引入是為了構建頁面,便於演示

application.properties 配置

spring.thymeleaf.cache=false
spring.security.user.name=user
spring.security.user.password=password
spring.security.user.roles=USER

同樣很簡單,禁用thymeleaf的緩存功能,另外配置了一個角色為 USER 的用戶,用戶名/密碼:user/password

security config 配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http.authorizeRequests()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.anyRequest().fullyAuthenticated()
.and()
.formLogin().loginPage("/login").failureUrl("/login?error").permitAll()
.and()
.logout().permitAll();
// @formatter:on
}
}

security 的配置很簡單,可以繼承WebSecurityConfigurerAdapter,WebSecurityConfigurerAdapter是默認情況下 spring security 的 http 配置。通常情況下,都會存在部分 url 請求不需要過安全驗證,此時可以通過configure()方法將不需要進行權限校驗的 url 排除掉。上面的例子,指定了 靜態資源、login 鏈接不需要過安全驗證,其餘 url 均需要

至此,整個 security 最簡單的功能就已經實現了,是不是非常簡單。下面我們用一個例子來試驗下。定義一個 HomeController

@Controller
public class HomeController implements WebMvcConfigurer {
@GetMapping("/")
public String home(Map<string> model) {
model.put("message", "Hello World");
model.put("title", "Hello Home");
model.put("date", new Date());
return "home";
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
}
}
/<string>

Spring 的 WebMvcConfigurer 接口提供了很多方法讓我們來定製 SpringMVC 的配置,這裡通過 addViewControllers 將 /login 請求映射到了資源 login.html

附上 WebMvcConfigurer 提供的配置方法

「spring boot 系列」spring security 實踐 + 源碼分析

好了,啟動 web 應用,可以體驗安全驗證的效果了。

如何實現多個用戶呢

上面最簡單的示例,用戶權限信息是直接再配置文件中寫死的,那麼如何實現多個用戶呢?多個角色呢?

通過自定義 UserDetailsService 實現,這裡列舉使用內存存放用戶信息的方式。在上述的SecurityConfig中增加配置:

 @Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager() throws Exception {
return new InMemoryUserDetailsManager(
User.withDefaultPasswordEncoder().username("admin").password("admin")
.roles("ADMIN", "USER", "ACTUATOR").build(),
User.withDefaultPasswordEncoder().username("user").password("user")
.roles("USER").build());
}

上述配置添加了2個用戶,admin 和 user

如何實現方法級別的權限控制呢?

答案是也很方便,只要加上一個註解配置即可。在SecurityConfig類上增加如下配置

@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)

開啟註解配置的方式,開啟方法執行前後的安全校驗

寫個簡單的 service 做測試:

@Service
public class SimpleSecureService {

@Secured("ROLE_USER")
public String secure() {
return "Hello User Security";
}
@PreAuthorize("hasRole('ADMIN')")
public String authorized() {
return "Hello Admin Security";
}
}

通過配置,即實現了方法級別的安全校驗,@Secured 和 @PreAuthorize 最大區別是後者支持 spring EL,前者不支持,故後者比前者功能更強大

如何實現權限集成呢?

像上面的例子 admin 只能訪問 admin 授權的接口,而不能訪問 user 的接口,而我們的業務場景往往是 admin 擁有最高權限,可訪問其他所有用戶的資源,故這裡涉及到一個權限繼承的問題(當然你可以在所有方法上都標記 admin 可訪問)。

spring 提供了 RoleHierarchy 接口來實現權限的級聯。

假設需要的級聯關係是

A > B
B > C
C > D
D > E
D > F

那麼對應的一級map配置

A --> [B]
B --> [C]
C --> [D]
D --> [E,F]

構造完之後的關係

A --> [B,C,D,E,F]
B --> [C,D,E,F]
C --> [D,E,F]
D --> [E,F]

關注我的微頭條:進群領取資料!

原理介紹

核心組件

SecurityContextHolder

SecurityContextHolder 用於存儲安全上下文(security context)的信息。當前操作的用戶是誰,該用戶是否已經被認證,他擁有哪些角色權限,這些都被保存在 SecurityContextHolder 中。SecurityContextHolder 默認使用ThreadLocal 策略來存儲認證信息。看到ThreadLocal 也就意味著,這是一種與線程綁定的策略。Spring Security 在用戶登錄時自動綁定認證信息到當前線程,在用戶退出時,自動清除當前線程的認證信息。

如何獲取當前用戶的信息?

因為身份信息是與線程綁定的,所以可以在程序的任何地方使用靜態方法獲取用戶信息。一個典型的獲取當前登錄用戶的姓名的例子如下所示:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}

getAuthentication()返回了認證信息,getPrincipal()返回了身份信息,UserDetails 便是 Spring 對身份信息封裝的一個接口。

Authentication

先看下接口定義

public interface Authentication extends Principal, Serializable {
Collection extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

Authentication 是 spring security 包中的接口,直接繼承自 Principal 類,而 Principal 是位於 java.security 包中的。可以見得,Authentication 在 spring security 中是最高級別的身份/認證的抽象。

由這個頂級接口,我們可以得到用戶擁有的權限信息列表,密碼,用戶細節信息,用戶身份信息,認證信息。接口詳細解讀如下:

  • getAuthorities(),權限信息列表,默認是 GrantedAuthority 接口的一些實現類,通常是代表權限信息的一系列字符串。
  • getCredentials(),密碼信息,用戶輸入的密碼字符串,在認證過後通常會被移除,用於保障安全。
  • getDetails(),細節信息,web 應用中的實現接口通常為 WebAuthenticationDetails,它記錄了訪問者的ip地址和sessionId的值。
  • getPrincipal(),最重要的身份信息,大部分情況下返回的是 UserDetails 接口的實現類,也是框架中的常用接口之一。

AuthenticationManager

初次接觸 Spring Securit y的朋友相信會被 AuthenticationManager,ProviderManager ,AuthenticationProvider,這麼多相似的 Spring 認證類搞得暈頭轉向,但只要稍微梳理一下就可以理解清楚它們的聯繫和設計者的用意。AuthenticationManager(接口)是認證相關的核心接口,也是發起認證的出發點,因為在實際需求中,我們可能會允許用戶使用用戶名+密碼登錄,同時允許用戶使用郵箱+密碼,手機號碼+密碼登錄,甚至,可能允許用戶使用指紋登錄),所以說 AuthenticationManager 一般不直接認證,AuthenticationManager 接口的常用實現類 ProviderManager 內部會維護一個 List<authenticationprovider> 列表,存放多種認證方式,實際上這是委託者模式的應用(Delegate)。/<authenticationprovider>

核心的認證入口始終只有一個:AuthenticationManager,不同的認證方式:用戶名+密碼(UsernamePasswordAuthenticationToken),郵箱+密碼,手機號碼+密碼登錄則對應了三個 AuthenticationProvider。在默認策略下,只需要通過一個 AuthenticationProvider 的認證,即可被認為是登錄成功。

ProviderManager 中的 List,會依照次序去認證,認證成功則立即返回,若認證失敗則返回 null,下一個AuthenticationProvider 會繼續嘗試認證,如果所有認證器都無法認證成功,則 ProviderManager 會拋出一個 ProviderNotFoundException 異常。

到這裡,如果不糾結於 AuthenticationProvider 的實現細節以及安全相關的過濾器,認證相關的核心類其實都已經介紹完畢了:身份信息的存放容器 SecurityContextHolder,身份信息的抽象 Authentication,身份認證器 AuthenticationManager 及其認證流程。姑且在這裡做一個分隔線。下面來介紹下 AuthenticationProvider 接口的具體實現。

DaoAuthenticationProvider

AuthenticationProvider 最最最常用的一個實現便是 DaoAuthenticationProvider。顧名思義,Dao 正是數據訪問層的縮寫,也暗示了這個身份認證器的實現思路。按照我們最直觀的思路,怎麼去認證一個用戶呢?用戶前臺提交了用戶名和密碼,而數據庫中保存了用戶名和密碼,認證便是負責比對同一個用戶名,提交的密碼和保存的密碼是否相同便是了。在 Spring Security 中。提交的用戶名和密碼,被封裝成了 UsernamePasswordAuthenticationToken,而根據用戶名加載用戶的任務則是交給了 UserDetailsService,在 DaoAuthenticationProvider 中,對應的方法便是 retrieveUser,返回一個 UserDetails。還需要完成 UsernamePasswordAuthenticationToken 和 UserDetails密碼的比對,這便是交給 additionalAuthenticationChecks 方法完成的,如果這個 void 方法沒有拋異常,則認為比對成功。比對密碼的過程,用到了 PasswordEncoder 和 SaltSource,密碼加密和鹽的概念相信不用我贅述了,它們為保障安全而設計,都是比較基礎的概念。

DaoAuthenticationProvider:它獲取用戶提交的用戶名和密碼,比對其正確性,如果正確,返回一個數據庫中的用戶信息(假設用戶信息被保存在數據庫中)。

UserDetails與UserDetailsService

上面不斷提到了 UserDetails 這個接口,它代表了最詳細的用戶信息,這個接口涵蓋了一些必要的用戶信息字段,具體的實現類對它進行了擴展。

它和 Authentication 接口很類似,比如它們都擁有 username,authorities,區分他們也是本文的重點內容之一。Authentication 的getCredentials()與 UserDetails 中的getPassword()需要被區分對待,前者是用戶提交的密碼憑證,後者是用戶正確的密碼,認證器其實就是對這兩者的比對。Authentication 中的getAuthorities()實際是由 UserDetails 的getAuthorities()傳遞而形成的。還記得Authentication 接口中的getUserDetails()方法嗎?其中的 UserDetails 用戶詳細信息便是經過了 AuthenticationProvider 之後被填充的。

UserDetailsService 只負責從特定的地方(通常是數據庫)加載用戶信息,僅此而已。UserDetailsService 常見的實現類有 JdbcDaoImpl,InMemoryUserDetailsManager,前者從數據庫加載用戶,後者從內存中加載用戶,也可以自己實現 UserDetailsService,通常這更加靈活。

概覽圖

「spring boot 系列」spring security 實踐 + 源碼分析

總結

用戶登陸,會被 AuthenticationProcessingFilter 攔截,調用 AuthenticationManager 的實現,AuthenticationManager 會調用ProviderManager來獲取用戶驗證信息(不同的 Provider 調用的服務不同,因為這些信息可以是在數據庫上,可以是xml配置文件上等),如果驗證通過後會將用戶的權限信息封裝一個User放到spring的全局緩存SecurityContextHolder中,以備後面訪問資源時使用。

訪問資源(即授權管理)時,會通過 AbstractSecurityInterceptor 攔截器攔截,其中會調用 FilterInvocationSecurityMetadataSource 的方法來獲取被攔截 url 所需的全部權限,在調用授權管理器 AccessDecisionManager,這個授權管理器會通過 spring 的全局緩存 SecurityContextHolder 獲取用戶的權限信息,還會獲取被攔截的url及所需的全部權限,然後根據所配的策略(有:一票決定,一票否定,少數服從多數等),如果權限足夠,則返回,權限不夠則報錯並調用權限不足頁面。

關注我的微頭條:進群領取資料!


分享到:


相關文章: