什么是ACL和RBAC
ACL
- Access Control list:訪問控制列表
- 優點:簡單易用,開發便捷
- 缺點:用戶和權限直接掛鈎,導致在授予時的復雜性,比較分散,不便於管理
- 例子:常見的文件系統權限設計,直接給用戶加權限
RBAC
- Role Based Access Control:基於角色的訪問控制
- 權限與角色相關聯,用戶通過成為適當角色的成員而得到這些角色的權限
- 優點:簡化了用戶與權限的管理,通過對用戶進行分類,使得角色與權限關聯起來
- 缺點:開發比ACL相對復雜
- 例子:基於RBAC模型的權限驗證框架,Apache Shiro
什么是Apache Shiro
官網地址
介紹
Apache Shiro是一個強大且易用的Java安全框架,執行身份驗證、授權、密碼和會話管理。使用Shiro的易於理解的API,您可以快速、輕松地獲得任何應用程序,從最小的移動應用程序到最大的網絡和企業應用程序。
什么是身份認證
Authentication,身份認證,一般就是登陸校驗
什么是授權
Authorization,給用戶分配角色或者訪問某些資源的權限
什么是會話管理
Session Management,用戶的會話管理員,多數情況下是web session
什么是加密
Cryptography,數據加密,比如密碼加解密
核心概念
Subject
我們把用戶或者程序稱為主體,主體去訪問系統或者資源
SecurityManager
安全管理器,Subject的認證和授權都要在安全管理器下進行
Realm
數據域,Shiro和安全數據的連接器,通過realm獲取認證授權相關信息
Authenticator
認證器,主要負責Subject的認證
Authorizer
授權器,主要負責Subject的授權,控制Subject擁有的角色或者權限
Crytography
加解密,Shiro的包含易於使用和理解的數據加解密方法,簡化了很多復雜的API
Cache Manager
緩存管理器,比如認證或授權信息,通過緩存進行管理,提高性能
快速上手
構建項目
認證和授權

