1. 程式人生 > 程式設計 >SpringBoot中整合Shiro實現許可權管理的示例程式碼

SpringBoot中整合Shiro實現許可權管理的示例程式碼

之前在 SSM 專案中使用過 shiro,發現 shiro 的許可權管理做的真不錯,但是在 SSM 專案中的配置太繁雜了,於是這次在 SpringBoot 中使用了 shiro,下面一起看看吧

一、簡介

Apache Shiro是一個強大且易用的Java安全框架,執行身份驗證、授權、密碼和會話管理。使用Shiro的易於理解的API,您可以快速、輕鬆地獲得任何應用程式,從最小的移動應用程式到最大的網路和企業應用程式。

三個核心元件:

1、Subject

即“當前操作使用者”。但是,在 Shiro 中,Subject 這一概念並不僅僅指人,也可以是第三方程序、後臺帳戶(Daemon Account)或其他類似事物。它僅僅意味著“當前跟軟體互動的東西”。Subject 代表了當前使用者的安全操作,SecurityManager 則管理所有使用者的安全操作。

2、SecurityManager

它是Shiro 框架的核心,典型的 Facade 模式,Shiro 通過 SecurityManager 來管理內部元件例項,並通過它來提供安全管理的各種服務。

3、Realm

Realm 充當了 Shiro 與應用安全資料間的“橋樑”或者“聯結器”。也就是說,當對使用者執行認證(登入)和授權(訪問控制)驗證時,Shiro 會從應用配置的 Realm 中查詢使用者及其許可權資訊。從這個意義上講,Realm 實質上是一個安全相關的 DAO:它封裝了資料來源的連線細節,並在需要時將相關資料提供給 Shiro。當配置 Shiro 時,你必須至少指定一個 Realm,用於認證和(或)授權。配置多個 Realm 是可以的,但是至少需要一個。Shiro 內建了可以連線大量安全資料來源(又名目錄)的 Realm,如 LDAP、關係資料庫(JDBC)、類似 INI 的文字配置資源以及屬性檔案等。如果預設的 Realm 不能滿足需求,你還可以插入代表自定義資料來源的自己的 Realm 實現。

二、整合 shiro

1、引入 maven 依賴

<!-- web支援 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- thymeleaf 模板引擎 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Shiro 許可權管理 -->
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-spring</artifactId>
  <version>1.2.4</version>
</dependency>
<!-- 為了能夠在 html 中使用 shiro 的標籤引入 -->
<dependency>
  <groupId>com.github.theborakompanioni</groupId>
  <artifactId>thymeleaf-extras-shiro</artifactId>
  <version>2.0.0</version>
</dependency>

我使用的 SpringBoot 版本是 2.3.1,其它依賴自己看著引入吧

2、建立 shiro 配置檔案

關於 shiro 的配置資訊,我們都放在 ShiroConfig.java 檔案中

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
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.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

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

/**
 * shiro配置類
 */
@Configuration
public class ShiroConfig {

  /**
   * 注入這個是是為了在thymeleaf中使用shiro的自定義tag。
   */
  @Bean(name = "shiroDialect")
  public ShiroDialect shiroDialect() {
    return new ShiroDialect();
  }

