登录与认证
本文说明 tio-boot-admin 与 tio-boot-admin-react(React + Umi)的登录联调约定。接口行为以本地后端真实 HTTP 请求的验证结果为依据;Java 示例结合仓库已有 Handler 示例整理,不替代项目已注册的内置处理器。
统一响应体让前端可以集中处理业务结果,Aop.get 复用业务服务,TioRequestContext 提供当前请求的响应和已认证身份。后台管理员与 AppUser 应用用户分别使用各自的接口和令牌。
1. 接口一览
JSON 请求使用 Content-Type: application/json。以下地址均不附加上下文前缀;/api/ 是接口路径的一部分,需要完整转发到后端。
| 体系 | 方法与路径 | 请求体字段 | 返回与认证说明 |
|---|---|---|---|
| 后台管理员 | POST /api/login/account | username、password、type;账号登录传 type: "account" | 成功返回登录数据;凭据错误也返回 HTTP 200 |
| 后台管理员 | POST /api/login/outLogin | 无需业务请求体 | 发送当前令牌,成功返回 data: null |
| 后台管理员 | GET /api/currentUser | 无 | 不带令牌返回 HTTP 401;合法令牌返回当前用户信息 |
| AppUser | POST /api/v1/login | username 或 email,以及 password | AppUserLoginHandler 提供登录;账号字段来自现有文档示例 |
| AppUser | POST /api/v1/logout | 无需业务请求体 | AppUserLoginHandler 提供退出,携带应用用户令牌 |
| AppUser | GET /api/v1/user/profile | 无 | AppUserHandler 提供资料读取,携带应用用户令牌 |
| AppUser | POST /api/v1/user/update | 资料字段对象,字段名和允许更新范围须按实际部署确认 | AppUserHandler 提供资料更新,携带应用用户令牌 |
AppUser 的详细请求字段可结合应用用户登录示例核对。本次实测事实未给出资料更新的完整字段契约,因此这里不将 displayName 等登录响应字段直接当作更新字段。
1.1 后台登录
请求示例,密码替换为手动初始化的账号密码:
{
"username": "admin",
"password": "填写你设置的密码",
"type": "account"
}
成功时服务端调用 RespBodyVo.ok(kv)。下面的 ID、令牌、时间是结构示意,不是可复用的凭据:
{
"data": {
"userId": 1,
"token": "示例令牌",
"tokenTimeout": 1893456000,
"type": "account",
"status": "ok"
},
"msg": null,
"ok": true,
"code": 1,
"error": null
}
凭据错误时 HTTP 状态仍为 200,响应如下:
{
"data": { "status": "false" },
"msg": null,
"ok": false,
"code": 0,
"error": null
}
data.status 中的 "false" 是字符串,不能用 if (data.status) 判断登录成功。后台登录必须同时判断 ok === true 与 data.status === "ok",再从 data.token 读取令牌。
1.2 退出与当前用户
POST /api/login/outLogin 成功响应:
{
"data": null,
"msg": null,
"ok": true,
"code": 1,
"error": null
}
GET /api/currentUser 未携带令牌时返回 HTTP 401。携带合法后台令牌时可读取当前用户信息;具体资料字段以实际响应为准,不要把登录数据结构当作当前用户结构。401 的完整响应体未在本次验证材料中给出,前端应首先按 HTTP 状态处理。
1.3 AppUser 登录
可使用用户名登录,或将 username 替换为 email。以下请求字段参考现有 AppUserLoginRequest 示例:
{
"username": "demo",
"password": "填写应用用户密码"
}
AppUser 登录成功同样使用 RespBodyVo,登录数据结构示意如下:
{
"code": 1,
"ok": true,
"msg": null,
"data": {
"userId": "示例用户ID",
"displayName": "示例用户",
"email": "[email protected]",
"token": "示例应用用户令牌",
"refreshToken": "示例刷新令牌",
"tokenTimeout": 1893456000
},
"error": null
}
这些字段属于登录数据,不代表退出、资料读取、资料更新都返回相同内容。AppUser 登录不要求后台专用的 data.status === "ok" 条件。现有登录设计对账号不存在和密码错误统一返回失败提示,前端同样显示“账号或密码错误”,避免透露账号是否已注册。
2. RespBodyVo 与前端判定
| 字段 | 含义与读取方式 |
|---|---|
code | 业务状态码,约定 1 表示成功;不能替代 HTTP 状态码 |
ok | 业务成功标志,成功为布尔值 true |
msg | 消息,可为 null;前端需要默认提示 |
data | 业务数据,可为对象或 null;令牌位于 data.token |
error | 错误信息,可为 null;不应未经处理直接展示内部细节 |
按顺序检查 HTTP 状态、业务成功标志和接口专用字段。后台登录可以额外校验 code === 1 和令牌是否非空;不要检查不存在的 success 字段,也不要从响应顶层读取 token。
登录页完成令牌保存后,再请求当前用户并更新 Umi 的用户状态。退出时向服务端发送当前令牌,完成后清理浏览器令牌和用户状态。仅清理浏览器存储不等于服务端已成功退出。
3. Authorization 令牌传递
服务端兼容以下两种请求头,按空白分隔后取最后一段作为令牌:
Authorization: Bearer your-token
Authorization: your-token
前端统一使用 Bearer 形式即可。后台接口发送后台令牌,AppUser 接口发送应用用户令牌;不要混用。tokenTimeout 的单位和续期策略应与实际服务端实现保持一致,服务端认证结果是最终依据;AppUser 返回 refreshToken 并不表示前端已经实现自动刷新。
4. 服务端 Handler 示例
内置配置已经注册登录接口时,直接使用现有接口,无需重复注册。下面的扩展示例演示如何复用内置后台登录 Handler,以及如何通过 Aop.get 调用 AppUser 服务、使用 Kv 组织数据和 RespBodyVo 包装响应。appProfile 是自定义资料投影示例,不是内置 profile 的完整响应契约。
package demo.auth;
import com.jfinal.kit.Kv;
import nexus.io.jfinal.aop.Aop;
import nexus.io.model.body.RespBodyVo;
import nexus.io.tio.boot.admin.handler.ApiLoginHandler;
import nexus.io.tio.boot.admin.services.AppUserService;
import nexus.io.tio.boot.admin.vo.AppUser;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
public class LoginExampleHandler {
private final ApiLoginHandler adminLogin = new ApiLoginHandler();
public HttpResponse account(HttpRequest request) {
// 复用内置密码校验、令牌签发和 RespBodyVo 响应。
return adminLogin.account(request);
}
public HttpResponse outLogin(HttpRequest request) {
return adminLogin.outLogin(request);
}
public HttpResponse appProfile(HttpRequest request) {
// 此方法须由 AppUser 鉴权拦截器保护,身份来自已验证的上下文。
String userId = TioRequestContext.getUserIdString();
HttpResponse response = TioRequestContext.getResponse();
if (userId == null) {
return response.setStatus(401).setJson(RespBodyVo.fail("请先登录"));
}
AppUserService service = Aop.get(AppUserService.class);
AppUser user = service.getUserById(userId);
if (user == null) {
return response.setStatus(401).setJson(RespBodyVo.fail("请重新登录"));
}
Kv data = Kv.by("userId", user.getId())
.set("displayName", user.getDisplayName())
.set("email", user.getEmail());
return response.setJson(RespBodyVo.ok(data));
}
}
该示例依赖应用已完成框架配置和数据库初始化。Handler 由路由配置创建,业务服务通过 Aop 获取。不要通过请求体中的 userId 确定当前用户,也不要仅解析 Authorization 就当作认证成功。鉴权注册方式见方法路由与业务鉴权。
5. React + Umi 调用示例
以下服务模块使用项目已有的 @umijs/max 请求工具,保存到前端的 services/auth.ts 后供登录页调用。请求返回值应保留完整 RespBodyVo;若项目已有统一解包或错误处理,需要按同一契约调整,避免重复解包。
import { request } from '@umijs/max';
interface RespBody<T> {
code: number;
ok: boolean;
msg: string | null;
data: T | null;
error: unknown;
}
interface AdminLoginData {
userId?: string | number;
token?: string;
tokenTimeout?: number;
type?: string;
status: string;
}
const tokenKey = 'tio-admin-token';
function authHeaders(): Record<string, string> {
const token = sessionStorage.getItem(tokenKey);
return token ? { Authorization: `Bearer ${token}` } : {};
}
export async function login(username: string, password: string) {
sessionStorage.removeItem(tokenKey);
const result = await request<RespBody<AdminLoginData>>('/api/login/account', {
method: 'POST',
data: { username, password, type: 'account' },
skipErrorHandler: true,
});
if (result.ok !== true || result.code !== 1 || result.data?.status !== 'ok') {
throw new Error('账号或密码错误');
}
const token = result.data.token;
if (typeof token !== 'string' || token.trim() === '') {
throw new Error('登录响应缺少令牌,请重试');
}
sessionStorage.setItem(tokenKey, token);
return result.data;
}
export async function currentUser() {
try {
const result = await request<RespBody<Record<string, unknown>>>('/api/currentUser', {
method: 'GET',
headers: authHeaders(),
skipErrorHandler: true,
});
if (result.ok !== true || result.code !== 1 || result.data === null) {
throw new Error('读取当前用户失败');
}
return result.data;
} catch (error) {
const status = (error as { response?: { status?: number } }).response?.status;
if (status === 401) {
sessionStorage.removeItem(tokenKey);
throw new Error('请重新登录');
}
throw error;
}
}
export async function logout() {
try {
const result = await request<RespBody<null>>('/api/login/outLogin', {
method: 'POST',
headers: authHeaders(),
skipErrorHandler: true,
});
if (result.ok !== true || result.code !== 1) {
throw new Error('服务端退出未成功,请重试');
}
} finally {
sessionStorage.removeItem(tokenKey);
}
}
这里使用 sessionStorage 保存当前标签页会话;已有项目使用其他存储时,应统一存取位置。不要在日志中输出密码或令牌。调用方捕获网络异常、401 和业务失败后显示对应提示,退出失败时不要显示“服务端退出成功”。
登录页提交函数示例(放入已有 React 登录页,替换原提交逻辑):
import { message } from 'antd';
import { login, currentUser } from '@/services/auth';
export async function submitLogin(values: { username: string; password: string }) {
try {
await login(values.username, values.password);
const user = await currentUser();
message.success('登录成功');
// 调用方将 user 写入已有 Umi initialState,再跳转到后台首页。
return user;
} catch (error) {
message.error(error instanceof Error ? error.message : '请求失败,请稍后重试');
return null;
}
}
6. 开发代理与联调
在前端实际启用的 Umi 配置中合并下面的代理配置。示例假定后端监听 8100,实际端口不同时替换 target。不要覆盖项目已有的其他配置项。
import { defineConfig } from '@umijs/max';
export default defineConfig({
proxy: {
'/api/': {
target: 'http://127.0.0.1:8100',
changeOrigin: true,
},
},
});
若项目将代理放在 proxy.ts 的 dev 环境下,应把上面的 /api/ 规则合并到 dev,并确认主配置和启动环境实际选中了它。修改后重启前端开发服务。这里不添加 pathRewrite,请求 /api/login/account 到达后端时仍为 /api/login/account,不添加额外应用名前缀。
| 现象 | 检查与处理 |
|---|---|
前端 8000 端口请求 /api/... 返回 404 | 先检查 dev 代理是否启用、后端端口是否正确、代理是否保留 /api/;对比直接请求后端的结果 |
| 登录 HTTP 200,但页面未进入后台 | 检查 ok 和 data.status;凭据错误属于业务失败,不应仅依赖 HTTP 200 |
| 登录返回成功,后续请求仍为 401 | 检查是否从 data.token 取值,Authorization 是否发送,以及后台/AppUser 令牌是否混用 |
未携带令牌访问 /api/currentUser 返回 401 | 这是该受保护接口的预期行为,先登录再携带合法令牌重试 |
| 登录失败时没有提示文字 | msg 可能为 null,使用“账号或密码错误”作为统一提示 |
| 退出后页面仍显示用户信息 | 同时清理令牌、Umi 当前用户状态及相关页面缓存 |
可按“失败登录 → 成功登录 → 不带令牌读取当前用户 → 分别携带 Bearer 与裸令牌读取 → 退出”的顺序验证。后台退出后的旧令牌失效时机、AppUser 刷新流程和资料更新字段未包含在本次实测材料中,需要结合部署实现补充验证。
7. 必需配置:后台令牌签名密钥
后台登录在通过账号密码校验之后需要签发令牌,签名密钥取自配置项 app.admin.secret.key。请在本地秘密配置(例如应用启动工作目录下的 .env)中提供:
app.admin.secret.key=replace-with-your-own-random-secret
该密钥同时用于后台令牌的签发与校验:多个实例使用同一密钥时签发的令牌可以互相识别;更换密钥后已签发的旧令牌立即失效。密钥只放在本地配置或环境变量中,不要写入源码、示例与文档。
未配置或配置为空白时的表现:登录请求会在签发令牌阶段失败并返回 500,服务端日志出现 Failed to calculate HMAC SHA256。框架在读取该配置时就会直接抛出具名错误 app.admin.secret.key is not configured,便于在联调早期定位到是配置缺失,而不是把它误判成账号或密码问题。
