《Shiro 一》SpringBoot + Shiro 構(gòu)建Web 工程

Shiro 一款簡單易用,功能強大的安全框架,幫助我們安全高效的構(gòu)建企業(yè)級應(yīng)用。之前幾個項目都用到過 Shiro,最近抽空梳理了一下,分享一些經(jīng)驗。

本文demo:https://gitee.com/yintianwen7/taven-springboot-learning/tree/master/springboot-shiro
本文demo選型:thymeleaf, springboot 2, shiro, ehcache
PS:如果我不拖延的話,估計還是會有后續(xù)的 :)

目錄

Shiro 能做什么
Shiro 常用組件介紹
Shiro 是如何工作的
Shiro 如何集成
關(guān)于 thymeleaf-extras-shiro


Shiro 能做什么
  • 認證:登錄用戶的認證
  • 權(quán)限:基于角色和權(quán)限的訪問權(quán)限(url權(quán)限),以及顆?;瘷?quán)限控制(按鈕權(quán)限)
  • 加密技術(shù):Shiro的crypto包中包含了一系列的易于理解和使用的加密、哈希(aka摘要)輔助類
  • session管理:可在web容器以及 EJB容器中使用 session,可擴展 (例如我們可以通過重寫 sessionDao 將 session 存儲到數(shù)據(jù)庫中)
  • RememberMe:基于cookie的記住我服務(wù)
Shiro 常用組件介紹
image.png
  • Subject:Subject其實代表的就是當前正在執(zhí)行操作的用戶,只不過因為“User”一般指代人,但是一個“Subject”可以是人,也可以是任何的第三方系統(tǒng),服務(wù)賬號等任何其他正在和當前系統(tǒng)交互的第三方軟件系統(tǒng)。
    所有的Subject實例都被綁定到一個SecurityManager,如果你和一個Subject交互,所有的交互動作都會被轉(zhuǎn)換成Subject與SecurityManager的交互

  • SecurityManager:Shiro的核心,他主要用于協(xié)調(diào)Shiro內(nèi)部各種安全組件,不過我們一般不用太關(guān)心SecurityManager,對于應(yīng)用程序開發(fā)者來說,主要還是使用Subject的API來處理各種安全驗證邏輯

  • Realm:這是用于連接Shiro和客戶系統(tǒng)的用戶數(shù)據(jù)的橋梁。一旦Shiro真正需要訪問各種安全相關(guān)的數(shù)據(jù)(比如使用用戶賬戶來做用戶身份驗證以及權(quán)限驗證)時,他總是通過調(diào)用系統(tǒng)配置的各種Realm來讀取數(shù)據(jù)

  • 關(guān)于Shiro 的其余核心組件參考 Shiro 官網(wǎng) 或者 Shiro的架構(gòu) 本文不做過多的闡述

Shiro 是如何工作的

簡單來講的話,在Spring項目中

  1. Shiro 會將他的所有組件注冊到 SecurityManager
  2. 再通過將 SecurityManager 注冊到 ShiroFilterFactoryBean(這個類實現(xiàn)了Spring 的BeanPostProcessor會預(yù)先加載) 中,
  3. 最后以 filter 的形式注冊到Spring容器(實現(xiàn)了Spring的FactoryBean,構(gòu)造一個 filter 注冊到 Spring 容器中),實現(xiàn)用戶權(quán)限的管理。
Shiro 如何集成
  • shiro 所需依賴,完整見demo源碼
<!--shiro-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.4.0</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.0</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>1.4.0</version>
</dependency>
<!-- 基于thymeleaf的shiro擴展 -->
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>
  • ShiroConfig
@Configuration
public class ShiroConfig {

    private static final Logger log = LoggerFactory.getLogger(ShiroConfig.class);

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

        Map<String, String> chainDefinition = new LinkedHashMap<>();
        // 靜態(tài)資源與登錄請求不攔截
        chainDefinition.put("/js/**", "anon");
        chainDefinition.put("/css/**", "anon");
        chainDefinition.put("/img/**", "anon");
        chainDefinition.put("/layui/**", "anon");
        chainDefinition.put("/login", "anon");
        chainDefinition.put("/login.html", "anon");
        // 用戶為授權(quán)通過認證 && 包含'admin'角色
        chainDefinition.put("/admin/**", "authc, roles[super_admin]");
        // 用戶為授權(quán)通過認證或者RememberMe && 包含'document:read'權(quán)限
        chainDefinition.put("/docs/**", "user, perms[document:read]");
        // 用戶訪問所有請求 授權(quán)通過 || RememberMe
        chainDefinition.put("/**", "user");

