1. 项目背景与核心需求
在HarmonyOS应用开发中,用户认证模块是几乎所有应用都需要的基础功能。华为AGConnect认证服务提供了开箱即用的解决方案,支持邮箱、手机号、第三方账号等多种认证方式。本文将以医疗行业的病房管理系统(EMR)为案例,深入解析邮箱验证码登录的完整实现方案。
为什么选择邮箱验证码登录?在医疗场景下,这种认证方式具有以下优势:
- 医护人员通常使用医院分配的专属邮箱
- 相比密码登录更安全,避免密码泄露风险
- 验证码有时效性,符合医疗系统的高安全要求
- 集成简单,减少开发团队在安全认证方面的投入
2. 环境配置与初始化
2.1 AGConnect服务开通
在开始编码前,需要完成以下准备工作:
- 登录华为开发者联盟控制台(https://developer.huawei.com/consumer/cn/)
- 进入"我的项目",选择或创建对应项目
- 在左侧导航栏选择"构建" → "认证服务"
- 点击"启用"按钮激活认证服务
- 在"认证方式"选项卡中开启"邮箱地址"认证
注意:每个项目需要单独启用认证服务,不同环境(开发/测试/生产)建议创建不同的项目配置
2.2 项目依赖配置
在HarmonyOS应用的oh-package.json5文件中添加必要依赖:
json复制{
"dependencies": {
"@hw-agconnect/auth": "^1.0.0",
"@hw-agconnect/cloud": "^1.0.0",
"@hw-agconnect/core": "^1.0.0"
}
}
这三个包分别提供:
- auth:认证相关功能
- cloud:云服务基础能力
- core:核心工具类
2.3 应用初始化
在应用入口文件EntryAbility.ets中进行AGConnect初始化:
typescript复制import { Ability } from '@kit.AbilityKit';
import cloud from '@hw-agconnect/cloud';
export default class EntryAbility extends Ability {
async onCreate(): Promise<void> {
// 必须传入正确的context
cloud.init(this.context);
// 可选:配置日志级别
cloud.setLogLevel(cloud.LogLevel.DEBUG);
}
}
3. 核心功能实现
3.1 页面状态管理
使用ArkUI的声明式开发模式定义登录页面状态:
typescript复制@Entry
@Component
struct LoginPage {
@State email: string = ''; // 用户邮箱
@State password: string = ''; // 用户密码
@State verifyCode: string = ''; // 验证码
@State countdown: number = 60; // 验证码倒计时
@State isCounting: boolean = false; // 是否正在倒计时
@State buttonText: string = '获取验证码'; // 按钮文字
private timer: number = 0; // 定时器ID
// 页面布局代码...
}
3.2 验证码获取实现
正确的验证码请求参数结构:
typescript复制private async requestVerifyCode() {
if (!this.validateEmail()) {
promptAction.showToast({ message: '请输入有效的邮箱地址' });
return;
}
try {
const params = {
verifyCodeType: {
kind: 'email',
email: this.email
},
action: cloud.VerifyCodeAction.REGISTER_LOGIN,
lang: 'zh_CN',
sendInterval: 60
};
await cloud.auth().requestVerifyCode(params);
this.startCountdown();
promptAction.showToast({ message: '验证码已发送' });
} catch (error) {
promptAction.showToast({ message: `发送失败: ${error.message}` });
}
}
private startCountdown() {
this.isCounting = true;
this.timer = setInterval(() => {
if (this.countdown <= 0) {
clearInterval(this.timer);
this.isCounting = false;
this.countdown = 60;
this.buttonText = '重新获取';
return;
}
this.countdown--;
this.buttonText = `${this.countdown}s`;
}, 1000);
}
3.3 用户注册流程
完整的邮箱注册实现:
typescript复制private async register() {
// 前端校验
if (!this.validateInputs()) {
return;
}
try {
const result = await cloud.auth().createUser({
kind: 'email',
email: this.email,
password: this.password,
verifyCode: this.verifyCode
});
if (result) {
// 注册成功后自动登录
const user = await cloud.auth().getCurrentUser();
await user?.updateProfile({
displayName: this.email.split('@')[0] // 默认使用邮箱前缀作为用户名
});
// 存储用户信息
AppStorage.setOrCreate('currentUser', user);
router.replaceUrl({ url: 'pages/Home' });
}
} catch (error) {
this.handleAuthError(error);
}
}
private validateInputs(): boolean {
// 邮箱格式校验
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(this.email)) {
promptAction.showToast({ message: '邮箱格式不正确' });
return false;
}
// 密码强度校验
if (this.password.length < 8) {
promptAction.showToast({ message: '密码至少8位' });
return false;
}
if (!/[A-Z]/.test(this.password) ||
!/[a-z]/.test(this.password) ||
!/[0-9]/.test(this.password)) {
promptAction.showToast({ message: '需包含大小写字母和数字' });
return false;
}
// 验证码校验
if (!this.verifyCode || this.verifyCode.length !== 6) {
promptAction.showToast({ message: '请输入6位验证码' });
return false;
}
return true;
}
3.4 用户登录实现
邮箱密码登录的核心逻辑:
typescript复制private async login() {
try {
// 清除已有会话
const currentUser = await cloud.auth().getCurrentUser();
if (currentUser) {
await cloud.auth().signOut();
}
// 执行登录
const result = await cloud.auth().signIn({
credentialInfo: {
kind: 'email',
email: this.email,
password: this.password
}
});
if (result) {
// 登录成功后跳转
const user = await cloud.auth().getCurrentUser();
AppStorage.setOrCreate('currentUser', user);
router.replaceUrl({ url: 'pages/Home' });
}
} catch (error) {
this.handleAuthError(error);
}
}
private handleAuthError(error: BusinessError) {
const errorMap: Record<number, string> = {
203818037: '该邮箱已被注册',
203818038: '用户不存在,请先注册',
203818049: '邮箱或密码错误',
203818129: '验证码错误或已过期'
};
const message = errorMap[error.code] || `操作失败: ${error.message}`;
promptAction.showToast({ message });
}
4. 深度踩坑解析
4.1 验证码接收失败问题
问题现象:
- 调用requestVerifyCode接口成功,但邮箱收不到验证码
- 控制台无任何错误日志
根本原因:
- 参数结构不符合AGConnect要求
- 邮箱服务配置未完成
- 邮箱地址未通过验证
解决方案:
- 确保参数格式正确:
typescript复制// 错误示例
{
email: 'user@example.com', // 直接放在顶层
action: VerifyCodeAction.REGISTER_LOGIN
}
// 正确示例
{
verifyCodeType: { // 必须嵌套
kind: 'email', // 必须指定类型
email: 'user@example.com'
},
action: VerifyCodeAction.REGISTER_LOGIN
}
-
在AGConnect控制台完成邮箱配置:
- 进入"认证服务" → "设置" → "邮件模板"
- 配置发件人邮箱和邮件模板
- 验证发件域名(重要!)
-
检查垃圾邮件箱,部分邮件服务商可能误判
4.2 密码策略导致的注册失败
常见错误:
- 错误码203818045:"password does not meet requirements"
华为云密码策略要求:
- 长度至少8字符
- 必须包含:
- 至少1个大写字母(A-Z)
- 至少1个小写字母(a-z)
- 至少1个数字(0-9)
- 建议但不强制:
- 包含特殊字符
- 避免常见密码组合
前端校验实现:
typescript复制function validatePassword(pwd: string): boolean {
const hasUpper = /[A-Z]/.test(pwd);
const hasLower = /[a-z]/.test(pwd);
const hasNumber = /[0-9]/.test(pwd);
return pwd.length >= 8 && hasUpper && hasLower && hasNumber;
}
4.3 异步操作未等待导致的问题
典型症状:
- 代码逻辑正确但功能不生效
- 随机性出现功能失效
- 控制台无错误日志
解决方案:
- 所有AGConnect API调用必须使用await:
typescript复制// 错误写法
cloud.auth().signIn(credentials); // 没有await
// 正确写法
await cloud.auth().signIn(credentials);
- 在调用链的顶层函数添加async:
typescript复制private async handleLogin() { // 必须声明async
try {
const user = await this.getUser();
await this.loadProfile(user);
} catch (error) {
// 错误处理
}
}
- 关键操作添加日志:
typescript复制console.debug('[Auth] Starting sign in process');
const result = await cloud.auth().signIn(credentials);
console.debug('[Auth] Sign in result:', result);
4.4 权限配置问题
常见权限错误:
-
媒体权限缺失:
- ohos.permission.READ_MEDIA
- ohos.permission.WRITE_MEDIA
-
网络权限缺失:
- ohos.permission.INTERNET
完整权限配置示例:
json复制// module.json5
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
"reason": "用于访问AGConnect服务"
},
{
"name": "ohos.permission.READ_MEDIA",
"reason": "读取用户头像",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.WRITE_MEDIA",
"reason": "保存用户头像",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
5. 高级功能扩展
5.1 自动登录与会话保持
实现用户二次打开应用时自动登录:
typescript复制async function checkAutoLogin() {
try {
const user = await cloud.auth().getCurrentUser();
if (user) {
// 检查token是否有效
const isValid = await user.isValid();
if (isValid) {
router.replaceUrl({ url: 'pages/Home' });
return;
}
}
// 无效会话跳转登录页
router.replaceUrl({ url: 'pages/Login' });
} catch (error) {
console.error('Auto login check failed:', error);
}
}
5.2 密码重置功能
通过邮箱验证码重置密码:
typescript复制async function resetPassword(email: string, newPassword: string, verifyCode: string) {
try {
await cloud.auth().resetPassword({
kind: 'email',
email,
newPassword,
verifyCode
});
promptAction.showToast({ message: '密码重置成功' });
return true;
} catch (error) {
handleAuthError(error);
return false;
}
}
5.3 用户信息管理
获取和更新用户个人信息:
typescript复制async function updateUserProfile(displayName: string, photoUrl: string) {
const user = await cloud.auth().getCurrentUser();
if (!user) {
throw new Error('用户未登录');
}
await user.updateProfile({
displayName,
photoUrl
});
// 刷新本地存储
AppStorage.setOrCreate('currentUser', user);
}
6. 性能优化与安全建议
6.1 性能优化方案
- 验证码缓存:
typescript复制// 使用Storage缓存验证码请求时间
const lastRequestTime = await Storage.get('lastVerifyCodeRequest');
if (lastRequestTime && Date.now() - lastRequestTime < 60000) {
promptAction.showToast({ message: '操作过于频繁' });
return;
}
// 请求成功后更新时间
await Storage.set('lastVerifyCodeRequest', Date.now());
- 请求合并:
- 将多个用户信息请求合并为单个批处理
- 使用Promise.all并行处理独立请求
- 本地缓存:
- 缓存用户基本信息
- 实现离线模式支持
6.2 安全增强措施
- 输入过滤:
typescript复制function sanitizeInput(input: string): string {
return input.replace(/[<>"'&]/g, '');
}
- 密码加密传输:
- 使用HTTPS协议
- 考虑前端加密(如SHA256)
- 会话监控:
typescript复制cloud.auth().onAuthStateChanged((user) => {
if (!user) {
// 会话过期处理
router.replaceUrl({ url: 'pages/Login' });
}
});
- 异常登录检测:
- 记录设备信息
- 实现异地登录提醒
7. 测试与调试技巧
7.1 单元测试要点
- 验证码请求测试:
typescript复制it('should request verify code successfully', async () => {
const email = 'test@example.com';
await authService.requestVerifyCode(email);
expect(cloud.auth().requestVerifyCode).toHaveBeenCalled();
});
- 登录功能测试:
typescript复制it('should login with valid credentials', async () => {
const mockUser = { uid: 'test123' };
jest.spyOn(cloud.auth(), 'signIn').mockResolvedValue(mockUser);
const result = await authService.login('test@example.com', 'ValidPass123');
expect(result).toEqual(mockUser);
});
7.2 真机调试技巧
- 日志过滤:
bash复制hdc shell hilog | grep AGC
- 网络抓包:
- 使用Charles或Fiddler
- 配置代理捕获AGConnect请求
- 性能分析:
bash复制hdc shell hitrace --trace_begin app
7.3 常见问题排查流程
-
验证码问题排查:
- 检查控制台邮件模板配置
- 验证参数格式是否正确
- 检查垃圾邮件箱
-
登录失败排查:
- 确认用户是否已注册
- 检查密码是否符合策略
- 查看网络连接状态
-
权限问题排查:
- 确认manifest配置正确
- 检查运行时权限是否授予
- 查看系统日志中的权限拒绝记录
8. 项目实战建议
在实际医疗系统开发中,我有以下几点经验分享:
- 多环境配置:
typescript复制// 根据编译环境切换配置
const agcConfig = process.env.NODE_ENV === 'production'
? require('./agconnect-services-prod.json')
: require('./agconnect-services-dev.json');
cloud.init(this.context, agcConfig);
- 错误统一处理:
typescript复制// 全局错误处理器
class AuthErrorHandler {
static handle(error: BusinessError) {
const message = this.getErrorMessage(error.code);
promptAction.showToast({ message });
Logger.error('AuthError', error);
}
private static getErrorMessage(code: number): string {
const errorMap = {
203818037: '邮箱已被注册',
203818038: '用户不存在',
// ...其他错误码
};
return errorMap[code] || '操作失败,请重试';
}
}
- 性能监控:
typescript复制// 关键操作耗时统计
async function trackAuthOperation(operation: string, fn: () => Promise<any>) {
const start = Date.now();
try {
await fn();
const duration = Date.now() - start;
Analytics.track(`auth_${operation}_success`, { duration });
} catch (error) {
Analytics.track(`auth_${operation}_failed`, { error: error.code });
throw error;
}
}
- 国际化的实现:
typescript复制// 多语言支持
function getI18nMessage(key: string): string {
const messages = {
'email_required': {
'zh': '请输入邮箱地址',
'en': 'Email is required'
},
// ...其他文案
};
const lang = AppStorage.get('language') || 'zh';
return messages[key][lang];
}
在医疗系统这类对稳定性要求极高的场景中,建议额外增加以下保障措施:
- 实现双因素认证
- 添加登录异常检测
- 记录完整操作日志
- 定期审计用户账号
- 建立应急预案流程