SpringBoot+Shiro+Jwt實現(xiàn)登錄認(rèn)證

詳細(xì)請查看https://zhuanlan.zhihu.com/p/391839846

1. 概述

1.1 SpringBoot

這個就沒什么好說的了盼樟,能看到這個教程的爷抓,估計都是可以說精通了SpringBoot的使用

1.2 Shiro

一個安全框架喉恋,但不只是一個安全框架处铛。它能實現(xiàn)多種多樣的功能失驶。并不只是局限在web層队询。在國內(nèi)的市場份額占比高于SpringSecurity浙滤,是使用最多的安全框架

可以實現(xiàn)用戶的認(rèn)證和授權(quán)伶授。比SpringSecurity要簡單的多沃呢。

1.3 Jwt

我的理解就是可以進(jìn)行客戶端與服務(wù)端之間驗證的一種技術(shù)年栓,取代了之前使用Session來驗證的不安全性

為什么不適用Session?

原理是薄霜,登錄之后客戶端和服務(wù)端各自保存一個相應(yīng)的SessionId某抓,每次客戶端發(fā)起請求的時候就得攜帶這個SessionId來進(jìn)行比對

  1. Session在用戶請求量大的時候服務(wù)器開銷太大了
  2. Session不利于搭建服務(wù)器的集群(也就是必須訪問原本的那個服務(wù)器才能獲取對應(yīng)的SessionId)

它使用的是一種令牌技術(shù)

Jwt字符串分為三部分

  1. Header

    存儲兩個變量

    1. 秘鑰(可以用來比對)
    2. 算法(也就是下面將Header和payload加密成Signature)
  2. payload

    存儲很多東西,基礎(chǔ)信息有如下幾個

    1. 簽發(fā)人惰瓜,也就是這個“令牌”歸屬于哪個用戶否副。一般是userId
    2. 創(chuàng)建時間,也就是這個令牌是什么時候創(chuàng)建的
    3. 失效時間崎坊,也就是這個令牌什么時候失效
    4. 唯一標(biāo)識备禀,一般可以使用算法生成一個唯一標(biāo)識
  3. Signature

    這個是上面兩個經(jīng)過Header中的算法加密生成的,用于比對信息奈揍,防止篡改Header和payload

然后將這三個部分的信息經(jīng)過加密生成一個JwtToken的字符串曲尸,發(fā)送給客戶端,客戶端保存在本地男翰。當(dāng)客戶端發(fā)起請求的時候攜帶這個到服務(wù)端(可以是在cookie另患,可以是在header,可以是在localStorage中)蛾绎,在服務(wù)端進(jìn)行驗證

好了昆箕,廢話不多說了,下面開始實戰(zhàn)秘通,實戰(zhàn)分為以下幾個部分

  1. SpringBoot整合Shiro
  2. SpringBoot整合Jwt
  3. SpringBoot+Shiro+Jwt
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.11.0</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

2. SpringBoot整合Shiro

兩種方式:

  1. 將ssm的整合的配置使用java代碼方式在springBoot中寫一遍
  2. 使用官方提供的start

2.1 使用start整合springBoot

pom.xml

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.4.0</version>
</dependency>
<!--注意不要寫成shiro-spring-boot-starter-->

application.properties

shiro.loginUrl="xxx"
#認(rèn)證不通過的頁面
shiro.UnauthorizedUrl="xxx"
#授權(quán)不通過的跳轉(zhuǎn)頁面

創(chuàng)建ShiroConfig.java進(jìn)行一些簡單的配置

@Configuration
public class SpringShiroConfig {
    @Bean
    public Realm customRealm() {
        return new CustomRealm();
    }
    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(customRealm());
        // 關(guān)閉 ShiroDAO 功能
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        // 不需要將 Shiro Session 中的東西存到任何地方(包括 Http Session 中)
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
    }
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
        // 哪些請求可以匿名訪問
        chain.addPathDefinition("/login", "anon");      // 登錄接口
        chain.addPathDefinition("/notLogin", "anon");   // 未登錄錯誤提示接口
        chain.addPathDefinition("/403", "anon");    // 權(quán)限不足錯誤提示接口
        // 除了以上的請求外为严,其它請求都需要登錄
        chain.addPathDefinition("/**", "authc");
        return chain;
    }
    // Shiro 和 Spring AOP 整合時的特殊設(shè)置
    @Bean
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }
}

//還有關(guān)閉ShiroDao功能

創(chuàng)建自定義的Realm

public class CustomRealm extends AuthorizingRealm {
    private static final Set<String> tomRoleNameSet = new HashSet<>();
    private static final Set<String> tomPermissionNameSet = new HashSet<>();
    private static final Set<String> jerryRoleNameSet = new HashSet<>();
    private static final Set<String> jerryPermissionNameSet = new HashSet<>();
    static {
        tomRoleNameSet.add("admin");
        jerryRoleNameSet.add("user");
        tomPermissionNameSet.add("user:insert");
        tomPermissionNameSet.add("user:update");
        tomPermissionNameSet.add("user:delete");
        tomPermissionNameSet.add("user:query");
        jerryPermissionNameSet.add("user:query");
    }
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = (String) principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo info =  new SimpleAuthorizationInfo();
        if (username.equals("tom")) {
            info.addRoles(tomRoleNameSet);
            info.addStringPermissions(tomPermissionNameSet);
        } else if (username.equals("jerry")) {
            info.addRoles(jerryRoleNameSet);
            info.addStringPermissions(jerryPermissionNameSet);
        }
        return info;
    }
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String username = (String) token.getPrincipal();
        if (username == null)
            throw new UnknownAccountException("用戶名不能為空");
        SimpleAuthenticationInfo info = null;
        if (username.equals("tom"))
            return new SimpleAuthenticationInfo("tom", "123", CustomRealm.class.getName());
        else if (username.equals("jerry"))
            return new SimpleAuthenticationInfo("jerry", "123", CustomRealm.class.getName());
        else
            return null;
    }
}

2.2 不使用starter

<!-- 自動依賴導(dǎo)入 shiro-core 和 shiro-web -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.1</version>
</dependency>

編寫 Shiro 的配置類:ShiroConfig

將 Shiro 的配置信息(spring-shiro.xml 和 spring-web.xml)以 Java 代碼配置的形式改寫:

@Configuration
public class ShiroConfig {
    @Bean
    public Realm realm() {
        return new CustomRealm();
    }
    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm());
        return securityManager;
    }
    @Bean
    public ShiroFilterFactoryBean shirFilter() {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager());

        shiroFilterFactoryBean.setLoginUrl("/loginPage");
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");

        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/loginPage", "anon");
        filterChainDefinitionMap.put("/403", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/hello", "anon");
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    }
    /* ################################################################# */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 強(qiáng)制指定注解的底層實現(xiàn)使用 cglib 方案
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

編寫 Controller

與 Shiro 和 SSM 的整合一樣敛熬。略

編寫 Thymeleaf 頁面

3. SpringBoot整合Jwt

3.1 依賴

1. springboot
2. java-jwt--核心依賴
3. jjwt--java版本的輔助幫助模塊

3.2 代碼

  1. 創(chuàng)建JwtUtil

    package cn.coderymy.utils;
    
    import java.util.*;
    import com.auth0.jwt.*;
    import com.auth0.jwt.algorithms.Algorithm;
    import io.jsonwebtoken.*;
    import org.apache.commons.codec.binary.Base64;
    
    import java.util.*;
    
    
    public class JwtUtil {
    
        // 生成簽名是所使用的秘鑰
        private final String base64EncodedSecretKey;
    
        // 生成簽名的時候所使用的加密算法
        private final SignatureAlgorithm signatureAlgorithm;
    
        public JwtUtil(String secretKey, SignatureAlgorithm signatureAlgorithm) {
            this.base64EncodedSecretKey = Base64.encodeBase64String(secretKey.getBytes());
            this.signatureAlgorithm = signatureAlgorithm;
        }
    
        /**
         * 生成 JWT Token 字符串
         *
         * @param iss       簽發(fā)人名稱
         * @param ttlMillis jwt 過期時間
         * @param claims    額外添加到荷部分的信息肺稀。
         *                  例如可以添加用戶名、用戶ID应民、用戶(加密前的)密碼等信息
         */
        public String encode(String iss, long ttlMillis, Map<String, Object> claims) {
            if (claims == null) {
                claims = new HashMap<>();
            }
    
            // 簽發(fā)時間(iat):荷載部分的標(biāo)準(zhǔn)字段之一
            long nowMillis = System.currentTimeMillis();
            Date now = new Date(nowMillis);
    
            // 下面就是在為payload添加各種標(biāo)準(zhǔn)聲明和私有聲明了
            JwtBuilder builder = Jwts.builder()
                    // 荷載部分的非標(biāo)準(zhǔn)字段/附加字段话原,一般寫在標(biāo)準(zhǔn)的字段之前夕吻。
                    .setClaims(claims)
                    // JWT ID(jti):荷載部分的標(biāo)準(zhǔn)字段之一,JWT 的唯一性標(biāo)識繁仁,雖不強(qiáng)求涉馅,但盡量確保其唯一性。
                    .setId(UUID.randomUUID().toString())
                    // 簽發(fā)時間(iat):荷載部分的標(biāo)準(zhǔn)字段之一黄虱,代表這個 JWT 的生成時間稚矿。
                    .setIssuedAt(now)
                    // 簽發(fā)人(iss):荷載部分的標(biāo)準(zhǔn)字段之一,代表這個 JWT 的所有者捻浦。通常是 username晤揣、userid 這樣具有用戶代表性的內(nèi)容。
                    .setSubject(iss)
                    // 設(shè)置生成簽名的算法和秘鑰
                    .signWith(signatureAlgorithm, base64EncodedSecretKey);
    
            if (ttlMillis >= 0) {
                long expMillis = nowMillis + ttlMillis;
                Date exp = new Date(expMillis);
                // 過期時間(exp):荷載部分的標(biāo)準(zhǔn)字段之一朱灿,代表這個 JWT 的有效期昧识。
                builder.setExpiration(exp);
            }
    
            return builder.compact();
        }
    
    
        /**
         * JWT Token 由 頭部 荷載部 和 簽名部 三部分組成。簽名部分是由加密算法生成盗扒,無法反向解密跪楞。
         * 而 頭部 和 荷載部分是由 Base64 編碼算法生成,是可以反向反編碼回原樣的侣灶。
         * 這也是為什么不要在 JWT Token 中放敏感數(shù)據(jù)的原因甸祭。
         *
         * @param jwtToken 加密后的token
         * @return claims 返回荷載部分的鍵值對
         */
        public Claims decode(String jwtToken) {
    
            // 得到 DefaultJwtParser
            return Jwts.parser()
                    // 設(shè)置簽名的秘鑰
                    .setSigningKey(base64EncodedSecretKey)
                    // 設(shè)置需要解析的 jwt
                    .parseClaimsJws(jwtToken)
                    .getBody();
        }
    
    
        /**
         * 校驗 token
         * 在這里可以使用官方的校驗,或褥影,
         * 自定義校驗規(guī)則淋叶,例如在 token 中攜帶密碼,進(jìn)行加密處理后和數(shù)據(jù)庫中的加密密碼比較伪阶。
         *
         * @param jwtToken 被校驗的 jwt Token
         */
        public boolean isVerify(String jwtToken) {
            Algorithm algorithm = null;
    
            switch (signatureAlgorithm) {
                case HS256:
                    algorithm = Algorithm.HMAC256(Base64.decodeBase64(base64EncodedSecretKey));
                    break;
                default:
                    throw new RuntimeException("不支持該算法");
            }
    
            JWTVerifier verifier = JWT.require(algorithm).build();
            verifier.verify(jwtToken);  // 校驗不通過會拋出異常
    
    
            /*
                // 得到DefaultJwtParser
                Claims claims = decode(jwtToken);
    
                if (claims.get("password").equals(user.get("password"))) {
                    return true;
                }
            */
    
            return true;
        }
    
        public static void main(String[] args) {
            JwtUtil util = new JwtUtil("tom", SignatureAlgorithm.HS256);
    
            Map<String, Object> map = new HashMap<>();
            map.put("username", "tom");
            map.put("password", "123456");
            map.put("age", 20);
    
            String jwtToken = util.encode("tom", 30000, map);
    
            System.out.println(jwtToken);
            /*
            util.isVerify(jwtToken);
            System.out.println("合法");
            */
    
            util.decode(jwtToken).entrySet().forEach((entry) -> {
                System.out.println(entry.getKey() + ": " + entry.getValue());
            });
        }
    }
    

    <font color="yellow">解析:</font>

    1. <font color="red">在創(chuàng)建JwtUtil對象的時候需要傳入幾個數(shù)值</font>
      1. 這個用戶煞檩,用來生成秘鑰
      2. 這個加密算法,用來加密生成jwt
    2. 通過jwt數(shù)據(jù)獲取用戶信息的方法(decode())
    3. 判斷jwt是否存在或者過期的方法
    4. 最后是測試方法
  2. 創(chuàng)建一個Controller

    1. 登錄的Controller
      1. 獲取username和password栅贴,進(jìn)行與數(shù)據(jù)庫的校驗斟湃,校驗成功執(zhí)行下一步,失敗直接返回
      2. 使用創(chuàng)建JwtUtil對象檐薯,傳入username和需要使用的加密算法
      3. 創(chuàng)建需要加在載荷中的一些基本信息的一個map對象
      4. 創(chuàng)建jwt數(shù)據(jù)凝赛,傳入username,保存時間坛缕,以及基本信息的map對象
    2. 校驗Controller
      1. 獲取前臺傳入的Jwt數(shù)據(jù)
      2. 使用JWTUtil中的isVerify進(jìn)行該jwt數(shù)據(jù)有效的校驗

