spring-boot-2.0.3應用篇 - shiro集成


前言

       上一篇:spring-boot-2.0.3源碼篇 - 國際化,講了如何實現國際化,實際上我工作用的模版引擎是freemaker,而不是thymeleaf,不過原理都是相通的。

       接着上一篇,這一篇我來講講spring-boot如何整合工作中用到的一個非常重要的功能:安全,而本文的主角就是一個安全框架:shiro。

       Apache Shiro是Java的一個安全框架。目前,使用Apache Shiro的人也越來越多,因為它相當簡單,對比Spring Security,可能沒有Spring Security的功能強大,但是在實際工作時可能並不需要那么復雜的東西,所以使用小而簡單的Shiro就足夠了。對於它倆到底哪個好,這個不必糾結,能更簡單的解決項目問題就好了。

摘自開濤兄的《跟我學Shiro》

       本文旨在整合spring-boot與shiro,實現簡單的認證功能,shiro的更多使用細節大家可以去閱讀《更我學shiro》或者看官方文檔

  本文項目地址:spring-boot-shiro

spring-boot整合shiro

       集成mybatis

    Shiro不會去維護用戶、維護權限;這些需要我們自己去設計/提供,然后通過相應的接口注入給Shiro;既然用戶、權限這些信息需要我們自己設計、維護,那么可想而知需要進行數據庫表的設計了(具體表結構看后文),既然涉及到數據庫的操作,那么我們就先整合mybatis,實現數據庫的操作。

    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>

    <groupId>com.lee</groupId>
    <artifactId>spring-boot-shiro</artifactId>
    <version>1.0-SNAPSHOT</version>

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

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.3.RELEASE</version>
    </parent>

    <dependencies>

        <!-- mybatis相關 -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.5</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- test -->
        <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>
View Code

    配置文件application.yml:

spring:
  #連接池配置
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://localhost:3306/spring-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8
      username: root
      password: 123456
      initial-size: 1                     #連接池初始大小
      max-active: 20                      #連接池中最大的活躍連接數
      min-idle: 1                         #連接池中最小的活躍連接數
      max-wait: 60000                     #配置獲取連接等待超時的時間
      pool-prepared-statements: true    #打開PSCache,並且指定每個連接上PSCache的大小
      max-pool-prepared-statement-per-connection-size: 20
      validation-query: SELECT 1 FROM DUAL
      validation-query-timeout: 30000
      test-on-borrow: false             #是否在獲得連接后檢測其可用性
      test-on-return: false             #是否在連接放回連接池后檢測其可用性
      test-while-idle: true             #是否在連接空閑一段時間后檢測其可用性
