SpringBoot+Shiro+Jwt實現登陸認證——最乾的乾貨

 1. 概述

1.1 SpringBoot

這個就沒什麼好說的了,能看到這個教程的,估計都是能夠說精通了SpringBoot的使用前端

1.2 Shiro

一個安全框架,但不僅是一個安全框架。它能實現多種多樣的功能。並不僅是侷限在web層。在國內的市場份額佔比高於SpringSecurity,是使用最多的安全框架java

能夠實現用戶的認證和受權。比SpringSecurity要簡單的多。web

1.3 Jwt

個人理解就是能夠進行客戶端與服務端之間驗證的一種技術,取代了以前使用Session來驗證的不安全性算法

爲何不適用Session?spring

原理是,登陸以後客戶端和服務端各自保存一個相應的SessionId,每次客戶端發起請求的時候就得攜帶這個SessionId來進行比對數據庫

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

它使用的是一種令牌技術apache

Jwt字符串分爲三部分json

  1. Header安全

    存儲兩個變量springboot

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

    存儲不少東西,基礎信息有以下幾個

    1. 簽發人,也就是這個「令牌」歸屬於哪一個用戶。通常是userId
    2. 建立時間,也就是這個令牌是何時建立的
    3. 失效時間,也就是這個令牌何時失效
    4. 惟一標識,通常可使用算法生成一個惟一標識
  3. Signature

    這個是上面兩個通過Header中的算法加密生成的,用於比對信息,防止篡改Header和payload

而後將這三個部分的信息通過加密生成一個JwtToken的字符串,發送給客戶端,客戶端保存在本地。當客戶端發起請求的時候攜帶這個到服務端(能夠是在cookie,能夠是在header,能夠是在localStorage中),在服務端進行驗證

好了,廢話很少說了,下面開始實戰,實戰分爲如下幾個部分

  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"
#認證不經過的頁面
shiro.UnauthorizedUrl="xxx"
#受權不經過的跳轉頁面

建立ShiroConfig.java進行一些簡單的配置

@Configuration
public class SpringShiroConfig {
    @Bean
    public Realm customRealm() {
        return new CustomRealm();
    }
    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(customRealm());
        // 關閉 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");    // 權限不足錯誤提示接口
        // 除了以上的請求外,其它請求都須要登陸
        chain.addPathDefinition("/**", "authc");
        return chain;
    }
    // Shiro 和 Spring AOP 整合時的特殊設置
    @Bean
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }
}

//還有關閉ShiroDao功能

建立自定義的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

<!-- 自動依賴導入 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();
        // 強制指定註解的底層實現使用 cglib 方案
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

 

3. SpringBoot整合Jwt

3.1 依賴

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

