Spring Boot Shiro 使用教程


Apache Shiro 已經大名鼎鼎,搞 Java 的沒有不知道的,這類似於 .Net 中的身份驗證 form 認證。跟 .net core 中的認證授權策略基本是一樣的。當然都不知道也沒有關系,因為所有的權限都是模擬的人或機構的社會行為。

本系列從簡單的權限講起,主要涉及到 Shiro、Spring Security、Jwt、OAuth2.0及其他自定義權限策略。

本章主要講解 Shiro 的基本原理與如何使用,本章主要用到以下基礎設施:

  • jdk1.8+
  • spring boot 2.1.6
  • idea 2018.1

本項目源碼下載

1 Spring Boot 快速集成 Shiro 示例

首先我們來一段真實的代碼演示下 Spring Boot 如何集成 Shiro 。本代碼示例暫時沒有使用到數據庫相關知識,本代碼主要使用到:

  1. shiro
  2. thymeeaf

本示例演示了網站用戶 admin 密碼 123456 的用戶使用用戶名密碼登錄網站,經過 Shiro 認證后,獲取了授權權限列表,演示了權限的使用。

本項目源碼下載

shiro示例項目目錄

1.1 新建 Spring Boot Maven 示例工程項目

  1. File > New > Project,如下圖選擇 Spring Initializr 然后點擊 【Next】下一步
  2. 填寫 GroupId(包名)、Artifact(項目名) 即可。點擊 下一步
    groupId=com.fishpro
    artifactId=shiro
  3. 選擇依賴 Spring Web Starter 前面打鈎。
  4. 項目名設置為 spring-boot-study-shiro.

1.2 依賴引入 Pom.xml

本代碼主要使用到:

  1. shiro
  2. thymeeaf

在Pom.xml中加入以下代碼

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.fishpro</groupId>
    <artifactId>shiro</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>shiro</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!--shiro 1.4.0 thymeleaf-extras-shiro 2.0.0 組合-->
        <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>
        <!--shiro for thymeleaf 生效需要加入 spring boot 2.x 請使用 2.0.0 版本 否則使用 1.2.1版本-->
        <dependency>
            <groupId>com.github.theborakompanioni</groupId>
            <artifactId>thymeleaf-extras-shiro</artifactId>
            <version>2.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

1.3 配置 application.yml

本示例使用默認配置,暫不需要在 application.yml 中配置 shiro。我們把 application.properties 改成了 application.yml (習慣問題) ,修改了默認端口

server:
  port: 8086

1.4 自定義 Realm 領域 UserRealm 實現自定義認證與授權

在 src/main/java/com/java/fishpro/shiro/config(config是新增的包名) 新增 UserRealm.java 文件
UserRealm 是一種安全數據源,用戶登錄認證核心在此類中實現,用戶授權也在此類中實現,具體看代碼注釋

  1. 重寫了 doGetAuthenticationInfo 實現對用戶名密碼的認證,返回一個 SimpleAuthenticationInfo 對象。*注意,因為 shiro 是一個安全框架,具體的身份證明的認證就要交給我們自己去實現,實際上認證是業務邏輯,最好我們自己實現。
  2. 重寫了 doGetAuthorizationInfo 實現對當前用戶的授權,返回一個 SimpleAuthorizationInfo 對象,注意,授權就是從業務系統數據庫中查詢到當前用戶的已知權限列表,寫在當前會話中,以便在使用的時候去做匹配,匹配成功表示授權成功,匹配失敗表示沒授權
//定義一個實體對象用於存儲用戶信息
public class UserDO {
    private Integer id;
    private String userName;//就是 shiro 中的身份,系統中唯一的存在
    private String password; //就是 shiro 中的證明

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
 
}

在包 config(沒有就新建 config) 下建立 UserRealm

package com.fishpro.shiro.config;

import com.fishpro.shiro.domain.UserDO;
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 java.security.Principal;
import java.util.*;

