项目总结
数据库设计
yml配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/xxxx username: xxxx password: xxxx
mybatis-plus: configuration: map-underscore-to-camel-case: false log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: isDelete logic-delete-value: 1 logic-not-delete-value: 0
|
数据表
🎈 快速生成SQL
用户表user
id |
账号 |
密码 |
用户昵称 |
用户头像 |
accessKey |
secretKey |
用户角色 |
创建时间 |
更新时间 |
是否删除 |
id |
userAccount |
userPassword |
userName |
userAvatar |
accessKey |
secretKey |
userRole |
createTime |
updateTime |
isDelete |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| create table if not exists user ( id bigint auto_increment comment 'id' primary key, userAccount varchar(256) not null comment '账号', userPassword varchar(512) not null comment '密码', userName varchar(256) null comment '用户昵称', userAvatar varchar(1024) null comment '用户头像', accessKey varchar(512) not null comment 'accessKey', secretKey varchar(512) not null comment 'secretKey', userRole varchar(256) default 'user' not null comment '用户角色:user/admin/ban', createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间', updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', isDelete tinyint default 0 not null comment '是否删除' ) comment '用户' collate = utf8mb4_unicode_ci;
|
接口信息表interface_info
Tips: 仿照网络中请求样例进行设计
- status中 0-关闭(默认) 1 -打开
id |
名称 |
描述 |
请求地址 |
请求类型 |
请求头 |
响应头 |
状态 |
创建人 |
创建时间 |
更新时间 |
是否删除 |
id |
name |
description |
url |
method |
requestHeader |
responseHeader |
status |
userId |
createTime |
updateTime |
isDelete |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| create table if not exists `interface_info` ( `id` bigint not null auto_increment comment '主键' primary key, `name` varchar(256) not null comment '接口名称', `description` varchar(256) null comment '接口描述', `url` varchar(512) not null comment '接口地址', `method` varchar(256) not null comment '请求方法', `requestHeader` text null comment '请求头', `responseHeader` text null comment '响应头', `status` int default 0 not null comment '接口状态 0-关闭(默认) 1-打开', `userId` bigint not null comment '创建人', `createTime` datetime default CURRENT_TIMESTAMP not null comment '创建时间', `updateTime` datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', `isDeleted` tinyint default 0 not null comment '是否删除(0-未删, 1-已删)' ) comment '接口信息表';
|
用户接口关系表user_interface_info
id |
用户id |
接口id |
拥有调用次数 |
已调用次数 |
状态 |
创建时间 |
更新时间 |
是否删除 |
id |
userId |
interfaceInfoId |
totalNum |
invokeNum |
status |
createTime |
updateTime |
isDelete |
1 2 3 4 5 6 7 8 9 10 11 12 13
| create table if not exists `user_interface_info` ( `id` bigint not null auto_increment comment '主键' primary key, `userId` bigint not null comment '用户id', `interfaceInfoId` bigint not null comment '接口id', `totalNum` int default 0 not null comment '拥有调用次数', `invokeNum` int default 0 not null comment '已调用次数', `status` int default 0 not null comment '0-正常(默认) 1-禁用(次数用光)', `createTime` datetime default CURRENT_TIMESTAMP not null comment '创建时间', `updateTime` datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', `isDeleted` tinyint default 0 not null comment '是否删除(0-未删, 1-已删)' ) comment '用户接口关系表';
|
快速生成CRUD
🎈 MybatisX
插件
模拟接口模块开发
提供接口
模块: api-interface
功能: 提供模拟的接口
提供三个模拟接口 GET接口 | POST接口(url传参) | (Restful)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
@RestController @RequestMapping("/name") @Slf4j public class NameController {
@GetMapping() public String getNameByGet(String name) { return "GET 你的名字是" + name; }
@PostMapping("/{name}") public String getNameByPost(@PathVariable String name) { return "POST 你的名字是" + name; }
@PostMapping() public String getUsernameByPost(@RequestBody User user) { return "POST 用户名是" + user.getUsername(); } }
|
网关流量染色验证
流量染色: 指在请求流中添加标识(例如特定的 HTTP 头部或请求参数),以标记不同类型的流量。通过这种方式,流量在经过各个 微服务和组件时可以被识别和处理。
避免非法调用故在网关处增加流量染色, 必须是网关转发的请求才会进行处理
🎈 实现流程:
- 实现全局请求拦截器判断请求是否是由网关转发而来
- 调用对应接口
🔗 全局请求拦截器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
@Component public class GlobalInterceptor implements HandlerInterceptor {
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String flag = request.getHeader("Gateway-Flag"); if (flag == null || !flag.equals("gbbdxstx")) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); response.getWriter().write("您无权限调用此接口"); return false; } return true; } }
|
🔗 注册自定义拦截器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
@Configuration public class WebMvcConfiguration extends WebMvcConfigurationSupport {
@Resource private GlobalInterceptor globalInterceptor;
protected void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(globalInterceptor) .addPathPatterns("/**"); }
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) { converters.stream() .filter(StringHttpMessageConverter.class::isInstance) .map(c -> (StringHttpMessageConverter) c) .forEach(c -> c.setDefaultCharset(StandardCharsets.UTF_8)); } }
|
接口调用SDK开发
模块: api-sdk
SDK
: 一组工具、库、示例代码的集合,帮助开发者|接口调用者创建应用程序或者进行特定的开发任务
模块功能: 提供客户端服务,使开发者|接口调用者只需要关注调用哪个接口,传递哪些参数,就跟调用现有的方法一样, 这样开发 者| 接口调用者便不用自己编写签名认证的代码, 只要调用客户端的方法即可
调用HTTP方式: HttpClient | RestTemplate | 第三方库(OKHTTP、Hutool)
🎈 本项目使用的是 Hutool 工具包
🎈 实现流程:
- 客户端开发, 封装请求头(签名认证信息), 向网关发送请求
- 签名工具类(加密)
- 编写配置类, 实现将从配置文件读取信息并把客户端交给Spring容器管理
- 实现开发者引用
SDK
自动配置
- 利用Maven打包, 上传到仓库中
🔗 API签名认证介绍
本质
用户每次调用接口都需要携带ak和sk, 只认签名, 不关注用户登录态
ak和sk类似用户名和密码, 区别是ak和sk是无状态的
- accessKey: 调用的标识
- secretKey: 密钥
实现
通过http request header 头传递参数, 主要包括
-
accessKey
: 调用的标识
-
secretKey
: 密钥 ✨ 密钥不能放到请求头中
-
用户请求参数
-
签名sign
- 生成: 用户参数 + 密钥 + 签名生成算法(MD5、Hmac、Sha1) => 不可解密的值
- 验证: 服务端用一模一样的参数和算法生成新签名, 判断与请求头中的签名是否一致
-
nonce
: 一个随机数, 只能用一次, 服务端要保存用过的随机数
-
timestamp
: 验证时间时间戳是否过期, 达到定时清理随机数的效果
分配accessKey和secretKey
在用户注册成功时自动分配 accessKey
和 secretKey
1 2 3 4 5 6 7
| String accessKey = DigestUtil.md5Hex(SALT + userAccount + RandomUtil.randomNumbers(5)); String secretKey = DigestUtil.md5Hex(SALT + userAccount + RandomUtil.randomNumbers(8));
User user = new User(); user.setAccessKey(accessKey); user.setSecretKey(secretKey);
|
作业
用户可以申请更换签名 – 有空可做
🔗 客户端开发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
|
public class ApiClient {
private String accessKey; private String secretKey;
private static final String GATEWAY_HOST = "http://localhost:8103";
public ApiClient(String accessKey, String secretKey) { this.accessKey = accessKey; this.secretKey = secretKey; }
public String getNameByGet(String name) { HashMap<String, Object> paramMap = new HashMap<>(); paramMap.put("name", name); String result = HttpRequest.get(GATEWAY_HOST + "/api/name") .form(paramMap) .addHeaders(getHeaderMap("")) .execute().body(); System.out.println(result); return result; }
public String getNameByPost(String name) { String urlString = GATEWAY_HOST + "/api/name/" + name; String result = HttpUtil.post(urlString, ""); System.out.println(result); return result; }
private Map<String, String> getHeaderMap(String body) { HashMap<String, String> hashMap = new HashMap<>(); hashMap.put("accessKey", accessKey); hashMap.put("nonce", RandomUtil.randomNumbers(4)); hashMap.put("body", URLEncoder.encode(body, StandardCharsets.UTF_8)); hashMap.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000)); hashMap.put("sign", SignUtil.getSign(body, secretKey)); return hashMap; }
public String getUsernameByPost(User user) { String json = JSONUtil.toJsonStr(user); HttpResponse httpResponse = HttpRequest.post(GATEWAY_HOST + "/api/name/user") .addHeaders(getHeaderMap(json)) .body(json) .execute(); System.out.println(httpResponse.getStatus()); String result = httpResponse.body(); System.out.println(result); return result; } }
|
🔗 签名工具类
1 2 3 4 5 6 7 8 9 10 11 12
|
@Component public class SignUtil {
public static String getSign(String body, String secretKey) { Digester md5 = new Digester(DigestAlgorithm.MD5); String content = body + "." + secretKey; return md5.digestHex(content); } }
|
🔗 配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Configuration @ConfigurationProperties("api.client") @Data @ComponentScan public class ApiClientConfig {
private String accessKey;
private String secretKey;
@Bean public ApiClient apiClient() { return new ApiClient(accessKey, secretKey); } }
|
🔗 实现开发者引入SDK
自动配置
创建resources\META-INF\spring.factories
文件, 写入以下内容
1 2
| org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.gbbdxstx.gbbdxstxapiclientsdk.ApiClientConfig
|
RPC开发
作用: 使调用者像调用本地方法一样调用远程方法
🎈 RPC模型:
🔗 公共服务接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
public interface DemoService {
String sayHello(String name);
boolean invokeCount(long userId, long interfaceInfoId);
User getInvokeUser(String accessKey);
InterfaceInfo getInterfaceInfo(String url, String method); }
|
🔗 提供者
启动类上要加@EnableDubbo
注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
|
@DubboService public class DemoServiceImpl implements DemoService {
@Resource private UserInterfaceInfoMapper userInterfaceInfoMapper;
@Resource private UserMapper userMapper;
@Resource private InterfaceInfoMapper interfaceInfoMapper;
@Override public String sayHello(String name) { return "Hello " + name; }
@Override public boolean invokeCount(long userId, long interfaceInfoId) { ThrowUtils.throwIf(userId <= 0 || interfaceInfoId <= 0, ErrorCode.PARAMS_ERROR); QueryWrapper<UserInterfaceInfo> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("userId", userId); queryWrapper.eq("interfaceInfoId", interfaceInfoId); queryWrapper.eq("status", 0); UserInterfaceInfo userInterfaceInfo = userInterfaceInfoMapper.selectOne(queryWrapper); ThrowUtils.throwIf(userInterfaceInfo == null, ErrorCode.NO_AUTH_ERROR); userInterfaceInfo.setInvokeNum(userInterfaceInfo.getInvokeNum() + 1); if (userInterfaceInfo.getTotalNum() == userInterfaceInfo.getInvokeNum()) { userInterfaceInfo.setStatus(1); } return userInterfaceInfoMapper.updateById(userInterfaceInfo) == 1; }
@Override public User getInvokeUser(String accessKey) { if (StringUtils.isAnyBlank(accessKey)) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } QueryWrapper queryWrapper = new QueryWrapper(); queryWrapper.eq("accessKey", accessKey); return userMapper.selectOne(queryWrapper); }
@Override public InterfaceInfo getInterfaceInfo(String url, String method) { if (StringUtils.isAnyBlank(url, method)) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } QueryWrapper queryWrapper = new QueryWrapper(); queryWrapper.eq("url", url); queryWrapper.eq("method", method); return interfaceInfoMapper.selectOne(queryWrapper); }
}
|
🔗 网关调用
启动类上要加@EnableDubbo
注解
1 2 3 4 5
| @DubboReference private DemoService demoService;
...调用提供的方法
|
网关模块开发
模块: api-gateway
目的: 利用网关实现统一处理不同项目不同请求的统一操作
功能: 实现路由转发, 黑白名单, 打印日志, 进行用户鉴权, 流量染色, 统计调用次数等功能
实现方法: 利用 Spring Cloud Gateway + Dubbo + Nacos实现网关功能
🔗 配置信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| spring: cloud: gateway: routes: - id: api_route uri: http://localhost:8102 predicates: - Path=/api/** default-filters: - AddResponseHeader=source, gbbdxstx dubbo: application: name: dubbo-springboot-demo-consumer logger: slf4j qos-port: 33333 registry: address: nacos://${nacos.address:127.0.0.1}:8848?username=nacos&password=nacos cache-file: D:/software/nacos/.mapping.dubbo.cache
|
🔗 全局请求过滤器
业务逻辑:
- 用户发送请求转发到 API 网关
- 请求日志
- 黑白名单
- 用户鉴权(判断 accessKey, secretKey 是否合法)
- 判断请求的模拟接口是否存在
- 请求转发,调用模拟接口
- 响应日志
- 调用成功,次数 + 1
- 调用失败,返回一个规范的错误码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
|
@Component @Slf4j public class CustomGlobalFilter implements GlobalFilter, Ordered {
@DubboReference private DemoService demoService;
private static final List<String> IP_WHITE_LIST = Arrays.asList("0:0:0:0:0:0:0:1", "127.0.0.1");
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String originUrl = request.getURI().toString(); Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR); String urlBefore = route.getUri().toString(); String url = urlBefore + request.getPath().value(); String method = request.getMethod().toString(); log.info("请求唯一标识: {}", request.getId()); log.info("请求路径: {}", url); log.info("请求方法: {}", method); log.info("请求参数: {}", request.getQueryParams()); String sourceAddress = request.getLocalAddress().getHostString(); log.info("请求来源地址: {}", sourceAddress); ServerHttpResponse response = exchange.getResponse();
if (!IP_WHITE_LIST.contains(sourceAddress)) { response.setStatusCode(HttpStatus.FORBIDDEN); return response.setComplete(); }
HttpHeaders headers = request.getHeaders(); String accessKey = headers.getFirst("accessKey"); String nonce = headers.getFirst("nonce"); String encodedBody = headers.getFirst("body"); String body = URLDecoder.decode(encodedBody, StandardCharsets.UTF_8); String timestamp = headers.getFirst("timestamp"); String sign = headers.getFirst("sign"); User invokeUser = null; try { invokeUser = demoService.getInvokeUser(accessKey); } catch (Exception e) { log.error("getInvokeUser error", e); } if (invokeUser == null) { return handleNoAuth(response); } if (Long.parseLong(nonce) > 10000L) { return handleNoAuth(response); } long currentTime = System.currentTimeMillis() / 1000; final long FIVE_MINUTES = 60 * 5L; if (currentTime - Long.parseLong(timestamp) > FIVE_MINUTES) { return handleNoAuth(response); } String secretKey = invokeUser.getSecretKey(); String serverSign = SignUtil.getSign(body, secretKey); if (sign == null || !sign.equals(serverSign)) { return handleNoAuth(response); } InterfaceInfo interfaceInfo = null; try { interfaceInfo = demoService.getInterfaceInfo(url, method); } catch (Exception e) { log.error("getInterfaceInfo error", e); } if (interfaceInfo == null) { return handleNoAuth(response); }
ServerHttpRequest mutatedRequest = request.mutate() .header("Gateway-Flag", "gbbdxstx").build(); ServerWebExchange mutatedExchange = exchange.mutate() .request(mutatedRequest) .build();
Mono<Void> filter = chain.filter(mutatedExchange);
log.info("响应: {}", response.getStatusCode()); Long userId = invokeUser.getId(); Long interfaceInfoId = interfaceInfo.getId(); try { DataBufferFactory bufferFactory = response.bufferFactory(); HttpStatus statusCode = response.getStatusCode(); if (statusCode != HttpStatus.OK) { return chain.filter(exchange); } ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(response) { @Override public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) { if (body instanceof Flux) { Flux<? extends DataBuffer> fluxBody = Flux.from(body);
return super.writeWith(fluxBody.buffer().map(dataBuffers -> { try { demoService.invokeCount(userId, interfaceInfoId); } catch (Exception e) { log.error("invokeCount error", e); } DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); DataBuffer buff = dataBufferFactory.join(dataBuffers); byte[] content = new byte[buff.readableByteCount()]; buff.read(content); DataBufferUtils.release(buff);
String result = new String(content); List<Object> rspArgs = new ArrayList<>(); rspArgs.add(response.getStatusCode().value()); rspArgs.add(exchange.getRequest().getURI()); rspArgs.add(result); log.info("响应结果: <-- {} {}\n{}", rspArgs.toArray());
getDelegate().getHeaders().setContentLength(result.getBytes().length); return bufferFactory.wrap(result.getBytes()); })); } else { handleInvokeError(response); log.error("<-- {} 响应code异常", getStatusCode()); } return super.writeWith(body); } }; return chain.filter(exchange.mutate().response(decoratedResponse).build());
} catch (Exception e) { log.error("网关处理响应异常" + e); return chain.filter(exchange); } }
@Override public int getOrder() { return -1; }
public Mono<Void> handleNoAuth(ServerHttpResponse response) { response.setStatusCode(HttpStatus.FORBIDDEN); return response.setComplete(); }
public Mono<Void> handleInvokeError(ServerHttpResponse response) { response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); return response.setComplete(); } }
|
管理员统计分析开发
SQL编写
1 2
| # 查询热点数据 select interfaceInfoId,sum(invokeNum) as invokeNum from user_interface_info group by interfaceInfoId order by invokeNum desc limit 3;
|
------------------------------------------------------------------待补充
接口测试调用流程
- 前端将用户输入的请求参数和要测试的接口id传到后端
- 后端对信息进行校验
- 校验成功后调用模拟接口
前端问题解决
layout = 'max'
但不生效
app.tst文件中获取全局默认设置
1 2 3 4 5 6 7 8 9 10
| const state: InitialState = { loginUser: undefined, settings: defaultSettings, }
return { ...initialState?.settings, }
|