本文主要展示內容有二:
- SpringBoot專案如何整合Shiro框架,從前端請求用戶登錄,到后端判斷用戶、角色、權限認證,再到登錄后請求獲取用戶資訊,最后用戶注銷,
- 基于以上整合代碼的示例,針對其中shiro框架核心邏輯進行原始碼決議,
在開始閱讀文章之前,建議了解Shiro框架的基本使用,才能更好了解本文中所展示的代碼示例,可以閱讀此文入門:Shiro框架基本使用,
首先了解下資料庫的表結構
這是一個典型的 用戶——角色——權限 表結構,用戶可以擁有多個角色,角色也可擁有多個權限,

SpringBoot專案匯入Shiro依賴
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.2</version>
</dependency>
自定義登錄認證Realm類
public class CertificationRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 獲取用戶授權資訊
*
* @param principals 封裝了用戶名的物件
* @return 用戶授權資訊
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); // 創建用戶授權資訊物件
User user = (User) principals.getPrimaryPrincipal(); // 獲取用戶名
List<Role> roles = userService.getRolesByAccount(user.getAccount()); // 根據用戶名獲取對應的角色串列
Set<String> roleIds = new HashSet<>();
roles.forEach(role -> roleIds.add(role.getId())); // 獲取角色串列的id,并封裝成set集合
authorizationInfo.setRoles(roleIds); // 添加角色id串列
List<Permission> permissions = userService.getPermissionsByAccount(user.getAccount()); // 根據用戶名獲取對應的權限串列
Set<String> permissionIds = new HashSet<>();
permissions.forEach(permission -> permissionIds.add(permission.getId())); // 獲取權限串列的id,并封裝成set集合
authorizationInfo.setStringPermissions(permissionIds); // 添加權限id串列
return authorizationInfo; // 回傳用戶授權資訊物件
}
/**
* 獲取用戶資訊
*
* @param token 用戶名、密碼token
* @return 用戶資訊
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String account = (String) token.getPrincipal(); // 獲取用戶名
User user = userService.getByAccount(account); // 根據用戶名獲取用戶
if (user == null) { // 用戶為null,回傳null
return null;
}
SimpleAuthenticationInfo authenticationInfo =
new SimpleAuthenticationInfo(user, user.getPassword(), getName()); // 新建用戶身份認證物件,并封裝用戶、用戶密碼、realm資訊
authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes("slot")); // 設定密碼加密鹽值
return authenticationInfo; // 回傳用戶身份認證物件
}
}
自定義過濾器,重寫Shiro框架默認的過濾器認證失敗邏輯
在Shiro框架默認的過濾器實作中,如果用戶未登錄系統就訪問請求,那么在請求到達框架默認的過濾器時,會認證失敗,然后重定向到 login.jsp 頁面,
這是Shiro框架默認的過濾器實作,在如今前后端專案分離的時代,前后端都是以JSON資料進行互動,因此需要我們自己自定義一個新的過濾器類,重寫Shiro框架默認的過濾器認證失敗邏輯,
public class LoginVerificationFilter extends FormAuthenticationFilter {
/**
* 自定義過濾器覆寫Shiro框架默認過濾認證失敗邏輯
*
* @param request 請求
* @param response 回應
* @return false
* @throws IOException
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
response.setCharacterEncoding("UTF-8");
PrintWriter writer = response.getWriter();
writer.write("用戶未登錄");
writer.flush();
writer.close();
return false;
}
}
創建Shiro配置類,定義相關bean
@Configuration
public class ShiroConfig {
/**
* 創建密碼匹配器,定義加密/解密規則
*
* @return 密碼匹配器
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5"); // 使用MD5演算法加密
hashedCredentialsMatcher.setHashIterations(2); // 加密次數為2
return hashedCredentialsMatcher;
}
/**
* 創建自定義用戶認證realm物件,并設定密碼匹配器
*
* @return 自定義用戶認證realm物件
*/
@Bean
public CertificationRealm certificationRealm(HashedCredentialsMatcher hashedCredentialsMatcher) {
CertificationRealm certificationRealm = new CertificationRealm();
certificationRealm.setCredentialsMatcher(hashedCredentialsMatcher); // 設定密碼匹配器
return certificationRealm;
}
/**
* 宣告安全管理器
*
* @param certificationRealm 自定義的用戶認證realm物件
* @return 安全管理器
*/
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(CertificationRealm certificationRealm) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(certificationRealm); // 設定自定義realm物件
return defaultWebSecurityManager;
}
/**
* 設定安全管理器
* 使用自定義的登錄校驗過濾器代替Shiro框架默認校驗過濾器,并指定需要排除校驗的路徑
*
* @param defaultWebSecurityManager 安全管理器
* @return Shiro過濾器工廠bean
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager); // 設定安全管理器
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("authc", new LoginVerificationFilter());
shiroFilterFactoryBean.setFilters(filterMap); // 將自定義的登錄校驗過濾器,替換掉框架默認的校驗過濾器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/user/login", "anon"); // 校驗排除/user/login路徑的請求
filterChainDefinitionMap.put("/**", "authc"); // 除了排除的請求,其他請求都需要進行校驗
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); // 設定各個定義好路徑的過濾器
return shiroFilterFactoryBean;
}
}
此處補充下有關Shiro框架過濾器的實體命名:
- anon:不認證也可以訪問,即該過濾器實體中設定的請求路徑,不需要進行用戶認證即可訪問,
- authc:必須認證后才可訪問,即該過濾器實體中設定的請求路徑,需要進行用戶認證,并認證成功才可訪問,
最后,創建Controller類進行接收請求
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("/login")
public String login(@RequestBody User user) {
String account = user.getAccount();
String password = user.getPassword();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(account, password);
Subject subject = SecurityUtils.getSubject();
// 校驗用戶名、密碼
try {
subject.login(usernamePasswordToken);
} catch (UnknownAccountException e) {
return "用戶不存在";
} catch (IncorrectCredentialsException e) {
return "密碼錯誤";
} catch (Exception e) {
return "登錄失敗";
}
// 校驗角色
if (!subject.hasRole("ar")) {
return "用戶無ar角色";
}
// 校驗權限
if (!subject.isPermitted("ap")) {
return "用戶無ap權限";
}
return "登錄成功";
}
@GetMapping("/getInfo")
public User getInfo() {
User user = (User) SecurityUtils.getSubject().getPrincipal();
return user;
}
@PostMapping("/logout")
public String logout() {
SecurityUtils.getSubject().logout();
return "注銷成功";
}
}
代碼整合完成,打開PostMan發送請求測驗功能
首先訪問/user/getInfo介面,獲取用戶資訊:

回應結果為“用戶未登錄”,這里對應的就是之前自定義的LoginVerificationFilter過濾器類中的認證失敗方法,
接下來,對用戶進行登錄,攜帶引數訪問/user/login介面:

用戶登錄成功(對于其他認證失敗的情況,例如:用戶不存在,密碼錯誤等情況,大家自行測驗),
然后再次訪問/user/getInfo介面,結果如下:

回應結果不再是“用戶未登錄”,而是回傳已登錄過的用戶資訊,
最后,用戶進行注銷,訪問介面/user/logout:

到此,SpringBoot整合Shiro框架完成,如果只是要求掌握SpringBoot專案中如何使用Shiro,那么可以結束閱讀了,因為接來下的內容就是針對以上整合的代碼,對Shiro框架中的核心原始碼進行決議,
Shiro核心原始碼決議
在閱讀原始碼前,先來了解下Shiro框架中核心的組件Security Mananger的繼承體系:

在專案中的Security Manager組件使用的正是DefaultWebSecurity Manager(Web安全管理器),而它同時繼承了AuthorizingSecurity Manager(授權認證安全管理器)和AuthenticatingSecurity Manager(身份認證安全管理器),在之后的原始碼決議中的用戶認證流程中會使用到這兩個安全管理器,在此先做個簡單的了解,
核心組件初始化
首先從Shiro核心組件初始化開始了解原始碼,因此來看下ShiroConfig配置類中初始化了哪些bean,
初始化核心組件DefaultWebSecurity Mananger:
/**
* 宣告安全管理器
*
* @param certificationRealm 自定義的用戶認證realm物件
* @return 安全管理器
*/
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(CertificationRealm certificationRealm) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(certificationRealm); // 設定自定義realm物件
return defaultWebSecurityManager;
}
進入DefaultWebSecurityManager的構造方法來看下其中的邏輯:
public DefaultWebSecurityManager() {
super(); // 初始化父類
..
}
可以看到在new DefaultWebSecurityManager()方法中,呼叫了super()對父類的物件也進行了初始化,跟蹤除錯發現,在上層的構造方法中也會層層呼叫super(),結合之前的繼承體系圖,發現所謂的AuthorizingSecurity Manager(授權認證安全管理器)和AuthenticatingSecurity Manager(身份認證安全管理器)也在這時被新建了:
public AuthorizingSecurityManager() {
super();
this.authorizer = new ModularRealmAuthorizer(); // authorizer主要負責存盤realm域物件,以及呼叫授權認證相關方法
}
public AuthenticatingSecurityManager() {
super();
this.authenticator = new ModularRealmAuthenticator(); // authenticator主要負責存盤realm域,以及呼叫身份認證相關方法
}
安全管理器主要是處理與安全相關的認證互動,簡單來說,就是身份認證、授權認證方法的呼叫者,那么認證的途徑有了,還需要認證的資料(用戶、角色、權限資訊),這時候就需要設定對應的Realm:
public void setRealm(Realm realm) { // 此處傳進來的是自定義的CertificationRealm物件
if (realm == null) {
throw new IllegalArgumentException("Realm argument cannot be null");
}
Collection<Realm> realms = new ArrayList<Realm>(1); // 定義realm域集合
realms.add(realm); // 添加realm
setRealms(realms); // 設定realm集合
}
public void setRealms(Collection<Realm> realms) {
if (realms == null) {
throw new IllegalArgumentException("Realms collection argument cannot be null.");
}
if (realms.isEmpty()) {
throw new IllegalArgumentException("Realms collection argument cannot be empty.");
}
this.realms = realms; // 屬性系結realm集合
afterRealmsSet();
}
此處需要關注重點方法afterRealmsSet(),跟蹤除錯后發現不單單是對DefaultWebSecurity Manager設定了realm域,同時也對authorizer和authenticator設定了realm域:
protected void afterRealmsSet() {
super.afterRealmsSet();
if (this.authorizer instanceof ModularRealmAuthorizer) {
((ModularRealmAuthorizer) this.authorizer).setRealms(getRealms()); // 對權限認證管理器設定realm
}
}
protected void afterRealmsSet() {
super.afterRealmsSet();
if (this.authenticator instanceof ModularRealmAuthenticator) {
((ModularRealmAuthenticator) this.authenticator).setRealms(getRealms()); // 對身份認證管理器設定realm
}
}
接著還需要初始化過濾器,因為Shiro的認證功能的實作,同時依賴于它的過濾器過濾非法(例如:未登錄狀態)用戶,所以再來看下過濾器是如何初始化的:
/**
* 設定安全管理器
* 使用自定義的登錄校驗過濾器代替Shiro框架默認校驗過濾器,并指定需要排除校驗的路徑
*
* @param defaultWebSecurityManager 安全管理器
* @return Shiro過濾器工廠bean
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager); // 設定安全管理器
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("authc", new LoginVerificationFilter());
shiroFilterFactoryBean.setFilters(filterMap); // 將自定義的登錄校驗過濾器,替換掉框架默認的校驗過濾器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/user/login", "anon"); // 校驗排除/user/login路徑的請求
filterChainDefinitionMap.put("/**", "authc"); // 除了排除的請求,其他請求都需要進行校驗
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); // 設定各個定義好路徑的過濾器
return shiroFilterFactoryBean;
}
從以上代碼可知,Shiro的過濾器主要由ShiroFilterFactoryBean工廠類提供實體,因此看下ShiroFilterFactoryBean的構造方法:
public ShiroFilterFactoryBean() {
this.filters = new LinkedHashMap<String, Filter>();
this.filterChainDefinitionMap = new LinkedHashMap<String, String>(); //order matters!
}
ShiroFilterFactoryBean的構造方法非常簡單,只是定義了各個過濾器實體映射,和各個過濾器對應路徑映射屬性,主要是為了滿足開發者定制化的需求(例如,以上例子就體現了替換默認過濾器,定義各過濾器對應過濾的請求路徑),除此之外,同時還注入并設定了已創建好的DefaultWebSecurityManager實體,意味著已規定好用戶的認證流程,
用戶登錄認證
初始化已完成,下面決議用戶認證原始碼,首先來看用戶登錄原始碼:
@PostMapping("/login")
public String login(@RequestBody User user) {
... // 省略多余代碼
// 校驗用戶名、密碼
try {
subject.login(usernamePasswordToken);
} catch (UnknownAccountException e) {
return "用戶不存在";
} catch (IncorrectCredentialsException e) {
return "密碼錯誤";
} catch (Exception e) {
return "登錄失敗";
}
...
}
public void login(AuthenticationToken token) throws AuthenticationException {
clearRunAsIdentitiesInternal(); // 清除之前的用戶登錄資訊
Subject subject = securityManager.login(this, token); // 使用安全管理器進行登錄認證
PrincipalCollection principals;
String host = null;
if (subject instanceof DelegatingSubject) {
DelegatingSubject delegating = (DelegatingSubject) subject;
//we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
principals = delegating.principals;
host = delegating.host;
} else {
principals = subject.getPrincipals();
}
if (principals == null || principals.isEmpty()) {
String msg = "Principals returned from securityManager.login( token ) returned a null or " +
"empty value. This value must be non null and populated with one or more elements.";
throw new IllegalStateException(msg);
}
this.principals = principals;
this.authenticated = true;
if (token instanceof HostAuthenticationToken) {
host = ((HostAuthenticationToken) token).getHost();
}
if (host != null) {
this.host = host;
}
Session session = subject.getSession(false);
if (session != null) {
this.session = decorate(session);
} else {
this.session = null;
}
}
在每次登錄認證前,都需要清除之前登錄認證成功所保存到session的資訊:
private void clearRunAsIdentitiesInternal() {
try {
clearRunAsIdentities();
} catch (SessionException se) {
log.debug("Encountered session exception trying to clear 'runAs' identities during logout. This can generally safely be ignored.", se);
}
}
private void clearRunAsIdentities() {
Session session = getSession(false); // 獲取已存在的session
if (session != null) { // 若不為空,則洗掉之前的已登錄資訊
session.removeAttribute(RUN_AS_PRINCIPALS_SESSION_KEY);
}
}
清除之前的登錄資訊,接著就是進行本次的登錄認證:
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = authenticate(token); // 根據傳入的用戶token進行登錄認證
} catch (AuthenticationException ae) {
...
}
Subject loggedIn = createSubject(token, info, subject);
onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
return this.authenticator.authenticate(token);
}
從這個authenticate的方法體中可以看到,其實真正進行身份認證的操作是由之前說到的this.authenticator去執行:
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
...
AuthenticationInfo info;
try {
info = doAuthenticate(token); // 根據token執行身份認證,并回傳用戶認證資訊
if (info == null) {
String msg = "No account information found for authentication token [" + token + "] by this Authenticator instance. Please check that it is configured correctly.";
throw new AuthenticationException(msg);
}
} catch (Throwable t) {
...
}
...
return info;
}
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured(); // 斷言設定的realm域是否存在
Collection<Realm> realms = getRealms(); // 獲取realm域集合
if (realms.size() == 1) { // realm域集合中只存在一個realm域元素
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); // 執行單個realm域認證
} else {
return doMultiRealmAuthentication(realms, authenticationToken); // 否則,執行多個realm域認證
}
}
認證前需要對realm域集合進行判斷,若realm域集合為空,則說明無法和token中的用戶資訊作對比認證,直接拋例外即可:
protected void assertRealmsConfigured() throws IllegalStateException {
Collection<Realm> realms = getRealms();
if (CollectionUtils.isEmpty(realms)) {
String msg = "Configuration error: No realms have been configured! One or more realms must be present to execute an authentication attempt.";
throw new IllegalStateException(msg);
}
}
確保存在至少一個realm域,則可以進行登錄認證(之前只設定了一個realm域,因此對執行單個realm域認證方法進行決議,多個realm認證也是相同的邏輯,有興趣可以自行研究):
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
...
AuthenticationInfo info = realm.getAuthenticationInfo(token); // 呼叫realm域物件方法,根據token認證用戶資訊,并回傳用戶認證資訊
if (info == null) { // 若回傳的用戶認證資訊為空,則說明不存在該用戶,拋出UnknownAccountException例外
String msg = "Realm [" + realm + "] was unable to find account data for the submitted AuthenticationToken [" + token + "].";
throw new UnknownAccountException(msg);
}
return info; // 用戶存在,回傳用戶認證資訊
}
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info = getCachedAuthenticationInfo(token); // 先嘗試從快取中獲取用戶認證資訊
if (info == null) { // 快取中無用戶認證資訊
info = doGetAuthenticationInfo(token); // 呼叫realm物件方法,獲取用戶認證資訊
log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
if (token != null && info != null) { // 獲取到的用戶認證資訊,以及傳入token不為空
cacheAuthenticationInfoIfPossible(token, info); // 則嘗試加入快取
}
} else {
log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
}
if (info != null) { // 若用戶認證資訊不為空
assertCredentialsMatch(token, info); // 則判斷是否存在加密匹配器,存在則利用加密匹配器中的加密規則,對token和info進行密碼匹配
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}
return info; // 最后回傳用戶認證資訊
}
看到doGetAuthenticationInfo方法是否很眼熟呢?是的,此處就是呼叫之前自定義的CertificationRealm類中獲取用戶認證資訊的方法:
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String account = (String) token.getPrincipal(); // 獲取用戶名
User user = userService.getByAccount(account); // 根據用戶名獲取用戶
if (user == null) { // 用戶為null,回傳null
return null;
}
SimpleAuthenticationInfo authenticationInfo =
new SimpleAuthenticationInfo(user, user.getPassword(), getName()); // 新建用戶身份認證物件,并封裝用戶、用戶密碼、realm資訊
authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes("slot")); // 設定密碼加密鹽值
return authenticationInfo; // 回傳用戶身份認證物件
}
從doGetAuthenticationInfo方法中可以看到,最后回傳的authenticationInfo物件當中封裝了用戶資訊物件、用戶密碼,以及當前執行認證方法的reaml名:
public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName) {
this.principals = new SimplePrincipalCollection(principal, realmName); // 將傳入的用戶資訊和realm名進行封裝,并賦值到principals屬性
this.credentials = credentials; // 將傳入的用戶密碼賦值到credentials屬性
}
以上代碼還有個細節,就是將用戶資訊和realm名進行統一的封裝成SimplePrincipalCollection物件,這里看下SimplePrincipalCollection的構造方法:
public SimplePrincipalCollection(Object principal, String realmName) {
// 添加用戶資訊和realm名
if (principal instanceof Collection) {
addAll((Collection) principal, realmName);
} else {
add(principal, realmName);
}
}
添加用戶資訊:
public void add(Object principal, String realmName) {
if (realmName == null) {
throw new IllegalArgumentException("realmName argument cannot be null.");
}
if (principal == null) {
throw new IllegalArgumentException("principal argument cannot be null.");
}
this.cachedToString = null;
getPrincipalsLazy(realmName).add(principal); // 根據realm名初始化用于存盤用戶資訊的集合,再添加用戶資訊
}
根據傳入的realm名初始化用戶資訊集合(實際上就是新建Map,以realm名為鍵,以用戶資訊Set集合為value保存對應的用戶資訊):
protected Collection getPrincipalsLazy(String realmName) {
if (realmPrincipals == null) {
realmPrincipals = new LinkedHashMap<String, Set>(); // 新建Map
}
Set principals = realmPrincipals.get(realmName); // 先嘗試根據realm名獲取對應的用戶資訊集合
if (principals == null) { // 用戶資訊集合為空,則添加新的集合
principals = new LinkedHashSet();
realmPrincipals.put(realmName, principals);
}
return principals; // 回傳對應的用戶資訊集合
}
回傳Set集合后,再將傳入的用戶資訊物件添加進去,到此,回傳的SimpleAuthenticationInfo用戶身份認證物件便創建完成,
回傳用戶認證物件不為空,說明用戶存在,則下一步就是驗證用戶密碼,而用戶密碼是從資料庫中讀取出來經過加密的密碼,而token中包含的用戶密碼則是請求傳入的明文密碼,這個時候就需要進一步進行密碼之間的加解密對比認證:
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
CredentialsMatcher cm = getCredentialsMatcher(); // 獲取之前設定的加密匹配器
if (cm != null) { // 加密匹配器不為空
if (!cm.doCredentialsMatch(token, info)) { // 使用加密匹配器中的加密規則,根據token和Info中的用戶密碼進行對比認證
String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";
throw new IncorrectCredentialsException(msg); // 密碼匹配錯誤,則拋出IncorrectCredentialsException例外
}
} else {
throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify credentials during authentication. If you do not wish for credentials to be examined, you can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance."); // 若不存在加密匹配器(若不設定加密匹配器,則Shiro會默認設定一個,只是沒有對應加密規則),則拋出AuthenticationException例外
}
}
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenCredentials = getCredentials(token);
Object accountCredentials = getCredentials(info);
return equals(tokenCredentials, accountCredentials); // 密碼對比認證
}
到此,如果用戶資訊認證賬號、密碼都通過的話,那么就會回傳對應的用戶認證資訊,這時候再回頭來看下,最初呼叫獲取用戶認證資訊的入口方法:
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = authenticate(token); // 認證通過,獲取到用戶認證資訊
} catch (AuthenticationException ae) {
...
}
Subject loggedIn = createSubject(token, info, subject); // 根據傳入token、用戶認證資訊、當前subject再創建一個新的屬于已登錄狀態的subject
onSuccessfulLogin(token, info, loggedIn);
return loggedIn; // 回傳新建的subject
}
回傳新建屬于已登錄狀態的subject之后,再回頭看獲到subject之后的邏輯:
public void login(AuthenticationToken token) throws AuthenticationException {
clearRunAsIdentitiesInternal();
Subject subject = securityManager.login(this, token); // 用戶登錄認證成功后,獲取屬于已登錄狀態的subject
PrincipalCollection principals;
String host = null;
// 獲取上面回傳的已登錄狀態的subject中的principals屬性(即在realm的doGetAuthenticationInfo方法中封裝的用戶資訊)
if (subject instanceof DelegatingSubject) {
DelegatingSubject delegating = (DelegatingSubject) subject;
principals = delegating.principals;
host = delegating.host;
} else {
principals = subject.getPrincipals();
}
if (principals == null || principals.isEmpty()) {
String msg = "Principals returned from securityManager.login( token ) returned a null or empty value. This value must be non null and populated with one or more elements.";
throw new IllegalStateException(msg);
}
this.principals = principals; // 將獲取到principals賦值到subject的principals屬性(之后若需要獲取用戶資訊就回傳此subject的principals屬性即可)
this.authenticated = true; // 將subject標識為已通過登錄認證
...
Session session = subject.getSession(false); // 獲取session
if (session != null) {
this.session = decorate(session); // 生成一個基于session的代理session,并賦值到subject的session的屬性
} else {
this.session = null;
}
}
到此,用戶登錄認證流程的原始碼決議完畢,
用戶角色、權限認證
@PostMapping("/login")
public String login(@RequestBody User user) {
...
// 校驗角色
if (!subject.hasRole("ar")) {
return "用戶無ar角色";
}
// 校驗權限
if (subject.isPermitted("ap")) {
return "用戶無ap權限";
}
...
}
首先來看下用戶的角色是如何認證:
public boolean hasRole(String roleIdentifier) {
// 首先需要判斷當前是否存在principals(即用戶資訊)
// 存在用戶資訊,說明用戶已登錄,接著再判斷用戶是否擁有指定角色
return hasPrincipals() && securityManager.hasRole(getPrincipals(), roleIdentifier);
}
確認用戶已登錄,使用初始化階段就創建好的authorizer判斷用戶是否擁有指定角色:
public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
return this.authorizer.hasRole(principals, roleIdentifier);
}
public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
assertRealmsConfigured(); // 斷言是否存在可認證的realm域
for (Realm realm : getRealms()) { // 遍歷realms集合(說明可設定多個realms域)
if (!(realm instanceof Authorizer)) continue;
if (((Authorizer) realm).hasRole(principals, roleIdentifier)) { // 對每個realm都進行角色認證,只要存在一個realm域可通過用戶角色認證,就認證成功
return true;
}
}
return false; // 否則認證失敗
}
先確保存在可認證的realm域:
protected void assertRealmsConfigured() throws IllegalStateException {
Collection<Realm> realms = getRealms();
if (realms == null || realms.isEmpty()) { // realm域集合為空,拋出例外,中止用戶角色認證
String msg = "Configuration error: No realms have been configured! One or more realms must be present to execute an authorization operation.";
throw new IllegalStateException(msg);
}
}
確認存在可認證的realm域后,繼續執行角色認證:
public boolean hasRole(PrincipalCollection principal, String roleIdentifier) {
AuthorizationInfo info = getAuthorizationInfo(principal); // 根據用戶資訊,獲取用戶授權資訊
return hasRole(roleIdentifier, info); // 根據傳入的指定角色和獲取到的用戶授權資訊作對比,查看用戶是否擁有指定角色
}
如何獲取用戶授權資訊:
protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
...
AuthorizationInfo info = null;
Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache(); // 從快取中獲取用戶授權資訊映射cache
if (cache != null) {
...
Object key = getAuthorizationCacheKey(principals);
info = cache.get(key); // 根據映射cache獲取已認證過的用戶授權資訊
...
}
if (info == null) { // info為空,說明快取獲取失敗
info = doGetAuthorizationInfo(principals); // 重點關注,獲取用戶授權資訊
if (info != null && cache != null) {
if (log.isTraceEnabled()) {
log.trace("Caching authorization info for principals: [" + principals + "].");
}
Object key = getAuthorizationCacheKey(principals);
cache.put(key, info); // 將用戶授權資訊加入快取
}
}
return info; // 最后回傳用戶授權資訊
}
以上原始碼中有個doGetAuthorizationInfo方法是否很眼熟呢?不錯,此處正是呼叫了自定義CertificationRealm類中獲取用戶授權資訊的方法:
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); // 創建用戶授權資訊物件
User user = (User) principals.getPrimaryPrincipal(); // 獲取用戶名
List<Role> roles = userService.getRolesByAccount(user.getAccount()); // 根據用戶名獲取對應的角色串列
Set<String> roleIds = new HashSet<>();
roles.forEach(role -> roleIds.add(role.getId())); // 獲取角色串列的id,并封裝成set集合
authorizationInfo.addRoles(roleIds); // 添加角色id串列
List<Permission> permissions = userService.getPermissionsByAccount(user.getAccount()); // 根據用戶名獲取對應的權限串列
Set<String> permissionIds = new HashSet<>();
permissions.forEach(permission -> permissionIds.add(permission.getId())); // 獲取權限串列的id,并封裝成set集合
authorizationInfo.addStringPermissions(permissionIds); // 添加權限id串列
return authorizationInfo; // 回傳用戶授權資訊物件
}
從自定義的doGetAuthorizationInfo方法中可以看到,所謂的用戶授權資訊,其實就是包含了用戶所擁有的的角色和權限資訊,
獲取到用戶授權資訊后,下一步就是用戶角色的認證:
protected boolean hasRole(String roleIdentifier, AuthorizationInfo info) {
// 所謂的用戶角色認證,其實就是根據從資料庫讀取的用戶角色串列中,是否存在指定的角色
return info != null && info.getRoles() != null && info.getRoles().contains(roleIdentifier);
}
最后回傳用戶角色認證結果true或false,到此,用戶角色認證原始碼決議完成,
因為用戶權限的認證和角色認證,原始碼以及流程都基本一致,在此就不在贅述了,感興趣的朋友可以自行研究,
獲取用戶資訊
當用戶通過認證后,會將從資料庫中獲取到的用戶資訊存入subject中,當我們需要獲取用戶資訊時候:
@GetMapping("/getInfo")
public User getInfo() {
User user = (User) SecurityUtils.getSubject().getPrincipal(); // 從subject中獲取用戶資訊
return user;
}
public Object getPrincipal() {
return getPrimaryPrincipal(getPrincipals()); // 從封裝過的用戶資訊物件中獲取原生的用戶資訊
}
private Object getPrimaryPrincipal(PrincipalCollection principals) {
if (!isEmpty(principals)) {
return principals.getPrimaryPrincipal(); // 獲取并回傳用戶資訊
}
return null;
}
public Object getPrimaryPrincipal() {
if (isEmpty()) {
return null;
}
return iterator().next(); // 回傳用戶資訊集合中的首個用戶
}
public Iterator iterator() {
return asSet().iterator();
}
經過層層的呼叫,終于到了關鍵代碼:
public Set asSet() {
if (realmPrincipals == null || realmPrincipals.isEmpty()) {
return Collections.EMPTY_SET;
}
Set aggregated = new LinkedHashSet(); // 新建一個保存用戶資訊的set集合
Collection<Set> values = realmPrincipals.values(); // 從登錄認證時候封裝了用戶資訊的realmPrincipals中獲取用戶資訊
for (Set set : values) {
aggregated.addAll(set); // 添加用戶資訊集合
}
if (aggregated.isEmpty()) {
return Collections.EMPTY_SET;
}
return Collections.unmodifiableSet(aggregated); // 最后回傳用戶資訊set集合
}
通過以上代碼可知,獲取用戶資訊其實就是從登錄認證時候封裝了用戶資訊的realmPrincipals中獲取并回傳,
用戶注銷
還有最后的用戶注銷原始碼,先來看下注銷的方法入口:
@PostMapping("/logout")
public String logout() {
SecurityUtils.getSubject().logout();
return "注銷成功";
}
依然是呼叫subject的注銷方法:
public void logout() {
try {
clearRunAsIdentitiesInternal(); // 清空session中的登錄認證資訊
this.securityManager.logout(this); // 使用安全管理器呼叫注銷方法
} finally {
// 清空登錄記錄
this.session = null;
this.principals = null;
this.authenticated = false;
}
}
public void logout(Subject subject) {
if (subject == null) {
throw new IllegalArgumentException("Subject method argument cannot be null.");
}
beforeLogout(subject);
PrincipalCollection principals = subject.getPrincipals(); // 獲取用戶認證資訊
if (principals != null && !principals.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Logging out subject with primary principal {}", principals.getPrimaryPrincipal());
}
Authenticator authc = getAuthenticator(); // 獲取身份認證器屬性
if (authc instanceof LogoutAware) {
((LogoutAware) authc).onLogout(principals); // 注銷用戶認證資訊
}
}
try {
delete(subject); // 洗掉這個已登錄狀態的subject
} catch (Exception e) {
...
} finally {
try {
stopSession(subject); // 禁用當前session
} catch (Exception e) {
...
}
}
}
public void onLogout(PrincipalCollection principals) {
super.onLogout(principals); // 發送注銷通知
Collection<Realm> realms = getRealms();
if (!CollectionUtils.isEmpty(realms)) {
for (Realm realm : realms) {
if (realm instanceof LogoutAware) {
((LogoutAware) realm).onLogout(principals); // 呼叫realm注銷用戶認證資訊
}
}
}
}
最后其實呼叫了自定義CertificationRealm類中的logout方法,該方法我們沒有重寫,所以呼叫的是父類CachingRealm的onLogout方法:
public void onLogout(PrincipalCollection principals) {
clearCache(principals); // 清空realm域快取中的用戶登錄認證、用戶角色、用戶權限資訊
}
到此,將有關當前登錄用戶的相關認證資訊全部清除,用戶注銷成功,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/286904.html
標籤:其他
