近来在研究一个单点登录的例子 , 以下是一些笔记,写错的地方请见谅。
Token【登录凭证】
网络身份验证 ,常常需要输入账号和密码, 在第一次登录成功之后,会生成一个Token字符串用于记录登录成功的信息, 这个Token可以在一定时间范围内用于证明自己曾经登录成功。当这段时间范围过期之后Token会失效。
Token可以翻译为令牌,我觉得译为“登录凭证”更加贴切。(古代锦衣卫获得皇帝的颁发之后,拿着这个牌子就可以到处走,不需要再次身份验证!现代时尚一点的解释就是,家里的汽车安装了高速公路ETC之后,路路通,无需在高速公路出口排队交钱)
JWT【全称 Json Web Token】
这是实现Token字符的一种规范格式,这个格式由A.B.C三部分组成,大概如下,
A部分:是头部header , 里面是一些加密方式,字符长度的内容;
B部分:是载荷Payload , 里面是过期时间,用户信息等内容
c部分:是加密签名Signature
这个图片想要实现的功能是: 多个网页系统都由一个统一的登陆入口进入 , 登录成功之后 ,不同系统之间无需重复输入密码验证。
比分说: 我打开BPM网页登陆之后 ,然后切换到ERP系统,无需重新输入一次密码,可以之间打开我的ERP账号。
首先制作一个叫sso 的asp.net mvc 站点 , 里面最重要的就是 Login 页面 , controller和 html如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using SSOTest2.DataAccess;
using ClassLibraryToken;
using SSOTest2.Models;
namespace SSOTest2.Controllers
{
public class LogInController : Controller
{
//登录页面
public ActionResult Index()
{
string sysCode = Request.Params["syscode"];
ViewBag.sysCode = sysCode;
return View();
}
//登录按钮
public ActionResult Sign(string account, string password)
{
UserLoginDAL dal = new UserLoginDAL();
if (dal.CheckPwd(account, password) == true)
{
string tokenVAL = TokenHelper.EncodingToken();
HttpCookie cookie = new HttpCookie("ssologin", tokenVAL);
HttpContext.Response.Cookies.Add(cookie);
//去到首页(syscode对应的页面)
var wheresys = SystemListInfo.AllConfig().Where(x => x.SystemCode.ToUpper() == Request.Form["sc"].ToUpper()).FirstOrDefault();
return Redirect(wheresys.SystemHomePage);
}
else
{
//不通过,回到登录界面
return Content("账号密码错误");
}
}
}
}
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
</head>
<body>
<div>
<form action="/login/sign" method="post">
<p>
用户名:
<input type="text" id="account" name="account" />
</p>
<p>
密码:
<input type="text" id="password" name="password" />
</p>
<p>
system:
<input type="text" value="@Html.Raw(ViewBag.sysCode)" id="sc" name="sc" />
</p>
<input type="submit" value="登录" />
</form>
</div>
</body>
</html>
可以看到Controller 里面有TokenHelp的帮助类 , 这个类就是用于 创建或者解释 Token(JWT)字符串的类,代码如下:
首先你要使用Nuget工具获取JWT的类库 , 如图
然后编写TokenHelp代码 ,值得一提的是, EncodingToken是把明文信息加密成A.B.C三部分加密信息。 DecodingToken 就是把A.B.C 解码成原始的信息。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using JWT;
using JWT.Algorithms;
using JWT.Serializers;
namespace ClassLibraryToken
{
public class TokenHelper
{
//自定义密钥
private const string SecretKey = "abcd1234";
//Token 加密
public static string EncodingToken()
{
string tokenSTR = "";
try
{
TokenInfo tokenInfo = new TokenInfo();
Dictionary<string, object> payload = new Dictionary<string, object>();
payload.Add("aud", tokenInfo.aud);
payload.Add("exp", tokenInfo.exp);
payload.Add("iat", tokenInfo.iat);
payload.Add("iss", tokenInfo.iss);
payload.Add("jti", tokenInfo.jti);
payload.Add("sub", tokenInfo.sub);
IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); // symmetric
IJsonSerializer serializer = new JsonNetSerializer();
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
tokenSTR = encoder.Encode(payload, SecretKey);
Console.WriteLine(tokenSTR);
}
catch (Exception ex)
{
//Console.WriteLine(ex.Message);
ClassLibraryLog.LogHelp.Error(ex.Message);
}
return tokenSTR;
}
//Token 解密
public static string DecodingToken(string token)
{
string tokenJson = "";
try
{
IJsonSerializer serializer = new JsonNetSerializer();
var provider = new UtcDateTimeProvider();
IJwtValidator validator = new JwtValidator(serializer, provider);
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); // symmetric
IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithm);
tokenJson = decoder.Decode(token, SecretKey, verify: true);
}
catch (Exception ex)
{
//Console.WriteLine(ex.Message);
ClassLibraryLog.LogHelp.Error(ex.Message);
}
return tokenJson;
}
}
public class TokenInfo
{
/*
iss:发行人 "iss":"xxxxx"
iat:发布时间 "iat": 1535967430,
exp:到期时间 "exp": 1535974630,
sub:面向的用户 "sub": "www.xxx.com",
aud:用户 "aud":"xxxx"
nbf:在此之前不可用 "nbf": 1535967430,
jti:唯一标记 "jti": "9f10e796726e332cec401c569969e13e"
除以上默认字段外,我们还可以自定义私有字段,例如 UserInfo
*/
/// <summary>
/// 签发者
/// </summary>
public string iss { get; set; }
/// <summary>
/// 当前时间戳
/// </summary>
public double iat { get; set; }
/// <summary>
/// 过期时间戳
/// </summary>
public double exp { get; set; }
/// <summary>
/// 接收方
/// </summary>
public string aud { get; set; }
/// <summary>
/// 面向的用户
/// </summary>
public string sub { get; set; }
/// <summary>
/// 唯一标识
/// </summary>
public string jti { get; set; }
public TokenInfo()
{
iss = "颁发者非必填";
aud = "接收方非必填";
sub = "面向的用户,一般是某个角色的管理员";
iat = (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds;
exp = iat + 120; //截至+120秒
jti = Guid.NewGuid().ToString();
}
}
}
等用户填写 userID 和 password 正确之后 , 浏览器会在cookie 里面记录一个 Token
HttpCookie cookie = new HttpCookie("ssologin", tokenVAL);
其他系统的home page url 进入的时候 ,先查看是否存在这个cookie ,
如果有的话 ,就正常进入 ; 没有的话,就跳转到login页面
其他系统的syscode和home page 可以写在一个config文件里面 ,我这里就简单演示
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace SSOTest2.Models
{
/// <summary>
/// 系统配置表
/// </summary>
public class ExternalSysConf
{
/// <summary>
/// 系统编号
/// </summary>
public string SystemCode { get; set; }
/// <summary>
/// 系统密钥
/// </summary>
public string SecretKey { get; set; }
/// <summary>
/// 系统首页地址
/// </summary>
public string SystemHomePage { get; set; }
}
/// <summary>
/// 系统详细配置(其实这个应该写在config里面,我就省略了)
/// </summary>
public class SystemListInfo
{
public static List<ExternalSysConf> AllConfig()
{
List<ExternalSysConf> conf = new List<ExternalSysConf>();
conf.Add(new ExternalSysConf { SecretKey = "aasdasdw", SystemCode = "ERP", SystemHomePage = "http://localhost:62494/home" });
conf.Add(new ExternalSysConf { SecretKey = "gfdfgdfg", SystemCode = "BPM", SystemHomePage = "http://localhost:59259/home?hello" });
conf.Add(new ExternalSysConf { SecretKey = "cvcvdfew", SystemCode = "MAA", SystemHomePage = "http://192.168.62.2/home/index" });
conf.Add(new ExternalSysConf { SecretKey = "34321212", SystemCode = "TYS", SystemHomePage = "http://192.168.34.6/home/index" });
conf.Add(new ExternalSysConf { SecretKey = "yt445frg", SystemCode = "TTS", SystemHomePage = "http://192.168.98.5/home/index" });
//这些配置可以写在XML文件里面,我这里只是演示
return conf;
}
}
}
写道这里,sso部分已经做好了 。
然后再来看ERP的一些页面 , 比如ERP的homeController是:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace ERP.Controllers
{
public class HomeController : Controller
{
// ERP: Home
public ActionResult Index()
{
HttpCookie ssoCookie = Request.Cookies.Get("ssologin");
if (ssoCookie != null)
{
string jwt = ssoCookie.Value;
ClassLibraryLog.LogHelp.Info("ERP Home Index:" + jwt);
string kai = ClassLibraryToken.TokenHelper.DecodingToken(jwt);
ClassLibraryLog.LogHelp.Info("jiekai:" + kai);
if (string.IsNullOrEmpty(kai) == true)
{
return Redirect("http://localhost:60683/login/index?syscode=erp");
}
}
else
{
ClassLibraryLog.LogHelp.Warn("no ssoLogin cookie");
return Redirect("http://localhost:60683/login/index?syscode=erp");
}
return View();
}
}
}
可以看到在进入index方法之后 ,首先是判断 cookie是否存在 , 然后再处理其他事情。
如何每一个action 都是这么写的话,显然是很累赘的,这时候就可以使用到MVC里面的过滤器。
下面我在BPM网站里面做一个授权过滤器的例子:
先看BPM 的HomeController 代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using BPM.Filters;
namespace BPM.Controllers
{
public class HomeController : Controller
{
// BPM: Home
[MyAuthorizationFilter]
public ActionResult Index()
{
return View();
}
}
}
可以发现 代码里面只有一行[MyAuthorzationFilter] 来检查cookie , 没有了erp网站的累赘代码 , 过滤器的代码实现如下
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace BPM.Filters
{
public class MyAuthorizationFilter : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
bool checkFlag = false;
try
{
HttpCookie ssoCookie = httpContext.Request.Cookies.Get("ssologin");
if (ssoCookie != null)
{
string jwt = ClassLibraryToken.TokenHelper.DecodingToken(ssoCookie.Value);
//ClassLibraryLog.LogHelp.Info(jwt);
if (string.IsNullOrEmpty(jwt) == false)
{
checkFlag = true;
}
else {
checkFlag = false;
}
}
else {
checkFlag = false;
ClassLibraryLog.LogHelp.Warn("BPM-filter find no ssoLogin token");
}
}
catch (Exception ex)
{
//Console.WriteLine(ex.Message);
ClassLibraryLog.LogHelp.Error(ex.Message);
}
//return base.AuthorizeCore(httpContext);
return checkFlag;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
//登陆页的url应该写在config里面
filterContext.HttpContext.Response.Redirect("http://localhost:60683/login/index?syscode=bpm");
base.HandleUnauthorizedRequest(filterContext);
}
}
}
过滤器里面可以通过override来重新方法 , 在visual studio 里面编辑的时候 ,只要敲override,然后按一下空格键,就能看到具体的信息。
写的这里, Token的实现和过滤器的使用,就写完了 ,希望大家能看懂。