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

前言

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

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

       Apache Shiro是Java的一個安全框架。目前,使用Apache Shiro的人也愈來愈多,由於它至關簡單,對比Spring Security,可能沒有Spring Security的功能強大,可是在實際工做時可能並不須要那麼複雜的東西,因此使用小而簡單的Shiro就足夠了。對於它倆到底哪一個好,這個沒必要糾結,能更簡單的解決項目問題就行了。java

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

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

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

spring-boot整合shiro

       集成mybatis

    Shiro不會去維護用戶、維護權限;這些須要咱們本身去設計/提供,而後經過相應的接口注入給Shiro;既然用戶、權限這些信息須要咱們本身設計、維護,那麼可想而知須要進行數據庫表的設計了(具體表結構看後文),既然涉及到數據庫的操做,那麼咱們就先整合mybatis,實現數據庫的操做。web

    pom.xml:redis

<?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:spring

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權限訪問的狀況,徹底按照咱們設想的劇本走的,剩下的用戶你們能夠本身去測試;另外還能夠多設置一些權限來進行驗證。

  預祝你們搭建成功,若是有什麼問題,能夠@我,或者直接和個人代碼進行比較,找出其中的問題。

疑問與解答

  一、我不修改日誌依賴,可是我只用其中的某種日誌打印日誌不就好了,不會衝突也能正常打日誌,爲何要修改日誌依賴?

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

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

  二、日誌依賴:爲何按文中的配置就能只依賴logback了

    maven的依賴有兩個原則:最短路徑原則、最早聲明原則;以咱們的pom.xml爲起點,那咱們自定義的spring-boot-starter-logging依賴路徑確定最短了,那麼maven就會選用咱們自定義的spring-boot-starter-logging,因此就把spring-boot-starter-logging的依賴所有剔除了,而<scope>test<scope>,你們都懂的;至於最早聲明原則,也就說在路徑相同的狀況下,誰在前聲明就依賴誰。

  三、遇到的一個坑,認證經過後,爲何受權回調沒有被調用

    首先要明白,認證與受權觸發的時間點是不一樣的,登陸觸發認證,可是登陸成功後不會當即觸發受權的;受權是有權限校驗的時候才觸發的;你們請看下圖

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

    另外shiro註解生效是配置兩個bean的,defaultAdvisorAutoProxyCreator和authorizationAttributeSourceAdvisor,我在這個問題上卡了一段時間;只配置authorizationAttributeSourceAdvisor沒用,代理沒打開,shiro註解的代理類就不會生成,註解配置了至關於沒配置,這裏須要你們注意。

 

參考

  《跟我學Shiro》

相關文章
相關標籤/搜索