16.授權中心

0.學習目標

1.無狀態登錄原理

1.1.什麼是有狀態?

有狀態服務,即服務端需要記錄每次會話的客戶端信息,從而識別客戶端身份,根據用戶身份進行請求的處理,典型的設計如tomcat中的session。

例如登錄:用戶登錄後,我們把登錄者的信息保存在服務端session中,並且給用戶一個cookie值,記錄對應的session。然後下次請求,用戶攜帶cookie值來,我們就能識別到對應session,從而找到用戶的信息。

缺點是什麼?

  • 服務端保存大量數據,增加服務端壓力
  • 服務端保存用戶狀態,無法進行水平擴展
  • 客戶端請求依賴服務端,多次請求必須訪問同一臺服務器

1.2.什麼是無狀態

微服務集羣中的每個服務,對外提供的都是Rest風格的接口。而Rest風格的一個最重要的規範就是:服務的無狀態性,即:

  • 服務端不保存任何客戶端請求者信息
  • 客戶端的每次請求必須具備自描述信息,通過這些信息識別客戶端身份

帶來的好處是什麼呢?

  • 客戶端請求不依賴服務端的信息,任何多次請求不需要必須訪問到同一臺服務
  • 服務端的集羣和狀態對客戶端透明
  • 服務端可以任意的遷移和伸縮
  • 減小服務端存儲壓力

1.3.如何實現無狀態

無狀態登錄的流程:

  • 當客戶端第一次請求服務時,服務端對用戶進行信息認證(登錄)
  • 認證通過,將用戶信息進行加密形成token,返回給客戶端,作爲登錄憑證
  • 以後每次請求,客戶端都攜帶認證的token
  • 服務端對token進行解密,判斷是否有效。

流程圖:
在這裏插入圖片描述

整個登錄過程中,最關鍵的點是什麼?

token的安全性

token是識別客戶端身份的唯一標示,如果加密不夠嚴密,被人僞造那就完蛋了。

採用何種方式加密纔是安全可靠的呢?

我們將採用JWT + RSA非對稱加密

1.4.JWT

1.4.1.簡介

JWT,全稱是Json Web Token, 是JSON風格輕量級的授權和身份認證規範,可實現無狀態、分佈式的Web應用授權;官網:https://jwt.io
在這裏插入圖片描述

GitHub上jwt的java客戶端:https://github.com/jwtk/jjwt

1.4.2.數據格式

JWT包含三部分數據:

  • Header:頭部,通常頭部有兩部分信息:

    • 聲明類型,這裏是JWT
    • 加密算法,自定義

    我們會對頭部進行base64加密(可解密),得到第一部分數據

  • Payload:載荷,就是有效數據,一般包含下面信息:

    • 用戶身份信息(注意,這裏因爲採用base64加密,可解密,因此不要存放敏感信息)
    • 註冊聲明:如token的簽發時間,過期時間,簽發人等

    這部分也會採用base64加密,得到第二部分數據

  • Signature:簽名,是整個數據的認證信息。一般根據前兩步的數據,再加上服務的的密鑰(secret)(不要泄漏,最好週期性更換),通過加密算法生成。用於驗證整個數據完整和可靠性

生成的數據格式:
在這裏插入圖片描述

可以看到分爲3段,每段就是上面的一部分數據

1.4.3.JWT交互流程

流程圖:
在這裏插入圖片描述

步驟翻譯:

  • 1、用戶登錄
  • 2、服務的認證,通過後根據secret生成token
  • 3、將生成的token返回給瀏覽器
  • 4、用戶每次請求攜帶token
  • 5、服務端利用公鑰解讀jwt簽名,判斷簽名有效後,從Payload中獲取用戶信息
  • 6、處理請求,返回響應結果

因爲JWT簽發的token中已經包含了用戶的身份信息,並且每次請求都會攜帶,這樣服務的就無需保存用戶信息,甚至無需去數據庫查詢,完全符合了Rest的無狀態規範。

1.4.4.非對稱加密

