Apache Shiro 已經大名鼎鼎,搞 Java 的沒有不知道的,這類似於 .Net 中的身份驗證 form 認證。跟 .net core 中的認證授權策略基本是一樣的。當然都不知道也沒有關系,因為所有的權限都是模擬的人或機構的社會行為。
本系列從簡單的權限講起,主要涉及到 Shiro、Spring Security、Jwt、OAuth2.0及其他自定義權限策略。
本章主要講解 Shiro 的基本原理與如何使用,本章主要用到以下基礎設施:
- jdk1.8+
- spring boot 2.1.6
- idea 2018.1
1 Spring Boot 快速集成 Shiro 示例
首先我們來一段真實的代碼演示下 Spring Boot 如何集成 Shiro 。本代碼示例暫時沒有使用到數據庫相關知識,本代碼主要使用到:
- shiro
- thymeeaf
本示例演示了網站用戶 admin 密碼 123456 的用戶使用用戶名密碼登錄網站,經過 Shiro 認證后,獲取了授權權限列表,演示了權限的使用。
1.1 新建 Spring Boot Maven 示例工程項目
- File > New > Project,如下圖選擇
Spring Initializr
然后點擊 【Next】下一步 - 填寫
GroupId
(包名)、Artifact
(項目名) 即可。點擊 下一步
groupId=com.fishpro
artifactId=shiro - 選擇依賴
Spring Web Starter
前面打鈎。 - 項目名設置為
spring-boot-study-shiro
.
1.2 依賴引入 Pom.xml
本代碼主要使用到:
- shiro
- thymeeaf
在Pom.xml中加入以下代碼
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.fishpro</groupId>
<artifactId>shiro</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>shiro</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--shiro 1.4.0 thymeleaf-extras-shiro 2.0.0 組合-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!--shiro for thymeleaf 生效需要加入 spring boot 2.x 請使用 2.0.0 版本 否則使用 1.2.1版本-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
<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>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
1.3 配置 application.yml
本示例使用默認配置,暫不需要在 application.yml 中配置 shiro。我們把 application.properties 改成了 application.yml (習慣問題) ,修改了默認端口
server:
port: 8086
1.4 自定義 Realm 領域 UserRealm 實現自定義認證與授權
在 src/main/java/com/java/fishpro/shiro/config(config是新增的包名) 新增 UserRealm.java 文件
UserRealm 是一種安全數據源,用戶登錄認證核心在此類中實現,用戶授權也在此類中實現,具體看代碼注釋
- 重寫了
doGetAuthenticationInfo
實現對用戶名密碼的認證,返回一個SimpleAuthenticationInfo
對象。*注意,因為 shiro 是一個安全框架,具體的身份證明的認證就要交給我們自己去實現,實際上認證是業務邏輯,最好我們自己實現。 - 重寫了
doGetAuthorizationInfo
實現對當前用戶的授權,返回一個SimpleAuthorizationInfo
對象,注意,授權就是從業務系統數據庫中查詢到當前用戶的已知權限列表,寫在當前會話中,以便在使用的時候去做匹配,匹配成功表示授權成功,匹配失敗表示沒授權
//定義一個實體對象用於存儲用戶信息
public class UserDO {
private Integer id;
private String userName;//就是 shiro 中的身份,系統中唯一的存在
private String password; //就是 shiro 中的證明
public Integer getId() {
return id;
}
public void setId(Integer 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;
}
}
在包 config(沒有就新建 config) 下建立 UserRealm
package com.fishpro.shiro.config;
import com.fishpro.shiro.domain.UserDO;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.PrincipalCollection;
import java.security.Principal;
import java.util.*;
/**
* 用戶領域 重寫了 AuthorizingRealm ,AuthorizingRealm(授權) 其實是繼承了 AuthenticatingRealm(認證)
* 所在在這里只要繼承 AuthorizingRealm(授權),主要實現 授權和認證的方法重寫
* 1.doGetAuthenticationInfo 重寫認證
* 2.doGetAuthorizationInfo 重寫授權
* */
public class UserRealm extends AuthorizingRealm {
/**
* doGetAuthenticationInfo 重寫認證
* @param authenticationToken token
* @return 返回認證信息實體(好看身份和證明) AuthenticationInfo
* */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username=(String)authenticationToken.getPrincipal();//身份 例如 用戶名
Map<String ,Object> map=new HashMap<>(16);
map.put("username",username);
String password =new String((char[]) authenticationToken.getCredentials());//證明 例如 密碼
//對身份+證明的數據認證 這里模擬了一個數據源
//如果是數據庫 那么這里應該調用數據庫判斷用戶名密碼是否正確
if(!"admin".equals(username) || !"123456".equals(password)){
throw new IncorrectCredentialsException("賬號或密碼不正確");
}
//認證通過
UserDO user=new UserDO();
user.setId(1);//假設用戶ID=1
user.setUserName(username);
user.setPassword(password);
//建立一個 SimpleAuthenticationInfo 認證模塊,包括了身份】證明等信息
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
return info;
}
/**
* 重寫授權 doGetAuthorizationInfo 返回 授權信息對象 AuthorizationInfo
* @param principalCollection 身份信息
* @return 返回 授權信息對象 AuthorizationInfo
* */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
UserDO userDO = (UserDO)principalCollection.getPrimaryPrincipal();
Integer userId= userDO.getId();//轉成 user 對象
//授權 新建一個授權模塊 SimpleAuthorizationInfo 把 權限賦值給當前的用戶
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//設置當前會話擁有的角色 實際場景根據業務來如從數據庫獲取角色列表
Set<String> roles=new HashSet<>();
roles.add("admin");
roles.add("finance");
info.setRoles(roles);
//設置當前會話可以擁有的權限 實際場景根據業務來如從數據庫獲取角色列表下的權限列表
Set<String> permissions=new HashSet<>();
permissions.add("system:article:article");
permissions.add("system:article:add");
permissions.add("system:article:edit");
permissions.add("system:article:remove");
permissions.add("system:article:batchRemove");
info.setStringPermissions(permissions);
return info;
}
}
1.6 shiro 實現登錄認證
這里主要是顯示 login.html 與 LoginController
1.6.1 登錄 html 頁面
新增文件 resources/templates/login.html 表示登錄頁面,這里使用 jquery 來實現邏輯
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>使用 shiro 登錄頁面</title>
</head>
<body>
<div>
<input id="userName" name="userName" value="">
</div>
<div>
<input id="password" name="password" value="">
</div>
<div>
<input type="button" id="btnSave" value="登錄">
</div>
<script src="https://cdn.bootcss.com/jquery/1.11.3/jquery.js"></script>
<script>
$(function() {
$("#btnSave").click(function () {
var username=$("#userName").val();
var password=$("#password").val();
$.ajax({
cache: true,
type: "POST",
url: "/login",
data: "userName=" + username + "&password=" + password,
dataType: "json",
async: false,
error: function (request) {
console.log("Connection error");
},
success: function (data) {
if (data.status == 0) {
window.location = "/index";
return false;
} else {
alert(data.message);
}
}
});
});
});
</script>
</body>
</html>
1.6.2 登錄邏輯
在 UserController中新增兩個方法, 路由都是 /login,一個是get 一個是post,因為登錄頁面是不需要認證,所有兩個路由都是 /login 的頁面不需要進行認證就可以訪問。
//get /login 方法,對應前端 login.html 頁面
@GetMapping("/login")
public String login(){
return "login";
}
//post /login 方法,對應登錄提交接口
@PostMapping("/login")
@ResponseBody
public Object loginsubmit(@RequestParam String userName,@RequestParam String password){
Map<String,Object> map=new HashMap<>();
//把身份 useName 和 證明 password 封裝成對象 UsernamePasswordToken
UsernamePasswordToken token=new UsernamePasswordToken(userName,password);
//獲取當前的 subject
Subject subject = SecurityUtils.getSubject();
try{
subject.login(token);
map.put("status",0);
map.put("message","登錄成功");
return map;
}catch (AuthenticationException e){
map.put("status",1);
map.put("message","用戶名或密碼錯誤");
return map;
}
}
1.7 shiro 實現Controller層方法授權
這里需要增加幾個頁面來實現這個功能
1.7.1 resources/templates/index.html 登陸成功后跳轉的頁面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>通過登錄驗證后跳轉到此頁面</title>
</head>
<body>
通過登錄驗證后跳轉到此頁面
<div>
<a href="/article">前往文章頁面</a>
</div>
<div>
<a href="/setting">前往設置頁面</a>
</div>
</body>
</html>
1.7.2 resources/templates/article.html 已授權訪問的頁面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>必須獲取 app:article 授權</title>
</head>
<body>
必須獲取 app:article 授權 才會顯示
</body>
</html>
1.7.3 resources/templates/setting.html 未授權訪問的頁面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>必須獲取 app:setting 授權 </title>
</head>
<body>
必須獲取 app:setting 授權 才會顯示
</body>
</html>
1.7.4 resources/templates/error/403.html 未授權統一吹頁面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>403 沒有授權</title>
</head>
<body>
你訪問的頁面沒有授權
</body>
</html>
1.7.5 controller/UserController.java Controller層方法
如下源代碼
- 方法 article 需要權限 app:article:article 才能進入
- 方法 setting 需要權限 app:setting:setting 才能進入
@Controller
public class UserController {
//shiro 認證成功后默認跳轉頁面
@GetMapping("/index")
public String index(){
return "index";
}
@GetMapping("/403")
public String err403(){
return "403";
}
/**
* 根據權限授權使用注解 @RequiresPermissions
* */
@GetMapping("/article")
@RequiresPermissions("app:article:article")
public String article(){
return "article";
}
/**
* 根據權限授權使用注解 @RequiresPermissions
* */
@GetMapping("/setting")
@RequiresPermissions("app:setting:setting")
public String setting(){
return "setting";
}
@GetMapping("/login")
public String login(){
return "login";
}
@PostMapping("/login")
@ResponseBody
public Object loginsubmit(@RequestParam String userName,@RequestParam String password){
Map<String,Object> map=new HashMap<>();
//把身份 useName 和 證明 password 封裝成對象 UsernamePasswordToken
UsernamePasswordToken token=new UsernamePasswordToken(userName,password);
//獲取當前的 subject
Subject subject = SecurityUtils.getSubject();
try{
subject.login(token);
map.put("status",0);
map.put("message","登錄成功");
return map;
}catch (AuthenticationException e){
map.put("status",1);
map.put("message","用戶名或密碼錯誤");
return map;
}
}
}
1.8 shiro 實現前端頁面中授權
我們使用了 Thymeleaf 作為前端的模板引擎,您也可以使用 JSP,FreeMarker 等引擎。Shiro 已經能夠很好的在 Thymeleaf 中使用,如下代碼我在首頁中使用
如下代碼,因為沒有 app:setting:setting 權限所有【前往設置頁面】不會顯示
<hr/>
<div>
<title style="color:red;">注意下面是包括權限的,第二個鏈接因為沒有授權是不可見的</title>
</div>
<div shiro:hasPermission="app:article:article">
<a href="/article">前往文章頁面</a>
</div>
<div shiro:hasPermission="app:setting:setting">
<a href="/setting">前往設置頁面</a>
</div>
1.9 shiro 在程序代碼塊中使用授權判斷
1.9.1 通過角色判斷
Subject subject = SecurityUtils.getSubject();
String str="";
if(subject.hasRole("admin")){
str=str+"您擁有 admin 權限";
}else{
str=str+"您沒有 admin 權限";
}
if(subject.hasRole("sale")){
str=str+"您擁有 sale 權限";
}
else{
str=str+"您沒有 sale 權限";
}
1.9.2 通過權限判斷
注意這里是直接拋出異常,會被全局異常捕捉
Subject subject = SecurityUtils.getSubject();
try{
subject.checkPermission("app:setting:setting");
str=str+"您擁有 app:setting:setting 權限";
}catch (UnauthenticatedException ex){
str=str+"您沒有 app:setting:setting 權限";
}
為什么我的注解沒生效?
要使用 shiro 注解來授權 Controller 的方法,那么需要在 ShiroConfig 中加入以下代碼
/**
* 開啟shiro aop注解支持 如@RequiresRoles,@RequiresPermissions
* 使用代理方式;所以需要開啟代碼支持;
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
1.10 登出/注銷
調用 subject 的logout 方法進行注銷
@GetMapping("/logout")
String logout(HttpSession session, SessionStatus sessionStatus, Model model) {
//會員中心退出登錄 當使用這兩屬性session屬性退出
session.removeAttribute("userData");
sessionStatus.setComplete();
SecurityUtils.getSubject().logout();
return "redirect:/login";
}
1.11 問題
@RequiresPermissions
注解無效
注解無效,沒有走到注解,基本就是AOP攔截問題,需要在 ShiroConfig 配置中增加配置
- spring boot shiro Not authorized to invoke method 當
@RequiresPermissions
中的權限沒有的時候發生
@RequiresPermissions
既然生效了,那為什么又會報錯呢,按道理已經登錄,但是沒有權限的方法體,應該跳轉到/403 頁面才對。
這里應該也是沒有攔截到方法這個錯。這個在 Spring Boot 全局異常處理 中講過,需要使用到異常捕捉機制,捕捉到這個異常org.apache.shiro.authz.UnauthorizedException
,然后做統一處理。
@ControllerAdvice(annotations = Controller.class)
public class MyExceptionController {
private static final Logger logger= LoggerFactory.getLogger(MyExceptionController.class);
public static final String DEFAULT_ERROR_VIEW = "error";
@ExceptionHandler(value = UnauthorizedException.class)//處理訪問方法時權限不足問題
public String defaultErrorHandler(HttpServletRequest req, Exception e) {
return "error/403";
}
}
- shiro:hasPermission 標簽在 thymeleaf 中不生效問題
shiro:hasPermission 標簽應用在 thymeleaf ,由於涉及到兩個框架,如果原生不支持,那么比如要引入第三方控件。
/**
* ShiroDialect,為了在thymeleaf里使用shiro的標簽的bean
* @return
*/
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
- 有的時候回報錯 org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'shiroDialect'
這個應該是第三方插件 com.github.theborakompanioni 與 spring boot 版本兼容性問題。我改為了以下版本 shiro for thymeleaf 生效需要加入 spring boot 2.x 請使用 2.0.0 版本
<!--shiro 1.4.0 thymeleaf-extras-shiro 2.0.0 組合-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!--shiro for thymeleaf 生效需要加入 spring boot 2.x 請使用 2.0.0 版本 否則使用 1.2.1版本-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>