Shiro和Spring MVC、Mybatis整合教程

薛8發表於2019-02-02
Shiro和Spring MVC、Mybatis整合教程

前言

Apache Shiro 是Java的安全框架,提供了認證(Authentication)、授權(Authorization)、會話(Session)管理、加密(Cryptography)等功能,且Shiro與Spring Security等安全框架相比具有簡單性、靈活性、支援細粒度鑑權、支援一級快取等,還有Shiro不跟任何容器(Tomcat等)和框架(Sping等)捆綁,可以獨立執行,這也造就了Shiro不僅僅是可以用在Java EE上還可以用在Java SE上

Shiro四大功能

在開始之前,首先了解一下Shiro的四大功能,俗話說“知己知彼百戰不殆”。

image

認證

認證就是使用者訪問系統的時候,系統要驗證使用者身份的合法性,比如我們通常所說的“登入”就是認證的一種方式,只有登入成功了之後我們才能訪問相應的資源。在Shiro中,我們可以將使用者理解為Subject主體,在使用者身份認證的時候,使用者需要提供能證明他身份的資訊,如使用者名稱、密碼等,使用者所提供的這些使用者名稱、密碼則對應Shiro中的Principal、 Credentials,即在Subject進行身份認證的時候,需要提供相應的Principal、 Credentials,對應的程式碼如下:

UsernamePasswordToken token = new UsernamePasswordToken(username, password);
Subject subject = SecurityUtils.getSubject();
subject.login(token); //提交認證
複製程式碼

我們知道Http協議是無狀態的,所以使用者認證成功後怎麼才能保持認證成功的狀態呢?如果是我們開發的話一般都是登入成功後將Session儲存在伺服器,然後再將Session返回給使用者,之後的請求使用者都將這個Session帶上,然後伺服器根據使用者請求攜帶的Session和伺服器儲存的Session進行比較來判斷使用者是否已認證。但是使用Shiro後,Shiro已經幫我們做好這個了(下面介紹的會話管理),是不是feel爽~

授權

授權可以理解為訪問控制,在使用者認證(登入)成功之後,系統對使用者訪問資源的許可權進行控制,即確定什麼使用者能訪問什麼資源,如普通使用者不能訪問後臺,但是管理員可以。在這裡我們還需要認識幾個概念,資源(Resource)、角色(Role)、許可權(Permission),上面提到的Subject主體可以有多個角色,每個角色又對應多個資源的多個許可權,這種基於資源的訪問控制可以實現細粒度的許可權。對主體設定角色、許可權的程式碼如下:

SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//  新增使用者的角色
authorizationInfo.addRoles(roleIdList);
//  新增使用者的許可權
authorizationInfo.addStringPermissions(resourceIdList);
複製程式碼

如果要實現這樣的授權功能,我們必定需要設計一個使用者組、許可權,給每個方法或者URL加上判斷,是否當前登入的使用者滿足條件。但是使用Shiro後,Shiro也幫我們幫這些都做好了

會話管理

會話管理的會話即Session,所謂會話,即使用者訪問應用時保持的連線關係,在多次互動中應用能夠識別出當前訪問的使用者是誰,且可以在多次互動中儲存一些資料。如訪問一些網站時登入成功後,網站可以記住使用者,且在退出之前都可以識別當前使用者是誰。在Shiro中,與使用者有關的一切資訊都可以通過Shiro的介面獲得,和使用者的會話Session也都由Shiro管理。如實現“記住我”或者“下次自動登入”的功能,如果要自己去開發的話,估計又得話不少時間。但是使用Shiro後,Shiro也幫我們幫這些都做好了

加密

使用者密碼明文儲存是不是安全,應不應該MD5加密,是不是應該加鹽,又要寫密碼加密的程式碼。 這些Shiro已經幫你做好了

Shiro三大核心概念

從整體概念上理解,Shiro的體系架構有三個主要的概念,Subject(主體),Security Manager (安全管理器)和 Realms (域)。

image

Subject主體

主體是當前正在操作的使用者的特定資料集合。主體可以是一個人,也可以代表第三方服務,守護程式,定時任務或類似的東西,也就是幾乎所有與該應用進行互動的事物。所有Subject都繫結到SecurityManager,與Subject的所有互動都會委託給 SecurityManager,可以把 Subject 認為是一個門面,SecurityManager 才是實際的執行者。

Security Manager安全管理器