加密技術是對信息進行編碼和解碼的技術,編碼是把原來可讀信息(又稱明文)譯成代碼形式(又稱密文),其逆過程就是解碼(解密),加密技術的要點是加密算法,加密算法可以分爲三類:

  • 對稱加密,如AES
    • 基本原理:將明文分成N個組,然後使用密鑰對各個組進行加密,形成各自的密文,最後把所有的分組密文進行合併,形成最終的密文。
    • 優勢:算法公開、計算量小、加密速度快、加密效率高
    • 缺陷:雙方都使用同樣密鑰,安全性得不到保證
  • 非對稱加密,如RSA
    • 基本原理:同時生成兩把密鑰:私鑰和公鑰,私鑰隱祕保存,公鑰可以下發給信任客戶端
      • 私鑰加密,持有私鑰或公鑰纔可以解密
      • 公鑰加密,持有私鑰纔可解密
    • 優點:安全,難以破解
    • 缺點:算法比較耗時
  • 不可逆加密,如MD5,SHA
    • 基本原理:加密過程中不需要使用密鑰,輸入明文後由系統直接經過加密算法處理成密文,這種加密後的數據是無法被解密的,無法根據密文推算出明文。

RSA算法歷史:

1977年,三位數學家Rivest、Shamir 和 Adleman 設計了一種算法,可以實現非對稱加密。這種算法用他們三個人的名字縮寫:RSA

1.5.結合Zuul的鑑權流程

我們逐步演進系統架構設計。需要注意的是:secret是簽名的關鍵,因此一定要保密,我們放到鑑權中心保存,其它任何服務中都不能獲取secret。

1.5.1.沒有RSA加密時

在微服務架構中,我們可以把服務的鑑權操作放到網關中,將未通過鑑權的請求直接攔截,如圖:
在這裏插入圖片描述

  • 1、用戶請求登錄
  • 2、Zuul將請求轉發到授權中心,請求授權
  • 3、授權中心校驗完成,頒發JWT憑證
  • 4、客戶端請求其它功能,攜帶JWT
  • 5、Zuul將jwt交給授權中心校驗,通過後放行
  • 6、用戶請求到達微服務
  • 7、微服務將jwt交給鑑權中心,鑑權同時解析用戶信息
  • 8、鑑權中心返回用戶數據給微服務
  • 9、微服務處理請求,返回響應

發現什麼問題了?

每次鑑權都需要訪問鑑權中心,系統間的網絡請求頻率過高,效率略差,鑑權中心的壓力較大。

1.5.2.結合RSA的鑑權

直接看圖:
在這裏插入圖片描述

  • 我們首先利用RSA生成公鑰和私鑰。私鑰保存在授權中心,公鑰保存在Zuul和各個微服務
  • 用戶請求登錄
  • 授權中心校驗,通過後用私鑰對JWT進行簽名加密
  • 返回jwt給用戶
  • 用戶攜帶JWT訪問
  • Zuul直接通過公鑰解密JWT,進行驗證,驗證通過則放行
  • 請求到達微服務,微服務直接用公鑰解析JWT,獲取用戶信息,無需訪問授權中心

2.授權中心

2.1.創建授權中心

授權中心的主要職責:

  • 用戶鑑權:
    • 接收用戶的登錄請求,通過用戶中心的接口進行校驗,通過後生成JWT
    • 使用私鑰生成JWT並返回
  • 服務鑑權:微服務間的調用不經過Zuul,會有風險,需要鑑權中心進行認證
    • 原理與用戶鑑權類似,但邏輯稍微複雜一些(此處我們不做實現)

因爲生成jwt,解析jwt這樣的行爲以後在其它微服務中也會用到,因此我們會抽取成工具。我們把鑑權中心進行聚合,一個工具module,一個提供服務的module

2.1.1.創建父module

我們先創建父module,名稱爲:leyou-auth
在這裏插入圖片描述

在這裏插入圖片描述

將pom打包方式改爲pom:

