.NET Core5.0 JWT鉴权SSO单点登录

JWT

JWT全称“JSON Web Token”,是基于JSON的用户身份认证的令牌。可跨域身份认证,所以JWT很适合做分布式的鉴权,单点登录(SingleSign,SSO)。
jwt有三部分组成用符号"."隔开,
HEADER:token头,描述token是什么类型,加密方式是什么,把json内容转为base64
PAYLOAD:内容,是暴露出来的信息,不可存敏感信息,把json内容转为base64
SIGNATURE:签名,按token头的加密方式把HEADER和PAYLOAD的信息加密生成签名,下面是官网上面的介绍,地址:https://jwt.io/

jwt的token是不可以篡改的,虽然前两部分的内容可以base64解码之后就能看到明文,但由于第三部分签名是把前两部分内容用一个密钥加密的,验证的时候也是把前两部分内容再次加密和原来签名对比是否一致,若内容被篡改了,则两次签名不一致校验不通过。

image.png

SSO

问题一:同一个公司的系统 ,不如果每个系统都有一套自己的用户名密码,那用户记
得头都大了啊。所以这时产生了一个鉴权中心,全部系统用同一套用户信息,同一个地方登录。
问题二:用同一套用户信息可以了,但如果进每个系统都要输一次账户密码登录还是很麻烦的。所以这里还要一处登录 ,处处登录,登录了其中一个系统,进入其它系统的时候不需要登录

效果如下图所示


image.png

用户在sso中心登录后的token在站点A,站点B都能使用,并且站点A,站点B和sso中心不需要通讯也能自己鉴别token是否是有效的。
怎么做到站点和sso串联呢?具体的流程是用户打开站点A,发现未登录 ,那站点A会跳转到sso中心登录并且把自己的url带上,sso中心登录成功后,跳转回站点带过来的url并把token也带上。
那站点A登录成功了,站点B怎么共享这个Token呢,做法是,sso中心登录成功的时候同时存一份Token到cookie(或localstorage等地方),当用户进入站点B的时候,发现没登录,跳转到sso中心带上自己的url,sso中心发现cookie有token了,直接跳转回站点B的url并把token带上,这样站点B就能实现token共享和自动登录了。


image.png

代码实现

鉴权中心核心代码

新建一个AuthenticationCenter项目
新建一个AuthenticatinController控制器

 /// <summary>
    /// 鉴权相关
    /// </summary>
    public class AuthenticationController : Controller
    {
        /// <summary>
        /// 登录界面
        /// </summary>
        /// <param name="redirectUrl">登录成功后跳转页面</param>
        /// <returns></returns>
        [HttpGet]
        public IActionResult Login(string redirectUrl)
        {
            //取出登录token
            string token = string.Empty;
            //从cookie把token取出来
             Request.Cookies.TryGetValue("identity_token", out token);

            //实际还需验证token是否有效
            if(!string.IsNullOrEmpty(token))
            {
                //已经登录过,直接跳转回网站
                redirectUrl += $"?token={token}";
                return Redirect(redirectUrl);
            }

            ViewBag.redirectUrl = redirectUrl;
            //没登录过,展示登录界面
            return View();
        }
        [HttpPost]
        public IActionResult Login(string account, string password, string redirectUrl)
        {
            if (account == "admin" && password == "123456") //实际数据库校验
            {
                User user = new User()
                {
                    userId = 1000,
                    account = account,
                    userName = "张三",
                    age = 18,
                    email = "zhangsan@qq.com"
                };
                string token = new JWTService().GetJwtToken(user);
                //把token写入cookie
                Response.Cookies.Append("identity_token", token);
                //登录成功跳转到对应系统
                redirectUrl+= $"?token={token}";
                return Redirect(redirectUrl);
            }
            else
            {
                //登录失败
                return Content("登录失败");
            }
        }
        /// <summary>
        /// 退出登录
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        public IActionResult LogOut()
        {
            //退出登录 
            HttpContext.Response.Cookies.Delete("identity_token");
            //回到登录页面
            return RedirectToAction("Login");
        }

    }

Login的view视图

<form action="/Authentication/Login" method="post">
    <input type="hidden" name="redirectUrl" value="@ViewBag.redirectUrl"/>
   账号: <input  name="account"/><br />
    密码:<input name="password" /><br />
    <input type="submit" value="登录" />
</form>