4. SpringBoot+Shiro+Jwt

  1. 由于需要對shiro的SecurityManager進(jìn)行設(shè)置墓猎,所以不能使用shiro-spring-boot-starter進(jìn)行與springboot的整合,只能使用spring-shiro

    <!-- 自動依賴導(dǎo)入 shiro-core 和 shiro-web -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.4.1</version>
    </dependency>
    
  1. 由于需要實現(xiàn)無狀態(tài)的web赚楚,所以使用不到Shiro的Session功能毙沾,嚴(yán)謹(jǐn)點(diǎn)就是將其關(guān)閉

    public class JwtDefaultSubjectFactory extends DefaultWebSubjectFactory {
    
        @Override
        public Subject createSubject(SubjectContext context) {
            // 不創(chuàng)建 session
            context.setSessionCreationEnabled(false);
            return super.createSubject(context);
        }
    }
    

    這樣如果調(diào)用getSession()方法會拋出異常

4.1 流程

  1. 用戶請求,不攜帶token宠页,就在JwtFilter處拋出異常/返回沒有登錄左胞,讓它去登陸
  2. 用戶請求寇仓,攜帶token,就到JwtFilter中獲取jwt烤宙,封裝成JwtToken對象遍烦。然后使用JwtRealm進(jìn)行認(rèn)證
  3. 在JwtRealm中進(jìn)行認(rèn)證判斷這個token是否有效,也就是
執(zhí)行流程:1. 客戶端發(fā)起請求躺枕,shiro的過濾器生效服猪,判斷是否是login或logout的請求<br/>    如果是就直接執(zhí)行請求<br/>    如果不是就進(jìn)入JwtFilter2. JwtFilter執(zhí)行流程    1. 獲取header是否有"Authorization"的鍵,有就獲取拐云,沒有就拋出異常    2. 將獲取的jwt字符串封裝在創(chuàng)建的JwtToken中蔓姚,使用subject執(zhí)行l(wèi)ogin()方法進(jìn)行校驗。這個方法會調(diào)用創(chuàng)建的JwtRealm    3. 執(zhí)行JwtRealm中的認(rèn)證方法慨丐,使用`jwtUtil.isVerify(jwt)`判斷是否登錄過    4. 返回true就使基礎(chǔ)執(zhí)行下去

