學習目標
- 掌握微服務網關的系統搭建
- 瞭解什麼是微服務網關以及它的作用
- 掌握系統中心微服務的搭建
- 掌握用戶密碼加密存儲bcrypt
- 瞭解JWT鑑權的介紹
- 掌握JWT的鑑權的使用
- 掌握網關使用JWT進行校驗
- 掌握網關限流
1 微服務網關
1.1 微服務網關的概述
不同的微服務一般會有不同的網絡地址,而外部客戶端可能需要調用多個服務的接口才能完成一個業務需求,如果讓客戶端直接與各個微服務通信,會有以下的問題:
- 客戶端會多次請求不同的微服務,增加了客戶端的複雜性
- 存在跨域請求,在一定場景下處理相對複雜
- 認證複雜,每個服務都需要獨立認證
- 難以重構,隨着項目的迭代,可能需要重新劃分微服務。例如,可能將多個服務合併成一個或者將一個服務拆分成多個。如果客戶端直接與微服務通信,那麼重構將會很難實施
- 某些微服務可能使用了防火牆 / 瀏覽器不友好的協議,直接訪問會有一定的困難
以上這些問題可以藉助網關解決。
網關是介於客戶端和服務器端之間的中間層,所有的外部請求都會先經過 網關這一層。也就是說,API 的實現方面更多的考慮業務邏輯,而安全、性能、監控可以交由 網關來做,這樣既提高業務靈活性又不缺安全性,典型的架構圖如圖所示:
優點如下:
- 安全 ,只有網關係統對外進行暴露,微服務可以隱藏在內網,通過防火牆保護。
- 易於監控。可以在網關收集監控數據並將其推送到外部系統進行分析。
- 易於認證。可以在網關上進行認證,然後再將請求轉發到後端的微服務,而無須在每個微服務中進行認證。
- 減少了客戶端與各個微服務之間的交互次數
- 易於統一授權。
總結:微服務網關就是一個系統,通過暴露該微服務網關係統,方便我們進行相關的鑑權,安全控制,日誌統一處理,易於監控的相關功能。
1.2 微服務網關技術
實現微服務網關的技術有很多,
- nginx Nginx (engine x) 是一個高性能的HTTP和反向代理web服務器,同時也提供了IMAP/POP3/SMTP服務
- zuul ,Zuul 是 Netflix 出品的一個基於 JVM 路由和服務端的負載均衡器。
- spring-cloud-gateway, 是spring 出品的 基於spring 的網關項目,集成斷路器,路徑重寫,性能比Zuul好。
我們使用gateway這個網關技術,無縫銜接到基於spring cloud的微服務開發中來。
gateway官網:
https://spring.io/projects/spring-cloud-gateway
2 網關係統使用
2.1 需求分析
由於我們開發的系統 有包括前臺系統和後臺系統,後臺的系統 給管理員使用。那麼也需要調用各種微服務,所以我們針對 系統管理搭建一個網關係統。分析如下:
2.2 搭建後臺網關係統
2.2.1 搭建分析
由上可知道,由於 需要有多個網關,所以爲了管理方便。我們新建一個項目,打包方式爲pom,在裏面建立各種網關係統模塊即可。如圖所示:
2.2.2 工程搭建
(1)引入依賴
修改changgou-gateway工程,打包方式爲pom
pom.xml如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
<?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>changgou-parent</artifactId> <groupId>com.changgou</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>changgou-gateway</artifactId> <packaging>pom</packaging> <modules> <module>changgou-gateway-web</module> </modules> <!--網關依賴--> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> </dependencies> </project>
在changgou-gateway工程中,創建 changgou-gateway-web工程,該網關主要用於對後臺微服務進行一個調用操作,將多個微服務串聯到一起。
pom.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<?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>changgou-gateway</artifactId> <groupId>com.changgou</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>changgou-gateway-web</artifactId> <description> 普通web請求網關 </description> </project>
(2)引導類
在changgou-gateway-web中創建一個引導類com.changgou.GatewayWebApplication,代碼如下:
1 2 3 4 5 6 7 8
@SpringBootApplication @EnableEurekaClient public class GatewayWebApplication { public static void main(String[] args) { SpringApplication.run(GatewayWebApplication.class,args); } }
(3)application.yml配置
在changgou-gateway-web的resources下創建application.yml,代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
spring: application: name: gateway-web server: port: 8001 eureka: client: service-url: defaultZone: http://127.0.0.1:7001/eureka instance: prefer-ip-address: true management: endpoint: gateway: enabled: true web: exposure: include: true
2.3 跨域配置
有時候,我們需要對所有微服務跨域請求進行處理,則可以在gateway中進行跨域支持。修改application.yml,添加如下代碼:
1 2 3 4 5 6 7 8 9 10 11 12
spring: cloud: gateway: globalcors: cors-configurations: '[/**]': # 匹配所有請求 allowedOrigins: "*" #跨域處理 允許所有的域 allowedMethods: # 支持的方法 - GET - POST - PUT - DELETE
最終文件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
spring: cloud: gateway: globalcors: cors-configurations: '[/**]': # 匹配所有請求 allowedOrigins: "*" #跨域處理 允許所有的域 allowedMethods: # 支持的方法 - GET - POST - PUT - DELETE application: name: gateway-web server: port: 8001 eureka: client: service-url: defaultZone: http://127.0.0.1:7001/eureka instance: prefer-ip-address: true management: endpoint: gateway: enabled: true web: exposure: include: true
2.4 網關過濾配置
路由過濾器允許以某種方式修改傳入的HTTP請求或傳出的HTTP響應。 路徑過濾器的範圍限定爲特定路徑。 Spring Cloud Gateway包含許多內置的GatewayFilter工廠。如上圖,根據請求路徑路由到不同微服務去,這塊可以使用Gateway的路由過濾功能實現。
過濾器 有 20 多個 實現 類, 包括 頭部 過濾器、 路徑 類 過濾器、 Hystrix 過濾器 和 變更 請求 URL 的 過濾器, 還有 參數 和 狀態 碼 等 其他 類型 的 過濾器。
內置的過濾器工廠有22個實現類,包括 頭部過濾器、路徑過濾器、Hystrix 過濾器 、請求URL 變更過濾器,還有參數和狀態碼等其他類型的過濾器。根據過濾器工廠的用途來劃分,可以分爲以下幾種:Header、Parameter、Path、Body、Status、Session、Redirect、Retry、RateLimiter和Hystrix。
2.4.1 Host 路由
比如用戶請求cloud.itheima.com的時候,可以將請求路由給http://localhost:18081服務處理,如下配置:
上圖配置如下:
1 2 3 4 5
routes: - id: changgou_goods_route uri: http://localhost:18081 predicates: - Host=cloud.itheima.com**
測試請求http://cloud.itheima.com:8001/brand
,效果如下:
注意:此時要想讓cloud.itheima.com訪問本地計算機,要配置C:\Windows\System32\drivers\etc\hosts
文件,映射配置如下:
127.0.0.1 cloud.itheima.com
2.4.2 路徑匹配過濾配置
我們還可以根據請求路徑實現對應的路由過濾操作,例如請求中以/brand/
路徑開始的請求,都直接交給http://localhost:180801
服務處理,如下配置:
上圖配置如下:
1 2 3 4 5
routes: - id: changgou_goods_route uri: http://localhost:18081 predicates: - Path=/brand/**
測試請求http://localhost:8001/brand
,效果如下:
2.4.3 PrefixPath 過濾配置
用戶每次請求路徑的時候,我們可以給真實請求加一個統一前綴,例如用戶請求http://localhost:8001
的時候我們讓它請求真實地址http://localhost:8001/brand
,如下配置:
上圖配置如下:
1 2 3 4 5 6 7 8
routes: - id: changgou_goods_route uri: http://localhost:18081 predicates: #- Host=cloud.itheima.com** - Path=/** filters: - PrefixPath=/brand
測試請求http://localhost:8001/
效果如下:
2.4.4 StripPrefix 過濾配置
很多時候也會有這麼一種請求,用戶請求路徑是/api/brand
,而真實路徑是/brand
,這時候我們需要去掉/api
纔是真實路徑,此時可以使用SttripPrefix功能來實現路徑的過濾操作,如下配置:
上圖配置如下:
1 2 3 4 5 6 7 8 9
routes: - id: changgou_goods_route uri: http://localhost:18081 predicates: #- Host=cloud.itheima.com** - Path=/** filters: #- PrefixPath=/brand - StripPrefix=1
測試請求http://localhost:8001/api/brand
,效果如下:
2.4.5 LoadBalancerClient 路由過濾器(客戶端負載均衡)
上面的路由配置每次都會將請求給指定的URL
處理,但如果在以後生產環境,併發量較大的時候,我們需要根據服務的名稱判斷來做負載均衡操作,可以使用LoadBalancerClientFilter
來實現負載均衡調用。LoadBalancerClientFilter
會作用在url以lb開頭的路由,然後利用loadBalancer
來獲取服務實例,構造目標requestUrl
,設置到GATEWAY_REQUEST_URL_ATTR
屬性中,供NettyRoutingFilter
使用。
修改application.yml配置文件,代碼如下:
上圖配置如下:
1 2 3 4 5 6 7 8 9 10
routes: - id: changgou_goods_route #uri: http://localhost:18081 uri: lb://goods predicates: #- Host=cloud.itheima.com** - Path=/** filters: #- PrefixPath=/brand - StripPrefix=1
測試請求路徑http://localhost:8001/api/brand
2.5 網關限流
網關可以做很多的事情,比如,限流,當我們的系統 被頻繁的請求的時候,就有可能 將系統壓垮,所以 爲了解決這個問題,需要在每一個微服務中做限流操作,但是如果有了網關,那麼就可以在網關係統做限流,因爲所有的請求都需要先通過網關係統才能路由到微服務中。
2.5.1 思路分析
2.5.2 令牌桶算法
令牌桶算法是比較常見的限流算法之一,大概描述如下:
1)所有的請求在處理之前都需要拿到一個可用的令牌纔會被處理;
2)根據限流大小,設置按照一定的速率往桶裏添加令牌;
3)桶設置最大的放置令牌限制,當桶滿時、新添加的令牌就被丟棄或者拒絕;
4)請求達到後首先要獲取令牌桶中的令牌,拿着令牌纔可以進行其他的業務邏輯,處理完業務邏輯之後,將令牌直接刪除;
5)令牌桶有最低限額,當桶中的令牌達到最低限額的時候,請求處理完之後將不會刪除令牌,以此保證足夠的限流
如下圖:
這個算法的實現,有很多技術,Guaua是其中之一,redis客戶端也有其實現。
2.5.3 使用令牌桶進行請求次數限流
spring cloud gateway 默認使用redis的RateLimter限流算法來實現。所以我們要使用首先需要引入redis的依賴
(1)引入redis依賴
在changgou-gateway的pom.xml中引入redis的依賴
1 2 3 4 5 6
<!--redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> <version>2.1.3.RELEASE</version> </dependency>
(2)定義KeyResolver
在Applicatioin引導類中添加如下代碼,KeyResolver用於計算某一個類型的限流的KEY也就是說,可以通過KeyResolver來指定限流的Key。
我們可以根據IP來限流,比如每個IP每秒鐘只能請求一次,在GatewayWebApplication定義key的獲取,獲取客戶端IP,將IP作爲key,如下代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
/*** * IP限流 * @return */ @Bean(name="ipKeyResolver") public KeyResolver userKeyResolver() { return new KeyResolver() { @Override public Mono<String> resolve(ServerWebExchange exchange) { //獲取遠程客戶端IP String hostName = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress(); System.out.println("hostName:"+hostName); return Mono.just(hostName); } }; }
(3)修改application.yml中配置項,指定限制流量的配置以及REDIS的配置,如圖
修改如下圖:
配置代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
spring: cloud: gateway: globalcors: corsConfigurations: '[/**]': # 匹配所有請求 allowedOrigins: "*" #跨域處理 允許所有的域 allowedMethods: # 支持的方法 - GET - POST - PUT - DELETE routes: - id: changgou_goods_route uri: lb://goods predicates: - Path=/api/brand** filters: - StripPrefix=1 - name: RequestRateLimiter #請求數限流 名字不能隨便寫 ,使用默認的facatory args: key-resolver: "#{@ipKeyResolver}" redis-rate-limiter.replenishRate: 1 redis-rate-limiter.burstCapacity: 1 application: name: gateway-web #Redis配置 redis: host: 192.168.211.132 port: 6379 server: port: 8001 eureka: client: service-url: defaultZone: http://127.0.0.1:7001/eureka instance: prefer-ip-address: true management: endpoint: gateway: enabled: true web: exposure: include: true
解釋:
redis-rate-limiter.replenishRate
是您希望允許用戶每秒執行多少請求,而不會丟棄任何請求。這是令牌桶填充的速率
redis-rate-limiter.burstCapacity
是指令牌桶的容量,允許在一秒鐘內完成的最大請求數,將此值設置爲零將阻止所有請求。
key-resolver: “#{@ipKeyResolver}” 用於通過SPEL表達式來指定使用哪一個KeyResolver.
如上配置:
表示 一秒內,允許 一個請求通過,令牌桶的填充速率也是一秒鐘添加一個令牌。
最大突發狀況 也只允許 一秒內有一次請求,可以根據業務來調整 。
多次請求會發生如下情況
3 用戶登錄
項目中有2個重要角色,分別爲管理員和用戶,下面幾章我們將實現購物下單和支付,用戶如果沒登錄是沒法下單和支付的,所以我們這裏需要實現一個登錄功能。
3.1 表結構介紹
changgou_user表如下:
用戶信息表tb_user
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
CREATE TABLE `tb_user` ( `username` varchar(50) NOT NULL COMMENT '用戶名', `password` varchar(100) NOT NULL COMMENT '密碼,加密存儲', `phone` varchar(20) DEFAULT NULL COMMENT '註冊手機號', `email` varchar(50) DEFAULT NULL COMMENT '註冊郵箱', `created` datetime NOT NULL COMMENT '創建時間', `updated` datetime NOT NULL COMMENT '修改時間', `source_type` varchar(1) DEFAULT NULL COMMENT '會員來源:1:PC,2:H5,3:Android,4:IOS', `nick_name` varchar(50) DEFAULT NULL COMMENT '暱稱', `name` varchar(50) DEFAULT NULL COMMENT '真實姓名', `status` varchar(1) DEFAULT NULL COMMENT '使用狀態(1正常 0非正常)', `head_pic` varchar(150) DEFAULT NULL COMMENT '頭像地址', `qq` varchar(20) DEFAULT NULL COMMENT 'QQ號碼', `is_mobile_check` varchar(1) DEFAULT '0' COMMENT '手機是否驗證 (0否 1是)', `is_email_check` varchar(1) DEFAULT '0' COMMENT '郵箱是否檢測(0否 1是)', `sex` varchar(1) DEFAULT '1' COMMENT '性別,1男,0女', `user_level` int(11) DEFAULT NULL COMMENT '會員等級', `points` int(11) DEFAULT NULL COMMENT '積分', `experience_value` int(11) DEFAULT NULL COMMENT '經驗值', `birthday` datetime DEFAULT NULL COMMENT '出生年月日', `last_login_time` datetime DEFAULT NULL COMMENT '最後登錄時間', PRIMARY KEY (`username`), UNIQUE KEY `username` (`username`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用戶表';
3.2 用戶微服務創建
創建工程之前,先使用代碼生成器生成對應的業務代碼。
(1)公共API創建
在changgou-service-api中創建changgou-service-user-api,並將pojo拷貝到工程中,如下圖:
在changgou-service中創建changgou-service-user微服務,並引入生成的業務邏輯代碼,如下圖:
(2)依賴
在changgou-service-user的pom.xml引入如下依賴:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
<?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>changgou-service</artifactId> <groupId>com.changgou</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>changgou-service-user</artifactId> <!--依賴--> <dependencies> <dependency> <groupId>com.changgou</groupId> <artifactId>changgou-service-user-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </project>
(3)啓動類創建
在changgou-service-user微服務中創建啓動類com.changgou.UserApplication,代碼如下:
1 2 3 4 5 6 7 8 9
@SpringBootApplication @EnableEurekaClient @MapperScan("com.changgou.user.dao") public class UserApplication { public static void main(String[] args) { SpringApplication.run(UserApplication.class,args); } }
(4)application.yml配置
在changgou-service-user的resources中創建application.yml配置,代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
server: port: 18089 spring: application: name: user datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.211.132:3306/changgou_user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC username: root password: 123456 eureka: client: service-url: defaultZone: http://127.0.0.1:7001/eureka instance: prefer-ip-address: true feign: hystrix: enabled: true
3.3 登錄
登錄的時候,需要進行密碼校驗,這裏採用了BCryptPasswordEncoder進行加密,需要將資料中的BCrypt導入到common工程中,其中BCrypt.checkpw(“明文”,“密文”)用於對比密碼是否一致。
修改changgou-service-user的com.changgou.user.controller.UserController添加登錄方法,代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
/*** * 用戶登錄 */ @RequestMapping(value = "/login") public Result login(String username,String password){ //查詢用戶信息 User user = userService.findById(username); if(user!=null && BCrypt.checkpw(password,user.getPassword())){ return new Result(true,StatusCode.OK,"登錄成功!",user); } return new Result(false,StatusCode.LOGINERROR,"賬號或者密碼錯誤!"); }
注意:這裏密碼進行了加密。
使用Postman測試如下:
3.4 網關關聯
在我們平時工作中,並不會直接將微服務暴露出去,一般都會使用網關對接,實現對微服務的一個保護作用,如上圖,當用戶訪問/api/user/
的時候我們再根據用戶請求調用用戶微服務的指定方法。當然,除了/api/user/
還有/api/address/
、/api/areas/
、/api/cities/
、/api/provinces/
都需要由user微服務處理,修改網關工程changgou-gateway-web
的application.yml配置文件,如下代碼:
上圖代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
spring: cloud: gateway: globalcors: corsConfigurations: '[/**]': # 匹配所有請求 allowedOrigins: "*" #跨域處理 允許所有的域 allowedMethods: # 支持的方法 - GET - POST - PUT - DELETE routes: - id: changgou_goods_route uri: lb://goods predicates: - Path=/api/goods/** filters: - StripPrefix=1 - name: RequestRateLimiter #請求數限流 名字不能隨便寫 ,使用默認的facatory args: key-resolver: "#{@ipKeyResolver}" redis-rate-limiter.replenishRate: 1 redis-rate-limiter.burstCapacity: 1 #用戶微服務 - id: changgou_user_route uri: lb://user predicates: - Path=/api/user/**,/api/address/**,/api/areas/**,/api/cities/**,/api/provinces/** filters: - StripPrefix=1 application: name: gateway-web #Redis配置 redis: host: 192.168.211.132 port: 6379 server: port: 8001 eureka: client: service-url: defaultZone: http://127.0.0.1:7001/eureka instance: prefer-ip-address: true management: endpoint: gateway: enabled: true web: exposure: include: true
使用Postman訪問http://localhost:8001/api/user/login?username=changgou&password=changgou
,效果如下:
4 JWT講解
4.1 需求分析
我們之前已經搭建過了網關,使用網關在網關係統中比較適合進行權限校驗。
那麼我們可以採用JWT的方式來實現鑑權校驗。
4.2 什麼是JWT
JSON Web Token(JWT)是一個非常輕巧的規範。這個規範允許我們使用JWT在用戶和服務器之間傳遞安全可靠的信息。
4.3 JWT的構成
一個JWT實際上就是一個字符串,它由三部分組成,頭部、載荷與簽名。
頭部(Header)
頭部用於描述關於該JWT的最基本的信息,例如其類型以及簽名所用的算法等。這也可以被表示成一個JSON對象。
{"typ":"JWT","alg":"HS256"}
在頭部指明瞭簽名算法是HS256算法。 我們進行BASE64編碼http://base64.xpcha.com/,編碼後的字符串如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
小知識:Base64是一種基於64個可打印字符來表示二進制數據的表示方法。由於2的6次方等於64,所以每6個比特爲一個單元,對應某個可打印字符。三個字節有24個比特,對應於4個Base64單元,即3個字節需要用4個可打印字符來表示。JDK 中提供了非常方便的 **BASE64Encoder** 和 **BASE64Decoder**,用它們可以非常方便的完成基於 BASE64 的編碼和解碼
載荷(playload)
載荷就是存放有效信息的地方。這個名字像是特指飛機上承載的貨品,這些有效信息包含三個部分
(1)標準中註冊的聲明(建議但不強制使用)
1 2 3 4 5 6 7
iss: jwt簽發者 sub: jwt所面向的用戶 aud: 接收jwt的一方 exp: jwt的過期時間,這個過期時間必須要大於簽發時間 nbf: 定義在什麼時間之前,該jwt都是不可用的. iat: jwt的簽發時間 jti: jwt的唯一身份標識,主要用來作爲一次性token,從而回避重放攻擊。
(2)公共的聲明
公共的聲明可以添加任何的信息,一般添加用戶的相關信息或其他業務需要的必要信息.但不建議添加敏感信息,因爲該部分在客戶端可解密.
(3)私有的聲明
私有聲明是提供者和消費者所共同定義的聲明,一般不建議存放敏感信息,因爲base64是對稱解密的,意味着該部分信息可以歸類爲明文信息。
這個指的就是自定義的claim。比如下面面結構舉例中的admin和name都屬於自定的claim。這些claim跟JWT標準規定的claim區別在於:JWT規定的claim,JWT的接收方在拿到JWT之後,都知道怎麼對這些標準的claim進行驗證(還不知道是否能夠驗證);而private claims不會驗證,除非明確告訴接收方要對這些claim進行驗證以及規則才行。
定義一個payload:
{"sub":"1234567890","name":"John Doe","admin":true}
然後將其進行base64加密,得到Jwt的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
簽證(signature)
jwt的第三部分是一個簽證信息,這個簽證信息由三部分組成:
header (base64後的)
payload (base64後的)
secret
這個部分需要base64加密後的header和base64加密後的payload使用.連接組成的字符串,然後通過header中聲明的加密方式進行加鹽secret組合加密,然後就構成了jwt的第三部分。
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
將這三部分用.連接成一個完整的字符串,構成了最終的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服務器端的,jwt的簽發生成也是在服務器端的,secret就是用來進行jwt的簽發和jwt的驗證,所以,它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味着客戶端是可以自我簽發jwt了。
4.4 JJWT的介紹和使用
JJWT是一個提供端到端的JWT創建和驗證的Java庫。永遠免費和開源(Apache License,版本2.0),JJWT很容易使用和理解。它被設計成一個以建築爲中心的流暢界面,隱藏了它的大部分複雜性。
官方文檔:
4.4.1 創建TOKEN
(1)依賴引入
在changgou-parent項目中的pom.xml中添加依賴:
1 2 3 4 5 6
<!--鑑權--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
(2)創建測試
在changgou-common的/test/java下創建測試類,並設置測試方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
public class JwtTest { /**** * 創建Jwt令牌 */ @Test public void testCreateJwt(){ JwtBuilder builder= Jwts.builder() .setId("888") //設置唯一編號 .setSubject("小白") //設置主題 可以是JSON數據 .setIssuedAt(new Date()) //設置簽發日期 .signWith(SignatureAlgorithm.HS256,"itcast");//設置簽名 使用HS256算法,並設置SecretKey(字符串) //構建 並返回一個字符串 System.out.println( builder.compact() ); } }
運行打印結果:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjIyODd9.RBLpZ79USMplQyfJCZFD2muHV_KLks7M1ZsjTu6Aez4
再次運行,會發現每次運行的結果是不一樣的,因爲我們的載荷中包含了時間。
4.4.2 TOKEN解析
我們剛纔已經創建了token ,在web應用中這個操作是由服務端進行然後發給客戶端,客戶端在下次向服務端發送請求時需要攜帶這個token(這就好像是拿着一張門票一樣),那服務端接到這個token 應該解析出token中的信息(例如用戶id),根據這些信息查詢數據庫返回相應的結果。
1 2 3 4 5 6 7 8 9 10 11 12
/*** * 解析Jwt令牌數據 */ @Test public void testParseJwt(){ String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjIyODd9.RBLpZ79USMplQyfJCZFD2muHV_KLks7M1ZsjTu6Aez4"; Claims claims = Jwts.parser(). setSigningKey("itcast"). parseClaimsJws(compactJwt). getBody(); System.out.println(claims); }
運行打印效果:
{jti=888, sub=小白, iat=1562062287}
試着將token或簽名祕鑰篡改一下,會發現運行時就會報錯,所以解析token也就是驗證token.
4.4.3 設置過期時間
有很多時候,我們並不希望簽發的token是永久生效的,所以我們可以爲token添加一個過期時間。
4.4.3.1 token過期設置
解釋:
.setExpiration(date)//用於設置過期時間 ,參數爲Date類型數據
運行,打印效果如下:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjI5MjUsImV4cCI6MTU2MjA2MjkyNX0._vs4METaPkCza52LuN0-2NGGWIIO7v51xt40DHY1U1Q
4.4.3.2 解析TOKEN
1 2 3 4 5 6 7 8 9 10 11 12
/*** * 解析Jwt令牌數據 */ @Test public void testParseJwt(){ String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjI5MjUsImV4cCI6MTU2MjA2MjkyNX0._vs4METaPkCza52LuN0-2NGGWIIO7v51xt40DHY1U1Q"; Claims claims = Jwts.parser(). setSigningKey("itcast"). parseClaimsJws(compactJwt). getBody(); System.out.println(claims); }
打印效果:
當前時間超過過期時間,則會報錯。
4.4.4 自定義claims
我們剛纔的例子只是存儲了id和subject兩個信息,如果你想存儲更多的信息(例如角色)可以定義自定義claims。
創建測試類,並設置測試方法:
創建token:
運行打印效果:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjMyOTIsImFkZHJlc3MiOiLmt7HlnLPpu5Hpqazorq3nu4PokKXnqIvluo_lkZjkuK3lv4MiLCJuYW1lIjoi546L5LqUIiwiYWdlIjoyN30.ZSbHt5qrxz0F1Ma9rVHHAIy4jMCBGIHoNaaPQXxV_dk
解析TOKEN:
1 2 3 4 5 6 7 8 9 10 11 12
/*** * 解析Jwt令牌數據 */ @Test public void testParseJwt(){ String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjMyOTIsImFkZHJlc3MiOiLmt7HlnLPpu5Hpqazorq3nu4PokKXnqIvluo_lkZjkuK3lv4MiLCJuYW1lIjoi546L5LqUIiwiYWdlIjoyN30.ZSbHt5qrxz0F1Ma9rVHHAIy4jMCBGIHoNaaPQXxV_dk"; Claims claims = Jwts.parser(). setSigningKey("itcast"). parseClaimsJws(compactJwt). getBody(); System.out.println(claims); }
運行效果:
4.5 鑑權處理
4.5.1 思路分析
1 2 3 4 5 6 7 8
1.用戶通過訪問微服務網關調用微服務,同時攜帶頭文件信息 2.在微服務網關這裏進行攔截,攔截後獲取用戶要訪問的路徑 3.識別用戶訪問的路徑是否需要登錄,如果需要,識別用戶的身份是否能訪問該路徑[這裏可以基於數據庫設計一套權限] 4.如果需要權限訪問,用戶已經登錄,則放行 5.如果需要權限訪問,且用戶未登錄,則提示用戶需要登錄 6.用戶通過網關訪問用戶微服務,進行登錄驗證 7.驗證通過後,用戶微服務會頒發一個令牌給網關,網關會將用戶信息封裝到頭文件中,並響應用戶 8.用戶下次訪問,攜帶頭文件中的令牌信息即可識別是否登錄
4.5.2用戶登錄簽發TOKEN
(1)生成令牌工具類
在changgou-common中創建類entity.JwtUtil,主要輔助生成Jwt令牌信息,代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
public class JwtUtil { //有效期爲 public static final Long JWT_TTL = 3600000L;// 60 * 60 *1000 一個小時 //Jwt令牌信息 public static final String JWT_KEY = "itcast"; public static String createJWT(String id, String subject, Long ttlMillis) { //指定算法 SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; //當前系統時間 long nowMillis = System.currentTimeMillis(); //令牌簽發時間 Date now = new Date(nowMillis); //如果令牌有效期爲null,則默認設置有效期1小時 if(ttlMillis==null){ ttlMillis=JwtUtil.JWT_TTL; } //令牌過期時間設置 long expMillis = nowMillis + ttlMillis; Date expDate = new Date(expMillis); //生成祕鑰 SecretKey secretKey = generalKey(); //封裝Jwt令牌信息 JwtBuilder builder = Jwts.builder() .setId(id) //唯一的ID .setSubject(subject) // 主題 可以是JSON數據 .setIssuer("admin") // 簽發者 .setIssuedAt(now) // 簽發時間 .signWith(signatureAlgorithm, secretKey) // 簽名算法以及密匙 .setExpiration(expDate); // 設置過期時間 return builder.compact(); } /** * 生成加密 secretKey * @return */ public static SecretKey generalKey() { byte[] encodedKey = Base64.getEncoder().encode(JwtUtil.JWT_KEY.getBytes()); SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); return key; } /** * 解析令牌數據 * @param jwt * @return * @throws Exception */ public static Claims parseJWT(String jwt) throws Exception { SecretKey secretKey = generalKey(); return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); } }
(2) 用戶登錄成功 則 簽發TOKEN,修改登錄的方法:
代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
/*** * 用戶登錄 */ @RequestMapping(value = "/login") public Result login(String username,String password){ //查詢用戶信息 User user = userService.findById(username); if(user!=null && BCrypt.checkpw(password,user.getPassword())){ //設置令牌信息 Map<String,Object> info = new HashMap<String,Object>(); info.put("role","USER"); info.put("success","SUCCESS"); info.put("username",username); //生成令牌 String jwt = JwtUtil.createJWT(UUID.randomUUID().toString(), JSON.toJSONString(info),null); return new Result(true,StatusCode.OK,"登錄成功!",jwt); } return new Result(false,StatusCode.LOGINERROR,"賬號或者密碼錯誤!"); }
4.5.3 網關過濾器攔截請求處理
拷貝JwtUtil到changgou-gateway-web中
4.5.4 自定義全局過濾器
創建 過濾器類,如圖所示:
AuthorizeFilter代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
@Component public class AuthorizeFilter implements GlobalFilter, Ordered { //令牌頭名字 private static final String AUTHORIZE_TOKEN = "Authorization"; /*** * 全局過濾器 * @param exchange * @param chain * @return */ @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { //獲取Request、Response對象 ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); //獲取請求的URI String path = request.getURI().getPath(); //如果是登錄、goods等開放的微服務[這裏的goods部分開放],則直接放行,這裏不做完整演示,完整演示需要設計一套權限系統 if (path.startsWith("/api/user/login") || path.startsWith("/api/brand/search/")) { //放行 Mono<Void> filter = chain.filter(exchange); return filter; } //獲取頭文件中的令牌信息 String tokent = request.getHeaders().getFirst(AUTHORIZE_TOKEN); //如果頭文件中沒有,則從請求參數中獲取 if (StringUtils.isEmpty(tokent)) { tokent = request.getQueryParams().getFirst(AUTHORIZE_TOKEN); } //如果爲空,則輸出錯誤代碼 if (StringUtils.isEmpty(tokent)) { //設置方法不允許被訪問,405錯誤代碼 response.setStatusCode(HttpStatus.METHOD_NOT_ALLOWED); return response.setComplete(); } //解析令牌數據 try { Claims claims = JwtUtil.parseJWT(tokent); } catch (Exception e) { e.printStackTrace(); //解析失敗,響應401錯誤 response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } //放行 return chain.filter(exchange); } /*** * 過濾器執行順序 * @return */ @Override public int getOrder() { return 0; } }
4.5.5 配置過濾規則
修改網關係統的yml文件:
上述代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
spring: cloud: gateway: globalcors: corsConfigurations: '[/**]': # 匹配所有請求 allowedOrigins: "*" #跨域處理 允許所有的域 allowedMethods: # 支持的方法 - GET - POST - PUT - DELETE routes: - id: changgou_goods_route uri: lb://goods predicates: - Path=/api/album/**,/api/brand/**,/api/cache/**,/api/categoryBrand/**,/api/category/**,/api/para/**,/api/pref/**,/api/sku/**,/api/spec/**,/api/spu/**,/api/stockBack/**,/api/template/** filters: - StripPrefix=1 - name: RequestRateLimiter #請求數限流 名字不能隨便寫 ,使用默認的facatory args: key-resolver: "#{@ipKeyResolver}" redis-rate-limiter.replenishRate: 1 redis-rate-limiter.burstCapacity: 1 #用戶微服務 - id: changgou_user_route uri: lb://user predicates: - Path=/api/user/**,/api/address/**,/api/areas/**,/api/cities/**,/api/provinces/** filters: - StripPrefix=1 application: name: gateway-web #Redis配置 redis: host: 192.168.211.132 port: 6379 server: port: 8001 eureka: client: service-url: defaultZone: http://127.0.0.1:7001/eureka instance: prefer-ip-address: true management: endpoint: gateway: enabled: true web: exposure: include: true
測試訪問http://localhost:8001/api/user/login?username=changgou&password=changgou
,效果如下:
測試訪問http://localhost:8001/api/user
,效果如下:
參考官方手冊:
4.6 會話保持
用戶每次請求的時候,我們都需要獲取令牌數據,方法有多重,可以在每次提交的時候,將數據提交到頭文件中,也可以將數據存儲到Cookie中,每次從Cookie中校驗數據,還可以每次將令牌數據以參數的方式提交到網關,這裏面採用Cookie的方式比較容易實現。
4.6.1 登錄封裝Cookie
修改user微服務,每次登錄的時候,添加令牌信息到Cookie中,修改changgou-service-user的com.changgou.user.controller.UserController
的login
方法,代碼如下:
4.6.2 過濾器獲取令牌數據
每次在網關中通過過濾器獲取Cookie中的令牌,然後對令牌數據進行解析,修改微服務網關changgou-gateway-web中的AuthorizeFilter,代碼如下:
登錄後測試,可以識別用戶身份,不登錄無法識別。如下訪問http://localhost:8001/api/user
會攜帶令牌數據:
4.6.3 添加Header信息
我們還可以在Gateway的全局過濾器中添加請求頭信息,例如可以講令牌信息添加到請求頭中,在微服務中獲取頭信息,如下代碼:
修改微服務網關中的AuthorizeFilter過濾器,在令牌信息校驗那塊將令牌加入到請求頭中,如下代碼:
在changgou-service-user微服務的UserController的findAll方法中獲取請求頭測試,代碼如下:
後臺輸出令牌數據如下: