Mokee Gateway 统一登录网关 · 系统对接完整指南
{{ sys?.systemCode }}{{ sys?.frontUrl }}{{ sys?.backendUrl }}┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ 业务前端 │────▶│ Mokee Gateway │────▶│ 业务后端 │ │ (Vue/React) │ │ gw.server.zgitm.com │ │ (任意语言) │ │ │ │ │ │ │ │ 携带 Token │ │ 验证 Token │ │ 从请求头读取: │ │ + SystemCode │ │ 识别系统编码 │ │ X-User-Id │ │ │ │ 转发请求 │ │ X-User-Name │ │ │ │ 注入用户信息头 │ │ X-System-Code │ └──────────────┘ └──────────────────┘ └──────────────────┘
业务前端的 baseURL 必须配置为 网关地址(如 {{ gatewayUrl }}),绝对不能直接调用业务后端。网关根据请求头 X-System-Code 自动识别并转发到对应后端。
业务后端需要允许来自网关域名的跨域请求。网关转发请求时 origin 是浏览器地址,不是网关地址。
Java Spring Boot 配置:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true);
}
}Token 验证由网关完成。业务后端只需从请求头读取用户信息,不需要再验证 Token、不需要查询用户表。
npm install axios
VITE_GATEWAY_URL = {{ gatewayUrl }}
VITE_LOGIN_URL = {{ loginUrl }}
VITE_SYSTEM_CODE = {{ sys?.systemCode || 'YOUR_CODE' }}
import axios from 'axios'
import { ElMessage } from 'element-plus'
const SYSTEM_CODE = import.meta.env.VITE_SYSTEM_CODE
const request = axios.create({
baseURL: import.meta.env.VITE_GATEWAY_URL,
timeout: 30000,
})
// 从 Cookie 读取 Token(登录后网关写入的跨域 Cookie)
function getToken(): string | null {
const match = document.cookie.match(/(?:^|;\\s*)token=([^;]*)/)
return match ? decodeURIComponent(match[1]) : null
}
// 请求拦截器:携带 Token 和系统编码
request.interceptors.request.use(config => {
const token = getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
// 关键:网关根据此头识别业务系统
config.headers['X-System-Code'] = SYSTEM_CODE
return config
})
// 响应拦截器:401 自动跳转登录
request.interceptors.response.use(
res => res.data.code === 200 ? res.data : Promise.reject(res.data),
err => {
if (err.response?.status === 401) {
window.location.href = `${import.meta.env.VITE_LOGIN_URL}?systemCode=${SYSTEM_CODE}`
}
return Promise.reject(err)
}
)
export default request
import { createRouter, createWebHistory } from 'vue-router'
const SYSTEM_CODE = import.meta.env.VITE_SYSTEM_CODE
const LOGIN_URL = import.meta.env.VITE_LOGIN_URL
const router = createRouter({
history: createWebHistory(),
routes: [ /* 你的路由 */ ],
})
router.beforeEach((to, _from, next) => {
const token = document.cookie.match(/(?:^|;\\s*)token=([^;]*)/)?.[1]
if (!token && to.path !== '/login') {
// 无 Token → 跳统一登录页
window.location.href = `${LOGIN_URL}?systemCode=${SYSTEM_CODE}`
return
}
next()
})
export default router
用户在统一登录页登录成功后,网关将 Token 写入 Cookie(domain: .zgitm.com,同域子站共享),
然后跳回业务前端的地址(即 VITE_LOGIN_URL 配置的地址)。
业务前端无需自行实现登录接口,只需从 Cookie 中读取 Token 即可。
// 从 Cookie 读取 Token(登录后网关自动写入的跨域 Cookie)
function getToken(): string | null {
const match = document.cookie.match(/(?:^|;\\s*)token=([^;]*)/)
return match ? decodeURIComponent(match[1]) : null
}
// App.vue onMounted 或 main.ts
const token = getToken()
if (token) {
// 用户已登录,正常渲染应用
// 如需获取用户信息,调用网关 API:
const res = await request.get('/zgapi/v1/auth/user/info')
console.log('当前用户:', res.data)
} else {
// 无 Token → 跳统一登录页
window.location.href = `${import.meta.env.VITE_LOGIN_URL}?systemCode=${SYSTEM_CODE}`
}
Token 存在 Cookie 中(而非 localStorage),因为 Cookie 支持跨子域共享(.zgitm.com),
这样统一登录、管理后台、文件服务等不同子站都能读取到同一个 Token。
Cookie 有效期 2 小时,与 Token 过期时间一致。
重要:这些请求头由网关注入,业务后端可以信任这些值。不需要再验证 Token 或查询用户信息。
用 ThreadLocal 缓存请求头,业务代码零侵入获取用户信息。
// ====== UserContext.java ======
import java.util.*;
public class UserContext {
private static final ThreadLocal<Map<String, String>> CTX = new ThreadLocal<>();
public static void set(Map<String, String> user) { CTX.set(user); }
public static Map<String, String> get() { return CTX.get(); }
public static void clear() { CTX.remove(); }
// 快捷方法
public static String getUserId() { return get().get("X-User-Id"); }
public static String getUserName() { return get().get("X-User-Name"); }
public static String getUserAccount(){ return get().get("X-User-Account"); }
public static String getSystemCode() { return get().get("X-System-Code"); }
public static String getSystemId() { return get().get("X-System-Id"); }
}
// ====== UserContextFilter.java ======
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.*;
@Component
public class UserContextFilter implements Filter {
private static final String[] HEADERS = {
"X-User-Id", "X-User-Name", "X-User-Account",
"X-User-Email", "X-User-Post", "X-User-Roles",
"X-System-Id", "X-System-Code", "X-System-Name"
};
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
Map<String, String> user = new HashMap<>();
for (String h : HEADERS) {
String v = request.getHeader(h);
if (v != null) user.put(h, v);
}
UserContext.set(user);
try {
chain.doFilter(req, res);
} finally {
UserContext.clear();
}
}
}
// ====== 业务代码中使用 ======
@RestController
public class OrderController {
@GetMapping("/api/orders")
public List<Order> list() {
String userId = UserContext.getUserId(); // "123"
String userName = UserContext.getUserName(); // "张三"
String systemCode = UserContext.getSystemCode(); // "msg"
return orderService.findByUser(userId, systemCode);
}
}
# ====== middleware.py ======
from flask import request, g
USER_HEADERS = [
'X-User-Id', 'X-User-Name', 'X-User-Account',
'X-User-Email', 'X-User-Post', 'X-User-Roles',
'X-System-Id', 'X-System-Code', 'X-System-Name',
]
def user_context_middleware():
"""将网关注入的用户信息存到 Flask g 对象"""
for h in USER_HEADERS:
val = request.headers.get(h)
if val:
setattr(g, h.replace('-', '_').lower(), val)
# 注册到 app (Flask)
# app.before_request(user_context_middleware)
# ====== 业务代码中使用 ======
from flask import g
@app.route('/api/orders')
def list_orders():
user_id = g.x_user_id # "123"
user_name = g.x_user_name # "张三"
system_code = g.x_system_code # "msg"
return order_service.query(user_id, system_code)
// ====== middleware.go ======
package middleware
import (
"context"
"net/http"
)
type contextKey string
func UserContext(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
for _, h := range []string{
"X-User-Id", "X-User-Name", "X-User-Account",
"X-User-Email", "X-User-Roles",
"X-System-Id", "X-System-Code",
} {
if v := r.Header.Get(h); v != "" {
ctx = context.WithValue(ctx, contextKey(h), v)
}
}
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// 快捷函数
func GetUserID(ctx context.Context) string {
return ctx.Value(contextKey("X-User-Id")).(string)
}
func GetSystemCode(ctx context.Context) string {
return ctx.Value(contextKey("X-System-Code")).(string)
}
// ====== main.go: 注册中间件 ======
// http.Handle("/api/", middleware.UserContext(yourHandler))
// ====== 业务代码 ======
func listOrders(w http.ResponseWriter, r *http.Request) {
userID := middleware.GetUserID(r.Context()) // "123"
systemCode := middleware.GetSystemCode(r.Context()) // "msg"
// ...
}
{
"code": 200,
"data": {
"userId": 1,
"username": "admin",
"realName": "管理员",
"email": "admin@zgitm.com",
"phone": "13800000000",
"avatar": null,
"post": "管理员"
}
}在网关平台的数据库执行以下 SQL,为「{{ sys?.systemName }}」创建默认菜单和角色。
{{ menuSql }}
业务系统接入后,系统管理员需要登录网关管理后台维护本系统的菜单和角色。
执行以下 SQL 创建一个应用系统管理员账号(角色编码 gateway-app-admin),该管理员只能看到自己绑定系统的数据。
{{ adminSql }}
密码需用 BCrypt 加密。可使用在线工具或 Java BCrypt.hashpw("your_password", BCrypt.gensalt()) 生成密文。
如果已创建过用户,跳过步骤 1,只执行步骤 2 和 3 绑定系统和角色即可。
文件上传/下载/删除由独立的文件对象存储服务提供,该服务直接对接网关数据库,上传后文件信息自动出现在文件列表中。
https://file.wg.zgitm.com请求体:
{
"systemCode": "{{ sys?.systemCode || 'YOUR_CODE' }}"
}响应:
{
"code": 200,
"data": {
"token": "eyJhbGciOiJIUzM4NCJ9...",
"expiresIn": 43200,
"systemName": "系统名称"
}
}curl 示例:
# 获取 Token
TOKEN=$(curl -s -X POST https://file.wg.zgitm.com/api/token \
-H "Content-Type: application/json" \
-d '{"systemCode":"{{ sys?.systemCode || 'YOUR_CODE' }}"}' | jq -r '.data.token')
# 上传
curl -X POST https://file.wg.zgitm.com/api/upload \
-H "Authorization: Bearer $TOKEN" \
-F "file=@/path/to/file.pdf"成功响应:
{
"code": 200,
"data": {
"fileId": "a1b2c3d4e5f67890...",
"fileName": "报告.pdf",
"fileSize": 1048576,
"uploadTime": "2026-06-13T10:30:00"
}
}
成功返回文件二进制流。
Token 两种传递方式:
① Header:Authorization: Bearer <token>
② Query 参数:/api/download/{fileId}?token=<token>(浏览器直链下载时使用)
删除会同时清理磁盘文件与数据库记录,不可恢复。
1. 每次操作前需重新获取 Token(一次一密)。
2. 文件最大 100MB,超时 12 小时。
3. 上传成功后文件信息自动写入网关数据库 sys_file 表。
4. 删除不可恢复,请谨慎操作。
以下接口通过一次性 Token 认证,Token 获取后 12 小时内有效,每次调用业务接口即消费。
请求体:
{
"systemCode": "{{ sys?.systemCode || 'YOUR_CODE' }}"
}响应:
{
"code": 200,
"data": {
"token": "eyJhbGciOiJIUzM4NCJ9...",
"expiresIn": 43200,
"systemName": "系统名称",
"usageNote": "此 Token 只能消费一次,消费后立即失效"
}
}请求体:
{
"to": "user@example.com",
"subject": "邮件主题",
"content": "<h1>邮件内容</h1>",
"contentType": "html"
}
参数说明:
to — 收件人邮箱(必填)
subject — 邮件主题(必填)
content — 邮件内容(必填,支持 HTML)
contentType — "html"(默认)或 "text"
curl 示例:
# 1. 获取 Token
TOKEN=$(curl -s -X POST https://gw.server.zgitm.com/zgapi/v1/open/token \
-H "Content-Type: application/json" \
-d '{"systemCode":"{{ sys?.systemCode || 'YOUR_CODE' }}"}' | jq -r '.data.token')
# 2. 发送邮件
curl -X POST https://gw.server.zgitm.com/zgapi/v1/open/email/send \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to": "user@example.com",
"subject": "测试邮件",
"content": "<h1>你好</h1><p>这是一封测试邮件</p>",
"contentType": "html"
}'成功响应:
{
"code": 200,
"data": {
"msg": "发送成功",
"tokenConsumed": true
}
}注意:邮件发送成功后才消费 Token;发送失败不消费 Token,可重试。
1. 每个 Token 只能使用一次,调用任意业务接口即消费。
2. 发送邮件前请先获取新 Token。
3. 发件人为 admin@zgitm.com(阿里企业邮箱)。
4. 编码统一使用 UTF-8。