使用SpringCloud技術(shù)棧搭建微服務(wù)集群,可以選擇的組件比較多,由于有些組件已經(jīng)閉源或停更,這里主要選用spring-cloud-alibaba作為我們的技術(shù)棧。
- 服務(wù)注冊(cè)與發(fā)現(xiàn): nacos-discovery
- 統(tǒng)一配置管理:nacos-config
- 微服務(wù)網(wǎng)關(guān):spring cloud gateway
由于nacos本身就已經(jīng)是完備的服務(wù),故參考官方文檔直接安裝使用就可以,這里重點(diǎn)介紹如何使用SpringCloud Gateway實(shí)現(xiàn)路由轉(zhuǎn)發(fā)和身份認(rèn)證。
一、微服務(wù)架構(gòu)

- 所有的請(qǐng)求先通過Nginx進(jìn)行負(fù)載和轉(zhuǎn)發(fā)
- API Gateway負(fù)責(zé)進(jìn)行微服務(wù)內(nèi)的路由轉(zhuǎn)發(fā)和身份認(rèn)證
二、實(shí)現(xiàn)路由轉(zhuǎn)發(fā)
1. 引入gateway包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
復(fù)制代碼
需要注意的是:如果啟動(dòng)時(shí)報(bào)錯(cuò),提示在依賴中發(fā)現(xiàn)的springMvc與gateway不能兼容,需要?jiǎng)h除spring-boot-starter-web相關(guān)引用
**********************************************************
Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway at this time. Please remove spring-boot-starter-web dependency.
**********************************************************
復(fù)制代碼
2. 添加啟動(dòng)類
@EnableDiscoveryClient
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
復(fù)制代碼
- @EnableDiscoveryClient 用于集群下的服務(wù)注冊(cè)與發(fā)現(xiàn)
3. 配置路由表
配置文件最好選用YAML,結(jié)構(gòu)清晰易讀
spring:
application:
name: cloud-api #服務(wù)名
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # nacos服務(wù)器地址
gateway:
routes:
- id: cloud-user
uri: lb://cloud-user # 后端服務(wù)名
predicates:
- Path=/user/** # 路由地址
filters:
- StripPrefix=1 # 去掉前綴
server:
port: 8000
# 用于actuator暴露監(jiān)控指標(biāo)
management:
endpoints:
web:
exposure:
include: "*"
復(fù)制代碼
- StripPrefix=1 用于在路由轉(zhuǎn)發(fā)時(shí)去掉前綴地址,若無則將前綴一起轉(zhuǎn)發(fā)給后端服務(wù),比如: 請(qǐng)求地址為:http://localhost:8000/user/home 在沒有加StripPrefix時(shí),轉(zhuǎn)發(fā)給后端服務(wù)地址為:http://{cloud-user}/user/home,否則為http://{cloud-user}/home
- management 配置用于暴露監(jiān)控指標(biāo),可請(qǐng)求 http://localhost:8000/actuator/gateway/routes 獲取所有的映射路由
三、實(shí)現(xiàn)身份認(rèn)證
在分布式系統(tǒng)中有三種常用的身份認(rèn)證方式:
1.使用Session,可使用spring security來實(shí)現(xiàn)Session的管理 ,使用redis來存儲(chǔ)會(huì)話狀態(tài),客戶端的sessionID需要cookie來存儲(chǔ)

優(yōu)點(diǎn) :
- 使用方便,客戶端無感知
- 安全性高
- 會(huì)話管理支持較好
缺點(diǎn) :
- 對(duì)客戶端應(yīng)用支持不友好
- 無法實(shí)現(xiàn)跨站跨端共享
- 實(shí)現(xiàn)方式相對(duì)復(fù)雜
- 需要客戶端Cookie支持
2.使用Token,由服務(wù)端簽發(fā),并將用戶信息存儲(chǔ)在redis中,客戶端每次請(qǐng)求都帶上進(jìn)行驗(yàn)證

優(yōu)點(diǎn) :
- 對(duì)多端共享支持友好
- 對(duì)多端共享會(huì)話支持友好
- 實(shí)現(xiàn)方式相對(duì)簡(jiǎn)單
- 安全性高
- 無須Cookie支持
缺點(diǎn) :
- 會(huì)話過期時(shí)間維護(hù)較復(fù)雜
- 服務(wù)端需要維持會(huì)話狀態(tài)
3.使用JWT,由服務(wù)端簽發(fā)且不保存會(huì)話狀態(tài),客戶端每次請(qǐng)求都需要驗(yàn)證合法性

優(yōu)點(diǎn) :
- 對(duì)多端共享支持友好
- 對(duì)多端共享會(huì)話支持友好
- 服務(wù)端無會(huì)話狀態(tài)
- 無須Cookie支持
- 可攜帶載荷數(shù)據(jù)
缺點(diǎn) :
- 會(huì)話過期時(shí)間維護(hù)較復(fù)雜
- 默認(rèn)情況下,安全性較低
- 一旦簽發(fā)無法撤銷,或撤銷較復(fù)雜
簡(jiǎn)單token驗(yàn)證
本例子的token是uuid生成隨機(jī)碼的方式,沒有使用算法做驗(yàn)證,這樣有可能導(dǎo)致客戶端窮舉token,不斷查詢r(jià)edis造成風(fēng)險(xiǎn)。在生產(chǎn)環(huán)境中可使用一定算法進(jìn)行token簽發(fā)(如加密解密,有效時(shí)間戳等),保證偽造token對(duì)服務(wù)器的影響降到最低。
1. 用戶登陸保存session狀態(tài)
@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);
}
}
復(fù)制代碼
保存會(huì)話狀態(tài)時(shí),需要設(shè)置過期時(shí)間,且不宜過長(zhǎng)或過短。如進(jìn)一步思考如何刷新會(huì)話過期時(shí)間。
2. 增加AuthCheckFilter,攔截路由請(qǐng)求
@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("當(dāng)前請(qǐng)求的url:{}, method:{}", request.getURI().getPath(), request.getMethodValue());
if (Strings.isEmpty(token)) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// 2. 驗(yàn)證用戶是否已登陸
LoginUser loginUser = this.session.getSession(token);
if (loginUser == null) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// 3. 將用戶名傳遞給后端服務(wù)
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);
};
}
}
復(fù)制代碼
此攔截器作用為驗(yàn)證請(qǐng)求是否已登陸,否則返回401狀態(tài),并將用戶會(huì)話信息傳遞給后端服務(wù)。
3. 配置Filter
在gateway項(xiàng)目的yml配置文件中配置需要進(jìn)行驗(yàn)證的路由filters: AuthCheckFilter
spring:
gateway:
routes:
- id: cloud-user
uri: lb://cloud-user # 后端服務(wù)名
predicates:
- Path=/user/** # 路由地址
filters:
- name: AuthCheckFilter #會(huì)話驗(yàn)證
- StripPrefix=1 # 去掉前綴
復(fù)制代碼
由此就實(shí)現(xiàn)了對(duì)后端路由地址的身份驗(yàn)證功能