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,
})
// 请求拦截器:携带 Token 和系统编码
request.interceptors.request.use(config => {
const token = localStorage.getItem('token')
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) {
localStorage.removeItem('token')
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 = localStorage.getItem('token')
if (!token && to.path !== '/login') {
// 无 Token → 跳统一登录页
window.location.href = `${LOGIN_URL}?systemCode=${SYSTEM_CODE}`
return
}
next()
})
export default router
统一登录成功后,网关会将 Token 写入 localStorage 并跳回业务前端。业务前端无需处理登录请求,只需从 localStorage 读取 Token。
// main.ts 或 App.vue onMounted
const token = localStorage.getItem('token')
if (token) {
// 用户已登录,正常渲染应用
// 如需获取用户信息,调用网关 API:
const res = await request.get('/zgapi/v1/auth/user/info')
console.log('当前用户:', res.data)
}
重要:这些请求头由网关注入,业务后端可以信任这些值。不需要再验证 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 绑定系统和角色即可。
对外开放 API 使用一次性 Token 认证,可用于外部系统上传文件到文件对象存储、或从存储中下载文件。 每个 Token 12 小时内有效,只能使用一次(调用任意接口即消费)。
请求体:
{
"systemCode": "{{ sys?.systemCode || 'YOUR_CODE' }}"
}响应:
{
"code": 200,
"data": {
"token": "eyJhbGciOiJIUzM4NCJ9...",
"expiresIn": 43200,
"systemName": "业务系统名称",
"usageNote": "此 Token 只能消费一次,消费后立即失效"
}
}请求体:
{
"token": "eyJhbGciOiJIUzM4NCJ9..."
}成功响应(Token 有效,未被消费):
{
"code": 200,
"data": {
"valid": true,
"systemCode": "gateway",
"msg": "Token 有效,未被消费"
}
}⚠ 此接口不会消费 Token,校验通过后 Token 仍可用于后续操作。
请求体:
{
"token": "eyJhbGciOiJIUzM4NCJ9...",
"action": "上传文件"
}参数说明:
成功响应:
{
"code": 200,
"data": {
"tokenConsumed": true,
"action": "上传文件",
"msg": "Token 已标记为已消费"
}
}适用场景:外部系统在自己的业务中消费了 Token(如上传到自有存储),调用此接口告知网关防止重复使用。
curl 示例:
# 获取 Token
TOKEN=$(curl -s -X POST "{{ gatewayUrl }}/zgapi/v1/open/token" \
-H "Content-Type: application/json" \
-d '{"systemCode":"{{ sys?.systemCode || 'YOUR_CODE' }}"}' | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
# 上传文件(Token 作为 form 字段)
curl -X POST "{{ gatewayUrl }}/zgapi/v1/open/file/upload" \
-F "file=@/path/to/file.png" \
-F "token=$TOKEN" \
-F "fileType=other"成功响应(Token 已消费):
{
"code": 200,
"data": {
"fileId": "a1b2c3d4e5f6",
"fileName": "file.png",
"storageFileName": "a1b2c3d4e5f6.png",
"fileSize": 51200,
"tokenConsumed": true
}
}?token=xxx(不能用 Header,浏览器 <a> 标签下载时无法自定义 Header)
curl 示例:
# 下载文件(Token 通过 query 参数传递)
curl -o downloaded.png \
"{{ gatewayUrl }}/zgapi/v1/open/file/a1b2c3d4e5f6/download?token=$TOKEN"成功响应:直接返回文件二进制流,Content-Disposition: attachment 触发浏览器下载。
错误响应:
请求体:
{
"to": "user@example.com",
"subject": "邮件主题",
"content": "<h1>邮件内容</h1>",
"contentType": "html"
}curl 示例:
curl -X POST "{{ gatewayUrl }}/zgapi/v1/open/email/send" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"to":"user@example.com","subject":"通知","content":"<h1>你好</h1>","contentType":"html"}'成功响应:
{
"code": 200,
"data": {
"msg": "发送成功",
"tokenConsumed": true
}
}1. 每个 Token 只能使用一次,调用任意开放接口(上传/下载/发邮件)即消费。
2. 调用失败不消费 Token,可重试(如邮件发送失败、文件写入失败)。
3. 文件上传大小限制为 2GB,超时时间 12 小时。
4. 下载链接中的 Token 通过 URL 明文传递,请勿在公开页面中暴露。
5. 上传成功后返回 fileId,请妥善保存用于后续下载。