SpringBoot整合Shiro權(quán)限控制實戰(zhàn)

Shiro是Apache下的一個開源的酒朵,強(qiáng)大且易用的Java安全框架露该,可以執(zhí)行身份驗證、授權(quán)乍惊、密碼和會話管理杜秸。相對于SpringSecurity簡單的多,也沒有SpringSecurity那么復(fù)雜污桦。因為作者使用的前后端分離開發(fā)模式亩歹,引入SpringSecurity還會給前端開發(fā)人員一定的工作量和兼容性問題。結(jié)合實際情況凡橱,最終采用Shiro作為權(quán)限控制安全框架小作。

1.shiro官方架構(gòu)圖

官方架構(gòu)圖

2. 主要功能

2.1 三個核心

(1) Subject

即“當(dāng)前操作用戶”。但是稼钩,在Shiro中顾稀,Subject這一概念并不僅僅指人,也可以是第三方進(jìn)程坝撑、后臺帳戶(Daemon Account)或其他類似事物静秆。它僅僅意味著“當(dāng)前跟軟件交互的東西”。但考慮到大多數(shù)的目的和用途巡李,你可以把它認(rèn)為是Shiro的“用戶”概念抚笔。Subject代表了當(dāng)前用戶的安全操作,而SecurityManager則管理所有用戶的安全操作侨拦。

(2) SecurityManager

它是Shiro框架的核心殊橙,典型的Facade模式,Shiro通過SecurityManager來管理內(nèi)部組件實例狱从,并通過它來提供安全管理的各種服務(wù)膨蛮。

(3) Realm

Realm充當(dāng)了Shiro與應(yīng)用安全數(shù)據(jù)間的“橋梁”或者“連接器”。也就是說季研,當(dāng)對用戶執(zhí)行認(rèn)證(登錄)和授權(quán)(訪問控制)驗證時敞葛,Shiro會從應(yīng)用配置的Realm中查找用戶及其權(quán)限信息。

從這個意義上講与涡,Realm實質(zhì)上是一個安全相關(guān)的DAO:它封裝了數(shù)據(jù)源的連接細(xì)節(jié)惹谐,并在需要時將相關(guān)數(shù)據(jù)提供給Shiro持偏。當(dāng)配置Shiro時,你必須至少指定一個Realm豺鼻,用于認(rèn)證和(或)授權(quán)综液。配置多個Realm是可以的款慨,但是至少需要一個儒飒。

Shiro內(nèi)置了可以連接大量安全數(shù)據(jù)源(又名目錄)的Realm,如LDAP檩奠、關(guān)系數(shù)據(jù)庫(JDBC)桩了、類似INI的文本配置資源以及屬性文件等。如果缺省的Realm不能滿足需求埠戳,你還可以插入代表自定義數(shù)據(jù)源的自己的Realm實現(xiàn)井誉。

2.2 相關(guān)功能類

1. Authentication:身份認(rèn)證/登錄(賬號密碼驗證)。

2. Authorization:授權(quán)整胃,即角色或者權(quán)限驗證颗圣。

3. Session Manager:會話管理,用戶登錄后的session相關(guān)管理屁使。

4. Cryptography:加密在岂,例如密碼加密等。

5. Web Support:Web支持蛮寂,集成Web環(huán)境蔽午。

6. Caching:緩存。把用戶信息酬蹋、角色及老、權(quán)限等信息緩存到如redis等緩存中。

7. Concurrency:多線程并發(fā)驗證范抓。在一個線程中開啟另一個線程骄恶,可以把權(quán)限自動傳播過去。

8.Web Integration web:系統(tǒng)集成匕垫。

9. Run As:允許一個用戶假裝為另一個用戶(如果他們允許)的身份進(jìn)行訪問僧鲁。

10. Remember Me:記住我,登錄后年缎,下次再來的話不用登錄了悔捶。

11.Interations:集成其它應(yīng)用,spring单芜、緩存框架蜕该。

3. Spring Boot整合Shiro

3.1 pom.xml中添加依賴

        <!-- shiro相關(guān)依賴 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!-- shiro+redis緩存插件 -->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>2.4.2.1-RELEASE</version>
        </dependency>