3.2 代碼

  1. 建立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       簽發人名稱
         * @param ttlMillis jwt 過時時間
         * @param claims    額外添加到荷部分的信息。
         *                  例如能夠添加用戶名、用戶ID、用戶(加密前的)密碼等信息
         */
        public String encode(String iss, long ttlMillis, Map<String, Object> claims) {
            if (claims == null) {
                claims = new HashMap<>();
            }
    
            // 簽發時間(iat):荷載部分的標準字段之一
            long nowMillis = System.currentTimeMillis();
            Date now = new Date(nowMillis);
    
            // 下面就是在爲payload添加各類標準聲明和私有聲明瞭
            JwtBuilder builder = Jwts.builder()
                    // 荷載部分的非標準字段/附加字段,通常寫在標準的字段以前。
                    .setClaims(claims)
                    // JWT ID(jti):荷載部分的標準字段之一,JWT 的惟一性標識,雖不強求,但儘可能確保其惟一性。
                    .setId(UUID.randomUUID().toString())
                    // 簽發時間(iat):荷載部分的標準字段之一,表明這個 JWT 的生成時間。
                    .setIssuedAt(now)
                    // 簽發人(iss):荷載部分的標準字段之一,表明這個 JWT 的全部者。一般是 username、userid 這樣具備用戶表明性的內容。
                    .setSubject(iss)
                    // 設置生成簽名的算法和祕鑰
                    .signWith(signatureAlgorithm, base64EncodedSecretKey);
    
            if (ttlMillis >= 0) {
                long expMillis = nowMillis + ttlMillis;
                Date exp = new Date(expMillis);
                // 過時時間(exp):荷載部分的標準字段之一,表明這個 JWT 的有效期。
                builder.setExpiration(exp);
            }
    
            return builder.compact();
        }
    
    
        /**
         * JWT Token 由 頭部 荷載部 和 簽名部 三部分組成。簽名部分是由加密算法生成,沒法反向解密。
         * 而 頭部 和 荷載部分是由 Base64 編碼算法生成,是能夠反向反編碼回原樣的。
         * 這也是爲何不要在 JWT Token 中放敏感數據的緣由。
         *
         * @param jwtToken 加密後的token
         * @return claims 返回荷載部分的鍵值對
         */
        public Claims decode(String jwtToken) {
    
            // 獲得 DefaultJwtParser
            return Jwts.parser()
                    // 設置簽名的祕鑰
                    .setSigningKey(base64EncodedSecretKey)
                    // 設置須要解析的 jwt
                    .parseClaimsJws(jwtToken)
                    .getBody();
        }
    
    
        /**
         * 校驗 token
         * 在這裏可使用官方的校驗,或,
         * 自定義校驗規則,例如在 token 中攜帶密碼,進行加密處理後和數據庫中的加密密碼比較。
         *
         * @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());
            });
        }
    }

    解析:

    1. 在建立JwtUtil對象的時候須要傳入幾個數值
      1. 這個用戶,用來生成祕鑰
      2. 這個加密算法,用來加密生成jwt
    2. 經過jwt數據獲取用戶信息的方法(decode())
    3. 判斷jwt是否存在或者過時的方法
    4. 最後是測試方法
  2. 建立一個Controller

    1. 登陸的Controller
      1. 獲取username和password,進行與數據庫的校驗,校驗成功執行下一步,失敗直接返回
      2. 使用建立JwtUtil對象,傳入username和須要使用的加密算法
      3. 建立須要加在載荷中的一些基本信息的一個map對象
      4. 建立jwt數據,傳入username,保存時間,以及基本信息的map對象
    2. 校驗Controller
      1. 獲取前臺傳入的Jwt數據
      2. 使用JWTUtil中的isVerify進行該jwt數據有效的校驗
4. SpringBoot+Shiro+Jwt
  1. 因爲須要對shiro的SecurityManager進行設置,因此不能使用shiro-spring-boot-starter進行與springboot的整合,只能使用spring-shiro

    <!-- 自動依賴導入 shiro-core 和 shiro-web -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.4.1</version>
    </dependency>
  2. 因爲須要實現無狀態的web,因此使用不到Shiro的Session功能,嚴謹點就是將其關閉

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

    這樣若是調用getSession()方法會拋出異常

4.1 流程

  1. 用戶請求,不攜帶token,就在JwtFilter處拋出異常/返回沒有登陸,讓它去登錄
  2. 用戶請求,攜帶token,就到JwtFilter中獲取jwt,封裝成JwtToken對象。而後使用JwtRealm進行認證
  3. 在JwtRealm中進行認證判斷這個token是否有效,也就是
執行流程:1. 客戶端發起請求,shiro的過濾器生效,判斷是不是login或logout的請求<br/>    若是是就直接執行請求<br/>    若是不是就進入JwtFilter2. JwtFilter執行流程    1. 獲取header是否有"Authorization"的鍵,有就獲取,沒有就拋出異常    2. 將獲取的jwt字符串封裝在建立的JwtToken中,使用subject執行login()方法進行校驗。這個方法會調用建立的JwtRealm    3. 執行JwtRealm中的認證方法,使用`jwtUtil.isVerify(jwt)`判斷是否登陸過    4. 返回true就使基礎執行下去

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) {
        // 不建立 session
        context.setSessionCreationEnabled(false);
        return super.createSubject(context);
    }
}

1. 建立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(),參數是=簽發人,存在時間,一些其餘的信息=。返回值是JwtToken對應的字符串
* 2. decode(),參數是=JwtToken=。返回值是荷載部分的鍵值對
* 3. isVerify(),參數是=JwtToken=。返回值是這個JwtToken是否存在
* */
public class JwtUtil {
    //建立默認的祕鑰和算法,供無參的構造方法使用
    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;
    }

    /*
     *這裏就是產生jwt字符串的地方
     * jwt字符串包括三個部分
     *  1. header
     *      -當前字符串的類型,通常都是「JWT」
     *      -哪一種算法加密,「HS256」或者其餘的加密算法
     *      因此通常都是固定的,沒有什麼變化
     *  2. payload
     *      通常有四個最多見的標準字段(下面有)
     *      iat:簽發時間,也就是這個jwt何時生成的
     *      jti:JWT的惟一標識
     *      iss:簽發人,通常都是username或者userId
     *      exp:過時時間
     *
     * */
    public String encode(String iss, long ttlMillis, Map<String, Object> claims) {
        //iss簽發人,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的惟一標識,通常設置成惟一的,這個方法能夠生成惟一標識
                .setIssuedAt(new Date(nowMillis))//1. 這個地方就是以毫秒爲單位,換算當前系統時間生成的iat
                .setSubject(iss)//3. 簽發人,也就是JWT是給誰的(邏輯上通常都是username或者userId)
                .signWith(signatureAlgorithm, base64EncodedSecretKey);//這個地方是生成jwt使用的算法和祕鑰
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);//4. 過時時間,這個也是使用毫秒生成的,使用當前時間+前面傳入的持續時間生成
            builder.setExpiration(exp);
        }
        return builder.compact();
    }

    //至關於encode的方向,傳入jwtToken生成對應的username和password等字段。Claim就是一個map
    //也就是拿到荷載部分全部的鍵值對
    public Claims decode(String jwtToken) {

        // 獲得 DefaultJwtParser
        return Jwts.parser()
                // 設置簽名的祕鑰
                .setSigningKey(base64EncodedSecretKey)
                // 設置須要解析的 jwt
                .parseClaimsJws(jwtToken)
                .getBody();
    }

    //判斷jwtToken是否合法
    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);  // 校驗不經過會拋出異常
        //判斷合法的標準: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. 建立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的時候進行帳號密碼登陸,登陸成功容許訪問,登陸失敗拒絕訪問
 * */