4.2 快速開始

0. JwtDeafultSubjectFactory

package cn.coderymy.shiro;

import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.SubjectContext;
import org.apache.shiro.web.mgt.DefaultWebSubjectFactory;

public class JwtDefaultSubjectFactory extends DefaultWebSubjectFactory {

    @Override
    public Subject createSubject(SubjectContext context) {
        // 不創(chuàng)建 session
        context.setSessionCreationEnabled(false);
        return super.createSubject(context);
    }
}

1. 創(chuàng)建JwtUtil

這個一般是固定的寫法坡脐,其中寫了大量注釋

package cn.coderymy.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.codec.binary.Base64;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/*
* 總的來說,工具類中有三個方法
* 獲取JwtToken房揭,獲取JwtToken中封裝的信息备闲,判斷JwtToken是否存在
* 1. encode(),參數(shù)是=簽發(fā)人捅暴,存在時間恬砂,一些其他的信息=。返回值是JwtToken對應(yīng)的字符串
* 2. decode()蓬痒,參數(shù)是=JwtToken=泻骤。返回值是荷載部分的鍵值對
* 3. isVerify(),參數(shù)是=JwtToken=梧奢。返回值是這個JwtToken是否存在
* */
public class JwtUtil {
    //創(chuàng)建默認(rèn)的秘鑰和算法狱掂,供無參的構(gòu)造方法使用
    private static final String defaultbase64EncodedSecretKey = "badbabe";
    private static final SignatureAlgorithm defaultsignatureAlgorithm = SignatureAlgorithm.HS256;

    public JwtUtil() {
        this(defaultbase64EncodedSecretKey, defaultsignatureAlgorithm);
    }

    private final String base64EncodedSecretKey;
    private final SignatureAlgorithm signatureAlgorithm;

    public JwtUtil(String secretKey, SignatureAlgorithm signatureAlgorithm) {
        this.base64EncodedSecretKey = Base64.encodeBase64String(secretKey.getBytes());
        this.signatureAlgorithm = signatureAlgorithm;
    }

    /*
     *這里就是產(chǎn)生jwt字符串的地方
     * jwt字符串包括三個部分
     *  1. header
     *      -當(dāng)前字符串的類型,一般都是“JWT”
     *      -哪種算法加密亲轨,“HS256”或者其他的加密算法
     *      所以一般都是固定的趋惨,沒有什么變化
     *  2. payload
     *      一般有四個最常見的標(biāo)準(zhǔn)字段(下面有)
     *      iat:簽發(fā)時間,也就是這個jwt什么時候生成的
     *      jti:JWT的唯一標(biāo)識
     *      iss:簽發(fā)人惦蚊,一般都是username或者userId
     *      exp:過期時間
     *
     * */
    public String encode(String iss, long ttlMillis, Map<String, Object> claims) {
        //iss簽發(fā)人器虾,ttlMillis生存時間,claims是指還想要在jwt中存儲的一些非隱私信息
        if (claims == null) {
            claims = new HashMap<>();
        }
        long nowMillis = System.currentTimeMillis();

        JwtBuilder builder = Jwts.builder()
                .setClaims(claims)
                .setId(UUID.randomUUID().toString())//2. 這個是JWT的唯一標(biāo)識蹦锋,一般設(shè)置成唯一的兆沙,這個方法可以生成唯一標(biāo)識
                .setIssuedAt(new Date(nowMillis))//1. 這個地方就是以毫秒為單位,換算當(dāng)前系統(tǒng)時間生成的iat
                .setSubject(iss)//3. 簽發(fā)人莉掂,也就是JWT是給誰的(邏輯上一般都是username或者userId)
                .signWith(signatureAlgorithm, base64EncodedSecretKey);//這個地方是生成jwt使用的算法和秘鑰
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);//4. 過期時間葛圃,這個也是使用毫秒生成的,使用當(dāng)前時間+前面?zhèn)魅氲某掷m(xù)時間生成
            builder.setExpiration(exp);
        }
        return builder.compact();
    }

    //相當(dāng)于encode的方向,傳入jwtToken生成對應(yīng)的username和password等字段装悲。Claim就是一個map
    //也就是拿到荷載部分所有的鍵值對
    public Claims decode(String jwtToken) {

        // 得到 DefaultJwtParser
        return Jwts.parser()
                // 設(shè)置簽名的秘鑰
                .setSigningKey(base64EncodedSecretKey)
                // 設(shè)置需要解析的 jwt
                .parseClaimsJws(jwtToken)
                .getBody();
    }

    //判斷jwtToken是否合法
    public boolean isVerify(String jwtToken) {
        //這個是官方的校驗規(guī)則昏鹃,這里只寫了一個”校驗算法“尚氛,可以自己加
        Algorithm algorithm = null;
        switch (signatureAlgorithm) {
            case HS256:
                algorithm = Algorithm.HMAC256(Base64.decodeBase64(base64EncodedSecretKey));
                break;
            default:
                throw new RuntimeException("不支持該算法");
        }
        JWTVerifier verifier = JWT.require(algorithm).build();
        verifier.verify(jwtToken);  // 校驗不通過會拋出異常
        //判斷合法的標(biāo)準(zhǔn):1. 頭部和荷載部分沒有篡改過诀诊。2. 沒有過期
        return true;
    }

    public static void main(String[] args) {
        JwtUtil util = new JwtUtil("tom", SignatureAlgorithm.HS256);
        //以tom作為秘鑰,以HS256加密
        Map<String, Object> map = new HashMap<>();
        map.put("username", "tom");
        map.put("password", "123456");
        map.put("age", 20);

        String jwtToken = util.encode("tom", 30000, map);

        System.out.println(jwtToken);
        util.decode(jwtToken).entrySet().forEach((entry) -> {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        });
    }
}

