SpringBoot整合Shiro(看完不會,直播喫屎)

首先開始前,在這裏吹個牛,如果願意仔細花時間看完這篇文章,如果還不會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>

<!--        &lt;!&ndash; https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security &ndash;&gt;-->
<!--        <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>

<!--        &lt;!&ndash;orcale數據庫&ndash;&gt;-->
<!--        &lt;!&ndash; https://mvnrepository.com/artifact/com.jslsolucoes/ojdbc6 &ndash;&gt;-->
<!--        <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的核心幾個文件就講的很清楚了,實際上很簡單,就是兩個都可以解決,那就是MyRealmShirlConfig

那到了這裏我們的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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章