在传统的登入系统中总是使用帐号密码的方式验证身份,这种方式如果密码不小心被盗取的话,帐号资料就会有被骇入的可能性。
为了提升帐号安全性,我们可以利用手机 App 产生一次性密码 (MOTP),做为登入的第二道密码使用又称双重验证,这样的优点是不容易受到攻击,需要登入密码及一次性密码才可以完成登入。
这次的教学重点会放在如何与 OTP Authenticator 免费 App 搭配产生一次性密码,并在网页上验证一次性密码 (OTP)。
我写了简单的範例,在 C# Asp.Net 网页产生注册 QR Code,并利用免费的 OTP Authenticator App 扫描 QR Code 产生一次性密码 (OTP) 后,再回到网页上验证身份。
範例建置环境
前端架构: Vue.js, jQuery, Bootstrap
后端架构: C# ASP.Net MVC .Net Framework
此登入範例为求重点展示 MOTP 所以没有使用资料库,大家了解 MOTP 规则后,可以应用在实务专案上。
文末会提供範例档下载连结。
行动一次性密码 (MOTP) 是什么
行动一次性密码(英语:Mobile One-Time Password,简称MOTP),又称动态密码或单次有效密码,利用行动装置上产生有效期只有一次的密码。
有效期採计时制,通常可设定为 30 秒到两分钟不等,时间过了之后就失效,下次需要时再产生新密码。
MOTP 产生密码的方法是手机取得注册时的密钥 (Token) 与 Pin 之后,以当下时间利用 MD5 加密后产生密码,并取前 6 码为一次性密码。
有关 MOTP 的原文介绍可参考: Mobile One Time Passwords
C# 产生使用者注册 QR Code
在範例页面中输入 UserID, Pin 及 密钥,就可以产生 OTP Authenticator 可注册的 QR Code。
Pin 要求为 4 码数字。密钥要求为 16 或 32 码字元,範例中会随机产生 16 码乱数。
接下来看一下程式码部份
HTML
<div class="panel panel-default"><div class="panel-heading">建立使用者</div><div class="panel-body"><div class="row"><div class="col-md-4"><div class="form-group"><label>登入 ID:</label><input type="text" class="form-control" v-model="form.UserID"></div></div><div class="col-md-4"><div class="form-group"><label>PIN (4 个数字):</label><input type="text" class="form-control" v-model="form.UserPin"></div></div><div class="col-md-4"><label>密钥 (16 个字元):</label><div class="input-group"><input type="text" class="form-control" v-model="form.UserKey"><div class="input-group-btn"><button class="btn btn-default" type="button" v-on:click="ChgKey()">更换</button></div></div></div></div><button type="button" class="btn btn-primary" v-on:click="GenUserQRCode()">产生使用者 QR Code</button><br /><img class="img-thumbnail" style="width: 300px;height:300px;" v-bind:src="form.QrCodePath"></div></div>
Javascript
// 产生使用者 QR Code, GenUserQRCode: function () {var self = this;var postData = {};postData['UserID'] = self.form.UserID;postData['UserPin'] = self.form.UserPin;postData['UserKey'] = self.form.UserKey;$.blockUI({ message: '处理中...' });$.ajax({url:'@Url.Content("~/Home/GenUserQRCode")',method:'POST',dataType:'json',data: { inModel: postData, __RequestVerificationToken: self.GetToken() },success: function (datas) {if (datas.ErrMsg != '') {alert(datas.ErrMsg);$.unblockUI();return;}self.form.QrCodePath = datas.FileWebPath;$.unblockUI();},error: function (err) {alert(err.responseText);$.unblockUI();},});}// 更换密钥, ChgKey: function () {var self = this;var key = self.MarkRan(16);self.form.UserKey = key;}// 随机密钥, MarkRan: function (length) {var result = '';var characters = 'abcdefghijklmnopqrstuvwxyz0123456789';var charactersLength = characters.length;for (var i = 0; i < length; i++) {result += characters.charAt(Math.floor(Math.random() * charactersLength));}return result;}
C# Controller
/// <summary>/// 产生使用者 QR Code/// </summary>/// <param name="inModel"></param>/// <returns></returns>[ValidateAntiForgeryToken]public ActionResult GenUserQRCode(GenUserQRCodeIn inModel){GenUserQRCodeOut outModel = new GenUserQRCodeOut();outModel.ErrMsg = "";if (inModel.UserKey.Length != 16){outModel.ErrMsg = "密钥长度需为 16 码";}if (inModel.UserPin.Length != 4){outModel.ErrMsg = "PIN 长度需为 4 码";}int t = 0;if (int.TryParse(inModel.UserPin, out t) == false){outModel.ErrMsg = "PIN 需为数字";}if (outModel.ErrMsg == ""){// 产生注册资料 For OTP Authenticatorstring motpUser = "<?xml version=\"1.0\" encoding=\"utf-8\"?><SSLOTPAuthenticator><mOTPProfile><ProfileName>{0}</ProfileName><PINType>0</PINType><PINSecurity>0</PINSecurity><Secret>{1}</Secret><AlgorithmMode>0</AlgorithmMode></mOTPProfile></SSLOTPAuthenticator>";motpUser = string.Format(motpUser, inModel.UserID, inModel.UserKey);// QR Code 设定BarcodeWriter bw = new BarcodeWriter{Format = BarcodeFormat.QR_CODE,Options = new QrCodeEncodingOptions //设定大小{Height = 300,Width = 300,}};//产生QRcodevar img = bw.Write(motpUser); //来源网址string FileName = "qrcode.png"; //产生图档名称Bitmap myBitmap = new Bitmap(img);string FileWebPath = Server.MapPath("~/") + FileName; //完整路径myBitmap.Save(FileWebPath, ImageFormat.Png);string FileWebUrl = Url.Content("~/") + FileName; // 产生网页可看到的路径outModel.FileWebPath = FileWebUrl;}// 输出jsonContentResult resultJson = new ContentResult();resultJson.ContentType = "application/json";resultJson.Content = JsonConvert.SerializeObject(outModel); ;return resultJson;}
程式码使用到 QR Code 元件,使用 NuGet 安装 ZXing.Net 元件,安装方法可参考: [C#]QR Code 网址产生与解析
此段程式码要先产生可读取的 XML 档,例如
再将此 XML 转为 QR Code 即可。
C# Model
public class GenUserQRCodeIn{public string UserID { get; set; }public string UserPin { get; set; }public string UserKey { get; set; }}public class GenUserQRCodeOut{public string ErrMsg { get; set; }public string FileWebPath { get; set; }}
程式产生 QR Code 之后,接下来就要利用 OTP Authenticator App 来操作了。
手机下载安装 OTP Authenticator
App 名称: OTP Authenticator
官网连结: https://www.swiss-safelab.com/en-us/products/otpauthenticator.aspx
App 性质: 免费软体
iOS App Store 下载: https://apps.apple.com/tw/app/otp-authenticator/id915359210
Android APK 下载: https://www.swiss-safelab.com/en-us/community/downloadcenter.aspx?Command=Core_Download&EntryId=1105
OTP Authenticator 是针对 Mobile-OTP,行动装置双因素身份验证规则而开发的免费 App,由 Swiss SafeLab 所开发。
Android 版本在 Google Play 没有连结,若下载连结失效,可至官网重新下载 Apk
OTP Authenticator 注册使用者帐号
打开 OTP Authenticator 后,左侧选单点击「Profiles」
下方点击「Create Profile」
点击「Scan Profile」
扫描网页上提供的 QR Code
完成后即会增加使用者列表
点击名称后,输入注册时的 Pin 4位数字,例如範例上的 「0000」
App 即会产生一次性密码,每 30 秒会更换新密码。此新密码在网页上使用者登入时会用到。
网页使用者登入验证 MOTP
在画面上输入登入ID, MOTP (手机上的一次性密码),再验证登入是否成功。
接下来看一下程式码部份
HTML
<div class="panel panel-default"><div class="panel-heading">验证登入</div><div class="panel-body"><div class="row"><div class="col-md-4"><div class="form-group"><label>登入 ID:</label><input type="text" class="form-control" v-model="form.UserID"></div></div><div class="col-md-4"><div class="form-group"><label>MOTP (6 个字元):</label><input type="text" class="form-control" v-model="form.MOTP"></div></div></div><button type="button" class="btn btn-primary" v-on:click="CheckLogin()">验证登入</button><br /><br /><span style="color:red;">检核结果:{{form.CheckResult}}</span></div></div>
Javascript
// 验证登入, CheckLogin: function () {var self = this;var postData = {};postData['UserID'] = self.form.UserID;postData['UserPin'] = self.form.UserPin;postData['UserKey'] = self.form.UserKey;postData['MOTP'] = self.form.MOTP;$.blockUI({ message: '处理中...' });$.ajax({url:'@Url.Content("~/Home/CheckLogin")',method:'POST',dataType:'json',data: { inModel: postData, __RequestVerificationToken: self.GetToken() },success: function (datas) {if (datas.ErrMsg != '') {alert(datas.ErrMsg);$.unblockUI();return;}self.form.CheckResult = datas.CheckResult;$.unblockUI();},error: function (err) {alert(err.responseText);$.unblockUI();},});}
C# Controller
public decimal timeStampEpoch = (decimal)Math.Round((DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds, 0); //Unix timestamp/// <summary>/// 验证登入/// </summary>/// <param name="inModel"></param>/// <returns></returns>[ValidateAntiForgeryToken]public ActionResult CheckLogin(CheckLoginIn inModel){CheckLoginOut outModel = new CheckLoginOut();outModel.ErrMsg = "";if (inModel.MOTP == null || inModel.MOTP.Length != 6){outModel.ErrMsg = "MOTP 长度需为 6 码";}if (inModel.UserKey.Length != 16){outModel.ErrMsg = "密钥长度需为 16 码";}if (inModel.UserPin.Length != 4){outModel.ErrMsg = "PIN 长度需为 4 码";}int t = 0;if (int.TryParse(inModel.UserPin, out t) == false){outModel.ErrMsg = "PIN 需为数字";}if (outModel.ErrMsg == ""){outModel.CheckResult = "登入失败";String otpCheckValueMD5 = "";decimal timeWindowInSeconds = 60; //1 分钟前的 motp 都检查for (decimal i = timeStampEpoch - timeWindowInSeconds; i <= timeStampEpoch + timeWindowInSeconds; i++){otpCheckValueMD5 = (Md5Hash(((i.ToString()).Substring(0, (i.ToString()).Length - 1) + inModel.UserKey + inModel.UserPin))).Substring(0, 6);if (inModel.MOTP.ToLower() == otpCheckValueMD5.ToLower()){outModel.CheckResult = "登入成功";break;}}}// 输出jsonContentResult resultJson = new ContentResult();resultJson.ContentType = "application/json";resultJson.Content = JsonConvert.SerializeObject(outModel); ;return resultJson;}/// <summary>/// MD5 编码/// </summary>/// <param name="inputString"></param>/// <returns></returns>public string Md5Hash(string inputString){using (MD5 md5 = MD5.Create()){byte[] input = Encoding.UTF8.GetBytes(inputString);byte[] hash = md5.ComputeHash(input);string md5Str = BitConverter.ToString(hash).Replace("-", "");return md5Str;}}
在验证 OTP 时需要确认手机的时区和伺服器时区是一样的,这样才能检查过去时间内有效的 OTP。
检核使用 timestamp 加上密钥及 Pin 用 MD5 加密,再取前 6 码做为一次性密码。
範例中以输入的 OTP 与过去 1 分钟内其中一组密码相等即为登入成功。
C# Model
public class CheckLoginIn{public string UserID { get; set; }public string UserPin { get; set; }public string UserKey { get; set; }public string MOTP { get; set; }}public class CheckLoginOut{public string ErrMsg { get; set; }public string CheckResult { get; set; }}
重点整理
产生使用者注册 QR Code安装 OTP AuthenticatorOTP 扫描 QR Code网页登入验证身份範例下载
付费后可下载此篇文章教学程式码。
相关学习文章
如何避免 MS-SQL 暴力登入攻击 (尝试评估密码时发生错误、找不到符合所提供名称的登入)
[C#]QR Code 製作与 Base 64 编码应用 (附範例)