.NET Core中JWT+Auth2.0实现SSO,附完整源码(.NET6)

🚀 优质资源分享 🚀

学习路线指引(点击解锁)知识定位人群定位
🧡 Python实战微信订餐小程序 🧡进阶级本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。
💛Python量化交易实战💛入门级手把手带你打造一个易扩展、更安全、效率更高的量化交易系统
目录
  • 一、简介
  • 二、实现目标
    • 1、一处登录,全部登录
    • 2、一处退出,全部退出
    • 3、双token机制
  • 三、功能实现和核心代码
    • 1、一处登录,全部登录实现
    • 2、一处退出,全部退出实现
    • 3、双token机制实现
  • 四、效果演示

回到顶部# 一、简介

单点登录(SingleSignOn,SSO)

指的是在多个应用系统中,只需登录一次,就可以访问其他相互信任的应用系统。

JWT

Json Web Token,这里不详细描述,简单说是一种认证机制。

Auth2.0

Auth2.0是一个认证流程,一共有四种方式,这里用的是最常用的授权码方式,流程为:

1、系统A向认证中心先获取一个授权码code。

2、系统A通过授权码code获取 token,refresh_token,expiry_time,scope。

token:系统A向认证方获取资源请求时带上的token。

refresh_token:token的有效期比较短,用来刷新token用。

expiry_time:token过期时间。

scope:资源域,系统A所拥有的资源权限,比喻scope:[“userinfo”],系统A只拥有获取用户信息的权限。像平时网站接入微信登录也是只能授权获取微信用户基本信息。

这里的SSO都是公司自己的系统,都是获取用户信息,所以这个为空,第三方需要接入我们的登录时才需要scope来做资源权限判断。

回到顶部# 二、实现目标

1、一处登录,全部登录

流程图为:

1、浏览器访问A系统,发现A系统未登录,跳转到统一登录中心(SSO),带上A系统的回调地址,

地址为:https://sso.com/SSO/Login?redirectUrl=https://web1.com/Account/LoginRedirect&clientId=web1,输入用户名,密码,登录成功,生成授权码code,创建一个全局会话(cookie,redis),带着授权码跳转回A系统地址:https://web1.com/Account/LoginRedirect?AuthCode=xxxxxxxx。然后A系统的回调地址用这个AuthCode调用SSO获取token,获取到token,创建一个局部会话(cookie,redis),再跳转到https://web1.com。这样A系统就完成了登录。

2、浏览器访问B系统,发现B系统没登录,跳转到统一登录中心(SSO),带上B系统的回调地址,

地址为:https://sso.com/SSO/Login?redirectUrl=https://web2.com/Account/LoginRedirect&clientId=web2,SSO有全局会话证明已经登录过,直接用全局会话code获取B系统的授权码code,

带着授权码跳转回B系统https://web2.com/Account/LoginRedirect?AuthCode=xxxxxxxx,然后B系统的回调地址用这个AuthCode调用SSO获取token,获取到token创建一个局部会话(cookie,redis),再跳转到https://web2.com。整个过程不用输入用户名密码,这些跳转基本是无感的,所以B就自动登录好了。

为什么要多个授权码而不直接带token跳转回A,B系统呢?因为地址上的参数是很容易被拦截到的,可能token会被截取到,非常不安全

还有为了安全,授权码只能用一次便销毁,A系统的token和B系统的token是独立的,不能相互访问。

2、一处退出,全部退出

流程图为:

A系统退出,把自己的会话删除,然后跳转到SSO的退出登录地址:https://sso.com/SSO/Logout?redirectUrl=https://web1.com/Account/LoginRedirect&clientId=web1,SSO删除全局会话,然后调接口删除获取了token的系统,然后在跳转到登录页面,https://sso.com/SSO/Login?redirectUrl=https://web1.com/Account/LoginRedirect&clientId=web1,这样就实现了一处退出,全部退出了。

3、双token机制

也就是带刷新token,为什么要刷新token呢?因为基于token式的鉴权授权有着天生的缺陷

token设置时间长,token泄露了,重放攻击。

token设置短了,老是要登录。问题还有很多,因为token本质决定,大部分是解决不了的。

所以就需要用到双Token机制,SSO返回token和refreshToken,token用来鉴权使用,refreshToken刷新token使用,

比喻token有效期10分钟,refreshToken有效期2天,这样就算token泄露了,最多10分钟就会过期,影响没那么大,系统定时9分钟刷新一次token,

这样系统就能让token滑动过期了,避免了频繁重新登录。

回到顶部#  三、功能实现和核心代码

1、一处登录,全部登录实现

建三个项目,SSO的项目,web1的项目,web2项目。

这里的流程就是web1跳转SSO输用户名登录成功获取code,把会话写到SSO的cookie,然后跳转回来根据code跟SSO获取token登录成功;

然后访问web2跳转到SSO,SSO已经登录,自动获取code跳回web2根据code获取token。

能实现一处登录处处登录的关键是SSO的cookie。

然后这里有一个核心的问题,如果我们生成的token有效期都是24小时,那么web1登录成功,获取的token有效期是24小时,

等到过了12个小时,我访问web2,web2也得到一个24小时的token,这样再过12小时,web1的登录过期了,web2还没过期,

这样就是web2是登录状态,然而web1却不是登录状态需要重新登录,这样就违背了一处登录处处登录的理念。

所以后面获取的token,只能跟第一次登录的token的过期时间是一样的。怎么做呢,就是SSO第一次登录时过期时间缓存下来,后面根据SSO会话获取的code,

换到的token的过期时间都和第一次一样。

SSO项目

SSO项目配置文件appsettings.json中加入web1,web2的信息,用来验证来源和生成对应项目的jwt token,实际项目应该存到数据库。

{"Logging": {"LogLevel": {"Default": "Information","Microsoft.AspNetCore": "Warning"}},"AllowedHosts": "*","AppSetting": {"appHSSettings": [{"domain": "https://localhost:7001","clientId": "web1","clientSecret": "Nu4Ohg8mfpPnNxnXu53W4g0yWLqF0mX2"},{"domain": "https://localhost:7002","clientId": "web2","clientSecret": "pQeP5X9wejpFfQGgSjyWB8iFdLDGHEV8"}]}}

domain:接入系统的域名,可以用来校验请求来源是否合法。

clientId:接入系统标识,请求token时传进来识别是哪个系统。

clientSecret:接入系统密钥,用来生成对称加密的JWT。

建一个IJWTService定义JWT生成需要的方法

 /// /// JWT服务接口/// public interface IJWTService{/// /// 获取授权码/// /// /// /// /// ResponseModel<string> GetCode(string clientId, string userName, string password);/// /// 根据会话Code获取授权码/// /// /// /// ResponseModel<string> GetCodeBySessionCode(string clientId, string sessionCode);/// /// 根据授权码获取Token+RefreshToken/// /// /// Token+RefreshTokenResponseModel GetTokenWithRefresh(string authCode);/// /// 根据RefreshToken刷新Token/// /// /// /// string GetTokenByRefresh(string refreshToken, string clientId);}

