Compare commits
5 Commits
4c744ab351
...
bd50b17709
Author | SHA1 | Date |
---|---|---|
土豆兄弟 | bd50b17709 | 1 month ago |
土豆兄弟 | 5f96e63086 | 1 month ago |
土豆兄弟 | 676a1e8004 | 1 month ago |
土豆兄弟 | 400a561234 | 1 month ago |
土豆兄弟 | eed300274c | 2 months ago |
@ -1,3 +0,0 @@
|
||||
# SpringCloud - Gateway
|
||||
|
||||
- 对 SpringCloud - Gateway 进行研究
|
@ -0,0 +1,17 @@
|
||||
package org.example;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
|
||||
/**
|
||||
* <h1>网关启动入口</h1>
|
||||
* */
|
||||
@EnableDiscoveryClient
|
||||
@SpringBootApplication
|
||||
public class GatewayApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(GatewayApplication.class, args);
|
||||
}
|
||||
}
|
@ -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,122 @@
|
||||
package org.example.conf;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
|
||||
import org.springframework.cloud.gateway.route.RouteDefinition;
|
||||
import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
|
||||
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.ApplicationEventPublisherAware;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 事件推送 Aware: 动态更新路由网关 Service
|
||||
*
|
||||
* 实现 ApplicationEventPublisherAware 事件推送接口
|
||||
* */
|
||||
@Slf4j
|
||||
@Service
|
||||
@SuppressWarnings("all")
|
||||
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {
|
||||
|
||||
/** 写路由定义 */
|
||||
private final RouteDefinitionWriter routeDefinitionWriter;
|
||||
/** 获取路由定义 */
|
||||
private final RouteDefinitionLocator routeDefinitionLocator;
|
||||
|
||||
/** 事件发布 */
|
||||
private ApplicationEventPublisher publisher;
|
||||
|
||||
public DynamicRouteServiceImpl(RouteDefinitionWriter routeDefinitionWriter,
|
||||
RouteDefinitionLocator routeDefinitionLocator) {
|
||||
this.routeDefinitionWriter = routeDefinitionWriter;
|
||||
this.routeDefinitionLocator = routeDefinitionLocator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
|
||||
// 完成事件推送句柄的初始化
|
||||
this.publisher = applicationEventPublisher;
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>增加路由定义</h2>
|
||||
* */
|
||||
public String addRouteDefinition(RouteDefinition definition) {
|
||||
|
||||
log.info("gateway add route: [{}]", definition);
|
||||
|
||||
// 保存路由配置并发布
|
||||
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
|
||||
// 发布事件通知给 Gateway, 同步新增的路由定义
|
||||
this.publisher.publishEvent(new RefreshRoutesEvent(this));
|
||||
|
||||
return "success";
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>更新路由</h2>
|
||||
* */
|
||||
public String updateList(List<RouteDefinition> definitions) {
|
||||
|
||||
log.info("gateway update route: [{}]", definitions);
|
||||
|
||||
// 先拿到当前 Gateway 中存储的路由定义
|
||||
List<RouteDefinition> routeDefinitionsExits =
|
||||
routeDefinitionLocator.getRouteDefinitions().buffer().blockFirst();
|
||||
if (!CollectionUtils.isEmpty(routeDefinitionsExits)) {
|
||||
// 清除掉之前所有的 "旧的" 路由定义
|
||||
routeDefinitionsExits.forEach(rd -> {
|
||||
log.info("delete route definition: [{}]", rd);
|
||||
deleteById(rd.getId());
|
||||
});
|
||||
}
|
||||
|
||||
// 把更新的路由定义同步到 gateway 中
|
||||
definitions.forEach(definition -> updateByRouteDefinition(definition));
|
||||
return "success";
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>根据路由 id 删除路由配置</h2>
|
||||
* */
|
||||
private String deleteById(String id) {
|
||||
|
||||
try {
|
||||
log.info("gateway delete route id: [{}]", id);
|
||||
this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
|
||||
// 发布事件通知给 gateway 更新路由定义
|
||||
this.publisher.publishEvent(new RefreshRoutesEvent(this));
|
||||
return "delete success";
|
||||
} catch (Exception ex) {
|
||||
log.error("gateway delete route fail: [{}]", ex.getMessage(), ex);
|
||||
return "delete fail";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>更新路由</h2>
|
||||
* 更新的实现策略比较简单: 删除 + 新增 = 更新
|
||||
* */
|
||||
private String updateByRouteDefinition(RouteDefinition definition) {
|
||||
|
||||
try {
|
||||
log.info("gateway update route: [{}]", definition);
|
||||
this.routeDefinitionWriter.delete(Mono.just(definition.getId()));
|
||||
} catch (Exception ex) {
|
||||
return "update fail, not find route routeId: " + definition.getId();
|
||||
}
|
||||
|
||||
try {
|
||||
this.routeDefinitionWriter.save(Mono.just(definition)).subscribe();
|
||||
this.publisher.publishEvent(new RefreshRoutesEvent(this));
|
||||
return "success";
|
||||
} catch (Exception ex) {
|
||||
return "update route fail";
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,127 @@
|
||||
package org.example.conf;
|
||||
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.nacos.api.NacosFactory;
|
||||
import com.alibaba.nacos.api.config.ConfigService;
|
||||
import com.alibaba.nacos.api.exception.NacosException;
|
||||
import com.alibaba.nacos.api.config.listener.Listener;
|
||||
import com.alibaba.nacos.common.utils.CollectionUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cloud.gateway.route.RouteDefinition;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* <h1>通过 nacos 下发动态路由配置, 监听 Nacos 中路由配置变更</h1>
|
||||
* */
|
||||
@Slf4j
|
||||
@Component
|
||||
@DependsOn({"gatewayConfig"}) // 依赖于 GatewayConfig 这个 Bean , 注意要首字母小写
|
||||
public class DynamicRouteServiceImplByNacos {
|
||||
/** Nacos 配置服务 */
|
||||
private ConfigService configService;
|
||||
private final DynamicRouteServiceImpl dynamicRouteService;
|
||||
|
||||
public DynamicRouteServiceImplByNacos(DynamicRouteServiceImpl dynamicRouteService) {
|
||||
this.dynamicRouteService = dynamicRouteService;
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>Bean 在容器中构造完成之后会执行 init 方法</h2>
|
||||
* */
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
|
||||
log.info("gateway route init....");
|
||||
|
||||
try {
|
||||
// 初始化 Nacos 配置客户端
|
||||
configService = initConfigService();
|
||||
if (null == configService) {
|
||||
log.error("init config service fail");
|
||||
return;
|
||||
}
|
||||
|
||||
// 通过 Nacos Config 并指定路由配置路径去获取路由配置
|
||||
String configInfo = configService.getConfig(
|
||||
GatewayConfig.NACOS_ROUTE_DATA_ID,
|
||||
GatewayConfig.NACOS_ROUTE_GROUP,
|
||||
GatewayConfig.DEFAULT_TIMEOUT
|
||||
);
|
||||
|
||||
log.info("get current gateway config: [{}]", configInfo);
|
||||
List<RouteDefinition> definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
|
||||
|
||||
if (CollectionUtils.isNotEmpty(definitionList)) {
|
||||
for (RouteDefinition definition : definitionList) {
|
||||
log.info("init gateway config: [{}]", definition.toString());
|
||||
dynamicRouteService.addRouteDefinition(definition);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception ex) {
|
||||
log.error("gateway route init has some error: [{}]", ex.getMessage(), ex);
|
||||
}
|
||||
|
||||
// 设置监听器
|
||||
dynamicRouteByNacosListener(GatewayConfig.NACOS_ROUTE_DATA_ID, GatewayConfig.NACOS_ROUTE_GROUP);
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>初始化 Nacos Config</h2>
|
||||
* */
|
||||
private ConfigService initConfigService() {
|
||||
|
||||
try {
|
||||
Properties properties = new Properties();
|
||||
properties.setProperty("serverAddr", GatewayConfig.NACOS_SERVER_ADDR);
|
||||
properties.setProperty("namespace", GatewayConfig.NACOS_NAMESPACE);
|
||||
return configService = NacosFactory.createConfigService(properties);
|
||||
} catch (Exception ex) {
|
||||
log.error("init gateway nacos config error: [{}]", ex.getMessage(), ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>监听 Nacos 下发的动态路由配置</h2>
|
||||
* */
|
||||
private void dynamicRouteByNacosListener(String dataId, String group) {
|
||||
|
||||
try {
|
||||
// 给 Nacos Config 客户端增加一个监听器
|
||||
configService.addListener(dataId, group, new Listener() {
|
||||
|
||||
/**
|
||||
* <h2>自己提供线程池执行操作</h2>
|
||||
* */
|
||||
@Override
|
||||
public Executor getExecutor() {
|
||||
// 通常不需要自己提供用默认提供的即可
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>监听器收到配置更新</h2>
|
||||
* @param configInfo Nacos 中最新的配置定义
|
||||
* */
|
||||
@Override
|
||||
public void receiveConfigInfo(String configInfo) {
|
||||
|
||||
log.info("start to update config: [{}]", configInfo);
|
||||
List<RouteDefinition> definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
|
||||
log.info("update route: [{}]", definitionList.toString());
|
||||
dynamicRouteService.updateList(definitionList);
|
||||
}
|
||||
});
|
||||
} catch (NacosException ex) {
|
||||
log.error("dynamic update gateway config error: [{}]", ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
}
|
@ -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,46 @@
|
||||
package org.example.conf;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* <h1>配置类, 读取 Nacos 相关的配置项, 用于配置监听器</h1>
|
||||
* */
|
||||
@Configuration
|
||||
public class GatewayConfig {
|
||||
|
||||
/** 读取配置的超时时间 */
|
||||
public static final long DEFAULT_TIMEOUT = 30000;
|
||||
|
||||
/** Nacos 服务器地址 */
|
||||
public static String NACOS_SERVER_ADDR;
|
||||
|
||||
/** 命名空间 */
|
||||
public static String NACOS_NAMESPACE;
|
||||
|
||||
/** data-id */
|
||||
public static String NACOS_ROUTE_DATA_ID;
|
||||
|
||||
/** 分组 id */
|
||||
public static String NACOS_ROUTE_GROUP;
|
||||
|
||||
@Value("${spring.cloud.nacos.discovery.server-addr}")
|
||||
public void setNacosServerAddr(String nacosServerAddr) {
|
||||
NACOS_SERVER_ADDR = nacosServerAddr;
|
||||
}
|
||||
|
||||
@Value("${spring.cloud.nacos.discovery.namespace}")
|
||||
public void setNacosNamespace(String nacosNamespace) {
|
||||
NACOS_NAMESPACE = nacosNamespace;
|
||||
}
|
||||
|
||||
@Value("${nacos.gateway.route.config.data-id}")
|
||||
public void setNacosRouteDataId(String nacosRouteDataId) {
|
||||
NACOS_ROUTE_DATA_ID = nacosRouteDataId;
|
||||
}
|
||||
|
||||
@Value("${nacos.gateway.route.config.group}")
|
||||
public void setNacosRouteGroup(String nacosRouteGroup) {
|
||||
NACOS_ROUTE_GROUP = nacosRouteGroup;
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package org.example.conf;
|
||||
|
||||
|
||||
import org.springframework.cloud.gateway.route.RouteLocator;
|
||||
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* <h1>配置登录请求转发规则</h1>
|
||||
* */
|
||||
@Configuration
|
||||
public class RouteLocatorConfig {
|
||||
|
||||
/**
|
||||
* <h2>使用代码定义路由规则, 在网关层面拦截下登录和注册接口</h2>
|
||||
* */
|
||||
@Bean
|
||||
public RouteLocator loginRouteLocator(RouteLocatorBuilder builder) {
|
||||
|
||||
// 手动定义 Gateway 路由规则需要指定 id、path 和 uri
|
||||
return builder.routes()
|
||||
.route(
|
||||
"e_commerce_authority",
|
||||
r -> r.path(
|
||||
"/dev-protocol-springcloud-gateway/e-commerce/login",
|
||||
"/dev-protocol-springcloud-gateway/e-commerce/register"
|
||||
).uri("http://localhost:9001/")
|
||||
).build();
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
server:
|
||||
port: 9001
|
||||
servlet:
|
||||
context-path: /dev-protocol-springcloud-gateway
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: dev-protocol-springcloud-gateway
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可
|
||||
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
|
||||
namespace: 1ccc74ae-9398-4dbe-b9d7-4f9addf9f40c
|
||||
metadata:
|
||||
management:
|
||||
context-path: ${server.servlet.context-path}/actuator
|
||||
# 静态路由
|
||||
# gateway:
|
||||
# routes:
|
||||
# - id: path_route # 路由的ID
|
||||
# uri: 127.0.0.1:8080/user/{id} # 匹配后路由地址
|
||||
# predicates: # 断言, 路径相匹配的进行路由
|
||||
# - Path=/user/{id}
|
||||
# kafka:
|
||||
# bootstrap-servers: 127.0.0.1:9092
|
||||
# producer:
|
||||
# retries: 3
|
||||
# consumer:
|
||||
# auto-offset-reset: latest
|
||||
# zipkin:
|
||||
# sender:
|
||||
# type: kafka # 默认是 web
|
||||
# base-url: http://localhost:9411/
|
||||
main:
|
||||
allow-bean-definition-overriding: true # 因为将来会引入很多依赖, 难免有重名的 bean
|
||||
|
||||
# 这个地方独立配置, 是网关的数据, 代码 GatewayConfig.java 中读取被监听
|
||||
nacos:
|
||||
gateway:
|
||||
route:
|
||||
config:
|
||||
data-id: dev-protocol-springcloud-gateway-router
|
||||
group: dev-protocol
|
||||
|
||||
# 暴露端点
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: '*'
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
@ -0,0 +1,17 @@
|
||||
### 登录 -> 向网关发
|
||||
POST 127.0.0.1:9001/dev-protocol-springcloud-gateway/e-commerce/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "q@by.com",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad"
|
||||
}
|
||||
|
||||
### 注册 -> 向网关发
|
||||
POST 127.0.0.1:9001/dev-protocol-springcloud-gateway/e-commerce/register
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "q@by.com",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad"
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
### 查询服务 - (测试gateway)
|
||||
GET http://127.0.0.1:9001/dev-protocol-springcloud-gateway/ecommerce-nacos-client/nacos-client/service-instance?serviceId=e-commerce-gateway
|
||||
Accept: application/json
|
||||
e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjE3LFwidXNlcm5hbWVcIjpcIkltb29jUWlueWlAaW1vb2MuY29tXCJ9IiwianRpIjoiMGIxNzQyYWItMWU3OC00OTZjLWIyNTAtMjNkZGQ1ZGEyZTU1IiwiZXhwIjoxNjI0MjA0ODAwfQ.QKGHzohSHdYDHzUVHpe9gNPUgzfkPwrSbB-WiMWYjLlt2tr9BufzZM8bSt-whb_bd0hKoC6rkYYO0WUVR67uSML-2yaTL1xMIn8GH9Flyig3rpO4vefL3Hp2TXIpwHHa7WlKsLzcUpNk9lxWs2B5k0ICdYCH_jD5Tx6N7CzfSUG9u4fOnVeM9UFE2nX_DURupUh_DKCc2oOoMeyCSR7Ma8-Ab4WQU3r-U0YivR8G1A0kmKOIoTeRhM3LcPuxUPh3rCbrjzMg--fexRGw0O38Qsby6pz-ku2IlTyFXY6_jNOG1BZR34-jBOnaIciP1TExw9bFumeuC2GcowTHJVH1Nw
|
||||
token: imooc
|
||||
# 这里的校验信息在 HeaderTokenGatewayFilter 中配置写死的
|
@ -0,0 +1,18 @@
|
||||
package org.example;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
/**
|
||||
* <h1>验证工程搭建的正确性测试用例</h1>
|
||||
* */
|
||||
@SpringBootTest
|
||||
@RunWith(SpringRunner.class)
|
||||
public class GatewayApplicationTest {
|
||||
|
||||
@Test
|
||||
public void contextLoad() {
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
package org.example.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* <h1>Java8 Predicate 使用方法与思想</h1>
|
||||
* */
|
||||
@Slf4j
|
||||
@SpringBootTest
|
||||
@RunWith(SpringRunner.class)
|
||||
public class PredicateTest {
|
||||
|
||||
public static List<String> MICRO_SERVICE = Arrays.asList(
|
||||
"nacos", "authority", "gateway", "ribbon", "feign", "hystrix", "e-commerce"
|
||||
);
|
||||
|
||||
/**
|
||||
* <h2>test 方法主要用于参数符不符合规则, 返回值是 boolean</h2>
|
||||
* */
|
||||
@Test
|
||||
public void testPredicateTest() {
|
||||
|
||||
Predicate<String> letterLengthLimit = s -> s.length() > 5;
|
||||
MICRO_SERVICE.stream().filter(letterLengthLimit).forEach(System.out::println);
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>and 方法等同于我们的逻辑与 &&, 存在短路特性, 需要所有的条件都满足</h2>
|
||||
* */
|
||||
@Test
|
||||
public void testPredicateAnd() {
|
||||
|
||||
Predicate<String> letterLengthLimit = s -> s.length() > 5;
|
||||
Predicate<String> letterStartWith = s -> s.startsWith("gate");
|
||||
|
||||
MICRO_SERVICE.stream().filter(
|
||||
letterLengthLimit.and(letterStartWith)
|
||||
).forEach(System.out::println);
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>or 等同于我们的逻辑或 ||, 多个条件主要一个满足即可</h2>
|
||||
* */
|
||||
@Test
|
||||
public void testPredicateOr() {
|
||||
|
||||
Predicate<String> letterLengthLimit = s -> s.length() > 5;
|
||||
Predicate<String> letterStartWith = s -> s.startsWith("gate");
|
||||
|
||||
MICRO_SERVICE.stream().filter(
|
||||
letterLengthLimit.or(letterStartWith)
|
||||
).forEach(System.out::println);
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>negate 等同于我们的逻辑非 !</h2>
|
||||
* */
|
||||
@Test
|
||||
public void testPredicateNegate() {
|
||||
|
||||
Predicate<String> letterStartWith = s -> s.startsWith("gate");
|
||||
MICRO_SERVICE.stream().filter(letterStartWith.negate()).forEach(System.out::println);
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>isEqual 类似于 equals(), 区别在于: 先判断对象是否为 NULL,
|
||||
* 不为 NULL 再使用 equals 进行比较</h2>
|
||||
* */
|
||||
@Test
|
||||
public void testPredicateIsEqual() {
|
||||
|
||||
Predicate<String> equalGateway = s -> Predicate.isEqual("gateway").test(s);
|
||||
MICRO_SERVICE.stream().filter(equalGateway).forEach(System.out::println);
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package org.example.controller;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.example.service.NacosClientService;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* <h1>nacos client controller</h1>
|
||||
* */
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/nacos-client")
|
||||
public class NacosClientController {
|
||||
|
||||
private final NacosClientService nacosClientService;
|
||||
// private final ProjectConfig projectConfig;
|
||||
|
||||
|
||||
public NacosClientController(NacosClientService nacosClientService
|
||||
// ProjectConfig projectConfig
|
||||
) {
|
||||
this.nacosClientService = nacosClientService;
|
||||
// this.projectConfig = projectConfig;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* <h2>根据 service id 获取服务所有的实例信息</h2>
|
||||
* */
|
||||
@GetMapping("/service-instance")
|
||||
public List<ServiceInstance> logNacosClientInfo(
|
||||
@RequestParam(defaultValue = "dev-protocol-spring-cloud-nacos") String serviceId) {
|
||||
|
||||
log.info("coming in log nacos client info: [{}]", serviceId);
|
||||
return nacosClientService.getNacosClientInfo(serviceId);
|
||||
}
|
||||
|
||||
/* *//**
|
||||
* <h2>动态获取 Nacos 中的配置信息</h2>
|
||||
* *//*
|
||||
@GetMapping("/project-config")
|
||||
public ProjectConfig getProjectConfig() {
|
||||
return projectConfig;
|
||||
}*/
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
### 查询服务实例信息
|
||||
GET http://127.0.0.1:8000/ecommerce-nacos-client/nacos-client/service-instance
|
||||
GET http://127.0.0.1:8000/dev-protocol-spring-cloud-nacos/nacos-client/service-instance
|
||||
Accept: application/json
|
||||
|
||||
### 动态从 Nacos Server 中获取配置信息
|
||||
GET http://127.0.0.1:8000/ecommerce-nacos-client/nacos-client/project-config
|
||||
GET http://127.0.0.1:8000/dev-protocol-spring-cloud-nacos/nacos-client/project-config
|
||||
Accept: application/json
|
||||
|
||||
### 查看 Sleuth 跟踪信息
|
||||
GET http://127.0.0.1:9001/imooc/ecommerce-nacos-client/sleuth/trace-info
|
||||
GET http://127.0.0.1:9001/imooc/dev-protocol-spring-cloud-nacos/sleuth/trace-info
|
||||
Accept: application/json
|
||||
e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjE3LFwidXNlcm5hbWVcIjpcIkltb29jUWlueWlAaW1vb2MuY29tXCJ9IiwianRpIjoiMGIxNzQyYWItMWU3OC00OTZjLWIyNTAtMjNkZGQ1ZGEyZTU1IiwiZXhwIjoxNjI0MjA0ODAwfQ.QKGHzohSHdYDHzUVHpe9gNPUgzfkPwrSbB-WiMWYjLlt2tr9BufzZM8bSt-whb_bd0hKoC6rkYYO0WUVR67uSML-2yaTL1xMIn8GH9Flyig3rpO4vefL3Hp2TXIpwHHa7WlKsLzcUpNk9lxWs2B5k0ICdYCH_jD5Tx6N7CzfSUG9u4fOnVeM9UFE2nX_DURupUh_DKCc2oOoMeyCSR7Ma8-Ab4WQU3r-U0YivR8G1A0kmKOIoTeRhM3LcPuxUPh3rCbrjzMg--fexRGw0O38Qsby6pz-ku2IlTyFXY6_jNOG1BZR34-jBOnaIciP1TExw9bFumeuC2GcowTHJVH1Nw
|
||||
token: imooc
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 279 KiB |
@ -0,0 +1,19 @@
|
||||
package org.example;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||
|
||||
/**
|
||||
* <h1>授权中心启动入口</h1>
|
||||
* */
|
||||
@EnableJpaAuditing // 允许 Jpa 自动审计
|
||||
@EnableDiscoveryClient
|
||||
@SpringBootApplication
|
||||
public class AuthorityCenterApplication {
|
||||
public static void main(String[] args) {
|
||||
|
||||
SpringApplication.run(AuthorityCenterApplication.class, args);
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package org.example.constant;
|
||||
|
||||
/**
|
||||
* <h1>授权需要使用的一些常量信息</h1>
|
||||
* */
|
||||
public final class AuthorityConstant {
|
||||
|
||||
/** RSA 私钥, 除了授权中心以外, 不暴露给任何客户端 */
|
||||
public static final String PRIVATE_KEY = "MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAKzKEUVNF+VVgyar2SAcm9xGDCL4yjMLEXXsy0BNM6rZTh2metCIqA1vbKvFvuruTLcTHYMtO0+urup2ScRhiZtkHMI9Vy/MGbiG0o5T2YrDUYZD4jPPRgvZ2L95uI+nFkEm1WZ7nuO37HvIE3+WDVvJnI84omFsLwN3bLCnStw5AgMBAAECgYACCZePWdAVL+HQlProXIJO1XXFwPN2MtCnzB0cIkk5Kc6zjxLIZf0M2dCGrGONG8BJVEj4Zn6BkXlEqTv+LXVCZfOLNJuXTOEdBTWQj1EFk2wuXIqrcZgFqT56ChSscMQTiEe/O7ydyQ2qD/ZbNDOcJMf6nto883ZDLtVtOTdzxQJBANwaMS8O0/X0/gkdzrY3dKjOJFmXOIybdURAR6Mum/PGkX6j9xAUO1clErKTEY1jkuqohLmnXw+pKTQTW/Gt290CQQDI+HFel+S64xZ9SGyISK+gXl1gK1mpMT2YaQjmzwotNljn7U3g0nChbltNANYsRcE5X8/kVoX7AihO+8RZp1oNAkEAgS66SVVZoJVfeHhPN/GKff0nppGz9grUI+/aW/NiQwz7nimcO4q0XWx78eWRuruDokiwRcrvZ2Cwt0jZgRq63QJBAIVli3LbZcK7K1lbclb/0Dulh1tnSutoONdqmLMDqGCcW2UO+guKA6LTqpyxOnhGkNwxgb+xwtr68qCCszFDSR0CQQDC52dm0bei9PCi0pebOhtQVYdPx+zfZE4p+aRCV7pYjm8HgMMJslKX8sgLEOg91gO/925QRb0uN5H5oDjOHFVh";
|
||||
|
||||
/** 默认的 Token 超时时间, 一天 */
|
||||
public static final Integer DEFAULT_EXPIRE_DAY = 1;
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package org.example.controller;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.example.annotation.IgnoreResponseAdvice;
|
||||
import org.example.service.IJWTService;
|
||||
import org.example.vo.JwtToken;
|
||||
import org.example.vo.UsernameAndPassword;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* <h1>对外暴露的授权服务接口</h1>
|
||||
* */
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/authority")
|
||||
public class AuthorityController {
|
||||
|
||||
private final IJWTService ijwtService;
|
||||
|
||||
public AuthorityController(IJWTService ijwtService) {
|
||||
this.ijwtService = ijwtService;
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>从授权中心获取 Token (其实就是登录功能), 且返回信息中没有统一响应的包装</h2>
|
||||
* */
|
||||
@IgnoreResponseAdvice // 不被统一响应解析
|
||||
@PostMapping("/token")
|
||||
public JwtToken token(@RequestBody UsernameAndPassword usernameAndPassword)
|
||||
throws Exception {
|
||||
|
||||
log.info("request to get token with param: [{}]",
|
||||
JSON.toJSONString(usernameAndPassword));
|
||||
return new JwtToken(ijwtService.generateToken(
|
||||
usernameAndPassword.getUsername(),
|
||||
usernameAndPassword.getPassword()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>注册用户并返回当前注册用户的 Token, 即通过授权中心创建用户</h2>
|
||||
* */
|
||||
@IgnoreResponseAdvice // 不被统一响应解析
|
||||
@PostMapping("/register")
|
||||
public JwtToken register(@RequestBody UsernameAndPassword usernameAndPassword)
|
||||
throws Exception {
|
||||
|
||||
log.info("register user with param: [{}]", JSON.toJSONString(
|
||||
usernameAndPassword
|
||||
));
|
||||
return new JwtToken(ijwtService.registerUserAndGenerateToken(
|
||||
usernameAndPassword
|
||||
));
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package org.example.dao;
|
||||
|
||||
import org.example.entity.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
/**
|
||||
* <h1>User Dao 接口定义</h1>
|
||||
* */
|
||||
public interface UserDao extends JpaRepository<User, Long> {
|
||||
|
||||
/**
|
||||
* <h2>根据用户名查询 User 对象</h2>
|
||||
* select * from t_dev_protocol_cloud_user where username = ?
|
||||
* */
|
||||
User findByUsername(String username);
|
||||
|
||||
/**
|
||||
* <h2>根据用户名和密码查询实体对象</h2>
|
||||
* select * from t_dev_protocol_cloud_user where username = ? and password = ?
|
||||
* */
|
||||
User findByUsernameAndPassword(String username, String password);
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package org.example.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.EntityListeners;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Table;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* <h1>用户表实体类定义</h1>
|
||||
* */
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Entity
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
@Table(name = "t_dev_protocol_cloud_user")
|
||||
public class User implements Serializable {
|
||||
|
||||
/** 自增主键 */
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id", nullable = false)
|
||||
private Long id;
|
||||
|
||||
/** 用户名 */
|
||||
@Column(name = "username", nullable = false)
|
||||
private String username;
|
||||
|
||||
/** MD5 密码 */
|
||||
@Column(name = "password", nullable = false)
|
||||
private String password;
|
||||
|
||||
/** 额外的信息, json 字符串存储 */
|
||||
@Column(name = "extra_info", nullable = false)
|
||||
private String extraInfo;
|
||||
|
||||
/** 创建时间 */
|
||||
@CreatedDate
|
||||
@Column(name = "create_time", nullable = false)
|
||||
private Date createTime;
|
||||
|
||||
/** 更新时间 */
|
||||
@LastModifiedDate
|
||||
@Column(name = "update_time", nullable = false)
|
||||
private Date updateTime;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package org.example.service;
|
||||
|
||||
|
||||
import org.example.vo.UsernameAndPassword;
|
||||
|
||||
/**
|
||||
* <h1>JWT 相关服务接口定义</h1>
|
||||
* */
|
||||
public interface IJWTService {
|
||||
|
||||
/**
|
||||
* <h2>生成 JWT Token, 使用默认的超时时间</h2>
|
||||
* */
|
||||
String generateToken(String username, String password) throws Exception;
|
||||
|
||||
/**
|
||||
* <h2>生成指定超时时间的 Token, 单位是天</h2>
|
||||
* */
|
||||
String generateToken(String username, String password, int expire) throws Exception;
|
||||
|
||||
/**
|
||||
* <h2>注册用户并生成 Token 返回</h2>
|
||||
* */
|
||||
String registerUserAndGenerateToken(UsernameAndPassword usernameAndPassword)
|
||||
throws Exception;
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
package org.example.service.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.example.constant.AuthorityConstant;
|
||||
import org.example.constant.CommonConstant;
|
||||
import org.example.dao.UserDao;
|
||||
import org.example.entity.User;
|
||||
import org.example.service.IJWTService;
|
||||
import org.example.vo.LoginUserInfo;
|
||||
import org.example.vo.UsernameAndPassword;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import sun.misc.BASE64Decoder;
|
||||
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
|
||||
/**
|
||||
* <h1>JWT 相关服务接口实现</h1>
|
||||
* */
|
||||
@Slf4j
|
||||
@Service
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public class JWTServiceImpl implements IJWTService {
|
||||
|
||||
private final UserDao ecommerceUserDao;
|
||||
|
||||
public JWTServiceImpl(UserDao ecommerceUserDao) {
|
||||
this.ecommerceUserDao = ecommerceUserDao;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateToken(String username, String password) throws Exception {
|
||||
|
||||
return generateToken(username, password, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateToken(String username, String password, int expire)
|
||||
throws Exception {
|
||||
|
||||
// 首先需要验证用户是否能够通过授权校验, 即输入的用户名和密码能否匹配数据表记录
|
||||
User ecommerceUser = ecommerceUserDao.findByUsernameAndPassword(
|
||||
username, password
|
||||
);
|
||||
if (null == ecommerceUser) {
|
||||
log.error("can not find user: [{}], [{}]", username, password);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Token 中塞入对象, 即 JWT 中存储的信息, 后端拿到这些信息就可以知道是哪个用户在操作
|
||||
LoginUserInfo loginUserInfo = new LoginUserInfo(
|
||||
ecommerceUser.getId(), ecommerceUser.getUsername()
|
||||
);
|
||||
|
||||
if (expire <= 0) {
|
||||
expire = AuthorityConstant.DEFAULT_EXPIRE_DAY;
|
||||
}
|
||||
|
||||
// 计算超时时间
|
||||
ZonedDateTime zdt = LocalDate.now().plus(expire, ChronoUnit.DAYS)
|
||||
.atStartOfDay(ZoneId.systemDefault());
|
||||
Date expireDate = Date.from(zdt.toInstant());
|
||||
|
||||
return Jwts.builder()
|
||||
// jwt payload --> KV (用户数据)
|
||||
.claim(CommonConstant.JWT_USER_INFO_KEY, JSON.toJSONString(loginUserInfo))
|
||||
// jwt id (标识, 没什么作用)
|
||||
.setId(UUID.randomUUID().toString())
|
||||
// jwt 过期时间
|
||||
.setExpiration(expireDate)
|
||||
// jwt 签名 --> 加密
|
||||
.signWith(SignatureAlgorithm.RS256, getPrivateKey())
|
||||
.compact();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String registerUserAndGenerateToken(UsernameAndPassword usernameAndPassword)
|
||||
throws Exception {
|
||||
|
||||
// 先去校验用户名是否存在, 如果存在, 不能重复注册
|
||||
User oldUser = ecommerceUserDao.findByUsername(
|
||||
usernameAndPassword.getUsername());
|
||||
if (null != oldUser) {
|
||||
log.error("username is registered: [{}]", oldUser.getUsername());
|
||||
return null;
|
||||
}
|
||||
|
||||
User ecommerceUser = new User();
|
||||
ecommerceUser.setUsername(usernameAndPassword.getUsername());
|
||||
ecommerceUser.setPassword(usernameAndPassword.getPassword()); // MD5 编码以后
|
||||
ecommerceUser.setExtraInfo("{}");
|
||||
|
||||
// 注册一个新用户, 写一条记录到数据表中
|
||||
ecommerceUser = ecommerceUserDao.save(ecommerceUser);
|
||||
log.info("register user success: [{}], [{}]", ecommerceUser.getUsername(),
|
||||
ecommerceUser.getId());
|
||||
|
||||
// 生成 token 并返回
|
||||
return generateToken(ecommerceUser.getUsername(), ecommerceUser.getPassword());
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>根据本地存储的私钥获取到 PrivateKey 对象</h2>
|
||||
* */
|
||||
private PrivateKey getPrivateKey() throws Exception {
|
||||
|
||||
PKCS8EncodedKeySpec priPKCS8 = new PKCS8EncodedKeySpec(
|
||||
new BASE64Decoder().decodeBuffer(AuthorityConstant.PRIVATE_KEY));
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
return keyFactory.generatePrivate(priPKCS8);
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
server:
|
||||
port: 7000
|
||||
servlet:
|
||||
context-path: /dev-protocol-springcloud-project-authority-center
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: dev-protocol-springcloud-project-authority-center
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可
|
||||
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
|
||||
# server-addr: 127.0.0.1:8848,127.0.0.1:8849,127.0.0.1:8850 # Nacos 服务器地址
|
||||
namespace: 1ccc74ae-9398-4dbe-b9d7-4f9addf9f40c
|
||||
metadata:
|
||||
management:
|
||||
context-path: ${server.servlet.context-path}/actuator
|
||||
jpa:
|
||||
show-sql: true
|
||||
hibernate:
|
||||
ddl-auto: none
|
||||
properties:
|
||||
hibernate.show_sql: true
|
||||
hibernate.format_sql: true
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.MySQLDialect
|
||||
open-in-view: false
|
||||
datasource:
|
||||
# 数据源
|
||||
url: jdbc:mysql://127.0.0.1:3306/dev_protocol_springcloud_project?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
|
||||
username: root
|
||||
password: root
|
||||
type: com.zaxxer.hikari.HikariDataSource
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
# 连接池
|
||||
hikari:
|
||||
maximum-pool-size: 8
|
||||
minimum-idle: 4
|
||||
idle-timeout: 30000
|
||||
connection-timeout: 30000
|
||||
max-lifetime: 45000
|
||||
auto-commit: true
|
||||
pool-name: devProtocolSpringcloudHikariCP
|
||||
# kafka:
|
||||
# bootstrap-servers: 127.0.0.1:9092
|
||||
# producer:
|
||||
# retries: 3
|
||||
# consumer:
|
||||
# auto-offset-reset: latest
|
||||
# zipkin:
|
||||
# sender:
|
||||
# type: kafka # 默认是 web
|
||||
# base-url: http://127.0.0.1:9411/
|
||||
|
||||
# 暴露端点
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: '*'
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
@ -0,0 +1,18 @@
|
||||
### 获取 Token -- 登录功能实现
|
||||
POST http://127.0.0.1:7000/dev-protocol-springcloud-project-authority-center/authority/token
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "q@bbbbbbyyyyyy.com",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad"
|
||||
}
|
||||
|
||||
|
||||
### 注册用户并返回 Token -- 注册功能实现
|
||||
POST http://127.0.0.1:7000/dev-protocol-springcloud-project-authority-center/authority/register
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "qccc@11.com",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad"
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
-- 创建 t_ecommerce_user 数据表
|
||||
CREATE TABLE IF NOT EXISTS `dev_protocol_springcloud_project`.`t_dev_protocol_cloud_user` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
|
||||
`username` varchar(64) NOT NULL DEFAULT '' COMMENT '用户名',
|
||||
`password` varchar(256) NOT NULL DEFAULT '' COMMENT 'MD5 加密之后的密码',
|
||||
`extra_info` varchar(1024) NOT NULL DEFAULT '' COMMENT '额外的信息',
|
||||
`create_time` datetime NOT NULL DEFAULT '0000-01-01 00:00:00' COMMENT '创建时间',
|
||||
`update_time` datetime NOT NULL DEFAULT '0000-01-01 00:00:00' COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `username` (`username`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
|
@ -0,0 +1,21 @@
|
||||
package org.example;
|
||||
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
/**
|
||||
* <h1>授权中心测试入口</h1>
|
||||
* 验证授权中心环境可用性
|
||||
* */
|
||||
@SpringBootTest
|
||||
@RunWith(SpringRunner.class)
|
||||
public class AuthorityCenterApplicationTests {
|
||||
|
||||
@Test
|
||||
public void contextLoad() {
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
package org.example;
|
||||
|
||||
import cn.smallbun.screw.core.Configuration;
|
||||
import cn.smallbun.screw.core.engine.EngineConfig;
|
||||
import cn.smallbun.screw.core.engine.EngineFileType;
|
||||
import cn.smallbun.screw.core.engine.EngineTemplateType;
|
||||
import cn.smallbun.screw.core.execute.DocumentationExecute;
|
||||
import cn.smallbun.screw.core.process.ProcessConfig;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
/**
|
||||
* <h1>数据库表文档生成</h1>
|
||||
* */
|
||||
@SpringBootTest
|
||||
@RunWith(SpringRunner.class)
|
||||
public class DBDocTest {
|
||||
|
||||
@Autowired
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Test
|
||||
public void buildDBDoc() {
|
||||
|
||||
DataSource dataSourceMysql = applicationContext.getBean(DataSource.class);
|
||||
|
||||
// 生成文件配置
|
||||
EngineConfig engineConfig = EngineConfig.builder()
|
||||
// 生成文件路径
|
||||
.fileOutputDir("C:\\Users\\Administrator\\Desktop\\study_pro\\dev-protocol\\dev-protocol-springcloud\\dev-protocol-springcloud-project-authority-center\\doc")
|
||||
// 打开目录
|
||||
.openOutputDir(false)
|
||||
// 文件类型
|
||||
.fileType(EngineFileType.HTML)
|
||||
.produceType(EngineTemplateType.freemarker).build();
|
||||
|
||||
// 生成文档配置, 包含自定义版本号、描述等等
|
||||
// 数据库名_description_version.html
|
||||
Configuration config = Configuration.builder()
|
||||
.version("1.0.0")
|
||||
.description("dev-protocol-springcloud")
|
||||
.dataSource(dataSourceMysql)
|
||||
.engineConfig(engineConfig)
|
||||
.produceConfig(getProduceConfig())
|
||||
.build();
|
||||
|
||||
// 执行生成
|
||||
new DocumentationExecute(config).execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>配置想要生成的数据表和想要忽略的数据表</h2>
|
||||
* */
|
||||
private ProcessConfig getProduceConfig() {
|
||||
|
||||
// 想要忽略的数据表
|
||||
List<String> ignoreTableName = Collections.singletonList("undo_log");
|
||||
// 忽略表前缀, 忽略 a、b 开头的数据库表
|
||||
List<String> ignorePrefix = Arrays.asList("a", "b");
|
||||
// 忽略表后缀
|
||||
List<String> ignoreSuffix = Arrays.asList("_test", "_Test");
|
||||
|
||||
return ProcessConfig.builder()
|
||||
// 根据名称指定表生成
|
||||
.designatedTableName(Collections.emptyList())
|
||||
// 根据表前缀生成
|
||||
.designatedTablePrefix(Collections.emptyList())
|
||||
// 根据表后缀生成
|
||||
.designatedTableSuffix(Collections.emptyList())
|
||||
// 忽略表
|
||||
.ignoreTableName(ignoreTableName)
|
||||
// 按照前缀忽略
|
||||
.ignoreTablePrefix(ignorePrefix)
|
||||
// 按照后缀忽略
|
||||
.ignoreTableSuffix(ignoreSuffix)
|
||||
.build();
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package org.example.dao;
|
||||
|
||||
import cn.hutool.crypto.digest.MD5;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.example.entity.User;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
|
||||
/**
|
||||
* <h1>User 相关的测试</h1>
|
||||
* */
|
||||
@Slf4j
|
||||
@SpringBootTest
|
||||
@RunWith(SpringRunner.class)
|
||||
public class UserDaoTest {
|
||||
|
||||
@Autowired
|
||||
private UserDao ecommerceUserDao;
|
||||
|
||||
@Test
|
||||
public void createUserRecord() {
|
||||
|
||||
User ecommerceUser = new User();
|
||||
ecommerceUser.setUsername("q@bbbbbbyyyyyy.com");
|
||||
ecommerceUser.setPassword(MD5.create().digestHex("12345678"));
|
||||
ecommerceUser.setExtraInfo("{}");
|
||||
log.info("save user: [{}]",
|
||||
JSON.toJSON(ecommerceUserDao.save(ecommerceUser)));
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package org.example.service;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.example.utils.TokenParseUtils;
|
||||
import org.example.vo.LoginUserInfo;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
/**
|
||||
* <h1>JWT 相关服务测试类</h1>
|
||||
* */
|
||||
@Slf4j
|
||||
@SpringBootTest
|
||||
@RunWith(SpringRunner.class)
|
||||
public class JWTServiceTest {
|
||||
|
||||
@Autowired
|
||||
private IJWTService ijwtService;
|
||||
|
||||
@Test
|
||||
public void testGenerateAndParseToken() throws Exception {
|
||||
|
||||
// 生成 token 返回给客户端
|
||||
String jwtToken = ijwtService.generateToken(
|
||||
"q@bbbbbbyyyyyy.com",
|
||||
"25d55ad283aa400af464c76d713c07ad"
|
||||
);
|
||||
log.info("jwt token is: [{}]", jwtToken);
|
||||
|
||||
// 客户端带 token 返回给我们的系统, 系统通过鉴权工具类解析, 得到用户信息
|
||||
LoginUserInfo userInfo = TokenParseUtils.parseUserInfoFromToken(jwtToken);
|
||||
log.info("parse token: [{}]", JSON.toJSONString(userInfo));
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
## 授权、鉴权中心微服务功能设计
|
||||
- 什么是 JWT
|
||||
- JSON Web Token (WT)是一个开放标准,它定义了一种紧凑的、自包含的方式,用于作为 JSON对象在各方之间安全地传输信息
|
||||
- 哪些场景下可以考虑使用 JWT?
|
||||
- 用户授权 信息交换
|
||||
---
|
||||
- JWT 的结构(组成部分)
|
||||
- JWT由三个部分组成:Header、Payload、Signature,且用圆点连接 xxxxx.yyyyy.ZZZZZ
|
||||
- Header:由两部分(Token 类型、加密算法名称)组成,并使用 Base64 编码 {'alg':"HS256",'type':"JWT"}
|
||||
- Payload:KV 形式的数据,即你想传递的数据(授权的话就是 Token 信息 )
|
||||
- Signature:为了得到签名部分,你必须有编码过的 Header、编码过的 payload、一个秘钥, 签名算法是 Header 中指定的那个,然对它们签名即可
|
||||
- HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
|
||||
---
|
||||
- 授权、鉴权中心微服务功能逻辑架构
|
||||
- ![授权鉴权中心微服务功能逻辑架构.png](pic/授权鉴权中心微服务功能逻辑架构.png)
|
||||
- 鉴权功能不走HTTP可以更快来进行完成开发, 因为微服务很多模块都要依赖鉴权服务
|
||||
---
|
||||
## 搭建授权、鉴权中心微服务
|
||||
- [FIX] Access to DialectResolutionInfo cannot be null when 'hibernate.dialect' not set
|
||||
- 加入配置:
|
||||
- spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
|
||||
- [FIX] Caused by: com.mysql.cj.exceptions.InvalidConnectionAttributeException: The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or represents more than one time
|
||||
- 在配置文件中 spring.datasource.url 加 serverTimezone=GMT%2B8
|
||||
## 数据表及 ORM 过程
|
||||
-
|
||||
## 生成 RSA256 公钥和私钥对
|
||||
- [RSATest.java]
|
||||
- 一般是放在服务器上, 用的时候拉取下来使用
|
||||
## 基于 JWT + RSA256 的授权
|
||||
- 查看代码实现即可
|
||||
## 验证服务可用性
|
||||
- Test 验证 Service 功能可用性
|
||||
- http 脚本验证 对外接口是否可用
|
||||
## 授权、鉴权中心微服务总结
|
||||
|
||||
- 生成数据库 文档
|
||||
- [DBDocTest.java]
|
@ -0,0 +1,55 @@
|
||||
<?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">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.example</groupId>
|
||||
<artifactId>dev-protocol</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>dev-protocol-springcloud-project-common</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
<!-- 模块名及描述信息 -->
|
||||
<name>dev-protocol-springcloud-project-common</name>
|
||||
<description>通用模块</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<jjwt.version>0.9.1</jjwt.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt</artifactId>
|
||||
<version>${jjwt.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.12.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.7.22</version>
|
||||
</dependency>
|
||||
<!-- 引入fastjson2-->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
<version>2.0.18</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@ -0,0 +1,19 @@
|
||||
package org.example.constant;
|
||||
|
||||
/**
|
||||
* @author q
|
||||
* @createTime 2024-08-19 18:18:06
|
||||
* @Description 通用模块常量定义
|
||||
*/
|
||||
public final class CommonConstant {
|
||||
|
||||
/** RSA 公钥 */
|
||||
public static final String PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCsyhFFTRflVYMmq9kgHJvcRgwi+MozCxF17MtATTOq2U4dpnrQiKgNb2yrxb7q7ky3Ex2DLTtPrq7qdknEYYmbZBzCPVcvzBm4htKOU9mKw1GGQ+Izz0YL2di/ebiPpxZBJtVme57jt+x7yBN/lg1byZyPOKJhbC8Dd2ywp0rcOQIDAQAB";
|
||||
|
||||
/** JWT 中存储用户信息的 key */
|
||||
public static final String JWT_USER_INFO_KEY = "dev-protocol-user";
|
||||
|
||||
/** 授权中心的 service-id */
|
||||
public static final String AUTHORITY_CENTER_SERVICE_ID = "dev-protocol-springcloud-project-authority-center";
|
||||
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package org.example.utils;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jws;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import org.example.constant.CommonConstant;
|
||||
import org.example.vo.LoginUserInfo;
|
||||
import sun.misc.BASE64Decoder;
|
||||
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Calendar;
|
||||
|
||||
/**
|
||||
* @author q
|
||||
* @createTime 2024-08-19 18:17:42
|
||||
* @Description JWT Token 解析工具类
|
||||
*/
|
||||
public class TokenParseUtils {
|
||||
|
||||
private TokenParseUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* <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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过公钥去解析 JWT Token
|
||||
*/
|
||||
private static Jws<Claims> parseToken(String token, PublicKey publicKey) {
|
||||
return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据本地存储的公钥获取到 PublicKey 对象
|
||||
*/
|
||||
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,39 @@
|
||||
package org.example.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @author q
|
||||
* @createTime 2024-08-19 18:17:02
|
||||
* @Description 通用响应对象定义
|
||||
* {
|
||||
* "code": 0,
|
||||
* "message": "",
|
||||
* "data": {}
|
||||
* }
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@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,22 @@
|
||||
package org.example.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* @author q
|
||||
* @createTime 2024-08-19 18:17:16
|
||||
* @Description 授权中心鉴权之后给客户端的 Token
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class JwtToken {
|
||||
|
||||
/** JWT */
|
||||
private String token;
|
||||
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package org.example.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* @author q
|
||||
* @createTime 2024-08-19 18:17:24
|
||||
* @Description 登录用户信息
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LoginUserInfo {
|
||||
|
||||
/** 用户 id */
|
||||
private Long id;
|
||||
|
||||
/** 用户名 */
|
||||
private String username;
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package org.example.vo;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* @author q
|
||||
* @createTime 2024-08-19 18:17:33
|
||||
* @Description 用户名和密码
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class UsernameAndPassword {
|
||||
|
||||
/** 用户名 */
|
||||
private String username;
|
||||
|
||||
/** 密码 */
|
||||
private String password;
|
||||
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
<?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">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.example</groupId>
|
||||
<artifactId>dev-protocol</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>dev-protocol-springcloud-project-mvc-config</artifactId>
|
||||
|
||||
<!-- 模块名及描述信息 -->
|
||||
<name>dev-protocol-springcloud-project-mvc-config</name>
|
||||
<description>通用配置模块</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- 引入 Web 功能 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 引入项目通用依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.example</groupId>
|
||||
<artifactId>dev-protocol-springcloud-project-common</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@ -0,0 +1,28 @@
|
||||
package org.example.advice;
|
||||
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.example.vo.CommonResponse;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
/**
|
||||
* @author q
|
||||
* @createTime 2024-08-19 18:13:14
|
||||
* @Description 全局异常捕获处理
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionAdvice {
|
||||
|
||||
@ExceptionHandler(value = Exception.class)
|
||||
@ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public CommonResponse<String> handlerCommerceException(Exception ex) {
|
||||
CommonResponse<String> response = new CommonResponse<>(-1, "business error");
|
||||
response.setData(ex.getMessage());
|
||||
log.error("commerce service has error: [{}]", ex.getMessage(), ex);
|
||||
return response;
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package org.example.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* @author q
|
||||
* @createTime 2024-08-19 18:21:14
|
||||
* @Description 忽略统一响应注解定义
|
||||
*/
|
||||
@Target({ElementType.TYPE, ElementType.METHOD}) // 可以放在class或方法上
|
||||
@Retention(RetentionPolicy.RUNTIME) // 定义该注解保留到运行时
|
||||
public @interface IgnoreResponseAdvice {
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
Binary file not shown.
After Width: | Height: | Size: 89 KiB |
Binary file not shown.
After Width: | Height: | Size: 82 KiB |
Binary file not shown.
After Width: | Height: | Size: 73 KiB |
@ -0,0 +1,49 @@
|
||||
<?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">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.example</groupId>
|
||||
<artifactId>dev-protocol</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>dev-protocol-springcloud-sleuth-zipkin</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<!-- sleuth -->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.springframework.cloud</groupId>-->
|
||||
<!-- <artifactId>spring-cloud-starter-sleuth</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
<!-- zipkin = spring-cloud-starter-sleuth + spring-cloud-sleuth-zipkin-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-zipkin</artifactId>
|
||||
</dependency>
|
||||
<!-- kafka -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.kafka</groupId>
|
||||
<artifactId>spring-kafka</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@ -0,0 +1,11 @@
|
||||
package org.example;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class SleuthApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SleuthApplication.class, args);
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package org.example.controller;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.example.service.SleuthTraceInfoService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* <h1>打印跟踪信息</h1>
|
||||
* */
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/sleuth")
|
||||
public class SleuthTraceInfoController {
|
||||
|
||||
private final SleuthTraceInfoService traceInfoService;
|
||||
|
||||
public SleuthTraceInfoController(SleuthTraceInfoService traceInfoService) {
|
||||
this.traceInfoService = traceInfoService;
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>打印日志跟踪信息</h2>
|
||||
* */
|
||||
@GetMapping("/trace-info")
|
||||
public void logCurrentTraceInfo() {
|
||||
traceInfoService.logCurrentTraceInfo();
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package org.example.sampler;
|
||||
|
||||
import brave.sampler.RateLimitingSampler;
|
||||
import brave.sampler.Sampler;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* <h2>使用配置的方式设定抽样率</h2>
|
||||
* */
|
||||
@Configuration
|
||||
public class SamplerConfig {
|
||||
/**
|
||||
* <h2>限速采集</h2>
|
||||
* */
|
||||
@Bean
|
||||
public Sampler sampler() {
|
||||
return RateLimitingSampler.create(100);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * <h2>概率采集, 默认的采样策略, 默认值是 0.1</h2>
|
||||
// * */
|
||||
// @Bean
|
||||
// public Sampler defaultSampler() {
|
||||
// return ProbabilityBasedSampler.create(0.5f);
|
||||
// }
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package org.example.service;
|
||||
|
||||
import brave.Tracer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* <h1>使用代码更直观的看到 Sleuth 生成的相关跟踪信息</h1>
|
||||
* */
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SleuthTraceInfoService {
|
||||
|
||||
/** brave.Tracer 跟踪对象 */
|
||||
private final Tracer tracer;
|
||||
|
||||
public SleuthTraceInfoService(Tracer tracer) {
|
||||
this.tracer = tracer;
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>打印当前的跟踪信息到日志中</h2>
|
||||
* */
|
||||
public void logCurrentTraceInfo() {
|
||||
|
||||
log.info("Sleuth trace id: [{}]", tracer.currentSpan().context().traceId());
|
||||
log.info("Sleuth span id: [{}]", tracer.currentSpan().context().spanId());
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
server:
|
||||
port: 9999
|
||||
servlet:
|
||||
context-path: /dev-protocol-springcloud-sleuth-zipkin
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: dev-protocol-springcloud-sleuth-zipkin # 应用名称也是构成 Nacos 配置管理 dataId 字段的一部分 (当 config.prefix 为空时)
|
||||
cloud:
|
||||
nacos:
|
||||
# 服务注册发现
|
||||
discovery:
|
||||
enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可
|
||||
server-addr: 127.0.0.1:8848
|
||||
# server-addr: 127.0.0.1:8848,127.0.0.1:8849,127.0.0.1:8850 # Nacos 服务器地址, 集群模式
|
||||
# todo 根据你自己的业务来进行区分找到
|
||||
namespace: 1ccc74ae-9398-4dbe-b9d7-4f9addf9f40c
|
||||
# group: e-commerce
|
||||
# metadata:
|
||||
# management:
|
||||
# context-path: ${server.servlet.context-path}/actuator
|
||||
# # 配置管理
|
||||
# config:
|
||||
# prefix: imooc-e-commerce
|
||||
# file-extension: yaml # 配置内容的数据格式, 默认为 properties
|
||||
# enabled: true # 如果不想使用 Nacos 进行配置管理, 设置为 false 即可
|
||||
# group: DEFAULT_GROUP # 组, 默认为 DEFAULT_GROUP
|
||||
# namespace: 1bc13fd5-843b-4ac0-aa55-695c25bc0ac6
|
||||
# server-addr: 127.0.0.1:8848
|
||||
kafka:
|
||||
bootstrap-servers: 127.0.0.1:9092
|
||||
producer:
|
||||
retries: 3
|
||||
consumer:
|
||||
auto-offset-reset: latest
|
||||
sleuth:
|
||||
sampler:
|
||||
# ProbabilityBasedSampler 抽样策略
|
||||
probability: 1.0 # 采样比例, 1.0 表示 100%, 默认是 0.1
|
||||
# RateLimitingSampler 抽样策略, 设置了限速采集, spring.sleuth.sampler.probability 属性值无效
|
||||
rate: 100 # 每秒间隔接受的 trace 量
|
||||
zipkin:
|
||||
sender:
|
||||
type: kafka # 默认是 web
|
||||
base-url: http://localhost:9411/
|
||||
main:
|
||||
allow-bean-definition-overriding: true
|
||||
#
|
||||
## Feign 的相关配置
|
||||
#feign:
|
||||
# # feign 开启 gzip 压缩
|
||||
# compression:
|
||||
# request:
|
||||
# enabled: true
|
||||
# mime-types: text/xml,application/xml,application/json
|
||||
# min-request-size: 1024
|
||||
# response:
|
||||
# enabled: true
|
||||
# # 禁用默认的 http, 启用 okhttp
|
||||
# httpclient:
|
||||
# enabled: false
|
||||
# okhttp:
|
||||
# enabled: true
|
||||
# # OpenFeign 集成 Hystrix
|
||||
# hystrix:
|
||||
# enabled: true
|
||||
|
||||
# 暴露端点
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: '*'
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
@ -0,0 +1,5 @@
|
||||
### 查看 Sleuth 跟踪信息
|
||||
GET http://127.0.0.1:9001/imooc/dev-protocol-spring-cloud-nacos/sleuth/trace-info
|
||||
Accept: application/json
|
||||
e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjE3LFwidXNlcm5hbWVcIjpcIkltb29jUWlueWlAaW1vb2MuY29tXCJ9IiwianRpIjoiMGIxNzQyYWItMWU3OC00OTZjLWIyNTAtMjNkZGQ1ZGEyZTU1IiwiZXhwIjoxNjI0MjA0ODAwfQ.QKGHzohSHdYDHzUVHpe9gNPUgzfkPwrSbB-WiMWYjLlt2tr9BufzZM8bSt-whb_bd0hKoC6rkYYO0WUVR67uSML-2yaTL1xMIn8GH9Flyig3rpO4vefL3Hp2TXIpwHHa7WlKsLzcUpNk9lxWs2B5k0ICdYCH_jD5Tx6N7CzfSUG9u4fOnVeM9UFE2nX_DURupUh_DKCc2oOoMeyCSR7Ma8-Ab4WQU3r-U0YivR8G1A0kmKOIoTeRhM3LcPuxUPh3rCbrjzMg--fexRGw0O38Qsby6pz-ku2IlTyFXY6_jNOG1BZR34-jBOnaIciP1TExw9bFumeuC2GcowTHJVH1Nw
|
||||
token: imooc
|
Loading…
Reference in New Issue