  /**
   * 地址過濾器
   * @param securityManager
   * @return
   */
  @Bean
  public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    // 設定securityManager
    shiroFilterFactoryBean.setSecurityManager(securityManager);
    // 設定登入url
    shiroFilterFactoryBean.setLoginUrl("/login");
    // 設定主頁url
    shiroFilterFactoryBean.setSuccessUrl("/");
    // 設定未授權的url
    shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
    Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
    // 開放登入介面
    filterChainDefinitionMap.put("/doLogin","anon");
    // 開放靜態資原始檔
    filterChainDefinitionMap.put("/css/**","anon");
    filterChainDefinitionMap.put("/img/**","anon");
    filterChainDefinitionMap.put("/js/**","anon");
    filterChainDefinitionMap.put("/layui/**","anon");
    // 其餘url全部攔截,必須放在最後
    filterChainDefinitionMap.put("/**","authc");
    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    return shiroFilterFactoryBean;
  }

 /**
 * 自定義安全管理策略
 */
  @Bean
  public SecurityManager securityManager() {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    /**
     設定自定義的relam
     */
    securityManager.setRealm(loginRelam());
    return securityManager;
  }

 /**
 * 登入驗證
 */
  @Bean
  public LoginRelam loginRelam() {
    return new LoginRelam();
  }

  /**
   * 以下是為了能夠使用@RequiresPermission()等標籤
   */
  @Bean
  @DependsOn({"lifecycleBeanPostProcessor"})
  public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
    DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
    advisorAutoProxyCreator.setProxyTargetClass(true);
    return advisorAutoProxyCreator;
  }

  @Bean
  public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
    return new LifecycleBeanPostProcessor();
  }

  @Bean
  public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
    AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
    authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
    return authorizationAttributeSourceAdvisor;
  }
}

上面開放靜態資原始檔,其它部落格說的是 **filterChainDefinitionMap.put("/static/**","anon");**,但我發現,我們在 html 檔案中引入靜態檔案時,請求路徑根本沒有經過 static,thymeleaf 自動預設配置 **static/** 下面就是靜態資原始檔,所以,我們開放靜態資原始檔需要指定響應的目錄路徑

2、登入驗證管理

關於登入驗證的一些邏輯,以及賦權等操作,我們都放在 LoginRelam.java 檔案中

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.zyxx.sbm.entity.UserInfo;
import com.zyxx.sbm.service.RolePermissionService;
import com.zyxx.sbm.service.UserInfoService;
import com.zyxx.sbm.service.UserRoleService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Set;

/**
 * 登入授權
 */
public class LoginRelam extends AuthorizingRealm {

  @Autowired
  private UserInfoService userInfoService;
  @Autowired
  private UserRoleService userRoleService;
  @Autowired
  private RolePermissionService rolePermissionService;

  /**
   * 身份認證
   *
   * @param authenticationToken
   * @return
   * @throws AuthenticationException
   */
  @Override
  protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    // 獲取基於使用者名稱和密碼的令牌:實際上這個authcToken是從LoginController裡面currentUser.login(token)傳過來的
    UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;

    //根據使用者名稱查詢到使用者資訊
    QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("account",token.getUsername());
    UserInfo userInfo = userInfoService.getOne(queryWrapper);

    // 沒找到帳號
    if (null == userInfo) {
      throw new UnknownAccountException();
    }

    // 校驗使用者狀態
    if ("1".equals(userInfo.getStatus())) {
      throw new DisabledAccountException();
    }

    // 認證快取資訊
    return new SimpleAuthenticationInfo(userInfo,userInfo.getPassword(),ByteSource.Util.bytes(userInfo.getAccount()),getName());
  }

  /**
   * 角色授權
   *
   * @param principalCollection
   * @return
   */
  @Override
  protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    UserInfo authorizingUser = (UserInfo) principalCollection.getPrimaryPrincipal();
    if (null != authorizingUser) {
      //許可權資訊物件info,用來存放查出的使用者的所有的角色(role)及許可權(permission)
      SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

      //獲得使用者角色列表
      Set<String> roleSigns = userRoleService.listUserRoleByUserId(authorizingUser.getId());
      simpleAuthorizationInfo.addRoles(roleSigns);

      //獲得許可權列表
      Set<String> permissionSigns = rolePermissionService.listRolePermissionByUserId(authorizingUser.getId());
      simpleAuthorizationInfo.addStringPermissions(permissionSigns);
      return simpleAuthorizationInfo;
    }
    return null;
  }

  /**
   * 自定義加密規則
   *
   * @param credentialsMatcher
   */
  @Override
  public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
    // 自定義認證加密方式
    CustomCredentialsMatcher customCredentialsMatcher = new CustomCredentialsMatcher();
    // 設定自定義認證加密方式
    super.setCredentialsMatcher(customCredentialsMatcher);
  }
}