#mybatis配置
mybatis:
  type-aliases-package: com.lee.shiro.entity
  #config-location: classpath:mybatis/mybatis-config.xml
  mapper-locations: classpath:mybatis/mapper/*.xml
# pagehelper配置
pagehelper:
  helperDialect: mysql
  #分頁合理化,pageNum<=0則查詢第一頁的記錄;pageNum大於總頁數,則查詢最后一頁的記錄
  reasonable: true
  supportMethodsArguments: true
  params: count=countSql
View Code

    在數據庫spring-boot中新建表tbl_user:

DROP TABLE IF EXISTS `tbl_user`;
CREATE TABLE `tbl_user` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  `username` varchar(50) NOT NULL COMMENT '名稱',
  `password` char(32) NOT NULL COMMENT '密碼',
  `salt` char(32) NOT NULL COMMENT '鹽,用於加密',
  `state` tinyint(2) NOT NULL DEFAULT '1' COMMENT '狀態, 1:可用, 0:不可用',
  `description` varchar(50) DEFAULT '' COMMENT '描述',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COMMENT='用戶表';

-- ----------------------------
-- Records of tbl_user
-- ----------------------------
INSERT INTO `tbl_user` VALUES ('1', 'admin', 'd3c59d25033dbf980d29554025c23a75', '8d78869f470951332959580424d4bf4f', '1', 'bing,作者自己');
INSERT INTO `tbl_user` VALUES ('2', 'brucelee', '5d5c735291a524c80c53ff669d2cde1b', '78d92ba9477b3661bc8be4bd2e8dd8c0', '1', '龍的傳人');
INSERT INTO `tbl_user` VALUES ('3', 'zhangsan', '5d5c735291a524c80c53ff669d2cde1b', '78d92ba9477b3661bc8be4bd2e8dd8c0', '1', '張三');
INSERT INTO `tbl_user` VALUES ('4', 'lisi', '5d5c735291a524c80c53ff669d2cde1b', '78d92ba9477b3661bc8be4bd2e8dd8c0', '1', '李四');
INSERT INTO `tbl_user` VALUES ('5', 'jiraya', '5d5c735291a524c80c53ff669d2cde1b', '78d92ba9477b3661bc8be4bd2e8dd8c0', '1', '自來也');
View Code

    mapper接口:UserMapper.java

package com.lee.shiro.mapper;

import com.lee.shiro.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface UserMapper {

    /**
     * 根據用戶名獲取用戶
     * @param username
     * @return
     */
    User findUserByUsername(@Param("username") String username);
}
View Code

    UserMapper.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lee.shiro.mapper.UserMapper">
    <select id="findUserByUsername" resultType="User">
        SELECT
            id,username,password,salt,state,description
        FROM
            tbl_user
        WHERE username=#{username}
    </select>
</mapper>
View Code

    service接口:IUserService.java

package com.lee.shiro.service;

import com.lee.shiro.entity.User;

public interface IUserService {

    /**
     * 根據用戶名獲取用戶
     * @param username
     * @return
     */
    User findUserByUsername(String username);
}
View Code

    service實現:UserServiceImpl.java

package com.lee.shiro.service.impl;

import com.lee.shiro.entity.User;
import com.lee.shiro.mapper.UserMapper;
import com.lee.shiro.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserServiceImpl implements IUserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public User findUserByUsername(String username) {
        User user = userMapper.findUserByUsername(username);
        return user;
    }

}
View Code

    啟動類:ShiroApplication.java

package com.lee.shiro;

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

@SpringBootApplication
public class ShiroApplication {

    public static void main(String[] args) {
        SpringApplication.run(ShiroApplication.class, args);
    }
}
View Code

    測試類:MybatisTest.java

package com.lee.shiro.test;

import com.lee.shiro.ShiroApplication;
import com.lee.shiro.entity.User;
import com.lee.shiro.service.IUserService;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = ShiroApplication.class)
public class MybatisTest {

    @Autowired
    private IUserService userService;

    @Test
    public void testFindUserByUsername() {
        User user = userService.findUserByUsername("brucelee");
        Assert.assertEquals(user.getDescription(), "龍的傳人");
    }
}
View Code

     測試用例順利通過,則表示mybatis集成成功

       開啟logback日志

    其實上面的pom配置已經引入了日志依賴,如圖:

    但是你會發現,spring-boot-starter-logging引入了3種類型的日志,你用其中任何一種都能正常打印日志;但是我們需要用3種嗎?根本用不到,我們只要用一種即可,至於選用那種,全憑大家自己的喜歡;我了,比較喜歡logback(接觸的項目中用的比較多,說白了就是這3種中最熟悉的把);我們來改下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>

    <groupId>com.lee</groupId>
    <artifactId>spring-boot-shiro</artifactId>
    <version>1.0-SNAPSHOT</version>

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

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.3.RELEASE</version>
    </parent>

    <dependencies>

        <!-- mybatis相關 -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.5</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- 日志 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
            <exclusions>                    <!-- 剔除spring-boot-starter-logging中的全部依賴 -->
                <exclusion>
                    <groupId>*</groupId>
                    <artifactId>*</artifactId>
                </exclusion>
            </exclusions>
            <scope>test</scope>             <!-- package或install的時候,spring-boot-starter-logging.jar也不會打進去 -->
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
        </dependency>

        <!-- test -->
        <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>