<?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">
    <parent>
        <artifactId>leyou</artifactId>
        <groupId>com.leyou.parent</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.auth</groupId>
    <artifactId>leyou-auth</artifactId>
    <version>1.0.0-SNAPSHOT</version>

</project>

2.1.2.通用module

然後是授權服務的通用模塊:leyou-auth-common:
在這裏插入圖片描述

1533264779361

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>leyou-auth</artifactId>
        <groupId>com.leyou.auth</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.auth</groupId>
    <artifactId>leyou-auth-common</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    
</project>

結構:
在這裏插入圖片描述

2.1.3.授權服務

在這裏插入圖片描述

在這裏插入圖片描述

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>leyou-auth</artifactId>
        <groupId>com.leyou.auth</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.auth</groupId>
    <artifactId>leyou-auth-service</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.leyou.auth</groupId>
            <artifactId>leyou-auth-common</artifactId>
            <version>1.0.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.leyou.common</groupId>
            <artifactId>leyou-common</artifactId>
            <version>1.0.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>

</project>

引導類:

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class LeyouAuthApplication {

    public static void main(String[] args) {
        SpringApplication.run(LeyouAuthApplication.class, args);
    }
}

application.yml

server:
  port: 8087
spring:
  application:
    name: auth-service
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
    registry-fetch-interval-seconds: 10
  instance:
    lease-renewal-interval-in-seconds: 5 # 每隔5秒發送一次心跳
    lease-expiration-duration-in-seconds: 10 # 10秒不發送就過期
    prefer-ip-address: true
    ip-address: 127.0.0.1
    instance-id: ${spring.application.name}:${server.port}

結構:
在這裏插入圖片描述

在leyou-gateway工程的application.yml中,修改路由:

zuul:
  prefix: /api # 路由路徑前綴
  routes:
    item-service: /item/** # 商品微服務的映射路徑
    search-service: /search/** # 搜索微服務
    user-service: /user/** # 用戶微服務
    auth-service: /auth/** # 授權中心微服務

2.2.JWT工具類

我們在leyou-auth-common中導入課前資料中的工具類:
在這裏插入圖片描述

需要在leyou-auth-common中引入JWT依賴:

<dependencies>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.0</version>
    </dependency>
    <dependency>
        <groupId>joda-time</groupId>
        <artifactId>joda-time</artifactId>
    </dependency>
</dependencies>

2.3.測試工具類

我們在leyou-auth-common中編寫測試類:
在這裏插入圖片描述

public class JwtTest {

    private static final String pubKeyPath = "C:\\tmp\\rsa\\rsa.pub";

    private static final String priKeyPath = "C:\\tmp\\rsa\\rsa.pri";

    private PublicKey publicKey;

    private PrivateKey privateKey;

    @Test
    public void testRsa() throws Exception {
        RsaUtils.generateKey(pubKeyPath, priKeyPath, "234");
    }

    @Before
    public void testGetRsa() throws Exception {
        this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
        this.privateKey = RsaUtils.getPrivateKey(priKeyPath);
    }

    @Test
    public void testGenerateToken() throws Exception {
        // 生成token
        String token = JwtUtils.generateToken(new UserInfo(20L, "jack"), privateKey, 5);
        System.out.println("token = " + token);
    }

    @Test
    public void testParseToken() throws Exception {
        String token = "eyJhbGciOiJSUzI1NiJ9.eyJpZCI6MjAsInVzZXJuYW1lIjoiamFjayIsImV4cCI6MTUzMzI4MjQ3N30.EPo35Vyg1IwZAtXvAx2TCWuOPnRwPclRNAM4ody5CHk8RF55wdfKKJxjeGh4H3zgruRed9mEOQzWy79iF1nGAnvbkraGlD6iM-9zDW8M1G9if4MX579Mv1x57lFewzEo-zKnPdFJgGlAPtNWDPv4iKvbKOk1-U7NUtRmMsF1Wcg";

        // 解析token
        UserInfo user = JwtUtils.getInfoFromToken(token, publicKey);
        System.out.println("id: " + user.getId());
        System.out.println("userName: " + user.getUsername());
    }
}

測試生成公鑰和私鑰,我們運行這段代碼:
在這裏插入圖片描述

運行之後,查看目標目錄:
在這裏插入圖片描述

公鑰和私鑰已經生成了!

測試生成token,把@Before的註釋去掉的:
在這裏插入圖片描述

在這裏插入圖片描述

測試解析token:
在這裏插入圖片描述

正常情況:
在這裏插入圖片描述

任意改動token,發現報錯了:
在這裏插入圖片描述

2.3.編寫登錄授權接口

接下來,我們需要在leyou-auth-servcice編寫一個接口,對外提供登錄授權服務。基本流程如下:

  • 客戶端攜帶用戶名和密碼請求登錄
  • 授權中心調用客戶中心接口,根據用戶名和密碼查詢用戶信息
  • 如果用戶名密碼正確,能獲取用戶,否則爲空,則登錄失敗
  • 如果校驗成功,則生成JWT並返回

2.3.1.生成公鑰和私鑰

我們需要在授權中心生成真正的公鑰和私鑰。我們必須有一個生成公鑰和私鑰的secret,這個可以配置到application.yml中:

leyou:
  jwt:
    secret: leyou@Login(Auth}*^31)&heiMa% # 登錄校驗的密鑰
    pubKeyPath: C:\\tmp\\rsa\\rsa.pub # 公鑰地址
    priKeyPath: C:\\tmp\\rsa\\rsa.pri # 私鑰地址
    expire: 30 # 過期時間,單位分鐘

然後編寫屬性類,加載這些數據:

@ConfigurationProperties(prefix = "ly.jwt")
public class JwtProperties {

    private String secret; // 密鑰

    private String pubKeyPath;// 公鑰

    private String priKeyPath;// 私鑰

    private int expire;// token過期時間

    private PublicKey publicKey; // 公鑰

    private PrivateKey privateKey; // 私鑰

    private static final Logger logger = LoggerFactory.getLogger(JwtProperties.class);

    /**
     * @PostContruct:在構造方法執行之後執行該方法
     */
    @PostConstruct
    public void init(){
        try {
            File pubKey = new File(pubKeyPath);
            File priKey = new File(priKeyPath);
            if (!pubKey.exists() || !priKey.exists()) {
                // 生成公鑰和私鑰
                RsaUtils.generateKey(pubKeyPath, priKeyPath, secret);
            }
            // 獲取公鑰和私鑰
            this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
            this.privateKey = RsaUtils.getPrivateKey(priKeyPath);
        } catch (Exception e) {
            logger.error("初始化公鑰和私鑰失敗!", e);
            throw new RuntimeException();
        }
    }
    
    // getter setter ...
}

2.3.2.Controller

編寫授權接口,我們接收用戶名和密碼,校驗成功後,寫入cookie中。

  • 請求方式:post
  • 請求路徑:/accredit
  • 請求參數:username和password
  • 返回結果:無

代碼:

@RestController
@EnableConfigurationProperties(JwtProperties.class)
public class AuthController {

    @Autowired
    private AuthService authService;

    @Autowired
    private JwtProperties prop;

    /**
     * 登錄授權
     *
     * @param username
     * @param password
     * @return
     */
    @PostMapping("accredit")
    public ResponseEntity<Void> authentication(
            @RequestParam("username") String username,
            @RequestParam("password") String password,
            HttpServletRequest request,
            HttpServletResponse response) {
        // 登錄校驗
        String token = this.authService.authentication(username, password);
        if (StringUtils.isBlank(token)) {
            return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
        }
        // 將token寫入cookie,並指定httpOnly爲true,防止通過JS獲取和修改
        CookieUtils.setCookie(request, response, prop.getCookieName(),
                token, prop.getCookieMaxAge(), null, true);
        return ResponseEntity.ok().build();
    }
}

這裏的cookie的name和生存時間,我們配置到屬性文件:application.yml:
在這裏插入圖片描述