2. 創(chuàng)建JwtFilter

也就是在Shiro的攔截器中多加一個阅嘶,等下需要在配置文件中注冊這個過濾器

package cn.coderymy.filter;

import cn.coderymy.shiro.JwtToken;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.web.filter.AccessControlFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/*
 * 自定義一個Filter属瓣,用來攔截所有的請求判斷是否攜帶Token
 * isAccessAllowed()判斷是否攜帶了有效的JwtToken
 * onAccessDenied()是沒有攜帶JwtToken的時候進(jìn)行賬號密碼登錄,登錄成功允許訪問讯柔,登錄失敗拒絕訪問
 * */
@Slf4j
public class JwtFilter extends AccessControlFilter {
    /*
     * 1. 返回true抡蛙,shiro就直接允許訪問url
     * 2. 返回false,shiro才會根據(jù)onAccessDenied的方法的返回值決定是否允許訪問url
     * */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        log.warn("isAccessAllowed 方法被調(diào)用");
        //這里先讓它始終返回false來使用onAccessDenied()方法
        return false;
    }

    /**
     * 返回結(jié)果為true表明登錄通過
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        log.warn("onAccessDenied 方法被調(diào)用");
        //這個地方和前端約定魂迄,要求前端將jwtToken放在請求的Header部分

        //所以以后發(fā)起請求的時候就需要在Header中放一個Authorization粗截,值就是對應(yīng)的Token
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        log.info("請求的 Header 中藏有 jwtToken {}", jwt);
        JwtToken jwtToken = new JwtToken(jwt);
        /*
         * 下面就是固定寫法
         * */
        try {
            // 委托 realm 進(jìn)行登錄認(rèn)證
            //所以這個地方最終還是調(diào)用JwtRealm進(jìn)行的認(rèn)證
            getSubject(servletRequest, servletResponse).login(jwtToken);
            //也就是subject.login(token)
        } catch (Exception e) {
            e.printStackTrace();
            onLoginFail(servletResponse);
            //調(diào)用下面的方法向客戶端返回錯誤信息
            return false;
        }

        return true;
        //執(zhí)行方法中沒有拋出異常就表示登錄成功
    }

    //登錄失敗時默認(rèn)返回 401 狀態(tài)碼
    private void onLoginFail(ServletResponse response) throws IOException {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        httpResponse.getWriter().write("login error");
    }
}

3. 創(chuàng)建JwtToken

其中封裝了需要傳遞的jwt字符串

package cn.coderymy.shiro;

import org.apache.shiro.authc.AuthenticationToken;

//這個就類似UsernamePasswordToken
public class JwtToken implements AuthenticationToken {

    private String jwt;

    public JwtToken(String jwt) {
        this.jwt = jwt;
    }

    @Override//類似是用戶名
    public Object getPrincipal() {
        return jwt;
    }

    @Override//類似密碼
    public Object getCredentials() {
        return jwt;
    }
    //返回的都是jwt
}

4. JwtRealm

創(chuàng)建判斷jwt是否有效的認(rèn)證方式的Realm

package cn.coderymy.realm;

import cn.coderymy.shiro.JwtToken;
import cn.coderymy.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
@Slf4j
public class JwtRealm extends AuthorizingRealm {
    /*
     * 多重寫一個support
     * 標(biāo)識這個Realm是專門用來驗證JwtToken
     * 不負(fù)責(zé)驗證其他的token(UsernamePasswordToken)
     * */
    @Override
    public boolean supports(AuthenticationToken token) {
        //這個token就是從過濾器中傳入的jwtToken
        return token instanceof JwtToken;
    }

    //授權(quán)
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    //認(rèn)證
    //這個token就是從過濾器中傳入的jwtToken
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        String jwt = (String) token.getPrincipal();
        if (jwt == null) {
            throw new NullPointerException("jwtToken 不允許為空");
        }
        //判斷
        JwtUtil jwtUtil = new JwtUtil();
        if (!jwtUtil.isVerify(jwt)) {
            throw new UnknownAccountException();
        }
        //下面是驗證這個user是否是真實存在的
        String username = (String) jwtUtil.decode(jwt).get("username");//判斷數(shù)據(jù)庫中username是否存在
        log.info("在使用token登錄"+username);
        return new SimpleAuthenticationInfo(jwt,jwt,"JwtRealm");
        //這里返回的是類似賬號密碼的東西,但是jwtToken都是jwt字符串捣炬。還需要一個該Realm的類名

    }

}

5. ShiroConfig

配置一些信息

  1. 因為不適用Session熊昌,所以為了防止會調(diào)用getSession()方法而產(chǎn)生錯誤,所以默認(rèn)調(diào)用自定義的Subject方法
  2. 一些修改湿酸,關(guān)閉SHiroDao等
  3. 注冊JwtFilter
package cn.coderymy.config;

import cn.coderymy.filter.JwtFilter;
import cn.coderymy.realm.JwtRealm;
import cn.coderymy.shiro.JwtDefaultSubjectFactory;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SubjectFactory;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.authc.AnonymousFilter;
import org.apache.shiro.web.filter.authc.LogoutFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

//springBoot整合jwt實現(xiàn)認(rèn)證有三個不一樣的地方婿屹,對應(yīng)下面abc
@Configuration
public class ShiroConfig {
    /*
     * a. 告訴shiro不要使用默認(rèn)的DefaultSubject創(chuàng)建對象,因為不能創(chuàng)建Session
     * */
    @Bean
    public SubjectFactory subjectFactory() {
        return new JwtDefaultSubjectFactory();
    }

    @Bean
    public Realm realm() {
        return new JwtRealm();
    }

    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm());
        /*
         * b
         * */
        // 關(guān)閉 ShiroDAO 功能
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        // 不需要將 Shiro Session 中的東西存到任何地方(包括 Http Session 中)
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        //禁止Subject的getSession方法
        securityManager.setSubjectFactory(subjectFactory());
        return securityManager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean() {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager());
        shiroFilter.setLoginUrl("/unauthenticated");
        shiroFilter.setUnauthorizedUrl("/unauthorized");
        /*
         * c. 添加jwt過濾器推溃,并在下面注冊
         * 也就是將jwtFilter注冊到shiro的Filter中
         * 指定除了login和logout之外的請求都先經(jīng)過jwtFilter
         * */
        Map<String, Filter> filterMap = new HashMap<>();
        //這個地方其實另外兩個filter可以不設(shè)置昂利,默認(rèn)就是
        filterMap.put("anon", new AnonymousFilter());
        filterMap.put("jwt", new JwtFilter());
        filterMap.put("logout", new LogoutFilter());
        shiroFilter.setFilters(filterMap);

        // 攔截器
        Map<String, String> filterRuleMap = new LinkedHashMap<>();
        filterRuleMap.put("/login", "anon");
        filterRuleMap.put("/logout", "logout");
        filterRuleMap.put("/**", "jwt");
        shiroFilter.setFilterChainDefinitionMap(filterRuleMap);

        return shiroFilter;
    }
}


