本章讲解 fmode-ng 中的用户管理功能,包括登录、注册、会话管理、路由守卫和权限控制。用户系统是大多数应用的基础,理解本章内容对于构建完整的业务应用至关重要。
Parse Server 内置了完整的用户管理系统,核心是 _User 表。每个用户自动包含以下字段:
| 字段 | 类型 | 说明 |
|---|---|---|
objectId |
string | 用户唯一 ID |
username |
string | 用户名(唯一) |
email |
string | 邮箱(可选,唯一) |
sessionToken |
string | 登录会话令牌 |
createdAt |
Date | 注册时间 |
updatedAt |
Date | 最后更新时间 |
重要:密码字段
password在数据库中是加密存储的,永远不会在查询结果中返回。
┌──────────┐ ┌──────────────┐ ┌──────────────┐
│ 用户 │ │ 前端应用 │ │ Parse Server │
│ 输入 │ │ (Angular) │ │ (后端) │
└────┬─────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
│ 用户名+密码 │ │
│──────────────────>│ │
│ │ Cloud.run │
│ │ ('user_login') │
│ │────────────────────>│
│ │ │ 验证密码
│ │ │ 生成 sessionToken
│ │ sessionToken │
│ │<────────────────────│
│ │ │
│ │ User.become(token) │
│ │ → 设置登录态 │
│ │ │
│ 登录成功 │ │
│<──────────────────│ │
项目中推荐使用云函数 user_login 处理登录逻辑,可以在服务端进行额外的安全验证(如 RSA 解密、登录日志等):
import { MSQParse } from '../../app/parse';
async function login(username: string, password: string): Promise<boolean> {
try {
// 1. 调用登录云函数
const response = await MSQParse.Cloud.run('user_login', {
username: username,
password: password
});
// 2. 检查返回结果
if (response.success && response.sessionToken) {
// 3. 用 sessionToken 设置前端登录态
await MSQParse.User.become(response.sessionToken);
console.log('登录成功!当前用户:', MSQParse.User.current());
return true;
} else {
console.error('登录失败:', response?.message);
return false;
}
} catch (error: any) {
console.error('登录请求失败:', error.message);
return false;
}
}
直接调用 Parse Server 的 REST 登录接口:
const PARSE_URL = 'https://server-msq.fmode.cn/parse';
const APP_ID = 'msq-voc';
async function loginViaREST(username: string, password: string): Promise<boolean> {
const resp = await fetch(`${PARSE_URL}/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Parse-Application-Id': APP_ID
},
body: JSON.stringify({ username, password })
});
const result = await resp.json();
if (result.sessionToken) {
// 在 SDK 中设置登录态
await MSQParse.User.become(result.sessionToken);
return true;
}
console.error('登录失败:', result.error || result);
return false;
}
在生产环境中,密码不应以明文传输。项目中使用 RSA 公钥加密:
import JSEncrypt from 'jsencrypt';
// RSA 公钥(由服务端提供,私钥保留在服务端)
const RSA_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6NxiJAxa...
-----END PUBLIC KEY-----`;
function encryptRSA(text: string): string {
const encryptor = new JSEncrypt();
encryptor.setPublicKey(RSA_PUBLIC_KEY);
const encrypted = encryptor.encrypt(text);
if (!encrypted) {
throw new Error('RSA 加密失败,请检查公钥格式');
}
return encrypted;
}
// 使用加密后的数据登录
async function secureLogin(username: string, password: string) {
const encryptedUsername = encryptRSA(username);
const encryptedPassword = encryptRSA(password);
const response = await MSQParse.Cloud.run('user_login', {
username: encryptedUsername,
password: encryptedPassword
});
if (response.success && response.sessionToken) {
await MSQParse.User.become(response.sessionToken);
return true;
}
return false;
}
npm install jsencrypt
become() 是登录流程中最关键的方法。它告诉 SDK:「使用这个 sessionToken 作为当前登录用户」。
// 调用后,SDK 内部会:
// 1. 用 sessionToken 向 Parse Server 验证有效性
// 2. 获取用户完整信息
// 3. 在 localStorage 中保存登录态
// 4. 后续所有请求自动带上该用户身份
await MSQParse.User.become('r:abc123sessionToken');
为什么不用 User.logIn()?
Cloud.run('user_login') 不会自动在前端设置登录态sessionToken,需要手动调用 become() 来同步状态logIn() 会直接向 Parse Server 发起登录请求,不经过云函数中的自定义逻辑获取当前登录的用户对象:
const user = MSQParse.User.current();
if (user) {
// 已登录
console.log('用户 ID:', user.id); // objectId
console.log('用户名:', user.get('username')); // username
console.log('Session:', user.getSessionToken()); // sessionToken
console.log('创建时间:', user.get('createdAt')); // 注册时间
// 获取自定义字段
console.log('角色:', user.get('role'));
console.log('昵称:', user.get('nickname'));
} else {
// 未登录
console.log('用户未登录');
}
注意:
User.current()可能返回 Promise(取决于 SDK 版本),在 async 函数中建议加await:> const user = await MSQParse.User.current(); > ``` ### 4.3.3 User.logOut() 退出登录,清除本地的登录态:typescript async function logout() { try {
// 清理自定义的 token(如 AI token) localStorage.removeItem('FMODE_AI_TOKEN');
// 退出 Parse 登录
await MSQParse.User.logOut();
console.log('已退出登录');
// 跳转到登录页
this.router.navigate(['/login']);
} catch (error) {
console.error('退出登录失败:', error);
} }
**logOut 做了什么**:
1. 向 Parse Server 发送登出请求,使 sessionToken 失效
2. 清除 localStorage 中的用户信息
3. 清除 SDK 内部的用户缓存
---
## 4.4 获取用户信息的实用方法
### 获取用户 ID
typescript function getUserId(): string | null { const user = MSQParse.User.current(); if (user) {
return user.id || user.objectId || null;
} return null; }
### 获取 SessionToken
typescript function getSessionToken(): string | null { const user = MSQParse.User.current(); if (user && typeof user.getSessionToken === 'function') {
return user.getSessionToken() || null;
} return null; }
### 在 RxJS 中使用(Service 模式)
typescript import { from, Observable, of } from 'rxjs'; import { map, catchError, timeout } from 'rxjs/operators';
/**
/**
获取 sessionToken(Observable 版本) */ getSessionToken(): Observable { return from(Promise.resolve(MSQParse.User.current())).pipe( timeout(3000), map((user: any) => { if (user && typeof user.getSessionToken === 'function') {
return user.getSessionToken() || null;
} return null; }), catchError(() => of(null)) ); }
---
## 4.5 路由守卫 (AuthGuard)
路由守卫用于保护需要登录才能访问的页面。未登录的用户会被重定向到登录页。
### 完整的 AuthGuard 实现
typescript
// src/app/core/guards/auth.guard.ts import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router'; import { Observable } from 'rxjs'; import { MSQParse } from '../../parse';
@Injectable({ providedIn: 'root' }) export class AuthGuard implements CanActivate {
constructor(private router: Router) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable | Promise | boolean | UrlTree {
return this.checkLogin(state.url);
}
/**
@param url 用户试图访问的 URL(用于登录后重定向) */ async checkLogin(url: string): Promise { try { const currentUser = await MSQParse.User.current();
if (currentUser) {
// 已登录 → 允许访问
return true;
} } catch (e) { console.error('Auth Check Error:', e); }
// 未登录 → 重定向到登录页,携带原始 URL return this.router.createUrlTree(['/login'], { queryParams: { returnUrl: url } }); } }
### 在路由中使用 AuthGuard
typescript
// src/app/app.routes.ts import { Routes } from '@angular/router'; import { AuthGuard } from './core/guards/auth.guard';
export const routes: Routes = [ // 登录页:不需要 guard { path: 'login', loadComponent: () => import('./modules/login/login.component') },
// 仪表盘:需要登录 {
path: 'dashboard',
canActivate: [AuthGuard],
loadComponent: () => import('./modules/dashboard/dashboard.component')
},
// 监控页:需要登录 {
path: 'monitoring',
canActivate: [AuthGuard],
loadChildren: () => import('./modules/monitoring/monitoring.routes')
},
// 默认重定向 { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, { path: '**', redirectTo: '/dashboard' } ];
### 登录后重定向回原始页面
typescript // login.component.ts export class LoginComponent { returnUrl = '/dashboard';
constructor(private router: Router, private route: ActivatedRoute) {
// 从 URL 参数中获取原始页面地址
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/dashboard';
}
async onSubmit() {
const success = await this.login(this.username, this.password);
if (success) {
// 登录成功后跳转回原始页面
this.router.navigateByUrl(this.returnUrl);
}
} }
---
## 4.6 用户修改密码
typescript async changePassword(oldPassword: string, newPassword: string): Promise { try {
const user = await MSQParse.User.current();
if (!user) {
console.error('用户未登录');
return false;
}
// 通过云函数修改密码(推荐,可以在服务端验证旧密码)
const result = await MSQParse.Cloud.run('change_password', {
userId: user.id,
oldPassword: oldPassword,
newPassword: newPassword
});
if (result.success) {
console.log('密码修改成功');
// 修改密码后可能需要重新登录
return true;
}
return false;
} catch (err: any) {
console.error('修改密码失败:', err.message);
return false;
} }
---
## 4.7 完整的登录组件示例
以下是项目中实际使用的登录组件核心逻辑:
typescript import { Component } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router, ActivatedRoute } from '@angular/router'; import { MSQParse } from '../../app/parse';
@Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.scss'] }) export class LoginComponent { loginForm: FormGroup; isLoading = false; errorMessage = ''; returnUrl = '/dashboard';
constructor(
private fb: FormBuilder,
private router: Router,
private route: ActivatedRoute
) {
this.loginForm = this.fb.group({
username: ['', [Validators.required, Validators.minLength(3)]],
password: ['', [Validators.required, Validators.minLength(6)]]
});
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/dashboard';
}
async onSubmit(): Promise {
if (!this.loginForm.valid) {
this.loginForm.markAllAsTouched();
return;
}
this.isLoading = true;
this.errorMessage = '';
const { username, password } = this.loginForm.value;
try {
// 1. 调用登录云函数
const response = await MSQParse.Cloud.run('user_login', {
username,
password
});
if (response.success && response.sessionToken) {
// 2. 设置前端登录态
await MSQParse.User.become(response.sessionToken);
console.log('当前登录用户:', MSQParse.User.current());
// 3. 同步 AI 鉴权(可选,详见第七章)
await this.ensureNovaAuth(username, password);
// 4. 跳转
this.router.navigateByUrl(this.returnUrl);
} else {
this.errorMessage = response?.message || '登录失败,请检查账号密码';
}
} catch (error: any) {
this.errorMessage = error?.message || '登录请求失败,请重试';
} finally {
this.isLoading = false;
}
}
/**
AI API 需要 Nova(ncloudmaster) 的 token,与 MSQ 用户不互通 */ private async ensureNovaAuth(username: string, password: string): Promise { const NOVA_URL = 'https://server.fmode.cn/parse'; const headers = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': 'ncloudmaster' };
try {
// 尝试登录 Nova
const loginResp = await fetch(${NOVA_URL}/login, {
method: 'POST', headers,
body: JSON.stringify({ username, password })
}); const loginResult = await loginResp.json();
if (loginResult.sessionToken) {
localStorage.setItem('FMODE_AI_TOKEN', loginResult.sessionToken);
return;
}
// 用户不存在 → 自动注册 if (loginResult.code === 101) {
const signupResp = await fetch(`${NOVA_URL}/users`, {
method: 'POST', headers,
body: JSON.stringify({ username, password })
});
const signupResult = await signupResp.json();
if (signupResult.sessionToken) {
localStorage.setItem('FMODE_AI_TOKEN', signupResult.sessionToken);
}
} } catch (err) { console.warn('[AI Auth] 鉴权异常:', err); } } } ```
Parse SDK 自动将 sessionToken 保存在 localStorage 中。这意味着:
logOut() 或 token 过期后才会失去登录态由于 sessionToken 存储在 localStorage 中,同一浏览器的多个标签页共享登录状态:
| Token | 存储位置 | 用途 | 清理时机 |
|---|---|---|---|
| Parse sessionToken | localStorage (SDK 管理) | MSQ 业务操作 | User.logOut() |
| FMODE_AI_TOKEN | localStorage (手动管理) | AI API 鉴权 | 手动 removeItem |
本章你学到了:
User.become()、User.current()、User.logOut()上一章:第三章 数据操作 — 增删改查
下一章:第五章 云函数编写与部署