.NET Core中JWT+OAuth2.0實現SSO,附完整源碼(.NET6)

来源:https://www.cnblogs.com/wei325/archive/2022/05/30/16316004.html
-Advertisement-
Play Games

一、簡介 單點登錄(SingleSignOn,SSO) 指的是在多個應用系統中,只需登錄一次,就可以訪問其他相互信任的應用系統。 JWT Json Web Token,這裡不詳細描述,簡單說是一種認證機制。 OAuth2.0 OAuth2.0是一個認證流程,一共有四種方式,這裡用的是最常用的授權碼方 ...


一、簡介

單點登錄(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",
    "	   

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...