然後在JwtProperties中添加屬性:
在這裏插入圖片描述

2.3.3.CookieUtils

要注意,這裏我們使用了一個工具類,CookieUtils,可以在課前資料中找到,我們把它添加到leyou-common中,然後引入servlet相關依賴即可:

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-core</artifactId>
</dependency>

代碼:略
在這裏插入圖片描述

2.3.3.UserClient

接下來我們肯定要對用戶密碼進行校驗,所以我們需要通過FeignClient去訪問 user-service微服務:

引入user-service依賴:

<dependency>
    <groupId>com.leyou.user</groupId>
    <artifactId>leyou-user-interface</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

編寫FeignClient:

@FeignClient(value = "user-service")
public interface UserClient extends UserApi {
}

在leyou-user-interface工程中添加api接口:
在這裏插入圖片描述

內容:

@RequestMapping("user")
public interface UserApi {

    @GetMapping("query")
    public User queryUser(
            @RequestParam("username") String username,
            @RequestParam("password") String password);
}

2.3.4.Service

@Service
public class AuthService {

    @Autowired
    private UserClient userClient;

    @Autowired
    private JwtProperties properties;

    public String authentication(String username, String password) {

        try {
            // 調用微服務,執行查詢
            User user = this.userClient.queryUser(username, password);

            // 如果查詢結果爲null,則直接返回null
            if (user == null) {
                return null;
            }

            // 如果有查詢結果,則生成token
            String token = JwtUtils.generateToken(new UserInfo(user.getId(), user.getUsername()),
                    properties.getPrivateKey(), properties.getExpire());
            return token;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

2.3.5.項目結構

在這裏插入圖片描述

2.3.6.測試

在這裏插入圖片描述

2.4.登錄頁面

接下來,我們看看登錄頁面,是否能夠正確的發出請求。

我們在頁面輸入登錄信息,然後點擊登錄:
在這裏插入圖片描述

查看控制檯:
在這裏插入圖片描述

發現請求的路徑不對,我們的認證接口是:

/api/auth/accredit

我們打開login.html,修改路徑信息:
在這裏插入圖片描述

頁面ajax請求:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-5fKRCfVD-1573625758633)(assets/1527517866435.png)]

然後再次測試,成功跳轉到了首頁:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-N09LMWZ2-1573625758634)(assets/1527518012727.png)]

2.5.解決cookie寫入問題

接下來我們查看首頁cookie:
在這裏插入圖片描述

什麼都沒有,爲什麼?

2.5.1.問題分析

我們在之前測試時,清晰的看到了響應頭中,有Set-Cookie屬性,爲什麼在這裏卻什麼都沒有?

我們之前在講cors跨域時,講到過跨域請求cookie生效的條件:

  • 服務的響應頭中需要攜帶Access-Control-Allow-Credentials並且爲true。
  • 響應頭中的Access-Control-Allow-Origin一定不能爲*,必須是指定的域名
  • 瀏覽器發起ajax需要指定withCredentials 爲true

看看我們的服務端cors配置:
在這裏插入圖片描述

沒有任何問題。

再看客戶端瀏覽器的ajax配置,我們在js/common.js中對axios進行了統一配置:
在這裏插入圖片描述

一切OK。

那說明,問題一定出在響應的set-cookie頭中。我們再次仔細看看剛纔的響應頭:
在這裏插入圖片描述

我們發現cookie的 domain屬性似乎不太對。

cookie也是有 的限制,一個網頁,只能操作當前域名下的cookie,但是現在我們看到的地址是0.0.1,而頁面是www.leyou.com,域名不匹配,cookie設置肯定失敗了!

2.5.2.跟蹤CookieUtils

我們去Debug跟蹤CookieUtils,看看到底是怎麼回事:

我們發現內部有一個方法,用來獲取Domain:
在這裏插入圖片描述

它獲取domain是通過服務器的host來計算的,然而我們的地址竟然是:127.0.0.1:8087,因此後續的運算,最終得到的domain就變成了:
在這裏插入圖片描述

