在企业发展初期,企业使用的系统很少,通常一个或者两个,每个系统都有自己的登录模块,运营人员每天用自己的账号登录,很方便。
但随着企业的发展,用到的系统随之增多,运营人员在操作不同的系统时,需要多次登录,而且每个系统的账号都不一样,这对于运营人员
来说,很不方便。于是,就想到是不是可以在一个系统登录,其他系统就不用登录了呢?这就是单点登录要解决的问题。
单点登录英文全称Single 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"]; }
}
}