验证码这东西,有人喜欢有人不喜欢。对于WebApi是否需要验证码,没去研究过,只是原来的SimpleCMS有,就加上吧。

在WeiApi上使用验证码,关键的地方在于WeiApi是没有状态的,也就是说,不能使用Session来保存验证码。因而,在WebApi上使用验证码。首先需要解决的是保存的问题。刚开始先测试AbpSession了,但发现机制和习惯的不同,无法使用,那就只能保存到数据库了。保存到数据库最大的麻烦是如何判断当前用户对应的验证码是那个,也就是需要一个唯一值来寻找验证码,而且这个唯一值必须在客户端刷新图片的时候一起返回到客户端,以便提交时,将该值一并提交到服务器。要将图片和唯一值一起返回客户端,也就是说不能以图片方式返回,不然不好添加唯一值。沿着这个思路想到了将图片转换为BASE64代码再返回这方式,这样,返回的就是字符串,可以携带其他信息返回了。后来,再想想,居然都已经转换为字符串了,何不直接将图片字符串做个MD5提交到服务器,然后把这个MD5作为搜索值,这样也省了处理多个数值的问题。使用MD5作为唯一值,唯一的思虑是,能确保图片都是唯一的么?对于这个问题,笔者觉得,能同时出现相同的图片,几乎比中彩票还难,26个字母加上10个数字的6位数排列已经是上十亿的可能,再加上字体、燥点和字体颜色等因素,真的中了,那你不是买彩票都对不起自己了。

思路明确后就可以开始工作了。先在Core项目创建VerifyCode的实体,代码如下:

    [Table("AppVerifyCodes")]
public class VerifyCode :Entity<long>
{
public const int MaxCodeLength = 6;
public const int MaxMd5KeyLength = 32; [Required]
[MaxLength(MaxMd5KeyLength)]
public string Md5Key { get; set; } [Required]
[MaxLength(MaxCodeLength)]
public string Code { get; set; } [Required]
public DateTime Expired { get; set; } }

在实体中添加Expired属性的作用是为验证码设置一个过期时间,以避免这个验证码比重复利用。在练习中,我将过期时间设置为了10分钟,在实际使用时,可以设置为30秒或者更小,不行就让用户刷新验证码就行。

创建实体后,将它添加到SimpleCmsWithAbpDbContext,然后就可执行Add-MigrationUpdate-Database命令在数据库添加实体对应的表了。

有了实体后,就要添加服务,以便将图片返回客户端。一般情况下,通过继承CrudAppServiceAsyncCrudAppService类就可以很简单的实现一个实体的CRUD操作,在没有特殊操作的情况下,基本不需要编写任何代码就能实现实体的CRUD操作了。由于验证码不需要完整的CRUD操作,只需要一个返回图片的操作,因而可以从IApplicationService接口、ApplicationService类或模版提供的SimpleCmsWithAbpAppServiceBase类等继承。SimpleCmsWithAbpAppServiceBase类是在ApplicationService的基础上添加了GetCurrentUserAsync方法用来返回当前用户,添加了GetCurrentTenantAsync方法用来返回当前租户。如果不需要这两方法,可以直接从ApplicationService基础。

了解了如何添加服务后,先在Application项目添加一个名为VerifyCodes的文件夹,并在该文件夹下创建一个名为Dto的文件夹。在Dto文件夹下, 先创建一个名为GetVerifyCodeOutput的类,代码如下:

    public class GetVerifyCodeOutput
{
public string Image { get; set; }
}

GetVerifyCodeOutput类将作为验证码的Get服务的返回对象。

在VerifyCodes文件夹下,创建一个名为IVerifyCodeAppService的接口,代码如下:

    public interface IVerifyCodeAppService : IApplicationService
{
Task<GetVerifyCodeOutput> Get();
}

