API开放平台完结
第一期
前端初始化
1、Ant Design Pro使用的@3.1.0
react 、ts
▼bash复制代码npm install -g @ant-design/pro-cli@3.1.0
2、项目瘦身
删除国际化
▼json复制代码"i18n-remove": "pro i18n-remove --locale=zh-CN --write",
删除scr下的locales文件
删除components下的SelectLang
▼ts复制代码export { AvatarDropdown, AvatarName, Footer, Question, SelectLang };
删除e2e(多段测试)
后端初始化
todo
自己写一个完整的v2.7的模板,用户的增删改查
▼sql复制代码-- 创建库 create database if not exists larly_api; -- 切换库 use larly_api; -- 用户表 create table if not exists user ( id bigint auto_increment comment 'id' primary key, userName varchar(256) null comment '用户昵称', userAccount varchar(256) not null comment '账号', userAvatar varchar(1024) null comment '用户头像', gender tinyint null comment '性别', userRole varchar(256) default 'user' not null comment '用户角色:user / admin', userPassword varchar(512) not null comment '密码', 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 '是否删除', constraint uni_userAccount unique (userAccount) ) comment '用户';
数据库表设计
id
name 接口名称
descrpition 描述
url 接口地址
type 接口类型
requestHeader 请求头
responseHeader 响应头
status 状态 (0 - 关闭 ,1- 开启)
conut 使用次数
isDelete
createTime
updateTime
SQL网址:https://sqlfather.yupi.icu/
▼sql复制代码-- 接口信息表 create table if not exists larly_api.`interface_Info` ( `id` bigint not null auto_increment comment '主键' primary key, `name` varchar(256) not null comment '接口名称', `descrpition` varchar(256) null comment '描述', `requestHeader` text not null comment '请求头', `url` varchar(256) not null comment '接口地址', `method` varchar(256) not null comment '接口类型', `responeHeader` varchar(256) not null comment '响应头', `status` int default 0 not null comment '接口状态(0-关闭,1-开启)', `count` int default 0 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 '接口信息表';
OPEN AI生成前端api接口的插件
使用apifox导出选项,已openAPI 导出json格式,在前端config中使用
▼ts复制代码//================ pro 插件配置 ================= presets: ['umi-presets-pro'], /** * @name openAPI 插件的配置 * @description 基于 openapi 的规范生成serve 和mock,能减少很多样板代码 * @doc https://pro.ant.design/zh-cn/docs/openapi/ */ openAPI: [ { requestLibPath: "import { request } from '@umijs/max'", schemaPath: 'http://127.0.0.1:4523/export/openapi/3?version=3.0', projectName: 'LarlyAPI_backend', }, ], mfsu: { strategy: 'normal', }, esbuildMinifyIIFE: true, requestRecord: {}, });
找到命令
▼json复制代码"openapi": "max openapi"
前端登录
设置全局状态
每次页面加载获取
表格渲染
todo
优化第一个项目---用户中心
第二期
前端优化
表格页面的增删改查
了解ProTable Promodal模块
了解Ant design Pro的全局状态管理
▼tsx复制代码import { PlusOutlined } from '@ant-design/icons'; import type { ActionType, ProColumns, ProDescriptionsItemProps } from '@ant-design/pro-components'; import { FooterToolbar, PageContainer, ProDescriptions, ProTable, } from '@ant-design/pro-components'; import '@umijs/max'; import { Button, Drawer, message } from 'antd'; import React, { useRef, useState } from 'react'; import type {SortOrder} from "antd/lib/table/interface"; import { getInterfaceInfoListPage, postInterfaceInfoAdd, postInterfaceInfoOpenApiDelete, postInterfaceInfoUpdate } from "@/services/larlyAPI_frontend/interfaceInfoController"; import CreateInterface from "@/pages/TableList/components/CreateInterface"; import UpdateInterface from "@/pages/TableList/components/UpdateInterface"; const TableList: React.FC = () => { /** * @en-US Pop-up window of new window * @zh-CN 新建窗口的弹窗 * */ const [createModalOpen, handleModalOpen] = useState<boolean>(false); /** * @en-US The pop-up window of the distribution update window * @zh-CN 分布更新窗口的弹窗 * */ const [updateModalOpen, handleUpdateModalOpen] = useState<boolean>(false); const [showDetail, setShowDetail] = useState<boolean>(false); const actionRef = useRef<ActionType>(); const [currentRow, setCurrentRow] = useState<API.InterfaceInfoVo>(); const [selectedRowsState, setSelectedRows] = useState<API.InterfaceInfoVo[]>([]); /** * @en-US Add node * @zh-CN 添加节点 * @param fields */ const handleAdd = async (fields: API.InterfaceInfoVo) => { const hide = message.loading('正在添加'); try { await postInterfaceInfoAdd({ ...fields, }); handleModalOpen(false) message.success('Added successfully'); actionRef.current?.reload(); return true; } catch (error) { hide(); message.error('添加失败'); return false; } }; /** * @en-US Update node * @zh-CN 更新节点 * * @param fields */ const handleUpdate = async (fields: API.InterfaceInfoVo) => { const hide = message.loading('修改中。。。。'); console.log(currentRow) try { await postInterfaceInfoUpdate({ id: currentRow?.id, ...fields, }); hide(); handleUpdateModalOpen(false) message.success('Configuration is successful'); actionRef.current?.reload(); return true; } catch (error) { hide(); message.error('修改失败'); return false; } }; /** * Delete node * @zh-CN 删除节点 * * @param selectedRows */ const handleRemove = async (selectedRows: API.InterfaceInfoVo) => { const hide = message.loading('正在删除'); console.log(selectedRows) if (!selectedRows) return true; try { await postInterfaceInfoOpenApiDelete({ id: selectedRows.id }) hide(); message.success('删除成功'); actionRef.current?.reload(); return true; } catch (error) { hide(); message.error('删除失败'); return false; } }; const columns: ProColumns<API.InterfaceInfoVo>[] = [ { title: 'id', dataIndex: 'id', valueType: 'text', hideInForm: true, }, { title: '接口名称', dataIndex: 'name', valueType: 'text', }, { title: '描述', dataIndex: 'descrpition', valueType: 'textarea', }, { title: '请求头', dataIndex: 'requestHeader', valueType: 'textarea', }, { title: '接口地址', dataIndex: 'url', valueType: 'text', }, { title: '接口类型', dataIndex: 'method', valueType: 'text', }, { title: '响应头', dataIndex: 'responeHeader', valueType: 'textarea', }, { title: '使用次数', dataIndex: 'callNo', sorter: true, hideInForm: true, renderText: (val: string) => `${val}${'万'}`, }, { title: '接口状态', dataIndex: 'status', hideInForm: true, valueEnum: { 0: { text: '关闭', status: 'Default', }, 1: { text: '运行中', status: 'Processing', }, }, }, { title: '创建时间', sorter: true, dataIndex: 'createTime', valueType: 'dateTime', hideInForm: true, }, { title: '操作', dataIndex: 'option', hideInForm: true, valueType: 'option', render: (_, record) => [ <a key="config" onClick={() => { handleUpdateModalOpen(true); setCurrentRow(record); }} > 编辑 </a>, <a onClick={() => handleRemove(record)} key="subscribeAlert"> 删除 </a>, ], }, ]; return ( <PageContainer> <ProTable<API.RuleListItem, API.PageParams> headerTitle={'查询表格'} actionRef={actionRef} rowKey="id" search={{ labelWidth: 120, }} toolBarRender={() => [ <Button type="primary" key="primary" onClick={() => { handleModalOpen(true); }} > <PlusOutlined /> 新建 </Button>, ]} tableAlertOptionRender={false} request={ async (params: API.InterfaceInfoQueryRequest & { pageSize?: number; current?: number; keyword?: string; }, sort: Record<string, SortOrder>, filter: Record<string, (string | number)[] | null>) => { // 获取排序字段和顺序 const sortField = Object.keys(sort)?.[0]; const sortOrder = sort?.[sortField]; const res = await getInterfaceInfoListPage({ pageSize: params.pageSize, pageNum: params.current, name: params.keyword, status: params.status, method: params.method, url: params.url, descrpition: params.descrpition, sortField: sortField, sortOrder: sortOrder as 'asc' | 'desc' | undefined, },{}) const formattedList = res?.data?.list || [] return{ data: formattedList, success: true, total: res?.data?.total || 0, } } } columns={columns} rowSelection={{ onChange: (_, selectedRows) => { setSelectedRows(selectedRows); }, }} /> <Drawer width={600} open={showDetail} onClose={() => { setCurrentRow(undefined); setShowDetail(false); }} closable={false} > {currentRow?.name && ( <ProDescriptions<API.RuleListItem> column={2} title={currentRow?.name} request={async () => ({ data: currentRow || {}, })} params={{ id: currentRow?.name, }} columns={columns as ProDescriptionsItemProps<API.RuleListItem>[]} /> )} </Drawer> <UpdateInterface values={currentRow} columns={columns} onCancel={() => handleUpdateModalOpen(false)} onSubmit={handleUpdate} open={updateModalOpen}/> <CreateInterface columns={columns} onCancel={() => handleModalOpen(false) } onSubmit={handleAdd} open={createModalOpen}/> </PageContainer> ); }; export default TableList;
编辑页面
▼tsx复制代码import { ProTable } from '@ant-design/pro-components'; import '@umijs/max'; import { Modal } from 'antd'; import React, {useEffect, useRef} from 'react'; import {ProFormInstance} from "@ant-design/pro-form/lib"; export type props = { values: API.InterfaceInfoVo | undefined columns: API.InterfaceInfoVo[] onCancel: () => void; onSubmit: (values:API.InterfaceInfoVo) => void; open: boolean; }; const UpdateInterface: React.FC<props> = (props) => { const { columns,open,onCancel,onSubmit,values} = props; const formRef = useRef<ProFormInstance>(); useEffect(() => { formRef?.current?.setFieldsValue(values); },[values]) return ( <Modal open={open} onCancel={() => onCancel()}> <ProTable type="form" formRef={formRef} form={ { initialValues: values } } columns={columns} onSubmit={async (values) => { onSubmit(values); }}/> </Modal> ) } export default UpdateInterface;
新建页面
▼tsx复制代码import { ProTable } from '@ant-design/pro-components'; import '@umijs/max'; import { Modal } from 'antd'; import React, {useEffect} from 'react'; export type props = { columns: API.InterfaceInfoVo[] onCancel: () => void; onSubmit: (values:API.InterfaceInfoVo) => void; open: boolean; }; const CreateInterface: React.FC<props> = (props) => { const { columns,open,onCancel,onSubmit} = props; return ( <Modal open={open} onCancel={() => onCancel()}> <ProTable type="form" columns={columns} onSubmit={async (values) => { onSubmit(values); }}/> </Modal> ) } export default CreateInterface;
后端模拟接口(三个)
1、GET 接口
2、POST (URL传参)
3、POST(对象传参)
调用接口(用程序)
几种HTTP调用方式:
1、HttpClient
2、RestTemplete
3、Hutool https://www.hutool.cn/docs/#/
这里使用RestTemplte
▼java复制代码package com.larly.project.larlyinterface.client; import com.larly.project.larlyinterface.model.User; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; import java.util.HashMap; import java.util.Map; /** * 调用第三方接口的客户端 */ public class LarlyAPIClient { private static final String BASEURL = "http://localhost:8081/api"; // GET请求 public void getName(String name){ RestTemplate restTemplate = new RestTemplate(); // 使用 UriComponentsBuilder 构建带查询参数的 URL String url = UriComponentsBuilder.fromHttpUrl(BASEURL + "/name/") .queryParam("name", name) .toUriString(); ResponseEntity<String> result = restTemplate.getForEntity(url,String.class); System.out.println(result.getBody()); } // POST请求 public void postName(@RequestParam String name){ RestTemplate restTemplate = new RestTemplate(); // 使用 UriComponentsBuilder 构建带查询参数的 URL String url = UriComponentsBuilder.fromHttpUrl(BASEURL + "/name/") .queryParam("name", name) .toUriString(); // String url = BASEURL + "/name/"; // Map<String, String> params = new HashMap<>(); // params.put("name", name); ResponseEntity<String> result = restTemplate.postForEntity(url, null, String.class); System.out.println(result.getBody()); } // Post请求 public void postNameJSON(@RequestBody User user){ RestTemplate restTemplate = new RestTemplate(); String url = BASEURL + "/name/user"; ResponseEntity<String> response = restTemplate.postForEntity(url, user, String.class); // System.out.println("POST对象传参" + user.getName()); System.out.println(response.getBody()); } }
API签名认证(重要,重要)
本质:
1、签发签名
2、使用签名
实现(放在请求头中获取)
参数一:accessKey: 调用的识别
参数二:secretKey:密钥(该参数不能传递)
(区别与用户名,密码:ak,sk无状态,用完一次就重置)
▼java复制代码package com.larly.project.larlyinterface.client; import com.larly.project.larlyinterface.model.User; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; import java.util.HashMap; import java.util.Map; /** * 调用第三方接口的客户端 */ public class LarlyAPIClient { /** * accessKey 和 secretKey */ private String accessKey; private String secretKey; public LarlyAPIClient(String accessKey, String secretKey) { this.accessKey = accessKey; this.secretKey = secretKey; } private static final String BASEURL = "http://localhost:8081/api"; // GET请求 public void getName(String name){ RestTemplate restTemplate = new RestTemplate(); // 使用 UriComponentsBuilder 构建带查询参数的 URL String url = UriComponentsBuilder.fromHttpUrl(BASEURL + "/name/") .queryParam("name", name) .toUriString(); ResponseEntity<String> result = restTemplate.getForEntity(url,String.class); System.out.println(result.getBody()); } // POST请求 public void postName(@RequestParam String name){ RestTemplate restTemplate = new RestTemplate(); // 使用 UriComponentsBuilder 构建带查询参数的 URL String url = UriComponentsBuilder.fromHttpUrl(BASEURL + "/name/") .queryParam("name", name) .toUriString(); // String url = BASEURL + "/name/"; // Map<String, String> params = new HashMap<>(); // params.put("name", name); ResponseEntity<String> result = restTemplate.postForEntity(url, null, String.class); System.out.println(result.getBody()); } // public MultiValueMap<String,String> getHeaderMap(String accessKey, String secretKey){ // MultiValueMap<String,String> headerMap = new LinkedMultiValueMap<>(); // headerMap.add("accessKey", accessKey); // headerMap.add("secretKey", secretKey); // return headerMap; // } // Post请求 public void postNameJSON(@RequestBody User user){ RestTemplate restTemplate = new RestTemplate(); String url = BASEURL + "/name/user"; HttpHeaders httpHeaders = new HttpHeaders(); // httpHeaders.addAll(getHeaderMap(accessKey, secretKey)); httpHeaders.add("accessKey", accessKey); httpHeaders.add("secretKey", secretKey); HttpEntity<User> request = new HttpEntity<>(user, httpHeaders); ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, request, String.class); // // 发送 POST 请求,user 会被自动序列化为 JSON // ResponseEntity<String> response = restTemplate.postForEntity(url, user, String.class); // System.out.println("POST对象传参" + user.getName()); System.out.println(response.getBody()); } }
▼java复制代码@PostMapping("/user") public String postName(@RequestBody User user, HttpServletRequest request){ String accessKey = request.getHeader("accessKey"); String secretKey = request.getHeader("secretKey"); if(!"larly".equals(accessKey)){ throw new Error("accessKey错误"); } if(!"123456".equals(secretKey)){ throw new Error("secretKey错误"); } return "POST对象传参" + user.getName(); }
不可以(密码不能明文传递,有可能被拦截)
加密方式:对称加密(根据密钥可以加密解密),非对称加密(公钥加密,私钥解密),单向签名(不可解密MD5)
参数五:用户参数
参数四:sign
用户参数+密钥 => 签名算法=> 不可解迷
后端怎么知道?
服务器用一模一样的加密方式加密对比
为了防止用户重复调用
参数五:加一个随机数:后端使用后就清除,只能用一次
参数六:时间戳:可以设置过期时间timestamp
服务端
▼java复制代码public String postName(@RequestBody User user, HttpServletRequest request){ String accessKey = request.getHeader("accessKey"); String timestamp = request.getHeader("timestamp"); String sign = request.getHeader("sign"); String nonce = request.getHeader("nonce"); String data = request.getHeader("data"); if(!"larly".equals(accessKey)){ throw new Error("accessKey错误"); } // todo 时间戳验证, 有效时间多少分 // todo 随机数验证, 防重放(数据库查) // secretKey实际上从数据库中取出来 String serveSign = SignUtils.getSign(data, "1234567"); if(!serveSign.equals(sign)){ throw new Error("sign错误"); } return "POST对象传参" + user.getName(); }
客户端 (其实这里我感觉还是不安全,这个客户端还是保存secretKey,只不过不是在传递过程中明文发送)
▼java复制代码public void postNameJSON(@RequestBody User user){ RestTemplate restTemplate = new RestTemplate(); String url = BASEURL + "/name/user"; HttpHeaders httpHeaders = new HttpHeaders(); // httpHeaders.addAll(getHeaderMap(accessKey, secretKey)); httpHeaders.add("accessKey", accessKey); // httpHeaders.add("secretKey", secretKey); // 添加用户数据 httpHeaders.add("data", user.toString()); // 添加随机数(防重放) httpHeaders.add("nonce", String.valueOf((int) (Math.random() * 1000000000))); // 加时间戳 httpHeaders.add("timestamp", String.valueOf(System.currentTimeMillis())); // 加sign httpHeaders.add("sign", SignUtils.getSign(user.toString(), secretKey)); HttpEntity<User> request = new HttpEntity<>(user, httpHeaders); ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, request, String.class); // // 发送 POST 请求,user 会被自动序列化为 JSON // ResponseEntity<String> response = restTemplate.postForEntity(url, user, String.class); // System.out.println("POST对象传参" + user.getName()); System.out.println(response.getBody()); }
MD5加密sign
▼java复制代码public static String getSign(String data , String secretKey) { // md5加密 Mac mac = null; try { mac = Mac.getInstance("HmacSHA256"); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(), "HmacSHA256"); try { mac.init(keySpec); } catch (InvalidKeyException e) { throw new RuntimeException(e); } byte[] result = mac.doFinal(data.getBytes()); return Base64.getEncoder().encodeToString(result); }
开发一个简单的SDK
开发者只用传递接口,不需要加密sign
1、新建项目 larly-client-sdk
删除maven自带的build打包工具,防止生成jar包
spring-boot-configuration-processor:帮助生成代码提示
新建META-INFO配置spring.factories文件
▼text复制代码org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.larly.larlyclientsdk.LarlyAPIClientConfig(你的sdk包名)
maven install(会下载到你本地的maven仓库)
再别的项目中引入你的依赖,就可以使用
▼java复制代码package com.larly.project.larlyinterface; import com.larly.larlyclientsdk.client.LarlyClient; import com.larly.larlyclientsdk.model.User; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import javax.annotation.Resource; @SpringBootTest class LarlyInterfaceApplicationTests { @Resource private LarlyClient larlyClient; @Test void contextLoads() { String larly = larlyClient.getNameByGet("larly"); System.out.println(larly); String larly1 = larlyClient.getNameByPost("larly"); System.out.println(larly1); User user = new User(); user.setName("larly"); String larly2 = larlyClient.getUserNameByPost(user); System.out.println(larly2); } }
第三期
开发接口发布/下线(管理员)
发布:
1、判断接口是否存在
2、接口是否可以使用
3、修改状态
下线:
1、判断接口是否存在
2、修改状态
前端浏览接口、查看接口文档(接口信息)
在管理员列表展示(发布、下线)
路由传参
申请签名(注册)
用户在注册成功。自动分配accessKey、secretKey
post
第四期
开发接口次数的统计
需求:用户每次调用接口成功,次数+1
1、接口用户关系表
▼sql复制代码-- 用户调用接口关系表 create table if not exists yuapi.`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 '总调用次数', `leftNum` 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 '更新时间', `isDelete` tinyint default 0 not null comment '是否删除(0-未删, 1-已删)' ) comment '用户调用接口关系'; -- 修改接口信息表 create table interface_info ( id bigint auto_increment comment '主键' primary key, name varchar(256) not null comment '接口名称', descrpition varchar(256) null comment '描述', requestHeader text not null comment '请求头', url varchar(256) not null comment '接口地址', method varchar(256) not null comment '接口类型', responeHeader varchar(256) not null comment '响应头', status int default 0 not null comment '接口状态(0-关闭,1-开启)', count int default 0 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-已删)', userId bigint not null comment '创建人', totalNum int default 0 not null comment '总次数', leftNum int default 0 not null comment '剩余次数' ) comment '接口信息表';
2、增删改查(给管理员使用)
每个接口成功都要+1,使用API网关可以抽离出来(AOP编程/API网关)
AOP缺点:只能在一个项目中使用
优化整个系统的构架(API网关)
什么是网关?理解成火车的中转站,统一去检票
1、路由
2、鉴权
3、跨域
4、缓存
5、流量染色
6、访问控制
7、统一的业务处理
8、发布控制
9、脱敏
10、负载均衡
11、接口保护
a、限制请求
c、降级(熔断)
d、限流
12、统一日志
13、统一文档
路由
转发作用,用户访问接口A,B网关会记录,并且会转发到对用的接口
/a=> 接口A
/b=> 接口B
负载均衡
路由基础上
/a=>集群A/服务A
统一鉴权
统一的鉴权
统一处理跨域
网关统一处理跨域
统一业务处理
把一些统一的业务逻辑放到网关。
访问控制(黑白名单)
限制DDOS IP
发布控制
发布灰度,比如一个新接口测试分布流量20%,旧接口使用80%
流量染色
给请求(流量)添加一些标识一般在请求头,来区分一些东西,或者过滤,比如为了防止绕过网关,流量在经过网关会添加请求头
统一的日志
统一的请求,响应输入
统一的文档
把不同接口的文档,通义在一起
网关的分类
1、业务网关(多个项目,多个微服务基础上):处理一些业务逻辑,作用是将请求转发到不同的业务/项目/接口
2、全局网关:负载均衡,作用是请求日志。。。
实现
1、Nginx(全局网关)、Kong网关(API网关),编程成本高
2、Spring Cloud Gateway(取代了zuul)性能高、可以使用java
Spring Cloud Gateway 用法
中文网:https://springdoc.cn/spring-cloud-gateway/#google_vignette
router(路由): 根据什么条件,转发请求
predicate(断言):一组规则,用来确定如何转发路由(请求地址)
filter(过滤器):对请求进行处理,比如添加请求头,鉴权
Gateway Client: 发送请求的客户端
GatewayHabdler Mapping:根据断言,转发请求
Gateway Web Handler:处理请求,过滤器
两种方式
1、配置式
▼yml复制代码spring: cloud: gateway: routes: - id: after_route uri: https://example.org predicates: - Cookie=mycookie,mycookievalue
2、编程式
建议开启日志
▼yml复制代码logging: level: org: springframework: cloud: gateway: trace
断言
1、After:在xx时间之后执行
2、Before:在xx时间之前执行
3、Between:在xx时间之间执行
4、请求类型
5、请求头(包含Cookie,鉴权)
6、查询参数
7、客户端地址(黑白名单)
8、权重(版本控制)
过滤器
1、添加请求头
2、添加请求参数
3、添加响应头
4、降级
5、限流
6、重试
▼xml复制代码<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId> </dependency>
第五期
把API网关运用到项目
1、实现统一的接口的鉴权、计费、调用次数
项目使用的特性
1、路由(转发到接口)
2、鉴权(acessKey、 secretKey)
3、流量染色 (记录是否是从网管来的。添加请求头标识)
4、访问控制(黑白名单)
5、统一的业务处理 (每次接口调用后。次数加1)
6、接口保护
a、限制请求
c、降级(熔断)
d、限流
7、统一日志(记录每次的请求和响应日志)
业务逻辑
1、用户发送请求到 API 网关
2、请求日志
3、用户鉴权(ak,sk是否合法)
4、判断接口是否存在
5、添加唯一标识
6、请求转发到接口
7、相应日志
8、调用次数加1
实现
1、请求转发
http://localhost:8081/api/name/?name=xhd 接口地址
API网关跳转:
http://localhost:8090/api/** => http://localhost:8081/api/name/?name=xhd
▼yaml复制代码#设置路由 spring: cloud: gateway: routes: - id: api_route # 测试接口 uri: http://localhost:8081 predicates: - Path=/api/**
uri: http://localhost:8081跳转时会带上path路径
2、编写业务逻辑
使用全局过滤器(编程式), 全局拦截处理拦截(类似AOP)
▼java复制代码package com.larly.apigateway; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import com.larly.larlyclientsdk.utils.SignUtils; import java.util.UUID; @Component @Slf4j /** * 全局过滤器 */ public class CustomGlobalFilter implements GlobalFilter, Ordered { // 1、用户发送请求到 API 网关 @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 2、请求日志 ServerHttpRequest request = exchange.getRequest(); log.info("请求唯一id:{},请求方法:{},请求路径:{}",request.getId(),request.getMethod(),request.getPath()); log.info("请求参数:{}",request.getQueryParams().values()); // 3、用户鉴权(ak,sk是否合法) String accessKey = request.getHeaders().getFirst("accessKey"); String timestamp = request.getHeaders().getFirst("timestamp"); String sign = request.getHeaders().getFirst("sign"); String nonce = request.getHeaders().getFirst("nonce"); String data = request.getHeaders().getFirst("data"); if(!"larly".equals(accessKey)){ throw new Error("accessKey错误"); } // todo 时间戳验证, 有效时间多少分 // todo 随机数验证, 防重放(数据库查) // secretKey实际上从数据库中取出来 String serveSign = SignUtils.getSign(data, "1234567"); if(!serveSign.equals(sign)){ throw new Error("sign错误"); } // 4、判断接口是否存在 // todo 调用api-backend 接口 获取结果 // 5、添加唯一标识 String traceId = UUID.randomUUID().toString(); ServerHttpRequest newRequest = exchange.getRequest().mutate() .header("X-Trace-ID", traceId) .build(); // 7、相应日志 ServerHttpResponse response = exchange.getResponse(); log.info("响应结果:{}",response.getStatusCode()); // todo 8、调用次数加1 // 6、请求转发到接口 return chain.filter(exchange.mutate().request(newRequest).build()); } @Override public int getOrder() { return -1; } }
注意:这是一个异步的操作,直到filter过滤器return才会执行接口
解决方法:利用response装饰者,增强原有的response
处理一下response结果
▼java复制代码/** * 处理响应 * * @param exchange * @param chain * @return */ public Mono<Void> handleResponse(ServerWebExchange exchange, GatewayFilterChain chain) { try { // 获取原始的响应对象 ServerHttpResponse originalResponse = exchange.getResponse(); // 获取数据缓冲工厂 DataBufferFactory bufferFactory = originalResponse.bufferFactory(); // 获取响应的状态码 HttpStatus statusCode = originalResponse.getStatusCode(); // 判断状态码是否为200 OK(按道理来说,现在没有调用,是拿不到响应码的,对这个保持怀疑 沉思.jpg) if(statusCode == HttpStatus.OK) { // 创建一个装饰后的响应对象(开始穿装备,增强能力) ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) { // 重写writeWith方法,用于处理响应体的数据 // 这段方法就是只要当我们的模拟接口调用完成之后,等它返回结果, // 就会调用writeWith方法,我们就能根据响应结果做一些自己的处理 @Override public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) { log.info("body instanceof Flux: {}", (body instanceof Flux)); // 判断响应体是否是Flux类型 if (body instanceof Flux) { Flux<? extends DataBuffer> fluxBody = Flux.from(body); // 返回一个处理后的响应体 // (这里就理解为它在拼接字符串,它把缓冲区的数据取出来,一点一点拼接好) return super.writeWith(fluxBody.map(dataBuffer -> { // 读取响应体的内容并转换为字节数组 byte[] content = new byte[dataBuffer.readableByteCount()]; dataBuffer.read(content); DataBufferUtils.release(dataBuffer);//释放掉内存 // 构建日志 StringBuilder sb2 = new StringBuilder(200); sb2.append("<--- {} {} \n"); List<Object> rspArgs = new ArrayList<>(); rspArgs.add(originalResponse.getStatusCode()); //rspArgs.add(requestUrl); String data = new String(content, StandardCharsets.UTF_8);//data sb2.append(data); log.info(sb2.toString(), rspArgs.toArray());//log.info("<-- {} {}\n", originalResponse.getStatusCode(), data); // 将处理后的内容重新包装成DataBuffer并返回 return bufferFactory.wrap(content); })); } else { log.error("<--- {} 响应code异常", getStatusCode()); } return super.writeWith(body); } }; // 对于200 OK的请求,将装饰后的响应对象传递给下一个过滤器链,并继续处理(设置repsonse对象为装饰过的) return chain.filter(exchange.mutate().response(decoratedResponse).build()); } // 对于非200 OK的请求,直接返回,进行降级处理 return chain.filter(exchange); }catch (Exception e){ // 处理异常情况,记录错误日志 log.error("gateway log exception.\n" + e); return chain.filter(exchange); } }
第六期
计划
1、补充完成网关的业务逻辑(操作数据库,复用之前的方法)
2、完善系统、开发一个监控统计功网管
业务逻辑
问题:网管项目比较纯净,没有操作数据库的包,并且调用之前写过的方法
理想:直接请求其他方法
怎么调用其他项目
1、复制代码
2、HTTP请求(提供接口)
3、RPC
4、把公共的代码打个jar包,其他项目引用(客户端SDK)
HTTP请求怎么调用
1、提供方开发一个接口
2、调用方使用HTTP Client发送请求
RPC
作用:像调用本地方法一样调用远程方法
假设你在项目A编写了一个方法想在其他项目中使用,可以使用RPC
和直接调用HTTP区别
1、对开发者透明,开发者不用写接口地址,请求方法
2、底层是向远程服务器发送请求,不一定使用HTTP,或者使用一些协议TCP
PRC实现步骤
提供者将地址和方法注册到注册中心,消费者在使用时向注册中心要地址和方法,调用方直接调用提供者的方法,不经过注册中心
Feign
Feign本质上也是HTTP客户端,他只是精简了HTTP请求的步骤
Dubbo框架(RPC实现)
https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/quick-start/starter/
两种使用方式:
1、Spring Boot代码(注解+编程式):写java接口,服务提供者实现这个接口,消费者调用
2、IDL(接口调用语言):创建一个公共的接口定义文件,服务提供者和消费者读取这个文件。优点式跨语言,所有框架都能识别
应用
1、backend作为服务提供者
2、三个方法:a、从数据库中获取用户ak,sk
b、判断接口是否存在
c、调用接口成功后,次数+1
3、gateway作为消费者
Zookeeper和Nacos
Zookeeper作为一个注册中心,Dubbo的作用就是将消费者、提供者、注册中心三者通过其框架整合在一起
Nacos也可以作为注册中心(推荐)
Nacos
https://nacos.io/docs/next/quickstart/quick-start/#0-%E7%89%88%E6%9C%AC%E9%80%89%E6%8B%A9
需要java8+
bin目录下
▼bash复制代码startup.cmd -m standalone
添加依赖
▼xml复制代码<!-- Dubbo --> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>3.3.0</version> </dependency> <!-- Nacos --> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-nacos-spring-boot-starter</artifactId> <version>3.3.0</version> </dependency>
添加配置
▼yml复制代码# 以下配置指定了应用的名称、使用的协议(Dubbo)、注册中心的类型(Nacos)和地址 dubbo: application: # 设置应用的名称 name: dubbo-springboot-demo-provider # 指定使用 Dubbo 协议,且端口设置为 -1,表示随机分配可用端口 protocol: name: dubbo port: -1 registry: # 配置注册中心为 Nacos,使用的地址是 nacos://localhost:8848 id: nacos-registry address: nacos://localhost:8848
提供者(backend)
消费者(gateway)
注意:消费者的包的地址要和注册中心的提供者包的地址一样,不然消费者会找不到提供者对应的包
第七期
1、完成网管业务逻辑
2、上线
封装总的业务逻辑
larly-common:使用install安装,并在backend和gateway引入依赖
1、获取用户的accessKey(accessKey,用户对象)
2、判断接口是否存在(接口名称,接口请求类型,接口对象)
3、接口调用完成次数加一(接口id,用户id)
