SpringBoot整合Shiro(完整版)

寫在前面: 從2018年底開始學習SpringBoot,也用SpringBoot寫過一些項目。這裏對學習Springboot的一些知識總結記錄一下。如果你也在學習SpringBoot,可以關注我,一起學習,一起進步。

相關文章:

【Springboot系列】Springboot入門到項目實戰



之前寫項目安全控件基本都是用的SpringSecurity,後來發現Shiro在實際開發中用的也挺多的。這裏一起來看一下Shiro在Springboot中的基本用法吧!想了解SpringSecurity的請移步:SpringSecurity安全控件使用指南

Shiro簡介

1、簡介

Apache Shiro 是 Java 的一個安全(權限)框架。 Shiro 可以非常容易的開發出足夠好的應用,其不僅可以用在 JavaSE 環境,也可以用在 JavaEE 環境。Shiro 可以完成:認證、授權、加密、會話管理、與Web 集成、緩存等。相對於SpringSecurity簡單的多,也沒有SpringSecurity那麼複雜。

2、shiro架構

在這裏插入圖片描述
Shiro三大功能模塊

  1. Subject:主體,一般指用戶(把操作交給SecurityManager)。
  2. SecurityManager:安全管理器,管理所有Subject,可以配合內部安全組件(關聯Realm)。
  3. Realms:用於進行權限信息的驗證,shiro鏈接數據的橋樑。

細分功能

  1. Authentication:身份認證/登錄,驗證用戶是否擁有相應的身份(賬號密碼驗證)。
  2. Authorization:授權,驗證某個已認證的用戶是否擁有某權限。
  3. Session Manager:會話管理,用戶登錄後,用戶信息保存在session會話中。
  4. Cryptography:加密,保護數據的安全性,如密碼加密存儲到數據庫,而不是明文存儲。
  5. Web Support:Web支持,集成Web環境。
  6. Caching:緩存,用戶信息、角色、權限等緩存到如redis等緩存中。
  7. Concurrency:多線程併發驗證,在一個線程中開啓另一個線程,可以把權限自動傳播過去。
  8. Testing:測試支持;
  9. Run As:允許一個用戶假裝爲另一個用戶(如果他們允許)的身份進行訪問。
  10. Remember Me:記住我,登錄後,下次再來的話不用登錄了。

數據庫設計

1、表關係

在這裏插入圖片描述
菜單(TbMenu)=====> 頁面上需要顯示的所有菜單

角色(SysRole)=====> 角色及角色對應的菜單

用戶(SysUser)=====> 用戶及用戶對應的角色

用戶和角色中間表(sys_user_role)====> 用戶和角色中間表
【注】 權限管理實現原理都差不多,雖然使用的安全控件不一樣,但數據庫表的設計基本一樣,這裏數據庫表的設計和SpringSecurity實現動態權限菜單控制基本一致。

2、數據庫表結構

菜單表tb_menu
在這裏插入圖片描述
角色及菜單權限表sys_role,其中父節點parent 爲null時爲角色,不爲null時爲對應角色的菜單權限。
在這裏插入圖片描述
用戶表sys_user。
在這裏插入圖片描述
用戶和角色多對多關係,用戶和角色中間表sys_user_role(由Spring-Data-Jpa自動生成)。
在這裏插入圖片描述

搭建項目

1、新建Springboot項目

創建一個SprintBoot項目,添加項目需要的依賴(這裏持久層使用的是SpringDataJpa)。
Shiro依賴

<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-spring</artifactId>
	<version>1.4.0</version>
</dependency>

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 https://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.2.5.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.mcy</groupId>
	<artifactId>springboot-shiro</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>springboot-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-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>

		<!-- shiro與spring整合依賴 -->
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-spring</artifactId>
			<version>1.4.0</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

2、項目結構

在這裏插入圖片描述

3、配置鏈接數據庫屬性

spring.datasource.url=jdbc:mysql://localhost:3306/shiro?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.hibernate.use-new-id-generator-mappings=false
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true

server.port=80
server.servlet.context-path=/shiro

編寫代碼

1、編寫實體類