/**
 * 用戶領域 重寫了 AuthorizingRealm ,AuthorizingRealm(授權) 其實是繼承了 AuthenticatingRealm(認證)
 * 所在在這里只要繼承 AuthorizingRealm(授權),主要實現 授權和認證的方法重寫
 * 1.doGetAuthenticationInfo 重寫認證
 * 2.doGetAuthorizationInfo 重寫授權
 * */
public class UserRealm extends AuthorizingRealm {
    /**
     * doGetAuthenticationInfo 重寫認證
     * @param authenticationToken token
     * @return 返回認證信息實體(好看身份和證明) AuthenticationInfo
     * */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username=(String)authenticationToken.getPrincipal();//身份 例如 用戶名
        Map<String ,Object> map=new HashMap<>(16);
        map.put("username",username);
        String password  =new String((char[]) authenticationToken.getCredentials());//證明 例如 密碼
        //對身份+證明的數據認證 這里模擬了一個數據源
        //如果是數據庫 那么這里應該調用數據庫判斷用戶名密碼是否正確
        if(!"admin".equals(username) || !"123456".equals(password)){
            throw new IncorrectCredentialsException("賬號或密碼不正確");
        }
        //認證通過
        UserDO user=new UserDO();
        user.setId(1);//假設用戶ID=1
        user.setUserName(username);
        user.setPassword(password);
        //建立一個 SimpleAuthenticationInfo 認證模塊,包括了身份】證明等信息
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
        return info;
    }

    /**
     * 重寫授權 doGetAuthorizationInfo 返回  授權信息對象 AuthorizationInfo
     * @param  principalCollection 身份信息
     * @return  返回  授權信息對象 AuthorizationInfo
     * */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        UserDO userDO  = (UserDO)principalCollection.getPrimaryPrincipal();
        Integer userId= userDO.getId();//轉成 user 對象
        //授權 新建一個授權模塊 SimpleAuthorizationInfo 把 權限賦值給當前的用戶
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        //設置當前會話擁有的角色 實際場景根據業務來如從數據庫獲取角色列表
        Set<String> roles=new HashSet<>();
        roles.add("admin");
        roles.add("finance");
        info.setRoles(roles);

        //設置當前會話可以擁有的權限 實際場景根據業務來如從數據庫獲取角色列表下的權限列表
        Set<String> permissions=new HashSet<>();
        permissions.add("system:article:article");
        permissions.add("system:article:add");
        permissions.add("system:article:edit");
        permissions.add("system:article:remove");
        permissions.add("system:article:batchRemove");
        info.setStringPermissions(permissions);
        return  info;
    }

}

1.6 shiro 實現登錄認證

這里主要是顯示 login.html 與 LoginController

shiro 登錄驗證邏輯

1.6.1 登錄 html 頁面

新增文件 resources/templates/login.html 表示登錄頁面,這里使用 jquery 來實現邏輯

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>使用 shiro 登錄頁面</title>
</head>
<body>
<div>
    <input id="userName" name="userName" value="">
</div>
<div>
    <input id="password" name="password" value="">
</div>
<div>
    <input type="button" id="btnSave"  value="登錄">
</div>
<script src="https://cdn.bootcss.com/jquery/1.11.3/jquery.js"></script>
<script>
    $(function() {
        $("#btnSave").click(function () {
            var username=$("#userName").val();
            var password=$("#password").val();
            $.ajax({
                cache: true,
                type: "POST",
                url: "/login",
                data: "userName=" + username + "&password=" + password,
                dataType: "json",
                async: false,
                error: function (request) {
                    console.log("Connection error");
                },
                success: function (data) {
                    if (data.status == 0) {
                        window.location = "/index";
                        return false;

                    } else {
                        alert(data.message);
                    }

                }
            });
        });
    });
</script>
</body>
</html>

1.6.2 登錄邏輯

在 UserController中新增兩個方法, 路由都是 /login,一個是get 一個是post,因為登錄頁面是不需要認證,所有兩個路由都是 /login 的頁面不需要進行認證就可以訪問。

//get /login 方法,對應前端 login.html 頁面
    @GetMapping("/login")
    public String login(){
        return "login";
    }

    //post /login 方法,對應登錄提交接口
    @PostMapping("/login")
    @ResponseBody
    public Object loginsubmit(@RequestParam String userName,@RequestParam String password){
        Map<String,Object> map=new HashMap<>();
        //把身份 useName 和 證明 password 封裝成對象 UsernamePasswordToken
        UsernamePasswordToken token=new UsernamePasswordToken(userName,password);
        //獲取當前的 subject
        Subject subject = SecurityUtils.getSubject();
        try{
            subject.login(token);
            map.put("status",0);
            map.put("message","登錄成功");
            return map;
        }catch (AuthenticationException e){
            map.put("status",1);
            map.put("message","用戶名或密碼錯誤");
            return map;
        }
    }

1.7 shiro 實現Controller層方法授權

shiro 授權邏輯

這里需要增加幾個頁面來實現這個功能

1.7.1 resources/templates/index.html 登陸成功后跳轉的頁面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>通過登錄驗證后跳轉到此頁面</title>
</head>
<body>
    通過登錄驗證后跳轉到此頁面
<div>
    <a href="/article">前往文章頁面</a>
</div>
    <div>
        <a href="/setting">前往設置頁面</a>
    </div>
</body>
</html>

1.7.2 resources/templates/article.html 已授權訪問的頁面

<!DOCTYPE html>
 <html lang="en">
 <head>
     <meta charset="UTF-8">
     <title>必須獲取 app:article 授權</title>
 </head>
 <body>
 必須獲取 app:article 授權 才會顯示
 </body>
 </html>

1.7.3 resources/templates/setting.html 未授權訪問的頁面

 <!DOCTYPE html>
 <html lang="en">
 <head>
     <meta charset="UTF-8">
     <title>必須獲取 app:setting 授權 </title>
 </head>
 <body>
 必須獲取 app:setting 授權 才會顯示
 </body>
 </html>

1.7.4 resources/templates/error/403.html 未授權統一吹頁面

<!DOCTYPE html>
 <html lang="en">
 <head>
     <meta charset="UTF-8">
     <title>403 沒有授權</title>
 </head>
 <body>
 你訪問的頁面沒有授權
 </body>
 </html>

1.7.5 controller/UserController.java Controller層方法

如下源代碼

  1. 方法 article 需要權限 app:article:article 才能進入
  2. 方法 setting 需要權限 app:setting:setting 才能進入

@Controller
public class UserController {
    //shiro 認證成功后默認跳轉頁面
    @GetMapping("/index")
    public String index(){
        return "index";
    }

    @GetMapping("/403")
    public String err403(){
        return "403";
    }
    /**
     * 根據權限授權使用注解 @RequiresPermissions
     * */
    @GetMapping("/article")
    @RequiresPermissions("app:article:article")
    public String article(){
        return "article";
    }

    /**
     * 根據權限授權使用注解 @RequiresPermissions
     * */
    @GetMapping("/setting")
    @RequiresPermissions("app:setting:setting")
    public String setting(){
        return "setting";
    }
    @GetMapping("/login")
    public String login(){
        return "login";
    }

    @PostMapping("/login")
    @ResponseBody
    public Object loginsubmit(@RequestParam String userName,@RequestParam String password){
        Map<String,Object> map=new HashMap<>();
        //把身份 useName 和 證明 password 封裝成對象 UsernamePasswordToken
        UsernamePasswordToken token=new UsernamePasswordToken(userName,password);
        //獲取當前的 subject
        Subject subject = SecurityUtils.getSubject();
        try{
            subject.login(token);
            map.put("status",0);
            map.put("message","登錄成功");
            return map;
        }catch (AuthenticationException e){
            map.put("status",1);
            map.put("message","用戶名或密碼錯誤");
            return map;
        }

    }
}

1.8 shiro 實現前端頁面中授權

我們使用了 Thymeleaf 作為前端的模板引擎,您也可以使用 JSP,FreeMarker 等引擎。Shiro 已經能夠很好的在 Thymeleaf 中使用,如下代碼我在首頁中使用

如下代碼,因為沒有 app:setting:setting 權限所有【前往設置頁面】不會顯示