建一个抽象类JWTBaseService加模板方法实现详细的逻辑

 /// /// jwt服务/// public abstract class JWTBaseService : IJWTService{protected readonly IOptions \_appSettingOptions;protected readonly Cachelper \_cachelper;public JWTBaseService(IOptions appSettingOptions, Cachelper cachelper){\_appSettingOptions = appSettingOptions;\_cachelper = cachelper;}/// /// 获取授权码/// /// /// /// /// public ResponseModel<string> GetCode(string clientId, string userName, string password){ResponseModel<string> result = new ResponseModel<string>();string code = string.Empty;AppHSSetting appHSSetting = \_appSettingOptions.Value.appHSSettings.Where(s => s.clientId == clientId).FirstOrDefault();if (appHSSetting == null){result.SetFail("应用不存在");return result;}//真正项目这里查询数据库比较if (!(userName == "admin" && password == "123456")){result.SetFail("用户名或密码不正确");return result;}//用户信息CurrentUserModel currentUserModel = new CurrentUserModel{id = 101,account = "admin",name = "张三",mobile = "13800138000",role = "SuperAdmin"};//生成授权码code = Guid.NewGuid().ToString().Replace("-", "").ToUpper();string key = $"AuthCode:{code}";string appCachekey = $"AuthCodeClientId:{code}";//缓存授权码\_cachelper.StringSet(key, currentUserModel, TimeSpan.FromMinutes(10));//缓存授权码是哪个应用的\_cachelper.StringSet<string>(appCachekey, appHSSetting.clientId, TimeSpan.FromMinutes(10));//创建全局会话string sessionCode = $"SessionCode:{code}";SessionCodeUser sessionCodeUser = new SessionCodeUser{expiresTime = DateTime.Now.AddHours(1),currentUser = currentUserModel};\_cachelper.StringSet(sessionCode, currentUserModel, TimeSpan.FromDays(1));//全局会话过期时间string sessionExpiryKey = $"SessionExpiryKey:{code}";DateTime sessionExpirTime = DateTime.Now.AddDays(1);\_cachelper.StringSet(sessionExpiryKey, sessionExpirTime, TimeSpan.FromDays(1));Console.WriteLine($"登录成功,全局会话code:{code}");//缓存授权码取token时最长的有效时间\_cachelper.StringSet($"AuthCodeSessionTime:{code}", sessionExpirTime, TimeSpan.FromDays(1));result.SetSuccess(code);return result;}/// /// 根据会话code获取授权码/// /// /// /// public ResponseModel<string> GetCodeBySessionCode(string clientId, string sessionCode){ResponseModel<string> result = new ResponseModel<string>();string code = string.Empty;AppHSSetting appHSSetting = \_appSettingOptions.Value.appHSSettings.Where(s => s.clientId == clientId).FirstOrDefault();if (appHSSetting == null){result.SetFail("应用不存在");return result;}string codeKey = $"SessionCode:{sessionCode}";CurrentUserModel currentUserModel = \_cachelper.StringGet(codeKey);if (currentUserModel == null){return result.SetFail("会话不存在或已过期", string.Empty);}//生成授权码code = Guid.NewGuid().ToString().Replace("-", "").ToUpper();string key = $"AuthCode:{code}";string appCachekey = $"AuthCodeClientId:{code}";//缓存授权码\_cachelper.StringSet(key, currentUserModel, TimeSpan.FromMinutes(10));//缓存授权码是哪个应用的\_cachelper.StringSet<string>(appCachekey, appHSSetting.clientId, TimeSpan.FromMinutes(10));//缓存授权码取token时最长的有效时间DateTime expirTime = \_cachelper.StringGet($"SessionExpiryKey:{sessionCode}");\_cachelper.StringSet($"AuthCodeSessionTime:{code}", expirTime, expirTime - DateTime.Now);result.SetSuccess(code);return result;}/// /// 根据刷新Token获取Token/// /// /// /// public string GetTokenByRefresh(string refreshToken, string clientId){//刷新Token是否在缓存CurrentUserModel currentUserModel = \_cachelper.StringGet($"RefreshToken:{refreshToken}");if(currentUserModel==null){return String.Empty;}//刷新token过期时间DateTime refreshTokenExpiry = \_cachelper.StringGet($"RefreshTokenExpiry:{refreshToken}");//token默认时间为600sdouble tokenExpiry = 600;//如果刷新token的过期时间不到600s了,token过期时间为刷新token的过期时间if(refreshTokenExpiry>DateTime.Now&&refreshTokenExpiry600)){tokenExpiry = (refreshTokenExpiry - DateTime.Now).TotalSeconds;}//从新生成Tokenstring token = IssueToken(currentUserModel, clientId, tokenExpiry);return token;}/// /// 根据授权码,获取Token/// /// /// /// public ResponseModel GetTokenWithRefresh(string authCode){ResponseModel result = new ResponseModel();string key = $"AuthCode:{authCode}";string clientIdCachekey = $"AuthCodeClientId:{authCode}";string AuthCodeSessionTimeKey = $"AuthCodeSessionTime:{authCode}";//根据授权码获取用户信息CurrentUserModel currentUserModel = \_cachelper.StringGet(key);if (currentUserModel == null){throw new Exception("code无效");}//清除authCode,只能用一次\_cachelper.DeleteKey(key);//获取应用配置string clientId = \_cachelper.StringGet<string>(clientIdCachekey);//刷新token过期时间DateTime sessionExpiryTime = \_cachelper.StringGet(AuthCodeSessionTimeKey);DateTime tokenExpiryTime = DateTime.Now.AddMinutes(10);//token过期时间10分钟//如果刷新token有过期期比token默认时间短,把token过期时间设成和刷新token一样if (sessionExpiryTime > DateTime.Now && sessionExpiryTime < tokenExpiryTime){tokenExpiryTime = sessionExpiryTime;}//获取访问tokenstring token = this.IssueToken(currentUserModel, clientId, (sessionExpiryTime - DateTime.Now).TotalSeconds);TimeSpan refreshTokenExpiry;if (sessionExpiryTime != default(DateTime)){refreshTokenExpiry = sessionExpiryTime - DateTime.Now;}else{refreshTokenExpiry = TimeSpan.FromSeconds(60 * 60 * 24);//默认24小时}//获取刷新tokenstring refreshToken = this.IssueToken(currentUserModel, clientId, refreshTokenExpiry.TotalSeconds);//缓存刷新token\_cachelper.StringSet($"RefreshToken:{refreshToken}", currentUserModel, refreshTokenExpiry);//缓存刷新token过期时间\_cachelper.StringSet($"RefreshTokenExpiry:{refreshToken}",DateTime.Now.AddSeconds(refreshTokenExpiry.TotalSeconds), refreshTokenExpiry);result.SetSuccess(new GetTokenDTO() { token = token, refreshToken = refreshToken, expires = 60 * 10 });Console.WriteLine($"client\_id:{clientId}获取token,有效期:{sessionExpiryTime.ToString("yyyy-MM-dd HH:mm:ss")},token:{token}");return result;}#region private/// /// 签发token/// /// /// /// /// private string IssueToken(CurrentUserModel userModel, string clientId, double second = 600){var claims = new[]{new Claim(ClaimTypes.Name, userModel.name),new Claim("Account", userModel.account),new Claim("Id", userModel.id.ToString()),new Claim("Mobile", userModel.mobile),new Claim(ClaimTypes.Role,userModel.role),};//var appHSSetting = getAppInfoByAppKey(clientId);//var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(appHSSetting.clientSecret));//var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);var creds = GetCreds(clientId);/*** Claims (Payload)Claims 部分包含了一些跟这个 token 有关的重要信息。 JWT 标准规定了一些字段,下面节选一些字段:iss: The issuer of the token,签发主体,谁给的sub: The subject of the token,token 主题aud: 接收对象,给谁的exp: Expiration Time。 token 过期时间,Unix 时间戳格式iat: Issued At。 token 创建时间, Unix 时间戳格式jti: JWT ID。针对当前 token 的唯一标识除了规定的字段外,可以包含其他任何 JSON 兼容的字段。* */var token = new JwtSecurityToken(issuer: "SSOCenter", //谁给的audience: clientId, //给谁的claims: claims,expires: DateTime.Now.AddSeconds(second),//token有效期notBefore: null,//立即生效 DateTime.Now.AddMilliseconds(30),//30s后有效signingCredentials: creds);string returnToken = new JwtSecurityTokenHandler().WriteToken(token);return returnToken;}/// /// 根据appKey获取应用信息/// /// /// private AppHSSetting getAppInfoByAppKey(string clientId){AppHSSetting appHSSetting = \_appSettingOptions.Value.appHSSettings.Where(s => s.clientId == clientId).FirstOrDefault();return appHSSetting;}/// /// 获取加密方式/// /// protected abstract SigningCredentials GetCreds(string clientId);#endregion}

