c#使用谷歌身份验证GoogleAuthenticator

此功能相当于给系统加了个令牌,只有输入对的一组数字才可以验证成功。类似于QQ令牌一样。

 

一丶创建最核心的一个类GoogleAuthenticator

此类包含了生成密钥,验证,将绑定密钥转为二维码。

  1 public class GoogleAuthenticator
  2     {
  3         private readonly static DateTime _epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
  4         private TimeSpan DefaultClockDriftTolerance { get; set; }
  5 
  6         public GoogleAuthenticator()
  7         {
  8             DefaultClockDriftTolerance = TimeSpan.FromMinutes(5);
  9         }
 10 
 11         /// <summary>
 12         /// Generate a setup code for a Google Authenticator user to scan
 13         /// </summary>
 14         /// <param name="issuer">Issuer ID (the name of the system, i.e. 'MyApp'), can be omitted but not recommended https://github.com/google/google-authenticator/wiki/Key-Uri-Format </param>
 15         /// <param name="accountTitleNoSpaces">Account Title (no spaces)</param>
 16         /// <param name="accountSecretKey">Account Secret Key</param>
 17         /// <param name="QRPixelsPerModule">Number of pixels per QR Module (2 pixels give ~ 100x100px QRCode)</param>
 18         /// <returns>SetupCode object</returns>
 19         public SetupCode GenerateSetupCode(string issuer, string accountTitleNoSpaces, string accountSecretKey, int QRPixelsPerModule)
 20         {
 21             byte[] key = Encoding.UTF8.GetBytes(accountSecretKey);
 22             return GenerateSetupCode(issuer, accountTitleNoSpaces, key, QRPixelsPerModule);
 23         }
 24 
 25         /// <summary>
 26         /// Generate a setup code for a Google Authenticator user to scan
 27         /// </summary>
 28         /// <param name="issuer">Issuer ID (the name of the system, i.e. 'MyApp'), can be omitted but not recommended https://github.com/google/google-authenticator/wiki/Key-Uri-Format </param>
 29         /// <param name="accountTitleNoSpaces">Account Title (no spaces)</param>
 30         /// <param name="accountSecretKey">Account Secret Key as byte[]</param>
 31         /// <param name="QRPixelsPerModule">Number of pixels per QR Module (2 = ~120x120px QRCode)</param>
 32         /// <returns>SetupCode object</returns>
 33         public SetupCode GenerateSetupCode(string issuer, string accountTitleNoSpaces, byte[] accountSecretKey, int QRPixelsPerModule)
 34         {
 35             if (accountTitleNoSpaces == null) { throw new NullReferenceException("Account Title is null"); }
 36             accountTitleNoSpaces = RemoveWhitespace(accountTitleNoSpaces);
 37             string encodedSecretKey = Base32Encoding.ToString(accountSecretKey);
 38             string provisionUrl = null;
 39             provisionUrl = String.Format("otpauth://totp/{2}:{0}?secret={1}&issuer={2}", accountTitleNoSpaces, encodedSecretKey.Replace("=",""), UrlEncode(issuer));
 40 
 41 
 42 
 43             using (QRCodeGenerator qrGenerator = new QRCodeGenerator())
 44             using (QRCodeData qrCodeData = qrGenerator.CreateQrCode(provisionUrl, QRCodeGenerator.ECCLevel.M))
 45             using (QRCode qrCode = new QRCode(qrCodeData))
 46             using (Bitmap qrCodeImage = qrCode.GetGraphic(QRPixelsPerModule))
 47             using (MemoryStream ms = new MemoryStream())
 48             {
 49                 qrCodeImage.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
 50 
 51                 return new SetupCode(accountTitleNoSpaces, encodedSecretKey, String.Format("data:image/png;base64,{0}", Convert.ToBase64String(ms.ToArray())));
 52             }
 53 
 54         }
 55 
 56         private static string RemoveWhitespace(string str)
 57         {
 58             return new string(str.Where(c => !Char.IsWhiteSpace(c)).ToArray());
 59         }
 60 
 61         private string UrlEncode(string value)
 62         {
 63             StringBuilder result = new StringBuilder();
 64             string validChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";
 65 
 66             foreach (char symbol in value)
 67             {
 68                 if (validChars.IndexOf(symbol) != -1)
 69                 {
 70                     result.Append(symbol);
 71                 }
 72                 else
 73                 {
 74                     result.Append('%' + String.Format("{0:X2}", (int)symbol));
 75                 }
 76             }
 77 
 78             return result.ToString().Replace(" ", "%20");
 79         }
 80 
 81         public string GeneratePINAtInterval(string accountSecretKey, long counter, int digits = 6)
 82         {
 83             return GenerateHashedCode(accountSecretKey, counter, digits);
 84         }
 85 
 86         internal string GenerateHashedCode(string secret, long iterationNumber, int digits = 6)
 87         {
 88             byte[] key = Encoding.UTF8.GetBytes(secret);
 89             return GenerateHashedCode(key, iterationNumber, digits);
 90         }
 91 
 92         internal string GenerateHashedCode(byte[] key, long iterationNumber, int digits = 6)
 93         {
 94             byte[] counter = BitConverter.GetBytes(iterationNumber);
 95 
 96             if (BitConverter.IsLittleEndian)
 97             {
 98                 Array.Reverse(counter);
 99             }
100 
101             HMACSHA1 hmac = new HMACSHA1(key);
102 
103             byte[] hash = hmac.ComputeHash(counter);
104 
105             int offset = hash[hash.Length - 1] & 0xf;
106 
107             // Convert the 4 bytes into an integer, ignoring the sign.
108             int binary =
109                 ((hash[offset] & 0x7f) << 24)
110                 | (hash[offset + 1] << 16)
111                 | (hash[offset + 2] << 8)
112                 | (hash[offset + 3]);
113 
114             int password = binary % (int)Math.Pow(10, digits);
115             return password.ToString(new string('0', digits));
116         }
117 
118         private long GetCurrentCounter()
119         {
120             return GetCurrentCounter(DateTime.UtcNow, _epoch, 30);
121         }
122 
123         private long GetCurrentCounter(DateTime now, DateTime epoch, int timeStep)
124         {
125             return (long)(now - epoch).TotalSeconds / timeStep;
126         }
127 
128         public bool ValidateTwoFactorPIN(string accountSecretKey, string twoFactorCodeFromClient)
129         {
130             return ValidateTwoFactorPIN(accountSecretKey, twoFactorCodeFromClient, DefaultClockDriftTolerance);
131         }
132 
133         public bool ValidateTwoFactorPIN(string accountSecretKey, string twoFactorCodeFromClient, TimeSpan timeTolerance)
134         {
135             var codes = GetCurrentPINs(accountSecretKey, timeTolerance);
136             return codes.Any(c => c == twoFactorCodeFromClient);
137         }
138 
139         public string[] GetCurrentPINs(string accountSecretKey, TimeSpan timeTolerance)
140         {
141             List<string> codes = new List<string>();
142             long iterationCounter = GetCurrentCounter();
143             int iterationOffset = 0;
144 
145             if (timeTolerance.TotalSeconds > 30)
146             {
147                 iterationOffset = Convert.ToInt32(timeTolerance.TotalSeconds / 30.00);
148             }
149 
150             long iterationStart = iterationCounter - iterationOffset;
151             long iterationEnd = iterationCounter + iterationOffset;
152 
153             for (long counter = iterationStart; counter <= iterationEnd; counter++)
154             {
155                 codes.Add(GeneratePINAtInterval(accountSecretKey, counter));
156             }
157 
158             return codes.ToArray();
159         }
160     }