还要创建一个名为VerifyCodeAppService的类,代码如下:

    public class VerifyCodeAppService:SimpleCmsWithAbpAppServiceBase,IVerifyCodeAppService
{
private readonly IRepository<VerifyCode, long> _repository; public VerifyCodeAppService(IRepository<VerifyCode, long> repository)
{
_repository = repository;
} public async Task<GetVerifyCodeOutput> Get()
{
var v = new VerifyCodeCore();
var code = v.CreateVerifyCode(); //取随机码
v.Padding = 10;
var bytes = v.CreateImage(code);
var image = $"data:{MimeTypeNames.ImageJpeg};base64,{Convert.ToBase64String(bytes)}";
var verifyCode = new VerifyCode()
{
Md5Key = GetMd5Key(image),
Code = code,
Expired = Clock.Now.AddMinutes(10)
};
await _repository.InsertAsync(verifyCode);
var output = new GetVerifyCodeOutput()
{
Image = image
};
return output;
} private static string GetMd5Key(string input)
{
var md5 = new MD5CryptoServiceProvider();
var inputBytes = System.Text.Encoding.ASCII.GetBytes(input);
var hashBytes = md5.ComputeHash(inputBytes); // Convert the byte array to hexadecimal string
var sb = new StringBuilder();
foreach (var t in hashBytes)
{
sb.Append(t.ToString("X2"));
}
return sb.ToString();
}
}

Get方法内,先调用VerifyCodeCoreCreateVerifyCode方法创建验证码,再调用CreateImage方法创建图像的二进制代码,并将二进制代码转换为BASE64代码。接下来是创建一个VerifyCode实体并通过存储的InsertAsync方法将实体添加到数据库。在这里调用了GetMd5Key方法将图片字符串转换为了MD5字符串。最后,创建GetVerifyCodeOutput的实体并返回。

在这里要注意的是,由于在.net core 2中并不包含System.Drawing对象,不能处理Bitmap对象,在使用VerifyCodeCore类的时候会出错,因而,需要在Application项目中添加System.Drawing.Common包,这个包目前还是预览版状态,需要在NuGet管理页中将包括预发行版选上才能找到。

重新生成解决方案,就可在swagger页的底部看到VerifyCode服务了,打开访问地址并单击Try it out!按钮就可看到以下的返回数据:

{
"result": {
"image": "data:image/jpeg;base64,此处省略图片输出"
},
"targetUrl": null,
"success": true,
"error": null,
"unAuthorizedRequest": false,
"__abp": true
}

这说明返回验证码没有问题了。下面要修改验证码的验证问题了。切换到Web.Core项目,在Models文件夹下,打开AuthenticateModel.cs文件,并将代码修改为以下代码:

    public class AuthenticateModel : ICustomValidate
{
[Required]
[StringLength(AbpUserBase.MaxEmailAddressLength)]
public string UserNameOrEmailAddress { get; set; } [Required]
[StringLength(AbpUserBase.MaxPlainPasswordLength)]
public string Password { get; set; } [Required]
[StringLength(6)]
public string VerifyCode { get; set; } [Required]
[StringLength(32)]
public string Key { get; set; } public bool RememberClient { get; set; } public void AddValidationErrors(CustomValidationContext context)
{
var verifyCodeRepository = context.IocResolver.Resolve<IRepository<VerifyCode, long>>();
var localizationManager = context.IocResolver.Resolve<ILocalizationManager>();
var record = verifyCodeRepository.FirstOrDefault(m =>m.Md5Key == Key.ToUpper());
if (record == null || (record.Code.ToUpper() != VerifyCode.ToUpper() || record.Expired < Clock.Now))
{
context.Results.Add(new ValidationResult(
localizationManager.GetString(SimpleCmsWithAbpConsts.LocalizationSourceName, "verifyCodeInvalid"),
new List<string>() {"VerifyCode"}));
}
else
{
verifyCodeRepository.Delete(record);
}
}
}