問題找到了:我們請求時的serverName明明是:api.leyou.com,現在卻被變成了:127.0.0.1,因此計算domain是錯誤的,從而導致cookie設置失敗!

2.5.3.解決host地址的變化

那麼問題來了:爲什麼我們這裏的請求serverName變成了:127.0.0.1:8087呢?

這裏的server name其實就是請求時的主機名:Host,之所以改變,有兩個原因:

  • 我們使用了nginx反向代理,當監聽到api.leyou.com的時候,會自動將請求轉發至127.0.0.1:10010,即Zuul。
  • 而後請求到達我們的網關Zuul,Zuul就會根據路徑匹配,我們的請求是/api/auth,根據規則被轉發到了 127.0.0.1:8087 ,即我們的授權中心。

我們首先去更改nginx配置,讓它不要修改我們的host:proxy_set_header Host $host;
在這裏插入圖片描述

把nginx進行reload:

nginx -s reload

這樣就解決了nginx這裏的問題。但是Zuul還會有一次轉發,所以要去修改網關的配置(leyou-gateway工程):
在這裏插入圖片描述

重啓後,我們再次測試。
在這裏插入圖片描述

最後計算得到的domain:
在這裏插入圖片描述

完美!

2.5.4.再次測試

我們再次登錄,發現依然沒有cookie!!

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(im在這裏插入圖片描述

怎麼回事呢?

我們通過RestClient訪問下看看:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Ch8AF6Rp-1573625758643)(assets/1527520407803.png)]

發現,響應頭中根本沒有set-cookie了。

這是怎麼回事??

2.5.5.Zuul的敏感頭過濾

Zuul內部有默認的過濾器,會對請求和響應頭信息進行重組,過濾掉敏感的頭信息:
在這裏插入圖片描述

會發現,這裏會通過一個屬性爲SensitiveHeaders的屬性,來獲取敏感頭列表,然後添加到IgnoredHeaders中,這些頭信息就會被忽略。

而這個SensitiveHeaders的默認值就包含了set-cookie

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-pBF3OYB1-1573625758644)(assets/1533733081367.png)]

解決方案有兩種:

全局設置:

  • zuul.sensitive-headers=

指定路由設置:

  • zuul.routes.<routeName>.sensitive-headers=
  • zuul.routes.<routeName>.custom-sensitive-headers=true

思路都是把敏感頭設置爲null
在這裏插入圖片描述

2.5.6.最後的測試

再次重啓後測試:
在這裏插入圖片描述

3.首頁判斷登錄狀態

雖然cookie已經成功寫入,但是我們首頁的頂部,登錄狀態依然沒能判斷出用戶信息:
在這裏插入圖片描述

這裏需要向後臺發起請求,根據cookie獲取當前用戶的信息。

我們先看頁面實現

3.1.頁面JS代碼

頁面的頂部已經被我們封裝爲一個獨立的Vue組件,在/js/pages/shortcut.js
在這裏插入圖片描述

打開js,發現裏面已經定義好了Vue組件,並且在created函數中,查詢用戶信息:
在這裏插入圖片描述

查看網絡控制檯,發現發起了請求:
在這裏插入圖片描述

因爲token在cookie中,因此本次請求肯定會攜帶token信息在頭中。

3.2.後臺實現校驗用戶接口

我們在leyou-auth-service中定義用戶的校驗接口,通過cookie獲取token,然後校驗通過返回用戶信息。

  • 請求方式:GET
  • 請求路徑:/verify
  • 請求參數:無,不過我們需要從cookie中獲取token信息
  • 返回結果:UserInfo,校驗成功返回用戶信息;校驗失敗,則返回401

代碼:

/**
  * 驗證用戶信息
  * @param token
  * @return
  */
