使用SpringCloud技術棧搭建微服務集群,可以選擇的組件比較多,由于有些組件已經閉源或停更,這里主要選用spring-cloud-alibaba作為我們的技術棧。
- 服務注冊與發現: nacos-discovery
- 統一配置管理:nacos-config
- 微服務網關:spring cloud gateway
由于nacos本身就已經是完備的服務,故參考官方文檔直接安裝使用就可以,這里重點介紹如何使用SpringCloud Gateway實現路由轉發和身份認證。
一、微服務架構
- 所有的請求先通過Nginx進行負載和轉發
- API Gateway負責進行微服務內的路由轉發和身份認證
二、實現路由轉發
1. 引入gateway包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
復制代碼
需要注意的是:如果啟動時報錯,提示在依賴中發現的springMvc與gateway不能兼容,需要刪除spring-boot-starter-web相關引用
**********************************************************
Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway at this time. Please remove spring-boot-starter-web dependency.
**********************************************************
復制代碼
2. 添加啟動類
@EnableDiscoveryClient
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
復制代碼
- @EnableDiscoveryClient 用于集群下的服務注冊與發現
3. 配置路由表
配置文件最好選用YAML,結構清晰易讀
spring:
application:
name: cloud-api #服務名
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # nacos服務器地址
gateway:
routes:
- id: cloud-user
uri: lb://cloud-user # 后端服務名
predicates:
- Path=/user/** # 路由地址
filters:
- StripPrefix=1 # 去掉前綴
server:
port: 8000
# 用于actuator暴露監控指標
management:
endpoints:
web:
exposure:
include: "*"
復制代碼
- StripPrefix=1 用于在路由轉發時去掉前綴地址,若無則將前綴一起轉發給后端服務,比如: 請求地址為:http://localhost:8000/user/home 在沒有加StripPrefix時,轉發給后端服務地址為:http://{cloud-user}/user/home,否則為http://{cloud-user}/home
- management 配置用于暴露監控指標,可請求 http://localhost:8000/actuator/gateway/routes 獲取所有的映射路由
三、實現身份認證
在分布式系統中有三種常用的身份認證方式:
1.使用Session,可使用spring security來實現Session的管理 ,使用redis來存儲會話狀態,客戶端的sessionID需要cookie來存儲
優點 :
- 使用方便,客戶端無感知
- 安全性高
- 會話管理支持較好
缺點 :
- 對客戶端應用支持不友好
- 無法實現跨站跨端共享
- 實現方式相對復雜
- 需要客戶端Cookie支持
2.使用Token,由服務端簽發,并將用戶信息存儲在redis中,客戶端每次請求都帶上進行驗證
優點 :
- 對多端共享支持友好
- 對多端共享會話支持友好
- 實現方式相對簡單
- 安全性高
- 無須Cookie支持
缺點 :
- 會話過期時間維護較復雜
- 服務端需要維持會話狀態
3.使用JWT,由服務端簽發且不保存會話狀態,客戶端每次請求都需要驗證合法性
優點 :
- 對多端共享支持友好
- 對多端共享會話支持友好
- 服務端無會話狀態
- 無須Cookie支持
- 可攜帶載荷數據
缺點 :
- 會話過期時間維護較復雜
- 默認情況下,安全性較低
- 一旦簽發無法撤銷,或撤銷較復雜
簡單token驗證
本例子的token是uuid生成隨機碼的方式,沒有使用算法做驗證,這樣有可能導致客戶端窮舉token,不斷查詢redis造成風險。在生產環境中可使用一定算法進行token簽發(如加密解密,有效時間戳等),保證偽造token對服務器的影響降到最低。
1. 用戶登陸保存session狀態
@Service
public class Session {
@Autowired
private RedisTemplate<String, String> redisTemplate;
Long expireTime = 10800L;
/**
* 保存session
* @param loginUser
*/
public void saveSession(LoginUser loginUser) {
String key = String.format("login:user:%s", loginUser.userToken);
redisTemplate.opsForValue().set(key, JSON.toJSONString(loginUser),
expireTime, TimeUnit.SECONDS);
}
/**
* 獲取session
* @param token
* @return
*/
public LoginUser getSession(String token){
String key = String.format("login:user:%s", token);
String s = redisTemplate.opsForValue().get(key);
if (Strings.isEmpty(s)){
return null;
}
return JSON.parseobject(s, LoginUser.class);
}
}
復制代碼
保存會話狀態時,需要設置過期時間,且不宜過長或過短。如進一步思考如何刷新會話過期時間。
2. 增加AuthCheckFilter,攔截路由請求
@Slf4j
@Component
public class AuthCheckFilter extends AbstractGatewayFilterFactory {
@Autowired
private Session session;
@Override
public GatewayFilter apply(Object config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
// 1. 獲取token
String token = request.getHeaders().getFirst("token");
log.info("當前請求的url:{}, method:{}", request.getURI().getPath(), request.getMethodValue());
if (Strings.isEmpty(token)) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// 2. 驗證用戶是否已登陸
LoginUser loginUser = this.session.getSession(token);
if (loginUser == null) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// 3. 將用戶名傳遞給后端服務
ServerWebExchange build;
try {
ServerHttpRequest host = exchange.getRequest().mutate()
.header("X-User-Name", loginUser.userName)
// 中文字符需要編碼
.header("X-Real-Name", URLEncoder.encode(loginUser.realName, "utf-8"))
.build();
build = exchange.mutate().request(host).build();
} catch (UnsupportedEncodingException e) {
build = exchange;
}
return chain.filter(build);
};
}
}
復制代碼
此攔截器作用為驗證請求是否已登陸,否則返回401狀態,并將用戶會話信息傳遞給后端服務。
3. 配置Filter
在gateway項目的yml配置文件中配置需要進行驗證的路由filters: AuthCheckFilter
spring:
gateway:
routes:
- id: cloud-user
uri: lb://cloud-user # 后端服務名
predicates:
- Path=/user/** # 路由地址
filters:
- name: AuthCheckFilter #會話驗證
- StripPrefix=1 # 去掉前綴
復制代碼
由此就實現了對后端路由地址的身份驗證功能