前言
最近輪到我在小組晨會來分享知識點,突然想到單點登入,準備來分享下如何實作單點登入,是以有了下文。實作方案以及代碼可能寫得不是很嚴謹,有漏洞的地方或者錯誤的地方歡迎大家指正。
剛開始頭腦中沒有思路,直接在部落格園裡面看看别人是如何來實作的,看了幾篇文章發現,發現解決方案有點問題,或者說不算實作了單點登入
名稱定義
為了友善說明先說明幾個文中出現的名詞的含義:
P站:統一登入授權驗證中心,demo中 域名是www.passport.com:801
A站:處于不同域名下的測試網站,demo中 域名是www.a.com:802
B站:處于不同域名下的測試網站,demo中 域名是www.b.com:803
Token:使用者通路P站的秘鑰
Ticket:用來儲存使用者資訊的加密字元串
單點登入
通路A站需要登陸的就跳轉P站中進行登陸,P站登陸之後跳轉回至A站,使用者再次通路B站需要登陸的頁面,使用者不需要進行登陸操作就可以正常通路。
實作思路
未登入使用者通路A站,首先會重定向跳轉至P站授權中心,P站首先通過檢測Cookie來判斷目前不是處于登陸狀态,就跳轉至登陸頁面進行登陸操作,登陸成功之後把使用者資訊加密ticket附在A的請求位址上傳回,A站通過解密ticket來擷取使用者資訊,解密成功并存進Session中(這樣使用者在A中就處于登陸狀态了),通路通過;當使用者再次通路B站的時候,對于B站來說,使用者是處于未登入狀态,則同樣會重定向跳轉至P站授權中心,P站檢測Cookie,判斷目前使用者處于登陸狀态,就把目前使用者資訊加密成ticket附在B的請求位址上傳回,後面的操作就和A站處理一樣;這樣都登陸之後再次通路A或者B,A和B中Session中都存儲了使用者資訊,就不會再次請求P站了。
簡單關系圖

A站主要邏輯
使用者首先通路A站,A站中會生成Token,并存入Cache中。Token是A通路P的鑰匙,P在回調給A的時候需要攜帶這個Token。A請求P,P驗證Token,P回調A,A檢測Token是否是發送出去的Token,驗證之後Token即失效,防止Token被再次使用。
Token的生成是通過取時間戳的不同字段進行MD5加密生成,當然這裡可以再加個鹽進行防僞。
1 /// <summary>
2 /// 生成秘鑰
3 /// </summary>
4 /// <param name="timestamp"></param>
5 /// <returns></returns>
6 public static string CreateToken(DateTime timestamp)
7 {
8 StringBuilder securityKey = new StringBuilder(MD5Encypt(timestamp.ToString("yyyy")));
9 securityKey.Append(MD5Encypt(timestamp.ToString("MM")));
10 securityKey.Append(MD5Encypt(timestamp.ToString("dd")));
11 securityKey.Append(MD5Encypt(timestamp.ToString("HH")));
12 securityKey.Append(MD5Encypt(timestamp.ToString("mm")));
13 securityKey.Append(MD5Encypt(timestamp.ToString("ss")));
14 return MD5Encypt(securityKey.ToString());
15 }
P回調A的時候進行,A中對Token進行校驗,校驗不成功則請求P站統一授權驗證。
1 /// <summary>
2 /// 授權枚舉
3 /// </summary>
4 public enum AuthCodeEnum
5 {
6 Public = 1,
7 Login = 2
8 }
9
10 /// <summary>
11 /// 授權過濾器
12 /// </summary>
13 public class AuthAttribute : ActionFilterAttribute
14 {
15 /// <summary>
16 /// 權限代碼
17 /// </summary>
18 public AuthCodeEnum Code { get; set; }
19
20 /// <summary>
21 /// 驗證權限
22 /// </summary>
23 /// <param name="filterContext"></param>
24 public override void OnActionExecuting(ActionExecutingContext filterContext)
25 {
26 var request = filterContext.HttpContext.Request;
27 var session = filterContext.HttpContext.Session;
28 //如果存在身份資訊
29 if (Common.CurrentUser == null)
30 {
31 if (Code == AuthCodeEnum.Public)
32 {
33 return;
34 }
35 string reqToken = request["Token"];
36 string ticket = request["Ticket"];
37 Cache cache = HttpContext.Current.Cache;
38 //沒有擷取到Token或者Token驗證不通過或者沒有取到從P回調的ticket 都進行再次請求P
39 TokenModel tokenModel= cache.Get(ConstantHelper.TOKEN_KEY)==null?null:(TokenModel)cache.Get(ConstantHelper.TOKEN_KEY);
40 if (string.IsNullOrEmpty(reqToken) || tokenModel == null || tokenModel.Token!= reqToken ||
41 string.IsNullOrEmpty(ticket))
42 {
43 DateTime timestamp = DateTime.Now;
44 string returnUrl = request.Url.AbsoluteUri;
45 tokenModel = new TokenModel
46 {
47 TimeStamp = timestamp,
48 Token = AuthernUtil.CreateToken(timestamp)
49 };
50 //Token加入緩存中,設計過期時間為20分鐘
51 cache.Add(ConstantHelper.TOKEN_KEY, tokenModel, null, DateTime.Now.AddMinutes(20),Cache.NoSlidingExpiration,CacheItemPriority.Default, null);
52 filterContext.Result = new ContentResult
53 {
54 Content = GetAuthernScript(AuthernUtil.GetAutherUrl(tokenModel.Token, timestamp), returnUrl)
55 };
56 return;
57 }
58 LoginService service = new LoginService();
59 var userinfo = service.GetUserInfo(ticket);
60 session[ConstantHelper.USER_SESSION_KEY] = userinfo;
61 //驗證通過,cache中去掉Token,保證每個token隻能使用一次
62 cache.Remove(ConstantHelper.TOKEN_KEY);
63 }
64 }
65
66 /// <summary>
67 /// 生成跳轉腳本
68 /// </summary>
69 /// <param name="authernUrl">統一授權位址</param>
70 /// <param name="returnUrl">回調位址</param>
71 /// <returns></returns>
72 private string GetAuthernScript(string authernUrl, string returnUrl)
73 {
74 StringBuilder sbScript = new StringBuilder();
75 sbScript.Append("<script type='text/javascript'>");
76 sbScript.AppendFormat("window.location.href='{0}&returnUrl=' + encodeURIComponent('{1}');", authernUrl, returnUrl);
77 sbScript.Append("</script>");
78 return sbScript.ToString();
79 }
80 }
代碼說明:這裡為了友善設定Token的過期時間,是以使用Cache來存取Token,設定Token的失效時間為兩分鐘,當驗證成功則從cache中移除Token。
調取過濾器
1 [Auth(Code = AuthCodeEnum.Login)]
2 public ActionResult Index()
3 {
4 return View();
5 }
P站主要邏輯
P站收到授權請求,P站首先通過Coookie來判斷是否登陸,未登入則跳轉至登陸頁面進行登陸操作。
1 /// <summary>
2 /// 授權登陸驗證
3 /// </summary>
4 /// <returns></returns>
5 [HttpPost]
6 public ActionResult PassportVertify()
7 {
8 var cookie=Request.Cookies[ConstantHelper.USER_COOKIE_KEY];
9 if (cookie == null ||string.IsNullOrEmpty(cookie.ToString()))
10 {
11 return RedirectToAction("Login", new { ReturnUrl = Request["ReturnUrl"] ,Token= Request["Token"] });
12 }
13 string userinfo = cookie.ToString();
14 var success= passportservice.AuthernVertify(Request["Token"], Convert.ToDateTime(Request["TimeStamp"]));
15 if (!success)
16 {
17 return RedirectToAction("Login", new { ReturnUrl = Request["ReturnUrl"], Token = Request["Token"] });
18 }
19 return Redirect(passportservice.GetReturnUrl(userinfo, Request["Token"],Request["ReturnUrl"]));
20 }
已登陸則驗證Token
1 /// <summary>
2 /// 驗證令牌
3 /// </summary>
4 /// <param name="token">令牌</param>
5 /// <param name="timestamp">時間戳</param>
6 /// <returns></returns>
7 public bool AuthernVertify(string token,DateTime timestamp)
8 {
9 return AuthernUtil.CreateToken(timestamp) == token;
10 }
測試說明
1、修改host
127.0.0.1 www.passport.com
127.0.0.1 www.a.com
127.0.0.1 www.b.com
2、部署IIS
P www.passport.com:801
A www.a.com:802
B www.b.com:803
3、測試賬号和webconfig
<add key="PassportCenterUrl" value="http://www.passport.com:801"/>
使用者名:admin 密碼:123
demo
https://github.com/hexuefengx/study
最後感謝大家的閱讀,這裡推薦一個公衆号 猿大俠的客棧,會不定時的推送分享IT相關技術文章。