以上就是登入時,需要指明 shiro 對使用者的一些驗證、授權等操作,還有自定義密碼驗證規則,在第3步會講到,獲取角色列表,許可權列表,需要獲取到角色與許可權的標識,每一個角色,每一個許可權都有唯一的標識,裝入 Set 中

3、自定義密碼驗證規則

密碼的驗證規則,我們放在了 CustomCredentialsMatcher.java 檔案中

import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
import org.apache.shiro.crypto.hash.SimpleHash;

/**
 * @ClassName CustomCredentialsMatcher
 * 自定義密碼加密規則
 * @Author Lizhou
 * @Date 2020-07-10 16:24:24
 **/
public class CustomCredentialsMatcher extends SimpleCredentialsMatcher {

  @Override
  public boolean doCredentialsMatch(AuthenticationToken authcToken,AuthenticationInfo info) {
    UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
    //加密型別,密碼,鹽值,迭代次數
    Object tokenCredentials = new SimpleHash("md5",token.getPassword(),token.getUsername(),6).toHex();
    // 資料庫儲存密碼
    Object accountCredentials = getCredentials(info);
    // 將密碼加密與系統加密後的密碼校驗,內容一致就返回true,不一致就返回false
    return equals(tokenCredentials,accountCredentials);
  }
}

我們採用的密碼加密方式為 MD5 加密,加密 6 次,使用登入賬戶作為加密密碼的鹽進行加密

4、密碼加密工具

上面我們自定義了密碼加密規則,我們建立一個密碼加密的工具類 PasswordUtils.java 檔案

import org.apache.shiro.crypto.hash.Md5Hash;


/**
 * 密碼加密的處理工具類
 */
public class PasswordUtils {

  /**
   * 迭代次數
   */
  private static final int ITERATIONS = 6;

  private PasswordUtils() {
    throw new AssertionError();
  }

  /**
   * 字串加密函式MD5實現
   *
   * @param password 密碼
   * @param loginName 使用者名稱
   * @return
   */
  public static String getPassword(String password,String loginName) {
    return new Md5Hash(password,loginName,ITERATIONS).toString();
  }
}

三、開始登入

上面,我們已經配置了 shiro 的一系列操作,從登入驗證、密碼驗證規則、使用者授權等等,下面我們就開始登入,登入的操作,放在了 LoginController.java 檔案中

import com.zyxx.common.consts.SystemConst;
import com.zyxx.common.enums.StatusEnums;
import com.zyxx.common.kaptcha.KaptchaUtil;
import com.zyxx.common.shiro.SingletonLoginUtils;
import com.zyxx.common.utils.PasswordUtils;
import com.zyxx.common.utils.ResponseResult;
import com.zyxx.sbm.entity.UserInfo;
import com.zyxx.sbm.service.PermissionInfoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @ClassName LoginController
 * @Description
 * @Author Lizhou
 * @Date 2020-07-02 10:54:54
 **/
@Api(tags = "後臺管理端--登入")
@Controller
public class LoginController {

  @Autowired
  private PermissionInfoService permissionInfoService;

  @ApiOperation(value = "請求登入頁面",notes = "請求登入頁面")
  @GetMapping("login")
  public String init() {
    return "login";
  }

  @ApiOperation(value = "請求主頁面",notes = "請求主頁面")
  @GetMapping("/")
  public String index() {
    return "index";
  }