@Slf4j
public class JwtFilter extends AccessControlFilter {
    /*
     * 1. 返回true,shiro就直接容許訪問url
     * 2. 返回false,shiro纔會根據onAccessDenied的方法的返回值決定是否容許訪問url
     * */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        log.warn("isAccessAllowed 方法被調用");
        //這裏先讓它始終返回false來使用onAccessDenied()方法
        return false;
    }

    /**
     * 返回結果爲true代表登陸經過
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        log.warn("onAccessDenied 方法被調用");
        //這個地方和前端約定,要求前端將jwtToken放在請求的Header部分

        //因此之後發起請求的時候就須要在Header中放一個Authorization,值就是對應的Token
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        log.info("請求的 Header 中藏有 jwtToken {}", jwt);
        JwtToken jwtToken = new JwtToken(jwt);
        /*
         * 下面就是固定寫法
         * */
        try {
            // 委託 realm 進行登陸認證
            //因此這個地方最終仍是調用JwtRealm進行的認證
            getSubject(servletRequest, servletResponse).login(jwtToken);
            //也就是subject.login(token)
        } catch (Exception e) {
            e.printStackTrace();
            onLoginFail(servletResponse);
            //調用下面的方法向客戶端返回錯誤信息
            return false;
        }

        return true;
        //執行方法中沒有拋出異常就表示登陸成功
    }

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

3. 建立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

建立判斷jwt是否有效的認證方式的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
     * 標識這個Realm是專門用來驗證JwtToken
     * 不負責驗證其餘的token(UsernamePasswordToken)
     * */
    @Override
    public boolean supports(AuthenticationToken token) {
        //這個token就是從過濾器中傳入的jwtToken
        return token instanceof JwtToken;
    }

    //受權
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    //認證
    //這個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");//判斷數據庫中username是否存在
        log.info("在使用token登陸"+username);
        return new SimpleAuthenticationInfo(jwt,jwt,"JwtRealm");
        //這裏返回的是相似帳號密碼的東西,可是jwtToken都是jwt字符串。還須要一個該Realm的類名

    }

}

5. ShiroConfig

配置一些信息

  1. 由於不適用Session,因此爲了防止會調用getSession()方法而產生錯誤,因此默認調用自定義的Subject方法
  2. 一些修改,關閉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實現認證有三個不同的地方,對應下面abc
@Configuration
public class ShiroConfig {
    /*
     * a. 告訴shiro不要使用默認的DefaultSubject建立對象,由於不能建立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
         * */
        // 關閉 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以外的請求都先通過jwtFilter
         * */
        Map<String, Filter> filterMap = new HashMap<>();
        //這個地方其實另外兩個filter能夠不設置,默認就是
        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 受權方面的信息

在JwtRealm中的受權部分,可使用JwtUtil.decode(jwt).get("username")獲取到username,使用username去數據庫中查找到對應的權限,而後將權限賦值給這個用戶就能夠實現權限的認證了

相關文章
相關標籤/搜索