其它相关类

 /// <summary>
    /// jwt处理
    /// </summary>
    public class JWTService
    {
        public string GetJwtToken(User user)
        {
            var claims = new[]
            {
                   new Claim(ClaimTypes.Name, user.userId.ToString()),
                   new Claim("userName", user.userName),
                   new Claim("account", user.account),
                   new Claim("age", user.age.ToString()),
                   new Claim("email", user.email)
            };
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(ConstOptions.SecurityKey));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            /**
             *  Claims (Payload)
                 Claims 部分包含了一些跟这个 token 有关的重要信息。 JWT 标准规定了一些字段,下面节选一些字段:

                 iss: The issuer of the token,token 是给谁的
                 sub: The subject of the token,token 主题
                 exp: Expiration Time。 token 过期时间,Unix 时间戳格式
                 iat: Issued At。 token 创建时间, Unix 时间戳格式
                 jti: JWT ID。针对当前 token 的唯一标识
                 除了规定的字段外,可以包含其他任何 JSON 兼容的字段。
             */
            var token = new JwtSecurityToken(
               issuer: ConstOptions.Issuer,
               audience: ConstOptions.Audience,
               claims: claims,
               expires: DateTime.Now.AddMinutes(60),//有效期
               notBefore: DateTime.Now,//开始有效时间,可以往后设置
               signingCredentials: creds);
            string returnToken = new JwtSecurityTokenHandler().WriteToken(token);
            return returnToken;
        }
    }
 /// <summary>
    /// 常量
    /// </summary>
    public static class ConstOptions
    {
        //密钥
        public const string SecurityKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDI2a2EJ7m872v0afyoSDJT2o";
        public const string Issuer = "jwtIssuer"; //发送者
        public const string Audience = "jwtAudience";//签收者
        public const string identityToken = "identity_token";
    }
  public class User
    {
        public int userId { get; set; }
        public string account { get; set; }
        public string userName { get; set; }
        public int age { get; set; }
        public string email { get; set; }
    }

上面sso的登录功能就完成了,打开Login页面就能获取到Token了。

站点核心代码

新建一个站点A
修改startup.cs文件,在ConfigureServices方法里加上

 //jwt校验对称加密
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options => {
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        //Audience,Issuer,SecurityKey的值要和sso的一致

                        //JWT有一些默认的属性,就是给鉴权时就可以筛选了
                        ValidateIssuer = true,//是否验证Issuer
                        ValidateAudience = true,//是否验证Audience
                        ValidateLifetime = true,//是否验证失效时间
                        ValidateIssuerSigningKey = true,//是否验证SecurityKey
                        ValidAudience = ConstOptions.Audience,//
                        ValidIssuer = ConstOptions.Issuer,//Issuer,这两项和前面签发jwt的设置一致
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(ConstOptions.SecurityKey)),//拿到SecurityKey
                                                                                                                
                    };
                });

在Configure方法里加上

  app.UseAuthentication();//鉴权:解析信息--就是读取token,解密token

新建一个UserController

 public class UserController : Controller
    {
        /// <summary>
        /// 登录
        /// </summary>
        /// <returns></returns>
        [AllowAnonymous]
        public IActionResult Login()
        {
            //重定向到 sso登录
            return Redirect("http://localhost:5000/Authentication/Login?redirectUrl=http://localhost:27271/User/LoginSuccess");
        }
        [AllowAnonymous]
        public string LoginSuccess(string token)
        {
            return $"登录成功,token:{token}";
        }
        [Authorize]
        public IActionResult GetString()
        {
            return Content("ok");
        }
        [Authorize]
        public IActionResult GetName()
        {
            var str = string.Empty;
            var Claims = HttpContext.User.Identities.First().Claims;

            var name = Claims.Where(s => s.Type == "userName").First().Value;
            foreach (var item in Claims)
            {
                str += $"{item.Type}:{item.Value},";
            }
            return Content(str);
        }
    }

其它相关类

 public class ConstOptions
    {
        public const string SecurityKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDI2a2EJ7m872v0afyoSDJT2o";
        public const string Issuer = "jwtIssuer";
        public const string Audience = "jwtAudience";
    }

密钥要和sso中心的保持一致,上面的5000端口是sso的端口,27271端口是站点端口。

测试效果:
1.gif

一开始我输入的是端口为27271站点A的User/Login,然后直接跳转到端口5000的sso中心,然后登录完又回到27271的登录成功界面,第二次我再在地址上输入端口为27271站点A的User/Login,然后不需要输入密码就回到自己的登录成功页面上了,两次的Token一致,如果把A的代码复制一份变成站点B,也一样是不用登录的。

验证Token是否有效,获取用户信息

借助postman,站点A的/User/GetString方法头部加上 [Authorize]特性后表明要校验身份,这时候header没传token,会报401没鉴权
image.png

把刚才登录成功的token放到header里面的Authorization请求,就能看到成功返回ok,注意Token前面要加上Bearer和一个空格,这个是标准格式。
image.png

站点A如果读取Token的信息呢,上面说了Token的内容是中间的payload,只是用了base64编码了,直接截取base64解码就能拿到了,或者像上面的/User/GetName里面获取。
image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容