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

本文介绍了如何在.NET Core中利用JWT和OAuth2.0实现单点登录(SSO),包括一处登录全部登录、一处退出全部退出及双token机制的详细流程,并提供完整源码。重点讨论了SSO的关键实现,如授权码的使用、全局会话管理和安全考虑。
摘要由CSDN通过智能技术生成

🚀 优质资源分享 🚀

学习路线指引(点击解锁) 知识定位 人群定位
🧡 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
Spring Security是一个功能强大的安全框架,用于在Java应用程序实现身份验证和授权。JWT(JSON Web Token)是一种轻量级的身份验证和授权机制,其包含了验证用户身份的加密信息。OAuth 2.0是一种开放标准的授权协议,它允许用户授权第三方应用程序访问受保护的资源。 Spring Security可以与JWT和OAuth 2.0结合使用,以提供更强大的身份验证和授权功能。使用JWT作为身份验证机制,可以在用户登录成功后生成一个JWT令牌,并将其加入到HTTP请求的Header。服务端可以使用JWT的信息,如用户名和权限,对请求进行验证,确保用户的身份是有效的。而OAuth 2.0允许用户通过授权服务器颁发的token来访问受保护的资源,Spring Security可以集成OAuth 2.0来实现授权验证的逻辑。 通过使用Spring Security结合JWT和OAuth 2.0,可以轻松实现可伸缩、安全的身份验证和授权机制。开发人员可以使用Spring Security提供的各种功能,如用户认证、角色授权和访问控制,来保护应用程序的敏感操作和数据。此外,使用JWT和OAuth 2.0,可以实现无状态的API身份验证和授权,提高系统的可扩展性和性能。 总之,Spring Security与JWT和OAuth 2.0的结合为应用程序提供了安全、可靠的身份验证和授权机制。开发人员可以根据具体的需求配置和使用这些功能,以保护应用程序的安全和数据的机密性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值