新建类JWTHSService实现对称加密

 /// /// JWT对称可逆加密/// public class JWTHSService : JWTBaseService{public JWTHSService(IOptions options, Cachelper cachelper):base(options,cachelper){}/// /// 生成对称加密签名凭证/// /// /// protected override SigningCredentials GetCreds(string clientId){var appHSSettings=getAppInfoByAppKey(clientId);var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(appHSSettings.clientSecret));var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);return creds;}/// /// 根据appKey获取应用信息/// /// /// private AppHSSetting getAppInfoByAppKey(string clientId){AppHSSetting appHSSetting = \_appSettingOptions.Value.appHSSettings.Where(s => s.clientId == clientId).FirstOrDefault();return appHSSetting;}}

新建JWTRSService类实现非对称加密,和上面的对称加密,只需要一个就可以里,这里把两种都写出来了

/// /// JWT非对称加密/// public class JWTRSService : JWTBaseService{public JWTRSService(IOptions options, Cachelper cachelper):base(options, cachelper){}/// /// 生成非对称加密签名凭证/// /// /// protected override SigningCredentials GetCreds(string clientId){var appRSSetting = getAppInfoByAppKey(clientId);var rsa = RSA.Create();byte[] privateKey = Convert.FromBase64String(appRSSetting.privateKey);//这里只需要私钥,不要begin,不要endrsa.ImportPkcs8PrivateKey(privateKey, out \_);var key = new RsaSecurityKey(rsa);var creds = new SigningCredentials(key, SecurityAlgorithms.RsaSha256);return creds;}/// /// 根据appKey获取应用信息/// /// /// private AppRSSetting getAppInfoByAppKey(string clientId){AppRSSetting appRSSetting = \_appSettingOptions.Value.appRSSettings.Where(s => s.clientId == clientId).FirstOrDefault();return appRSSetting;}}

什么时候用JWT的对称加密,什么时候用JWT的非对称加密呢?

对称加密:双方保存同一个密钥,签名速度快,但因为双方密钥一样,所以安全性比非对称加密低一些。

非对称加密:认证方保存私钥,系统方保存公钥,签名速度比对称加密慢,但公钥私钥互相不能推导,所以安全性高。

所以注重性能的用对称加密,注重安全的用非对称加密,一般是公司的系统用对称加密,第三方接入的话用非对称加密。

web1项目:

appsettings.json存着web1的信息

{"Logging": {"LogLevel": {"Default": "Information","Microsoft.AspNetCore": "Warning"}},"AllowedHosts": "*","SSOSetting": {"issuer": "SSOCenter","audience": "web1","clientId": "web1","clientSecret": "Nu4Ohg8mfpPnNxnXu53W4g0yWLqF0mX2"}
}