AuthenticateModel类是登录时用来接收登录数据的模型类。在该类中,添加了VerifyCodeKey两个属性用来接收验证码和与验证码相关的搜索值,并添加了自定义验证的AddValidationErrors方法来验证验证码。在AddValidationErrors内,先通过Resolve方法获取到VerifyCode实体的存储和本地化资源管理接口ILocalizationManager,再调用存储的FirstOrDefault方法来获取与验证码相关的实体,然后进行验证。如果记录不存在,或者记录的验证码不对,或者已经超时,就返回验证错误,否则删除实体,并继续执行后续的验证的操作。

在实现这个的时候,经历了一些波折,在刚开始的时候,笔者习惯使用Equals方法来验证字段与提交值是否相等,但得到的都是错误的结果,这就奇怪了。于是,笔者就查看日志到底是怎么回事,但是日志并没有记录查询时的SQL语句,这就麻烦了。在没有使用ABP框架时,要记录实体查询时的SQL语句很简单,只要调用UseLoggerFactory方法添加工厂类就行了,但是经过搜索,发现ABP框架使用的日志包castle.windsor并没有跟上时代的步伐,为这提供相应的支持,为此,ABP框架的人还去GitHub和castle.windsor的项目负责人进行了交流,最后也没啥结果。没办法,只能自己来解决这个问题了。先在EntityFrameworkCore包添加Microsoft.Extensions.Logging.Log4Net.AspNetCore包,然后打开SimpleCmsWithAbpDbContextConfigurer.cs文件,并将代码修改为以下代码:

    public static class SimpleCmsWithAbpDbContextConfigurer
{ public static readonly LoggerFactory MyLoggerFactory
= new LoggerFactory(new[]
{
new Log4NetProvider("log4net.config",new Func<object, Exception, string>((o, exception) =>exception.Message ))
});
public static void Configure(DbContextOptionsBuilder<SimpleCmsWithAbpDbContext> builder, string connectionString)
{
//builder.UseSqlServer(connectionString);
builder.UseLoggerFactory(MyLoggerFactory).UseMySql(connectionString);
} public static void Configure(DbContextOptionsBuilder<SimpleCmsWithAbpDbContext> builder, DbConnection connection)
{
//builder.UseSqlServer(connection);
builder.UseLoggerFactory(MyLoggerFactory).UseMySql(connection);
}
}

代码主要添加了一个LoggerFactory实例,用于记录实体的操作日志。代码里一定要将UseLoggerFactory方法放在UseMySql的前面,不然不起任何作用。好了,现在可以在日志中记录SQL 语句了。通过查看日志,发现使用Equals方法不能将查询值传递给SQL语句,这就奇怪了,不知道是ABP问题还是Entity Framework Core的问题了。不管了,还成==就没问题了。

使用带有 module-zero的模版,本地化信息可存储在数据库,也可保存在XML文件中。笔者是将信息保存在数据库中,例如AddValidationErrors使用到的verifyCodeInvalid信息,可在abplanguagetexts表中添加一条记录,记录的内容如下: - Key:verifyCodeInvalid - LanguageName:zh-CN - Source:SimpleCmsWithAbp - Value:验证码错误

这里要注意的是CreationTime字段的值不能为0,不然会出现错误,随便添加个时间就行了。还有就是关于Source的值,如果要自定义源的话,需要将源添加到本地化管理中,不然会提示找不到源。为了简便起见,使用SimpleCmsWithAbpConsts.LocalizationSourceName常数指定的源挺好,在本项目里,LocalizationSourceName的值是SimpleCmsWithAbp,因而Source的值为SimpleCmsWithAbp

重新生成解决方案,验证码验证功能就已经可用了。最后要修改的是客户端。

由于默认的服务访问接口都有前缀api/services/app,为了能方便处理这种情况,需要先修改SimpleCMS.util.Url类,将get方法修改成以下代码:

    defaultPath: '/api/services/app',

    get: function(controller, action, notDefaultPath) {
var me = this;
if (!Ext.isString(controller) || Ext.isEmpty(controller)) Ext.raise('非法的控制器名称');
if (!Ext.isString(action) && !Ext.isNumber(action)) Ext.raise('非法的操作名称');
return Ext.String.format(me.urlFormat, ROOTPATH + (notDefaultPath ? '' : me.defaultPath), controller, me.defaultActions[action] || me.actions[action] || action);
},