<hr/>
    <div>
        <title style="color:red;">注意下面是包括權限的,第二個鏈接因為沒有授權是不可見的</title>
    </div>
    <div shiro:hasPermission="app:article:article">
        <a href="/article">前往文章頁面</a>
    </div>
    <div shiro:hasPermission="app:setting:setting">
        <a  href="/setting">前往設置頁面</a>
    </div>

1.9 shiro 在程序代碼塊中使用授權判斷

1.9.1 通過角色判斷

 Subject subject = SecurityUtils.getSubject();
        String str="";
        if(subject.hasRole("admin")){
            str=str+"您擁有 admin 權限";
        }else{
            str=str+"您沒有 admin 權限";
        }
        if(subject.hasRole("sale")){
            str=str+"您擁有 sale 權限";
        }
        else{
            str=str+"您沒有 sale 權限";
        }

1.9.2 通過權限判斷

注意這里是直接拋出異常,會被全局異常捕捉

        Subject subject = SecurityUtils.getSubject();
        try{
            subject.checkPermission("app:setting:setting");
            str=str+"您擁有 app:setting:setting 權限";

        }catch (UnauthenticatedException ex){
            str=str+"您沒有 app:setting:setting 權限";
        }

為什么我的注解沒生效?

要使用 shiro 注解來授權 Controller 的方法,那么需要在 ShiroConfig 中加入以下代碼

/**
     *  開啟shiro aop注解支持 如@RequiresRoles,@RequiresPermissions
     *  使用代理方式;所以需要開啟代碼支持;
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

1.10 登出/注銷

調用 subject 的logout 方法進行注銷

@GetMapping("/logout")
    String logout(HttpSession session, SessionStatus sessionStatus, Model model) {
        //會員中心退出登錄 當使用這兩屬性session屬性退出
        session.removeAttribute("userData");
        sessionStatus.setComplete();
        SecurityUtils.getSubject().logout();
        return "redirect:/login";

    }

1.11 問題

  1. @RequiresPermissions 注解無效
    注解無效,沒有走到注解,基本就是AOP攔截問題,需要在 ShiroConfig 配置中增加配置
  1. spring boot shiro Not authorized to invoke method 當 @RequiresPermissions 中的權限沒有的時候發生
    @RequiresPermissions 既然生效了,那為什么又會報錯呢,按道理已經登錄,但是沒有權限的方法體,應該跳轉到/403 頁面才對。
    這里應該也是沒有攔截到方法這個錯。這個在 Spring Boot 全局異常處理 中講過,需要使用到異常捕捉機制,捕捉到這個異常 org.apache.shiro.authz.UnauthorizedException ,然后做統一處理。
@ControllerAdvice(annotations = Controller.class)
public class MyExceptionController {
    private static final Logger logger= LoggerFactory.getLogger(MyExceptionController.class);
    public static final String DEFAULT_ERROR_VIEW = "error";

    @ExceptionHandler(value = UnauthorizedException.class)//處理訪問方法時權限不足問題
    public String defaultErrorHandler(HttpServletRequest req, Exception e)  {
        return "error/403";
    }
}
  1. shiro:hasPermission 標簽在 thymeleaf 中不生效問題

shiro:hasPermission 標簽應用在 thymeleaf ,由於涉及到兩個框架,如果原生不支持,那么比如要引入第三方控件。

/**
     * ShiroDialect,為了在thymeleaf里使用shiro的標簽的bean
     * @return
     */
    @Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }
  1. 有的時候回報錯 org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'shiroDialect'

這個應該是第三方插件 com.github.theborakompanioni 與 spring boot 版本兼容性問題。我改為了以下版本 shiro for thymeleaf 生效需要加入 spring boot 2.x 請使用 2.0.0 版本

 <!--shiro 1.4.0 thymeleaf-extras-shiro 2.0.0 組合-->
        <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>
        <!--shiro for thymeleaf 生效需要加入 spring boot 2.x 請使用 2.0.0 版本 否則使用 1.2.1版本-->
        <dependency>
            <groupId>com.github.theborakompanioni</groupId>
            <artifactId>thymeleaf-extras-shiro</artifactId>
            <version>2.0.0</version>
        </dependency>

本項目源碼下載


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM