.net core JWT SSO单点登陆详解

在企业发展初期,企业使用的系统很少,通常一个或者两个,每个系统都有自己的登录模块,运营人员每天用自己的账号登录,很方便。

但随着企业的发展,用到的系统随之增多,运营人员在操作不同的系统时,需要多次登录,而且每个系统的账号都不一样,这对于运营人员

来说,很不方便。于是,就想到是不是可以在一个系统登录,其他系统就不用登录了呢?这就是单点登录要解决的问题。

单点登录英文全称S­in­gle Sign On,简称就是SSO。它的解释是:在多个应用系统中,只需要登录一次,就可以访问其他相互信任的应用系统。

如图所示,图中有4个系统,分别是Application1、Application2、Application3、和SSO。Application1、Application2、Application3没有登录模块,而SSO只有登录模块,没有其他的业务模块,当Application1、Application2、Application3需要登录时,将跳到SSO系统,SSO系统完成登录,其他的应用系统也就随之登录了。这完全符合我们对单点登录(SSO)的定义

技术实现

在说单点登录(SSO)的技术实现之前,我们先说一说普通的登录认证机制。

如上图所示,我们在浏览器(Browser)中访问一个应用,这个应用需要登录,我们填写完用户名和密码后,完成登录认证。这时,我们在这个用户的session中标记登录状态为yes(已登录),同时在浏览器(Browser)中写入Cookie,这个Cookie是这个用户的唯一标识。下次我们再访问这个应用的时候,请求中会带上这个Cookie,服务端会根据这个Cookie找到对应的session,通过session来判断这个用户是否登录。如果不做特殊配置,这个Cookie的名字叫做jsessionid,值在服务端(server)是唯一的。

同域下的单点登录

一个企业一般情况下只有一个域名,通过二级域名区分不同的系统。比如我们有个域名叫做:a.com,同时有两个业务系统分别为:app1.a.com和app2.a.com。我们要做单点登录(SSO),需要一个登录系统,叫做:sso.a.com。

这个时候可以通过一级域名来控制登陆写入cookie凭证,在web.config文件中设置加密方式,二级域名进行解密获取cookie信息来完成单点登陆。

但是其实这种只能算是伪单点登陆。

不同域下的单点登录

同域下的单点登录是巧用了Cookie顶域的特性。如果是不同域呢?不同域之间Cookie是不共享的,怎么办?

这个问题也困扰了很久,直到发现JWT机制,才真正解决了这一难题。至于JWT到底是什么,有兴趣的朋友可以去看看我之前写的两篇文章或者是百度看看。接下来直接上代码思路:

首先创建用户类:User

 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; }
    }