安全管理器,即所有與安全有關的操作都會與SecurityManager互動,且它管理著所有Subject可以看出它是Shiro的核心,它負責與後邊介紹的其他元件進行互動,如果學習過 SpringMVC,你可以把它看成DispatcherServlet前端控制器,一般來說,一個應用只會存在一個SecurityManager例項

Realms域

域,Shiro從Realm獲取安全資料(如使用者、角色、許可權),就是說SecurityManager要驗證使用者身份,那麼它需要從Realm獲取相應的使用者進行比較以確定使用者身份是否合法,也需要從Realm得到使用者相應的角色 / 許可權進行驗證使用者是否能進行操作,即Realms作為Shiro與應用程式安全資料之間的“橋樑”。從這個意義上講,Realm實質上是一個安全相關的DAO,它封裝了資料來源的連線細節,並在需要時將相關資料提供給Shiro。其中Realm有2個方法,doGetAuthenticationInfo用來認證,doGetAuthorizationInfo用來授權。

Spring、Spring MVC、Mybatis、Shiro整合

專案目錄

image

新增依賴包

pox.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>

  <groupId>shiro</groupId>
  <artifactId>shiro</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <name>shiro Maven Webapp</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.7</maven.compiler.source>
    <maven.compiler.target>1.7</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>

    <!--Sping核心依賴-->
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>5.1.3.RELEASE</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-web -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>5.1.3.RELEASE</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>5.1.3.RELEASE</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>5.1.3.RELEASE</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.1.3.RELEASE</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-context-support -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context-support</artifactId>
      <version>5.1.3.RELEASE</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aop</artifactId>
      <version>5.1.3.RELEASE</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-test -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>5.1.3.RELEASE</version>
      <scope>test</scope>
    </dependency>

    <!--Mybatis依賴-->
    <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.4.6</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>1.3.2</version>
    </dependency>

    <!--MySQL連線驅動-->
    <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.13</version>
    </dependency>


    <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core -->
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-core</artifactId>
      <version>1.4.0</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-web -->
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-web</artifactId>
      <version>1.4.0</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring -->
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-spring</artifactId>
      <version>1.4.0</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>4.0.1</version>
      <scope>provided</scope>
    </dependency>


  </dependencies>

  <build>
    <finalName>shiro</finalName>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <plugins>
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.1.0</version>
        </plugin>
        <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-war-plugin</artifactId>
          <version>3.2.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>2.5.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>2.8.2</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>

複製程式碼

建立資料庫和實體類

為了減少篇幅,只做簡單介紹,詳情可以檢視原始碼,資料庫檔案在本專案根目錄。

image

  • resource表:資源表,有idname兩個欄位,分別對應資源id和許可權。
  • role表:角色表,有idname兩個欄位,分別對應角色id和角色名。
  • role_resource表:角色資源許可權表,有idroleidresid三個欄位,分別對應自增id、角色id和資源id。
  • user表:使用者表,有idusernamepassword三個欄位,分別對應自增id、使用者名稱和密碼。
  • user_role表:有iduidrid三個欄位,分別對應自增id、使用者id、和角色id。

Dao層

AccountDao.java:

public interface AccountDao {
    User findUserByUsername(String username);
    List<Role> findRoleByUserId(int id);
    List<Resource> findResourceByUserId(int id);
}
複製程式碼

service層

AccountService.java:

public interface AccountService {
    User findUserByUsername(String username);
    List<Role> findRoleByUserId(int id);
    List<Resource> findResourceByUserId(int id);
    boolean login(User user);
}
複製程式碼

AccountServiceImpl.java:

package com.shiro.service.impl;

import com.shiro.dao.AccountDao;
import com.shiro.entity.Role;
import com.shiro.entity.User;
import com.shiro.service.AccountService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

/**
 * @program: shiro
 * @description:
 * @author: Xue 8
 * @create: 2019-02-01 15:37
 **/
@Service
public class AccountServiceImpl implements AccountService {
    @Resource
    AccountDao accountDao;

    /**
     * @description: 根據使用者名稱查詢使用者資訊
     * @param: [username]
     * @return: com.shiro.entity.User
     * @author: Xue 8
     * @date: 2019/2/1
     */
    @Override
    public User findUserByUsername(String username) {
        return accountDao.findUserByUsername(username);
    }

    @Override
    public List<Role> findRoleByUserId(int id) {
        return accountDao.findRoleByUserId(id);
    }

    @Override
    public List<com.shiro.entity.Resource> findResourceByUserId(int id) {
        return accountDao.findResourceByUserId(id);
    }

    public boolean login(User user){
//        獲取當前使用者物件subject
        Subject subject = SecurityUtils.getSubject();
        System.out.println("subject:" + subject.toString());
//        建立使用者名稱/密碼身份證驗證Token
        UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());
        System.out.println("token" + token);
        try {
            subject.login(token);
            System.out.println("登入成功");
            return true;
        } catch (Exception e) {
            System.out.println("登入失敗" + e);
            return false;
        }
    }
}

複製程式碼

MyRealm.java

package com.shiro.service.impl;

import com.shiro.entity.Role;
import com.shiro.entity.User;
import com.shiro.service.AccountService;
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 javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

/**
 * @program: shiro
 * @description:
 * @author: Xue 8
 * @create: 2019-02-01 15:16
 **/

public class MyRealm extends AuthorizingRealm {
    @Resource
    AccountService accountService;

    /**
      * 身份認證的方法 認證成功獲取身份驗證資訊
     * 這裡最主要的是user.login(token);這裡有一個引數token,這個token就是使用者輸入的使用者密碼,
     * 我們平時可能會用一個物件user來封裝使用者名稱和密碼,shiro用的是token,這個是控制層的程式碼,還沒到shiro,
     * 當呼叫user.login(token)後,就交給shiro去處理了,接下shiro應該是去token中取出使用者名稱,然後根據使用者去查資料庫,
     * 把資料庫中的密碼查出來。這部分程式碼一般都是要求我們自定義實現,自定義一個realm,重寫doGetAuthenticationInfo方法
    **/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//        獲取使用者輸入的使用者名稱和密碼
//        實際上這個token是從UserResource面currentUser.login(token)傳過來的
//        兩個token的引用都是一樣的
        String username = (String) authenticationToken.getPrincipal();
//        密碼要用字元陣列來接受 因為UsernamePasswordToken(username, password) 儲存密碼的時候是將字串型別轉成字元陣列的 檢視原始碼可以看出
        String password = new String((char[]) authenticationToken.getCredentials());
//        呼叫service 根據使用者名稱查詢使用者資訊
        User user = accountService.findUserByUsername(username);
//        String password = user.getPassword();
//        判斷使用者是否存在 不存在則丟擲異常
        if (user != null) {
//            判斷使用者密碼是否匹配 匹配則不匹配則丟擲異常
            if (user.getPassword().equals(password)) {
//                登入成功 把使用者資訊儲存在Session中
                Session session = SecurityUtils.getSubject().getSession();
                session.setAttribute("userSession", user);
                session.setAttribute("userSessionId", user.getId());
//                認證成功 返回一個AuthenticationInfo的實現
                return new SimpleAuthenticationInfo(username, password, getName());
            } else {
                System.out.println("密碼不正確");
                throw new IncorrectCredentialsException();
            }
        } else {
            System.out.println("賬號不存在");
            throw new UnknownAccountException();
        }
    }

    /**
      * 授權的方法
     * 1、subject.hasRole(“admin”) 或 subject.isPermitted(“admin”):自己去呼叫這個是否有什麼角色或者是否有什麼許可權的時候;
     *
     * 2、@RequiresRoles("admin") :在方法上加註解的時候;
     *
     * 3、[@shiro.hasPermission name = "admin"][/@shiro.hasPermission]:在頁面上加shiro標籤的時候,即進這個頁面的時候掃描到有這個標籤的時候。
     * 4、xml配置許可權的時候也會走
    **/
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("授權");
//        從principalCollection獲取使用者資訊
//        如果doGetAuthenticationInfo(user,password,getName()); 傳入的是user型別的資料 那這裡getPrimaryPrincipal獲取到的也是user型別的資料
        String username = (String) principalCollection.getPrimaryPrincipal();
        User user = accountService.findUserByUsername(username);
//        獲取該使用者的所有角色
        List<Role> roleList = accountService.findRoleByUserId(user.getId());
//        將角色的id放到一個String列表中 因為authorizationInfo.addRoles()方法只支援角色的String列表或者單個角色String
        List<String> roleIdList = new ArrayList<String>();
        for (Role role:roleList) {
            roleIdList.add(role.getName());
        }