View Code

        logback.xml:

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
  <!--定義日志文件的存儲地址 勿在 LogBack 的配置中使用相對路徑 -->
  <property name="LOG_HOME" value="/log" />
  <!-- 控制台輸出 -->
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
      <pattern>%d{yyyy-MM-dd HH:mm:ss} |%logger| |%level|%msg%n</pattern>
    </encoder>
  </appender>
  <!-- 按照每天生成日志文件 -->
  <appender name="FILE"
            class="ch.qos.logback.core.rolling.RollingFileAppender">
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <!--日志文件輸出的文件名 -->
      <FileNamePattern>${LOG_HOME}/spring-boot-shiro.log.%d{yyyy-MM-dd}.log</FileNamePattern>
      <!--日志文件保留天數 -->
      <MaxHistory>30</MaxHistory>
    </rollingPolicy>
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
      <!--格式化輸出:%d表示日期,%thread表示線程名,%-5level:級別從左顯示5個字符寬度%msg:日志消息,%n是換行符 -->
      <pattern>%d{yyyy-MM-dd HH:mm:ss} |%logger| |%level|%msg%n</pattern>
    </encoder>
    <!--日志文件最大的大小 -->
    <triggeringPolicy
            class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
      <MaxFileSize>10MB</MaxFileSize>
    </triggeringPolicy>
  </appender>

  <!-- 日志輸出級別 -->
  <root level="INFO">
    <appender-ref ref="STDOUT" />
    <appender-ref ref="FILE" />
  </root>
</configuration>
View Code

  開啟web功能

    在pom.xml中加入web依賴和thymeleaf依賴:

<?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>com.lee</groupId>
    <artifactId>spring-boot-shiro</artifactId>
    <version>1.0-SNAPSHOT</version>

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

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.3.RELEASE</version>
    </parent>

    <dependencies>

        <!-- mybatis相關 -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.5</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- 日志 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
            <exclusions>                    <!-- 剔除spring-boot-starter-logging中的全部依賴 -->
                <exclusion>
                    <groupId>*</groupId>
                    <artifactId>*</artifactId>
                </exclusion>
            </exclusions>
            <scope>test</scope>             <!-- package或install的時候,spring-boot-starter-logging.jar也不會打進去 -->
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
        </dependency>

        <!-- web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!-- test -->
        <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>
View Code

    application.yml中加入端口配置:

server:
  port: 8881
spring:
  #連接池配置
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://localhost:3306/spring-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8
      username: root
      password: 123456
      initial-size: 1                     #連接池初始大小
      max-active: 20                      #連接池中最大的活躍連接數
      min-idle: 1                         #連接池中最小的活躍連接數
      max-wait: 60000                     #配置獲取連接等待超時的時間
      pool-prepared-statements: true    #打開PSCache,並且指定每個連接上PSCache的大小
      max-pool-prepared-statement-per-connection-size: 20
      validation-query: SELECT 1 FROM DUAL
      validation-query-timeout: 30000
      test-on-borrow: false             #是否在獲得連接后檢測其可用性
      test-on-return: false             #是否在連接放回連接池后檢測其可用性
      test-while-idle: true             #是否在連接空閑一段時間后檢測其可用性
