主頁 > .NET開發 > .NET Core中JWT+OAuth2.0實作SSO,附完整原始碼(.NET6)

.NET Core中JWT+OAuth2.0實作SSO,附完整原始碼(.NET6)

2022-05-31 06:56:06 .NET開發

一、簡介

單點登錄(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生成需要的方法

 /// <summary>
    /// JWT服務介面
    /// </summary>
    public interface IJWTService
    {
        /// <summary>
        /// 獲取授權碼
        /// </summary>
        /// <param name="userName"></param>
        /// <param name="password"></param>
        /// <returns></returns>
        /// <exception cref="NotImplementedException"></exception>
         ResponseModel<string> GetCode(string clientId, string userName, string password);
        /// <summary>
        /// 根據會話Code獲取授權碼
        /// </summary>
        /// <param name="clientId"></param>
        /// <param name="sessionCode"></param>
        /// <returns></returns>
        ResponseModel<string> GetCodeBySessionCode(string clientId, string sessionCode);

        /// <summary>
        /// 根據授權碼獲取Token+RefreshToken
        /// </summary>
        /// <param name="authCode"></param>
        /// <returns>Token+RefreshToken</returns>
        ResponseModel<GetTokenDTO> GetTokenWithRefresh(string authCode);

        /// <summary>
        /// 根據RefreshToken重繪Token
        /// </summary>
        /// <param name="refreshToken"></param>
        /// <param name="clientId"></param>
        /// <returns></returns>
        string GetTokenByRefresh(string refreshToken, string clientId);
    }

建一個抽象類JWTBaseService加模板方法實作詳細的邏輯

 /// <summary>
    /// jwt服務
    /// </summary>
    public abstract class JWTBaseService : IJWTService
    {
        protected readonly IOptions<AppSettingOptions> _appSettingOptions;
        protected readonly Cachelper _cachelper;
        public JWTBaseService(IOptions<AppSettingOptions> appSettingOptions, Cachelper cachelper)
        {
            _appSettingOptions = appSettingOptions;
            _cachelper = cachelper;
        }

        /// <summary>
        /// 獲取授權碼
        /// </summary>
        /// <param name="userName"></param>
        /// <param name="password"></param>
        /// <returns></returns>
        /// <exception cref="NotImplementedException"></exception>
        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<CurrentUserModel>(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<CurrentUserModel>(sessionCode, currentUserModel, TimeSpan.FromDays(1));
            //全域會話過期時間
            string sessionExpiryKey = $"SessionExpiryKey:{code}";
            DateTime sessionExpirTime = DateTime.Now.AddDays(1);
            _cachelper.StringSet<DateTime>(sessionExpiryKey, sessionExpirTime, TimeSpan.FromDays(1));
            Console.WriteLine($"登錄成功,全域會話code:{code}");
            //快取授權碼取token時最長的有效時間
            _cachelper.StringSet<DateTime>($"AuthCodeSessionTime:{code}", sessionExpirTime, TimeSpan.FromDays(1));

            result.SetSuccess(code);
            return result;
        }
        /// <summary>
        /// 根據會話code獲取授權碼
        /// </summary>
        /// <param name="clientId"></param>
        /// <param name="sessionCode"></param>
        /// <returns></returns>
        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<CurrentUserModel>(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<CurrentUserModel>(key, currentUserModel, TimeSpan.FromMinutes(10));
            //快取授權碼是哪個應用的
            _cachelper.StringSet<string>(appCachekey, appHSSetting.clientId, TimeSpan.FromMinutes(10));

            //快取授權碼取token時最長的有效時間
            DateTime expirTime = _cachelper.StringGet<DateTime>($"SessionExpiryKey:{sessionCode}");
            _cachelper.StringSet<DateTime>($"AuthCodeSessionTime:{code}", expirTime, expirTime - DateTime.Now);

            result.SetSuccess(code);
            return result;

        }

        /// <summary>
        /// 根據重繪Token獲取Token
        /// </summary>
        /// <param name="refreshToken"></param>
        /// <param name="clientId"></param>
        /// <returns></returns>
        public string GetTokenByRefresh(string refreshToken, string clientId)
        {
            //重繪Token是否在快取
            CurrentUserModel currentUserModel = _cachelper.StringGet<CurrentUserModel>($"RefreshToken:{refreshToken}");
            if(currentUserModel==null)
            {
                return String.Empty;
            }
            //重繪token過期時間
            DateTime refreshTokenExpiry = _cachelper.StringGet<DateTime>($"RefreshTokenExpiry:{refreshToken}");
            //token默認時間為600s
            double tokenExpiry = 600;
            //如果重繪token的過期時間不到600s了,token過期時間為重繪token的過期時間
            if(refreshTokenExpiry>DateTime.Now&&refreshTokenExpiry<DateTime.Now.AddSeconds(600))
            {
                tokenExpiry = (refreshTokenExpiry - DateTime.Now).TotalSeconds;
            }

                //從新生成Token
                string token = IssueToken(currentUserModel, clientId, tokenExpiry);
                return token;

        }

        /// <summary>
        /// 根據授權碼,獲取Token
        /// </summary>
        /// <param name="userInfo"></param>
        /// <param name="appHSSetting"></param>
        /// <returns></returns>
        public ResponseModel<GetTokenDTO> GetTokenWithRefresh(string authCode)
        {
            ResponseModel<GetTokenDTO> result = new ResponseModel<GetTokenDTO>();

            string key = $"AuthCode:{authCode}";
            string clientIdCachekey = $"AuthCodeClientId:{authCode}";
            string AuthCodeSessionTimeKey = $"AuthCodeSessionTime:{authCode}";

            //根據授權碼獲取用戶資訊
            CurrentUserModel currentUserModel = _cachelper.StringGet<CurrentUserModel>(key);
            if (currentUserModel == null)
            {
                throw new Exception("code無效");
            }
            //清除authCode,只能用一次
            _cachelper.DeleteKey(key);

            //獲取應用配置
            string clientId = _cachelper.StringGet<string>(clientIdCachekey);
            //重繪token過期時間
            DateTime sessionExpiryTime = _cachelper.StringGet<DateTime>(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($"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
        /// <summary>
        /// 簽發token
        /// </summary>
        /// <param name="userModel"></param>
        /// <param name="clientId"></param>
        /// <param name="second"></param>
        /// <returns></returns>
        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;
        }

        /// <summary>
        /// 根據appKey獲取應用資訊
        /// </summary>
        /// <param name="clientId"></param>
        /// <returns></returns>
        private AppHSSetting getAppInfoByAppKey(string clientId)
        {
            AppHSSetting appHSSetting = _appSettingOptions.Value.appHSSettings.Where(s => s.clientId == clientId).FirstOrDefault();
            return appHSSetting;
        }
        /// <summary>
        /// 獲取加密方式
        /// </summary>
        /// <returns></returns>
        protected abstract SigningCredentials GetCreds(string clientId);
        
        #endregion
    }

 

新建類JWTHSService實作對稱加密

 /// <summary>
    /// JWT對稱可逆加密
    /// </summary>
    public class JWTHSService : JWTBaseService
    {
        public JWTHSService(IOptions<AppSettingOptions> options, Cachelper cachelper):base(options,cachelper)
        {

        }
        /// <summary>
        /// 生成對稱加密簽名憑證
        /// </summary>
        /// <param name="clientId"></param>
        /// <returns></returns>
        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;
        }
        /// <summary>
        /// 根據appKey獲取應用資訊
        /// </summary>
        /// <param name="clientId"></param>
        /// <returns></returns>
        private AppHSSetting getAppInfoByAppKey(string clientId)
        {
            AppHSSetting appHSSetting = _appSettingOptions.Value.appHSSettings.Where(s => s.clientId == clientId).FirstOrDefault();
            return appHSSetting;
        }
       
    }

新建JWTRSService類實作非對稱加密,和上面的對稱加密,只需要一個就可以里,這里把兩種都寫出來了

 

/// <summary>
    /// JWT非對稱加密
    /// </summary>
    public class JWTRSService : JWTBaseService
    {
  
        public JWTRSService(IOptions<AppSettingOptions> options, Cachelper cachelper):base(options, cachelper)
        {
 
        }
        /// <summary>
        /// 生成非對稱加密簽名憑證
        /// </summary>
        /// <param name="clientId"></param>
        /// <returns></returns>
        protected override SigningCredentials GetCreds(string clientId)
        {
            var appRSSetting = getAppInfoByAppKey(clientId);
            var rsa = RSA.Create();
            byte[] privateKey = Convert.FromBase64String(appRSSetting.privateKey);//這里只需要私鑰,不要begin,不要end
            rsa.ImportPkcs8PrivateKey(privateKey, out _);
            var key = new RsaSecurityKey(rsa);
            var creds = new SigningCredentials(key, SecurityAlgorithms.RsaSha256);
            return creds;
        }
        /// <summary>
        /// 根據appKey獲取應用資訊
        /// </summary>
        /// <param name="clientId"></param>
        /// <returns></returns>
        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<Cachelper>();
builder.Services.Configure<AppOptions>(builder.Configuration.GetSection("AppOptions"));
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 = 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
//        };
//    });

#endregion



var 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

 /// <summary>
    /// 用戶資訊
    /// </summary>

    public class AccountController : Controller
    {
        private IHttpClientFactory _httpClientFactory;
        private readonly Cachelper _cachelper;
        public AccountController(IHttpClientFactory httpClientFactory, Cachelper cachelper)
        {
            _httpClientFactory = httpClientFactory;
            _cachelper = cachelper;
        }

        /// <summary>
        /// 獲取用戶資訊,介面需要進行權限校驗
        /// </summary>
        /// <returns></returns>
        [MyAuthorize]
        [HttpPost]
        public ResponseModel<UserDTO> GetUserInfo()
        {
            ResponseModel<UserDTO> user = new ResponseModel<UserDTO>();
            return user;
        }
        /// <summary>
        /// 登錄成功回呼
        /// </summary>
        /// <returns></returns>
        public ActionResult LoginRedirect()
        {
            return View();
        }
        //根據authCode獲取token
        [HttpPost]
        public async Task<ResponseModel<GetTokenDTO>> GetAccessCode([FromBody] GetAccessCodeRequest request)
        {
            ResponseModel<GetTokenDTO> result = new ResponseModel<GetTokenDTO>();
            //請求SSO獲取 token
            var client = _httpClientFactory.CreateClient();
            var param = new { authCode = request.authCode };
            string jsonData =https://www.cnblogs.com/wei325/p/ System.Text.Json.JsonSerializer.Serialize(param);
            StringContent paramContent = new StringContent(jsonData);

            //請求sso獲取token
            var 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<ResponseModel<GetTokenDTO>>(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;
        }
        /// <summary>
        /// 退出登錄
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        [HttpPost]
        public  ResponseModel LogOut([FromBody] LogOutRequest request)
        {
            string key = $"SessionCode:{request.SessionCode}";
            //根據會話取出token
            string token = _cachelper.StringGet<string>(key);
            if (!string.IsNullOrEmpty(token))
            {
                //清除token
                string tokenKey = $"token:{token}";
                _cachelper.DeleteKey(tokenKey);
            }
            Console.WriteLine($"會話Code:{request.SessionCode}退出登錄");
            return new ResponseModel().SetSuccess();
        }
    }

 

還有得到的token還沒過期,如果我退出登錄了,怎么判斷這個會話token失效了呢?

這里需要攔截認證過濾器,判斷token在快取中被洗掉,則認證不通過,增加檔案MyAuthorize

  /// <summary>
    /// 攔截認證過濾器
    /// </summary>
    public class MyAuthorize : Attribute, IAuthorizationFilter
    {
        private static Cachelper _cachelper = ServiceLocator.Instance.GetService<Cachelper>();

        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 "才是真正的token
            if (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";
}

<div class="text-center">
       <h1 class="display-4">歡迎來到Web1</h1>
    <p>Learn about <a href=https://www.cnblogs.com/wei325/p/"https://web2.com:7002">跳轉到Web2</a>.</p>
        <p>Learn about <a onclick="logOut()" href=https://www.cnblogs.com/wei325/p/"javascript:void(0);">退出登錄</a>.</p>
</div>
@section Scripts{
    <script src=https://www.cnblogs.com/wei325/p/"~/js/Common.js"></script>
<script>
                    getUserInfo()
            //獲取用戶資訊
            function getUserInfo(){
                //1.cookie是否有 token
                const token=getCookie('token')
                console.log('gettoken',token)
                if(!token)
                {
                    redirectLogin()
                }
                $.ajax({
          type: 'POST',
          url: '/Account/GetUserInfo',
          headers:{"Authorization":'Bearer ' + token},
          success: success,
          error:error
        });
            }
            function success(){
                console.log('成功')
            }
            function error(xhr, exception){
                if(xhr.status===401) //鑒權失敗
                {
                    console.log('未鑒權')
                    redirectLogin()
                }
            }
                      //重定向到登錄
            function redirectLogin(){
                     window.location.href="https://sso.com:7000/SSO/Login?clientId=web1&redirectUrl=https://web1.com:7001/Account/LoginRedirect"
            }
            //退出登錄
            function logOut(){
                clearCookie("token") //清除cookie token
                 clearCookie("refreshToken") //清除cookie refreshToken
                  clearCookie("sessionCode")  //清除cookie 會話

                  //跳轉到SSO退出登錄
                    window.location.href=https://www.cnblogs.com/wei325/p/"https://sso.com:7000/SSO/LogOut?clientId=web1&redirectUrl=https://web1.com:7001/Account/LoginRedirect"
               
            }

</script>
}

sso登錄完要跳轉回web1的頁面

@*
    For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
    Layout = null;
}
   <script src=https://www.cnblogs.com/wei325/p/"~/lib/jquery/dist/jquery.min.js"></script>
      <script src=https://www.cnblogs.com/wei325/p/"~/js/Common.js"></script>
   <script>
       GetAccessToken();
       //根據code獲取token
       function GetAccessToken(){
   
          var params=GetParam()
                  //code
          var authCode=params["authCode"]
          var sessionCode=params["sessionCode"]
          console.log('authcode',authCode)
          var params={authCode,sessionCode}     
$.ajax({
  url:'/Account/GetAccessCode',
  type:"POST",
  data:JSON.stringify(params),
  contentType:"application/json; charset=utf-8",
  dataType:"json",
  success: function(data){
     console.log('token',data)
     if(data.code===0) //成功
     { 
         console.log('設定cookie')
         //把token存到 cookie,過期時間為token有效時間少一分鐘
         setCookie("token",data.data.token,data.data.expires-60,"/")
         //重繪token,有效期1天
         setCookie("refreshToken",data.data.refreshToken,24*60*60,"/")
         setCookie("SessionCode",sessionCode,24*60*60,"/")
         //跳轉到主頁
          window.location.href=https://www.cnblogs.com/wei325/p/"/Home/Index"
     }
  }})

       }
           
   </script>

到這里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 id="form">
    <div>用戶名:<input type="text" id=userName name="userName" /></div>
    <div>密碼:<input type="password" id="password" name="password" /></div>
    <div><input type="button" value=https://www.cnblogs.com/wei325/p/"提交" onclick="login()" /></div>
</form>

<script src=https://www.cnblogs.com/wei325/p/"~/lib/jquery/dist/jquery.min.js"></script>
<script src=https://www.cnblogs.com/wei325/p/"~/js/Common.js"></script>
<script>
    sessionCheck();
    //會話檢查
    function sessionCheck(){
          //獲取引數集合
            const urlParams=GetParam();
            const clientId=urlParams['clientId'];
            const redirectUrl=urlParams['redirectUrl']
            const sessionCode=getCookie("SessionCode")
            if(!sessionCode)
            {
                return;
            }
            //根據授權碼獲取code
            var params={clientId,sessionCode}
            $.ajax({
            url:'/SSO/GetCodeBySessionCode',
            data:JSON.stringify(params),
            method:'post',
            dataType:'json',
            contentType:'application/json',
            success:function(data){
                if(data.code===0)
                {
                     const code=data.data
                      window.location.href=redirectUrl+'?authCode='+code+"&sessionCode="+sessionCode
                }
            }
            })
    }

        function login(){
            //獲取引數集合
            const urlParams=GetParam();

            const clientId=urlParams['clientId'];
            const redirectUrl=urlParams['redirectUrl']
                const userName=$("#userName").val()
                const password=$("#password").val()
                const params={clientId,userName,password}
            $.ajax({
                    url:'/SSO/GetCode',
                    data:JSON.stringify(params),
                    method:'post',
                    dataType:'json',
                    contentType:"application/json",
                    success:function(data){
                        //獲得code,跳轉回客戶頁面
                        if(data.code===0)
                        {    
                        const code=data.data

                       //存盤會話,這里的時間最好減去幾分鐘,不然那邊的token過期,這里剛好多了幾秒沒過期又重新登錄了
                        setCookie("SessionCode",code,24*60*60,"/")
                      window.location.href=redirectUrl+'?authCode='+code+'&sessionCode='+code
                        }
                
                    }
                })
            }
            
       
</script>

這里的SessionCode是關鍵,作為一個全域code,系統登錄會同步到個系統,用于統一退出登錄時用

SSO的退出登錄頁面LogOut.cshtml

@*
    For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
}
<p>退出登錄中...</p>
<script src=https://www.cnblogs.com/wei325/p/"~/lib/jquery/dist/jquery.min.js"></script>
<script src=https://www.cnblogs.com/wei325/p/"~/js/Common.js?v=1"></script>
<script>
      logOut()
      function logOut()
      {
          var sessionCode=getCookie("SessionCode")
      //清除會話
        clearCookie("SessionCode")
        //獲取引數集合
              const urlParams=GetParam();
        //跳轉到登錄
          const clientId=urlParams['clientId'];
              const redirectUrl=urlParams['redirectUrl']

              var params={sessionCode}
              //退出登錄
              $.ajax({
    url:'/SSO/LogOutApp',
    type:"POST",
    data:JSON.stringify(params),
    contentType:"application/json; charset=utf-8",
    dataType:"json",
    success: function(data){
       console.log('token',data)
       if(data.code===0) //成功
       {
           //跳轉到登錄頁面
            window.location.href=https://www.cnblogs.com/wei325/p/'/SSO/Login'+'?clientId='+clientId+'&redirectUrl='+redirectUrl
       }
    }})


      }

</script>

退出登錄介面:

     /// <summary>
        /// 退出登錄
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        [HttpPost]
        public async Task<ResponseModel> LogOutApp([FromBody] LogOutRequest request)
        {
            //洗掉全域會話
            string sessionKey = $"SessionCode:{request.sessionCode}";
            _cachelper.DeleteKey(sessionKey);
            var client = _httpClientFactory.CreateClient();
            var param = new { sessionCode = request.sessionCode };
            string jsonData =https://www.cnblogs.com/wei325/p/ 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<ResponseModel>(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的退出登錄介面

     /// <summary>
        /// 退出登錄
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        [HttpPost]
        public  ResponseModel LogOut([FromBody] LogOutRequest request)
        {
            string key = $"SessionCode:{request.SessionCode}";
            //根據會話取出token
            string token = _cachelper.StringGet<string>(key);
            if (!string.IsNullOrEmpty(token))
            {
                //清除token
                string 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的代碼:

     /// <summary>
        /// 根據授權碼,獲取Token
        /// </summary>
        /// <param name="userInfo"></param>
        /// <param name="appHSSetting"></param>
        /// <returns></returns>
        public ResponseModel<GetTokenDTO> GetTokenWithRefresh(string authCode)
        {
            ResponseModel<GetTokenDTO> result = new ResponseModel<GetTokenDTO>();

            string key = $"AuthCode:{authCode}";
            string clientIdCachekey = $"AuthCodeClientId:{authCode}";
            string AuthCodeSessionTimeKey = $"AuthCodeSessionTime:{authCode}";

            //根據授權碼獲取用戶資訊
            CurrentUserModel currentUserModel = _cachelper.StringGet<CurrentUserModel>(key);
            if (currentUserModel == null)
            {
                throw new Exception("code無效");
            }
            //清除authCode,只能用一次
            _cachelper.DeleteKey(key);

            //獲取應用配置
            string clientId = _cachelper.StringGet<string>(clientIdCachekey);
            //重繪token過期時間
            DateTime sessionExpiryTime = _cachelper.StringGet<DateTime>(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(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代碼:

     /// <summary>
        /// 根據重繪Token獲取Token
        /// </summary>
        /// <param name="refreshToken"></param>
        /// <param name="clientId"></param>
        /// <returns></returns>
        public string GetTokenByRefresh(string refreshToken, string clientId)
        {
            //重繪Token是否在快取
            CurrentUserModel currentUserModel = _cachelper.StringGet<CurrentUserModel>($"RefreshToken:{refreshToken}");
            if(currentUserModel==null)
            {
                return String.Empty;
            }
            //重繪token過期時間
            DateTime refreshTokenExpiry = _cachelper.StringGet<DateTime>($"RefreshTokenExpiry:{refreshToken}");
            //token默認時間為600s
            double tokenExpiry = 600;
            //如果重繪token的過期時間不到600s了,token過期時間為重繪token的過期時間
            if(refreshTokenExpiry>DateTime.Now&&refreshTokenExpiry<DateTime.Now.AddSeconds(600))
            {
                tokenExpiry = (refreshTokenExpiry - DateTime.Now).TotalSeconds;
            }

                //從新生成Token
                string 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)

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/483268.html

標籤:.NET Core

上一篇:.NET CORE 基礎

下一篇:反應imgonError沒有觸發

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more