//        獲取該使用者的所有許可權
        List<com.shiro.entity.Resource> resourceList = accountService.findResourceByUserId(user.getId());
        List<String> resourceIdList = new ArrayList<String>();
//        將許可權id放到一個String列表中 因為authorizationInfo.addRoles()方法只支援角色的String列表或者單個角色String
        for (com.shiro.entity.Resource resource:resourceList) {
            resourceIdList.add(resource.getName());
        }
        System.out.println("授權11");
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//        新增使用者的角色
        authorizationInfo.addRoles(roleIdList);
//        新增使用者的許可權
        authorizationInfo.addStringPermissions(resourceIdList);
        return authorizationInfo;
    }
}

複製程式碼

controller層

AccountController.java

package com.shiro.controller;

import com.shiro.entity.User;
import com.shiro.service.AccountService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

/**
 * @program: shiro
 * @description:
 * @author: Xue 8
 * @create: 2019-02-01 13:14
 **/
@Controller
public class AccountController {
    @Resource
    AccountService accountService;
    @Resource
    HttpServletRequest servletRequest;

    @RequestMapping(value = "/home")
    public String  home(){
        return "home";
    }

    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public String  getLogin(){
        return "login";
    }

    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public String doLogin(@RequestParam(value = "username") String username,
                        @RequestParam(value = "password") String password){
        User user = new User();
        user.setUsername(username);
        user.setPassword(password);
        if (accountService.login(user)) {
            return "/home";
        }
        return "/login";
    }
}

複製程式碼

GET方法訪問/login的時候,會出現登入頁面,輸入賬號密碼點選登入資料將以POST方式提交給/login,如果賬號密碼匹配返回/home的頁面,否則返回/login的頁面。/home頁面只有在登入且有許可權的情況下才可以訪問,未登入情況下訪問會轉跳/login頁面,這個在Shiro的配置檔案裡面配置。

配置檔案

applicationContext.xml:配置Spring

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">

    <!--開啟掃描註冊-->
    <context:component-scan base-package="com.shiro"></context:component-scan>

    <!--讀取properties配置-->
    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="location" value="classpath:jdbcConfig.properties"></property>
    </bean>

    <!--配置資料來源-->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="${driverClassName}"></property>
        <property name="username" value="${username}"></property>
        <property name="password" value="${password}"></property>
        <property name="url" value="${url}"></property>
    </bean>

    <!--配置session工廠-->
    <bean id="sessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"></property>
        <property name="configLocation" value="classpath:mybatis-config.xml"></property>
        <property name="mapperLocations" value="classpath:mapping/*.xml"></property>
    </bean>

    <!--配置掃描mapping-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.shiro.dao"></property>
        <property name="sqlSessionFactoryBeanName" value="sessionFactoryBean"></property>
    </bean>
</beans>
複製程式碼

spring-shiro.xml:配置Shiro

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="myRealm"></property>
    </bean>

    <bean id="myRealm" class="com.shiro.service.impl.MyRealm">
        <!--關閉許可權快取 不然doGetAuthorizationInfo授權方法不執行-->
        <property name="authorizationCachingEnabled" value="false"/>
    </bean>

    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"></property>
        <property name="successUrl" value="/success"></property>
        <!--登入頁面-->
        <property name="loginUrl" value="/login"></property> 
        <property name="filterChainDefinitions">
            <value>
                <!--配置`/home`只有擁有`admin`角色的使用者才可以訪問-->
                /home = authc,roles[admin]
            </value>
        </property>
    </bean>

</beans>
複製程式碼

這裡需要注意的是 在配置Realm的時候,如果沒用上快取功能的話,需要將快取關掉,不然進不到doGetAuthorizationInfo授權方法。

測試

開啟http://localhost:8080/login登入頁面,填寫正確使用者名稱和密碼登入

image
登入成功 轉跳成功頁面
image
清除瀏覽器cookie之後(未登入狀態),開啟http://localhost:8080/home頁面,自動轉跳到了/login登入頁面(即沒有許可權訪問),登入賬戶,再次開啟http://localhost:8080/home頁面即可正常訪問。

總結

這是我學習Shiro時候根據自己的情況記錄下來的,希望對大家有所幫助,如果大家想對Shiro進一步研究的話,推薦大家看張開濤老師的《跟我學Shiro》,最後附上本專案的Github地址:github.com/xue8/Java-D…

原文地址:ddnd.cn/2019/02/02/…

相關文章