前言
上一篇: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>
配置文件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
在數據庫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', '自來也');
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); }
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>
service接口:IUserService.java

package com.lee.shiro.service; import com.lee.shiro.entity.User; public interface IUserService { /** * 根據用戶名獲取用戶 * @param username * @return */ User findUserByUsername(String username); }
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; } }
啟動類: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); } }
測試類: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(), "龍的傳人"); } }
測試用例順利通過,則表示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>
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>
開啟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>
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
加入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');
實現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; } }
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; } }
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; } }
更詳細、完整的代碼請參考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》