3.2 Shiro的配置類

@Configuration
public class ShiroConfig {

    /**
     * 配置Shiro核心 安全管理器 SecurityManager
     * SecurityManager安全管理器:所有與安全有關(guān)的操作都會與SecurityManager交互;且它管理著所有Subject洲鸠;負(fù)責(zé)與后邊介紹的其他組件進(jìn)行交互堂淡。(類似于SpringMVC中的DispatcherServlet控制器)
     */
    @Bean
    public SecurityManager securityManager(UserRealm userRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //將自定義的realm交給SecurityManager管理
        securityManager.setRealm(userRealm);
        // 自定義緩存實現(xiàn) 使用redis
        securityManager.setCacheManager(cacheManager());
        // 自定義session管理 使用redis
        securityManager.setSessionManager(SessionManager());
        // 使用記住我
        securityManager.setRememberMeManager(rememberMeManager());
        return securityManager;
    }

    /*
  自定義Realm
   */
    @Bean
    public UserRealm userRealm(){
        return new UserRealm();
    }

    /**
     * 配置Shiro的Web過濾器馋缅,攔截瀏覽器請求并交給SecurityManager處理
     *
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean webFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //設(shè)置securityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //配置攔截鏈 使用LinkedHashMap,因為LinkedHashMap是有序的,shiro會根據(jù)添加的順序進(jìn)行攔截
        // Map<K,V> K指的是攔截的url V值的是該url是否攔截
        Map<String, String> filterChainMap = new LinkedHashMap<String, String>(16);
        //authc:所有url都必須認(rèn)證通過才可以訪問; anon:所有url都都可以匿名訪問,先配置anon再配置authc绢淀。這里可不配置萤悴。
        //filterChainMap.put("/debug/test1", "anon");
      //  filterChainMap.put("/debug/test", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 開啟aop注解支持
     * 即在controller中使用 @RequiresPermissions("")
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor attributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        //設(shè)置安全管理器
        attributeSourceAdvisor.setSecurityManager(securityManager);
        return attributeSourceAdvisor;
    }


    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }

    /**
     * redisManager
     *
     * @return
     */
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost("127.0.0.1");
        redisManager.setPort(6379);
        // 配置過期時間 一周
        redisManager.setExpire(604800);
        return redisManager;
    }

    /**
     * cacheManager
     *
     * @return
     */
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }

    /**
     * redisSessionDAO
     */
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        return redisSessionDAO;
    }

    /**
     * sessionManager
     */
    public DefaultWebSessionManager SessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setGlobalSessionTimeout(604800000L);
        sessionManager.setSessionDAO(redisSessionDAO());
        return sessionManager;
    }

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


    /**
     * cookie管理對象;記住我功能
     * @return
     */
    public CookieRememberMeManager rememberMeManager(){
         CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
         cookieRememberMeManager.setCookie(rememberMeCookie());
        // cookieRememberMeManager.setCipherKey用來設(shè)置加密的Key,參數(shù)類型byte[], 字節(jié)數(shù)組長度要求16;     
  cookieRememberMeManager.setCipherKey(Base64.decode("3AvVhmFLUs0KTA3Kprsdag=="));
        return cookieRememberMeManager;
    }
}

3.3 Shiro的自定義Realm

**
 * 自定義Realm
 * (1)AuthenticatingRealm:shiro中的用于進(jìn)行認(rèn)證的領(lǐng)域皆的,實現(xiàn)doGetAuthentcationInfo方法實現(xiàn)用戶登錄時的認(rèn)證邏輯覆履;
 * (2)AuthorizingRealm:shiro中用于授權(quán)的領(lǐng)域,實現(xiàn)doGetAuthrozitionInfo方法實現(xiàn)用戶的授權(quán)邏輯费薄,AuthorizingRealm繼承了AuthenticatingRealm硝全,
 * 所以在實際使用中主要用到的就是這個AuthenticatingRealm類;
 * (3)AuthenticatingRealm楞抡、AuthorizingRealm這兩個類都是shiro中提供了一些線程的realm接口
 * (4)在與spring整合項目中伟众,shiro的SecurityManager會自動調(diào)用這兩個方法,從而實現(xiàn)認(rèn)證和授權(quán)召廷,可以結(jié)合shiro的CacheManager將認(rèn)證和授權(quán)信息保存在緩存中凳厢,
 * 這樣可以提高系統(tǒng)的處理效率。    
 *
 */
