首先開始前,在這裏吹個牛,如果願意仔細花時間看完這篇文章,如果還不會shiro,直播喫屎(就是這麼自信)
本文代碼示例已放入github:請點擊我
快速導航-------->src.main.java.yq.Shiro
1.Apache Shiro是什麼?
答:ApacheShiro是Java安全框架,執行身份驗證、授權、密碼和會話管理
2.爲什麼使用Apache Shiro?
答:Apache Shiro功能強大,使用簡單,快速上手而且相對獨立,不依賴其他框架,從最小的移動應用程序到最大的網絡和企業應用程序都可以使用Shiro作爲安全框架。
3.怎麼使用Apache Shiro?
首先項目主要技術:Springboot2.1.6,shiro1.3.2,jjwt0.7.0,Jpa,Mysql等
其中jjwt(Json Web Token)我用來生產登錄token以及密碼加密和解密
其次就是該Dome的模式採用Token模式,也就是用戶登錄之後會返回一個Token,後續關鍵請求基於Token進行身份驗證,從而達到取代session的作用
在使用之前我們先了解一下Shiro的主要功能,以及執行流程
Shiro三大核心組件:Subject SecurityManager Realms.
Subject:即“當前操作用戶”。但是,在Shiro中,Subject這一概念並不僅僅指人,也可以是第三方進程、後臺帳戶(Daemon Account)或其他類似事物。它僅僅意味着“當前跟軟件交互的東西”。(獲取用戶信息,用戶實例)
SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通過SecurityManager來管理內部組件實例,並通過它來提供安全管理的各種服務。(核心,中央處理器)
Realm: Realm充當了Shiro與應用安全數據間的“橋樑”或者“連接器”。也就是說,當對用戶執行認證(登錄)和授權(訪問控制)驗證時,Shiro會從應用配置的Realm中查找用戶及其權限信息。(也就是管理用戶登錄和授權)
從這個意義上講,Realm實質上是一個安全相關的DAO:它封裝了數據源的連接細節,並在需要時將相關數據提供給Shiro。當配置Shiro時,你必須至少指定一個Realm,用於認證和(或)授權。配置多個Realm是可以的,但是至少需要一個
授權流程圖:
接下來我開始詳細講解:
- 1.創建我們的SpringBoot項目以及加入核心依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>yq</groupId>
<artifactId>test</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<!-- 引入jwt依賴 使用jwt協議進行單點token登錄 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<!-- 引入shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!-- <!– https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security –>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-security</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.30</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- <!–orcale數據庫–>-->
<!-- <!– https://mvnrepository.com/artifact/com.jslsolucoes/ojdbc6 –>-->
<!-- <dependency>-->
<!-- <groupId>com.jslsolucoes</groupId>-->
<!-- <artifactId>ojdbc6</artifactId>-->
<!-- <version>11.2.0.1.0</version>-->
<!-- </dependency>-->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
<resources>
<!--非class應均在該目錄下-->
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
</resource>
</resources>
</build>
</project>
這裏我就不多介紹jar的的作用了
- 2.搭建一個Web項目的相關準備
/*
Navicat Premium Data Transfer
Source Server : 192.168.0.21
Source Server Type : MySQL
Source Server Version : 80015
Source Host : 192.168.0.21:3306
Source Schema : workorder
Target Server Type : MySQL
Target Server Version : 80015
File Encoding : 65001
Date: 12/08/2019 16:31:47
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for test
-- ----------------------------
DROP TABLE IF EXISTS `test`;
CREATE TABLE `test` (
`id` bigint(20) NOT NULL,
`user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`phone` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`pass_word` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`role` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`turisdiction` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of test
-- ----------------------------
INSERT INTO `test` VALUES (16168479940608, '張三', '17549684489', 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTYifQ.q5OCp5vPp2XnwLCxqcxexnu341YbNd0987xJiVY_Qew', 'admin', 'all');
INSERT INTO `test` VALUES (16168534069248, '李四', '17549684489', 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTYifQ.q5OCp5vPp2XnwLCxqcxexnu341YbNd0987xJiVY_Qew', 'user', 'check');
SET FOREIGN_KEY_CHECKS = 1;
這是一個數據庫的表的信息,如下圖:
至於字段信息請看下面的實體類:
@Data
@Entity
public class Test {
@Id
private Long id; //數據庫主鍵
private String phone; //電話號碼
private String passWord; //密碼
private String userName; //用戶名
private String role; //角色
private String turisdiction; //權限 使用英文 , 隔開
}
現在我們有了實體類,有了數據庫,就開始創建一個dao層和Controller層,至於service層不是重點,就不寫出來了
這就是我們的dao層,因爲項目簡單就不用拓展接口,直接使用jpa自帶的完全滿足業務需求
@Repository
public interface MySqlMapper extends JpaRepository<Test,Long> {
}
由於我們在數據庫中的密碼進行了加密,而且我們要生產我們的Token所以這裏我們封裝了使用jwt對字符串加密的一個服務類
/**
* 使用jjwt實現的token生成策略 以及密碼加密策略
*/
@Slf4j
public class TokenService {
// "iss":"Issuer —— 用於說明該JWT是由誰簽發的",
// "sub":"Subject —— 用於說明該JWT面向的對象",
// "aud":"Audience —— 用於說明該JWT發送給的用戶",
// "exp":"Expiration Time —— 數字類型,說明該JWT過期的時間",
// "nbf":"Not Before —— 數字類型,說明在該時間之前JWT不能被接受與處理",
// "iat":"Issued At —— 數字類型,說明該JWT何時被簽發",
// "jti":"JWT ID —— 說明標明JWT的唯一ID",
// "user-definde1":"自定義屬性舉例",
// "user-definde2":"自定義屬性舉例"
//讀取配置文件 祕匙 (這是用來後面接收到token的時候用於解密用的祕匙
private String secretKey;
//過期時間 兩週
private Long outTime_towWeeks;
@Autowired
private Environment environment;
@PostConstruct
private void inir() {
this.secretKey = environment.getProperty("secretKey");
Integer outTime = Integer.parseInt(environment.getProperty("outTime"));
//過期時間兩週
this.outTime_towWeeks = outTime * 1000L * 60 * 60 * 24;
log.info("JWTTokenUtil初始化完成,secretKey爲:{} ,loginToken過期時間爲:{}", secretKey, outTime_towWeeks);
}
/**
* 字符串加密 如果參數type不是null 那麼就是用戶token生成。那是需要過期時間的
* 如果type爲null 那麼就是密碼加密 是不需過期時間的
* @param subject 傳遞的字符串
* @param type 需要加密類型 如果
* @return
*/
private String createJWT(String subject,String type) {
//加密算法
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(secretKey);
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
//創建jwt對象
JwtBuilder builder = Jwts.builder()
.setSubject(subject)
.signWith(signatureAlgorithm, signingKey);
//如果是null 就表示是密碼加密 密碼加密是不需要執行過期時間的
if(StringUtils.isEmpty(type)){
return builder.compact();
}
//設置兩週之後過期
builder.setExpiration(new Date(System.currentTimeMillis()+outTime_towWeeks));
return builder.compact();
}
/**
* token解密過程
* @param jwtToken token
* @return 解密後的值
*/
public String parseJWT(String jwtToken) {
Claims claims = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(secretKey))
.parseClaimsJws(jwtToken).getBody();
Date expiration = claims.getExpiration();
//表示沒有設置過期時間
if(expiration == null){
return claims.getSubject();
}
//表示已經過期
if(System.currentTimeMillis() >= expiration.getTime()){
throw new DIYException("token已經過期");
}
return claims.getSubject();
}
/**
* 用戶密碼加密
* @param passWord 原密碼
* @return 加密之後的密碼
*/
public String passWordEncryption(String passWord){
return createJWT(passWord, null);
}
/**
* 創建用戶token
* @param param 需要封裝的參數的String類型
* @return 生成的用戶token
*/
public String createToken(String param){
return createJWT(param, "create");
}
}
這就是我們的對字符串加密解密以及生產token的服務類了,但是我們這裏沒有馬上使用@Service注入到容器之中,至於爲什麼,我們後面會講到
這裏會讀取兩個配置文件,如下:
secretKey: myKey
#過期時間 單位天數
outTime: 15
接下來我們創建我們的Controller層
@RestController
public class TestShiroController extends BaseApiService {
@Autowired
private TokenService tokenService;
@Autowired
private MySqlService mySqlService;
/*
用戶登錄 需要傳遞用戶郵箱和密碼
*/
@PostMapping(value = "/user/login")
public ResponseBase login(@RequestBody Test test) {
//根據id查詢Test
Test testById = mySqlService.getTestById(test.getId());
//判斷不能爲null
Assert.notNull(testById,"用戶賬號錯誤");
//獲取到加密之後的password
String encryptionPassWord = tokenService.parseJWT(testById.getPassWord());
//密碼判斷
if(! test.getPassWord().equals(encryptionPassWord)){
throw new IllegalArgumentException("密碼錯誤");
}
Subject subject = SecurityUtils.getSubject();
//設置登錄token 過期時間爲30分鐘
String token = tokenService.createToken(JSON.toJSONString(testById));
//這個類 是我們繼承與shiro的AuthenticationToken 這樣就可以做一些定製化的東西
NewAuthenticationToken newAuthenticationToken = new NewAuthenticationToken(testById.getPhone(), token);
//登錄操作
subject.login(newAuthenticationToken);
//返回客戶端數據
JSONObject jsonObject = new JSONObject();
jsonObject.put(AuthFilter.TOKEN, token);
return setResultSuccessData(jsonObject.toString(), "用戶登錄成功");
}
@PostMapping(value = "/api/test001")
public ResponseBase test001(){
return setResultSuccess("測試登錄成功");
}
//測試權限使用
@PostMapping(value = "/api/testRole")
public ResponseBase testRole(){
return setResultSuccess("測試角色成功");
}
//測試權限使用
@PostMapping(value = "/api/testPerms")
public ResponseBase testPerms(){
return setResultSuccess("測試權限成功");
}
}
可以看到我們這裏有四個接口,很簡單的接口,分別是測試登錄。驗證是否登錄,以及測試角色,和測試權限的四個接口
在這裏登錄的時候會使用到一個類 NewAuthenticationToken 這個類是我們自定義的但是是繼承與shiro的AuthenticationToken類,爲什麼要繼承他呢,這樣我們就可以更加透明化的知道shiro登錄的流程,以及可以定製化一些東西
shiro登錄流程:subject.login(AuthenticationToken authenticationToken) --> realm.doGetAuthenticationInfo(AuthenticationToken authenticationToken)
爲什麼需要這麼一個東西呢(AuthenticationToken):我們可以點進去看源碼的註釋,簡單點說,這個東西就是在我們執行了subject.login()方法之後會執行MyRealm的doGetAuthenticationInfo方法進行登陸,而進行獲取證明身份的數據
(自定義的NewAuthenticationToken 以及 Realm 這個我們後面講,我們先從簡單的零件講)
好了,到了這裏我們準備工作做完了,name馬上涉及到Shiro最核心的部分了
- 3.開始搭建shiro
首先我們創建一個 NewAuthenticationToken 繼承於 AuthenticationToken 爲了就是定製化以及更加了解shiro登錄流程
/**
* 用戶身份驗證的憑證
*/
@Data
//生成默認構造器
@NoArgsConstructor
//生產帶所有屬性的構造器
@AllArgsConstructor
public class NewAuthenticationToken implements AuthenticationToken {
private String phone;
private String token;
//得到主體
@Override
public Object getPrincipal() {
return this.phone;
}
//得到憑證
@Override
public Object getCredentials() {
return this.token;
}
}
這個類一出來,應該就明白了爲什麼在Controller的時候我們傳遞了一個用戶的電話號碼(因爲是唯一的)和一個用戶登錄的token了吧,但是作用呢,我們後面再講。
NewAuthenticationToken newAuthenticationToken = new NewAuthenticationToken(testById.getPhone(), token);
接下來我們創建一個shiro的三大核心之一的MyRealm
@Service
public class MyRealm extends AuthorizingRealm {
@Autowired
private MySqlService mySqlService;
@Autowired
private TokenService tokenService;
/**
* 大坑!,必須重寫此方法,不然Shiro會報錯
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof NewAuthenticationToken;
}
/**
* 保存角色和權限
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
Long testId = (Long) principals.getPrimaryPrincipal();
Test testById = mySqlService.getTestById(testId);
if(testById == null){
throw new IllegalArgumentException("錯誤的角色");
}
//在這裏給用戶角色進行授權
//在這裏拿到用戶的信息 並且賦值角色和權限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//設置用戶角色
simpleAuthorizationInfo.addRole(testById.getRole());
//添加角色的權限
simpleAuthorizationInfo.addStringPermissions(Arrays.asList(testById.getTurisdiction().split(",")));
return simpleAuthorizationInfo;
}
/**
* 身份認證
* @param auth
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
//因爲我們在用戶登錄的時候傳遞的參數 主體就是電話號碼
String phone = auth.getPrincipal().toString();
//證明用戶信息的東西
String token = auth.getCredentials().toString();
//因爲我們傳遞的是json類型的Test對象
String jsonTest = tokenService.parseJWT(token);
Test test = JSON.parseObject(jsonTest, Test.class);
if(! test.getPhone().equals(phone)){
throw new AuthenticationException("用戶身份驗證失敗");
}
//保存用戶信息?test, token, "my_realm"
return new SimpleAuthenticationInfo(test.getId(),token,"myRealm");
}
}
這個類非常重要,首先我們看到了兩個方法,這兩個方法是非常重要的
doGetAuthorizationInfo:該方法就是用來對登錄的用戶進行角色賦值和權限賦值的,這個方法不會立馬執行,會在進行角色判斷或者權限判斷的時候才執行該方法。這裏我們就暫時不說。
doGetAuthenticationInfo:該方法就是用來登錄的,在使用subject.login的時候會調用該方法,從代碼中可以看到我們可以使用AuthenticationToken這個類調用getPrincipal方法獲取主體信息,getCredentials方法獲取憑證信息,而我們就可以利用該信息進行剛剛登錄的用戶身份驗證(我這裏覺得這個身份驗證不是很有必要,因爲我們在Controller已經進行了身份驗證)
在執行了doGetAuthenticationInfo方法的時候我們看到了如下代碼
return new SimpleAuthenticationInfo(test.getId(),token,"myRealm");
那麼返回的這個對象又是幹什麼的呢?我們點進去可以看到:
簡單點說,這個對象就是保存的我們的用戶登錄的信息,第一個參數同樣是主體,第二個參數同樣是證明,第三個參數就是使用的什麼 realm 那麼他作用是什麼?說簡單點,他的作用就是我們在後面進行身份驗證的時候可以使用subject.getPrincipal()進行判斷用戶是否登錄。
所以看到這裏,我們大致理一下shiro是怎麼登錄的,又是怎麼判斷用戶登錄的:
首先使用subject.login(authenticationToken)方法調用realm中的doGetAuthenticationInfo方法進行獲取用戶登錄的信息進行身份驗證,驗證通過的時候保存到AuthenticationInfo的實現類SimpleAuthenticationInfo中的,然後我們就可以使用subject.getPrincipal()獲取主體對象是否爲null或者是我們指定的類型來判斷用戶是否登錄
首先這裏有兩個問題是我也遇到的這裏就爲大家解讀一下:
這裏必讀:
1.在用戶身份驗證成功的時候SimpleAuthenticationInfo的主體是傳遞username還是傳遞user,這是引用百度的上網友的問題,那麼這裏我們就應該是傳遞Id還是傳遞Test對象呢,這個還是根據情況來定,如果我們使用的shiro是的緩存是基於Redis的話,那麼還是推薦是喲Id也就是唯一的主鍵進行保存爲主體,但是我們這個項目的保存對象是基於session的,所以就對於主體保存test對象還是id主鍵沒有太多要求,都可以,說實話直接保存test會方便很多,但是爲了演示效果我們這裏還是使用的保存id。
2.使用subject.getPrincipal()能返回當前的用戶主體對象,那麼問題來了,shiro是怎麼知道返回的是哪個對象呢?這個問題就是shiro在登錄的時候會把用戶信息進行綁定到當前的線程中特就是threadLocal裏面,在基於瀏覽器的cookie和session進行的身份驗證,那麼如果session和cookie失效了怎麼辦,所以這就是爲什麼還會有一個subject.credentials()得到用戶憑證的原意。
具體想了解爲什麼上面兩個原因可以自行百度,我們不過多詳解,瞭解即可
好了我們realm完成了,接下來就是核心的shiroConfig了,也就是SecurityManager相關
@Configuration
public class ShiroConfig {
@Autowired
private TokenService tokenService;
@Bean
public TokenService tokenService(){
return new TokenService();
}
//常量一:表示是角色
public static final String CONS_TYPE_ONE = "ROLE";
//常量二:表示是權限
public static final String CONS_TYPE_TWO = "PERM";
/**
* 權限管理 核心安全事務管理器
* @param realm
* @return
*/
@Bean("securityManager")
public DefaultWebSecurityManager getManager(MyRealm realm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
// 使用自己的realm
manager.setRealm(realm);
return manager;
}
//Filter工廠,設置對應的過濾條件和跳轉條件
@Bean("shiroFilter")
public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, Filter> myFilters = new HashMap<>();
myFilters.put("authFilter",new AuthFilter(tokenService()));
shiroFilterFactoryBean.setFilters(myFilters);
Map<String,String> map = new LinkedHashMap<>();
//用戶登錄 自由訪問
map.put("/user/login","anon");
map.put("/static/**","anon");
//需要admin角色
map.put("/api/testRole","authFilter["+CONS_TYPE_ONE+",admin]");
//需要test權限才能訪問
map.put("/api/testPerms","authFilter["+CONS_TYPE_TWO+",test]");
// shiroFilterFactoryBean.setUnauthorizedUrl("/user/error");
//其他的api請求都需要認證
map.put("/api/**","authFilter");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
/**
* 下面的代碼是添加註解支持 aop(用於解決註解不生效的原因
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
/**
* 管理shirobean的生命週期
* @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 加入註解
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
這裏需要說的事情不多,首先就是配置securityManager,指定我們自己realm進行認證
其次就是我們的shiroFilter過濾器的配置
這裏我們的角色認證和權限認證,以及身份驗證都是使用的自定義的authFilter進行實現的,當然也可以使用shiro自帶的過濾器進行實現,但是爲了更好的理解shiro的認證流程和原理我還是使用了自定義的filter進行實現該功能
shior幾大攔截器:https://blog.csdn.net/fenglixiong123/article/details/77119857
可以參考該文章瞭解一下shiro的幾大攔截器的作用以及怎麼配置,
從我們的shiroFilter中我們可以看到我們配置了:
/user/login anon :意思就是不需要身份驗證,都可以進行訪問
/api/testRole authFilter["+CONS_TYPE_ONE+",admin] : 意思就是這個接口需要admin的角色纔可以訪問,至於爲什麼要這麼寫,因爲我們這哥filter過濾器實現了三種功能,分別是身份驗證,和角色驗證以及權限認證,所以我們只能根據[]內的第一個參數進行判斷是角色認證還是權限認證,所以第一個參數是寫死的,同樣我們可以添加多種角色和權限,只需要使用英文的逗號進行隔開 所以這就是我們這樣寫的作用
/api/** authFilter :表示以api開頭的接口需要進行身份驗證,這裏同樣使用我們的自定義的接口
另外在這裏我說一下有幾個坑:
必看:
1.首先自定的filter過濾器不能使用@Service或者@Component交給Spring進行管理,這樣會導致我們配置的過濾策略找不到,會報錯:org.apache.shiro.UnavailableSecurityManagerException: No SecurityManager accessible to the calling code, either bound to the org.apache.shiro.util.ThreadContext or as a vm static singleton. This is an invalid application configuration.
所以要使用:
Map<String, Filter> myFilters = new HashMap<>();
myFilters.put("authFilter",new AuthFilter(tokenService()));
shiroFilterFactoryBean.setFilters(myFilters);
以下方式進行注入到shiro的filter管理器中,
2.那就是配置filter過濾策略的順序 map.put("/api/**","authFilter"); --->一定要放在權限後面,不然會覆蓋,導致我們的角色認證和權限認證失效。
3.爲什麼TokenService不在生成的時候使用註解@Component注入容器呢?因爲我們自定的的AuthFilter過濾器會依賴這個服務,但是使用@Autowired會找不到,因爲可能shiro的相關配置會先於spring的執行,具體原因有待發掘,所以我們這裏只能使用AuthFilter的構造函數進行注入,然後在調用方法之後手動的把TokenService進行注入到容器之中
那麼接下來就開啓我們的手寫過濾器實現權限認證,角色認證以及用戶認證之AuthFilter
/**
* 用於角色身份驗證
*/
@Slf4j
public class AuthFilter extends AuthorizationFilter {
//token
private final TokenService tokenService;
public AuthFilter(TokenService tokenService){
this.tokenService = tokenService;
System.out.println(tokenService);
}
public static final String TOKEN = "token";
private Subject getSubject(){
return SecurityUtils.getSubject();
}
/**
* 身份認證方法
*/
private Boolean authorization(HttpServletRequest request, HttpServletResponse response) {
try {
//獲取token並解析
String token = request.getHeader(TOKEN);
Assert.hasLength(token,"token不能爲空");
String jsonTest = tokenService.parseJWT(token);
Test test = JSON.parseObject(jsonTest, Test.class);
Assert.notNull(test,"錯誤的token");
Subject subject = getSubject();
//表示還沒有登錄 爲什麼這裏要這麼寫 就是因爲shiro我們的緩存是基於session,cookie等
//如果服務重啓了 或者沒了cookiet咋辦,所以我們就在這裏掉用一下shiro的登錄
if(subject.getPrincipal() == null){
//那就登錄
subject.login(new NewAuthenticationToken(test.getPhone(),token));
return true;
}
//如果是已經登錄的 就進行身份驗證
Long testId = (Long) subject.getPrincipal();
if(! test.getId().equals(testId)){
return false;
}
return true;
} catch (Exception e) {
log.error("用戶驗證失敗的地址:{}",request.getRequestURL());
log.error("錯誤原因:{}",e.getMessage());
response.setHeader("messgae",e.getMessage());
return false;
}
}
/**
* 認證失敗
* @param response
*/
private void authorizationFailure(HttpServletResponse response){
try{
//認證失敗 之後返回頁面的數據
response.setContentType("application/json;charset=utf-8");
//封裝一個map返回頁面
HashMap<Object, Object> result = new HashMap<>();
result.put("data","null");
result.put("message",response.getHeader("message"));
result.put("rtnCode","401");
response.getWriter().append(JSON.toJSONString(result));
}catch (Exception e){
log.error("響應錯誤:{}",e.getMessage());
}
}
/**
* 權限認證的方法
* @param perms
* @param response
* @param request
* @return
*/
private Boolean permissions(String[] perms,HttpServletResponse response,HttpServletRequest request){
Boolean result = false;
try{
Subject subject = getSubject();
//調用方法進行判斷權限
if(! subject.isPermittedAll(perms)){
throw new Exception("您沒有該訪問權限");
}
result = true;
}catch (Exception e){
log.error("角色不對應導致無訪問權限的地址:{}"+request.getRequestURL());
log.error("錯誤原因:{}",e.getMessage());
response.setHeader("message",e.getMessage());
}finally {
return result;
}
}
//角色認證認證
private Boolean roles(List<String> roles, HttpServletResponse response, HttpServletRequest request){
Boolean result = false;
try{
if(roles == null || roles.size() <= 0){
return true;
}
Subject subject = getSubject();
//調用方法判斷 是否存在指定角色
if(! subject.hasAllRoles(roles)){
throw new Exception("沒有該訪問權限");
}
result = true;
}catch (Exception e){
log.error("沒有權限的訪問地址:{}",request.getRequestURL());
log.error("錯誤原因:{}",e.getMessage());
response.setHeader("message",e.getMessage());
}finally {
return result;
}
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
String[] values = (String[]) mappedValue;
HttpServletRequest newRequest = (HttpServletRequest) request;
HttpServletResponse newResponse = (HttpServletResponse) response;
Subject subject = getSubject();
//身份認證
if(values == null){
return authorization(newRequest,newResponse);
}
//表示是角色認證
if(values[0].equals(ShiroConfig.CONS_TYPE_ONE)){
//因爲我們使用asList轉換代碼爲List的時候不是util包下面的List而是array下面的,
// 所以我們需要轉換爲util包下的,才能執行remove方法
//那麼爲什麼我們需要刪除第0個呢?就是因爲我們在shiroConfig的時候配置的過濾策略
//因爲我們的自定的authFilte需要執行的認證種類太多,所以需要第一個參數進行判斷類型,
//但是這第零個參數又是屬於權限和角色範圍,所以在類型判斷之後需要刪除
List<String> strings = Arrays.asList(values);
List<String> params = new ArrayList<>(strings);
params.remove(0);
//調用角色認證方法
return roles(params,newResponse,newRequest);
}
//權限認證
if(values[0].equals(ShiroConfig.CONS_TYPE_TWO)){
//同上 一樣的意思
List<String> strings = Arrays.asList(values);
List<String> params = new ArrayList<>(strings);
params.remove(0);
//因爲 我們的權限認證方法的參數是需要的是 String... 類型
// 但是String[] 有沒有刪除第一個的實現,所以就比較麻煩先轉換list刪除第一個,然後又轉換回去
String[] perm = new String[params.size()];
String[] newPerm = params.toArray(perm);
//調用權限認證方法
return permissions(newPerm,newResponse,newRequest);
}
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
//如果權限或者角色也或者是角色認證不通過
authorizationFailure((HttpServletResponse) response);
return false;
}
}
我想我這裏爲什麼需要這樣寫代碼裏面的註釋已經寫得很清楚了,但是我還是簡單概括一下
1.爲什麼使用構造函數注入tokenService,因爲使用@Autowired註解注入會找不到
2.爲什麼我們shiroConfig中的shiroFilter的關於權限和角色配置,會在authFilter的第一個參數傳遞兩個常量,就是因爲我們的過濾器是有三種功能,身份認證-->這個不需要參數,但是權限認證和角色是需要傳遞角色和權限的,而第一個常量就是用來區別到底是角色認證還是權限認證。但是常量又不是屬於角色和權限裏面的,所以在判斷出是角色或者權限之後要刪除掉第一個常量,角色和權限都可以傳遞多個參數,中間使用英文逗號分隔,在AuthFilter中的mappedValue參數就可以獲取到我們在shiroFilter中配置過濾策略的時候傳遞的參數。
3.onAccessDenied:表示的是驗證失敗執行的地方,isAccessAllowed:是執行驗證方法地方
到了這裏基本上shiro的核心幾個文件就講的很清楚了,實際上很簡單,就是兩個都可以解決,那就是MyRealm和ShirlConfig
那到了這裏我們的shiro的執行流程就很清晰了,那就是登錄的時候使用subject,login()進行登路,然後會調用MyRealm中的doGetAuthorizationInfo進行身份驗證,以及保存當前登錄對象的一些信息,可以用來獲取身份信息。
然後在需要角色認證或者權限認證的時候,首先活進入到Filter過濾器中,由於我們這裏配置的是自定義的過濾器,所以在需要角色認證或者權限認證以及身份認證(是否登錄)的時候會先進入到我們的AuthFilter過濾器中,然後判斷是那種驗證(身份,權限,以及角色)並執行響應的過濾流程,當然如果我們不適用自定義的,使用shiro自帶的anon(不需要認證),authc(身份認證,也就是必須要登錄),roles[?,?](角色認證),perms[?,?,?](權限認證)----->角色和權限都是需要使用引文逗號分隔
可以參考:https://blog.csdn.net/fenglixiong123/article/details/77119857 shiro幾大攔截器
然後當需要角色或者權限認證的時候會執行MyReaml中的doGetAuthorizationInfo方法 就這樣shiro的整個登錄以及驗證流程就完畢了。
接下來我們看一看我們執行的結果吧:
首先我們在我們的AuthFilter中的角色認證,權限認證,以及身份認證打上斷點一會debug調試
好了,斷點有了我們使用具有admin角色的賬號登錄:
發現我們的攔截器並沒有進入?因爲我們配置的/user/login的過濾策略是anon,表示不需要身份認證直接訪問,而且我們看到了我們的登錄返回的token
那麼繼續
我們測試帶api開頭的接口,因爲我們過濾策略是api是需要登錄的,我們先輸入正確的token,debug停在了我們打斷點需要執行身份驗證的地方,而且返回值也是正常,
然後我們換成爲user角色賬號登錄,並更換token重新發起請求發現:
同樣的,權限判斷是一個道理,這裏就不掩飾了
最後送上我的shiro結構圖:
這文章很長,看完需要不少的時間,但是如果您不會shiro我想您的收穫會是很大的
~~~謝謝大家
本文代碼示例已放入github:請點擊我
快速導航-------->src.main.java.yq.Shiro