parent
eed300274c
commit
400a561234
@ -0,0 +1,19 @@
|
||||
package org.example.common.constant;
|
||||
|
||||
/**
|
||||
* <h1>通用模块常量定义</h1>
|
||||
* */
|
||||
public final class CommonConstant {
|
||||
|
||||
/** RSA 公钥 */
|
||||
public static final String PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnV+iGlE1e8Z825G+ChIwRJ2H2jOMCBu" +
|
||||
"HV7BPrUE8dAGjqAlRtCaxMyJw7NV9NIUl/rY7RWBUQwelkGmGuQomnUAFIgN9f8UxSC6G935lo1ZoBVJWYmfs5ToXLz+fQugmqHZvF+Vc5l" +
|
||||
"UEo1YapeiaymkOxDORMGjzQBoxoBt316IAwNEPIvcV+F6T+WNFJX/p5Xj48Z1rtmbOQ8ffF+pEWKZGsYg/9b+pKiqFJtuyHqwj/9oxFBE98" +
|
||||
"MCu5RfK6M7Ff9/1dyNen1HKjI7Awj8ZnSceVUldcXEdnP89YagevbhtSl/+CvCsKwHq5+ZLkcuONSxE4dIFWTjxA92wJjYf9wIDAQAB";
|
||||
|
||||
/** JWT 中存储用户信息的 key */
|
||||
public static final String JWT_USER_INFO_KEY = "e-commerce-user";
|
||||
|
||||
/** 授权中心的 service-id */
|
||||
public static final String AUTHORITY_CENTER_SERVICE_ID = "e-commerce-authority-center";
|
||||
}
|
@ -0,0 +1 @@
|
||||
package org.example.common;
|
@ -0,0 +1,63 @@
|
||||
package org.example.common.util;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jws;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import org.example.common.constant.CommonConstant;
|
||||
import org.example.common.vo.LoginUserInfo;
|
||||
import sun.misc.BASE64Decoder;
|
||||
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Calendar;
|
||||
|
||||
/**
|
||||
* <h1>JWT Token 解析工具类</h1>
|
||||
* */
|
||||
public class TokenParseUtil {
|
||||
|
||||
/**
|
||||
* <h2>从 JWT Token 中解析 LoginUserInfo 对象</h2>
|
||||
* */
|
||||
public static LoginUserInfo parseUserInfoFromToken(String token) throws Exception {
|
||||
|
||||
if (null == token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Jws<Claims> claimsJws = parseToken(token, getPublicKey());
|
||||
Claims body = claimsJws.getBody();
|
||||
|
||||
// 如果 Token 已经过期了, 返回 null
|
||||
if (body.getExpiration().before(Calendar.getInstance().getTime())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 返回 Token 中保存的用户信息
|
||||
return JSON.parseObject(
|
||||
body.get(CommonConstant.JWT_USER_INFO_KEY).toString(),
|
||||
LoginUserInfo.class
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>通过公钥去解析 JWT Token</h2>
|
||||
* */
|
||||
private static Jws<Claims> parseToken(String token, PublicKey publicKey) {
|
||||
|
||||
return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>根据本地存储的公钥获取到 PublicKey 对象</h2>
|
||||
* */
|
||||
private static PublicKey getPublicKey() throws Exception {
|
||||
|
||||
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(
|
||||
new BASE64Decoder().decodeBuffer(CommonConstant.PUBLIC_KEY)
|
||||
);
|
||||
return KeyFactory.getInstance("RSA").generatePublic(keySpec);
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package org.example.common.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* <h1>通用响应对象定义</h1>
|
||||
* {
|
||||
* "code": 0,
|
||||
* "message": "",
|
||||
* "data": {}
|
||||
* }
|
||||
* */
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CommonResponse<T> implements Serializable {
|
||||
|
||||
/** 错误码 */
|
||||
private Integer code;
|
||||
|
||||
/** 错误消息 */
|
||||
private String message;
|
||||
|
||||
/** 泛型响应数据 */
|
||||
private T Data;
|
||||
|
||||
public CommonResponse(Integer code, String message) {
|
||||
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package org.example.common.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* <h1>授权中心鉴权之后给客户端的 Token</h1>
|
||||
* */
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class JwtToken {
|
||||
|
||||
/** JWT */
|
||||
private String token;
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package org.example.common.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* <h1>登录用户信息</h1>
|
||||
* */
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LoginUserInfo {
|
||||
|
||||
/** 用户 id */
|
||||
private Long id;
|
||||
|
||||
/** 用户名 */
|
||||
private String username;
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package org.example.common.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* <h1>用户名和密码</h1>
|
||||
* */
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UsernameAndPassword {
|
||||
|
||||
/** 用户名 */
|
||||
private String username;
|
||||
|
||||
/** 密码 */
|
||||
private String password;
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package org.example.conf;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* <h1>网关需要注入到容器中的 Bean</h1>
|
||||
* */
|
||||
@Configuration
|
||||
public class GatewayBeanConf {
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
return new RestTemplate();
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package org.example.constant;
|
||||
|
||||
/**
|
||||
* <h1>网关常量定义</h1>
|
||||
* */
|
||||
public class GatewayConstant {
|
||||
|
||||
/** 登录 uri */
|
||||
public static final String LOGIN_URI = "/dev-protocol-springcloud-gateway/login";
|
||||
|
||||
/** 注册 uri */
|
||||
public static final String REGISTER_URI = "/dev-protocol-springcloud-gateway/register";
|
||||
|
||||
/** 去授权中心拿到登录 token 的 uri 格式化接口 */
|
||||
public static final String AUTHORITY_CENTER_TOKEN_URL_FORMAT =
|
||||
"http://%s:%s/dev-protocol-authority-center/authority/token";
|
||||
|
||||
/** 去授权中心注册并拿到 token 的 uri 格式化接口 */
|
||||
public static final String AUTHORITY_CENTER_REGISTER_URL_FORMAT =
|
||||
"http://%s:%s/dev-protocol-authority-center/authority/register";
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package org.example.filter;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.example.constant.GatewayConstant;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* <h1>缓存请求 body 的全局过滤器</h1>
|
||||
* Spring WebFlux
|
||||
* */
|
||||
@Slf4j
|
||||
@Component
|
||||
@SuppressWarnings("all")
|
||||
public class GlobalCacheRequestBodyFilter implements GlobalFilter, Ordered {
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
|
||||
// 只对 注册和登录的信息进行缓存
|
||||
boolean isloginOrRegister =
|
||||
exchange.getRequest().getURI().getPath().contains(GatewayConstant.LOGIN_URI)
|
||||
|| exchange.getRequest().getURI().getPath().contains(GatewayConstant.REGISTER_URI);
|
||||
|
||||
if (null == exchange.getRequest().getHeaders().getContentType()
|
||||
|| !isloginOrRegister) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
// DataBufferUtils.join 拿到请求中的数据 --> DataBuffer
|
||||
return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
|
||||
|
||||
// 确保数据缓冲区不被释放, 必须要 DataBufferUtils.retain
|
||||
DataBufferUtils.retain(dataBuffer);
|
||||
// defer、just 都是去创建数据源, 得到当前数据的副本
|
||||
Flux<DataBuffer> cachedFlux = Flux.defer(() ->
|
||||
Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
|
||||
// 重新包装 ServerHttpRequest, 重写 getBody 方法, 能够返回请求数据
|
||||
ServerHttpRequest mutatedRequest =
|
||||
new ServerHttpRequestDecorator(exchange.getRequest()) {
|
||||
@Override
|
||||
public Flux<DataBuffer> getBody() {
|
||||
return cachedFlux;
|
||||
}
|
||||
};
|
||||
// 将包装之后的 ServerHttpRequest 向下继续传递
|
||||
return chain.filter(exchange.mutate().request(mutatedRequest).build());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return HIGHEST_PRECEDENCE + 1;
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package org.example.filter;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.time.StopWatch;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* <h1>全局接口耗时日志过滤器</h1>
|
||||
* */
|
||||
@Slf4j
|
||||
@Component
|
||||
public class GlobalElapsedLogFilter implements GlobalFilter, Ordered {
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
|
||||
// 前置逻辑
|
||||
StopWatch sw = StopWatch.createStarted();
|
||||
String uri = exchange.getRequest().getURI().getPath();
|
||||
|
||||
return chain.filter(exchange).then(
|
||||
// 后置逻辑
|
||||
Mono.fromRunnable(() ->
|
||||
log.info("[{}] elapsed: [{}ms]",
|
||||
uri, sw.getTime(TimeUnit.MILLISECONDS)))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return HIGHEST_PRECEDENCE;
|
||||
}
|
||||
}
|
@ -0,0 +1,175 @@
|
||||
package org.example.filter;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.example.common.constant.CommonConstant;
|
||||
import org.example.common.util.TokenParseUtil;
|
||||
import org.example.common.vo.JwtToken;
|
||||
import org.example.common.vo.LoginUserInfo;
|
||||
import org.example.common.vo.UsernameAndPassword;
|
||||
import org.example.constant.GatewayConstant;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* <h1>全局登录鉴权过滤器</h1>
|
||||
* */
|
||||
@Slf4j
|
||||
@Component
|
||||
public class GlobalLoginOrRegisterFilter implements GlobalFilter, Ordered {
|
||||
/** 注册中心客户端, 可以从注册中心中获取服务实例信息 */
|
||||
private final LoadBalancerClient loadBalancerClient;
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
public GlobalLoginOrRegisterFilter(LoadBalancerClient loadBalancerClient,
|
||||
RestTemplate restTemplate) {
|
||||
this.loadBalancerClient = loadBalancerClient;
|
||||
this.restTemplate = restTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>登录、注册、鉴权</h2>
|
||||
* 1. 如果是登录或注册, 则去授权中心拿到 Token 并返回给客户端
|
||||
* 2. 如果是访问其他的服务, 则鉴权, 没有权限返回 401
|
||||
* */
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
|
||||
// 1. 如果是登录
|
||||
if (request.getURI().getPath().contains(GatewayConstant.LOGIN_URI)) {
|
||||
// 去授权中心拿 token
|
||||
String token = getTokenFromAuthorityCenter(
|
||||
request, GatewayConstant.AUTHORITY_CENTER_TOKEN_URL_FORMAT
|
||||
);
|
||||
// header 中不能设置 null
|
||||
response.getHeaders().add(
|
||||
CommonConstant.JWT_USER_INFO_KEY,
|
||||
null == token ? "null" : token
|
||||
);
|
||||
response.setStatusCode(HttpStatus.OK);
|
||||
return response.setComplete();
|
||||
}
|
||||
|
||||
// 2. 如果是注册
|
||||
if (request.getURI().getPath().contains(GatewayConstant.REGISTER_URI)) {
|
||||
// 去授权中心拿 token: 先创建用户, 再返回 Token
|
||||
String token = getTokenFromAuthorityCenter(
|
||||
request,
|
||||
GatewayConstant.AUTHORITY_CENTER_REGISTER_URL_FORMAT
|
||||
);
|
||||
response.getHeaders().add(
|
||||
CommonConstant.JWT_USER_INFO_KEY,
|
||||
null == token ? "null" : token
|
||||
);
|
||||
response.setStatusCode(HttpStatus.OK);
|
||||
return response.setComplete();
|
||||
}
|
||||
|
||||
// 3. 访问其他的服务, 则鉴权, 校验是否能够从 Token 中解析出用户信息
|
||||
HttpHeaders headers = request.getHeaders();
|
||||
String token = headers.getFirst(CommonConstant.JWT_USER_INFO_KEY);
|
||||
LoginUserInfo loginUserInfo = null;
|
||||
|
||||
try {
|
||||
loginUserInfo = TokenParseUtil.parseUserInfoFromToken(token);
|
||||
} catch (Exception ex) {
|
||||
log.error("parse user info from token error: [{}]", ex.getMessage(), ex);
|
||||
}
|
||||
|
||||
// 获取不到登录用户信息, 返回 401
|
||||
if (null == loginUserInfo) {
|
||||
response.setStatusCode(HttpStatus.UNAUTHORIZED);
|
||||
return response.setComplete();
|
||||
}
|
||||
|
||||
// 解析通过, 则放行
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return HIGHEST_PRECEDENCE + 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>从授权中心获取 Token</h2>
|
||||
* */
|
||||
private String getTokenFromAuthorityCenter(ServerHttpRequest request, String uriFormat) {
|
||||
|
||||
// service id 就是服务名字, 负载均衡
|
||||
ServiceInstance serviceInstance = loadBalancerClient.choose(
|
||||
CommonConstant.AUTHORITY_CENTER_SERVICE_ID
|
||||
);
|
||||
log.info("Nacos Client Info: [{}], [{}], [{}]",
|
||||
serviceInstance.getServiceId(), serviceInstance.getInstanceId(),
|
||||
JSON.toJSONString(serviceInstance.getMetadata()));
|
||||
|
||||
String requestUrl = String.format(
|
||||
uriFormat, serviceInstance.getHost(), serviceInstance.getPort()
|
||||
);
|
||||
UsernameAndPassword requestBody = JSON.parseObject(
|
||||
parseBodyFromRequest(request), UsernameAndPassword.class
|
||||
);
|
||||
log.info("login request url and body: [{}], [{}]", requestUrl, JSON.toJSONString(requestBody));
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
JwtToken token = restTemplate.postForObject(
|
||||
requestUrl,
|
||||
new HttpEntity<>(JSON.toJSONString(requestBody), headers),
|
||||
JwtToken.class
|
||||
);
|
||||
|
||||
if (null != token) {
|
||||
return token.getToken();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>从 Post 请求中获取到请求数据</h2>
|
||||
* */
|
||||
private String parseBodyFromRequest(ServerHttpRequest request) {
|
||||
|
||||
// 获取请求体
|
||||
Flux<DataBuffer> body = request.getBody();
|
||||
// 原子引用
|
||||
AtomicReference<String> bodyRef = new AtomicReference<>();
|
||||
|
||||
// 订阅缓冲区去消费请求体中的数据
|
||||
body.subscribe(buffer -> {
|
||||
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
|
||||
// 一定要使用 DataBufferUtils.release 释放掉, 因为前面使用 DataBufferUtils.retain(dataBuffer); 保证不被释放, 这里不手动释放, 会出现内存泄露
|
||||
DataBufferUtils.release(buffer);
|
||||
bodyRef.set(charBuffer.toString());
|
||||
});
|
||||
|
||||
// 获取 request body
|
||||
return bodyRef.get();
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package org.example.filter;
|
||||
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* <h1>HTTP 请求头部携带 Token 验证过滤器</h1>
|
||||
* */
|
||||
public class HeaderTokenGatewayFilter implements GatewayFilter, Ordered {
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
|
||||
// 从 HTTP Header 中寻找 key 为 token, value 为 imooc 的键值对
|
||||
String name = exchange.getRequest().getHeaders().getFirst("token");
|
||||
if ("imooc".equals(name)) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
// 标记此次请求没有权限, 并结束这次请求
|
||||
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
|
||||
return exchange.getResponse().setComplete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return HIGHEST_PRECEDENCE + 2;
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package org.example.filter.factory;
|
||||
|
||||
import org.example.filter.HeaderTokenGatewayFilter;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
|
||||
public class HeaderTokenGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
|
||||
@Override
|
||||
public GatewayFilter apply(Object config) {
|
||||
return new HeaderTokenGatewayFilter();
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue