Skip to content

Commit

Permalink
OAuthServer登录成功后根据code先换取token,再请求用户信息
Browse files Browse the repository at this point in the history
  • Loading branch information
nnhy committed Feb 9, 2018
1 parent 8f38f4e commit fd2400b
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 25 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ bld/
/XCode/Sharding/Config
/XCode/Demo/Config
/XCode/Membership
/Keys
1 change: 1 addition & 0 deletions NewLife.Core/NewLife.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@
<Compile Include="Web\OAuth\QQClient.cs" />
<Compile Include="Web\OAuthServer.cs" />
<Compile Include="Web\OAuthConfig.cs" />
<Compile Include="Web\TokenProvider.cs" />
<Compile Include="Yun\AMap.cs" />
<Compile Include="Yun\BaiduMap.cs" />
<Compile Include="Web\Js.cs" />
Expand Down
4 changes: 2 additions & 2 deletions NewLife.Core/Security/DSAHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ namespace NewLife.Security
public static class DSAHelper
{
#region 产生密钥
/// <summary>产生非对称密钥对</summary>
/// <summary>产生非对称密钥对(私钥和公钥)</summary>
/// <param name="keySize">密钥长度,默认1024位强密钥</param>
/// <returns></returns>
/// <returns>私钥和公钥</returns>
public static String[] GenerateKey(Int32 keySize = 1024)
{
var dsa = new DSACryptoServiceProvider(keySize);
Expand Down
9 changes: 8 additions & 1 deletion NewLife.Core/Web/OAuthClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ public virtual String Authorize(String redirect, String state = null, Uri baseUr
_state = state;

var url = GetUrl(AuthUrl);
WriteLog("Authorize {0}", url);
if (!state.IsNullOrEmpty()) WriteLog("Authorize {0}", url);

return url;
}
Expand Down Expand Up @@ -197,6 +197,13 @@ public virtual String GetAccessToken(String code)
if (dic.ContainsKey("expires_in")) Expire = DateTime.Now.AddSeconds(dic["expires_in"].Trim().ToInt());
if (dic.ContainsKey("refresh_token")) RefreshToken = dic["refresh_token"].Trim();

// 如果响应区域包含用户信息,则增加用户地址
if (UserUrl.IsNullOrEmpty() && dic.ContainsKey("scope"))
{
var ss = dic["scope"].Trim().Split(",");
if (ss.Contains("UserInfo")) UserUrl = "userinfo?access_token={token}";
}

OnGetInfo(dic);
}
Items = dic;
Expand Down
59 changes: 45 additions & 14 deletions NewLife.Core/Web/OAuthServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,22 @@ public class OAuthServer
{
#region 属性
private ICache Cache { get; } = NewLife.Caching.Cache.Default;

/// <summary>令牌提供者</summary>
public TokenProvider TokenProvider { get; set; }

/// <summary>令牌有效期。默认24小时</summary>
public Int32 Expire { get; set; } = 24 * 3600;
#endregion

#region 静态
/// <summary>实例</summary>
public static OAuthServer Instance { get; set; } = new OAuthServer();
#endregion

#region 构造
#endregion

#region 方法
/// <summary>验证用户身份</summary>
/// <remarks>
Expand All @@ -32,9 +41,9 @@ public class OAuthServer
/// <returns></returns>
public virtual Int32 Authorize(String appid, String redirect_uri, String response_type = null, String scope = null, String state = null)
{
if (appid.IsNullOrEmpty()) throw new ArgumentNullException(nameof(appid));
if (redirect_uri.IsNullOrEmpty()) throw new ArgumentNullException(nameof(redirect_uri));
if (response_type.IsNullOrEmpty()) response_type = "code";
//if (appid.IsNullOrEmpty()) throw new ArgumentNullException(nameof(appid));
//if (redirect_uri.IsNullOrEmpty()) throw new ArgumentNullException(nameof(redirect_uri));
//if (response_type.IsNullOrEmpty()) response_type = "code";

if (!response_type.EqualIgnoreCase("code")) throw new NotSupportedException(nameof(response_type));

Expand All @@ -56,7 +65,7 @@ public virtual Int32 Authorize(String appid, String redirect_uri, String respons
}
while (!Cache.Add("Model:" + key, model, 20 * 60));

if (Log != null) WriteLog("Authorize key={0} {1}", key, model.ToJson(false));
if (Log != null) WriteLog("Authorize {2} key={0} {1}", key, model.ToJson(false), appid);

return key;
}
Expand All @@ -73,8 +82,14 @@ public virtual String GetResult(Int32 key, IManageUser user)

Cache.Remove(k);

// 保存用户信息
model.User = user;
//// 保存用户信息
//model.User = user;
var prv = TokenProvider;
if (prv == null) prv = TokenProvider = new TokenProvider();
if (prv.Key.IsNullOrEmpty()) prv.ReadKey("..\\Keys\\OAuth.prvkey", true);

// 建立令牌
model.Token = prv.Encode(user.Name, DateTime.Now.AddSeconds(Expire));

// 随机code,并尝试加入缓存
var code = "";
Expand All @@ -84,34 +99,50 @@ public virtual String GetResult(Int32 key, IManageUser user)
}
while (!Cache.Add("Code:" + code, model, 20 * 60));

if (Log != null) WriteLog("key={0} code={1}", key, code);
if (Log != null) WriteLog("{2} key={0} code={1}", key, code, model.AppID);

var url = model.Uri;
if (url.Contains("?"))
url += "&";
else
url += "?";
url += "code=" + code;
if (!model.State.IsNullOrEmpty()) url += "state=" + model.State;
if (!model.State.IsNullOrEmpty()) url += "&state=" + model.State;

return url;
}