菜單表實體類TbMenu,Spring-Data-Jpa可以根據實體類去數據庫新建或更新對應的表結構,詳情可以訪問Spring-Data-Jpa入門

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.data.annotation.CreatedBy;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

/**
 * 菜單表
 * @author
 */
@Entity
public class TbMenu {
    private Integer id;
    private String name;
    private String url;
    private Integer idx;
    @JsonIgnore
    private TbMenu parent;
    @JsonIgnore
    private List<TbMenu> children = new ArrayList<>();

    @Id
    @GeneratedValue
    public Integer getId() {
        return id;
    }

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

    @Column(unique = true)
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public Integer getIdx() {
        return idx;
    }

    public void setIdx(Integer idx) {
        this.idx = idx;
    }

    @ManyToOne
    @CreatedBy
    public TbMenu getParent() {
        return parent;
    }

    public void setParent(TbMenu parent) {
        this.parent = parent;
    }

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "parent")
    @OrderBy(value = "idx")
    public List<TbMenu> getChildren() {
        return children;
    }

    public void setChildren(List<TbMenu> children) {
        this.children = children;
    }
}

角色及權限表SysRole,parent 爲null時爲角色,不爲null時爲權限。

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.data.annotation.CreatedBy;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
/***
 * 角色及角色對應的菜單權限
 * @author
 *parent 爲null時爲角色,不爲null時爲權限
 */
public class SysRole {
    private Integer id;
    private String name;    //名稱
    @JsonIgnore
    private SysRole parent;
    private Integer idx;    //排序
    @JsonIgnore
    private List<SysRole> children = new ArrayList<>();

    @Id
    @GeneratedValue
    public Integer getId() {
        return id;
    }

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

    @Column(length = 20)
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @ManyToOne
    @CreatedBy
    public SysRole getParent() {
        return parent;
    }

    public void setParent(SysRole parent) {
        this.parent = parent;
    }

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "parent")
    public List<SysRole> getChildren() {
        return children;
    }

    public void setChildren(List<SysRole> children) {
        this.children = children;
    }

    public Integer getIdx() {
        return idx;
    }

    public void setIdx(Integer idx) {
        this.idx = idx;
    }
}

最後實現的就是用戶管理了,只需要對添加的用戶分配對應的角色就可以了,用戶登錄時,顯示角色對應的權限。

import com.fasterxml.jackson.annotation.JsonIgnore;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

/**
 * 用戶表
 */
@Entity
public class SysUser {
    private Integer id;
    private String username;    //賬號
    private String password;    //密碼
    private String name;        //姓名
    @JsonIgnore
    private List<SysRole> roles = new ArrayList<>();    //角色

    @Id
    @GeneratedValue
    public Integer getId() {
        return id;
    }

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

    @Column(length = 20, unique = true)
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Column(length = 100)
    public String getPassword() {
        return password;
    }

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

    @Column(length = 20)
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @ManyToMany(cascade = CascadeType.REFRESH, fetch = FetchType.EAGER)
    @JoinTable(name = "sys_user_role", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
    public List<SysRole> getRoles() {
        return roles;
    }

    public void setRoles(List<SysRole> roles) {
        this.roles = roles;
    }
}

2、Shiro配置類

Shiro配置類ShiroConfig。

import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @description Shiro配置類
 */
@Configuration
public class ShiroConfig {

    @Bean("hashedCredentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        //指定加密方式爲MD5
        credentialsMatcher.setHashAlgorithmName("MD5");
        //加密次數
        credentialsMatcher.setHashIterations(1024);
        credentialsMatcher.setStoredCredentialsHexEncoded(true);
        return credentialsMatcher;
    }

    @Bean("userRealm")
    public UserRealm userRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher) {
        UserRealm userRealm = new UserRealm();
        userRealm.setCredentialsMatcher(matcher);
        return userRealm;
    }

