Shiro 是一個強大、簡單易用的 Java 安全框架,主要用來更便捷的認(rèn)證,授權(quán),加密,會話管等等,可為任何應(yīng)用提供安全保障。本課程主要來介紹 Shiro 的認(rèn)證和授權(quán)功能。
Shiro 三大核心組件
Shiro 有三大核心的組件: Subject 、 SecurityManager 和 Realm 。先來看一下它們之間的關(guān)系。
1. Subject:認(rèn)證主體。它包含兩個信息:Principals 和 Credentials??匆幌逻@兩個信息具體是什么。
Principals:身份??梢允怯脩裘?,郵件,手機號碼等等,用來標(biāo)識一個登錄主體身份; Credentials:憑證。常見有密碼,數(shù)字證書等等。
說白了,就是需要認(rèn)證的東西,最常見的就是用戶名密碼了,比如用戶在登錄的時候,Shiro 需要去進(jìn)
行身份認(rèn)證,就需要 Subject 認(rèn)證主體。
2. SecurityManager:安全管理員。這是 Shiro 架構(gòu)的核心,它就像 Shiro 內(nèi)部所有原件的保護(hù)傘一樣。我們在項目中一般都會配置 SecurityManager,開發(fā)人員大部分精力主要是在 Subject 認(rèn)證主體上面。我們在與 Subject 進(jìn)行交互的時候,實際上是 SecurityManager 在背后做一些安全操作。
3. Realms:Realms 是一個域,它是連接 Shiro 和具體應(yīng)用的橋梁,當(dāng)需要與安全數(shù)據(jù)交互的時候, 比如用戶賬戶、訪問控制等,Shiro 就會從一個或多個 Realms 中去查找。我們一般會自己定制Realm,這在下文會詳細(xì)說明。
1. Shiro 身份和權(quán)限認(rèn)證
1.2 Shiro 身份認(rèn)證
我們來分析一下 Shiro 身份認(rèn)證的過程,看一下官方的一個認(rèn)證圖:
Step1:應(yīng)用程序代碼在調(diào)用 Subject.login(token) 方法后,傳入代表最終用戶的身份和憑證的AuthenticationToken 實例 token。
Step2:將 Subject 實例委托給應(yīng)用程序的 SecurityManager(Shiro的安全管理)來開始實際的認(rèn)證工
作。這里開始真正的認(rèn)證工作了。
Step3,4,5:然后 SecurityManager 就會根據(jù)具體的 realm 去進(jìn)行安全認(rèn)證了。 從圖中可以看出,
realm 可以自定義(Custom Realm)。
1.3 Shiro 權(quán)限認(rèn)證
權(quán)限認(rèn)證,也就是訪問控制,即在應(yīng)用中控制誰能訪問哪些資源。在權(quán)限認(rèn)證中,最核心的三個要素是:權(quán)限,角色和用戶。
權(quán)限(permission):即操作資源的權(quán)利,比如訪問某個頁面,以及對某個模塊的數(shù)據(jù)的添加, 修改,刪除,查看的權(quán)利; 角色(role):指的是用戶擔(dān)任的的角色,一個角色可以有多個權(quán)限; 用戶(user):在 Shiro 中,代表訪問系統(tǒng)的用戶,即上面提到的 Subject 認(rèn)證主體。
它們之間的的關(guān)系可以用下圖來表示:
一個用戶可以有多個角色,而不同的角色可以有不同的權(quán)限,也可由有相同的權(quán)限。
比如說現(xiàn)在有三個角色,1是普通角色,2也是普通角色,3是管理員,角色1只能查看信息,角色2只能添加信息,管理員都可以,而且還可以刪除信息,類似于這樣。
2. Spring Boot 集成 Shiro 過程
2.1 依賴導(dǎo)入
Spring Boot 2.7.5 集成 Shiro 需要導(dǎo)入如下 starter 依賴:
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency>
2.2 數(shù)據(jù)庫表數(shù)據(jù)初始化
這里主要涉及到三張表:用戶表、角色表和權(quán)限表, 表信息結(jié)構(gòu)如下:
//角色表 DROP TABLE IF EXISTS `role`; CREATE TABLE `role` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `rolename` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色名稱', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of role -- ---------------------------- INSERT INTO `role` VALUES (1, 'admin'); INSERT INTO `role` VALUES (2, 'teacher'); INSERT INTO `role` VALUES (3, 'student'); SET FOREIGN_KEY_CHECKS = 1;
//權(quán)限表 DROP TABLE IF EXISTS `permission`; CREATE TABLE `permission` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `permissionname` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '權(quán)限名', `roleid` int(11) NULL DEFAULT NULL COMMENT '外鍵關(guān)聯(lián)role', PRIMARY KEY (`id`) USING BTREE, INDEX `roleid`(`roleid`) USING BTREE, CONSTRAINT `permission_ibfk_1` FOREIGN KEY (`roleid`) REFERENCES `role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of permission -- ---------------------------- INSERT INTO `permission` VALUES (1, 'user:*', 1); INSERT INTO `permission` VALUES (2, 'student:*', 2); INSERT INTO `permission` VALUES (3, 'user:*', 2); SET FOREIGN_KEY_CHECKS = 1;
//用戶表: DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用戶主鍵', `username` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用戶名', `password` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密碼', `roleid` int(11) NULL DEFAULT NULL COMMENT '外鍵關(guān)聯(lián)role表', PRIMARY KEY (`id`) USING BTREE, INDEX `role_id`(`roleid`) USING BTREE, CONSTRAINT `user_ibfk_1` FOREIGN KEY (`roleid`) REFERENCES `role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of user -- ---------------------------- INSERT INTO `user` VALUES (1, 'csdn1', '123456', 1); INSERT INTO `user` VALUES (2, 'csdn2', '123456', 2); INSERT INTO `user` VALUES (3, 'csdn3', '123456', 3); SET FOREIGN_KEY_CHECKS = 1;
添加一些數(shù)據(jù)如下:
解釋一下這里的權(quán)限: user:* 表示權(quán)限可以是 user:create 或者其他, * 處表示一個占位符,我們
可以自己定義(比如:add表示添加,delete表示刪除,update表示修改,list表示查詢),具體的會在下文 Shiro 配置那里說明。
具體實現(xiàn)如下,相關(guān)的解釋我們放在代碼的注釋中,這樣更加方便直觀:
//自定義reaml public class MyRealm extends AuthorizingRealm { @Resource private UserService userService; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //獲取用戶名 String username = (String) principalCollection.getPrimaryPrincipal(); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); //給該用戶設(shè)置角色,角色信息豐在role表中取 authorizationInfo.setRoles(userService.getRoles(username)); authorizationInfo.setStringPermissions(userService.getPermissions(username)); return authorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //根據(jù)token獲取用戶名, 后面會提講到該token是怎么來的 String username = (String) authenticationToken.getPrincipal(); //根據(jù)用戶名用數(shù)據(jù)庫查詢該用戶 User user = userService.getByUsername(username); if(user != null){ //把當(dāng)前用戶存到session中 SecurityUtils.getSubject().getSession().setAttribute("user",user); //傳入用戶名和密碼進(jìn)行身份認(rèn)證, 并返回認(rèn)證信息 AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(),"myRealm"); return authenticationInfo; } else{ return null; } } }
從上面兩個方法中可以看出:
驗證身份的時候是根據(jù)用戶輸入的用戶名先從數(shù)據(jù)庫中查出該用戶名對應(yīng)的用戶,這時候并沒有涉及到密碼,也就是說到這一步的時候,即使用戶輸入的密碼不對,也是可以查出來該用戶的,然后將該用戶的正確信息封裝到 authcInfo 中返回給 Shiro,接下來就是Shiro的事了,它會根據(jù)這里面的真實信息與用戶前臺輸入的用戶名和密碼進(jìn)行校驗, 這個時候也要校驗密碼了,如果
校驗通過就讓用戶登錄,否則跳轉(zhuǎn)到指定頁面。同理,權(quán)限驗證的時候也是先根據(jù)用戶名從數(shù)據(jù)庫中獲取與該用戶名有關(guān)的角色和權(quán)限,然后封裝到 authorizationInfo 中返回給 Shiro。
2.3 Shiro 配置
自定義的 realm 寫好了,接下來需要對 Shiro 進(jìn)行配置了。我們主要配置三個東西:自定義 realm、安
全管理器 SecurityManager 和 Shiro 過濾器。如下:
配置自定義 realm:
@Configuration public class ShiroConfig { private static final Logger logger = LoggerFactory.getLogger(ShiroConfig.class); /** * 注入自定義的realm * @return MyRealm */ @Bean public MyRealm myAuthRealm() { MyRealm myRealm = new MyRealm(); logger.info("====myRealm注冊完成====="); return myRealm; } /** * 注入安全管理器 * @return SecurityManager */ @Bean public SecurityManager securityManager() { // 將自定義realm加進(jìn)來 DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(myAuthRealm()); logger.info("====securityManager注冊完成===="); return securityManager; } /** * 注入Shiro過濾器 * @param securityManager 安全管理器 * @return ShiroFilterFactoryBean */ @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { // 定義shiroFactoryBean ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean(); // 設(shè)置自定義的securityManager shiroFilterFactoryBean.setSecurityManager(securityManager); // 設(shè)置默認(rèn)登錄的url,身份認(rèn)證失敗會訪問該url shiroFilterFactoryBean.setLoginUrl("/login"); // 設(shè)置成功之后要跳轉(zhuǎn)的鏈接 shiroFilterFactoryBean.setSuccessUrl("/success"); // 設(shè)置未授權(quán)界面,權(quán)限認(rèn)證失敗會訪問該url shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized"); // LinkedHashMap是有序的,進(jìn)行順序攔截器配置 Map<String,String> filterChainMap = new LinkedHashMap<>(); // 配置可以匿名訪問的地址,可以根據(jù)實際情況自己添加,放行一些靜態(tài)資源等,anon表示放行 filterChainMap.put("/css/**", "anon"); filterChainMap.put("/img/**", "anon"); filterChainMap.put("/js/**", "anon"); filterChainMap.put("/swagger-*/**", "anon"); filterChainMap.put("/swagger-ui.html/**", "anon"); // 登錄url 放行 filterChainMap.put("/login", "anon"); // “/user/admin” 開頭的需要身份認(rèn)證,authc表示要身份認(rèn)證 filterChainMap.put("/user/admin*", "authc"); // “/user/student” 開頭的需要角色認(rèn)證,是“admin”才允許 filterChainMap.put("/user/student*/**", "roles[admin]"); // “/user/teacher” 開頭的需要權(quán)限認(rèn)證,是“user:create”才允許 filterChainMap.put("/user/teacher*/**", "perms[\"user:create\"]"); // 配置logout過濾器 filterChainMap.put("/logout", "logout"); // 設(shè)置shiroFilterFactoryBean的FilterChainDefinitionMap shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap); logger.info("====shiroFilterFactoryBean注冊完成===="); return shiroFilterFactoryBean; } }
配置 Shiro 過濾器時會傳入一個安全管理器,可以看出,這是一環(huán)套一環(huán),reaml -> SecurityManager-> filter。在過濾器中,我們需要定義一個 shiroFactoryBean,然后將 SecurityManager 添加進(jìn)來,結(jié)合上面代碼可以看出,要配置的東西主要有:
默認(rèn)登錄的 url:身份認(rèn)證失敗會訪問該 url 認(rèn)證成功之后要跳轉(zhuǎn)的 url 權(quán)限認(rèn)證失敗會訪問該 url 需要攔截或者放行的 url:這些都放在一個 map 中
從上述代碼中可以看出,在 map 中,針對不同的 url,有不同的權(quán)限要求,這里總結(jié)一下常用的幾個權(quán)限。
Dao接口內(nèi)容如下:
@Repository public interface UserDao { @Select("select * from user where username = #{username}") User getByUsername(String username); @Select("select r.rolename from user u,role r " + "where u.roleid = r.id and u.username = #{username}") Set<String> getRoles(String username); //返回集合,避免重復(fù) @Select("select p.permissionname from user u,role r,permission p " + "where u.roleid = r.id and p.roleid = r.id and u.username = #{username}") Set<String> getPermissions(String username); }
UserServiceImpl:
@Service("userService") public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Override public User getByUsername(String username) { return userDao.getByUsername(username); } @Override public Set<String> getRoles(String username) { return userDao.getRoles(username); } @Override public Set<String> getPermissions(String username) { return userDao.getPermissions(username); } }
2.4 使用 Shiro 進(jìn)行認(rèn)證
到這里,我們對 Shiro 的準(zhǔn)備工作都做完了,接下來開始使用 Shiro 進(jìn)行認(rèn)證工作。我們首先來設(shè)計幾個接口:
接口一: 使用 http://localhost:8080/user/admin 來驗證身份認(rèn)證 接口二: 使用 http://localhost:8080/user/student 來驗證角色認(rèn)證 接口三: 使用 http://localhost:8080/user/teacher 來驗證權(quán)限認(rèn)證 接口四: 使用 http://localhost:8080/user/login 來實現(xiàn)用戶登錄
然后來一下認(rèn)證的流程:
流程一: 直接訪問接口一(此時還未登錄),認(rèn)證失敗,跳轉(zhuǎn)到 login.html 頁面讓用戶登錄,登 錄會請求接口四,實現(xiàn)用戶登錄功能,此時 Shiro 已經(jīng)保存了用戶信息了。 流程二: 再次訪問接口一(此時用戶已經(jīng)登錄),認(rèn)證成功,跳轉(zhuǎn)到 success.html 頁面,展示 用戶信息。 流程三: 訪問接口二,測試角色認(rèn)證是否成功。 流程四: 訪問接口三,測試權(quán)限認(rèn)證是否成功。
2.4.1 身份、角色、權(quán)限認(rèn)證接口
@Controller @RequestMapping("/user") public class UserController { @RequestMapping("/logout") public String logout() { SecurityUtils.getSubject().logout(); return "user/login"; } /** * 身份認(rèn)證測試接口 * @param request * @return */ @RequestMapping("/admin") public String admin(HttpServletRequest request) { Object user = request.getSession().getAttribute("user"); return "user/success"; } /** * 角色認(rèn)證測試接口這三個接口很簡單,直接返回到指定頁面展示即可,只要認(rèn)證成功就會正常跳轉(zhuǎn),如果認(rèn)證失敗,就會 跳轉(zhuǎn)到上文 ShrioConfig 中配置的頁面進(jìn)行展示。 2.4.2 用戶登錄接口 * @param request * @return */ @RequestMapping("/student") public String student(HttpServletRequest request) { return "user/success"; } /** * 權(quán)限認(rèn)證測試接口 * @param request * @return */ @RequestMapping("/teacher") public String teacher(HttpServletRequest request) { return "user/success"; } /** * 用戶登錄接口 * @param user user * @param request request * @return string */ @PostMapping("/login") public String login(User user, HttpServletRequest request) { // 根據(jù)用戶名和密碼創(chuàng)建token UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword()); // 獲取subject認(rèn)證主體 Subject subject = SecurityUtils.getSubject(); try{ // 開始認(rèn)證,這一步會跳到我們自定義的realm中 subject.login(token); request.getSession().setAttribute("user", user); return "user/success"; }catch(Exception e){ System.out.println(e.getMessage()); request.getSession().setAttribute("user", user); request.setAttribute("error", "用戶名或密碼錯誤!"); return "/user/login"; } } }
我們重點分析一下這個登錄接口,首先會根據(jù)前端傳過來的用戶名和密碼,創(chuàng)建一個 token,然后使用SecurityUtils 來創(chuàng)建一個認(rèn)證主體,接下來開始調(diào)用 subject.login(token) 開始進(jìn)行身份認(rèn)證了,注意這里傳了剛剛創(chuàng)建的 token,就如注釋中所述,這一步會跳轉(zhuǎn)到我們自定義的 realm 中,進(jìn)入doGetAuthenticationInfo 方法,所以到這里,您就會明白該方法中那個參數(shù) token 了。然后就是上文分析的那樣,開始進(jìn)行身份認(rèn)證。
2.4.3 測試一下
最后,啟動項目,測試一下: