上一篇: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
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>
配置文件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: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', '自來也');
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集成成功
其實上面的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>
在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配置只須要在application.yml中加入druid配置,同時在config目錄下加上DruidConfig.java配置文件便可,具體內容可參考:spring-boot-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緩存,你們能夠自行去實現。
通過上述的步驟,工程已經搭建完畢咱們來驗證下效果
以下圖
在shiro配置中,咱們放行了/druid/**,因此druid後臺的地址都沒有被攔截,druid相關的由druid本身控制,不受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》