    @Bean
    public ShiroFilterFactoryBean shirFilter(@Qualifier("securityManager")DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        // 設置 SecurityManager,安全管理器
        bean.setSecurityManager(securityManager);

        // 設置登錄跳轉頁面
        bean.setLoginUrl("/index");
        // 設置未授權提示頁面(沒有權限訪問後的頁面)
        bean.setUnauthorizedUrl("/unAuth");
        /**
         * Shiro內置過濾器,可以實現攔截器相關的攔截器
         *    常用的過濾器:
         *      anon:無需認證(登錄)可以訪問
         *      authc:必須認證纔可以訪問
         *      user:如果使用rememberMe的功能可以直接訪問
         *      perms:該資源必須得到資源權限纔可以訪問
         *      role:該資源必須得到角色權限纔可以訪問
         **/
        Map<String, String> filterMap = new LinkedHashMap<>();

        filterMap.put("/login","anon");
        filterMap.put("/user","authc");
        filterMap.put("/system","perms[system]");
        filterMap.put("/static/**","anon");

        filterMap.put("/**","authc");
        filterMap.put("/logout", "logout");

        bean.setFilterChainDefinitionMap(filterMap);
        return bean;
    }

    /**
     * 注入 securityManager
     */
    @Bean(name="securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(HashedCredentialsMatcher hashedCredentialsMatcher) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 關聯realm.
        securityManager.setRealm(userRealm(hashedCredentialsMatcher));
        return securityManager;
    }
}

自定義Realm用於查詢用戶的角色和權限信息並保存到權限管理器中。代碼如下:

import com.mcy.springbootshiro.entity.SysRole;
import com.mcy.springbootshiro.entity.SysUser;
import com.mcy.springbootshiro.service.SysUserService;
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.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class UserRealm extends AuthorizingRealm {
    @Autowired
    private SysUserService userService;
    
    /**
     * 授權邏輯方法
     **/
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("執行授權");

        //獲取當前登錄對象
        Subject subject = SecurityUtils.getSubject();
        SysUser user = (SysUser)subject.getPrincipal();
        if(user != null){
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            // 角色與權限字符串集合
            Collection<String> rolesCollection = new HashSet<>();

            //獲得當前用戶的角色
            List<SysRole> roles = user.getRoles();
            for(SysRole role : roles){
                rolesCollection.add(role.getName());
            }
            //添加當前用戶的角色權限,用於判斷可以訪問那些功能
            info.addStringPermissions(rolesCollection);
            return info;
        }
        return null;
    }

    /**
     * 認證
     **/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

        System.out.println("執行認證");

        // 編寫shiro判斷邏輯,判斷用戶名和密碼
        UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
        // 判斷用戶名
        SysUser bean = userService.findByUsername(token.getUsername());

        //用戶名不存在
        if(bean == null){
            throw new UnknownAccountException();
        }

        //以賬號作爲加密的鹽值
        ByteSource credentialsSalt = ByteSource.Util.bytes(bean.getUsername());

        // 判斷密碼,
        return new SimpleAuthenticationInfo(bean, bean.getPassword(),
                credentialsSalt, getName());
    }
}

其中密碼加密使用的是MD5加密,加密代碼如下:

public static void main(String[] args){
    String hashAlgorithName = "MD5";
    //加密密碼
    String password = "123456";
    //加密次數
    int hashIterations = 1024;
    //賬號作爲加密的鹽值
    ByteSource credentialsSalt = ByteSource.Util.bytes("admin");
    Object obj = new SimpleHash(hashAlgorithName, password, credentialsSalt, hashIterations);
    System.out.println(obj);
}

3、編寫控制器

新建IndexController測試控制器。這裏持久層框架使用的是SpringDataJpa,查詢只需遵循查詢方法命名規範即可,所以這裏就直接寫控制器代碼了。想進一步瞭解SpringDataJpa的小夥伴,請移步:Spring-Data-Jpa入門。
IndexController控制器代碼如下:

import com.mcy.springbootshiro.entity.SysRole;
import com.mcy.springbootshiro.entity.SysUser;
import com.mcy.springbootshiro.service.SysRoleService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class IndexController {

    @Autowired
    private SysRoleService roleService;

    @RequestMapping({"/index", "/"})
    public String index(){
        return "login";
    }

    @RequestMapping(value = "/login")
    public String login(String username, String password, Model model){
        //1.獲取subject
        Subject subject = SecurityUtils.getSubject();

        //2.封裝用戶數據
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);

        try {
            //3.執行登錄方法
            subject.login(token);
            //登錄成功
            return "redirect:/mian";
        } catch (UnknownAccountException e) {
            //e.printStackTrace();
            //登錄失敗:用戶名不存在
            model.addAttribute("msg", "用戶名不存在");
            return "login";
        } catch (IncorrectCredentialsException e) {
            //e.printStackTrace();
            //登錄失敗:用戶名不存在
            model.addAttribute("msg", "密碼輸入有誤");
            return "login";
        }
    }

    @RequestMapping(value = "/mian")
    public String main(){
        SysUser user = (SysUser) SecurityUtils.getSubject().getPrincipal();
        System.out.println(user.getName());
        //登錄成功後,輸出對應的角色和菜單
        for(SysRole role: user.getRoles()){
            System.out.println(role.getName()+"=====角色");
            for(SysRole roles : role.getChildren()){
                System.out.println(roles.getName()+"===="+role.getName()+"角色對應的菜單");
            }
        }
        return "main";
    }

    @RequestMapping("/logout")
    public String logout(){
        Subject subject = SecurityUtils.getSubject();
        if (subject != null) {
            subject.logout();
        }
        return "redirect:/main";
    }

    @RequestMapping("/unAuth")
    public String unAuth(){
        return "unAuth";
    }

    @RequestMapping("/system")
    public String system(){
        return "system";
    }
    
    @RequestMapping("/user")
    public String user(){
        return "user";
    }
}

4、編寫頁面

頁面內容都比較簡單,主要爲一個登錄頁面,登錄成功後的頁面和幾個訪問頁面。
登錄頁面login.html代碼如下(測試頁面寫的比較簡單,頁面中font標籤爲登錄失敗的提示信息,內容爲後臺傳遞過來的):

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登錄頁面</title>
    <style type="text/css">
        #main{
            width: 400px;
            margin: 100px auto;
        }
        input{
            border-radius: 5px;
            width: 200px;
            height: 20px;
        }
        button{
            margin-left: 30px;
            width: 80px;
            border-radius: 15px;
        }
    </style>
</head>
<body>
    <div id="main">
        <form action="login" method="post">
            <font color="red" th:text="${msg}"></font><br><br>
            賬號:<input type="text" name="username"><br><br>
            密碼:<input type="password" name="password"><br><br>
            <button type="submit">登錄</button>
        </form>
    </div>
</body>
</html>

登錄成功main.html頁面(幾個頁面跳轉超鏈接)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <li><a href="user">登錄即可訪問</a></li>
    <li><a href="system">管理員權限才能訪問</a></li>
    <li><a href="logout">退出登錄</a></li> 
</body>
</html>

管理員權限才能訪問的頁面system.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    管理員權限,才能訪問
</body>
</html>

登錄即可訪問的頁面user.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    登錄後即可訪問
</body>
</html>

沒有權限訪問的頁面,提示頁面unAuth.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<p>您暫時沒有權限訪問該頁面</p>
</body>
</html>

測試

1、應用測試

運行項目後訪問http://localhost/shiro/index,進入登錄頁面,登錄擁有管理員權限的用戶。
在這裏插入圖片描述
登錄成功後,可以從編輯器控制檯中看出第一步進行了Shrio的認證。之後執行了登錄成功後跳轉頁面的後臺方法,輸出了對應的角色和角色對應的菜單。
【注】 此案例中前臺菜單沒有進行遍歷,菜單,權限等功能也沒有寫對應的增刪改查。直接在數據庫中添加數據進行測試的。如果想了解菜單遍歷,菜單權限等功能的實現,請訪問SpringSecurity實現動態菜單加載。其中數據庫設計,功能操作都是一樣的。

點擊“管理員權限才能訪問”首先會進行執行授權,根據判斷是否有管理員身份,確定是否可以訪問。因爲是擁有管理員身份的用戶登錄的,所以可以訪問,效果如下:
在這裏插入圖片描述
正是system.html頁面中的內容。如果沒有管理員身份,則顯示效果如下:
在這裏插入圖片描述

2、案例代碼下載

案例代碼下載地址:https://github.com/machaoyin/springboot-shrio

最後有什麼不足之處,歡迎大家指出,期待與你的交流。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章