隐藏真实接口地址,node 做权限转发到内网接口
由于开发前期 java 任务紧,没时间做,所有java接口就没做相关的权限认证和角色区分。所以这部分工作就由node去实现了,node 转发到java内网接口,并处理权限的相关认证
前端接口的统一
前端访问的接口:domain.com/api/__proxy (get,post,put,delete,patch 目前这几种就够用了)
/*
参数含义:name (在node config中定义的接口)
参数含义:type (在node config所对应的服务,比如说py服务还是java服务)
参数含义:p1 (在node config所对应的服务,用来完成restful风格的接口拼接,多层拼接自己随便定义)
示例:/api/__proxy?__gateway_method_id=${name}&__gateway_place=${type}&__gateway_p1=${p1}
*/
// axios 的封装代码就不贴出来了,代码量不多。以下是前端的调用方式
// restful 拼接的情况
this.$http.gateway.get({
name: "GET_STATUS_LIST",
p1: "pickItem"
})
// 无拼接的情况
this.$http.gateway.get("GET_STATUS_LIST")
node 定义路由,权限验证,参数处理 (使用的阿里的egg)
// 路由的定义
router.get("/api/__proxy", controller.api.proxy.__proxy)
router.post("/api/__proxy", controller.api.proxy.__proxy)
router.put("/api/__proxy", controller.api.proxy.__proxy)
router.delete("/api/__proxy", controller.api.proxy.__proxy)
router.patch("/api/__proxy", controller.api.proxy.__proxy)
// Controller 层参数的处理
async __proxy() {
const { method, body } = this.ctx.request
let query = this.ctx.query
const apiName = query.__gateway_method_id // 接口的定义
delete query.__gateway_method_id
const apiPlace = this.ctx.query.__gateway_place || "ADMIN" // 服务的类型
delete this.ctx.query.__gateway_place
const isGetOrDel = method === "GET" || method === "DELETE"
let data = isGetOrDel ? this.ctx.query : body
const options = { method, data }
if (isGetOrDel) {
options.dataAsQueryString = true
} else {
options.contentType = "json"
}
const ret = await this.ctx.service.http.request(apiName, options, apiPlace)
this.ctx.body = ret
}
/*
service 层 代码 我大概讲下做了什么
1.根据参数找到config 文件对应得接口完成拼接
2.根据请求类型传参数
3.发出请求与返回数据统一结构输出
4.异常状态吗异步发送通知
5.并且支持mock数据
*/
// 部分代码示例
async __request(apiName, options = {}, type = "ADMIN") {
// mock 数据
let realApiName = typeof apiName === "object" ? apiName.name : apiName
if (this.ctx.app.config.env === "local" && isJavaAdmin) {
const hasMockModules = await utils.hasMockModule(this, realApiName)
if (hasMockModules) {
const mockModule = utils.getMockModule(this, realApiName)
if (mockModule.enable && mockModule.mockFn) {
let mockResult = await mockModule.mockFn(options.data || {})
mockResult.mockTips = "请注意,这个是本地mock的假数据"
return mockResult
}
}
}
// 根绝参数得到真实得内网请求地址
const apiPath = utils.getApi(this.ctx, apiName, type, options)
const result = await this.ctx.curl(apiPath, {
method: "POST",
dataType: "json",
...options
})
// result 包装过程省略...
}
// 中间件权限的拦截
const utils = require("../lib/utils")
const WHITE_API = ["/api/loginAccount", "/api/loginForDingDing", "/api/baseInfo", "/api/loginOut"]
module.exports = () => {
return async function(ctx, next) {
if (ctx.request.path.indexOf("/api/") > 0 && WHITE_API.indexOf(ctx.request.path) === -1) {
let { user = {}, userIp = "" } = ctx.session || {}
const currentUserIp = ctx.ips.length > 0 ? ctx.ips[ctx.ips.length - 1] : ctx.ip
const sameUserIp = currentUserIp === userIp
if (Object.keys(user).length && sameUserIp) {
let { user_info = {}, super_admin = false } = user
let { __gateway_method_id = "", __gateway_place = "" } = ctx.request.query
let apiKey = utils.getApiKey(ctx, __gateway_method_id, __gateway_place)
if (apiKey) {
if (
((user_info.menus && user_info.menus.indexOf(apiKey)) !== -1 || super_admin) &&
user_info.status !== 2
) {
await next()
} else {
ctx.body = {
code: 4001,
msg: "你没有该接口的操作使用权限"
}
}
} else {
await next()
}
} else {
ctx.status = 401
ctx.logger.warn("401权限拦截", `接口地址:${ctx.request.url}`)
ctx.session = null
ctx.body = { code: 401, msg: "你有权限嘛?就想访问!" }
}
} else {
await next()
}
}
}
权限key输出到html入口文件 (njk模板)
<!DOCTYPE html>
<html lang="cn">
<head>
<title>{{title}}</title>
{% for key, item in meta -%}
<meta {{item.key}}="{{key}}" content="{{item.value}}">
{%- endfor %}
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon"/>
<!--当前的访问的终端:{{userAgent}}-->
{% for item in cdn.js -%}
<link href="{{item}}" rel="preload" as="script">
{%- endfor %}
{% if env !== "local" %}
{% for item in preload.js -%}
<link href="{{item}}" rel="prefetch" as="script">
{%- endfor %}
{% for item in preload.css -%}
<link rel="prefetch" href="{{item}}" as="style">
{%- endfor %}
{% for item in preload.font -%}
<link rel="preload" crossorigin as="font" href="{{item}}">
{%- endfor %}
{% endif %}
{% for item in cdn.css -%}
<link rel="stylesheet" href="{{item}}" />
{%- endfor %}
{% for item in asset.css -%}
<link rel="stylesheet" href="{{item}}" />
{%- endfor %}
</head>
<body>
<div id="app"></div>
<script></script>
{% for item in cdn.js -%}
<script src="{{item}}"></script>
{%- endfor %}
{% for item in asset.js -%}
<script src="{{item}}"></script>
{%- endfor %}
</body>
</html>
在有时间,有可用服务器的资源情况下建议大家去折腾一波。node写内部管理后台是很好的解决方案,前后端都是js写起来多舒服鸭!
egg 的启动和停止的钉钉通知
刚开始使用egg的时候,觉得每次停服更新还要通知他人也挺不方便的,干脆就拉个群,监控,接口问题都自动发送钉钉群通知。但是通知会有个一个小问题,就是egg再启动时默认会根据cpu数来启动对应数量的worker,会导致钉钉通知发送多次,针对这个问题只需要在启动的时候随便记录一个pid,然后再停止的时候指定worker执行发送信息就可以了
gitlab 钩子 + Jenkins 构建部署还是很省心了