public class UserRealm extends AuthorizingRealm {

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private UserService userService;

    @Override
    /**
     * 認(rèn)證
     */
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //編寫shiro判斷邏輯竞慢,判斷用戶名和密碼
        //1.判斷用戶名  token中的用戶信息是登錄時候傳進(jìn)來的
        UsernamePasswordToken  usernamePasswordToken = (UsernamePasswordToken)token;
        //在自己數(shù)據(jù)庫找當(dāng)前用戶
        User user = userSerivce.findByName(usernamePasswordToken.getUsername());
        if(user == null){
            //用戶名不存在
            return null;//shiro底層會拋出UnKnowAccountException
        }
        //2.判斷密碼
        //第二個字段是user.getPassword()先紫,注意這里是指從數(shù)據(jù)庫中獲取的password。第三個字段是realm梗顺,即當(dāng)前realm的名稱泡孩。
        //這塊對比邏輯是先對比username,但是username肯定是相等的寺谤,所以真正對比的是password仑鸥。
        //從這里傳入的password(這里是從數(shù)據(jù)庫獲取的)和token(filter中登錄時生成的)中的password做對比,如果相同就允許登錄变屁,
        // 不相同就拋出IncorrectCredentialsException異常眼俊。
        //如果認(rèn)證不通過,就不會執(zhí)行下面的授權(quán)方法了
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user,user.getPassword, getName());

        //3.返回身份處理對象
        return simpleAuthenticationInfo;
    }
    @Override
    /**
     * 授權(quán)
     */
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
        //1.獲取當(dāng)前登錄的用戶
        User user = (User) principal.getPrimaryPrincipal();
        //通過SimpleAuthenticationInfo做授權(quán)
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //2.添加角色
        //這里從緩存獲取當(dāng)前用戶的角色信息粟关,并賦予simpleAuthorizationInfo
        RMapCache<String, Set<String>>  userRoleCodeCache = redissonClient.getMapCache(CacheKey.USER_ROLE);
        Set<String> roleCode = roleCache.get(user.getId());
        if (!CollectionUtils.isEmpty(roleCode)) {
            simpleAuthorizationInfo.addRoles(roleCode);
        }
        //3.添加權(quán)限
       //這里從緩存獲取當(dāng)前用戶的權(quán)限信息疮胖,并賦予simpleAuthorizationInfo
        RMapCache<String, Set<String>> userPermissionCodeCache = redissonClient.getMapCache(CacheKey.USER_PERMISSION);
        Set<String> userPermissionCode = userPermissionCodeCache.get(user.getId());
        if (!CollectionUtils.isEmpty(userFunPermissionCode)) {
            simpleAuthorizationInfo.addStringPermissions(userFunPermissionCode);
        }
        return simpleAuthorizationInfo;
    }
}

3.4 自定義Shiro異常攔截

當(dāng)Shiro拋出UnauthorizedException,表明當(dāng)前用戶沒有權(quán)限闷板。
當(dāng)Shiro拋出UnauthenticatedException澎灸,表明當(dāng)前用戶沒有被Shiro管理,也就是沒有登錄遮晚,讓用戶重新登錄性昭。
我們在這統(tǒng)一攔截異常并封裝返回異常結(jié)果。

@ControllerAdvice
public class ExceptionController extends BaseApp {
    @ResponseBody
    @ExceptionHandler(value = {UnauthorizedException.class,UnauthenticatedException.class})
    public Map<String, Object> handleClientException(HttpServletRequest req, HttpServletResponse resp, Exception e) {
        if (e instanceof UnauthorizedException) {
            resp.setStatus(HttpStatus.FORBIDDEN.value());
            return buildResponse(EBusinessCode.NOT_PERMISSION, null);
           } else if (e instanceof UnauthenticatedException) {
            resp.setStatus(HttpStatus.FORBIDDEN.value());
            return buildResponse(EBusinessCode.RE_LOGIN, null);
        } 
    }
}

