javascript
SpringBoot集成Shiro前后端分离使用redis做缓存
文章目錄
- 一 、shiro介紹
- 1、基礎介紹
- 2、基本功能點
- 3、基本流程圖
- 二、 常用的權限管理表關系
- 2.1. 表組成
- 2.2. 表結構
- 三、實戰案例
- 3.1. 案例介紹
- 3.2. 依賴
- 3.3. Shiro全局配置
- 3.4. 自定義ShiroRealm
- 3.5. ShiroUtils
- 3.6. 自定義SessionManager
- 3.7. 登錄/出主方法
- 3.8. 測試主方法
- 四、 前后端分離需要注意的點
- 五、測試驗鏈接
- 六、測試驗證
- 6.1. 登錄測試
- 6.1. 1. 鏈接
- 6.1. 2. 參數
- 6.1. 3. 登陸后的token
- 6.2. 獲取用戶列表
- 6.2. 1. 鏈接
- 6.2. 2. 參數
- 6.3. 獲取用戶詳情
- 6.3. 1. 鏈接
- 6.3. 2. 參數
- 6.4. 添加用戶
- 6.4. 1. 鏈接
- 6.4. 2. 參數
- 6.5. 刪除用戶
- 6.5. 1. 鏈接
- 6.5. 2. 參數
- 6.6. 注銷登錄
- 6.6. 1. 鏈接
- 6.6. 2. 參數
技術選型
| 環境 | JDK | 1.8 |
| 后臺 | SpringBoot | 2.1.7.RELEASE |
| 權限控制 | Shiro | 1.4.0 |
| 數據庫 | Mysql | 8.0.17 |
| 數據源 | druid | 1.1.10 |
| 持久層 | mybatis | 1.3.2 |
| 緩存 | shiro-redis | 3.1.0 |
一 、shiro介紹
1、基礎介紹
Apache Shiro是一個強大且易用的Java安全框架,執行身份驗證、授權、密碼和會話管理。作為一款安全框架Shiro的設計相當巧妙。Shiro的應用不依賴任何容器,它不僅可以在JavaEE下使用,還可以應用在JavaSE環境中。
2、基本功能點
1、Authentication:身份認證/登錄,驗證用戶是不是擁有相應的身份。
2、Authorization:授權,即權限驗證,驗證某個已認證的用戶是否擁有某個權限;即判斷用戶是否能做事情,常見的如:驗證某個用戶是否擁有某個角色。或者細粒度的驗證某個用戶對某個資源是否具有某個權限。
3、Session Manager:會話管理,即用戶登錄后就是一次會話,在沒有退出之前,它的所有信息都在會話中;會話可以是普通JavaSE環境的,也可以是如Web環境的。
4、Cryptography:加密,保護數據的安全性,如密碼加密存儲到數據庫,而不是明文存儲。
5、Web Support:Web支持,可以非常容易的集成到Web環境。
6、Caching:緩存,比如用戶登錄后,其用戶信息、擁有的角色/權限不必每次去查,這樣可以提高效率。
7、Concurrency:shiro支持多線程應用的并發驗證,即如在一個線程中開啟另一個線程,能把權限自動傳播過去。
8、Testing:提供測試支持。
9、Run As:允許一個用戶假裝為另一個用戶(如果他們允許)的身份進行訪問。
10、Remember Me:記住我,這個是非常常見的功能,即一次登錄后,下次再來的話不用登錄了。
3、基本流程圖
Subject:主體,代表了當前“用戶”,這個用戶不一定是一個具體的人,與當前應用交互的任何東西都是Subject,如網絡爬蟲,機器人等;即一個抽象概念;所有Subject都綁定到SecurityManager,與Subject的所有交互都會委托給SecurityManager;可以把Subject認為是一個門面;SecurityManager才是實際的執行者。
SecurityManager:安全管理器;即所有與安全有關的操作都會與SecurityManager交互;且它管理著所有Subject;可以看出它是Shiro的核心,它負責與后邊介紹的其他組件進行交互,如果學習過SpringMVC,你可以把它看成DispatcherServlet前端控制器。
Realm:域,Shiro從從Realm獲取安全數據(如用戶、角色、權限),就是說SecurityManager要驗證用戶身份,那么它需要從Realm獲取相應的用戶進行比較以確定用戶身份是否合法;也需要從Realm得到用戶相應的角色/權限進行驗證用戶是否能進行操作;可以把Realm看成DataSource,即安全數據源。
流程如下:步驟一:Shiro把用戶的數據封裝成標識token,token一般封裝著用戶名,密碼等信息。步驟二:使用Subject門面獲取到封裝著用戶的數據的標識token步驟三:Subject把標識token交給SecurityManager,在SecurityManager安全中心中,SecurityManager把標識token委托給認證器Authenticator進行身份驗證。認證器的作用一般是用來指定如何驗證,它規定本次認證用到哪些Realm。步驟四:認證器Authenticator將傳入的標識token,與數據源Realm對比,驗證token是否合法。二、 常用的權限管理表關系
2.1. 表組成
- 5張表,也就是現在流行的權限設計模型RBAC
分別是:用戶表 ,角色表,菜單(權限)表 , 用戶和角色關聯表,角色和菜單關聯表
2.2. 表結構
-- 表結構總覽-- -- 權限表-- DROP TABLE IF EXISTS `menu`;CREATE TABLE `menu` (`menu_id` bigint(20) NOT NULL AUTO_INCREMENT,`parent_id` bigint(20) DEFAULT NULL COMMENT '父菜單ID,一級菜單為0',`name` varchar(50) DEFAULT NULL COMMENT '菜單名稱',`url` varchar(200) DEFAULT NULL COMMENT '菜單URL',`perms` varchar(500) DEFAULT NULL COMMENT '授權(多個用逗號分隔,如:user:list,user:create)',`type` int(11) DEFAULT NULL COMMENT '類型 0:目錄 1:菜單 2:按鈕',`icon` varchar(50) DEFAULT NULL COMMENT '菜單圖標',`order_num` int(11) DEFAULT NULL COMMENT '排序',`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',PRIMARY KEY (`menu_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='菜單管理';LOCK TABLES `menu` WRITE;INSERT INTO `menu` (`menu_id`, `parent_id`, `name`, `url`, `perms`, `type`, `icon`, `order_num`, `create_time`, `update_time`) VALUES(1,0,'權限菜單','menu/list','menu:list',0,'system',0,'2019-08-30 03:06:59','2019-08-30 07:38:01'),(2,0,'用戶列表','user/list','user:list',0,'user',0,'2019-08-30 07:38:46','2019-08-30 07:38:56'),(3,0,'用戶詳情','user/detail','user:detail',0,'user',0,'2019-08-30 07:38:52','2019-08-30 07:39:43'),(4,0,'添加用戶','user/add','user:add',0,'user',0,'2019-08-30 07:38:52','2019-08-30 07:42:54');UNLOCK TABLES;-- 角色表表-- DROP TABLE IF EXISTS `role`;CREATE TABLE `role` (`role_id` bigint(20) NOT NULL AUTO_INCREMENT,`role_name` varchar(100) DEFAULT NULL COMMENT '角色名稱',`remark` varchar(100) DEFAULT NULL COMMENT '備注',`create_user_id` bigint(20) DEFAULT NULL COMMENT '創建者ID',`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',PRIMARY KEY (`role_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色';LOCK TABLES `role` WRITE;INSERT INTO `role` (`role_id`, `role_name`, `remark`, `create_user_id`, `create_time`, `update_time`) VALUES (1,'admin','超級管理員',1,'2019-08-30 07:41:08','2019-08-30 07:41:08');UNLOCK TABLES;-- 角色和權限的關系表-- DROP TABLE IF EXISTS `role_menu`;CREATE TABLE `role_menu` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`role_id` bigint(20) DEFAULT NULL COMMENT '角色ID',`menu_id` bigint(20) DEFAULT NULL COMMENT '菜單ID',`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色與菜單對應關系';LOCK TABLES `role_menu` WRITE;INSERT INTO `role_menu` (`id`, `role_id`, `menu_id`, `create_time`, `update_time`) VALUES(1,1,1,'2019-08-30 07:41:16','2019-08-30 07:41:16'),(3,1,3,'2019-08-30 07:41:28','2019-08-30 07:41:28'),(4,1,4,'2019-08-30 07:46:14','2019-08-30 07:46:14');UNLOCK TABLES;-- 用戶表-- DROP TABLE IF EXISTS `user`;CREATE TABLE `user` (`user_id` bigint(20) NOT NULL AUTO_INCREMENT,`username` varchar(50) NOT NULL COMMENT '用戶名',`password` varchar(100) DEFAULT NULL COMMENT '密碼',`salt` varchar(20) DEFAULT NULL COMMENT '鹽',`email` varchar(100) DEFAULT NULL COMMENT '郵箱',`mobile` varchar(100) DEFAULT NULL COMMENT '手機號',`name` varchar(100) DEFAULT NULL COMMENT '姓名',`status` tinyint(4) DEFAULT NULL COMMENT '狀態 0:禁用 1:正常',`create_user_id` bigint(20) DEFAULT NULL COMMENT '創建者ID',`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',PRIMARY KEY (`user_id`),UNIQUE KEY `username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='系統用戶';LOCK TABLES `user` WRITE;INSERT INTO `user` (`user_id`, `username`, `password`, `salt`, `email`, `mobile`, `name`, `status`, `create_user_id`, `create_time`, `update_time`) VALUES(1,'admin','3743a4c09a17e6f2829febd09ca54e627810001cf255ddcae9dabd288a949c4a','123','123@gblfy.com','18967835678',NULL,1,1,'2019-01-18 11:11:11','2019-01-18 11:11:11');UNLOCK TABLES;-- 用戶和角色關系表 DROP TABLE IF EXISTS `user_role`;CREATE TABLE `user_role` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`user_id` bigint(20) DEFAULT NULL COMMENT '用戶ID',`role_id` bigint(20) DEFAULT NULL COMMENT '角色ID',`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用戶與角色對應關系';LOCK TABLES `user_role` WRITE;INSERT INTO `user_role` (`id`, `user_id`, `role_id`, `create_time`, `update_time`) VALUES(1,1,1,'2019-08-30 07:40:51','2019-08-30 07:40:51');UNLOCK TABLES;注:建表語句在項目中
三、實戰案例
3.1. 案例介紹
3.2. 依賴
<!--springboot web啟動器--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--mybatis--><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>${mybatis.version}</version></dependency><!--數據源--><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>${druid.version}</version></dependency><!--數據庫--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!--lombok插件--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!-- shiro核心包 --><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>${shiro-spring.version}</version></dependency><!--大神的開源插件--><dependency><groupId>org.crazycake</groupId><artifactId>shiro-redis</artifactId><version>${shiro-redis.version}</version></dependency>3.3. Shiro全局配置
package com.gblfy.shiro.config;import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.servlet.SimpleCookie; import org.crazycake.shiro.RedisCacheManager; import org.crazycake.shiro.RedisManager; import org.crazycake.shiro.RedisSessionDAO; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;import java.time.Duration; import java.util.LinkedHashMap; import java.util.Map;/*** @Author: http://gblfy.com* @Version 1.0.0*/ @Configuration @Slf4j @Data @ConfigurationProperties(prefix = "spring.redis") public class ShiroConfig {private String host = "localhost";private int port = 6379;private Duration timeout;/*** Filter工廠,設置對應的過濾條件和跳轉條件** @return ShiroFilterFactoryBean*/@Beanpublic ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();shiroFilterFactoryBean.setSecurityManager(securityManager);// 過濾器鏈定義映射Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();/** anon:所有url都都可以匿名訪問,authc:所有url都必須認證通過才可以訪問;* 過濾鏈定義,從上向下順序執行,authc 應放在 anon 下面* */filterChainDefinitionMap.put("/login", "anon");// 配置不會被攔截的鏈接 順序判斷,如果前端模板采用了thymeleaf,這里不能直接使用 ("/static/**", "anon")來配置匿名訪問,必須配置到每個靜態目錄filterChainDefinitionMap.put("/css/**", "anon");filterChainDefinitionMap.put("/fonts/**", "anon");filterChainDefinitionMap.put("/img/**", "anon");filterChainDefinitionMap.put("/js/**", "anon");filterChainDefinitionMap.put("/html/**", "anon");// 所有url都必須認證通過才可以訪問filterChainDefinitionMap.put("/**", "authc");// 配置退出 過濾器,其中的具體的退出代碼Shiro已經替我們實現了, 位置放在 anon、authc下面filterChainDefinitionMap.put("/logout", "logout");// 如果不設置默認會自動尋找Web工程根目錄下的"/login.jsp"頁面// 配器shirot認登錄累面地址,前后端分離中登錄累面跳轉應由前端路由控制,后臺僅返回json數據, 對應LoginController中unauth請求shiroFilterFactoryBean.setLoginUrl("/un_auth");// 登錄成功后要跳轉的鏈接, 此項目是前后端分離,故此行注釋掉,登錄成功之后返回用戶基本信息及token給前端// shiroFilterFactoryBean.setSuccessUrl("/index");// 未授權界面, 對應LoginController中 unauthorized 請求shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);return shiroFilterFactoryBean;}/*** RedisSessionDAO shiro sessionDao層的實現 通過redis, 使用的是shiro-redis開源插件** @return RedisSessionDAO*/@Beanpublic RedisSessionDAO redisSessionDAO() {RedisSessionDAO redisSessionDAO = new RedisSessionDAO();redisSessionDAO.setRedisManager(redisManager());redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());redisSessionDAO.setExpire(1800);return redisSessionDAO;}/*** Session ID 生成器** @return JavaUuidSessionIdGenerator*/@Beanpublic JavaUuidSessionIdGenerator sessionIdGenerator() {return new JavaUuidSessionIdGenerator();}/*** 自定義sessionManager** @return SessionManager*/@Beanpublic SessionManager sessionManager() {MySessionManager mySessionManager = new MySessionManager();mySessionManager.setSessionDAO(redisSessionDAO());return mySessionManager;}/*** 配置shiro redisManager, 使用的是shiro-redis開源插件** @return RedisManager*/private RedisManager redisManager() {RedisManager redisManager = new RedisManager();redisManager.setHost(host);redisManager.setPort(port);redisManager.setTimeout((int) timeout.toMillis());return redisManager;}/*** cacheManager 緩存 redis實現, 使用的是shiro-redis開源插件** @return RedisCacheManager*/@Beanpublic RedisCacheManager cacheManager() {RedisCacheManager redisCacheManager = new RedisCacheManager();redisCacheManager.setRedisManager(redisManager());// 必須要設置主鍵名稱,shiro-redis 插件用過這個緩存用戶信息redisCacheManager.setPrincipalIdFieldName("userId");return redisCacheManager;}/*** 權限管理,配置主要是Realm的管理認證** @return SecurityManager*/@Beanpublic SecurityManager securityManager(ShiroRealm shiroRealm) {DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();securityManager.setRealm(shiroRealm);// 自定義session管理 使用redissecurityManager.setSessionManager(sessionManager());// 自定義緩存實現 使用redissecurityManager.setCacheManager(cacheManager());return securityManager;}/** 開啟Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP掃描使用Shiro注解的類,并在必要時進行安全邏輯驗證* 配置以下兩個bean(DefaultAdvisorAutoProxyCreator(可選)和AuthorizationAttributeSourceAdvisor)即可實現此功能*/@Beanpublic DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();advisorAutoProxyCreator.setProxyTargetClass(true);return advisorAutoProxyCreator;}@Beanpublic AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);return authorizationAttributeSourceAdvisor;}@Beanpublic SimpleCookie cookie() {// cookie的name,對應的默認是 JSESSIONIDSimpleCookie cookie = new SimpleCookie("SHARE_JSESSIONID");cookie.setHttpOnly(true);// path為 / 用于多個系統共享 JSESSIONIDcookie.setPath("/");return cookie;}/* 此項目使用 shiro 場景為前后端分離項目,這里先注釋掉,統一異常處理已在 GlobalExceptionHand.java 中實現 */}3.4. 自定義ShiroRealm
package com.gblfy.shiro.config;import com.gblfy.shiro.entity.Role; import com.gblfy.shiro.util.ShiroUtils; import com.gblfy.shiro.entity.Menu; import com.gblfy.shiro.entity.User; import com.gblfy.shiro.mapper.MenuMapper; import com.gblfy.shiro.mapper.RoleMapper; import com.gblfy.shiro.mapper.UserMapper; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authc.credential.CredentialsMatcher; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component;import java.util.List; import java.util.Objects;/*** @Author: http://gblfy.com* @Version 1.0.0*/ @Slf4j @Component public class ShiroRealm extends AuthorizingRealm {private UserMapper userMapper;private RoleMapper roleMapper;private MenuMapper menuMapper;@Autowired@SuppressWarnings("all")public ShiroRealm(UserMapper userMapper, RoleMapper roleMapper, MenuMapper menuMapper) {this.userMapper = userMapper;this.roleMapper = roleMapper;this.menuMapper = menuMapper;}/*** 授權** @param principalCollection* @return*/@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {log.info("開始執行授權操作.......");SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();/*** 查詢用戶角色* 如果身份認證的時候沒有傳入User對象,這里只能取到userName* 也就是SimpleAuthenticationInfo構造的時候第一個參數傳遞需要User對象*/User user = (User) principalCollection.getPrimaryPrincipal();if (user == null) {log.error("用戶不存在");throw new UnknownAccountException("用戶不存在");}//TODO 是否為超級管理員 是 全部菜單權限/*** 查詢用戶角色*/List<Role> roles = this.roleMapper.listRoleByUserId(user.getUserId());if(CollectionUtils.isNotEmpty(roles)){for (Role role : roles) {authorizationInfo.addRole(role.getRoleName());// 根據角色查詢權限List<Menu> menus = this.menuMapper.listMenuByRoleId(role.getRoleId());for (Menu m : menus) {authorizationInfo.addStringPermission(m.getPerms());}}}return authorizationInfo;}/*** 認證** @param authenticationToken* @return* @throws AuthenticationException*/@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {log.info("開始進行身份認證......");//獲取用戶的輸入的賬號.String username = (String) authenticationToken.getPrincipal();//通過username從數據庫中查找 User對象.//實際項目中,這里可以根據實際情況做緩存,如果不做,Shiro自己也是有時間間隔機制,2分鐘內不會重復執行該方法User user = userMapper.findByUsername(username);if (Objects.isNull(user)) {return null;}return new SimpleAuthenticationInfo(// 這里傳入的是user對象,比對的是用戶名,直接傳入用戶名也沒錯,但是在授權部分就需要自己重新從數據庫里取權限user,// 密碼user.getPassword(),// salt = username + saltByteSource.Util.bytes(user.getSalt()),// realm namegetName());}/*** 將自己的驗證方式加入容器** 憑證匹配器(由于我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了)** @param credentialsMatcher*/@Overridepublic void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();/*** 散列算法:這里可以使用MD5算法 也可以使用SHA-256*/hashedCredentialsMatcher.setHashAlgorithmName(ShiroUtils.hashAlgorithmName);// 散列的次數,比如散列16次,相當于 md5(md5(""));hashedCredentialsMatcher.setHashIterations(ShiroUtils.hashIterations);super.setCredentialsMatcher(hashedCredentialsMatcher);}}3.5. ShiroUtils
package com.gblfy.shiro.util;import com.gblfy.shiro.entity.User; import org.apache.shiro.SecurityUtils; import org.apache.shiro.crypto.hash.SimpleHash; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject;/*** Shiro工具類*/ public class ShiroUtils {/** 加密算法 */public final static String hashAlgorithmName = "SHA-256";/** 循環次數 */public final static int hashIterations = 16;public static String sha256(String password, String salt) {return new SimpleHash(hashAlgorithmName, password, salt, hashIterations).toString();}// 獲取一個測試賬號 adminpublic static void main(String[] args) {// 3743a4c09a17e6f2829febd09ca54e627810001cf255ddcae9dabd288a949c4aSystem.out.println(sha256("admin","123")) ;}/*** 獲取會話*/public static Session getSession() {return SecurityUtils.getSubject().getSession();}/*** Subject:主體,代表了當前“用戶”*/public static Subject getSubject() {return SecurityUtils.getSubject();}public static User getUserEntity() {return (User)SecurityUtils.getSubject().getPrincipal();}public static Long getUserId() {return getUserEntity().getUserId();}public static void setSessionAttribute(Object key, Object value) {getSession().setAttribute(key, value);}public static Object getSessionAttribute(Object key) {return getSession().getAttribute(key);}public static boolean isLogin() {return SecurityUtils.getSubject().getPrincipal() != null;}public static void logout() {SecurityUtils.getSubject().logout();} }3.6. 自定義SessionManager
package com.gblfy.shiro.config;import org.apache.commons.lang3.StringUtils; import org.apache.shiro.web.servlet.ShiroHttpServletRequest; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.apache.shiro.web.util.WebUtils;import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import java.io.Serializable;/**** @Author: http://gblfy.com* @Version 1.0.0** 自定義session管理* <br/>* 傳統結構項目中,shiro從cookie中讀取sessionId以此來維持會話,在前后端分離的項目中(也可在移動APP項目使用),* 我們選擇在ajax的請求頭中傳遞sessionId,因此需要重寫shiro獲取sessionId的方式。* 自定義MySessionManager類繼承DefaultWebSessionManager類,重寫getSessionId方法*/ public class MySessionManager extends DefaultWebSessionManager {private static final String AUTHORIZATION = "Authorization";private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";@Overrideprotected Serializable getSessionId(ServletRequest request, ServletResponse response) {String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);//如果請求頭中有 Authorization 則其值為sessionIdif (!StringUtils.isEmpty(id)) {request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);return id;} else {//否則按默認規則從cookie取sessionIdreturn super.getSessionId(request, response);}}}3.7. 登錄/出主方法
package com.gblfy.shiro.controller;import com.gblfy.shiro.util.CacheUser; import com.gblfy.shiro.util.Response; import com.gblfy.shiro.entity.User; import com.gblfy.shiro.service.UserService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;/*** @author http://gblfy.com* @Description 登錄* @Date 2019/9/14 15:34* @version1.0*/ @Slf4j @RestController public class LoginController {private Response response;private UserService userService;@Autowired@SuppressWarnings("all")public LoginController(Response response, UserService userService) {this.response = response;this.userService = userService;}/*** description: 登錄** @return 登錄結果*/@PostMapping("/login")public Response login(User user) {log.warn("進入登錄.....");String username = user.getUsername();String password = user.getPassword();if (StringUtils.isBlank(username)) {return response.failure("用戶名為空!");}if (StringUtils.isBlank(password)) {return response.failure("密碼為空!");}CacheUser loginUser = userService.login(username, password);// 登錄成功返回用戶信息return response.success("登錄成功!", loginUser);}/*** description: 登出*/@RequestMapping("/logout")public Response logOut() {userService.logout();return response.success("登出成功!");}/*** 未登錄,shiro應重定向到登錄界面,此處返回未登錄狀態信息由前端控制跳轉頁面* @return*/@RequestMapping("/un_auth")public Response unAuth() {return response.failure(HttpStatus.UNAUTHORIZED, "用戶未登錄!", null);}/*** 未授權,無權限,此處返回未授權狀態信息由前端控制跳轉頁面* @return*/@RequestMapping("/unauthorized")public Response unauthorized() {return response.failure(HttpStatus.FORBIDDEN, "用戶無權限!", null);}}3.8. 測試主方法
package com.gblfy.shiro.controller;import com.gblfy.shiro.util.Response; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresRoles; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*;/*** @author http://gblfy.com* @Description 測試主方法* @Date 2019/9/14 15:34* @version1.0*/ @RestController @Slf4j @RequestMapping("user") public class UserController {@Autowiredprivate Response response;@GetMapping("list")@RequiresPermissions("user:list")public Response listUser() {return response.success("用戶列表");}@GetMapping("{userId}")@RequiresPermissions("user:detail")public Response detailUser(@PathVariable("userId") Long userId) {return response.success("用戶詳情");}@PostMapping("add")@RequiresRoles("admin")@RequiresPermissions("user:add")public Response addUser() {return response.success("添加用戶成功");}@DeleteMapping("del")@RequiresRoles("role")public Response delUser() {return response.success("刪除用戶");} }四、 前后端分離需要注意的點
傳統結構項目中,shiro從cookie中讀取sessionId以此來維持會話,在前后端分離的項目中(也可在移動APP項目使用),我們選擇在ajax的請求頭中傳遞sessionId,因此需要重寫shiro獲取sessionId的方式。自定義MySessionManager類繼承DefaultWebSessionManager類,重寫getSessionId方法
登入失敗,登入地址,前后端分離,不應該直接跳轉頁面,而是返回響應結果
五、測試驗鏈接
| 登錄 | POST | localhost:80/login?username=admin&password=admin | 無 |
| 獲取用戶列表 | GET | localhost:80/user/list | Authorization |
| 獲取用戶詳情 | GET | localhost:80/user/1 | Authorization |
| 添加用戶 | POST | localhost:80/user/add | Authorization |
| 刪除用戶 | DELETE | localhost:80/user/del | Authorization |
| 退出登入 查詢redis中數據是否清除緩存信息 | GET | localhost:80/logout | Authorization |
六、測試驗證
6.1. 登錄測試
6.1. 1. 鏈接
localhost:80/login?username=admin&password=admin6.1. 2. 參數
無6.1. 3. 登陸后的token
025e0f13-ba78-47ff-bd2a-3fb6f459f102
6.2. 獲取用戶列表
6.2. 1. 鏈接
localhost:80/user/list6.2. 2. 參數
key:Authorization #登錄后的token values: 025e0f13-ba78-47ff-bd2a-3fb6f459f102
6.3. 獲取用戶詳情
6.3. 1. 鏈接
localhost:80/user/16.3. 2. 參數
key:Authorization #登錄后的token values: 025e0f13-ba78-47ff-bd2a-3fb6f459f102
6.4. 添加用戶
6.4. 1. 鏈接
localhost:80/user/add6.4. 2. 參數
key:Authorization #登錄后的token values: 025e0f13-ba78-47ff-bd2a-3fb6f459f102
6.5. 刪除用戶
6.5. 1. 鏈接
localhost:80/user/del6.5. 2. 參數
key:Authorization #登錄后的token values: 025e0f13-ba78-47ff-bd2a-3fb6f459f102
6.6. 注銷登錄
6.6. 1. 鏈接
localhost:80/logout6.6. 2. 參數
key:Authorization #登錄后的token values: 025e0f13-ba78-47ff-bd2a-3fb6f459f102
從redis中,再次查看,token消失了,簡言之,用戶登錄信息,銷毀了
注:不填寫登錄的token或者填寫token不正確,會提示用戶未登錄
再次查詢詳情
gitlab項目鏈接:
https://gitlab.com/gb-heima/springoot-shiro-redis
git下載方式:
zip下載方式:
https://gitlab.com/gb-heima/springoot-shiro-redis/-/archive/master/springoot-shiro-redis-master.zip
總結
以上是生活随笔為你收集整理的SpringBoot集成Shiro前后端分离使用redis做缓存的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Centos6.8安装Nginx+域名转
- 下一篇: SpringBoot入门到精通_第3篇