溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

SpringBoot整合Shiro安全框架

發(fā)布時(shí)間:2020-06-07 18:15:24 來源:網(wǎng)絡(luò) 閱讀:6327 作者:Java_老男孩 欄目:編程語(yǔ)言

SpringBoot?是為了簡(jiǎn)化?Spring?應(yīng)用的創(chuàng)建、運(yùn)行、調(diào)試、部署等一系列問題而誕生的產(chǎn)物,自動(dòng)裝配的特性讓我們可以更好的關(guān)注業(yè)務(wù)本身而不是外部的XML配置,我們只需遵循規(guī)范,引入相關(guān)的依賴就可以輕易的搭建出一個(gè) WEB 工程

Shiro 是 Apache 旗下開源的一款強(qiáng)大且易用的Java安全框架,身份驗(yàn)證、授權(quán)、加密、會(huì)話管理。?相比?Spring Security?而言?Shiro更加輕量級(jí),且 API 更易于理解…

Shiro

Shiro?主要分為?安全認(rèn)證?和?接口授權(quán)?兩個(gè)部分,其中的核心組件為?Subject、SecurityManager、Realms,公共部分?Shiro?都已經(jīng)為我們封裝好了,我們只需要按照一定的規(guī)則去編寫響應(yīng)的代碼即可…

  • Subject?即表示主體,將用戶的概念理解為當(dāng)前操作的主體,因?yàn)樗纯梢允且粋€(gè)通過瀏覽器請(qǐng)求的用戶,也可能是一個(gè)運(yùn)行的程序,外部應(yīng)用與 Subject 進(jìn)行交互,記錄當(dāng)前操作用戶。Subject 代表了當(dāng)前用戶的安全操作,SecurityManager 則管理所有用戶的安全操作。
  • SecurityManager?即安全管理器,對(duì)所有的 Subject 進(jìn)行安全管理,并通過它來提供安全管理的各種服務(wù)(認(rèn)證、授權(quán)等)
  • Realm?充當(dāng)了應(yīng)用與數(shù)據(jù)安全間的?橋梁?或?連接器。當(dāng)對(duì)用戶執(zhí)行認(rèn)證(登錄)和授權(quán)(訪問控制)驗(yàn)證時(shí),Shiro 會(huì)從應(yīng)用配置的 Realm 中查找用戶及其權(quán)限信息。

本章目標(biāo)

利用?Spring Boot?與?Shiro?實(shí)現(xiàn)安全認(rèn)證和授權(quán)….

導(dǎo)入依賴