  @ApiOperation(value = "登入驗證",notes = "登入驗證")
  @ApiImplicitParams({
      @ApiImplicitParam(name = "account",value = "賬號",required = true),@ApiImplicitParam(name = "password",value = "密碼",@ApiImplicitParam(name = "resCode",value = "驗證碼",@ApiImplicitParam(name = "rememberMe",value = "記住登入",required = true)
  })
  @PostMapping("doLogin")
  @ResponseBody
  public ResponseResult doLogin(String account,String password,String resCode,Boolean rememberMe,HttpServletRequest request,HttpServletResponse response) throws Exception {
    // 驗證碼
    if (!KaptchaUtil.validate(resCode,request)) {
      return ResponseResult.getInstance().error(StatusEnums.KAPTCH_ERROR);
    }
    // 驗證帳號和密碼
    Subject subject = SecurityUtils.getSubject();
    UsernamePasswordToken token = new UsernamePasswordToken(account,password);
    // 記住登入狀態
    token.setRememberMe(rememberMe);
    try {
      // 執行登入
      subject.login(token);
      // 將使用者儲存到session中
  UserInfo userInfo = (UserInfo) subject.getPrincipal();
      request.getSession().setAttribute(SystemConst.SYSTEM_USER_SESSION,userInfo);
      return ResponseResult.getInstance().success();
    } catch (UnknownAccountException e) {
      return ResponseResult.getInstance().error("賬戶不存在");
    } catch (DisabledAccountException e) {
      return ResponseResult.getInstance().error("賬戶已被凍結");
    } catch (IncorrectCredentialsException e) {
      return ResponseResult.getInstance().error("密碼不正確");
    } catch (ExcessiveAttemptsException e) {
      return ResponseResult.getInstance().error("密碼連續輸入錯誤超過5次,鎖定半小時");
    } catch (RuntimeException e) {
      return ResponseResult.getInstance().error("未知錯誤");
    }
  }

  @ApiOperation(value = "登入成功,跳轉主頁面",notes = "登入成功,跳轉主頁面")
  @PostMapping("success")
  public String success() {
    return "redirect:/";
  }

  @ApiOperation(value = "初始化選單資料",notes = "初始化選單資料")
  @GetMapping("initMenu")
  @ResponseBody
  public String initMenu() {
    return permissionInfoService.initMenu();
  }

  @ApiOperation(value = "退出登入",notes = "退出登入")
  @GetMapping(value = "loginOut")
  public String logout() {
    Subject subject = SecurityUtils.getSubject();
    subject.logout();
    return "login";
  }
}

當執行 subject.login(token); 時,就會進入我們在 第二步中第二條登入驗證中,對使用者密碼、狀態進行檢查,對使用者授權等操作,登入的密碼,一定是通過密碼加密工具得到的,不然驗證不通過

四、頁面許可權控制

我們本次使用的是 thymeleaf 模板引擎,我們需要在 html 檔案中加入以下內容

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">

引入了 thymeleaf 的依賴,以及 shiro 的依賴,這樣我們就能在 html 檔案中使用 thymeleaf、shiro 的標籤了

例如:

1、判斷當前使用者有無此許可權,通過許可權標識

<button class="layui-btn" shiro:hasPermission="user_info_add"><i class="layui-icon">&#xe654;</i> 新增 </button> 

2、與上面相反,判斷當前使用者無此許可權,通過許可權標識,沒有時驗證通過

<button class="layui-btn" shiro:lacksPermission="user_info_add"><i class="layui-icon">&#xe654;</i> 新增 </button> 

3、判斷當前使用者有無以下全部許可權,通過許可權標識

<button class="layui-btn" shiro:hasAllPermissions="user_info_add"><i class="layui-icon">&#xe654;</i> 新增 </button>

4、判斷當前使用者有無以下任一許可權,通過許可權標識

<button class="layui-btn" shiro:hasAnyPermissions="user_info_add"><i class="layui-icon">&#xe654;</i> 新增 </button>

5、判斷當前使用者有無此角色,通過角色標識

<a shiro:hasRole="admin" href="admin.html" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >Administer the system</a>

6、與上面相反,判斷當前使用者無此角色,通過角色標識,沒有時驗證通過

<a shiro:lacksRole="admin" href="admin.html" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >Administer the system</a>

7、判斷當前使用者有無以下全部角色,通過角色標識

<a shiro:hasAllRoles="admin,role1,role2" href="admin.html" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >Administer the system</a>

8、判斷當前使用者有無以下任一角色,通過角色標識

<a shiro:hasAnyRoles="admin,role2" href="admin.html" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >Administer the system</a>

到此這篇關於SpringBoot中整合Shiro實現許可權管理的示例程式碼的文章就介紹到這了,更多相關SpringBoot整合Shiro許可權內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!