6. 測試

package cn.coderymy.controller;

import cn.coderymy.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.HashMap;
import java.util.Map;
@Slf4j
@Controller
public class LoginController {

    @RequestMapping("/login")
    public ResponseEntity<Map<String, String>> login(String username, String password) {
        log.info("username:{},password:{}",username,password);
        Map<String, String> map = new HashMap<>();
        if (!"tom".equals(username) || !"123".equals(password)) {
            map.put("msg", "用戶名密碼錯誤");
            return ResponseEntity.ok(map);
        }
        JwtUtil jwtUtil = new JwtUtil();
        Map<String, Object> chaim = new HashMap<>();
        chaim.put("username", username);
        String jwtToken = jwtUtil.encode(username, 5 * 60 * 1000, chaim);
        map.put("msg", "登錄成功");
        map.put("token", jwtToken);
        return ResponseEntity.ok(map);
    }
    @RequestMapping("/testdemo")
    public ResponseEntity<String> testdemo() {
        return ResponseEntity.ok("我愛蛋炒飯");
    }

}

4.3 授權(quán)方面的信息

在JwtRealm中的授權(quán)部分,可以使用JwtUtil.decode(jwt).get("username")獲取到username铁坎,使用username去數(shù)據(jù)庫中查找到對應(yīng)的權(quán)限蜂奸,然后將權(quán)限賦值給這個用戶就可以實現(xiàn)權(quán)限的認(rèn)證了

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市硬萍,隨后出現(xiàn)的幾起案子窝撵,更是在濱河造成了極大的恐慌,老刑警劉巖襟铭,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件碌奉,死亡現(xiàn)場離奇詭異,居然都是意外死亡寒砖,警方通過查閱死者的電腦和手機(jī)赐劣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來哩都,“玉大人魁兼,你說我怎么就攤上這事∧叮” “怎么了咐汞?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵盖呼,是天一觀的道長。 經(jīng)常有香客問我化撕,道長几晤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任植阴,我火速辦了婚禮蟹瘾,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘掠手。我一直安慰自己憾朴,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布喷鸽。 她就那樣靜靜地躺著众雷,像睡著了一般。 火紅的嫁衣襯著肌膚如雪做祝。 梳的紋絲不亂的頭發(fā)上砾省,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天,我揣著相機(jī)與錄音剖淀,去河邊找鬼纯蛾。 笑死,一個胖子當(dāng)著我的面吹牛纵隔,可吹牛的內(nèi)容都是我干的翻诉。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼捌刮,長吁一口氣:“原來是場噩夢啊……” “哼碰煌!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起绅作,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤芦圾,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后俄认,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體个少,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年眯杏,在試婚紗的時候發(fā)現(xiàn)自己被綠了夜焦。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡岂贩,死狀恐怖茫经,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤卸伞,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布抹镊,位于F島的核電站,受9級特大地震影響荤傲,放射性物質(zhì)發(fā)生泄漏垮耳。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一弃酌、第九天 我趴在偏房一處隱蔽的房頂上張望氨菇。 院中可真熱鬧儡炼,春花似錦妓湘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至妹田,卻和暖如春唬党,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鬼佣。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工驶拱, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人晶衷。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓蓝纲,卻偏偏與公主長得像,于是被迫代替她去往敵國和親晌纫。 傳聞我的和親對象是個殘疾皇子税迷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容