依賴?spring-boot-starter-web

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
    <shiro.version>1.4.0</shiro.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- shiro 相關(guān)包 -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>${shiro.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>${shiro.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-ehcache</artifactId>
        <version>${shiro.version}</version>
    </dependency>
    <!-- End  -->
</dependencies>

屬性配置

緩存配置

Shiro 為我們提供了?CacheManager?即緩存管理,將用戶權(quán)限數(shù)據(jù)存儲(chǔ)在緩存,可以提高它的性能。支持?EhCache、Redis?等常規(guī)緩存,這里為了簡(jiǎn)單起見就用?EhCache?了 , 在resources?目錄下創(chuàng)建一個(gè)?ehcache-shiro.xml?文件

<?xml version="1.0" encoding="UTF-8"?>
<ehcache updateCheck="false" name="shiroCache">
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            overflowToDisk="false"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
    />
</ehcache>

實(shí)體類

創(chuàng)建一個(gè)?User.java?,標(biāo)記為數(shù)據(jù)庫(kù)用戶

package com.battcn.entity;

/**
 * @author Levin
 * @since 2018/6/28 0028
 */
public class User {
    /** 自增ID */
    private Long id;
    /** 賬號(hào) */
    private String username;
    /** 密碼 */
    private String password;
    /** 角色名:Shiro 支持多個(gè)角色,而且接收參數(shù)也是 Set<String> 集合,但這里為了簡(jiǎn)單起見定義成 String 類型了 */
    private String roleName;
    /** 是否禁用 */
    private boolean locked;
    // 省略 GET SET 構(gòu)造函數(shù)...
}

偽造數(shù)據(jù)

支持?rolespermissions,比如你一個(gè)接口可以允許用戶擁有某一個(gè)角色,也可以是擁有某一個(gè)?permission?…

package com.battcn.config;

import com.battcn.entity.User;

import java.util.*;

/**
 * 主要不想連接數(shù)據(jù)庫(kù)..
 *
 * @author Levin
 * @since 2018/6/28 0028
 */
public class DBCache {

    /**
     * K 用戶名
     * V 用戶信息
     */
    public static final Map<String, User> USERS_CACHE = new HashMap<>();
    /**
     * K 角色I(xiàn)D
     * V 權(quán)限編碼
     */
    public static final Map<String, Collection<String>> PERMISSIONS_CACHE = new HashMap<>();

    static {
        // TODO 假設(shè)這是數(shù)據(jù)庫(kù)記錄
        USERS_CACHE.put("u1", new User(1L, "u1", "p1", "admin", true));
        USERS_CACHE.put("u2", new User(2L, "u2", "p2", "admin", false));
        USERS_CACHE.put("u3", new User(3L, "u3", "p3", "test", true));

        PERMISSIONS_CACHE.put("admin", Arrays.asList("user:list", "user:add", "user:edit"));
        PERMISSIONS_CACHE.put("test", Collections.singletonList("user:list"));

    }
}

ShiroConfiguration

Shiro 的主要配置信息都在此文件內(nèi)實(shí)現(xiàn);

package com.battcn.config;

import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Shiro 配置
 *
 * @author Levin
 */
@Configuration
public class ShiroConfiguration {

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

    @Bean
    public EhCacheManager getEhCacheManager() {
        EhCacheManager em = new EhCacheManager();
        em.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
        return em;
    }

    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 加密器:這樣一來數(shù)據(jù)庫(kù)就可以是密文存儲(chǔ),為了演示我就不開啟了
     *
     * @return HashedCredentialsMatcher
     */
//    @Bean
//    public HashedCredentialsMatcher hashedCredentialsMatcher() {
//        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//        //散列算法:這里使用MD5算法;
//        hashedCredentialsMatcher.setHashAlgorithmName("md5");
//        //散列的次數(shù),比如散列兩次,相當(dāng)于 md5(md5(""));
//        hashedCredentialsMatcher.setHashIterations(2);
//        return hashedCredentialsMatcher;
//    }

    @Bean
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        autoProxyCreator.setProxyTargetClass(true);
        return autoProxyCreator;
    }

    @Bean(name = "authRealm")
    public AuthRealm authRealm(EhCacheManager cacheManager) {
        AuthRealm authRealm = new AuthRealm();
        authRealm.setCacheManager(cacheManager);
        return authRealm;
    }

    @Bean(name = "securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(AuthRealm authRealm) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(authRealm);
        // <!-- 用戶授權(quán)/認(rèn)證信息Cache, 采用EhCache 緩存 -->
        defaultWebSecurityManager.setCacheManager(getEhCacheManager());
        return defaultWebSecurityManager;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(
            DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    /**
     * ShiroFilter<br/>
     * 注意這里參數(shù)中的 StudentService 和 IScoreDao 只是一個(gè)例子,因?yàn)槲覀冊(cè)谶@里可以用這樣的方式獲取到相關(guān)訪問數(shù)據(jù)庫(kù)的對(duì)象,
     * 然后讀取數(shù)據(jù)庫(kù)相關(guān)配置,配置到 shiroFilterFactoryBean 的訪問規(guī)則中。實(shí)際項(xiàng)目中,請(qǐng)使用自己的Service來處理業(yè)務(wù)邏輯。
     *
     * @param securityManager 安全管理器
     * @return ShiroFilterFactoryBean
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 必須設(shè)置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 如果不設(shè)置默認(rèn)會(huì)自動(dòng)尋找Web工程根目錄下的"/login"頁(yè)面
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 登錄成功后要跳轉(zhuǎn)的連接
        shiroFilterFactoryBean.setSuccessUrl("/index");
        shiroFilterFactoryBean.setUnauthorizedUrl("/denied");
        loadShiroFilterChain(shiroFilterFactoryBean);
        return shiroFilterFactoryBean;
    }

    /**
     * 加載shiroFilter權(quán)限控制規(guī)則(從數(shù)據(jù)庫(kù)讀取然后配置)
     */
    private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean) {
        /////////////////////// 下面這些規(guī)則配置最好配置到配置文件中 ///////////////////////
        // TODO 重中之重啊,過濾順序一定要根據(jù)自己需要排序
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 需要驗(yàn)證的寫 authc 不需要的寫 anon
        filterChainDefinitionMap.put("/resource/**", "anon");
        filterChainDefinitionMap.put("/install", "anon");
        filterChainDefinitionMap.put("/hello", "anon");
        // anon:它對(duì)應(yīng)的過濾器里面是空的,什么都沒做
        log.info("##################從數(shù)據(jù)庫(kù)讀取權(quán)限規(guī)則,加載到shiroFilter中##################");

        // 不用注解也可以通過 API 方式加載權(quán)限規(guī)則
        Map<String, String> permissions = new LinkedHashMap<>();
        permissions.put("/users/find", "perms[user:find]");
        filterChainDefinitionMap.putAll(permissions);
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    }
}

AuthRealm

上面介紹過?Realm?,安全認(rèn)證和權(quán)限驗(yàn)證的核心處理就是重寫?AuthorizingRealm?中的?doGetAuthenticationInfo(登錄認(rèn)證)?與?doGetAuthorizationInfo(權(quán)限驗(yàn)證)

package com.battcn.config;

import com.battcn.entity.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.context.annotation.Configuration;

import java.util.*;

/**
 * 認(rèn)證領(lǐng)域
 *
 * @author Levin
 * @version 2.5.1
 * @since 2018-01-10
 */
@Configuration
public class AuthRealm extends AuthorizingRealm {

    /**
     * 認(rèn)證回調(diào)函數(shù),登錄時(shí)調(diào)用
     * 首先根據(jù)傳入的用戶名獲取User信息;然后如果user為空,那么拋出沒找到帳號(hào)異常UnknownAccountException;
     * 如果user找到但鎖定了拋出鎖定異常LockedAccountException;最后生成AuthenticationInfo信息,
     * 交給間接父類AuthenticatingRealm使用CredentialsMatcher進(jìn)行判斷密碼是否匹配,
     * 如果不匹配將拋出密碼錯(cuò)誤異常IncorrectCredentialsException;
     * 另外如果密碼重試此處太多將拋出超出重試次數(shù)異常ExcessiveAttemptsException;
     * 在組裝SimpleAuthenticationInfo信息時(shí), 需要傳入:身份信息(用戶名)、憑據(jù)(密文密碼)、鹽(username+salt),
     * CredentialsMatcher使用鹽加密傳入的明文密碼和此處的密文密碼進(jìn)行匹配。
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {
        String principal = (String) token.getPrincipal();
        User user = Optional.ofNullable(DBCache.USERS_CACHE.get(principal)).orElseThrow(UnknownAccountException::new);
        if (!user.isLocked()) {
            throw new LockedAccountException();
        }
        // 從數(shù)據(jù)庫(kù)查詢出來的賬號(hào)名和密碼,與用戶輸入的賬號(hào)和密碼對(duì)比
        // 當(dāng)用戶執(zhí)行登錄時(shí),在方法處理上要實(shí)現(xiàn) user.login(token)
        // 然后會(huì)自動(dòng)進(jìn)入這個(gè)類進(jìn)行認(rèn)證
        // 交給 AuthenticatingRealm 使用 CredentialsMatcher 進(jìn)行密碼匹配,如果覺得人家的不好可以自定義實(shí)現(xiàn)
        // TODO 如果使用 HashedCredentialsMatcher 這里認(rèn)證方式就要改一下 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(principal, "密碼", ByteSource.Util.bytes("密碼鹽"), getName());
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(principal, user.getPassword(), getName());
        Session session = SecurityUtils.getSubject().getSession();
        session.setAttribute("USER_SESSION", user);
        return authenticationInfo;
    }

    /**
     * 只有需要驗(yàn)證權(quán)限時(shí)才會(huì)調(diào)用, 授權(quán)查詢回調(diào)函數(shù), 進(jìn)行鑒權(quán)但緩存中無用戶的授權(quán)信息時(shí)調(diào)用.在配有緩存的情況下,只加載一次.
     * 如果需要?jiǎng)討B(tài)權(quán)限,但是又不想每次去數(shù)據(jù)庫(kù)校驗(yàn),可以存在ehcache中.自行完善
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
        Session session = SecurityUtils.getSubject().getSession();
        User user = (User) session.getAttribute("USER_SESSION");
        // 權(quán)限信息對(duì)象info,用來存放查出的用戶的所有的角色(role)及權(quán)限(permission)
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        // 用戶的角色集合
        Set<String> roles = new HashSet<>();
        roles.add(user.getRoleName());
        info.setRoles(roles);
        // 用戶的角色對(duì)應(yīng)的所有權(quán)限,如果只使用角色定義訪問權(quán)限,下面可以不要
        // 只有角色并沒有顆粒度到每一個(gè)按鈕 或 是操作選項(xiàng)  PERMISSIONS 是可選項(xiàng)
        final Map<String, Collection<String>> permissionsCache = DBCache.PERMISSIONS_CACHE;
        final Collection<String> permissions = permissionsCache.get(user.getRoleName());
        info.addStringPermissions(permissions);
        return info;
    }
}

控制器

在?ShiroConfiguration?中的?shiroFilter?處配置了?/hello = anon,意味著可以不需要認(rèn)證也可以訪問,那么除了這種方式外?Shiro還為我們提供了一些注解相關(guān)的方式…

常用注解

  • @RequiresGuest?代表無需認(rèn)證即可訪問,同理的就是?/path = anon
  • @RequiresAuthentication?需要認(rèn)證,只要登錄成功后就允許你操作
  • @RequiresPermissions?需要特定的權(quán)限,沒有則拋出AuthorizationException
  • @RequiresRoles?需要特定的橘色,沒有則拋出AuthorizationException
  • @RequiresUser?不太清楚,不常用…

LoginController

package com.battcn.controller;

import com.battcn.config.ShiroConfiguration;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

/**
 * @author Levin
 * @since 2018/6/28 0028
 */
@RestController
public class LoginController {

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

    @GetMapping(value = "/hello")
    public String hello() {
        log.info("不登錄也可以訪問...");
        return "hello...";
    }

    @GetMapping(value = "/index")
    public String index() {
        log.info("登陸成功了...");
        return "index";
    }

    @GetMapping(value = "/denied")
    public String denied() {
        log.info("小伙子權(quán)限不足,別無謂掙扎了...");
        return "denied...";
    }

    @GetMapping(value = "/login")
    public String login(String username, String password, RedirectAttributes model) {
        // 想要得到 SecurityUtils.getSubject() 的對(duì)象..訪問地址必須跟 shiro 的攔截地址內(nèi).不然后會(huì)報(bào)空指針
        Subject sub = SecurityUtils.getSubject();
        // 用戶輸入的賬號(hào)和密碼,,存到UsernamePasswordToken對(duì)象中..然后由shiro內(nèi)部認(rèn)證對(duì)比,
        // 認(rèn)證執(zhí)行者交由 com.battcn.config.AuthRealm 中 doGetAuthenticationInfo 處理
        // 當(dāng)以上認(rèn)證成功后會(huì)向下執(zhí)行,認(rèn)證失敗會(huì)拋出異常
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try {
            sub.login(token);
        } catch (UnknownAccountException e) {
            log.error("對(duì)用戶[{}]進(jìn)行登錄驗(yàn)證,驗(yàn)證未通過,用戶不存在", username);
            token.clear();
            return "UnknownAccountException";
        } catch (LockedAccountException lae) {
            log.error("對(duì)用戶[{}]進(jìn)行登錄驗(yàn)證,驗(yàn)證未通過,賬戶已鎖定", username);
            token.clear();
            return "LockedAccountException";
        } catch (ExcessiveAttemptsException e) {
            log.error("對(duì)用戶[{}]進(jìn)行登錄驗(yàn)證,驗(yàn)證未通過,錯(cuò)誤次數(shù)過多", username);
            token.clear();
            return "ExcessiveAttemptsException";
        } catch (AuthenticationException e) {
            log.error("對(duì)用戶[{}]進(jìn)行登錄驗(yàn)證,驗(yàn)證未通過,堆棧軌跡如下", username, e);
            token.clear();
            return "AuthenticationException";
        }
        return "success";
    }
}

UserController

package com.battcn.controller;

import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Levin
 * @since 2018/6/28 0028
 */
@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping
    public String get() {
        return "get.....";
    }

    /**
     * RequiresRoles 是所需角色 包含 AND 和 OR 兩種
     * RequiresPermissions 是所需權(quán)限 包含 AND 和 OR 兩種
     *
     * @return msg
     */
    @RequiresRoles(value = {"admin", "test"}, logical = Logical.OR)
    //@RequiresPermissions(value = {"user:list", "user:query"}, logical = Logical.OR)
    @GetMapping("/query")
    public String query() {
        return "query.....";
    }

    @GetMapping("/find")
    public String find() {
        return "find.....";
    }
}

主函數(shù)

package com.battcn;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author Levin
 */
@SpringBootApplication
public class Chapter25Application {

    public static void main(String[] args) {

        SpringApplication.run(Chapter25Application.class, args);

    }
}

測(cè)試

啟動(dòng)?Chapter25Application.java?中的?main?方法,為了更好的演示效果這里打開了?postman?做的測(cè)試,只演示其中一個(gè)流程,剩下的可以自己復(fù)制代碼測(cè)試…

先登錄,由于?u3?在?DBCache?中擁有的角色是?test,只有?user:list?這一個(gè)權(quán)限

SpringBoot整合Shiro安全框架
登陸

訪問?/users/query?成功,因?yàn)槲覀兎享憫?yīng)的角色/權(quán)限

SpringBoot整合Shiro安全框架
訪問Queryji

訪問?/users/find?失敗,并重定向到了?/denied?接口,問題來了為什么?/users/find?沒有寫注解也權(quán)限不足呢?

SpringBoot整合Shiro安全框架
權(quán)限不足

細(xì)心的朋友肯定會(huì)發(fā)現(xiàn)?在 ShiroConfiguration 中寫了一句 permissions.put(“/users/find”, “perms[user:find]”);?意味著我們不僅可以通過注解方式,同樣可以通過初始化時(shí)加載數(shù)據(jù)庫(kù)中的權(quán)限樹做控制,看各位喜好了….

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI