You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Expo+Node+SendGrid密码重置:如何跳转客户端并传递用户标识

密码重置功能实现方案(React Native Expo + Node.js)

完全可以只在客户端保留改密表单,点击邮件链接直接跳转至客户端并获取用户标识,核心是用Expo深度链接携带加密的用户验证信息,以下是具体实现步骤和代码修正:


一、后端调整(Node.js)

核心是生成安全的短期重置令牌(而非明文传邮箱/ID),构造正确的带参深度链接,修复邮件发送逻辑:

const sgMail = require('@sendgrid/mail');
const jwt = require('jsonwebtoken'); // 需先安装:npm install jsonwebtoken
const bcrypt = require('bcrypt'); // 密码加密用,需安装:npm install bcrypt

sendResetPasswordLink: async (email, baseLink) => {
  sgMail.setApiKey(process.env.SENDGRID_API_KEY);
  
  // 1. 生成1小时有效期的重置令牌,包含用户邮箱信息
  const resetToken = jwt.sign(
    { email: email },
    process.env.JWT_RESET_SECRET, // 自己在.env里配置密钥
    { expiresIn: '1h' }
  );

  // 2. 拼接带令牌的深度链接
  const resetLink = `${baseLink}?token=${resetToken}`;

  // 3. 发送邮件(替换硬编码的邮箱和内容)
  const msg = {
    to: email, // 用传入的目标用户邮箱
    from: process.env.SENDER_EMAIL, // 替换成SendGrid验证过的发件邮箱
    subject: '重置你的账号密码',
    text: `点击链接重置密码:${resetLink}`,
    html: `<p>请点击下面的链接重置你的密码(1小时内有效):</p><a href="${resetLink}">立即重置密码</a>`,
  };

  try {
    await sgMail.send(msg);
    return { success: true };
  } catch (error) {
    console.error(error);
    return { success: false, error: error.message };
  }
}

二、客户端调整(React Native Expo)

需要监听深度链接打开事件,解析令牌并跳转改密页面,提交新密码时携带令牌验证:

1. 根组件监听链接事件(比如App.js)

import { Linking, useEffect } from 'react-native';
import { useNavigation } from '@react-navigation/native';

function App() {
  const navigation = useNavigation();

  useEffect(() => {
    // 处理应用已打开时的链接跳转
    const handleOpenURL = async (event) => {
      const url = event.url;
      const params = new URLSearchParams(url.split('?')[1]);
      const resetToken = params.get('token');
      
      if (resetToken) {
        navigation.navigate('ChangePassword', { resetToken });
      }
    };

    // 监听链接打开事件
    Linking.addEventListener('url', handleOpenURL);

    // 处理应用未启动时,通过链接直接打开的情况
    const checkInitialURL = async () => {
      const initialURL = await Linking.getInitialURL();
      if (initialURL) handleOpenURL({ url: initialURL });
    };
    checkInitialURL();

    return () => Linking.removeEventListener('url', handleOpenURL);
  }, [navigation]);

  // ...其他组件逻辑
}

2. 请求发送重置链接的代码修正

// 生成指向改密页面的基础深度链接
const baseLink = Linking.createURL('/ChangePassword');
// 传给后端邮箱和基础链接
axios.post('/send_reset_password_link', { email, baseLink })
  .then(() => alert('重置链接已发送到你的邮箱'))
  .catch(err => alert('发送失败,请稍后重试'));

3. 改密页面(ChangePassword.js)

import { useState } from 'react';
import { View, TextInput, Button, Alert } from 'react-native';
import axios from 'axios';

const ChangePassword = ({ route, navigation }) => {
  const { resetToken } = route.params;
  const [newPassword, setNewPassword] = useState('');

  const handleReset = async () => {
    if (!newPassword.trim()) return Alert.alert('提示', '请输入新密码');
    
    try {
      await axios.post('/reset_password', {
        token: resetToken,
        newPassword: newPassword
      });
      Alert.alert('成功', '密码重置完成,请重新登录');
      navigation.navigate('Login');
    } catch (err) {
      Alert.alert('失败', err.response?.data?.message || '链接已过期或无效');
    }
  };

  return (
    <View style={{ padding: 20, gap: 15 }}>
      <TextInput
        placeholder="输入新密码"
        secureTextEntry
        value={newPassword}
        onChangeText={setNewPassword}
        style={{ borderBottomWidth: 1, padding: 10 }}
      />
      <Button title="确认重置密码" onPress={handleReset} />
    </View>
  );
};

export default ChangePassword;

三、后端新增密码重置验证接口

const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const User = require('./models/User'); // 你的用户数据模型

app.post('/reset_password', async (req, res) => {
  const { token, newPassword } = req.body;

  try {
    // 验证令牌有效性
    const decoded = jwt.verify(token, process.env.JWT_RESET_SECRET);
    const user = await User.findOne({ email: decoded.email });
    
    if (!user) return res.status(404).json({ message: '用户不存在' });
    // 加密新密码后更新
    const hashedPwd = await bcrypt.hash(newPassword, 10);
    user.password = hashedPwd;
    await user.save();

    res.status(200).json({ message: '密码重置成功' });
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(400).json({ message: '重置链接已过期' });
    }
    res.status(400).json({ message: '无效的重置链接' });
  }
});

关键注意事项

  • 绝对不要明文传递用户ID/邮箱,必须用加密令牌,防止恶意篡改
  • 生产环境建议用Expo通用链接(Universal Links)替代本地IP的exp链接,提升兼容性
  • 令牌有效期设置短一些(1-2小时),降低安全风险
  • 所有用户密码必须哈希加密后存储,禁止明文保存

内容的提问来源于stack exchange,提问作者learncode

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 10:25:41