/// <summary>根据Code获取用户信息</summary>
/// <summary>根据Code获取令牌</summary>
/// <param name="code"></param>
/// <returns></returns>
public virtual IManageUser GetUser(String code)
public virtual String GetToken(String code)
{
var k = "Code:" + code;
var model = Cache.Get<Model>(k);
if (model == null) throw new ArgumentOutOfRangeException(nameof(code));

if (Log != null) WriteLog("GetUser code={0} user={1}", code, model.User);
if (Log != null) WriteLog("Token {0} code={1} token={2}", model.AppID, code, model.Token);

Cache.Remove(k);

return model.User;
return model.Token;
}

///// <summary>根据Token获取用户信息</summary>
///// <param name="token"></param>
///// <returns></returns>
//public virtual IManageUser GetUser(String token)
//{
// var k = "Code:" + token;
// var model = Cache.Get<Model>(k);
// if (model == null) throw new ArgumentOutOfRangeException(nameof(token));

// if (Log != null) WriteLog("GetUser code={0} user={1}", token, model.User);

// Cache.Remove(k);

// return model.User;
//}
#endregion

#region 内嵌
Expand All @@ -123,9 +154,9 @@ class Model
public String Scope { get; set; }
public String State { get; set; }

//public String Token { get; set; }
public String Token { get; set; }

public IManageUser User { get; set; }
//public IManageUser User { get; set; }
}
#endregion

Expand Down
96 changes: 96 additions & 0 deletions NewLife.Core/Web/TokenProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System;
using System.IO;
using NewLife.Security;

namespace NewLife.Web
{
/// <summary>令牌提供者</summary>
public class TokenProvider
{
#region 属性
/// <summary>密钥。签发方用私钥,验证方用公钥</summary>
public String Key { get; set; }
#endregion

#region 方法
/// <summary>读取密钥</summary>
/// <param name="file">文件</param>
/// <param name="generate">是否生成</param>
/// <returns></returns>
public Boolean ReadKey(String file, Boolean generate = false)
{
if (file.IsNullOrEmpty()) return false;

file = file.GetFullPath();
if (File.Exists(file))
{
Key = File.ReadAllText(file);

if (!Key.IsNullOrEmpty()) return true;
}

if (!generate || !file.EndsWithIgnoreCase(".prvkey")) return false;

var ss = DSAHelper.GenerateKey();
File.WriteAllText(file.EnsureDirectory(true), ss[0]);
file = Path.ChangeExtension(file, ".pubkey");
File.WriteAllText(file, ss[1]);

Key = ss[0];

return true;
}

/// <summary>编码用户和有效期得到令牌</summary>
/// <param name="user">用户</param>
/// <param name="expire">有效期</param>
/// <returns></returns>
public String Encode(String user, DateTime expire)
{
if (user.IsNullOrEmpty()) throw new ArgumentNullException(nameof(user));
if (expire.Year < 2000) throw new ArgumentOutOfRangeException(nameof(expire));
if (Key.IsNullOrEmpty()) throw new ArgumentNullException(nameof(Key));

var dt = new DateTime(1970, 1, 1);
var secs = (Int32)(expire - dt).TotalSeconds;

// 拼接数据并签名
var data = (user + "," + secs).GetBytes();
var sig = DSAHelper.Sign(data, Key);

// Base64拼接数据和签名
return data.ToUrlBase64() + "." + sig.ToUrlBase64();
}

/// <summary>令牌解码得到用户和有效期</summary>
/// <param name="token">令牌</param>
/// <param name="expire">有效期</param>
/// <returns></returns>
public String Decode(String token, out DateTime expire)
{
if (token.IsNullOrEmpty()) throw new ArgumentNullException(nameof(token));
if (Key.IsNullOrEmpty()) throw new ArgumentNullException(nameof(Key));
expire = DateTime.MinValue;

// Base64拆分数据和签名
var p = token.IndexOf('.');
var data = token.Substring(0, p).ToBase64();
var sig = token.Substring(p + 1).ToBase64();

// 验证签名
//if (!DSAHelper.Verify(data, Key, sig)) throw new InvalidOperationException("签名验证失败!");
if (!DSAHelper.Verify(data, Key, sig)) return null;

// 拆分数据和有效期
var str = data.ToStr();
p = str.LastIndexOf(',');

var user = str.Substring(0, p);
var secs = str.Substring(p + 1).ToInt();
expire = new DateTime(1970, 1, 1).AddSeconds(secs);

return user;
}
#endregion
}
}
61 changes: 53 additions & 8 deletions NewLife.Cube/Controllers/SsoController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ public virtual ActionResult LoginInfo(String code, String state)
var prov = Provider;
var client = prov.GetClient(name);