#mybatis配置
mybatis:
  type-aliases-package: com.lee.shiro.entity
  #config-location: classpath:mybatis/mybatis-config.xml
  mapper-locations: classpath:mybatis/mapper/*.xml
# pagehelper配置
pagehelper:
  helperDialect: mysql
  #分頁合理化,pageNum<=0則查詢第一頁的記錄;pageNum大於總頁數,則查詢最后一頁的記錄
  reasonable: true
  supportMethodsArguments: true
  params: count=countSql
View Code

    加入controller,處理web請求,具體代碼參考:spring-boot-shiro

    用post測試下,出現下圖,表示web開啟成功

   配置druid監控后台

      可配可不配,但是建議配置上,它能提供很多監控信息,對排查問題非常有幫助,配置好后,界面如下

      提供的內容還是非常多的,更多的druid配置大家可以查看druid官網

          druid配置只需要在application.yml中加入druid配置,同時在config目錄下加上DruidConfig.java配置文件即可,具體內容可參考:spring-boot-shiro

  集成shiro,並用redis實現shiro緩存

    集成shiro非常簡單,我們只需要將用戶、權限信息傳給shiro即可。表結構信息:

DROP TABLE IF EXISTS `tbl_user`;
CREATE TABLE `tbl_user` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  `username` varchar(50) NOT NULL COMMENT '名稱',
  `password` char(32) NOT NULL COMMENT '密碼',
  `salt` char(32) NOT NULL COMMENT '鹽,用於加密',
  `state` tinyint(2) NOT NULL DEFAULT '1' COMMENT '狀態, 1:可用, 0:不可用',
  `description` varchar(50) DEFAULT '' COMMENT '描述',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用戶表';

-- ----------------------------
-- Records of tbl_user
-- ----------------------------
INSERT INTO `tbl_user` VALUES ('1', 'admin', 'd3c59d25033dbf980d29554025c23a75', '8d78869f470951332959580424d4bf4f', '1', 'bing,作者自己');
INSERT INTO `tbl_user` VALUES ('2', 'brucelee', '5d5c735291a524c80c53ff669d2cde1b', '78d92ba9477b3661bc8be4bd2e8dd8c0', '1', '龍的傳人');
INSERT INTO `tbl_user` VALUES ('3', 'zhangsan', 'b8432e3a2a5adc908bd4ff22ba1f2d65', '78d92ba9477b3661bc8be4bd2e8dd8c0', '1', '張三');
INSERT INTO `tbl_user` VALUES ('4', 'lisi', '1fdda90367c23a1f1230eb202104270a', '78d92ba9477b3661bc8be4bd2e8dd8c0', '1', '李四');
INSERT INTO `tbl_user` VALUES ('5', 'jiraya', 'e7c5afb5e2fe7da78641721f2c5aad82', '78d92ba9477b3661bc8be4bd2e8dd8c0', '1', '自來也');

-- ----------------------------
-- Table structure for `tbl_user_role`
-- ----------------------------
DROP TABLE IF EXISTS `tbl_user_role`;
CREATE TABLE `tbl_user_role` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  `user_id` int(10) unsigned NOT NULL COMMENT '用戶id',
  `role_id` int(10) unsigned NOT NULL COMMENT '角色id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用戶角色表';

-- ----------------------------
-- Records of tbl_user_role
-- ----------------------------
INSERT INTO `tbl_user_role` VALUES ('1', '1', '1');
INSERT INTO `tbl_user_role` VALUES ('2', '2', '4');

-- ----------------------------
-- Table structure for `tbl_permission`
-- ----------------------------
DROP TABLE IF EXISTS `tbl_permission`;
CREATE TABLE `tbl_permission` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  `name` varchar(50) NOT NULL COMMENT '名稱',
  `permission` varchar(50) NOT NULL COMMENT '權限',
  `url` varchar(50) NOT NULL COMMENT 'url',
  `description` varchar(50) DEFAULT '' COMMENT '描述',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='權限表';

-- ----------------------------
-- Records of tbl_permission
-- ----------------------------
INSERT INTO `tbl_permission` VALUES ('1', '用戶列表', 'user:view', 'user/userList', '用戶列表');
INSERT INTO `tbl_permission` VALUES ('2', '用戶添加', 'user:add', 'user/userAdd', '用戶添加');
INSERT INTO `tbl_permission` VALUES ('3', '用戶刪除', 'user:del', 'user/userDel', '用戶刪除');

-- ----------------------------
-- Table structure for `tbl_role`
-- ----------------------------
DROP TABLE IF EXISTS `tbl_role`;
CREATE TABLE `tbl_role` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  `name` varchar(50) NOT NULL COMMENT '名稱',
  `description` varchar(50) DEFAULT '' COMMENT '描述',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';

-- ----------------------------
-- Records of tbl_role
-- ----------------------------
INSERT INTO `tbl_role` VALUES ('1', '超級管理員', '擁有全部權限');
INSERT INTO `tbl_role` VALUES ('2', '角色管理員', '擁有全部查看權限,以及角色的增刪改權限');
INSERT INTO `tbl_role` VALUES ('3', '權限管理員', '擁有全部查看權限,以及權限的增刪改權限');
INSERT INTO `tbl_role` VALUES ('4', '用戶管理員', '擁有全部查看權限,以及用戶的增刪改權限');
INSERT INTO `tbl_role` VALUES ('5', '審核管理員', '擁有全部查看權限,以及審核的權限');

-- ----------------------------
-- Table structure for `tbl_role_permission`
-- ----------------------------
DROP TABLE IF EXISTS `tbl_role_permission`;
CREATE TABLE `tbl_role_permission` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  `role_id` int(10) unsigned NOT NULL COMMENT '角色id',
  `permission_id` int(10) unsigned NOT NULL COMMENT '權限id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色權限表';

-- ----------------------------
-- Records of tbl_role_permission
-- ----------------------------
INSERT INTO `tbl_role_permission` VALUES ('1', '1', '1');
INSERT INTO `tbl_role_permission` VALUES ('2', '1', '2');
INSERT INTO `tbl_role_permission` VALUES ('3', '1', '3');
INSERT INTO `tbl_role_permission` VALUES ('4', '4', '1');
INSERT INTO `tbl_role_permission` VALUES ('5', '4', '2');
INSERT INTO `tbl_role_permission` VALUES ('6', '4', '3');
View Code

    實現role、permission的mapper(user的在之前已經實現了),然后將用戶信息、權限信息注入到shiro的realm中即可,ShiroConfig.java:

package com.lee.shiro.config;

import com.lee.shiro.entity.Role;
import com.lee.shiro.entity.User;
import com.lee.shiro.mapper.PermissionMapper;
import com.lee.shiro.mapper.RoleMapper;
import com.lee.shiro.service.IUserService;
import com.lee.shiro.util.ByteSourceUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

@Configuration
public class ShiroConfig {

    private static final Logger LOGGER = LoggerFactory.getLogger(ShiroConfig.class);

    @Autowired
    private IUserService userService;
    @Autowired
    private RoleMapper roleMapper;
    @Autowired
    private PermissionMapper permissionMapper;

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {

        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/logout", "logout");
        filterChainDefinitionMap.put("/favicon.ico", "anon");
        filterChainDefinitionMap.put("/druid/**", "anon");              // druid登錄交給druid自己
        filterChainDefinitionMap.put("/**", "authc");
        //authc表示需要驗證身份才能訪問,還有一些比如anon表示不需要驗證身份就能訪問等。
        shiroFilterFactoryBean.setLoginUrl("/login");
        shiroFilterFactoryBean.setSuccessUrl("/index");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    @Bean
    public SecurityManager securityManager(AuthorizingRealm myShiroRealm, CacheManager shiroRedisCacheManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setCacheManager(shiroRedisCacheManager);
        securityManager.setRememberMeManager(cookieRememberMeManager());
        securityManager.setRealm(myShiroRealm);
        return securityManager;
    }

    @Bean
    public AuthorizingRealm myShiroRealm(HashedCredentialsMatcher hashedCredentialsMatcher) {
        AuthorizingRealm  myShiroRealm = new AuthorizingRealm() {

            @Override
            protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
                LOGGER.info("認證 --> MyShiroRealm.doGetAuthenticationInfo()");
                //獲取用戶的輸入的賬號.
                String username = (String)token.getPrincipal();
                LOGGER.info("界面輸入的用戶名:{}", username);
                //通過username從數據庫中查找 User對象,
                User user = userService.findUserByUsername(username);
                if(user == null){
                    //沒有返回登錄用戶名對應的SimpleAuthenticationInfo對象時,就會在LoginController中拋出UnknownAccountException異常
                    return null;
                }
                SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                        user, //用戶名
                        user.getPassword(), //密碼
                        ByteSourceUtils.bytes(user.getCredentialsSalt()),//salt=username+salt
                        getName()  //realm name
                );
                return authenticationInfo;
            }

            @Override
            protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
                LOGGER.info("權限配置 --> MyShiroRealm.doGetAuthorizationInfo()");

                SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
                User user  = (User)principal.getPrimaryPrincipal();
                List<Role> roles = roleMapper.findRoleByUsername(user.getUsername());
                LOGGER.info("用戶:{}, 角色有{}個", user.getUsername(), roles.size());
                roles.stream().forEach(
                        role -> {
                            authorizationInfo.addRole(role.getName());
                            permissionMapper.findPermissionByRoleId(role.getId()).stream().forEach(
                                    permission -> {
                                        authorizationInfo.addStringPermission(permission.getPermission());
                                    }
                            );
                        }
                );
                return authorizationInfo;
            }
        };
        myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher); //設置加密規則
        myShiroRealm.setCachingEnabled(true);
        myShiroRealm.setAuthorizationCachingEnabled(true);
        myShiroRealm.setAuthenticationCachingEnabled(true);
        return myShiroRealm;
    }

    // 需要與存儲密碼時的加密規則一致
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:這里使用MD5算法;
        hashedCredentialsMatcher.setHashIterations(2);//散列的次數,比如散列兩次,相當於 md5(md5(""));
        return hashedCredentialsMatcher;
    }

    /**
     * DefaultAdvisorAutoProxyCreator,Spring的一個bean,由Advisor決定對哪些類的方法進行AOP代理<
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
        proxyCreator.setProxyTargetClass(true);
        return proxyCreator;
    }

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

    @Bean
    public SimpleMappingExceptionResolver resolver() {
        SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver();
        Properties properties = new Properties();
        properties.setProperty("UnauthorizedException", "/403");
        exceptionResolver.setExceptionMappings(properties);
        return exceptionResolver;
    }

    //cookie對象;
    @Bean
    public SimpleCookie rememberMeCookie() {
        LOGGER.info("ShiroConfiguration.rememberMeCookie()");
        //這個參數是cookie的名稱,對應前端的checkbox的name = rememberMe
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");

        //<!-- 記住我cookie生效時間 ,單位秒;-->
        simpleCookie.setMaxAge(60);
        return simpleCookie;
    }

    //cookie管理對象;
    @Bean
    public CookieRememberMeManager cookieRememberMeManager() {
        LOGGER.info("ShiroConfiguration.rememberMeManager()");
        CookieRememberMeManager manager = new CookieRememberMeManager();
        manager.setCookie(rememberMeCookie());
        return manager;
    }

}
View Code

    shiro的緩存也是提供的接口,我們實現該接口即可接入我們自己的緩存實現,至於具體的緩存實現是redis、memcache還是其他的,shiro並不關心;而本文用redis實現shiro的緩存。采用spring的redisTemplate來操作redis,具體的實現,如下

    ShiroRedisCacheManager:

package com.lee.shiro.config;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class ShiroRedisCacheManager implements CacheManager {

    @Autowired
    private Cache shiroRedisCache;

    @Override
    public <K, V> Cache<K, V> getCache(String s) throws CacheException {
        return shiroRedisCache;
    }
}
View Code

    ShiroRedisCache:

package com.lee.shiro.config;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Component
public class ShiroRedisCache<K,V> implements Cache<K,V>{

    @Autowired
    private RedisTemplate<K,V> redisTemplate;

    @Value("${spring.redis.expireTime}")
    private long expireTime;

    @Override
    public V get(K k) throws CacheException {
        return redisTemplate.opsForValue().get(k);
    }

    @Override
    public V put(K k, V v) throws CacheException {
        redisTemplate.opsForValue().set(k,v,expireTime, TimeUnit.SECONDS);
        return null;
    }

    @Override
    public V remove(K k) throws CacheException {
        V v = redisTemplate.opsForValue().get(k);
        redisTemplate.opsForValue().getOperations().delete(k);
        return v;
    }

    @Override
    public void clear() throws CacheException {
    }

    @Override
    public int size() {
        return 0;
    }

    @Override
    public Set<K> keys() {
        return null;
    }

    @Override
    public Collection<V> values() {
        return null;
    }
}
View Code

    更詳細、完整的代碼請參考spring-boot-shiro,上文的緩存只是針對realm緩存,也就是權限相關的,至於其他緩存像session緩存,大家可以自行去實現。

效果展示

  經過上述的步驟,工程已經搭建完畢我們來驗證下效果

  druid后台監控

    如下圖

    在shiro配置中,我們放行了/druid/**,所以druid后台的地址都沒有被攔截,druid相關的由druid自己控制,不受shiro的影響。

  shiro權限控制

    由spring-boot-shiro.sql、UserController.java可知,5個用戶中只有admin和brucelee有/user/userList、/user/userAdd、/user/userDel的訪問權限,而/user/findUserByUsername沒做權限限制,那么5個用戶都可以訪問;但是登錄是必須的(5個用戶的密碼都是123456);效果如下:

    上圖中展示了zhangsan用戶和admin權限訪問的情況,完全按照我們設想的劇本走的,剩下的用戶大家可以自己去測試;另外還可以多設置一些權限來進行驗證。

  預祝大家搭建成功,如果有什么問題,可以@我,或者直接和我的代碼進行比較,找出其中的問題。

疑問與解答

  1、我不修改日志依賴,但是我只用其中的某種日志打印日志不就行了,不會沖突也能正常打日志,為什么要修改日志依賴?

    說的沒錯,你不修改依賴也能正常工作,還不用書寫更多的pom配置;但是你仔細去觀察的話,你會發現你工程打包出來的時候,這些依賴的日志jar包全在包中,項目部署的時候,這些jar都會加載到內存中的,你沒用到的日志jar也會加載到內存中,數量少、jar包小還能接受,一旦無用的jar包數量多、jar文件太大,那可想而知會浪費多少內存資源;內存資源不比磁盤,是比較稀有的。

    強烈建議把無用的依賴剔除掉,既能節省資源、也能避免未知的一些錯誤。

  2、日志依賴:為什么按文中的配置就能只依賴logback了

    maven的依賴有兩個原則:最短路徑原則、最先聲明原則;以我們的pom.xml為起點,那我們自定義的spring-boot-starter-logging依賴路徑肯定最短了,那么maven就會選用我們自定義的spring-boot-starter-logging,所以就把spring-boot-starter-logging的依賴全部剔除了,而<scope>test<scope>,大家都懂的;至於最先聲明原則,也就說在路徑相同的情況下,誰在前聲明就依賴誰。

  3、遇到的一個坑,認證通過后,為什么授權回調沒有被調用

    首先要明白,認證與授權觸發的時間點是不同的,登錄觸發認證,但是登錄成功后不會立即觸發授權的;授權是有權限校驗的時候才觸發的;大家請看下圖

    登錄只是觸發了認證、當有權限校驗的時候才會授權(角色校驗的時候也會),第一次權限校驗請求數據庫,數據會緩存到redis中,下次權限校驗的時候就從緩存中獲取,而不用再從數據庫獲取了。

    另外shiro注解生效是配置兩個bean的,defaultAdvisorAutoProxyCreator和authorizationAttributeSourceAdvisor,我在這個問題上卡了一段時間;只配置authorizationAttributeSourceAdvisor沒用,代理沒打開,shiro注解的代理類就不會生成,注解配置了相當於沒配置,這里需要大家注意。

 

參考

  《跟我學Shiro》


免責聲明!

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



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