Program.cs文件加入认证代码,加入builder.Services.AddAuthentication(。。。和加入app.UseAuthentication(),完整代码如下:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using RSAExtensions;
using SSO.Demo.Web1.Models;
using SSO.Demo.Web1.Utils;
using System.Security.Cryptography;
using System.Text;var builder = WebApplication.CreateBuilder(args);// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddHttpClient();
builder.Services.AddSingleton();
builder.Services.Configure(builder.Configuration.GetSection("AppOptions"));
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>{options.TokenValidationParameters = new TokenValidationParameters{//Audience,Issuer,clientSecret的值要和sso的一致//JWT有一些默认的属性,就是给鉴权时就可以筛选了ValidateIssuer = true,//是否验证IssuerValidateAudience = true,//是否验证AudienceValidateLifetime = true,//是否验证失效时间ValidateIssuerSigningKey = true,//是否验证client secretValidIssuer = builder.Configuration["SSOSetting:issuer"],//ValidAudience = builder.Configuration["SSOSetting:audience"],//Issuer,这两项和前面签发jwt的设置一致IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["SSOSetting:clientSecret"]))//client secret};});#region 非对称加密-鉴权
//var rsa = RSA.Create();
//byte[] publickey = Convert.FromBase64String(AppSetting.PublicKey); //公钥,去掉begin... end ...
rsa.ImportPkcs8PublicKey 是一个扩展方法,来源于RSAExtensions包
//rsa.ImportPkcs8PublicKey(publickey);
//var key = new RsaSecurityKey(rsa);
//var signingCredentials = new SigningCredentials(key, SecurityAlgorithms.RsaPKCS1);//builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
// .AddJwtBearer(options =>
// {
// options.TokenValidationParameters = new TokenValidationParameters
// {
// //Audience,Issuer,clientSecret的值要和sso的一致// //JWT有一些默认的属性,就是给鉴权时就可以筛选了
// ValidateIssuer = true,//是否验证Issuer
// ValidateAudience = true,//是否验证Audience
// ValidateLifetime = true,//是否验证失效时间
// ValidateIssuerSigningKey = true,//是否验证client secret
// ValidIssuer = builder.Configuration["SSOSetting:issuer"],//
// ValidAudience = builder.Configuration["SSOSetting:audience"],//Issuer,这两项和前面签发jwt的设置一致
// IssuerSigningKey = signingCredentials.Key
// };
// });#endregionvar app = builder.Build();
ServiceLocator.Instance = app.Services; //用于手动获取DI对象
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{app.UseExceptionHandler("/Home/Error");// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.app.UseHsts();
}app.UseHttpsRedirection();
app.UseStaticFiles();app.UseRouting();
app.UseAuthentication();//这个加在UseAuthorization 前
app.UseAuthorization();app.MapControllerRoute(name: "default",pattern: "{controller=Home}/{action=Index}/{id?}");app.Run();

然后加接口根据授权code获取token,增加AccountController

 /// /// 用户信息/// public class AccountController : Controller{private IHttpClientFactory \_httpClientFactory;private readonly Cachelper \_cachelper;public AccountController(IHttpClientFactory httpClientFactory, Cachelper cachelper){\_httpClientFactory = httpClientFactory;\_cachelper = cachelper;}/// /// 获取用户信息,接口需要进行权限校验/// /// [MyAuthorize][HttpPost]public ResponseModel GetUserInfo(){ResponseModel user = new ResponseModel();return user;}/// /// 登录成功回调/// /// public ActionResult LoginRedirect(){return View();}//根据authCode获取token[HttpPost]public async Task> GetAccessCode([FromBody] GetAccessCodeRequest request){ResponseModel result = new ResponseModel();//请求SSO获取 tokenvar client = \_httpClientFactory.CreateClient();var param = new { authCode = request.authCode };string jsonData = System.Text.Json.JsonSerializer.Serialize(param);StringContent paramContent = new StringContent(jsonData);//请求sso获取tokenvar response = await client.PostAsync("https://localhost:7000/SSO/GetToken", new StringContent(jsonData, Encoding.UTF8, "application/json"));string resultStr = await response.Content.ReadAsStringAsync();result = System.Text.Json.JsonSerializer.Deserialize>(resultStr);if (result.code == 0) //成功{//成功,缓存token到局部会话string token = result.data.token;string key = $"SessionCode:{request.sessionCode}";string tokenKey = $"token:{token}";\_cachelper.StringSet<string>(key, token, TimeSpan.FromSeconds(result.data.expires));\_cachelper.StringSet<bool>(tokenKey, true, TimeSpan.FromSeconds(result.data.expires));Console.WriteLine($"获取token成功,局部会话code:{request.sessionCode},{Environment.NewLine}token:{token}");}return result;}/// /// 退出登录/// /// /// [HttpPost]public ResponseModel LogOut([FromBody] LogOutRequest request){string key = $"SessionCode:{request.SessionCode}";//根据会话取出tokenstring token = \_cachelper.StringGet<string>(key);if (!string.IsNullOrEmpty(token)){//清除tokenstring tokenKey = $"token:{token}";\_cachelper.DeleteKey(tokenKey);}Console.WriteLine($"会话Code:{request.SessionCode}退出登录");return new ResponseModel().SetSuccess();}}

还有得到的token还没过期,如果我退出登录了,怎么判断这个会话token失效了呢?

这里需要拦截认证过滤器,判断token在缓存中被删除,则认证不通过,增加文件MyAuthorize

  /// /// 拦截认证过滤器/// public class MyAuthorize : Attribute, IAuthorizationFilter{private static Cachelper \_cachelper = ServiceLocator.Instance.GetService();public void OnAuthorization(AuthorizationFilterContext context){string id = context.HttpContext.User.FindFirst("id")?.Value;if(string.IsNullOrEmpty(id)){//token检验失败context.Result = new StatusCodeResult(401); //返回鉴权失败return;}Console.WriteLine("我是Authorization过滤器");//请求的地址var url = context.HttpContext.Request.Path.Value;//获取打印头部信息var heads = context.HttpContext.Request.Headers;//取到token "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoi5byg5LiJIiwiQWNjb3VudCI6ImFkbWluIiwiSWQiOiIxMDEiLCJNb2JpbGUiOiIxMzgwMDEzODAwMCIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IlN1cGVyQWRtaW4iLCJleHAiOjE2NTMwNjA0MDIsImlzcyI6IlNTT0NlbnRlciIsImF1ZCI6IndlYjIifQ.aAi5a0zr\_nLQQaSxSBqEhHZQ6ALFD\_rWn2tnLt38DeA"string token = heads["Authorization"];token = token.Replace("Bearer", "").TrimStart();//去掉 "Bearer "才是真正的tokenif (string.IsNullOrEmpty(token)){Console.WriteLine("校验不通过");return;}//redis校验这个token的有效性,确定来源是sso和确定会话没过期string tokenKey = $"token:{token}";bool isVaid = \_cachelper.StringGet<bool>(tokenKey);//token无效if (isVaid == false){Console.WriteLine($"token无效,token:{token}");context.Result = new StatusCodeResult(401); //返回鉴权失败}}}

然后需要认证的控制器或方法头部加上[MyAuthorize]即能自动认证。

web1需要登录的页面

@{ViewData["Title"] = "Home Page";
}class="text-center"># class="display-4">欢迎来到Web1Learn about ["https://web2.com:7002">跳转到Web2](<span).Learn about "logOut()" href="javascript:void(0);">退出登录.@section Scripts{"</span><span style="color: rgba(128, 0, 0, 1)">~/js/Common.js</span><span style="color: rgba(128, 0, 0, 1)">"</span>>
<span style="color: rgba(0, 0, 0, 1)">getUserInfo()</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">获取用户信息</span>
<span style="color: rgba(0, 0, 0, 1)"> function getUserInfo(){</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">1.cookie是否有 token</span><span style="color: rgba(0, 0, 255, 1)">const</span> token=getCookie(<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">token</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">)console.log(</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">gettoken</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">,token)</span><span style="color: rgba(0, 0, 255, 1)">if</span>(!<span style="color: rgba(0, 0, 0, 1)">token){redirectLogin()}$.ajax({type: </span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">POST</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">,url: </span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">/Account/GetUserInfo</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">,headers:{</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Authorization</span><span style="color: rgba(128, 0, 0, 1)">"</span>:<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">Bearer </span><span style="color: rgba(128, 0, 0, 1)">'</span> +<span style="color: rgba(0, 0, 0, 1)"> token},success: success,error:error});}function success(){console.log(</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">成功</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">)}function error(xhr, exception){</span><span style="color: rgba(0, 0, 255, 1)">if</span>(xhr.status===<span style="color: rgba(128, 0, 128, 1)">401</span>) <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">鉴权失败</span>
<span style="color: rgba(0, 0, 0, 1)"> {console.log(</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">未鉴权</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">)redirectLogin()}}</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">重定向到登录</span>
<span style="color: rgba(0, 0, 0, 1)"> function redirectLogin(){window.location.href</span>=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">https://sso.com:7000/SSO/Login?clientId=web1&amp;redirectUrl=https://web1.com:7001/Account/LoginRedirect</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">}</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">退出登录</span>
<span style="color: rgba(0, 0, 0, 1)"> function logOut(){clearCookie(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">token</span><span style="color: rgba(128, 0, 0, 1)">"</span>) <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">清除cookie token</span>clearCookie(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">refreshToken</span><span style="color: rgba(128, 0, 0, 1)">"</span>) <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">清除cookie refreshToken</span>clearCookie(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">sessionCode</span><span style="color: rgba(128, 0, 0, 1)">"</span>) <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">清除cookie 会话</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">跳转到SSO退出登录</span>window.location.href=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">https://sso.com:7000/SSO/LogOut?clientId=web1&amp;redirectUrl=https://web1.com:7001/Account/LoginRedirect</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">}</span>
}

sso登录完要跳转回web1的页面

@*For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{Layout = null;
}"</span><span style="color: rgba(128, 0, 0, 1)">~/lib/jquery/dist/jquery.min.js</span><span style="color: rgba(128, 0, 0, 1)">"</span>>"</span><span style="color: rgba(128, 0, 0, 1)">~/js/Common.js</span><span style="color: rgba(128, 0, 0, 1)">"</span>><span style="color: rgba(0, 0, 0, 1)">GetAccessToken();</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">根据code获取token</span>
<span style="color: rgba(0, 0, 0, 1)"> function GetAccessToken(){</span><span style="color: rgba(0, 0, 255, 1)">var</span> <span style="color: rgba(0, 0, 255, 1)">params</span>=<span style="color: rgba(0, 0, 0, 1)">GetParam()</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">code</span><span style="color: rgba(0, 0, 255, 1)">var</span> authCode=<span style="color: rgba(0, 0, 255, 1)">params</span>[<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">authCode</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">]</span><span style="color: rgba(0, 0, 255, 1)">var</span> sessionCode=<span style="color: rgba(0, 0, 255, 1)">params</span>[<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">sessionCode</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">]console.log(</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">authcode</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">,authCode)</span><span style="color: rgba(0, 0, 255, 1)">var</span> <span style="color: rgba(0, 0, 255, 1)">params</span>=<span style="color: rgba(0, 0, 0, 1)">{authCode,sessionCode} 
$.ajax({url:</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">/Account/GetAccessCode</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">,type:</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">POST</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,data:JSON.stringify(</span><span style="color: rgba(0, 0, 255, 1)">params</span><span style="color: rgba(0, 0, 0, 1)">),contentType:</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">application/json; charset=utf-8</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,dataType:</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">json</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,success: function(data){console.log(</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">token</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">,data)</span><span style="color: rgba(0, 0, 255, 1)">if</span>(data.code===<span style="color: rgba(128, 0, 128, 1)">0</span>) <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">成功</span>
<span style="color: rgba(0, 0, 0, 1)"> { console.log(</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">设置cookie</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">)</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">把token存到 cookie,过期时间为token有效时间少一分钟</span>setCookie(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">token</span><span style="color: rgba(128, 0, 0, 1)">"</span>,data.data.token,data.data.expires-<span style="color: rgba(128, 0, 128, 1)">60</span>,<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">/</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">刷新token,有效期1天</span>setCookie(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">refreshToken</span><span style="color: rgba(128, 0, 0, 1)">"</span>,data.data.refreshToken,<span style="color: rgba(128, 0, 128, 1)">24</span>*<span style="color: rgba(128, 0, 128, 1)">60</span>*<span style="color: rgba(128, 0, 128, 1)">60</span>,<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">/</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)setCookie(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">SessionCode</span><span style="color: rgba(128, 0, 0, 1)">"</span>,sessionCode,<span style="color: rgba(128, 0, 128, 1)">24</span>*<span style="color: rgba(128, 0, 128, 1)">60</span>*<span style="color: rgba(128, 0, 128, 1)">60</span>,<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">/</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">跳转到主页</span>window.location.href=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">/Home/Index</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">}}})}</span>

到这里web1的核心代码就完成了,web2的代码跟web1除了配置里面的加密key,其他全部一样,就不再贴出代码了,后面源码有。

到这里,就实现了一处登录,全部登录了。

2、一处退出,全部退出实现

一处退出,处处退出的流程像实现目标中的流程图,web1系统退出,跳转到SSO,让SSO发http请求退出其他的系统,跳转回登录页。

退出有个核心的问题就是,SSO只能让全部系统在当前浏览器上退出,比喻用户A在电脑1的浏览器登录了,在电脑2的浏览器也登录了,在电脑1上退出只能退出电脑1浏览器的登录,

电脑2的登录不受影响,web1退出了,SSO中的http请求退出web2的时候是不经过浏览器请求的,web2怎么知道清除那个token呢?

这里需要在SSO登录的时候生成了一个全局会话,SSO的cookie这时可以生成一个全局code,每个系统登录的时候带过去作为token的缓存key,这样就能保证全部系统的局部会话缓存key是同一个了,

退出登录的时候只需要删除这个缓存key的token即可。

SSO的登录页面Login.cshtml

@*For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
}
"form">用户名:"text" id=userName name="userName" />密码:"password" id="password" name="password" />"button" value="提交" onclick="login()" />"</span><span style="color: rgba(128, 0, 0, 1)">~/lib/jquery/dist/jquery.min.js</span><span style="color: rgba(128, 0, 0, 1)">"</span>>
"</span><span style="color: rgba(128, 0, 0, 1)">~/js/Common.js</span><span style="color: rgba(128, 0, 0, 1)">"</span>>
<span style="color: rgba(0, 0, 0, 1)">sessionCheck();</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">会话检查</span>
<span style="color: rgba(0, 0, 0, 1)"> function sessionCheck(){</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">获取参数集合</span><span style="color: rgba(0, 0, 255, 1)">const</span> urlParams=<span style="color: rgba(0, 0, 0, 1)">GetParam();</span><span style="color: rgba(0, 0, 255, 1)">const</span> clientId=urlParams[<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">clientId</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">];</span><span style="color: rgba(0, 0, 255, 1)">const</span> redirectUrl=urlParams[<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">redirectUrl</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">]</span><span style="color: rgba(0, 0, 255, 1)">const</span> sessionCode=getCookie(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">SessionCode</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)</span><span style="color: rgba(0, 0, 255, 1)">if</span>(!<span style="color: rgba(0, 0, 0, 1)">sessionCode){</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">;}</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">根据授权码获取code</span><span style="color: rgba(0, 0, 255, 1)">var</span> <span style="color: rgba(0, 0, 255, 1)">params</span>=<span style="color: rgba(0, 0, 0, 1)">{clientId,sessionCode}$.ajax({url:</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">/SSO/GetCodeBySessionCode</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">,data:JSON.stringify(</span><span style="color: rgba(0, 0, 255, 1)">params</span><span style="color: rgba(0, 0, 0, 1)">),method:</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">post</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">,dataType:</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">json</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">,contentType:</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">application/json</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">,success:function(data){</span><span style="color: rgba(0, 0, 255, 1)">if</span>(data.code===<span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)">){</span><span style="color: rgba(0, 0, 255, 1)">const</span> code=<span style="color: rgba(0, 0, 0, 1)">data.datawindow.location.href</span>=redirectUrl+<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">?authCode=</span><span style="color: rgba(128, 0, 0, 1)">'</span>+code+<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">&amp;sessionCode=</span><span style="color: rgba(128, 0, 0, 1)">"</span>+<span style="color: rgba(0, 0, 0, 1)">sessionCode}}})}function login(){</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">获取参数集合</span><span style="color: rgba(0, 0, 255, 1)">const</span> urlParams=<span style="color: rgba(0, 0, 0, 1)">GetParam();</span><span style="color: rgba(0, 0, 255, 1)">const</span> clientId=urlParams[<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">clientId</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">];</span><span style="color: rgba(0, 0, 255, 1)">const</span> redirectUrl=urlParams[<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">redirectUrl</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">]</span><span style="color: rgba(0, 0, 255, 1)">const</span> userName=$(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">#userName</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">).val()</span><span style="color: rgba(0, 0, 255, 1)">const</span> password=$(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">#password</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">).val()</span><span style="color: rgba(0, 0, 255, 1)">const</span> <span style="color: rgba(0, 0, 255, 1)">params</span>=<span style="color: rgba(0, 0, 0, 1)">{clientId,userName,password}$.ajax({url:</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">/SSO/GetCode</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">,data:JSON.stringify(</span><span style="color: rgba(0, 0, 255, 1)">params</span><span style="color: rgba(0, 0, 0, 1)">),method:</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">post</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">,dataType:</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">json</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">,contentType:</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">application/json</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,success:function(data){</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">获得code,跳转回客户页面</span><span style="color: rgba(0, 0, 255, 1)">if</span>(data.code===<span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)">){ </span><span style="color: rgba(0, 0, 255, 1)">const</span> code=<span style="color: rgba(0, 0, 0, 1)">data.data</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">存储会话,这里的时间最好减去几分钟,不然那边的token过期,这里刚好多了几秒没过期又重新登录了</span>setCookie(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">SessionCode</span><span style="color: rgba(128, 0, 0, 1)">"</span>,code,<span style="color: rgba(128, 0, 128, 1)">24</span>*<span style="color: rgba(128, 0, 128, 1)">60</span>*<span style="color: rgba(128, 0, 128, 1)">60</span>,<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">/</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)window.location.href</span>=redirectUrl+<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">?authCode=</span><span style="color: rgba(128, 0, 0, 1)">'</span>+code+<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">&amp;sessionCode=</span><span style="color: rgba(128, 0, 0, 1)">'</span>+<span style="color: rgba(0, 0, 0, 1)">code}}})}</span>

这里的SessionCode是关键,作为一个全局code,系统登录会同步到个系统,用于统一退出登录时用

SSO的退出登录页面LogOut.cshtml

@*For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
}
退出登录中..."</span><span style="color: rgba(128, 0, 0, 1)">~/lib/jquery/dist/jquery.min.js</span><span style="color: rgba(128, 0, 0, 1)">"</span>>
"</span><span style="color: rgba(128, 0, 0, 1)">~/js/Common.js?v=1</span><span style="color: rgba(128, 0, 0, 1)">"</span>>
<span style="color: rgba(0, 0, 0, 1)">logOut()function logOut(){</span><span style="color: rgba(0, 0, 255, 1)">var</span> sessionCode=getCookie(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">SessionCode</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">清除会话</span>clearCookie(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">SessionCode</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">获取参数集合</span><span style="color: rgba(0, 0, 255, 1)">const</span> urlParams=<span style="color: rgba(0, 0, 0, 1)">GetParam();</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">跳转到登录</span><span style="color: rgba(0, 0, 255, 1)">const</span> clientId=urlParams[<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">clientId</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">];</span><span style="color: rgba(0, 0, 255, 1)">const</span> redirectUrl=urlParams[<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">redirectUrl</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">]</span><span style="color: rgba(0, 0, 255, 1)">var</span> <span style="color: rgba(0, 0, 255, 1)">params</span>=<span style="color: rgba(0, 0, 0, 1)">{sessionCode}</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">退出登录</span>
<span style="color: rgba(0, 0, 0, 1)"> $.ajax({url:</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">/SSO/LogOutApp</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">,type:</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">POST</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,data:JSON.stringify(</span><span style="color: rgba(0, 0, 255, 1)">params</span><span style="color: rgba(0, 0, 0, 1)">),contentType:</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">application/json; charset=utf-8</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,dataType:</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">json</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,success: function(data){console.log(</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">token</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">,data)</span><span style="color: rgba(0, 0, 255, 1)">if</span>(data.code===<span style="color: rgba(128, 0, 128, 1)">0</span>) <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">成功</span>
<span style="color: rgba(0, 0, 0, 1)"> {</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">跳转到登录页面</span>window.location.href=<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">/SSO/Login</span><span style="color: rgba(128, 0, 0, 1)">'</span>+<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">?clientId=</span><span style="color: rgba(128, 0, 0, 1)">'</span>+clientId+<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">&amp;redirectUrl=</span><span style="color: rgba(128, 0, 0, 1)">'</span>+<span style="color: rgba(0, 0, 0, 1)">redirectUrl}}})}</span>

退出登录接口:

     /// /// 退出登录/// /// /// [HttpPost]public async Task LogOutApp([FromBody] LogOutRequest request){//删除全局会话string sessionKey = $"SessionCode:{request.sessionCode}";\_cachelper.DeleteKey(sessionKey);var client = \_httpClientFactory.CreateClient();var param = new { sessionCode = request.sessionCode };string jsonData = System.Text.Json.JsonSerializer.Serialize(param);StringContent paramContent = new StringContent(jsonData);//这里实战中是用数据库或缓存取List<string> urls = new List<string>(){"https://localhost:7001/Account/LogOut","https://localhost:7002/Account/LogOut"};//这里可以异步mq处理,不阻塞返回foreach (var url in urls){//web1退出登录var logOutResponse = await client.PostAsync(url, new StringContent(jsonData, Encoding.UTF8, "application/json"));string resultStr = await logOutResponse.Content.ReadAsStringAsync();ResponseModel response = System.Text.Json.JsonSerializer.Deserialize(resultStr);if (response.code == 0) //成功{Console.WriteLine($"url:{url},会话Id:{request.sessionCode},退出登录成功");}else{Console.WriteLine($"url:{url},会话Id:{request.sessionCode},退出登录失败");}};return new ResponseModel().SetSuccess();}

web1,web2的退出登录接口

     /// /// 退出登录/// /// /// [HttpPost]public ResponseModel LogOut([FromBody] LogOutRequest request){string key = $"SessionCode:{request.SessionCode}";//根据会话取出tokenstring token = \_cachelper.StringGet<string>(key);if (!string.IsNullOrEmpty(token)){//清除tokenstring tokenKey = $"token:{token}";\_cachelper.DeleteKey(tokenKey);}Console.WriteLine($"会话Code:{request.SessionCode}退出登录");return new ResponseModel().SetSuccess();}

到这里,一处退出,全部退出也完成了。

3、双token机制实现

token和refresh_token生成算法一样就可以了,知识token的有效期端,refresh_token的有效期长。

那刷新token时怎么知道这个是刷新token呢,SSO生成刷新token的时候,把它保存到缓存中,刷新token的时候判断缓存中有就是刷新token。

生成双token的代码:

     /// /// 根据授权码,获取Token/// /// /// /// public ResponseModel GetTokenWithRefresh(string authCode){ResponseModel result = new ResponseModel();string key = $"AuthCode:{authCode}";string clientIdCachekey = $"AuthCodeClientId:{authCode}";string AuthCodeSessionTimeKey = $"AuthCodeSessionTime:{authCode}";//根据授权码获取用户信息CurrentUserModel currentUserModel = \_cachelper.StringGet(key);if (currentUserModel == null){throw new Exception("code无效");}//清除authCode,只能用一次\_cachelper.DeleteKey(key);//获取应用配置string clientId = \_cachelper.StringGet<string>(clientIdCachekey);//刷新token过期时间DateTime sessionExpiryTime = \_cachelper.StringGet(AuthCodeSessionTimeKey);DateTime tokenExpiryTime = DateTime.Now.AddMinutes(10);//token过期时间10分钟//如果刷新token有过期期比token默认时间短,把token过期时间设成和刷新token一样if (sessionExpiryTime > DateTime.Now && sessionExpiryTime < tokenExpiryTime){tokenExpiryTime = sessionExpiryTime;}//获取访问tokenstring token = this.IssueToken(currentUserModel, clientId, (sessionExpiryTime - DateTime.Now).TotalSeconds);TimeSpan refreshTokenExpiry;if (sessionExpiryTime != default(DateTime)){refreshTokenExpiry = sessionExpiryTime - DateTime.Now;}else{refreshTokenExpiry = TimeSpan.FromSeconds(60 * 60 * 24);//默认24小时}//获取刷新tokenstring refreshToken = this.IssueToken(currentUserModel, clientId, refreshTokenExpiry.TotalSeconds);//缓存刷新token\_cachelper.StringSet(refreshToken, currentUserModel, refreshTokenExpiry);result.SetSuccess(new GetTokenDTO() { token = token, refreshToken = refreshToken, expires = 60 * 10 });Console.WriteLine($"client\_id:{clientId}获取token,有效期:{sessionExpiryTime.ToString("yyyy-MM-dd HH:mm:ss")},token:{token}");return result;}

根据刷新token获取token代码:

     /// /// 根据刷新Token获取Token/// /// /// /// public string GetTokenByRefresh(string refreshToken, string clientId){//刷新Token是否在缓存CurrentUserModel currentUserModel = \_cachelper.StringGet($"RefreshToken:{refreshToken}");if(currentUserModel==null){return String.Empty;}//刷新token过期时间DateTime refreshTokenExpiry = \_cachelper.StringGet($"RefreshTokenExpiry:{refreshToken}");//token默认时间为600sdouble tokenExpiry = 600;//如果刷新token的过期时间不到600s了,token过期时间为刷新token的过期时间if(refreshTokenExpiry>DateTime.Now&&refreshTokenExpiry600)){tokenExpiry = (refreshTokenExpiry - DateTime.Now).TotalSeconds;}//从新生成Tokenstring token = IssueToken(currentUserModel, clientId, tokenExpiry);return token;}

回到顶部# 四、效果演示

这里项目的SSO地址是:https://localhost:7000 ,web1地址是:https://localhost:7001,web2地址是:https://localhost:7002

修改hosts文件,让他们在不同域名下,cookie不能共享。

win10路径:C:\Windows\System32\drivers\etc\hosts 在最后加入

127.0.0.1 sso.com
127.0.0.1 web1.com
127.0.0.1 web2.com

这样得到新的地址,SSO地址:https://sso.com:7000 ,web1地址是:https://web1.com,web2地址是:https://web2.com

1、 这里一开始,访问https://web2.com没登录跳转到https://sso.com。

2、然后访问https://web1.com也没登录,也跳转到了https://sso.com,证明web1,web2都没登录。

3、然后在跳转的sso登录后跳转回web1,然后点https://web2.com的连接跳转到https://web2.com,自动登录了。

4、然后在web1退出登录,web2刷新页面,也退出了登录。

再看一下这些操作下SSO日志打印的记录。

到这里.NET6下基于JWT+OAuth2.0的SSO就完成了。

最后附源码:

百度云盘:https://pan.baidu.com/s/1MZbFC7KojIRT0LEL5HBkGg    提取码:qcn1

github: https://github.com/weixiaolong325/SSO.Demo.SSO   (github网络有点问题,还没上传完整,用百度云盘的,晚点上传完整到github)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/400945.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

11步提高代码质量和整体工作效率

作为专业程序员这些年来学到的能真正提高代码质量和整体工作效率的11件事情。 1. 永远不要复制代码 不 惜任何代价避免重复的代码。如果一个常用的代码片段出现在了程序中的几个不同地方&#xff0c;重构它&#xff0c;把它放到一个自己的函数里。重复的代码会导致你的同事 在读…

CDZSC_2015寒假新人(1)——基础 i

Description “Point, point, life of student!” This is a ballad&#xff08;歌谣&#xff09;well known in colleges, and you must care about your score in this exam too. How many points can you get? Now, I told you the rules which are used in this course. T…

MySql触发器使用

&#x1f680; 优质资源分享 &#x1f680; 学习路线指引&#xff08;点击解锁&#xff09;知识定位人群定位&#x1f9e1; Python实战微信订餐小程序 &#x1f9e1;进阶级本课程是python flask微信小程序的完美结合&#xff0c;从项目搭建到腾讯云部署上线&#xff0c;打造一…

Go微服务框架go-kratos实战04:kratos中服务注册和服务发现的使用

&#x1f680; 优质资源分享 &#x1f680; 学习路线指引&#xff08;点击解锁&#xff09;知识定位人群定位&#x1f9e1; Python实战微信订餐小程序 &#x1f9e1;进阶级本课程是python flask微信小程序的完美结合&#xff0c;从项目搭建到腾讯云部署上线&#xff0c;打造一…

我注册了某音帐号之后。。。(内涵推荐算法)

&#x1f680; 优质资源分享 &#x1f680; 学习路线指引&#xff08;点击解锁&#xff09;知识定位人群定位&#x1f9e1; Python实战微信订餐小程序 &#x1f9e1;进阶级本课程是python flask微信小程序的完美结合&#xff0c;从项目搭建到腾讯云部署上线&#xff0c;打造一…

第5讲++创建管理数据库

实例 create database db on (namedb51,filenameD:\xxx\db51.mdf,size100,maxsize200,filegrowth20 ), (namedb52,filenameD:\xxx\db52.ndf,size100,maxsize200,filegrowth20 ), (namedb53,filenameD:\xxx\db53.ndf,size100,maxsize200,filegrowth20 ) log on (namedb5log1,fi…

R数据分析:如何简洁高效地展示统计结果

&#x1f680; 优质资源分享 &#x1f680; 学习路线指引&#xff08;点击解锁&#xff09;知识定位人群定位&#x1f9e1; Python实战微信订餐小程序 &#x1f9e1;进阶级本课程是python flask微信小程序的完美结合&#xff0c;从项目搭建到腾讯云部署上线&#xff0c;打造一…

实现权重计算

今天试图实现内部点权重的计算。在论文中&#xff0c;权重的计算过程是这样的:其中solveNNLS是求解这个方程s为表面点&#xff0c;q为内部点发现两种方法&#xff1a;1.一种是把内部点位置表示成表面点的线性组合2.另一种是把内部点的位移表示成表面点的线性组合第2种可以化成内…

Android LBS系列05 位置策略(一)

Location Strategies 定位的实现 在Android系统上实现定位主要是通过GPS或者是基于网络的定位方法。 GPS是精度最高的&#xff0c;但是它只在户外有用&#xff0c;并且很耗电&#xff0c;并且首次定位花费时间较长。 基于网络的定位利用通信网络蜂窝基站和Wi-Fi信号&#xff0c…

ABP官方文档翻译 3.1 实体

实体 实体类聚合根类 领域事件常规接口 审计软删除激活/失活实体实体改变事件IEntity接口实体是DDD(领域驱动设计)的核心概念之一。Eric Evans描述它为"An object that is not fundamentally defined by its attributes, but rather by a thread of continuity and identi…

目标检测之线段检测---lsd line segment detector

&#xff08;1&#xff09;线段检测应用背景 &#xff08;2&#xff09;线段检测原理简介 &#xff08;3&#xff09;线段检测实例 a line segment detector &#xff08;4&#xff09;hough 变换和 lsd 的区别 ---------------------author&#xff1a;pkf ------------------…

利用Mac创建一个 IPv6 WIFI 热点

标签&#xff1a; iOSIPv6Create NAT64 NetWork创建NAT64网络2016-05-20 10:58 14156人阅读 评论(11) 收藏 举报分类&#xff1a;开发技巧&#xff08;8&#xff09; 版权声明&#xff1a;本文为博主原创文章&#xff0c;未经博主允许不得转载。 苹果商店6月1日起&#xff0c;强…

经典电影

那些经典的电影&#xff0c;第一波&#xff01;&#xff01; 《怦然心动》&#xff0c;欧美电影——布莱斯全家搬到小镇&#xff0c;邻家女孩朱丽前来帮忙。她对他一见钟情&#xff0c;心愿是获得他的吻。两人是同班同学&#xff0c;她一直想方设法接近他&#xff0c;但是他避之…

go-zero 微服务实战系列(一、开篇)

&#x1f680; 优质资源分享 &#x1f680; 学习路线指引&#xff08;点击解锁&#xff09;知识定位人群定位&#x1f9e1; Python实战微信订餐小程序 &#x1f9e1;进阶级本课程是python flask微信小程序的完美结合&#xff0c;从项目搭建到腾讯云部署上线&#xff0c;打造一…

大数据时代,怎么做全渠道的营销

大数据时代,怎么做全渠道的营销 营销在中国有很多变化&#xff0c;1978年之前主要是计划经济的时代&#xff0c;寻求政府的保护。1984年进入商品经济之后&#xff0c;中国企业通过大规模的扩大生产&#xff0c;扩大工厂得到生产效率。1992年进入市场经济之后&#xff0c;会涌现…

oracle slient静默安装并配置数据库及仅安装数据库不配置数据库shell

oracle slient静默安装并配置数据库及仅安装数据库不配置数据库shell <1&#xff0c;>仅安装数据库软件不配置数据库 ./x86oracle.sh /ruiy/ocr/DBSoftware/app/oracle /ruiy/ocr/DBSoftware/app/oraInventory /ruiy/ins_soft.rsp <2&#xff0c;>静默安装数据库软…

hdu 2048 神、上帝以及老天爷

题解&#xff1a; 本题主要思路是&#xff0c;算出i个人的总选择数a和以悲剧结尾的总选择数b&#xff0c;然后b/a&#xff0c;完成。属于递推题&#xff1a; 图中横杠代表人&#xff0c;竖杠代表签名。Ai代表i个人共有A[i]个选择&#xff0c;Bi起过度作用。因为最多有20人&…

探究Presto SQL引擎(3)-代码生成

&#x1f680; 优质资源分享 &#x1f680; 学习路线指引&#xff08;点击解锁&#xff09;知识定位人群定位&#x1f9e1; Python实战微信订餐小程序 &#x1f9e1;进阶级本课程是python flask微信小程序的完美结合&#xff0c;从项目搭建到腾讯云部署上线&#xff0c;打造一…

【学习笔记】计算几何

&#x1f680; 优质资源分享 &#x1f680; 学习路线指引&#xff08;点击解锁&#xff09;知识定位人群定位&#x1f9e1; Python实战微信订餐小程序 &#x1f9e1;进阶级本课程是python flask微信小程序的完美结合&#xff0c;从项目搭建到腾讯云部署上线&#xff0c;打造一…

支持自动水平拆分的高性能分布式数据库TDSQL

随着互联网应用的广泛普及&#xff0c;海量数据的存储和访问成为系统设计的瓶颈问题。对于大型的互联网应用&#xff0c;每天几十亿的PV无疑对数据库造成了相当高的负载。给系统的稳定性和扩展性造成了极大的问题。通过数据的切分来提高系统整体性能&#xff0c;扩充系统整体容…