client.WriteLog("LoginInfo name={0} code={1} state={2}", name, code, state);

// 构造redirect_uri,部分提供商(百度)要求获取AccessToken的时候也要传递
var redirect = prov.GetRedirect(Request);
client.Authorize(redirect);
Expand Down Expand Up @@ -207,7 +209,7 @@ public virtual ActionResult Authorize(String client_id, String redirect_uri, Str
// 如果已经登录,直接返回。否则跳到登录页面
var user = prov?.Current;
if (user != null)
url = sso.GetResult(key, user as IManageUser);
url = sso.GetResult(key, user);
else
url = prov.LoginUrl.AppendReturn("~/Sso/Auth2/" + key);

Expand All @@ -232,7 +234,7 @@ public virtual ActionResult Auth2(Int32 id)
// code 授权码,子系统凭借该代码来索取用户信息
// state 子系统传过来的用户状态数据,原样返回

var url = sso.GetResult(id, user as IManageUser);
var url = sso.GetResult(id, user);

return Redirect(url);
}
Expand Down Expand Up @@ -261,14 +263,57 @@ public virtual ActionResult Access_Token(String client_id, String client_secret,
// refresh_token 刷新令牌
// openid 用户唯一标识

var user = OAuthServer.Instance.GetUser(code);
var sso = OAuthServer.Instance;
try
{
var token = OAuthServer.Instance.GetToken(code);

return Json(new
// 返回UserInfo告知客户端可以请求用户信息
return Json(new
{
//userid = user.ID,
//username = user.Name,
//nickname = user.NickName,
access_token = token,
expires_in = sso.Expire,
scope = "basic,UserInfo",
}, JsonRequestBehavior.AllowGet);
}
catch (Exception ex)
{
userid = user.ID,
username = user.Name,
user.NickName,
}, JsonRequestBehavior.AllowGet);
return Json(new { error = ex.GetTrue().Message }, JsonRequestBehavior.AllowGet);
}
}

/// <summary>3,根据token获取用户信息</summary>
/// <param name="access_token">访问令牌</param>
/// <returns></returns>
[AllowAnonymous]
public virtual ActionResult UserInfo(String access_token)
{
if (access_token.IsNullOrEmpty()) throw new ArgumentNullException(nameof(access_token));

try
{
var sso = OAuthServer.Instance;
var username = sso.TokenProvider.Decode(access_token, out var expire);
if (username.IsNullOrEmpty()) throw new Exception("非法访问令牌");
if (expire < DateTime.Now) throw new Exception("令牌已过期");

var user = Provider?.Provider?.FindByName(username);
if (user == null) throw new Exception("用户不存在");

return Json(new
{
userid = user.ID,
username = user.Name,
nickname = user.NickName,
}, JsonRequestBehavior.AllowGet);
}
catch (Exception ex)
{
return Json(new { error = ex.GetTrue().Message }, JsonRequestBehavior.AllowGet);
}
}

/// <summary>4,注销登录</summary>
Expand Down

0 comments on commit fd2400b

Please sign in to comment.