@GetMapping("verify")
public ResponseEntity<UserInfo> verifyUser(@CookieValue("LY_TOKEN")String token){
    try {
        // 從token中解析token信息
        UserInfo userInfo = JwtUtils.getInfoFromToken(token, this.properties.getPublicKey());
        // 解析成功返回用戶信息
        return ResponseEntity.ok(userInfo);
    } catch (Exception e) {
        e.printStackTrace();
    }
    // 出現異常則,響應500
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

3.3.測試

在這裏插入圖片描述

在這裏插入圖片描述

頁面效果:
在這裏插入圖片描述

3.4.刷新token

每當用戶在頁面進行新的操作,都應該刷新token的過期時間,否則30分鐘後用戶的登錄信息就無效了。而刷新其實就是重新生成一份token,然後寫入cookie即可。

那麼問題來了:我們怎麼知道用戶有操作呢?

事實上,每當用戶來查詢其個人信息,就證明他正在瀏覽網頁,此時刷新cookie是比較合適的時機。因此我們可以對剛剛的校驗用戶登錄狀態的接口進行改進,加入刷新token的邏輯。

/**
  * 驗證用戶信息
  * @param token
  * @return
  */
@GetMapping("verify")
public ResponseEntity<UserInfo> verifyUser(@CookieValue("LY_TOKEN")String token, HttpServletRequest request, HttpServletResponse response){
    try {
        // 從token中解析token信息
        UserInfo userInfo = JwtUtils.getInfoFromToken(token, this.properties.getPublicKey());
        // 解析成功要重新刷新token
        token = JwtUtils.generateToken(userInfo, this.properties.getPrivateKey(), this.properties.getExpire());
        // 更新cookie中的token
        CookieUtils.setCookie(request, response, this.properties.getCookieName(), token, this.properties.getCookieMaxAge());

        // 解析成功返回用戶信息
        return ResponseEntity.ok(userInfo);
    } catch (Exception e) {
        e.printStackTrace();
    }
    // 出現異常則,響應500
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

4.網關的登錄攔截器

接下來,我們在Zuul編寫攔截器,對用戶的token進行校驗,如果發現未登錄,則進行攔截。

4.1.引入jwt相關配置

既然是登錄攔截,一定是前置攔截器,我們在leyou-gateway中定義。

首先在pom.xml中,引入所需要的依賴:

<dependency>
    <groupId>com.leyou.common</groupId>
    <artifactId>leyou-common</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>com.leyou.auth</groupId>
    <artifactId>leyou-auth-common</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

然後編寫application.yml屬性文件,添加如下內容:

leyou:
  jwt:
    pubKeyPath:  C:\\tmp\\rsa\\rsa.pub # 公鑰地址
    cookieName: LY_TOKEN # cookie的名稱

編寫屬性類,讀取公鑰:
在這裏插入圖片描述

@ConfigurationProperties(prefix = "leyou.jwt")
public class JwtProperties {

    private String pubKeyPath;// 公鑰

    private PublicKey publicKey; // 公鑰

    private String cookieName;

    private static final Logger logger = LoggerFactory.getLogger(JwtProperties.class);

    @PostConstruct
    public void init(){
        try {
            // 獲取公鑰和私鑰
            this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
        } catch (Exception e) {
            logger.error("初始化公鑰失敗!", e);
            throw new RuntimeException();
        }
    }

    public String getPubKeyPath() {
        return pubKeyPath;
    }

    public void setPubKeyPath(String pubKeyPath) {
        this.pubKeyPath = pubKeyPath;
    }

    public PublicKey getPublicKey() {
        return publicKey;
    }

    public void setPublicKey(PublicKey publicKey) {
        this.publicKey = publicKey;
    }

    public String getCookieName() {
        return cookieName;
    }

    public void setCookieName(String cookieName) {
        this.cookieName = cookieName;
    }
}

4.2.編寫過濾器邏輯

基本邏輯:

  • 獲取cookie中的token
  • 通過JWT對token進行校驗
  • 通過:則放行;不通過:則重定向到登錄頁

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-C7t0GrjQ-1573625758660)(assets/1527557510032.png)]

@Component
@EnableConfigurationProperties(JwtProperties.class)
public class LoginFilter extends ZuulFilter {

