55.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--新增功能--实现手机邮箱登录 - 实践

news/2025/9/24 10:27:44/文章来源:https://www.cnblogs.com/tlnshuju/p/19108710

55.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--新增功能--实现手机邮箱登录 - 实践

2025-09-24 10:26  tlnshuju  阅读(0)  评论(0)    收藏  举报

本文将介绍如何实现手机和邮箱登录功能。我们在现有的用户名/密码登录基础上,扩展了手机号验证码和邮箱验证码两种登录方式。用户登录时可以选择任一方式,系统会相应发送验证码到用户手机或邮箱。用户在登录界面输入验证码后,系统会进行校验,通过后生成身份认证token供后续访问使用。

一、增加自定义授权模式

在实现手机号码和邮箱登录功能时,我们首先需要解决的是授权模式的扩展问题。OpenIddict作为一个强大的认证授权框架,虽然默认支持多种标准的OAuth2.0授权模式,但并不直接支持手机号码和邮箱验证码登录这样的自定义认证方式。这就需要我们对OpenIddict进行扩展,以支持这些额外的认证场景。

扩展授权模式的实现方式非常直接。我们需要在应用程序的OpenIddict配置中,也就是在OpenIddictServiceExtensions扩展类的AddServer方法中,通过调用options.AllowCustomFlow("sms_otp").AllowCustomFlow("email_code");来注册这两个自定义的授权流程。其中,sms_otp流程将用于处理手机短信验证码登录,而email_code流程则负责邮箱验证码登录的实现。

这种扩展方式的优势是,它完全遵循了OAuth2.0的框架设计理念,同时又提供了足够的灵活性。通过这样的自定义授权模式,我们可以在保持原有OAuth2.0标准授权流程的基础上,无缝地集成短信验证码和邮箱验证码这两种更适合移动应用场景的认证方式。这不仅提升了系统的安全性,还为用户提供了更多样化的登录选择。

二、Service 实现短信/邮箱验证码登录

在 Service 中实现短信/邮箱验证码登录的逻辑。首先在接口IAuthorizationService中新增两个方法LoginBySmSCodeAsyncLoginByEmailCodeAsync,这两个方法分别用来实现短信和邮箱验证登录,接口代码如下:

/// <summary>/// 短信验证登录
/// </summary>
/// <param name="phoneNumber"></param>
/// <param name="code"></param>
/// <param name="scopes"></param>
/// <returns>ClaimsPrincipal</returns>
Task<ClaimsPrincipal> LoginBySmSCodeAsync(string phoneNumber, string code, ImmutableArray<string> scopes);/// <summary>/// 邮箱验证码登录/// </summary>
/// <param name="email"></param>
/// <param name="code"></param>
/// <param name="scopes"></param>
/// <returns>ClaimsPrincipal</returns>Task<ClaimsPrincipal> LoginByEmailCodeAsync(string email, string code, ImmutableArray<string> scopes);

上述代码虽然看起来简单直观,但其中包含了完整的验证码登录流程实现。在AuthorizationServiceImpl类中,我们需要实现这两个关键的验证方法。这两个方法的核心逻辑基本相同,都遵循一个统一的验证流程:首先对用户提供的验证码(code)进行验证,确保验证码的有效性和正确性;接着系统会检查用户信息是否存在于数据库中,如果用户确实存在,则进入下一步;最后,系统会为该用户生成一个包含必要身份信息的认证token并返回给客户端。具体实现代码如下:

/// <summary>/// 短信验证登录
/// </summary>
/// <param name="phoneNumber"></param>
/// <param name="code"></param>
/// <param name="scopes"></param>
/// <returns>ClaimsPrincipal</returns>
public async Task<ClaimsPrincipal> LoginBySmSCodeAsync(string phoneNumber, string code,ImmutableArray<string> scopes){if (string.IsNullOrEmpty(phoneNumber) || string.IsNullOrEmpty(code)){throw new BusinessException("手机号或验证码不能为空");}// 验证短信验证码bool isOk = await _smsService.VerifyCodeAsync(phoneNumber, SmSPurposeEnum.Login, code);if (!isOk){throw new BusinessException("验证码错误");}// 查找用户var user = await _userManager.Users.FirstOrDefaultAsync(u => u.PhoneNumber == phoneNumber);if (user == null){throw new BusinessException("用户不存在");}return await BuildPrincipalAsync(user, scopes);}/// <summary>/// 邮箱验证码登录/// </summary>
/// <param name="email"></param>
/// <param name="code"></param>
/// <param name="scopes"></param>
/// <returns>ClaimsPrincipal</returns>public async Task<ClaimsPrincipal> LoginByEmailCodeAsync(string email, string code, ImmutableArray<string> scopes){if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(code)){throw new BusinessException("邮箱或验证码不能为空");}var redisCode = await _redis.GetStringAsync(email);if (string.IsNullOrEmpty(redisCode)){throw new BusinessException("验证码已过期或不存在");}if (redisCode != code.Trim()){throw new BusinessException("验证码错误");}// 查找用户var user = await _userManager.FindByEmailAsync(email);if (user == null){throw new BusinessException("用户不存在");}await _redis.RemoveAsync(email);return await BuildPrincipalAsync(user, scopes);}/// <summary>/// 通用生成ClaimsPrincipal方法/// </summary>/// <param name="user"></param>/// <param name="scopes"></param>/// <returns>ClaimsPrincipal</returns>private async Task<ClaimsPrincipal> BuildPrincipalAsync(SpUser user, ImmutableArray<string> scopes){var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);identity.AddClaim(OpenIddictConstants.Claims.Subject, await _userManager.GetUserIdAsync(user));identity.AddClaim(OpenIddictConstants.Claims.Name, user.UserName ?? string.Empty);if (!string.IsNullOrWhiteSpace(user.Email)){identity.AddClaim(OpenIddictConstants.Claims.Email, user.Email);}identity.AddClaim(OpenIddictConstants.Claims.Audience, "api");foreach (var role in await _userManager.GetRolesAsync(user)){identity.AddClaim(ClaimTypes.Role, role);}identity.SetDestinations(static claim => claim.Type switch{OpenIddictConstants.Claims.Name when claim.Subject.HasScope(OpenIddictConstants.Permissions.Scopes.Profile)=>[OpenIddictConstants.Destinations.AccessToken, OpenIddictConstants.Destinations.IdentityToken],OpenIddictConstants.Claims.Email when claim.Subject.HasScope(OpenIddictConstants.Permissions.Scopes.Email)=>[OpenIddictConstants.Destinations.AccessToken, OpenIddictConstants.Destinations.IdentityToken],ClaimTypes.Role =>[OpenIddictConstants.Destinations.AccessToken],_ =>[OpenIddictConstants.Destinations.AccessToken]});var principal = new ClaimsPrincipal(identity);var validScopes = new[] {"api", OpenIddictConstants.Scopes.OfflineAccess};var filtered = scopes.Intersect(validScopes).ToList();principal.SetScopes(filtered.Any() ? filtered : new[] {"api"});// 可根据角色/设备等动态设置生命周期(此处使用默认)principal.SetAccessTokenLifetime(TimeSpan.FromMinutes(30));principal.SetRefreshTokenLifetime(TimeSpan.FromDays(14));return principal;}

在上述代码实现中,我们构建了一个完整的验证码登录体系。其核心是通过封装统一的BuildPrincipalAsync方法来生成ClaimsPrincipal对象,该方法负责处理用户身份信息的构建和授权范围的设置。这个方法首先创建一个基于OpenIddict认证方案的ClaimsIdentity,然后向其中添加用户的基本信息声明,包括用户ID、用户名、邮箱等。同时,它还会加入用户的角色信息,并通过SetDestinations方法为每个声明指定其可用范围。

在此基础上,我们实现了两种验证码登录方式:LoginByEmailCodeAsyncLoginBySmSCodeAsync。邮箱验证码登录通过Redis进行验证码的存储和校验,而短信验证码登录则依赖于专门的短信服务进行验证。这两个方法都遵循相似的业务流程:首先验证用户输入的验证码是否正确,然后查找对应的用户信息,最后调用BuildPrincipalAsync方法生成包含完整身份信息的ClaimsPrincipal对象。

三、AuthorizationController 实现短信/邮箱验证码登录API是手机号找回。

在ResetEnum枚举中,我们定义了Email和Phone两个选项,分别对应邮箱找回和手机号找回两种方式。这个枚举的设计简单明了,能清晰地表达业务含义。

在AuthorizationServiceImpl中的ResetPasswordAsync方法实现了具体的密码重置逻辑。方法首先验证传入参数的有效性,然后根据ResetBy的值走不同的验证流程。如果是手机号找回,就调用短信服务验证验证码;如果是邮箱找回,则从Redis中获取之前存储的验证码进行验证。验证通过后,方法会查找对应的用户,并使用ASP.NET Core Identity框架提供的方法重置密码。整个实现既保证了安全性,又提供了良好的用户体验。

这套密码重置机制的设计考虑周全,既支持多种找回方式,又实现了必要的安全验证,是一个典型的企业级应用密码找回解决方案。
在这一小节,我们将修改AuthorizationController控制器中的GetToken() Action 使其支持短信/邮箱验证码登录。代码如下:

public async Task<ActionResult> GetToken(){// more code ...// 短信验证码登录if (string.Equals(request.GrantType, "sms_otp", StringComparison.Ordinal)){var phoneNumber = (string?)request.GetParameter("phone_number");var code = (string?)request.GetParameter("code");if (string.IsNullOrEmpty(phoneNumber) || string.IsNullOrEmpty(code)){throw new BusinessException("手机号或验证码不能为空");}var principal =await _authorizationService.LoginBySmSCodeAsync(phoneNumber, code,request.GetScopes());var signInResult = SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);return signInResult;}// 邮箱验证码登录if (string.Equals(request.GrantType, "email_code", StringComparison.Ordinal)){var email = (string?)request.GetParameter("email");var code = (string?)request.GetParameter("code");if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(code)){throw new BusinessException("邮箱或验证码不能为空");}var principal =await _authorizationService.LoginByEmailCodeAsync(email, code,request.GetScopes());var signInResult = SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);return signInResult;}// more code ...}

首先,通过判断请求中的grant_type参数来识别具体的登录方式。当grant_type为"sms_otp"时表示短信验证码登录,为"email_code"时表示邮箱验证码登录。

对于短信验证码登录,系统会从请求参数中获取phone_number(手机号)和code(验证码)。系统会先验证这两个参数是否为空,如果为空则抛出业务异常。验证通过后,调用_authorizationService.LoginBySmSCodeAsync方法进行实际的登录验证,该方法会验证短信验证码的正确性并查找对应的用户信息。邮箱验证码登录的处理方式类似,从请求参数中获取email和code,进行空值检查后调用_authorizationService.LoginByEmailCodeAsync方法进行验证和用户查找。两种登录方式在验证成功后都会得到一个包含用户身份信息的ClaimsPrincipal对象。最后通过SignIn方法使用OpenIddict的认证方案创建登录会话,并返回包含访问令牌的响应。

三、总结

本文详细介绍了如何在.NET 8项目中实现手机号和邮箱验证码登录功能。通过扩展OpenIddict框架的授权模式,我们成功实现了sms_otp和email_code两种自定义的授权流程。在Service层,我们封装了验证码校验、用户查找和身份信息构建等核心逻辑,确保了登录流程的安全性和可靠性。同时,在Controller层通过统一的Token获取接口,集成了这两种登录方式。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/915555.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

游戏网站的设计方案新农村建设管理网站

Problem - C - Codeforces 题目分析 <1>0&#xff1a;想被分割至左边&#xff1b; 1&#xff1a;想被分割至右边 <2>使得左右两侧均有一半及其以上的人满意&#xff08;我*******&#xff09; <3>答案若有多个&#xff0c;取最接近中间位置的答案 <4…

怎么选择网站建设干部信息管理系统

1 、请用 Python 手写实现插入排序。 解析&#xff1a; 插入排序&#xff08; Insertion Sort &#xff09;的工作原理是通过构建有序序列&#xff0c;对于未排序数据&#xff0c; 在已排序序列中从后向前扫描&#xff0c;找到相应位置并插入。 算法执行步骤&#xff1a; &…

详细介绍:Xilinx系列FPGA实现12G-SDI音视频编解码,支持4K60帧分辨率,提供2套工程源码和技术支持

详细介绍:Xilinx系列FPGA实现12G-SDI音视频编解码,支持4K60帧分辨率,提供2套工程源码和技术支持pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block …

CentOS6.8安装docker教程

在VMware新安装CentOS6.8系统CentOS6.8可在阿里镜像库下载: https://mirrors.aliyun.com/centos-vault/6.8/isos/x86_64/ 在新安装系统配置yum源2.1 进入目录 /etc/yum.repos.d2.2 对原配置备份mv *.repo ./bak2.3 下…

使用 VMware Workstation 安装 CentOS-7 虚拟机

使用 VMware Workstation 安装 CentOS-7 虚拟机1. 环境说明和软件准备 环境说明:宿主机操作系统:Window 10 宿主机 CPU 架构:x86_64 虚拟机软件:VMware Workstation Pro 15 虚拟机系统:CentOS-7.6软件下载:CentO…

K12教育 和 STEAM教育

K12教育定义:K12是“Kindergarten through twelfth grade”的缩写,指从幼儿园(Kindergarten,通常5-6岁)到十二年级(Grade 12,通常17-18岁)的教育阶段。它涵盖了学前教育、小学教育、初中教育和高中教育,是国际…

网站底部代码大全建设网站一定要电脑吗

binary 和 varbinary固定长度 (binary) 的或可变长度 (varbinary) 的 binary 数据类型。binary [ ( n ) ]固定长度的 n 个字节二进制数据。N 必须从 1 到 8,000。存储空间大小为 n4 字节。varbinary [ ( n ) ]n 个字节变长二进制数据。n 必须从 1 到 8,000。存储空间大小为实际…

uv Python安装镜像加速

uv Python安装镜像加速感谢南京大学开源镜像站!国内目前能找到的唯一镜像! Windows系统cmd设置环境变量命令如下: setx UV_PYTHON_INSTALL_MIRROR "https://mirror.nju.edu.cn/github-release/astral-sh/pytho…

AT_arc167_c [ARC167C] MST on Line++

首先遇到这种题先不要慌,先拆贡献。 考察一个权值为 \(a_i\) 的边会被 MST 包含多少次,因为我们确定了 \(p\),所以 \(a\) 的顺序就没有关系了,我们先将 \(a\) 排序,钦定某一种边权出现次数很难做,但是我们如果钦…

CentOS操作系统

CentOS操作系统CentOS操作系统更新时间:2025-07-31 09:59:20产品详情我的收藏 本文详细介绍CentOS所处的生命周期阶段,以及可以采取哪些应对方案来应对CentOS停止维护后的风险。CentOS生命周期概述 CentOS Linux 是一…

龙虎榜——20250912 - 详解

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

Lombok无法使用get set方法

问题描述:使用lombok@Data注解后,无法调用实体类的getter setter方法。 解决方案:安装lombok插件(记得安装后应用)

网站怎么做直播小制作简单手工

基本思想&#xff1a;需要使用linux系统调用alliedvisio工业相机完成业务&#xff0c;这里只做驱动相机调用&#xff0c;具体不涉及业务开发 Alvium 相机选型 - Allied Vision 一、先用软件调用一下用于机器视觉和嵌入式视觉的Vimba X 软件开发包 - Allied Vision VimbaX_Set…

网站与网页移动商城官网 积分兑换

论文笔记整理&#xff1a;谭亦鸣&#xff0c;东南大学博士生。来源&#xff1a;WWW 2020链接&#xff1a;https://dl.acm.org/doi/pdf/10.1145/3366423.3380114概述这篇论文关注的任务是&#xff1a;基于给定文本的“多跳问题生成”&#xff08;多关系问题&#xff09;。作者提…

redis的哈希扩容

Redis 哈希的扩容过程是其高效性的关键所在,它采用了一种非常巧妙的渐进式 rehash 策略来避免一次性扩容带来的服务停顿。 步骤 1:准备工作 当满足扩容条件时,为 ht[1] 分配空间。新的大小根据上述规则计算。 将字典…

vite tailwindcss配置

1. 安装tailwindcss依赖yarn add tailwindcss @tailwindcss/vite2.新建css文件引入tailwindcss//assets/styles/tailwind.css@import tailwindcss;3.main.js引入css文件import ./assets/styles/tailwind.css4.配置vite…

window系统下使用二进制包安装MySQL数据库

window系统使用二进制包安装MySQL数据库以下仅为本人工作、学习过程中所接触到的内容,不足之处欢迎指出。 安装说明 1、安装数据库的window系统为win7专业版64位2、MySQL版本为mysql-5.7.17-winx64 下载解压 下载地址…

在Vona ORM中实现多数据库/多数据源

在Vona ORM中实现多数据库/多数据源非常直观、简便。下面以 Model User/Order 为例,通过查询用户的订单列表,来演示多数据库/多数据源的使用方法在Vona ORM中实现多数据库/多数据源非常直观、简便。下面以 Model Use…

网站开发工程师是什么意思重庆大学建设管理与房地产学院网站

什么是token&#xff1f;token是一个用户自定义的任意字符串&#xff0c;目前开发中&#xff0c;token都是在服务端生成并且token的值会保存到服务器后台。只有服务器和客户端知道这个字符串&#xff0c;于是&#xff0c;这个token就成了两者之间的秘钥&#xff0c;它可以让服务…

实用指南:python全栈-数据可视化

实用指南:python全栈-数据可视化pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco&q…