好好學習,天天向上
本文已收錄至我的Github倉庫DayDayUP:github.com/RobodLee/DayDayUP,歡迎Star,更多文章請前往:目錄導航
- 暢購商城(一):環境搭建
- 暢購商城(二):分散式檔案系統FastDFS
- 暢購商城(三):商品管理
- 暢購商城(四):Lua、OpenResty、Canal實現廣告快取與同步
- 暢購商城(五):Elasticsearch實現商品搜尋
- 暢購商城(六):商品搜尋
- 暢購商城(七):Thymeleaf實現靜態頁
- 暢購商城(八):微服務閘道器和JWT令牌
微服務閘道器
介紹
閘道器是介於使用者和微服務之前的中間層。說白了,閘道器就像是小區的保安,無論你想到小區的哪一戶人家去,你都得先通過小區的大門。所以,小區的保安可以做人員統計,還可以控制某個時間段進去小區的人數,限制進入小區的資格等。保證了小區業主們的安全。微服務閘道器同樣起著這些作用。
為什麼要有微服務閘道器
不同的微服務一般會有不同的網路地址,而外部客戶端可能需要呼叫多個服務的介面才能完成一個業務需求,如果讓客戶端直接與各個微服務通訊,會有以下的問題:
- 客戶端會多次請求不同的微服務,增加了客戶端的複雜性
- 存在跨域請求,在一定場景下處理相對複雜
- 認證複雜,每個服務都需要獨立認證
- 難以重構,隨著專案的迭代,可能需要重新劃分微服務。例如,可能將多個服務合併成一個或者將一個服務拆分成多個。如果客戶端直接與微服務通訊,那麼重構將會很難實施
- 某些微服務可能使用了防火牆 / 瀏覽器不友好的協議,直接訪問會有一定的困難
那麼有了微服務閘道器之後,這些問題就可以得到解決。它有著以下優點。
- 安全 ,只有閘道器係統對外進行暴露,微服務可以隱藏在內網,通過防火牆保護。
- 易於監控。可以在閘道器收集監控資料並將其推送到外部系統進行分析。
- 易於認證。可以在閘道器上進行認證,然後再將請求轉發到後端的微服務,而無須在每個微服務中進行認證。
- 減少了客戶端與各個微服務之間的互動次數
- 易於統一授權。
總結:微服務閘道器就是一個系統,通過暴露該微服務閘道器係統,方便我們進行相關的鑑權,安全控制,日誌統一處理,易於監控的相關功能。
閘道器微服務
微服務搭建
一個專案中可能會用到不止一個閘道器,所以我們將閘道器微服務放在changgou-gateway父工程下。現在我們建立一個名為changou-gateway-web的微服務。有些依賴是所有閘道器微服務都要用到的,所以將這些依賴放在父工程下:
<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>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
啟動類和配置檔案不能少,啟動類就不貼了,配置檔案如下?
spring:
application:
name: gateway-web
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]': # 匹配所有請求
allowedOrigins: "*" #跨域處理 允許所有的域
allowedMethods: # 支援的方法
- GET
- POST
- PUT
- DELETE
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
閘道器過濾配置
Host 路由
# 使用者請求的域名規格配置,所有以robod.changgou.com開頭的請求都將被路由到http://localhost:18081微服務
# 例如 http://robod.changgou.com:8001/brand ——> http://localhost:18081/brand
# 但是首先得在hosts檔案中配置一下: 127.0.0.1 robod.changgou.com
spring:
cloud:
gateway:
routes:
- id: changgou_goods_route # 唯一識別符號
uri: http://localhost:18081
predicates:
- Host=robod.changgou.com**
- Path 路徑匹配過濾配置
# 所有以/brand開頭的請求都將路由到http://localhost:18081
# 例如 localhost:8001/brand ——> localhost:18081/brand
spring:
cloud:
gateway:
routes:
- id: changgou_goods_route
uri: http://localhost:18081
predicates:
- Path=/brand/**
PrefixPath 過濾配置
# 自動加上某個字首,使用者請求/** ——>/brand/**
# 例如 localhost:8001/111 ——> localhost:8001/brand/111 ——> localhost:18081/brand/111
spring:
cloud:
gateway:
routes:
- id: changgou_goods_route
uri: http://localhost:18081
predicates:
- Path=/**
filters:
- PrefixPath=/brand
StripPrefix 過濾配置
# 將請求路徑中的前n個路徑去掉,請求路徑以/區分,一個/代表一個路徑
# 例如 localhost:8001/api/brand/111 ——> localhost:8001/brand/111 ——> localhost:18081/brand/111
spring:
cloud:
gateway:
routes:
- id: changgou_goods_route
uri: http://localhost:18081
predicates:
- Path=/**
filters:
- StripPrefix=1
LoadBalancerClient 路由過濾器(客戶端負載均衡)
# 使用LoadBalancerClient實現負載均衡,後面的goods是微服務的名稱,主要應用於叢集環境
# 比如現在有5臺伺服器都是goods微服務,閘道器就會自動將請求傳送給不同的伺服器達到負載均衡的目的
spring:
cloud:
gateway:
routes:
- id: changgou_goods_route
uri: lb://goods
閘道器限流
當訪問量多大的時候,我們的服務就可能會掛掉,所以我們需要對每個微服務進行限流,但是這樣比較麻煩。有了閘道器之後,我們可以對閘道器進行限流,因為所有的請求必須通過閘道器才能到達微服務,這樣比較方便。
令牌桶演算法
常見的限流演算法有計數器,漏斗,令牌桶演算法。令牌桶演算法有以下幾個特點:
- 所有的請求在處理之前都需要拿到一個可用的令牌才會被處理;
- 根據限流大小,設定按照一定的速率往桶裡新增令牌;
- 桶設定最大的放置令牌限制,當桶滿時、新新增的令牌就被丟棄或者拒絕;
- 請求達到後首先要獲取令牌桶中的令牌,拿著令牌才可以進行其他的業務邏輯,處理完業務邏輯之後,將令牌直接刪除;
- 令牌桶有最低限額,當桶中的令牌達到最低限額的時候,請求處理完之後將不會刪除令牌,以此保證足夠的限流
使用令牌桶進行請求次數限流
spring cloud gateway 預設使用redis的RateLimter限流演算法來實現。首先在changgou-gateway-web中新增Redis的依賴:
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
然後我們需要有限流的Key,這裡用IP來當作限流的Key,限制某一個IP在一定時間段的訪問次數,在啟動類中定義一個Bean用於獲取key:
@Bean(name = "ipKeyResolver")
public KeyResolver userKeyResolver() {
return exchange -> {
String ip = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getHostName();
return Mono.just(ip);
};
}
我這裡使用了Lamda去簡化書寫。接下來還得在配置檔案中配置一下:
spring:
application:
name: gateway-web
cloud:
gateway:
routes:
filters:
- name: RequestRateLimiter #請求數限流 名字不能隨便寫 ,使用預設的factory
args:
# 使用者身份唯一識別符號
key-resolver: "#{@ipKeyResolver}"
# 允許使用者每秒執行多少請求,而不會丟棄任何請求。這是令牌桶填充的速率
redis-rate-limiter.replenishRate: 1
# 令牌桶的容量,允許在一秒鐘內完成的最大請求數
redis-rate-limiter.burstCapacity: 1
既然是使用redis的RateLimter限流演算法,那麼Redis的配置自然不能少。
#Redis配置
spring:
application:
redis:
host: 192.168.31.200
port: 6379
限流的配置就配置好了,現在如果在1秒內請求超過1次的話就會被拒絕。
JWT
在實現使用者登入功能之前,我們先來介紹一下JWT(JSON Web Token)。是一種用於通訊雙方之間傳遞安全資訊的簡潔的、URL安全的表述性宣告規範。
JWT的構成
一個JWT實際上就是一個字串,它由三部分組成,頭部、載荷與簽名。為了能夠直觀的看到JWT的結構,我畫了一張思維導圖:
最終生成的JWT令牌就是下面這樣,有三部分,用 .
分隔。
base64UrlEncode(JWT 頭)+"."+base64UrlEncode(載荷)+"."+HMACSHA256(base64UrlEncode(JWT 頭) + "." + base64UrlEncode(有效載荷),金鑰)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
JWT的使用
- 匯入依賴:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
- 建立Token
public String createToken() {
JwtBuilder builder = Jwts.builder()
.setId("test1")
.setSubject("Robod")
.setAudience("馬化騰")
.setIssuedAt(new Date());
.signWith(SignatureAlgorithm.HS256,"robod666");
Map<String,Object> map = new HashMap<>();
map.put("ha","哈哈哈");
builder.addClaims(map);
return builder.compact();
}
- 解析Token
public String parseToken() {
String compactJwt="eyJhbGciOiJIUzI1NiJ9" +
".eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjIyODd9" +
".RBLpZ79USMplQyfJCZFD2muHV_KLks7M1ZsjTu6Aez4";
Claims claims = Jwts.parser().
setSigningKey("robod666").
parseClaimsJws(compactJwt).
getBody();
return claims.toString();
}
使用者登入與鑑權
介紹了JWT之後,我們就來用JWT實現使用者登入與鑑權。流程如下:
首先我們需要準備一個JWT的工具類,JWTUtil,放在changgou-common下:
public class JwtUtil {
//預設有效期,一個小時
public static final Long JWT_TTL = 3600000L;
//Jwt令牌資訊
public static final String JWT_KEY = "RobodLee";
//金鑰
public static SecretKey secretKey = generalKey();
//生成令牌
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);
//封裝Jwt令牌資訊
JwtBuilder builder = Jwts.builder()
.setId(id) //唯一的ID
.setSubject(subject) // 主題 可以是JSON資料
.setIssuer("robod") // 簽發者
.setIssuedAt(now) // 簽發時間
.signWith(signatureAlgorithm,secretKey) // 簽名演算法以及密匙
.setExpiration(expDate); // 設定過期時間
return builder.compact();
}
//生成加密 secretKey
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getEncoder().encode(JwtUtil.JWT_KEY.getBytes());
return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
}
//解析令牌
public static Claims parseJWT(String jwt) throws Exception {
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
我發現資料提供的程式碼中每次呼叫generalKey()、parseJWT()方法的時候都去呼叫generalKey()方法去生成SecretKey,但是generalKey()方法內容是不變的,所以可以將SecretKey單獨提取出來,這樣就不用每次都呼叫generalKey()去生成了。
然後建立一個使用者微服務changou-service-user,在UserController中編寫登入邏輯?
@RequestMapping("/login")
public Result<String> login(String username, String password, HttpServletResponse response) {
User user = userService.findById(username);
if (BCrypt.checkpw(password,user.getPassword())){
Map<String,Object> tokenInfo = new HashMap<>(4);
tokenInfo.put("role","USER");
tokenInfo.put("success","SUCCESS");
tokenInfo.put("username",username);
String token = JwtUtil.createJWT(UUID.randomUUID().toString(), JSON.toJSONString(tokenInfo), null);
Cookie cookie = new Cookie("Authorization",token);
cookie.setDomain("localhost");
cookie.setPath("/");
response.addCookie(cookie);
return new Result<>(true,StatusCode.OK,"登入成功",token);
}
return new Result<>(false,StatusCode.LOGIN_ERROR,"登入失敗");
}
在這段程式碼中,呼叫Service層從資料庫中查出對應的User,然後比對password,看密碼是否正確。如果正確,就呼叫JwtUtil建立一個JWT令牌,並放入一些簡單的資訊。然後將JWT令牌存入Cookie中,並返回給前端。如果登入失敗就返回登入失敗的資訊。
然後就是在閘道器微服務中新增相應的邏輯了,在changgou-gateway-web中配置一下,配置一下User微服務的路由。
spring:
application:
name: gateway-web
cloud:
gateway:
routes:
- id: changgou_user_route # 唯一識別符號
uri: http://localhost:18088
predicates:
- Path=/api/user/**,/api/address/**,/api/areas/**,/api/cities/**,/api/provinces/**
filters:
- StripPrefix=1
再新增一個過濾器:
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
private static final String AUTHORIZE_TOKEN = "Authorization";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String token;
//從頭中獲取Token
token = request.getHeaders().getFirst(AUTHORIZE_TOKEN);
//請求頭中沒有Token就從引數中獲取
if (StringUtils.isEmpty(token)){
token = request.getQueryParams().getFirst(AUTHORIZE_TOKEN);
}
//引數中再沒有Token就從Cookie中獲取
if (StringUtils.isEmpty(token)){
HttpCookie cookie = request.getCookies().getFirst(AUTHORIZE_TOKEN);
if (cookie!=null){
token = cookie.getValue();
}
}
//還是沒有Token就攔截
if (StringUtils.isEmpty(token)){
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//Token不為空就校驗Token
try {
JwtUtil.parseJWT(token);
} catch (Exception e) {
//報異常說明Token是錯誤的,攔截
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
這段程式碼就是分別從Header,引數,Cookie中看有沒有Token資訊,沒有的話就說明使用者沒有許可權,攔截下來。有Token的話就解析一下Token有沒有錯,錯誤就攔截下來。如果都沒有問題的話就放行,將請求路由到使用者微服務中。
這是沒有Token的情況下?
當我們登陸後就會獲取到Token?
當我們攜帶著token去訪問就沒有問題了?
小結
這篇文章中,首先介紹了微服務閘道器及閘道器的搭建及過濾配置和限流配置。然後介紹了JWT,最後使用了JWT去實現了使用者登入與鑑權的操作。
如果我的文章對你有些幫助,不要忘了點贊,收藏,轉發,關注。要是有什麼好的意見歡迎在下方留言。讓我們下期再見!