前言

Spring Security 是一個安全框架, 可以簡單地認為 Spring Security 是放在使用者和 Spring 應用之間的一個安全屏障, 每一個 web 請求都先要經過 Spring Security 進行 Authenticate 和 Authoration 驗證

核心元件

SecurityContextHolder

SecurityContextHolder它持有的是安全上下文(security context)的資訊。當前操作的使用者是誰,該使用者是否已經被認證,他擁有哪些角色權等等,這些都被儲存在SecurityContextHolder中。SecurityContextHolder預設使用ThreadLocal 策略來儲存認證資訊。看到ThreadLocal 也就意味著,這是一種與執行緒繫結的策略。在web環境下,Spring Security在使用者登入時自動繫結認證資訊到當前執行緒,在使用者退出時,自動清除當前執行緒的認證資訊

看原始碼他有靜態方法

  //獲取 上下文
public static SecurityContext getContext() {
return strategy.getContext();
}
//清除上下文
public static void clearContext() {
strategy.clearContext();
}
SecurityContextHolder.getContext().getAuthentication().getPrincipal()

getAuthentication()返回了認證資訊,getPrincipal()返回了身份資訊

UserDetails便是Spring對身份資訊封裝的一個介面

SecurityContext

安全上下文,主要持有Authentication物件,如果使用者未鑑權,那Authentication物件將會是空的。看原始碼可知

package org.springframework.security.core.context;

import java.io.Serializable;
import org.springframework.security.core.Authentication; public interface SecurityContext extends Serializable {
Authentication getAuthentication(); void setAuthentication(Authentication var1);
}

Authentication

鑑權物件,該物件主要包含了使用者的詳細資訊(UserDetails)和使用者鑑權時所需要的資訊,如使用者提交的使用者名稱密碼、Remember-me Token,或者digest hash值等,按不同鑑權方式使用不同的Authentication實現

看原始碼可知道

package org.springframework.security.core;

import java.io.Serializable;
import java.security.Principal;
import java.util.Collection; public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities(); Object getCredentials(); Object getDetails(); Object getPrincipal(); boolean isAuthenticated(); void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
  1. Authentication是spring security包中的介面,直接繼承自Principal類,而Principal是位於java.security包中的。可以見得,Authentication在spring security中是最高級別的身份/認證的抽象。由這個頂級介面,我們可以得到使用者擁有的許可權資訊列表,密碼,使用者細節資訊,使用者身份資訊,認證資訊。

  2. getAuthorities(),許可權資訊列表,預設是GrantedAuthority介面的一些實現類,通常是代表權限資訊的一系列字串。

  3. getCredentials(),密碼資訊,使用者輸入的密碼字串,在認證過後通常會被移除,用於保障安全。

  4. getDetails(),細節資訊,web應用中的實現介面通常為 WebAuthenticationDetails,它記錄了訪問者的ip地址和sessionId的值。

  5. getPrincipal(),敲黑板!!!最重要的身份資訊,大部分情況下返回的是UserDetails介面的實現類,也是框架中的常用介面之一

注意GrantedAuthority該介面表示了當前使用者所擁有的許可權(或者角色)資訊。這些資訊由授權負責物件AccessDecisionManager來使用,並決定終端使用者是否可以訪問某資源(URL或方法呼叫或域物件)。鑑權時並不會使用到該物件

UserDetails

這個介面規範了使用者詳細資訊所擁有的欄位,譬如使用者名稱、密碼、賬號是否過期、是否鎖定等。在Spring Security中,獲取當前登入的使用者的資訊,一般情況是需要在這個介面上面進行擴充套件,用來對接自己系統的使用者

看原始碼可知

package org.springframework.security.core.userdetails;

import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority; public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled();
}

UserDetailsService

這個介面只提供一個介面loadUserByUsername(String username),這個介面非常重要,一般情況我們都是通過擴充套件這個介面來顯示獲取我們的使用者資訊,使用者登陸時傳遞的使用者名稱和密碼也是通過這裡這查找出來的使用者名稱和密碼進行校驗,但是真正的校驗不在這裡,而是由AuthenticationManager以及AuthenticationProvider負責的,需要強調的是,如果使用者不存在,不應返回NULL,而要丟擲異常UsernameNotFoundException

看原始碼可知

package org.springframework.security.core.userdetails;

public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

Spring Security安全身份認證流程原理

  1. 使用者名稱和密碼被過濾器獲取到,封裝成Authentication,通常情況下是UsernamePasswordAuthenticationToken這個實現類。

  2. AuthenticationManager 身份管理器負責驗證這個Authentication

  3. 認證成功後,AuthenticationManager身份管理器返回一個被填充滿了資訊的(包括上面提到的許可權資訊,身份資訊,細節資訊,但密碼通常會被移除)Authentication例項。

  4. SecurityContextHolder安全上下文容器將第3步填充了資訊的Authentication,通過SecurityContextHolder.getContext().setAuthentication()方法,設定到其中。

AuthenticationManager

初次接觸Spring Security的朋友相信會被AuthenticationManager,ProviderManager ,AuthenticationProvider …這麼多相似的Spring認證類搞得暈頭轉向,但只要稍微梳理一下就可以理解清楚它們的聯絡和設計者的用意。

AuthenticationManager(介面)是認證相關的核心介面,也是發起認證的出發點,因為在實際需求中,我們可能會允許使用者使用使用者名稱+密碼登入,同時允許使用者使用郵箱+密碼,手機號碼+密碼登入,甚至,可能允許使用者使用指紋登入(還有這樣的操作?沒想到吧),所以說AuthenticationManager一般不直接認證,

AuthenticationManager介面的常用實現類ProviderManager 內部會維護一個List<AuthenticationProvider>列表,存放多種認證方式,實際上這是委託者模式的應用(Delegate)。

也就是說,核心的認證入口始終只有一個:AuthenticationManager,不同的認證方式:使用者名稱+密碼(UsernamePasswordAuthenticationToken),郵箱+密碼,手機號碼+密碼登入則對應了三個AuthenticationProvider。這樣一來就好理解多了

UserDetails和UserDetailsService

UserDetails

上面不斷提到了UserDetails這個介面,它代表了最詳細的使用者資訊,這個介面涵蓋了一些必要的使用者資訊欄位,我們一般都需要對它進行必要的擴充套件。

它和Authentication介面很類似,比如它們都擁有username,authorities,區分他們也是本文的重點內容之一。

Authentication的getCredentials()與UserDetails中的getPassword()需要被區分對待,前者是使用者提交的密碼憑證,後者是使用者正確的密碼,認證器其實就是對這兩者的比對。Authentication中的getAuthorities()實際是由UserDetails的getAuthorities()傳遞而形成的。還記得Authentication介面中的getUserDetails()方法嗎?其中的UserDetails使用者詳細資訊便是經過了AuthenticationProvider之後被填充的。

UserDetailsService

UserDetailsService和AuthenticationProvider兩者的職責常常被人們搞混,UserDetailsService只負責從特定的地方載入使用者資訊,可以是資料庫、redis快取、介面等

全域性獲取使用者資訊方式

  1. 通過注入 Principal 介面獲取使用者資訊

在執行過程中,Spring 會將 Username、Password、Authentication、Token 注入到 Principal 介面中,我們可以直接在controller獲取使用

   @GetMapping("/home")
@ApiOperation("使用者中心")
public Result getUserHome(Principal principal) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=(UsernamePasswordAuthenticationToken)principal;
return ResultResponse.success(usernamePasswordAuthenticationToken.getPrincipal());
}
  1. 使用 @AuthenticationPrincipal 註解引數的方式
   @GetMapping("/home")
@ApiOperation("使用者中心")
public Result getUserHome(@AuthenticationPrincipal cn.soboys.kmall.security.entity.User user ) {
return ResultResponse.success(user);
}
  1. 全域性上下文獲取

由於獲取當前使用者的使用者名稱是一種比較常見的需求,其實 Spring Security 在 Authentication 中的實現類中已經為我們做了相關實現,所以獲取當前使用者的使用者名稱有如下更簡單的方式

@RestController
public class HelloController { @GetMapping("/hello")
public String hello() {
return "當前登入使用者:" + SecurityContextHolder.getContext().getAuthentication().getName();
}
}
  1. 獲取當前登入使用者的 UserDetails 例項,然後再轉換成自定義的使用者實體類 User,這樣便能獲取使用者的 ID 等資訊
@RestController
public class HelloController { @GetMapping("/hello")
public String hello() {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
User user = (User)principal;
return "當前登入使用者資訊:" + user.toString();
}
}
  1. 非同步方法中獲取使用者資訊

Spring Security在預設情況下無法在使用@Async註解的方法中獲取當前登入使用者的。若想在@Async方法中獲取當前登入使用者,則需要呼叫SecurityContextHolder.setStrategyName方法並設定相關的策略

參考

  1. Spring Security在@Async非同步方法中獲取登入使用者
  2. Spring Security