3.5 測試

  //將當(dāng)前用戶交給Shiro管理县遣,這里的處理邏輯可加在原有項目的登錄邏輯里面糜颠。
   @RequestMapping(value = "/test1", method = RequestMethod.GET)
    public @ResponseBody Map<String, Object> Test1() throws BaseException {
        User user = new User();
        user.setId("123456");
        user.setName("123456");
        user.setPassword("123456");

        //1.將user交給shiro汹族。
        UsernamePasswordToken token = new UsernamePasswordToken(user.getName, user.getPassword());
        token.setRememberMe(true);
        Subject currentUser = SecurityUtils.getSubject();
        //主體提交登錄請求到SecurityManager
        currentUser.login(t);
        //2.查找用戶角色和權(quán)限,并將用戶的角色和權(quán)限放到緩存其兴。
        //添加角色
        RMapCache<String, Set<String>> roleCache = redissonClient.getMapCache(CacheKey.USER_ROLE);
        Set<String> roleCode =  new HashSet<>();
        roleCode.add("superMan");
        roleCache.put(user.getId(),roleCode)
        //添加權(quán)限
        RMapCache<String, Set<String>> userPermissionCodeCache = redissonClient.getMapCache(CacheKey.USER_FUNCTION_PERMISSION);
        Set<String> permissionCode =  new HashSet<>();
        permissionCode.add("1000");
        userPermissionCodeCache.put(user.getId(),permissionCode)
        return buildResponse();
    }

//對這個接口實行權(quán)限控制顶瞒,前提需要把當(dāng)前用戶交給Shiro管理,如果Shiro識別不到當(dāng)前用戶元旬,則會拋出UnauthenticatedException異常榴徐,讓用戶重新登錄。
//這里表明需要當(dāng)前用戶同時擁有角色為superMan和root法绵,且擁有代號為1000或1001的權(quán)限才能訪問箕速,否則拋出UnauthorizedException,表明當(dāng)前用戶沒有權(quán)限訪問該接口朋譬。
//logical = Logical.OR表示或的關(guān)系,logical = Logical.AND表示且的關(guān)系
 @RequestMapping(value = "/test", method = RequestMethod.GET)
 @RequiresRoles(value = {"superMan","root"}, logical = Logical.AND)
 @RequiresPermissions(value = {"1000", "1001"}, logical = Logical.OR) public @ResponseBody
    Map<String, Object> Test() throws Exception {
        return buildResponse();
    }

4. Shiro認(rèn)證過程

shiro認(rèn)證過程
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末兴垦,一起剝皮案震驚了整個濱河市徙赢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌探越,老刑警劉巖狡赐,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異钦幔,居然都是意外死亡枕屉,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進(jìn)店門鲤氢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來搀擂,“玉大人,你說我怎么就攤上這事卷玉∩谒蹋” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵相种,是天一觀的道長威恼。 經(jīng)常有香客問我,道長寝并,這世上最難降的妖魔是什么箫措? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮衬潦,結(jié)果婚禮上斤蔓,老公的妹妹穿的比我還像新娘。我一直安慰自己别渔,他們只是感情好附迷,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布惧互。 她就那樣靜靜地躺著,像睡著了一般喇伯。 火紅的嫁衣襯著肌膚如雪喊儡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天稻据,我揣著相機(jī)與錄音艾猜,去河邊找鬼。 笑死捻悯,一個胖子當(dāng)著我的面吹牛匆赃,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播今缚,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼算柳,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了姓言?” 一聲冷哼從身側(cè)響起瞬项,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎何荚,沒想到半個月后囱淋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡餐塘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年妥衣,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片戒傻。...
    茶點(diǎn)故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡税手,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出稠鼻,到底是詐尸還是另有隱情冈止,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布候齿,位于F島的核電站熙暴,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏慌盯。R本人自食惡果不足惜周霉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望亚皂。 院中可真熱鬧俱箱,春花似錦、人聲如沸灭必。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至跟衅,卻和暖如春孵睬,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背伶跷。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工掰读, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人叭莫。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓蹈集,卻偏偏與公主長得像,于是被迫代替她去往敵國和親雇初。 傳聞我的和親對象是個殘疾皇子拢肆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評論 2 354