方法主要添加了一个notDefaultPath参数,用来指定是否添加默认路径,如果不设置该值,则添加,否则就不添加。

在客户端需要一个MD5类用来将图片的字符串转换为MD5字符串,在Sencha官方论坛找到了这个类,具体地址为Ext.util.MD5。类下载后,在app\util\文件夹下添加一个名为MD5.js的文件,然后将下载的代码粘贴到类里,将类名修改为SimpleCMS.util.MD5 ,并在app.js中添加对它的引用,build一次就能用了。

使用WebApi,表单就不能直接提交了,需要将表单内的数据转换为JSON格式提交,而要实现这个,只要在表单中将jsonSubmit设置为true就行了,但是每次都要设置就太麻烦了,通过重写方式可一劳永逸的解决这个问题,但尝试重写Ext.form.Basic发现不起作用,重写Ext.form.action.Submit才行。

数据是能以JSON提交,但发现WebApi在验证错误的时候,返回的是400错误,而登录失败返回的是500错误,而且,验证错误的返回格式与Ext JS的默认格式也不同,这些都需要通过重写Ext.form.action.Submit来实现,完成的后代码如下:

Ext.define('Overrides.form.action.Submit', {
override: "Ext.form.action.Submit", jsonSubmit: true, onFailure: function(response) {
var me = this,
form = me.form,
formActive = form && !form.destroying && !form.destroyed,
result; me.response = response;
//this.failureType = Ext.form.action.Action.CONNECT_FAILURE; if (response.status === 400) {
result = me.processResponse(response);
if (result.error.validationErrors) {
me.form.markInvalid(me.processValidationErrors(result.error.validationErrors));
me.failureType = "validationErrors";
}
} else {
me.failureType = Ext.form.Action.CONNECT_FAILURE;
} if (formActive) {
form.afterAction(me, false);
}
}, processValidationErrors: function(errors) {
var result = {},
ln = errors.length,
i = 0,
error, j, jn, fields, field;
for (i; i < ln; i++) {
error = errors[i];
fields = error.members;
jn = fields.length;
for (j = 0; j < jn; j++) {
field = result[fields[j]];
if (!field) field = result[fields[j]] = [];
field.push(error['message']);
}
}
return result;
} });

以上的代码参考了Sencha官方论坛的Aren’t Http Status Codes enough?这个帖子,不过,帖子中重写的是failure方法,不起左右,要重写onFailure方法才行。

onFailure方法内,如果返回的状态码是400,则判断是否存在error.validationErrors的数据,如果存在,是否是验证错误,需要从error.validationErrors中,将数据提取出来,将数据转换为Ext JS认识的错误格式。转换过程主要是从返回的每个错误中的members中获取字段名称,在新的对象中以字段名称作为属性名称,message的值作为错误信息数组中的一个值。

重写类写好以后,需要build一次以加载重写类。完成build后,打开登录视图app\view\authentication\Login.js,将里面的字段的name都修改为与AuthenticateModel类中属性对应的名称。修改完name后,打开app\view\authentication\AuthenticationController.js文件,修改verifyCodeUrl属性、onLoginButton方法和onRefrestVcode方法,具体代码如下:

    onLoginButton: function () {
var me = this,
view = me.getView(),
f = view.getForm(),
src = view.down('image').getSrc();
if (f.isValid()) {
f.submit({
//jsonSubmit:true,
params:{key: SimpleCMS.util.MD5(src)},
url: URI.get('api/TokenAuth', 'Authenticate', true),
waitMsg: I18N.LoginSubmitWaitMsg,
waitTitle: I18N.LoginSubmitWaitTitle,
success: function (form, action) {
window.location.reload();
},
failure: function(form,action){
this.onRefrestVcode();
FAILED.form(form,action);
},
scope: me
});
}
}, verifyCodeUrl: URI.get('VerifyCode', 'Get'),
onRefrestVcode: function () {
var me = this,
view = me.getView(),
img = view.down('image');
Ext.Ajax.request({
url: me.verifyCodeUrl,
scope: me,
success: function(response, opts) {
var obj = Ext.decode(response.responseText),
view = this.getView();
if(view && obj.success && obj.result && obj.result.image){
view.down('image').setSrc(obj.result.image);
}
},
failure: function(response, opts) {
TOAST.toast('获取验证码失败', this.getView().el, 'bl' );
}
})
},