其中GenerateSetupCode 这个方法是用于把绑定的密钥直接转成二维码图片,然后再转成base64图片 输出再页面上,这样在APP上直接用扫一扫即可绑定。

 

二丶由于生成的密钥不可以直接使用,需要进行Base32进行编码。下面是Base32Encoding类

  1     public class Base32Encoding
  2     {
  3         /// <summary>
  4         /// Base32 encoded string to byte[]
  5         /// </summary>
  6         /// <param name="input">Base32 encoded string</param>
  7         /// <returns>byte[]</returns>
  8         public static byte[] ToBytes(string input)
  9         {
 10             if (string.IsNullOrEmpty(input))
 11             {
 12                 throw new ArgumentNullException("input");
 13             }
 14 
 15             input = input.TrimEnd('='); //remove padding characters
 16             int byteCount = input.Length * 5 / 8; //this must be TRUNCATED
 17             byte[] returnArray = new byte[byteCount];
 18 
 19             byte curByte = 0, bitsRemaining = 8;
 20             int mask = 0, arrayIndex = 0;
 21 
 22             foreach (char c in input)
 23             {
 24                 int cValue = CharToValue(c);
 25 
 26                 if (bitsRemaining > 5)
 27                 {
 28                     mask = cValue << (bitsRemaining - 5);
 29                     curByte = (byte)(curByte | mask);
 30                     bitsRemaining -= 5;
 31                 }
 32                 else
 33                 {
 34                     mask = cValue >> (5 - bitsRemaining);
 35                     curByte = (byte)(curByte | mask);
 36                     returnArray[arrayIndex++] = curByte;
 37                     curByte = (byte)(cValue << (3 + bitsRemaining));
 38                     bitsRemaining += 3;
 39                 }
 40             }
 41 
 42             //if we didn't end with a full byte
 43             if (arrayIndex != byteCount)
 44             {
 45                 returnArray[arrayIndex] = curByte;
 46             }
 47 
 48             return returnArray;
 49         }
 50 
 51         /// <summary>
 52         /// byte[] to Base32 string, if starting from an ordinary string use Encoding.UTF8.GetBytes() to convert it to a byte[]
 53         /// </summary>
 54         /// <param name="input">byte[] of data to be Base32 encoded</param>
 55         /// <returns>Base32 String</returns>
 56         public static string ToString(byte[] input)
 57         {
 58             if (input == null || input.Length == 0)
 59             {
 60                 throw new ArgumentNullException("input");
 61             }
 62 
 63             int charCount = (int)Math.Ceiling(input.Length / 5d) * 8;
 64             char[] returnArray = new char[charCount];
 65 
 66             byte nextChar = 0, bitsRemaining = 5;
 67             int arrayIndex = 0;
 68 
 69             foreach (byte b in input)
 70             {
 71                 nextChar = (byte)(nextChar | (b >> (8 - bitsRemaining)));
 72                 returnArray[arrayIndex++] = ValueToChar(nextChar);
 73 
 74                 if (bitsRemaining < 4)
 75                 {
 76                     nextChar = (byte)((b >> (3 - bitsRemaining)) & 31);
 77                     returnArray[arrayIndex++] = ValueToChar(nextChar);
 78                     bitsRemaining += 5;
 79                 }
 80 
 81                 bitsRemaining -= 3;
 82                 nextChar = (byte)((b << bitsRemaining) & 31);
 83             }
 84 
 85             //if we didn't end with a full char
 86             if (arrayIndex != charCount)
 87             {
 88                 returnArray[arrayIndex++] = ValueToChar(nextChar);
 89                 while (arrayIndex != charCount) returnArray[arrayIndex++] = '='; //padding
 90             }
 91 
 92             return new string(returnArray);
 93         }
 94 
 95         private static int CharToValue(char c)
 96         {
 97             int value = (int)c;
 98 
 99             //65-90 == uppercase letters
100             if (value < 91 && value > 64)
101             {
102                 return value - 65;
103             }
104             //50-55 == numbers 2-7
105             if (value < 56 && value > 49)
106             {
107                 return value - 24;
108             }
109             //97-122 == lowercase letters
110             if (value < 123 && value > 96)
111             {
112                 return value - 97;
113             }
114 
115             throw new ArgumentException("Character is not a Base32 character.", "c");
116         }
117 
118         private static char ValueToChar(byte b)
119         {
120             if (b < 26)
121             {
122                 return (char)(b + 65);
123             }
124 
125             if (b < 32)
126             {
127                 return (char)(b + 24);
128             }
129 
130             throw new ArgumentException("Byte is not a value Base32 value.", "b");
131         }
132     }

 

 

