主頁 >  其他 > SpringBoot整合Shiro框架 && Shiro框架核心原始碼全面決議

SpringBoot整合Shiro框架 && Shiro框架核心原始碼全面決議

2021-06-13 06:50:55 其他

本文主要展示內容有二:

  1. SpringBoot專案如何整合Shiro框架,從前端請求用戶登錄,到后端判斷用戶、角色、權限認證,再到登錄后請求獲取用戶資訊,最后用戶注銷,
  2. 基于以上整合代碼的示例,針對其中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框架過濾器的實體命名:

  1. anon:不認證也可以訪問,即該過濾器實體中設定的請求路徑,不需要進行用戶認證即可訪問,
  2. 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

標籤:其他

上一篇:create-react-app專案所遇問題總結之跨域

下一篇:【Java原理探索】帶你探究String類不可變的特性 | Java開發實戰

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more