在onLoginButton方法中,主要修改的地方是在提交前,先获取Ext.Img组件的src属性的值,调用SimpleCMS.util.MD5方法将图片字符串转换为MD5字符,并作为Key值提交到服务器。由于提交地址为/api/TokenAuth/Authenticate,不是默认的WebApi提交地址,因而需要在调用get方法时添加第3个参数。在failure的回调中,只有出现错误,就刷新一次验证码,不能再使用旧的验证码,因为旧的验证码已经删除了。

onRefrestVcode方法中,主要修改的地方就是需要通过Ajax的方式来获取验证码,而不能直接使用修改访问地址的方式来刷新验证码。在获取到验证码后,将返回的字符串值作为图片的src值就行了。

由于登录失败都是以500错误返回的,因而需要修改SimpleCMS.util.Failed以处理这种情况,具体修改代码如下:

    form: function(form, action) {
if (action.failureType === 'validationErrors') return;
if (action.response.status === 500) {
var result = Ext.decode(action.response.responseText);
if (result.error && result.error.message) {
TOAST.toast(
result.error.message + (result.error.details ? result.error.details : ''),
form.owner.el,
'bl'
);
}
return;
}
FAILED.ajax(action.response);
}

代码先判断failureType是否为验证错误,如果是,说明已经处理过了,不用处理,直接返回。如果状态码为500,就判断结果是否包含errorerror.message两个数据,如果包含,说明有错误信息,就在窗口上使用Ext.window.Toast来输出信息。如果是其他情况,调用ajax方法来处理错误信息。

在最后,还需要打开application.js文件,在onAjaxBeforeRequest方法中,将options.jsonData = true;这句删除,不然表单提交的时候不会提交任何数据。

至此,验证码功能已经实现了。

Asp.NET Core+ABP框架+IdentityServer4+MySQL+Ext JS之验证码的更多相关文章

  1. ASP.NET Core 入门教程 2、使用ASP.NET Core MVC框架构建Web应用

    一.前言 1.本文主要内容 使用dotnet cli创建基于解决方案(sln+csproj)的项目 使用Visual Studio Code开发基于解决方案(sln+csproj)的项目 Visual ...

  2. Asp.Net Core MVC框架内置过滤器

    第一部分.MVC框架内置过滤器 下图展示了Asp.Net Core MVC框架默认实现的过滤器的执行顺序: Authorization Filters:身份验证过滤器,处在整个过滤器通道的最顶层.对应 ...

  3. ASP.NET Core Identity 框架 - ASP.NET Core 基础教程 - 简单教程,简单编程

    原文:ASP.NET Core Identity 框架 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core Identity 框架 前面我们使用了 N 多个章节, ...

  4. [ASP.NET Core 3框架揭秘] 跨平台开发体验: Windows [上篇]

    微软在千禧年推出 .NET战略,并在两年后推出第一个版本的.NET Framework和IDE(Visual Studio.NET 2002,后来改名为Visual Studio),如果你是一个资深的 ...

  5. [ASP.NET Core 3框架揭秘] 跨平台开发体验: Docker

    对于一个 .NET Core开发人员,你可能没有使用过Docker,但是你不可能没有听说过Docker.Docker是Github上最受欢迎的开源项目之一,它号称要成为所有云应用的基石,并把互联网升级 ...

  6. [ASP.NET Core 3框架揭秘] 依赖注入:控制反转

    ASP.NET Core框架建立在一些核心的基础框架之上,这些基础框架包括依赖注入.文件系统.配置选项和诊断日志等.这些框架不仅仅是支撑ASP.NET Core框架的基础,我们在进行应用开发的时候同样 ...

  7. [ASP.NET Core 3框架揭秘] 依赖注入[5]: 利用容器提供服务

    毫不夸张地说,整个ASP.NET Core框架是建立在依赖注入框架之上的.ASP.NET Core应用在启动时构建管道以及利用该管道处理每个请求过程中使用到的服务对象均来源于依赖注入容器.该依赖注入容 ...

  8. [ASP.NET Core 3框架揭秘] 文件系统[1]:抽象的“文件系统”

    ASP.NET Core应用 具有很多读取文件的场景,比如配置文件.静态Web资源文件(比如CSS.JavaScript和图片文件等)以及MVC应用的View文件,甚至是直接编译到程序集中的内嵌资源文 ...

  9. [ASP.NET Core 3框架揭秘] 文件系统[2]:总体设计

    在<抽象的"文件系统">中,我们通过几个简单的实例演示从编程的角度对文件系统做了初步的体验,接下来我们继续从设计的角度来进一步认识它.这个抽象的文件系统以目录的形式来组 ...