三丶主程序里面直接调用方法

1  private SetupCode Google(string key, string Guids)
2  {
3     GoogleAuthenticator gat = new GoogleAuthenticator();
4     return gat.GenerateSetupCode("Supported Giving", key, Guids, 5);
5  }

 

//key系统的账号,Guid是进行加密的字符串,要求唯一,不然密钥会重复,所以这里使用Guid.   2为二维码的大小约120x120px。

SetupCode结果类为

 public class SetupCode
    {
        public string Account { get; internal set; }
        public string AccountSecretKey { get; internal set; }
        public string ManualEntryKey { get; internal set; }
        /// <summary>
        /// Base64-encoded PNG image
        /// </summary>
        public string QrCodeSetupImageUrl { get; internal set; }
}
ManualEntryKey 是手机绑定的密钥。如果想手动输入密钥绑定就使用此字符串。
QrCodeSetupImageUrl 是将密钥转成的二维码图片

 

 下载这个APP

进入APP后直接绑定,就会出现一下界面,即为绑定成功,然后我们就可以使用此令牌验证了。

 

 

 

验证方法

//Guids 之前生成密钥的字符,此时当做唯一键来查询,CheckCode为手机上动态的6位验证吗。校验成功会返回true

GoogleAuthenticator gat = new GoogleAuthenticator();
var result = gat.ValidateTwoFactorPIN(parameters["Guids"].ToString(), parameters["CheckCode"].ToString());
if (result)
{
return "True";
}
else
{
return "False";
}

 

这样功能就完成了。

 

posted @ 2019-09-29 16:18  心如大海  阅读(3372)  评论(1编辑  收藏  举报