        shiroFilter.setFilterChainDefinitionMap(chainDefinition);
        // 當 用戶身份失效時重定向到 loginUrl
        shiroFilter.setLoginUrl("/login.html");
        // 用戶登錄后默認重定向請求
        shiroFilter.setSuccessUrl("/index.html");
        return shiroFilter;
    }

    @Bean
    public Realm realm() {
        ShiroRealm realm = new ShiroRealm();
        realm.setCredentialsMatcher(credentialsMatcher());
        realm.setCacheManager(ehCacheManager());
        return realm;
    }

    @Bean
    public CacheManager ehCacheManager() {
        EhCacheManager cacheManager = new EhCacheManager();
        cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
        return cacheManager;
    }

    @Bean
    public CredentialsMatcher credentialsMatcher() {
        AuthCredentialsMatcher credentialsMatcher = new AuthCredentialsMatcher(ehCacheManager());
        credentialsMatcher.setHashAlgorithmName(AuthCredentialsMatcher.HASH_ALGORITHM_NAME);
        credentialsMatcher.setHashIterations(AuthCredentialsMatcher.HASH_ITERATIONS);
        credentialsMatcher.setStoredCredentialsHexEncoded(true);
        return credentialsMatcher;
    }

    @Bean
    public DefaultWebSecurityManager securityManager() {
        log.debug("--------------shiro已經(jīng)加載----------------");
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setCacheManager(ehCacheManager());
        manager.setRealm(realm());
        manager.setRememberMeManager(rememberMeManager());
        return manager;
    }

    @Bean
    public RememberMeManager rememberMeManager() {
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        //rememberMe cookie加密的密鑰 建議每個項目都不一樣 默認AES算法 密鑰長度(128 256 512 位)
        cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));
        cookieRememberMeManager.setCookie(rememberMeCookie());
        return cookieRememberMeManager;
    }

    @Bean
    public SimpleCookie rememberMeCookie(){
        //這個參數(shù)是cookie的名稱,對應(yīng)前端的checkbox的name = rememberMe
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        //<!-- 記住我cookie生效時間30天 ,單位秒;-->
        simpleCookie.setMaxAge(259200);
        return simpleCookie;
    }

    /**
     * Shiro生命周期處理器:
     * 用于在實現(xiàn)了Initializable接口的Shiro bean初始化時調(diào)用Initializable接口回調(diào)(例如:UserRealm)
     * 在實現(xiàn)了Destroyable接口的Shiro bean銷毀時調(diào)用 Destroyable接口回調(diào)(例如:DefaultSecurityManager)
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 啟用shrio授權(quán)注解攔截方式,AOP式方法級權(quán)限檢查
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor =
                new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * thymeleaf的shiro擴展
     *
     * @return
     */
    @Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }

}

以上基本是Spring項目集成 Shiro 的通用配置,下面針對上述的幾個Bean 聊一聊
1. ShiroFilterFactoryBean:用于定義 請求的攔截規(guī)則, Shiro為我們默認提供了一些選項,常用如下

  • anon: 請求不攔截
  • authc: 要求用戶必須認證通過
  • user: 要求用戶為記住我狀態(tài)
  • roles[xxx]: 要求用戶必須滿足 xxx 角色
  • perms[xxx]: 要求用戶必須滿足 xxx 權(quán)限
    其實上述每一個都對應(yīng)了一個 Shiro 過濾器
Filter Name Class
anon org.apache.shiro.web.filter.authc.AnonymousFilter
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
logout org.apache.shiro.web.filter.authc.LogoutFilter
noSessionCreation org.apache.shiro.web.filter.session.NoSessionCreationFilter
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
port org.apache.shiro.web.filter.authz.PortFilter
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
ssl org.apache.shiro.web.filter.authz.SslFilter
user org.apache.shiro.web.filter.authc.UserFilter
  • 我們也可以自定義 過濾器來實現(xiàn)攔截

2. Realm:上面提到過Realm是用于連接Shiro和客戶系統(tǒng)的用戶數(shù)據(jù)的橋梁, 我們通過實現(xiàn)AuthorizingRealm 來提供用戶認證和授權(quán)兩個API

public class ShiroRealm extends AuthorizingRealm {

    private static final Logger log = LoggerFactory.getLogger(AuthorizingRealm.class);

    @Autowired
    @Lazy // 這里lazy 是有必要的, shiro組件會預(yù)先加載,導(dǎo)致依賴的bean 沒有生成代理對象(AOP失效)
    private UserService userService;

    /**
     * 認證
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username = (String) authenticationToken.getPrincipal();

        if (log.isDebugEnabled()) {
            log.debug(String.format("user:%s executing doGetAuthenticationInfo", username));
        }

        User user = userService.getUserByUsername(username);

        if (user == null) {
            throw new UnknownAccountException();
        }

        if (Constant.IS_LOCK.equals(user.getIsLock())) {
            throw new LockedAccountException();
        }

        // ShiroUser 作為實際的 principal
        ShiroUser shiroUser = new ShiroUser();
        BeanUtils.copyProperties(user, shiroUser);

        // SimpleAuthenticationInfo(Object principal, Object credentials, String realmName)
        // principal 會被封裝到 subject 中
        // shiro 默認會把我們的 credentials (也就是password) 和 token 中的作對比,所以我們可以不用做密碼校驗
        ByteSource salt = ByteSource.Util.bytes(user.getUsername());
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(shiroUser, user.getPassword(), salt, getName());

        if (log.isDebugEnabled()) {
            log.debug(String.format("user:%s executed doGetAuthenticationInfo", username));
        }

        return info;
    }

    /**
     * 授權(quán)
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        ShiroUser shiroUser = (ShiroUser) principalCollection.getPrimaryPrincipal();

        if (log.isDebugEnabled()) {
            log.debug(String.format("user:%s executing doGetAuthorizationInfo", shiroUser.getUsername()));
        }

        AuthorizationDTO authorizationDTO = userService.getRolesAndPermissions(shiroUser.getId());

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addRoles(authorizationDTO.getRoleCodeSet());
        info.addStringPermissions(authorizationDTO.getPermissionCodeSet());

        if (log.isDebugEnabled()) {
            log.debug(String.format("user:%s executed doGetAuthorizationInfo", shiroUser.getUsername()));
        }

        return info;
    }

}
  • doGetAuthenticationInfo : 認證方法,在執(zhí)行 subject.login(token);后,Shiro認證器會讀取 Realm 中的該方法獲取 AuthenticationInfo對象(認證信息),包含principal(我們存儲在shiro subject中的對象),credentials (密碼)。

  • doGetAuthorizationInfo: 授權(quán)方法,在需要校驗用戶訪問權(quán)限的時候,Shiro授權(quán)器會讀取 Realm 中的該方法獲取 AuthorizationInfo對象(授權(quán)信息)讀取DB后,可以通過 addRoles(roleCollection)addStringPermissions(permCollection) 設(shè)置當前用戶的角色和權(quán)限。Shiro 在拿到這個權(quán)限信息后,會去找緩存管理器,以當前 subject 的 principal 作為key 緩存起來。

3. CredentialsMatcher: 密碼匹配器,用于匹配 doGetAuthenticationInfo 方法返回的 credentials 和 subject.login(token);時的 token 中的 password是否一致。常用的實現(xiàn)有 SimpleCredentialsMatcher(默認是該實現(xiàn))、HashedCredentialsMatcher (該實現(xiàn)可以進行加密匹配)

4. DefaultWebSecurityManager:如上述,用于協(xié)調(diào)Shiro內(nèi)部各種安全組件,我們需要將我們擴展的bean 注冊到 SecurityManager 中

5. RememberMeManager:開啟該組件后使用記住我服務(wù), token 中 rememberMe 為 true 時,登錄成功之后會創(chuàng)建RememberMe cookie。

其余參考上文代碼注釋

關(guān)于 thymeleaf-extras-shiro

Shiro 默認支持在 jsp 中使用 shiro標簽。但是想在 thymeleaf 中使用 Shiro 標簽?zāi)兀?/p>

使用 thymeleaf-extras-shiro 完美解決 thymeleaf 顆粒化權(quán)限控制

你好, <span th:text="${principal}"></span><br>
<p shiro:hasRole="super_admin">當前角色超級管理員</p>
<button shiro:hasPermission="'sys:user:add'">添加</button>
<button shiro:hasPermission="'sys:user:update'">編輯</button>
<button shiro:hasPermission="'sys:user:lock'">凍結(jié)</button>
<div shiro:hasAllPermissions="'sys:user:add, sys:user:update, sys:user:lock'">
    <span>滿足所有權(quán)限時顯示</span>
</div>
<div shiro:hasAnyPermissions="'sys:user:add, sys:user:update, sys:user:lock'">
    <span>滿足一個權(quán)限即可顯示</span>
</div>

更多用法參考
Github 文檔:https://github.com/theborakompanioni/thymeleaf-extras-shiro

本文demo:https://gitee.com/yintianwen7/taven-springboot-learning/tree/master/springboot-shiro
如果你發(fā)現(xiàn)我的文章或者demo中存在問題,請聯(lián)系我

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容