随机推荐

  1. 八、golang文本操作、json协议

    一.终端读写 操作终端相关文件语句常量,go所有的都是接口 os.Stdin:标准输入 os.Stdout:标准输入,只要实现输出这个接口,打印到终端, os.Stderr:标准错误输出 os.Ope ...

  2. 七、golang中接口、反射

    一.接口定义 1.定义 interface类型可以定义一组方法,但是这些不需要实现,并且interface不能包含任何变量 package main import ( "fmt" ...

  3. 13.常见模块re-正则模块

    1.正则 正则表达式是计算机科学的一个概念,正则表通常被用来检索.替换那些符合某个模式(规则)的文本.也就是说使用正则表达式可以在字符串中匹配出你需要的字符或者字符串,甚至可以替换你不需要的字符或者字 ...

  4. groupby和agg的使用

    先来看一段代码: 分析下groupby和agg的联合使用: reset_index()表示重新设置索引 agg传进来的统计特征: 按照A这一列作聚合,C这一列作统计 注意:df = df.groupb ...

  5. win10系统下载地址

    Win10正式版微软官方原版ISO系统镜像下载: Win10正式版32位简体中文版(含家庭版.专业版) 文件名: cn_windows_10_multiple_editions_x86_dvd_684 ...

  6. poj 3080 Blue Jeans【字符串处理+ 亮点是:字符串函数的使用】

    题目:http://poj.org/problem?id=3080 Sample Input 3 2 GATACCAGATACCAGATACCAGATACCAGATACCAGATACCAGATACCA ...

  7. MapReduce-从HBase读取数据处理后再写入HBase

    MapReduce-从HBase读取处理后再写入HBase 代码如下 package com.hbase.mapreduce; import java.io.IOException; import o ...

  8. UvaLive 5811 概率DP

    题意 : 有54张牌 问抽多少张牌能使每种花色都至少是给定的数字 两张王牌可以被选择为任何花色 高放学长真是太腻害辣! 设置dp[][][][][x][y] 前四维代表四种真的花色的数量 后两维xy代 ...

  9. Java -- JDBC 事务处理, 事务的隔离级别 脏读 不可重复读 等...

    1. 事务指逻辑上的一组操作,组成这组操作的各个单元,要不全部成功,要不全部不成功. 数据库开启事务命令 •start transaction 开启事务 •Rollback  回滚事务 •Commit ...

  10. Intel Code Challenge Elimination Round (Div.1 + Div.2, combined) D. Generating Sets 贪心+优先队列

    D. Generating Sets time limit per test 2 seconds memory limit per test 256 megabytes input standard ...