JWT方法类:JWTService

 public class JWTService
    {
        public static IConfiguration Configuration { get; set; }
        static JWTService()
        {
            //ReloadOnChange = true 当appsettings.json被修改时重新加载            
            Configuration = new ConfigurationBuilder()
            .Add(new JsonConfigurationSource 
{ Path = "appsettings.json", ReloadOnChange = true })
            .Build();
        }

        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
(DecodeJwt.Configuration["Jwt:Secret"]));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512);
            /**
             *  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: DecodeJwt.Configuration["Jwt:Issuer"],
               audience: DecodeJwt.Configuration["Jwt:Audience"],
               claims: claims,
               expires: DateTime.Now.AddMinutes(2),//有效期
               notBefore: DateTime.Now,//开始有效时间,可以往后设置
               signingCredentials: creds);
            string returnToken = new JwtSecurityTokenHandler().WriteToken(token);
            return returnToken;
        }
    }

创建登陆方法,主系统登陆

        /// <summary>
        /// SSO单点登陆 jwt写入cookie数据
        /// </summary>
        /// <param name="account"></param>
        /// <param name="password"></param>
        /// <param name="redirectUrl"></param>
        /// <returns></returns>
        [HttpPost]
            public IActionResult WriteInToken(User user1)
        {
            if (user1.userName == "admin" || user1.userName == "张三"
 && user1.passWord == "123456") //实际数据库校验
            {
                User user = new User()
                {
                    userId = 1000,
                    account = user1.userName,
                    userName = user1.userName,
                    age = 18,
                    email = "zhangsan@qq.com"
                };
                string token = new JWTService().GetJwtToken(user);

                //把token写入cookie
                Response.Cookies.Append("identity_token", token);
                //登录成功跳转到对应系统
                user1.redirectUrl += $"https://jwttwo.openwebsite.cn/Home
/LoginYZ?Authorization={token}";
               // return Redirect(user1.redirectUrl);
                //return RedirectToAction("LoginYZ", "Home");
                return Json(token);
            }
            else
            {
                //登录失败
                return Json("登录失败");
            }
        }
        /// <summary>
        /// 退出登录
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        public IActionResult LogOut()
        {
            //回到登录页面
            return RedirectToAction("Login");
        }

在跳转子系统中全局文件(或启动文件)中配置拦截器等验证

var builder = WebApplication.CreateBuilder(args);
//builder.WebHost.UseUrls("http://192.168.1.110:9010");
// Add services to the container.
builder.Configuration.AddJsonFile("appsettings.json");
builder.Services.AddControllersWithViews();
//注入服务  WebAPI访问控制
//1 携带token访问,返回了想要的数据    2 未携带token,返回401
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters()
        {
            ValidateIssuer = true,//是否验证Issuer
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidateAudience = true,//是否验证Audience
            ValidAudience = builder.Configuration["Jwt:Audience"],
            ValidateLifetime = true,//是否验证失效时间
            ClockSkew = TimeSpan.FromSeconds(5),
            ValidateIssuerSigningKey = true,//是否验证SecurityKey
            IssuerSigningKey = new SymmetricSecurityKey
(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"])),
        };

        options.Events = new JwtBearerEvents()
        {
            //首次收到协议消息时调用。
            OnMessageReceived = context =>
            {
                  //携带Authorization
                              string token = context.Request.Query["Authorization"];
                              if (!string.IsNullOrEmpty(token))
                              {
                                  string jmtoken = DecodeJwt.decode(token);
                                  JWT wT = JsonConvert.DeserializeObject<JWT>(jmtoken);

                                  DateTime dtStart = JWTService.StampToDateTime(wT.exp);
                                  if (dtStart >= DateTime.Now)
                                  {
                                      context.Token = token;
                                  }
                              }
                             // context.Token = context.Request.Query["Authorization"];
                              return Task.CompletedTask;
            },
            //如果在请求处理过程中身份验证失败,则调用
            OnAuthenticationFailed = context =>
            {
                //如果过期,则把<是否过期>添加到,返回头信息中
                if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                {
                    //context.Response.Headers.Add("Token-Expired", "true");
                    //自定义自己想要返回的数据结果,我这里要返回的是Json对象
,通过引用Newtonsoft.Json库进行转换
                    var payload = JsonConvert.SerializeObject(new { Code = "402"
, Message = "很抱歉,Token无效或已过期" });
                    //自定义返回的数据类型
                    context.Response.ContentType = "application/json";
                    //自定义返回状态码,默认为401 我这里改成 200
                    context.Response.StatusCode = StatusCodes.Status200OK;
                    //输出Json数据结果
                    context.Response.WriteAsync(payload);
                   // return Task.FromResult(0);
                }
                return Task.CompletedTask;
            }
            ,
            //在安全令牌已通过验证并生成 ClaimsIdentity 后调用。
            OnTokenValidated = context =>
            { 
           //携带Authorization
                              string token = context.Request.Query["Authorization"];
                              //判断token不为空时
                              if (!string.IsNullOrEmpty(token))
                              {
                                  string jmtoken = DecodeJwt.decode(token);

                                  User user = JsonConvert.DeserializeObject<User>(jmtoken);
                                  UserContext userContext = new UserContext
                                  {
                                      User = new UserInfo
                                      {
                                          UserName = user.userName,
                                          UserCode = user.passWord
                                      }
                                  };
                                  context.HttpContext.Items["Properties"] = userContext;
                              }
                              else
                              {
                                  UserContext userContext = new UserContext { };
                                  context.HttpContext.Items["Properties"] = userContext;
                              }
                              return Task.CompletedTask;
           
              
            }
            //,
            ////在将质询发送回调用方之前调用。
            //OnChallenge = context =>
            //{
            //    //此处代码为终止.Net Core默认的返回类型和数据结果,这个很重要哦,必须
            //    context.HandleResponse();

            //    //自定义自己想要返回的数据结果,我这里要返回的是Json对象
,通过引用Newtonsoft.Json库进行转换
            //    var payload = JsonConvert.SerializeObject(new { Code = "401"
, Message = "很抱歉,您无权访问该接口" });

            //        //自定义返回的数据类型
            //        context.Response.ContentType = "application/json";
            //        //自定义返回状态码,默认为401 我这里改成 200
            //        context.Response.StatusCode = StatusCodes.Status200OK;

            //    //context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            //    //输出Json数据结果
            //    context.Response.WriteAsync(payload);

            //    return Task.FromResult(0);
            //}
        };
    });
var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");

    app.UseHsts();
}
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

///添加jwt验证
app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

这里跳转的子系统拦截器以及在全局写入获取的token信息已经完成了,下面就可以在子系统中获取信息了。

        /// <summary>
        /// SSO二级页面获取数据
        /// </summary>
        /// <returns></returns>
        //验证token是否有效
        [Authorize]
        public IActionResult LoginYZ()
        {
            //获取信息
            string name = this.UserContext.User.UserName;
            //没登录过,展示登录界面
            return View();
        }

控制器中继承BaseQJController

 public class BaseQJController : Controller
    {
        /// <summary>
        /// 当前的用户信息。
        /// </summary>
        protected IUserContext UserContext
        {
            get { return (IUserContext)this.HttpContext.Items["Properties"]; }
        }

    }

注意:JWT返回token密文由三部分组成,单纯返回容易被破解。可以把返回密文做二次加密,这样就能保证信息的安全性了。

记住每次请求前携带:Authorization参数

为您推荐

发表评论

您的电子邮箱地址不会被公开。