    @Autowired
    private JwtProperties properties;

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 5;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        // 獲取上下文
        RequestContext context = RequestContext.getCurrentContext();
        // 獲取request
        HttpServletRequest request = context.getRequest();
        // 獲取token
        String token = CookieUtils.getCookieValue(request, this.properties.getCookieName());
        // 校驗
        try {
            // 校驗通過什麼都不做,即放行
            JwtUtils.getInfoFromToken(token, this.properties.getPublicKey());
        } catch (Exception e) {
            // 校驗出現異常,返回403
            context.setSendZuulResponse(false);
            context.setResponseStatusCode(HttpStatus.FORBIDDEN.value());
        }
        return null;
    }
}

重啓,刷新頁面,發現請求校驗的接口也被攔截了:
在這裏插入圖片描述

證明我們的攔截器生效了,但是,似乎有什麼不對的。這個路徑似乎不應該被攔截啊!

4.3.白名單

要注意,並不是所有的路徑我們都需要攔截,例如:

  • 登錄校驗接口:/auth/**
  • 註冊接口:/user/register
  • 數據校驗接口:/user/check/**
  • 發送驗證碼接口:/user/code
  • 搜索接口:/search/**

另外,跟後臺管理相關的接口,因爲我們沒有做登錄和權限,因此暫時都放行,但是生產環境中要做登錄校驗:

  • 後臺商品服務:/item/**

所以,我們需要在攔截時,配置一個白名單,如果在名單內,則不進行攔截。

application.yaml中添加規則:

leyou:
  filter:
    allowPaths:
      - /api/auth
      - /api/search
      - /api/user/register
      - /api/user/check
      - /api/user/code
      - /api/item

然後讀取這些屬性:
在這裏插入圖片描述

內容:

@ConfigurationProperties(prefix = "leyou.filter")
public class FilterProperties {

    private List<String> allowPaths;

    public List<String> getAllowPaths() {
        return allowPaths;
    }

    public void setAllowPaths(List<String> allowPaths) {
        this.allowPaths = allowPaths;
    }
}

在過濾器中的shouldFilter方法中添加判斷邏輯:
在這裏插入圖片描述

代碼:

@Component
@EnableConfigurationProperties({JwtProperties.class, FilterProperties.class})
public class LoginFilter extends ZuulFilter {

    @Autowired
    private JwtProperties jwtProp;

    @Autowired
    private FilterProperties filterProp;

    private static final Logger logger = LoggerFactory.getLogger(LoginFilter.class);

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 5;
    }

    @Override
    public boolean shouldFilter() {
        // 獲取上下文
        RequestContext ctx = RequestContext.getCurrentContext();
        // 獲取request
        HttpServletRequest req = ctx.getRequest();
        // 獲取路徑
        String requestURI = req.getRequestURI();
        // 判斷白名單
        return !isAllowPath(requestURI);
    }

    private boolean isAllowPath(String requestURI) {
        // 定義一個標記
        boolean flag = false;
        // 遍歷允許訪問的路徑
        for (String path : this.filterProp.getAllowPaths()) {
            // 然後判斷是否是符合
            if(requestURI.startsWith(path)){
                flag = true;
                break;
            }
        }
        return flag;
    }

    @Override
    public Object run() throws ZuulException {
        // 獲取上下文
        RequestContext ctx = RequestContext.getCurrentContext();
        // 獲取request
        HttpServletRequest request = ctx.getRequest();
        // 獲取token
        String token = CookieUtils.getCookieValue(request, jwtProp.getCookieName());
        // 校驗
        try {
            // 校驗通過什麼都不做,即放行
            JwtUtils.getInfoFromToken(token, jwtProp.getPublicKey());
        } catch (Exception e) {
            // 校驗出現異常,返回403
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(403);
            logger.error("非法訪問,未登錄,地址:{}", request.getRemoteHost(), e );
        }
        return null;
    }
}

再次測試:
在這裏插入圖片描述

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