package com.ybchen.springboot_shiro; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.realm.SimpleAccountRealm; import org.apache.shiro.subject.Subject; import org.junit.Before; import org.junit.Test; /** * @Description: * @Author:chenyanbin * @Date:2020/12/27 7:43 下午 * @Versiion:1.0 */ public class QuickStartTest { private DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); private SimpleAccountRealm accountRealm = new SimpleAccountRealm(); @Before public void init() { //初始化數據源,模擬從數據庫中取的數據 accountRealm.addAccount("laochen", "123"); accountRealm.addAccount("laowang", "123456"); //構建環境 defaultSecurityManager.setRealm(accountRealm); } @Test public void testAuthentication() { //設置上下文 SecurityUtils.setSecurityManager(defaultSecurityManager); //獲取當前主體 Subject subject = SecurityUtils.getSubject(); //模擬用戶登錄,賬戶、密碼 UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("laowang", "123456"); subject.login(usernamePasswordToken); //判斷是否成功 boolean authenticated = subject.isAuthenticated(); System.out.println("認證結果:" + authenticated); } }
常用API
//是否有對應的角色
subject.hasRole("root");
//獲取subject名
subject.getPrincipal();
//檢查是否有對應的角色,無返回值,直接在SecurityManager里面進行判斷
subject.checkRole("admin");
//檢查是否有對應的角色
subject.hasRole("admin");
//退出登錄
subject.logout();

package com.ybchen.springboot_shiro;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Before;
import org.junit.Test;
/**
* @Description:
* @Author:chenyanbin
* @Date:2020/12/27 7:43 下午
* @Versiion:1.0
*/
public class QuickStartAPITest {
private DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
private SimpleAccountRealm accountRealm = new SimpleAccountRealm();
@Before
public void init() {
//初始化數據源,模擬從數據庫中取的數據
accountRealm.addAccount("laochen", "123","root","admin");
accountRealm.addAccount("laowang", "123456","user");
//構建環境
defaultSecurityManager.setRealm(accountRealm);
}
@Test
public void testAuthentication() {
//設置上下文
SecurityUtils.setSecurityManager(defaultSecurityManager);
//獲取當前主體
Subject subject = SecurityUtils.getSubject();
//模擬用戶登錄,賬戶、密碼
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("laochen", "123");
subject.login(usernamePasswordToken);
//判斷是否成功
boolean authenticated = subject.isAuthenticated();
System.out.println("認證結果:" + authenticated);
//是否有對應的角色
System.out.println("是否有對應的root角色:"+subject.hasRole("root"));
//獲取subject名
System.out.println("獲取subject名:"+subject.getPrincipal());
//檢查是否有對應的角色,無返回值,直接在SecurityManager里面進行判斷,沒有的話,直接報錯
subject.checkRole("admin");
//檢查是否有對應的角色
System.out.println("是否存在admin角色:"+subject.hasRole("admin"));
//退出登錄
subject.logout();
System.out.println("退出登錄后,認證結果:" + authenticated);
}
}

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.1</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.ybchen</groupId> <artifactId>springboot_shiro</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springboot_shiro</name> <description>SpringBoot整合Shiro</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.3</version> </dependency> <!--Shiro--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.7.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
realm實戰
作用
Shiro從Realm獲取安全數據
概念
- principal:主體的標識,可以有多個,但是需要具有唯一性,如:手機號、郵箱
- credential:憑證,一般就是密碼
內置ini realm

package com.ybchen.springboot_shiro; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.Factory; import org.junit.Before; import org.junit.Test; /** * @Description:從ini配置文件中讀取用戶與角色 * @Author:chenyanbin * @Date:2020/12/27 8:52 下午 * @Versiion:1.0 */ public class QuickStartIniTest { @Before public void init() { } @Test public void testAuthentication() { //創建SecurityManager工廠,通過配置文件ini創建 Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); SecurityManager securityManager=factory.getInstance(); //將securityManager設置到當前運行環境中 SecurityUtils.setSecurityManager(securityManager); //獲取主體 Subject subject = SecurityUtils.getSubject(); //模擬用戶登錄,賬戶、密碼 UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("laochen", "123"); subject.login(usernamePasswordToken); //判斷是否成功 //判斷是否成功 boolean authenticated = subject.isAuthenticated(); System.out.println("認證結果:" + authenticated); //是否有對應的角色 System.out.println("是否有對應的root角色:"+subject.hasRole("root")); //獲取subject名 System.out.println("獲取subject名:"+subject.getPrincipal()); //檢查是否有對應的角色,無返回值,直接在SecurityManager里面進行判斷,沒有的話,直接報錯 subject.checkRole("admin"); //檢查是否有對應的角色 System.out.println("是否存在admin角色:"+subject.hasRole("admin")); //退出登錄 subject.logout(); System.out.println("退出登錄后,認證結果:" + authenticated); } }

# 格式 name=password,role1,role2,..roleN
[users]
# 賬戶=laochen;密碼=123;角色=admin
laochen = 123, admin
laowang = 456, user
# 格式 role=permission1,permission2...permissionN 也可以用通配符
# 下面配置user的權限為所有video:find,video:buy,如果需要配置video全部操作crud 則 user = video:*
[roles]
user = video:find,video:buy
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
校驗權限

package com.ybchen.springboot_shiro; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.Factory; import org.junit.Before; import org.junit.Test; /** * @Description:從ini配置文件中讀取用戶與角色 * @Author:chenyanbin * @Date:2020/12/27 8:52 下午 * @Versiion:1.0 */ public class QuickStartIniTest { @Before public void init() { } @Test public void testAuthentication() { //創建SecurityManager工廠,通過配置文件ini創建 Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); SecurityManager securityManager=factory.getInstance(); //將securityManager設置到當前運行環境中 SecurityUtils.setSecurityManager(securityManager); //獲取主體 Subject subject = SecurityUtils.getSubject(); //模擬用戶登錄,賬戶、密碼 UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("laochen", "123"); subject.login(usernamePasswordToken); //判斷是否成功 //判斷是否成功 boolean authenticated = subject.isAuthenticated(); System.out.println("認證結果:" + authenticated); //是否有對應的角色 System.out.println("是否有對應的root角色:"+subject.hasRole("root")); //獲取subject名 System.out.println("獲取subject名:"+subject.getPrincipal()); //檢查是否有對應的角色,無返回值,直接在SecurityManager里面進行判斷,沒有的話,直接報錯 subject.checkRole("admin"); //檢查是否有對應的角色 System.out.println("是否存在admin角色:"+subject.hasRole("admin")); //================權限,沒有的話直接報錯================ subject.checkPermission("video:delete"); System.out.println("是否有video:delete權限:"+subject.isPermitted("video:delete")); //退出登錄 subject.logout(); System.out.println("退出登錄后,認證結果:" + subject.isAuthenticated()); } }

# 格式 name=password,role1,role2,..roleN
[users]
# 賬戶=laochen;密碼=123;角色=admin
laochen = 123, admin
laowang = 456, user
# 格式 role=permission1,permission2...permissionN 也可以用通配符
# 下面配置user的權限為所有video:find,video:buy,如果需要配置video全部操作crud 則 user = video:*
[roles]
user = video:find,video:buy
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
注:配置文件必須ini結尾
內置JdbcRealm
方式一

package com.ybchen.springboot_shiro; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.Factory; import org.junit.Test; /** * @Description: * @Author:chenyanbin * @Date:2020/12/27 10:40 下午 * @Versiion:1.0 */ public class QuickStartJdbcIniTest { @Test public void testAuthentication(){ //創建SecurityManager工廠,通過配置文件ini創建 Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:jdbcrealm.ini"); SecurityManager securityManager=factory.getInstance(); //將securityManager設置到當前運行環境中 SecurityUtils.setSecurityManager(securityManager); //獲取主體 Subject subject = SecurityUtils.getSubject(); //模擬用戶登錄,賬戶、密碼 UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("laochen", "123"); subject.login(usernamePasswordToken); //判斷是否成功 boolean authenticated = subject.isAuthenticated(); System.out.println("認證結果:" + authenticated); //是否有對應的角色 System.out.println("是否有對應的root角色:"+subject.hasRole("root")); //獲取subject名 System.out.println("獲取subject名:"+subject.getPrincipal()); //檢查是否有對應的角色,無返回值,直接在SecurityManager里面進行判斷,沒有的話,直接報錯 subject.checkRole("role1"); //檢查是否有對應的角色 System.out.println("是否存在role1角色:"+subject.hasRole("role1")); //================權限,沒有的話直接報錯================ //subject.checkPermission("video:delete"); System.out.println("是否有video:buy權限:"+subject.isPermitted("video:buy")); //退出登錄 subject.logout(); System.out.println("退出登錄后,認證結果:" + subject.isAuthenticated()); } }

#聲明Realm,指定realm類型 jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm #配置數據源 #dataSource=com.mchange.v2.c3p0.ComboPooledDataSource dataSource=com.alibaba.druid.pool.DruidDataSource # mysql-connector-java 5 用的驅動url是com.mysql.jdbc.Driver,mysql-connector-java6以后用的是com.mysql.cj.jdbc.Driver dataSource.driverClassName=com.mysql.cj.jdbc.Driver #避免安全警告 dataSource.url=jdbc:mysql://127.0.0.1:3306/shiro?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false #賬號、密碼 dataSource.username=root dataSource.password=root #指定數據源 jdbcRealm.dataSource=$dataSource #開啟查找權限, 默認是false jdbcRealm.permissionsLookupEnabled=true #指定SecurityManager的Realms實現,設置realms,可以有多個,用逗號隔開 securityManager.realms=$jdbcRealm

/*
Navicat Premium Data Transfer
Source Server : localhost
Source Server Type : MySQL
Source Server Version : 50731
Source Host : localhost:3306
Source Schema : shiro
Target Server Type : MySQL
Target Server Version : 50731
File Encoding : 65001
Date: 27/12/2020 23:06:45
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for roles_permissions
-- ----------------------------
DROP TABLE IF EXISTS `roles_permissions`;
CREATE TABLE `roles_permissions` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`role_name` varchar(100) DEFAULT NULL COMMENT '角色名',
`permission` varchar(100) DEFAULT NULL COMMENT '權限名',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_roles_permissions` (`role_name`,`permission`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of roles_permissions
-- ----------------------------
BEGIN;
INSERT INTO `roles_permissions` VALUES (4, 'admin', 'video:*');
INSERT INTO `roles_permissions` VALUES (3, 'role1', 'video:buy');
INSERT INTO `roles_permissions` VALUES (2, 'role1', 'video:find');
INSERT INTO `roles_permissions` VALUES (5, 'role2', '*');
INSERT INTO `roles_permissions` VALUES (1, 'root', '*');
COMMIT;
-- ----------------------------
-- Table structure for user_roles
-- ----------------------------
DROP TABLE IF EXISTS `user_roles`;
CREATE TABLE `user_roles` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`username` varchar(100) DEFAULT NULL COMMENT '用戶名',
`role_name` varchar(100) DEFAULT NULL COMMENT '角色名',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_user_roles` (`username`,`role_name`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of user_roles
-- ----------------------------
BEGIN;
INSERT INTO `user_roles` VALUES (1, 'laochen', 'role1');
INSERT INTO `user_roles` VALUES (2, 'laochen', 'role3');
INSERT INTO `user_roles` VALUES (4, 'laowang', 'admin');
INSERT INTO `user_roles` VALUES (3, 'laowang', 'root');
COMMIT;
-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`username` varchar(100) DEFAULT NULL COMMENT '用戶名',
`password` varchar(100) DEFAULT NULL COMMENT '密碼',
`password_salt` varchar(100) DEFAULT NULL COMMENT '密碼加鹽規則',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_users_username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of users
-- ----------------------------
BEGIN;
INSERT INTO `users` VALUES (1, 'laochen', '123', NULL);
INSERT INTO `users` VALUES (2, 'laowang', '456', NULL);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
注意
表名和字段要對應上,否則自定義定,繼承:AuthorizingRealm,重寫sql查詢語句!!!!並重新指定realm類型!!!!
方式二

package com.ybchen.springboot_shiro; import com.alibaba.druid.pool.DruidDataSource; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.realm.jdbc.JdbcRealm; import org.apache.shiro.subject.Subject; import org.junit.Test; /** * @Description: * @Author:chenyanbin * @Date:2020/12/27 10:40 下午 * @Versiion:1.0 */ public class QuickStartJdbc2Test { @Test public void testAuthentication() { DefaultSecurityManager securityManager = new DefaultSecurityManager(); DruidDataSource ds = new DruidDataSource(); ds.setDriverClassName("com.mysql.cj.jdbc.Driver"); ds.setUrl("jdbc:mysql://127.0.0.1:3306/shiro?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false"); ds.setUsername("root"); ds.setPassword("root"); JdbcRealm jdbcRealm = new JdbcRealm(); //開啟查找權限, 默認是false jdbcRealm.setPermissionsLookupEnabled(true); //配置數據源 jdbcRealm.setDataSource(ds); //jdbc與DefaultSecurityManager關聯 securityManager.setRealm(jdbcRealm); //=======================下面內容相同============================== //將securityManager設置到當前運行環境中 SecurityUtils.setSecurityManager(securityManager); //獲取主體 Subject subject = SecurityUtils.getSubject(); //模擬用戶登錄,賬戶、密碼 UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("laochen", "123"); subject.login(usernamePasswordToken); //判斷是否成功 boolean authenticated = subject.isAuthenticated(); System.out.println("認證結果:" + authenticated); //是否有對應的角色 System.out.println("是否有對應的root角色:" + subject.hasRole("root")); //獲取subject名 System.out.println("獲取subject名:" + subject.getPrincipal()); //檢查是否有對應的角色,無返回值,直接在SecurityManager里面進行判斷,沒有的話,直接報錯 subject.checkRole("role1"); //檢查是否有對應的角色 System.out.println("是否存在role1角色:" + subject.hasRole("role1")); //================權限,沒有的話直接報錯================ //subject.checkPermission("video:delete"); System.out.println("是否有video:buy權限:" + subject.isPermitted("video:buy")); //退出登錄 subject.logout(); System.out.println("退出登錄后,認證結果:" + subject.isAuthenticated()); } }
自定義realm
繼承AuthorizingRealm,重寫授權方法doGetAuthorizationInfo、重寫認證方法doGetAuthenticationInfo。
UsernamePasswordToken:對應就是shiro的token中有Principal和Credential。
SimpleAuthorizationInfo:代表用戶角色權限信息
SimpleAuthenticationInfo:代表該用戶的認證信息

package com.ybchen.springboot_shiro; 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.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * @Description:自定義Realm * @Author:chenyanbin * @Date:2020/12/28 8:41 下午 * @Versiion:1.0 */ public class CustomRealm extends AuthorizingRealm { private final Map<String, String> userInfoMap = new HashMap<>(); //role-->permission private final Map<String, Set<String>> permissionMap = new HashMap<>(); //user-->role private final Map<String, Set<String>> roleMap = new HashMap<>(); /** * 代碼塊初始化數據 */ { userInfoMap.put("laochen", "123"); userInfoMap.put("laowang", "456"); //================================ Set<String> set1 = new HashSet<>(); set1.add("video:find"); set1.add("video:buy"); Set<String> set2 = new HashSet<>(); set2.add("video:add"); set2.add("video:delete"); permissionMap.put("laochen", set1); permissionMap.put("laowang", set2); //================================ Set<String> set3 = new HashSet<>(); Set<String> set4 = new HashSet<>(); set3.add("role1"); set3.add("role2"); set4.add("root"); roleMap.put("laochen", set3); roleMap.put("laowang", set4); } /** * 授權認證 * * @param principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("授權 AuthorizationInfo"); String name = (String) principals.getPrimaryPrincipal(); //權限 Set<String> permissions = getPermissionsByNameFromDB(name); //角色 Set<String> roles = getRoleByNameFromDB(name); SimpleAuthorizationInfo simpleAuthorizationInfo=new SimpleAuthorizationInfo(); simpleAuthorizationInfo.setRoles(roles); simpleAuthorizationInfo.setStringPermissions(permissions); return simpleAuthorizationInfo; } /** * 模擬從數據庫中取角色 * * @param name * @return */ private Set<String> getRoleByNameFromDB(String name) { return roleMap.get(name); } /** * 模擬從數據庫中取權限 * * @param name * @return */ private Set<String> getPermissionsByNameFromDB(String name) { return permissionMap.get(name); } /** * 登錄認證 * * @param token * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("認證 doGetAuthenticationInfo"); //用戶名 String name = (String) token.getPrincipal(); //從DB中根據用戶取密碼 String pwd = getPwdByUserNameFromDB(name); if (pwd == null || "".equals(pwd)) { return null; } SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(name, pwd, this.getName()); return simpleAuthenticationInfo; } /** * 模擬從數據庫中取密碼 * * @param name * @return */ private String getPwdByUserNameFromDB(String name) { return userInfoMap.get(name); } }

package com.ybchen.springboot_shiro; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.subject.Subject; import org.junit.Before; import org.junit.Test; /** * @Description:自定義realm * @Author:chenyanbin * @Date:2020/12/28 8:43 下午 * @Versiion:1.0 */ public class QuickCustomRealmTest { private CustomRealm customRealm=new CustomRealm(); private DefaultSecurityManager defaultSecurityManager=new DefaultSecurityManager(); @Before public void init() { //構建環境 defaultSecurityManager.setRealm(customRealm); SecurityUtils.setSecurityManager(defaultSecurityManager); } @Test public void testAuthentication(){ //獲取當前操作的主體 Subject subject = SecurityUtils.getSubject(); //用戶輸入賬號、密碼 UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken("laochen","123"); subject.login(usernamePasswordToken); System.out.println("認證結果:"+subject.isAuthenticated()); //拿到主體標識屬性 System.out.println("獲取subject名:"+subject.getPrincipal()); //是否有role1角色,沒有則報錯 subject.checkRole("role1"); //是否有對應的角色 System.out.println("是否有對應的角色:"+subject.hasRole("role1")); //是否有對應的權限 System.out.println("是否有對應的權限:"+subject.isPermitted("video:find")); } }
Filter過濾器
- 核心過濾器
- DefaultFilter,配置那個路徑對應那個攔截器進行處理
- authc:org.apache.shiro.web.filter.authc.FormAuthenticationFilter
- 需要認證登錄才能訪問
- user:org.apache.shiro.web.filter.authc.UseerrFilter
- 用戶攔截器,表示必須存在用戶
- anon:org.apache.shiro.web.filter.authc.AnonymoousFilter
- 匿名攔截器,不需要登錄即可訪問的資源,匿名用戶或游客,一般用於過濾靜態資源。
- roles:org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
- 角色授權攔截器,驗證用戶是否擁有角色
- 參數可寫多個,表示某些角色才能通過,多個參數時,寫roles["root,role1"],當有多個參數時必須每個參數都通過才算通過
- perms:org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
- 權限授權攔截器,驗證用戶是否擁有權限
- 參數可寫多個,表示需要某些權限才能通過,多個參數寫perms["user,admin"],當有多個參數時必須每個參數都通過才算可以
- authcBasci:org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
- httpBasic,身份驗證攔截器
- logout:org.apache.shiro.web.filter.authc.LogoutFilter
- 退出攔截器,執行后會直接跳轉到shiroFilterFactoryBean.setLoginUrl(),設置的url
- port:org.apache.shiro.web.filter.authz.PortFilter
- 端口攔截器,可通過的端口
- ssl:org.apache.shiro.web.filter.authz.SslFilter
- ssl攔截器,只有請求協議是https才能通過
Filter配置路徑
- 路徑通配符支持?、*、**,注意通配符匹配不包含目錄分隔符“/”
- *:可以匹配所有,不加*,可以進行前綴匹配,但多個冒號就需要多個*來匹配
url權限采取第一次匹配優先的方式 ?:匹配一個字符,如:/user?,匹配:/user1,但不匹配:/user/ *:匹配零個或多個字符串,如:/add*,匹配:/addtest,但不匹配:/user/1 **:匹配路徑中的零個或多個路徑,如:/user/**將匹配:/user/xxx/yyy
Shiro權限控制注解
注解方式
- @RequiresRoles(value={"admin","editor"},logical=Logical.AND)
- 需要角色:admin和editor兩個角色,AND表示兩個同時成立
- RequiresPermissions(value={"user:add","user:del"},logical.OR)
- 需要權限user:add或user:del權限其中一個,OR是或的意思
- @RequiresAuthentication
- 已經授過權,調用Subject.isAuthenticated()返回true
- @RequiresUser
- 身份驗證或通過記住我登錄過的
使用文件的方式
使用ShiroConfig。
編程方式
SpringBoot整合Shiro
技術棧
前后端分離+SpringBoot+Mysql+Mybatis+Shiro+Redis+JDK8
數據庫表

/* Navicat Premium Data Transfer Source Server : localhost Source Server Type : MySQL Source Server Version : 50731 Source Host : localhost:3306 Source Schema : shiro_2 Target Server Type : MySQL Target Server Version : 50731 File Encoding : 65001 Date: 03/01/2021 22:36:28 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for permission -- ---------------------------- DROP TABLE IF EXISTS `permission`; CREATE TABLE `permission` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `name` varchar(255) DEFAULT NULL COMMENT '權限名稱', `url` varchar(255) DEFAULT NULL COMMENT '路徑', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COMMENT='權限'; -- ---------------------------- -- Records of permission -- ---------------------------- BEGIN; INSERT INTO `permission` VALUES (1, 'video_update', '/api/video/update'); INSERT INTO `permission` VALUES (2, 'video_delete', '/api/video/delete'); INSERT INTO `permission` VALUES (3, 'video_add', '/api/video/add'); INSERT INTO `permission` VALUES (4, 'order_list', '/api/order/list'); INSERT INTO `permission` VALUES (5, 'user_list', '/api/user/list'); COMMIT; -- ---------------------------- -- Table structure for role -- ---------------------------- DROP TABLE IF EXISTS `role`; CREATE TABLE `role` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `name` varchar(255) DEFAULT NULL COMMENT '角色名稱', `description` varchar(255) DEFAULT NULL COMMENT '描述', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='角色'; -- ---------------------------- -- Records of role -- ---------------------------- BEGIN; INSERT INTO `role` VALUES (1, 'admin', '系統管理員'); INSERT INTO `role` VALUES (2, 'root', '超級管理員'); INSERT INTO `role` VALUES (3, 'user', '普通用戶'); COMMIT; -- ---------------------------- -- Table structure for role_permission -- ---------------------------- DROP TABLE IF EXISTS `role_permission`; CREATE TABLE `role_permission` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `role_id` int(11) DEFAULT NULL COMMENT '角色id', `permission_id` int(11) DEFAULT NULL COMMENT '權限id', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8 COMMENT='角色-權限'; -- ---------------------------- -- Records of role_permission -- ---------------------------- BEGIN; INSERT INTO `role_permission` VALUES (1, 1, 1); INSERT INTO `role_permission` VALUES (2, 1, 2); INSERT INTO `role_permission` VALUES (3, 2, 1); INSERT INTO `role_permission` VALUES (4, 2, 2); INSERT INTO `role_permission` VALUES (5, 2, 3); INSERT INTO `role_permission` VALUES (6, 2, 4); INSERT INTO `role_permission` VALUES (7, 2, 5); INSERT INTO `role_permission` VALUES (8, 3, 5); COMMIT; -- ---------------------------- -- Table structure for user -- ---------------------------- DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `username` varchar(255) DEFAULT NULL COMMENT '用戶名', `password` varchar(255) DEFAULT NULL COMMENT '密碼', `create_time` datetime DEFAULT NULL COMMENT '創建時間', `salt` varchar(255) DEFAULT NULL COMMENT '加鹽', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='用戶'; -- ---------------------------- -- Records of user -- ---------------------------- BEGIN; INSERT INTO `user` VALUES (1, 'laochen', '123', NULL, NULL); INSERT INTO `user` VALUES (2, 'laowang', '456', NULL, NULL); INSERT INTO `user` VALUES (3, 'laoli', '789', NULL, NULL); COMMIT; -- ---------------------------- -- Table structure for user_role -- ---------------------------- DROP TABLE IF EXISTS `user_role`; CREATE TABLE `user_role` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `role_id` int(11) DEFAULT NULL COMMENT '角色id', `user_id` int(11) DEFAULT NULL COMMENT '用戶id', `remark` varchar(255) DEFAULT NULL COMMENT '備注', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COMMENT='角色-用戶關聯表'; -- ---------------------------- -- Records of user_role -- ---------------------------- BEGIN; INSERT INTO `user_role` VALUES (1, 1, 1, 'laochen是系統管理員'); INSERT INTO `user_role` VALUES (2, 2, 2, 'laowang是超級管理員'); INSERT INTO `user_role` VALUES (3, 3, 3, 'laoli是普通用戶'); INSERT INTO `user_role` VALUES (4, 1, 2, 'laowang是系統管理員'); COMMIT; SET FOREIGN_KEY_CHECKS = 1;
項目結構

package com.ybchen.springboot_shiro.config; import com.ybchen.springboot_shiro.domain.Role; import com.ybchen.springboot_shiro.domain.User; import com.ybchen.springboot_shiro.service.UserService; 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.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; /** * @Description:自定義realm * @Author:chenyanbin * @Date:2021/1/2 11:16 下午 * @Versiion:1.0 */ public class CustomRealm extends AuthorizingRealm { @Autowired private UserService userService; /** * 進行權限校驗的時候會調用 * * @param principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("CustomRealm doGetAuthorizationInfo 授權"); //獲取用戶名 String userName = (String) principals.getPrimaryPrincipal(); User user = userService.findAllUserInfoByUserName(userName); if (user == null) { return null; } //角色集合 List<String> stringRoleList = new ArrayList<>(); //權限集合 List<String> stringPermissionList = new ArrayList<>(); List<Role> roleList = user.getRoleList(); stringRoleList = roleList.stream().map( obj -> { stringPermissionList.addAll(obj.getPermissionList() .stream() .map(per -> per.getName()).collect(Collectors.toList())); return obj.getName(); }).collect(Collectors.toList()); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); simpleAuthorizationInfo.addRoles(stringRoleList); simpleAuthorizationInfo.addStringPermissions(stringPermissionList); return simpleAuthorizationInfo; } /** * 用戶登錄的時候會調用 * * @param token * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("CustomRealm doGetAuthenticationInfo 認證"); //從token中獲取用戶信息 String uesrName = (String) token.getPrincipal(); User user = userService.findAllUserInfoByUserName(uesrName); if (user == null) { return null; } //密碼 String pwd = user.getPassword(); if (pwd == null || "".equals(pwd)) { return null; } return new SimpleAuthenticationInfo(uesrName, pwd, this.getClass().getName()); } }

package com.ybchen.springboot_shiro.config; 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; /** * @Description:自定義SessionManager * @Author:chenyanbin * @Date:2021/1/3 4:54 下午 * @Versiion:1.0 */ public class CustomSessionManager extends DefaultWebSessionManager { public static final String AUTHORIZATION="token"; public CustomSessionManager() { super(); } @Override protected Serializable getSessionId(ServletRequest request, ServletResponse response) { //獲取sessionId String sessionId= WebUtils.toHttp(request).getHeader(AUTHORIZATION); if (sessionId!=null){ request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); return sessionId; }else { return super.getSessionId(request,response); } } }

package com.ybchen.springboot_shiro.config; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.LinkedHashMap; import java.util.Map; /** * @Description: * @Author:chenyanbin * @Date:2021/1/3 4:12 下午 * @Versiion:1.0 */ @Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { System.out.println("ShiroConfig ShiroFilterFactoryBean 執行"); ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //設置SecurityManager shiroFilterFactoryBean.setSecurityManager(securityManager); //如果訪問需要登錄的某個接口,卻沒有登錄,則調用此接口(如果不是前后端分離,則跳轉頁面) shiroFilterFactoryBean.setLoginUrl("/pub/need_login"); //shiroFilterFactoryBean.setLoginUrl("/xxx.jsp"); //登錄成功后,跳轉的鏈接,若前后端分離,沒必要設置這個 //shiroFilterFactoryBean.setSuccessUrl(""); //登錄成功,未授權會調用此方法 shiroFilterFactoryBean.setUnauthorizedUrl("/pub/not_permit"); //攔截路徑,必須使用:LinkedHashMap,要不然攔截效果會時有時無,因為使用的是無序的Map Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); //key=正則表達式路徑,value=org.apache.shiro.web.filter.mgt.DefaultFilter //退出過濾器 filterChainDefinitionMap.put("/logout", "logout"); //匿名可以訪問,游客模式 filterChainDefinitionMap.put("/pub/**", "anon"); //登錄用戶才可以訪問 filterChainDefinitionMap.put("/authc/**", "authc"); //管理員角色才能訪問 filterChainDefinitionMap.put("/admin/**", "roles[admin]"); //有編輯權限才能訪問 filterChainDefinitionMap.put("/video/update", "perms[video_update]"); //authc:url必須通過認證才可以訪問 //anon:url可以匿名訪問 //過濾鏈是順序執行,從上而下,一般把/**,放到最下面 filterChainDefinitionMap.put("/**", "authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //如果不是前后端分離,不用設置setSessionManager securityManager.setSessionManager(sessionManager()); securityManager.setRealm(customRealm()); return securityManager; } /** * 自定義realm * * @return */ @Bean public CustomRealm customRealm() { CustomRealm customRealm = new CustomRealm(); //因為數據庫密碼存的是明文,所以無需使用雙重md5校驗 // customRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return customRealm; } /** * 密碼驗證器,雙重md5 * * @return */ @Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); //設置散列算法,使用md5算法 hashedCredentialsMatcher.setHashAlgorithmName("md5"); //散列次數,使用2次md5算法,相當於md5(md5(xxx)) hashedCredentialsMatcher.setHashIterations(2); return hashedCredentialsMatcher; } /** * 自定義SessionManager * * @return */ @Bean public SessionManager sessionManager() { CustomSessionManager customSessionManager = new CustomSessionManager(); //超時時間,默認 30分鍾,會話超時,單位毫秒 // customSessionManager.setGlobalSessionTimeout(200000); return customSessionManager; } }

package com.ybchen.springboot_shiro.controller; import com.ybchen.springboot_shiro.utils.JsonData; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Arrays; import java.util.List; /** * @Description: * @Author:chenyanbin * @Date:2021/1/3 7:22 下午 * @Versiion:1.0 */ @RestController @RequestMapping("admin") public class AdminController { @GetMapping("/video/video_list") public JsonData videoList() { List<String> list = Arrays.asList("docker", "k8s", "jenkins"); return JsonData.buildSuccess(list); } }

package com.ybchen.springboot_shiro.controller; import org.springframework.web.bind.annotation.RestController; /** * @Description: * @Author:chenyanbin * @Date:2021/1/3 10:01 下午 * @Versiion:1.0 */ @RestController public class LogoutController { // /** // * 退出,沒必要能這個,退出時,前端直接將token清空即可 // * 還需要獲取前端傳來的token,然后從shiro從清空指定的session_id // * @return // */ // @GetMapping("logout") // public JsonData logout(){ // Subject subject= SecurityUtils.getSubject(); // subject.logout(); // return JsonData.buildSuccess("退出成功"); // } }

package com.ybchen.springboot_shiro.controller; import com.ybchen.springboot_shiro.utils.JsonData; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; /** * @Description: * @Author:chenyanbin * @Date:2021/1/3 6:28 下午 * @Versiion:1.0 */ @RestController @RequestMapping("authc") public class OrderController { /** * 購買記錄 * @return */ @GetMapping("/video/play_record") public JsonData findMyPlayRecord(){ Map<String,String> recordMap=new HashMap<>(); recordMap.put("1","SpringBoot"); recordMap.put("2","SpringMvc"); return JsonData.buildSuccess(recordMap); } }

package com.ybchen.springboot_shiro.controller; import com.ybchen.springboot_shiro.utils.JsonData; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** * @Description: * @Author:chenyanbin * @Date:2021/1/3 9:20 下午 * @Versiion:1.0 */ @RestController public class OtherController { @GetMapping("a") public JsonData a(){ return JsonData.buildSuccess("ok"); } }

package com.ybchen.springboot_shiro.controller; import com.ybchen.springboot_shiro.domain.UserQuery; import com.ybchen.springboot_shiro.utils.JsonData; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @Description: * @Author:chenyanbin * @Date:2021/1/3 1:12 上午 * @Versiion:1.0 */ @RestController @RequestMapping("pub") public class PublicController { /** * 需要登錄 * * @return */ @GetMapping("need_login") public JsonData needLogin() { return JsonData.buildSuccess(-1, "溫馨提示:請使用對應的賬號登錄"); } /** * 沒權限 * * @return */ @GetMapping("not_permit") public JsonData notPermit() { return JsonData.buildSuccess(-1, "溫馨提示:拒絕訪問,沒權限"); } /** * 首頁 * * @return */ @GetMapping("index") public JsonData index() { List<String> list = Arrays.asList("SpringBoot", "SpringMvc", "Mysql", "Redis"); return JsonData.buildSuccess(list); } /** * 登錄接口 * * @param userQuery * @param request * @param response * @return */ @PostMapping("login") public JsonData login(@RequestBody UserQuery userQuery, HttpServletRequest request, HttpServletResponse response) { //拿到主體 Subject subject = SecurityUtils.getSubject(); try { UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userQuery.getUserName(), userQuery.getPassword()); subject.login(usernamePasswordToken); Map<String,Object> info=new HashMap<>(); info.put("msg","登錄成功"); info.put("session_id",subject.getSession().getId()); return JsonData.buildSuccess(info); }catch (Exception e){ e.printStackTrace(); return JsonData.buildError("賬號或密碼錯誤"); } } }

package com.ybchen.springboot_shiro.controller; import com.ybchen.springboot_shiro.utils.JsonData; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @Description: * @Author:chenyanbin * @Date:2021/1/3 9:41 下午 * @Versiion:1.0 */ @RestController @RequestMapping("video") public class VideoController { @GetMapping("update") public JsonData updateVideo() { return JsonData.buildSuccess("更新成功"); } @GetMapping("add") public JsonData add(){ return JsonData.buildSuccess("添加成功"); } }

package com.ybchen.springboot_shiro.dao; import com.ybchen.springboot_shiro.domain.Permission; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import java.util.List; /** * 權限 */ public interface PermissionMapper { /** * 根據roleId查詢所有權限 * @param roleId * @return */ @Select("select p.id id,p.name name,p.url url from role_permission rp " + "left join permission p on rp.permission_id=p.id " + "where rp.role_id=#{roleId}") List<Permission> findByPermissionListByRoleId(@Param("roleId") int roleId); }

package com.ybchen.springboot_shiro.dao; import com.ybchen.springboot_shiro.domain.Role; import org.apache.ibatis.annotations.*; import org.apache.ibatis.mapping.FetchType; import java.util.List; /** * 角色 */ public interface RoleMapper { /** * 根據用戶查詢所有的角色 * * @param userId 用戶id * @return */ @Select("select r.id id,r.name name,r.description description from user_role ur " + "left join role r on ur.role_id=r.id " + "where ur.user_id=#{userId}") @Results( value = { @Result(id = true, property = "id", column = "id"), @Result(property = "name", column = "name"), @Result(property = "description", column = "description"), @Result(property = "permissionList", column = "id", many = @Many(select = "com.ybchen.springboot_shiro.dao.PermissionMapper.findByPermissionListByRoleId", fetchType = FetchType.DEFAULT)) } ) List<Role> findRoleListByUserId(@Param("userId") int userId); }

package com.ybchen.springboot_shiro.dao; import com.ybchen.springboot_shiro.domain.User; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; /** * 用戶 */ public interface UserMapper { /** * 根據用戶名查詢用戶 * * @param userName 用戶名 * @return */ @Select("select * from user where username=#{userName}") User findByUserName(@Param("userName") String userName); /** * 根據主鍵查詢用戶 * * @param id 主鍵 * @return */ @Select("select * from user where id=#{userId}") User findById(@Param("userId") int id); /** * 根據用戶名和密碼查詢用戶 * * @param userName 用戶名 * @param password 密碼 * @return */ @Select("select * from user where userName=#{userName} and password=#{password}") User findByUserNameAndPassword(@Param("userName") String userName, @Param("password") String password); }

package com.ybchen.springboot_shiro.domain; /** * @Description:權限 * @Author:chenyanbin * @Date:2021/1/2 11:47 下午 * @Versiion:1.0 */ public class Permission { /** * 主鍵 */ private int id; /** * 權限名稱 */ private String name; /** * 路徑 */ private String url; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } @Override public String toString() { return "Permission{" + "id=" + id + ", name='" + name + '\'' + ", url='" + url + '\'' + '}'; } }

package com.ybchen.springboot_shiro.domain; import java.util.ArrayList; import java.util.List; /** * @Description:角色 * @Author:chenyanbin * @Date:2021/1/2 11:43 下午 * @Versiion:1.0 */ public class Role { /** * 主鍵 */ private int id; /** * 角色名稱 */ private String name; /** * 描述 */ private String description; /** * 權限集合 */ private List<Permission> permissionList=new ArrayList<>(); public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public List<Permission> getPermissionList() { return permissionList; } public void setPermissionList(List<Permission> permissionList) { this.permissionList = permissionList; } }

package com.ybchen.springboot_shiro.domain; /** * @Description:角色權限 * @Author:chenyanbin * @Date:2021/1/2 11:44 下午 * @Versiion:1.0 */ public class RolePermission { /** * 主鍵 */ private int id; /** * 角色id */ private int roleId; /** * 權限id */ private int permissiionId; public int getId() { return id; } public void setId(int id) { this.id = id; } public int getRoleId() { return roleId; } public void setRoleId(int roleId) { this.roleId = roleId; } public int getPermissiionId() { return permissiionId; } public void setPermissiionId(int permissiionId) { this.permissiionId = permissiionId; } @Override public String toString() { return "RolePermission{" + "id=" + id + ", roleId=" + roleId + ", permissiionId=" + permissiionId + '}'; } }

package com.ybchen.springboot_shiro.domain; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * @Description:用戶表 * @Author:chenyanbin * @Date:2021/1/2 11:41 下午 * @Versiion:1.0 */ public class User { /** * 主鍵 */ private int id; /** * 用戶名 */ private String username; /** * 密碼 */ private String password; /** * 創建時間 */ private Date createTime; /** * 密碼加鹽 */ private String salt; /** * 角色集合 */ private List<Role> roleList=new ArrayList<>(); public int getId() { return id; } public void setId(int id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime = createTime; } public String getSalt() { return salt; } public void setSalt(String salt) { this.salt = salt; } public List<Role> getRoleList() { return roleList; } public void setRoleList(List<Role> roleList) { this.roleList = roleList; } @Override public String toString() { return "User{" + "id=" + id + ", username='" + username + '\'' + ", password='" + password + '\'' + ", createTime=" + createTime + ", salt='" + salt + '\'' + ", roleList=" + roleList + '}'; } }

package com.ybchen.springboot_shiro.domain; import java.io.Serializable; /** * @Description:接收用戶名和密碼 * @Author:chenyanbin * @Date:2021/1/3 6:19 下午 * @Versiion:1.0 */ public class UserQuery implements Serializable { private String userName; private String password; public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String toString() { return "UserQuery{" + "userName='" + userName + '\'' + ", password='" + password + '\'' + '}'; } }

package com.ybchen.springboot_shiro.domain; /** * @Description:用戶角色 * @Author:chenyanbin * @Date:2021/1/2 11:46 下午 * @Versiion:1.0 */ public class UserRole { /** * 主鍵 */ private int id; /** * 角色id */ private int roleId; /** * 用戶id */ private int userId; /** * 備注 */ private String remark; public int getId() { return id; } public void setId(int id) { this.id = id; } public int getRoleId() { return roleId; } public void setRoleId(int roleId) { this.roleId = roleId; } public int getUserId() { return userId; } public void setUserId(int userId) { this.userId = userId; } public String getRemark() { return remark; } public void setRemark(String remark) { this.remark = remark; } @Override public String toString() { return "UserRole{" + "id=" + id + ", roleId=" + roleId + ", userId=" + userId + ", remark='" + remark + '\'' + '}'; } }

package com.ybchen.springboot_shiro.exception; /** * @Description:自定義異常 * @Author:chenyanbin * @Date:2021/1/3 7:31 下午 * @Versiion:1.0 */ public class CustomException extends RuntimeException{ private Integer code; private String msg; public CustomException(Integer code, String msg) { this.code = code; this.msg = msg; } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } @Override public String toString() { return "CustomException{" + "code=" + code + ", msg='" + msg + '\'' + '}'; } }

package com.ybchen.springboot_shiro.exception; import com.ybchen.springboot_shiro.utils.JsonData; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; /** * @ClassName:GlobalExceptiions * @Description:TODO * @Author:chenyb * @Date:2020/12/9 11:34 上午 * @Versiion:1.0 */ @ControllerAdvice public class GlobalExceptiions { private final Logger logger = LoggerFactory.getLogger(getClass()); @ExceptionHandler(value = Exception.class) @ResponseBody public JsonData handle(Exception ex) { logger.info("[ 全局異常 ] ===============》 {}", ex); if (ex instanceof CustomException) { CustomException customException = (CustomException) ex; return JsonData.buildError(customException.getCode(), customException.getMsg()); } return JsonData.buildError("系統內部錯誤,請聯系管理員!"); } }

package com.ybchen.springboot_shiro.service.impl; import com.ybchen.springboot_shiro.dao.RoleMapper; import com.ybchen.springboot_shiro.dao.UserMapper; import com.ybchen.springboot_shiro.domain.Role; import com.ybchen.springboot_shiro.domain.User; import com.ybchen.springboot_shiro.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; /** * @Description: * @Author:chenyanbin * @Date:2021/1/3 1:15 上午 * @Versiion:1.0 */ @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Autowired private RoleMapper roleMapper; @Override public User findAllUserInfoByUserName(String userName) { User user = userMapper.findByUserName(userName); //用戶角色的集合 List<Role> roleList = roleMapper.findRoleListByUserId(user.getId()); user.setRoleList(roleList); return user; } @Override public User findSimpleUserInfoById(int userId) { return userMapper.findById(userId); } @Override public User findSimpleUserInfoByUserName(String userName) { return userMapper.findByUserName(userName); } }

package com.ybchen.springboot_shiro.service; import com.ybchen.springboot_shiro.domain.User; public interface UserService { /** * 獲取全部用戶信息,包括角色、權限 * @param userName * @return */ User findAllUserInfoByUserName(String userName); /** * 獲取用戶基本信息 * @param userId * @return */ User findSimpleUserInfoById(int userId); /** * 根據用戶名查詢用戶信息 * @param userName * @return */ User findSimpleUserInfoByUserName(String userName); }

package com.ybchen.springboot_shiro.utils; import java.io.Serializable; public class JsonData implements Serializable { private static final long serialVersionUID = 1L; /** * 狀態碼,0表示成功過,1表示處理中,-1表示失敗 */ private Integer code; /** * 業務數據 */ private Object data; /** * 信息描述 */ private String msg; public JsonData() { } public JsonData(Integer code, Object data, String msg) { this.code = code; this.data = data; this.msg = msg; } /** * 成功,不用返回數據 * * @return */ public static JsonData buildSuccess() { return new JsonData(0, null, null); } /** * 成功,返回數據 * * @param data 返回數據 * @return */ public static JsonData buildSuccess(Object data) { return new JsonData(0, data, null); } /** * 成功,返回數據 * * @param code 狀態碼 * @param data 返回數據 * @return */ public static JsonData buildSuccess(int code, Object data) { return new JsonData(code, data, null); } /** * 失敗,返回信息 * * @param msg 返回信息 * @return */ public static JsonData buildError(String msg) { return new JsonData(-1, null, msg); } /** * 失敗,返回信息和狀態碼 * * @param code 狀態碼 * @param msg 返回信息 * @return */ public static JsonData buildError(Integer code, String msg) { return new JsonData(code, null, msg); } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } @Override public String toString() { return "JsonData{" + "code=" + code + ", data=" + data + ", msg='" + msg + '\'' + '}'; } }

package com.ybchen.springboot_shiro; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication //掃描mapper @MapperScan(value = "com.ybchen.springboot_shiro.dao") public class SpringbootShiroApplication { public static void main(String[] args) { SpringApplication.run(SpringbootShiroApplication.class, args); } }

server.port=12888 #============數據庫================= spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://127.0.0.1:3306/shiro_2?useUnicode=true&characterEncoding=utf-8&useSSL=false spring.datasource.username=root spring.datasource.password=root # 使用阿里巴巴druid數據源,默認使用自帶的 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource # 開啟控制台打印sql mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl # mybatis下划線轉駝峰配置 mybatis.configuration.map-underscore-to-camel-case=true

package com.ybchen.springboot_shiro; import org.apache.shiro.crypto.hash.SimpleHash; import org.junit.Test; /** * @Description: * @Author:chenyanbin * @Date:2021/1/3 10:12 下午 * @Versiion:1.0 */ public class Md5Test { @Test public void testMd5(){ String hashName="md5"; String pwd="123"; SimpleHash simpleHash = new SimpleHash(hashName, pwd, null, 2); System.out.println(simpleHash); } }

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.1</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.ybchen</groupId> <artifactId>springboot_shiro</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springboot_shiro</name> <description>SpringBoot整合Shiro</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> </dependency> <!--druid--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.3</version> </dependency> <!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--shiro--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.7.0</version> </dependency> <!--mybatis--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.4</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
項目源碼
鏈接: https://pan.baidu.com/s/1adjwICKge83YcPycE8ZaEQ 密碼: if9s
項目postman測試
127.0.0.1:12888/pub/index
127.0.0.1:12888/pub/not_permit
127.0.0.1:12888/pub/need_login
127.0.0.1:12888/pub/login
127.0.0.1:12888/authc/video/play_record
127.0.0.1:12888/admin/video/video_list
127.0.0.1:12888/video/add
127.0.0.1:12888/video/update
備注
因為鏈接較多,就不一一做gif動圖了,直接導入項目源碼,請求的時候,在header上加入token即可~
Filter過濾器
業務需求
- 一個接口,可以讓2個角色中的任意一個訪問
- 自定義一個類,繼承:AuthorizationFilter
package com.ybchen.springboot_shiro.config; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.CollectionUtils; import org.apache.shiro.web.filter.authz.AuthorizationFilter; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import java.util.Set; /** * @Description:自定義Filter * @Author:chenyanbin * @Date:2021/1/4 11:14 下午 * @Versiion:1.0 */ public class CustomRolesOrAuthorizationFilter extends AuthorizationFilter { @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { Subject subject = getSubject(request, response); //filterChainDefinitionMap.put("/admin/**", "roles[admin,user]"); mappedValue <==> admin,user String[] rolesArray = (String[]) mappedValue; if (rolesArray == null || rolesArray.length == 0) { return true; } Set<String> roles = CollectionUtils.asSet(rolesArray); //當前subject是roles中的任意一個,則有權限訪問 for (String role : roles) { if (subject.hasRole(role)) { return true; } } return false; } }
@Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { System.out.println("ShiroConfig ShiroFilterFactoryBean 執行"); ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //設置SecurityManager shiroFilterFactoryBean.setSecurityManager(securityManager); //如果訪問需要登錄的某個接口,卻沒有登錄,則調用此接口(如果不是前后端分離,則跳轉頁面) shiroFilterFactoryBean.setLoginUrl("/pub/need_login"); //shiroFilterFactoryBean.setLoginUrl("/xxx.jsp"); //登錄成功后,跳轉的鏈接,若前后端分離,沒必要設置這個 //shiroFilterFactoryBean.setSuccessUrl(""); //登錄成功,未授權會調用此方法 shiroFilterFactoryBean.setUnauthorizedUrl("/pub/not_permit"); //設置自定義Filter Map<String, Filter> filterMap=new LinkedHashMap<>(); filterMap.put("roleOrFilter",new CustomRolesOrAuthorizationFilter()); shiroFilterFactoryBean.setFilters(filterMap); //攔截路徑,必須使用:LinkedHashMap,要不然攔截效果會時有時無,因為使用的是無序的Map Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); //key=正則表達式路徑,value=org.apache.shiro.web.filter.mgt.DefaultFilter //退出過濾器 filterChainDefinitionMap.put("/logout", "logout"); //匿名可以訪問,游客模式 filterChainDefinitionMap.put("/pub/**", "anon"); //登錄用戶才可以訪問 filterChainDefinitionMap.put("/authc/**", "authc"); //管理員角色才能訪問 // filterChainDefinitionMap.put("/admin/**", "roles[admin,user]"); filterChainDefinitionMap.put("/admin/**", "roleOrFilter[admin,user]"); //有編輯權限才能訪問 filterChainDefinitionMap.put("/video/update", "perms[video_update]"); //authc:url必須通過認證才可以訪問 //anon:url可以匿名訪問 //過濾鏈是順序執行,從上而下,一般把/**,放到最下面 filterChainDefinitionMap.put("/**", "authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; }
Redis整合CacheManager
原因
授權的時候每次都去查詢數據庫,對於頻繁訪問的接口,性能和響應速度比較慢,此處可以使用緩存,提高響應速度,也可以使用Guava(本地內存緩存)。
Redis(分布式緩存)還不了解的小伙伴,在這里我就不一一講解了,可以看我以前寫過的博客。
添加依賴
<!--shiro+redis--> <dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis</artifactId> <version>3.3.1</version> </dependency>
在ShiroConfig中添加如下代碼
//使用自定義cacheManager securityManager.setCacheManager(cacheManager()); /** * 配置redisManager * @return */ public RedisManager getRedisManager(){ RedisManager redisManager=new RedisManager(); redisManager.setHost("127.0.0.1:6379"); //連接那個數據庫 redisManager.setDatabase(0); //設置密碼 // redisManager.setPassword("123"); return redisManager; } /** * 設置具體cache實現類 * @return */ public RedisCacheManager cacheManager(){ RedisCacheManager redisCacheManager=new RedisCacheManager(); redisCacheManager.setRedisManager(getRedisManager()); return redisCacheManager; }
修改CustomRealm
設置redis緩存過期時間
Redis整合SessionManager
為啥Session也要持久化
重啟應用,用戶無感知,可以繼續以原先的狀態繼續訪問。
修改shiroconfig

package com.ybchen.springboot_shiro.config; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.crazycake.shiro.RedisCacheManager; import org.crazycake.shiro.RedisManager; import org.crazycake.shiro.RedisSessionDAO; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.servlet.Filter; import java.util.LinkedHashMap; import java.util.Map; /** * @Description: * @Author:chenyanbin * @Date:2021/1/3 4:12 下午 * @Versiion:1.0 */ @Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { System.out.println("ShiroConfig ShiroFilterFactoryBean 執行"); ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //設置SecurityManager shiroFilterFactoryBean.setSecurityManager(securityManager); //如果訪問需要登錄的某個接口,卻沒有登錄,則調用此接口(如果不是前后端分離,則跳轉頁面) shiroFilterFactoryBean.setLoginUrl("/pub/need_login"); //shiroFilterFactoryBean.setLoginUrl("/xxx.jsp"); //登錄成功后,跳轉的鏈接,若前后端分離,沒必要設置這個 //shiroFilterFactoryBean.setSuccessUrl(""); //登錄成功,未授權會調用此方法 shiroFilterFactoryBean.setUnauthorizedUrl("/pub/not_permit"); //設置自定義Filter Map<String, Filter> filterMap=new LinkedHashMap<>(); filterMap.put("roleOrFilter",new CustomRolesOrAuthorizationFilter()); shiroFilterFactoryBean.setFilters(filterMap); //攔截路徑,必須使用:LinkedHashMap,要不然攔截效果會時有時無,因為使用的是無序的Map Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); //key=正則表達式路徑,value=org.apache.shiro.web.filter.mgt.DefaultFilter //退出過濾器 filterChainDefinitionMap.put("/logout", "logout"); //匿名可以訪問,游客模式 filterChainDefinitionMap.put("/pub/**", "anon"); //登錄用戶才可以訪問 filterChainDefinitionMap.put("/authc/**", "authc"); //管理員角色才能訪問 // filterChainDefinitionMap.put("/admin/**", "roles[admin,user]"); filterChainDefinitionMap.put("/admin/**", "roleOrFilter[admin,user]"); //有編輯權限才能訪問 filterChainDefinitionMap.put("/video/update", "perms[video_update]"); //authc:url必須通過認證才可以訪問 //anon:url可以匿名訪問 //過濾鏈是順序執行,從上而下,一般把/**,放到最下面 filterChainDefinitionMap.put("/**", "authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //如果不是前后端分離,不用設置setSessionManager securityManager.setSessionManager(sessionManager()); //使用自定義cacheManager securityManager.setCacheManager(cacheManager()); securityManager.setRealm(customRealm()); return securityManager; } /** * 配置redisManager * @return */ public RedisManager getRedisManager(){ RedisManager redisManager=new RedisManager(); redisManager.setHost("127.0.0.1:6379"); //連接那個數據庫 redisManager.setDatabase(0); //設置密碼 // redisManager.setPassword("123"); return redisManager; } /** * 設置具體cache實現類 * @return */ public RedisCacheManager cacheManager(){ RedisCacheManager redisCacheManager=new RedisCacheManager(); redisCacheManager.setRedisManager(getRedisManager()); //設置緩存過期時間 redisCacheManager.setExpire(20); return redisCacheManager; } /** * 自定義realm * * @return */ @Bean public CustomRealm customRealm() { CustomRealm customRealm = new CustomRealm(); //因為數據庫密碼存的是明文,所以無需使用雙重md5校驗 // customRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return customRealm; } /** * 密碼驗證器,雙重md5 * * @return */ @Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); //設置散列算法,使用md5算法 hashedCredentialsMatcher.setHashAlgorithmName("md5"); //散列次數,使用2次md5算法,相當於md5(md5(xxx)) hashedCredentialsMatcher.setHashIterations(2); return hashedCredentialsMatcher; } /** * 自定義SessionManager * * @return */ @Bean public SessionManager sessionManager() { CustomSessionManager customSessionManager = new CustomSessionManager(); //超時時間,默認 30分鍾,會話超時,單位毫秒 // customSessionManager.setGlobalSessionTimeout(200000); //配置session持久化 customSessionManager.setSessionDAO(redisSessionDAO()); return customSessionManager; } /** * 自定義session持久化 * @return */ public RedisSessionDAO redisSessionDAO(){ RedisSessionDAO redisSessionDAO=new RedisSessionDAO(); redisSessionDAO.setRedisManager(getRedisManager()); return redisSessionDAO; } }
Shiro整合Redis后的源碼
鏈接: https://pan.baidu.com/s/1cNQfBiw50A-U5izzOQclpw 密碼: 6wqt