🚀 优质资源分享 🚀
学习路线指引(点击解锁) | 知识定位 | 人群定位 |
---|---|---|
🧡 Python实战微信订餐小程序 🧡 | 进阶级 | 本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。 |
💛Python量化交易实战💛 | 入门级 | 手把手带你打造一个易扩展、更安全、效率更高的量化交易系统 |
目录 |
回到顶部# 一、简介
单点登录(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+RefreshToken
ResponseModel 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默认时间为600s
double tokenExpiry = 600;
//如果刷新token的过期时间不到600s了,token过期时间为刷新token的过期时间
if(refreshTokenExpiry>DateTime.Now&&refreshTokenExpiry600))
{
tokenExpiry = (refreshTokenExpiry - DateTime.Now).TotalSeconds;
}
//从新生成Token
string 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;
}
//获取访问token
string 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小时
}
//获取刷新token
string refreshToken = this.IssueToken(currentUserModel, clientId, refreshTokenExpiry.TotalSeconds);
//缓存刷新token
\_cachelper.StringSet($"Re