首页
视频
资源
登录
原
.net core 3.1 Identity Server4 (Code模式)
4223
人阅读
2020/12/15 14:33
总访问:
2535967
评论:
0
收藏:
0
手机
分类:
Ids4
![.netcore](https://img.tnblog.net/arcimg/hb/c857299a86d84ee7b26d181a31e58234.jpg ".netcore") >#.net core 3.1 Identity Server4 (Code模式) [TOC] ![](https://img.tnblog.net/arcimg/hb/8e4abea9067d4157944d80e90497ace8.png) >### Code 模式的理解 ![](https://img.tnblog.net/arcimg/hb/6f40298e06614feaa20cd36bcfce267d.png) tn>大致说一下,这种授权模式的意义。 A. 用户通过浏览器在页面上请求客户端需要授权的页面时,会自动跳转到授权服务器上去登录(这里授权服务器会去验证客户端信息) B. 然后用户在授权服务器上的登录页面进行登录,登录成功后会返回一个授权验证码(授权验证码!=授权码) C. 然后跳转到用户需要授权的页面 D. 客户端就会拿这这授权验证码去授权服务器那边验证,然后获取授权码[access_token](注意这里授权服务器会去验证你的客户端,比如验证访问链接来源,secret,clientid) E. 获取到Access Token >### 创建MVC客户端(AiDaSi.OcDemo.MVC) ![](https://img.tnblog.net/arcimg/hb/ea68377dd22f4a7297bd6f81dfe8f99d.png) tn>安装依赖包 `Microsoft.AspNetCore.Authentication.OpenIdConnect` ```bash Install-Package Microsoft.AspNetCore.Authentication.OpenIdConnect -Version 3.1.9 ``` tn>修改`Startup.cs` ```csharp public void ConfigureServices(IServiceCollection services) { // 我们关闭了JWT的Claim 类型映射, 以便允许well-known claims JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); JwtSecurityTokenHandler.DefaultMapInboundClaims = false; services.AddAuthentication(options => { options.DefaultScheme = "Cookies"; options.DefaultChallengeScheme = "oidc"; }) .AddCookie("Cookies") // 我们用作Cookies作为首选方式 .AddOpenIdConnect("oidc", options => { options.SignInScheme = "Cookies"; options.Authority = "https://localhost:7200"; // 授权地址 options.ClientId = "client_id_mvc"; options.ClientSecret = "mvc_secret"; // 退出设置 options.SignedOutCallbackPath = "/Home/Index"; // options.RequireHttpsMetadata = true; // 为http方式请求尝试了一下行不通必须https两边都是 options.ResponseType = "code";// 类型 options.SaveTokens = true; // 保存token // options.GetClaimsFromUserInfoEndpoint = true; // 获取所有信息 options.Scope.Clear(); // 清理范围 options.Scope.Add("ApiOne"); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("rc.bc"); options.Scope.Add("offline_access");// 脱机访问令牌 }); services.AddHttpClient(); services.AddControllersWithViews(); } ``` tn>在下面的`Configure`方法中添加好下列两句代码 ```csharp app.UseAuthentication(); app.UseAuthorization(); ``` tn>修改客户端`HomeController`中的`Privacy`方法与页面 ```csharp [Authorize] public async Task<IActionResult> Privacy() { var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken); var idToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken); var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken); var code = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.Code); ViewData["accessToken"] = accessToken; ViewData["idToken"] = idToken; ViewData["refreshToken"] = refreshToken; ViewData["code"] = code; // 获取接口数据 var httpClient = _httpClientFactory.CreateClient(); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var Result = await httpClient.GetAsync("http://localhost:5280/WeatherForecast"); if (Result.IsSuccessStatusCode) { ViewData["Apione"] = await Result.Content.ReadAsStringAsync(); } return View(); } ``` ```html @{ ViewData["Title"] = "Privacy Policy"; } <h1>@ViewData["Title"]</h1> <h2>Access Token:</h2> <p>@ViewData["accessToken"]</p> <h2>Id Token:</h2> <p>@ViewData["idToken"]</p> <h2>Refresh Token:</h2> <p>@ViewData["refreshToken"]</p> <h2>Code:</h2> <p>@ViewData["code"]</p> <h2>Apione:</h2> <p>@ViewData["Apione"]</p> <dl> @foreach (var claim in User.Claims) { <dt>@claim.Type</dt> <dd>@claim.Value</dd> } </dl> ``` tn>将AiDaSi.OcDemo.Authenzation(授权服务器),ApIDemo1(接口)修改为https。因为本人亲自尝试了很多遍,如果为http,在授权服务器登录成功后将会一直循环到login登录页面那儿...(这是幻术伊邪那美^_^)。 ![](https://img.tnblog.net/arcimg/hb/16d213ca6696411aaa9c73df93bf5df8.png) ![](https://img.tnblog.net/arcimg/hb/f67335b9d42a467aa234416c01b4182b.png) ![](https://img.tnblog.net/arcimg/hb/90c1f146e4a54401b1a1faf5f04071af.png) >### 修改授权服务器 tn>在`Config.cs`添加`Mvc`客户端 ```csharp new Client { ClientId = "client_id_mvc", ClientName = "ASP.NET Core MVC Client", AllowedGrantTypes = GrantTypes.Code, ClientSecrets = { new Secret("mvc_secret".Sha256()) }, // Species允许 URI返回令牌或授权代码到 RedirectUris = { "https://localhost:5002/signin-oidc" }, // 为基于HTTP前端通道的注销指定客户端的注销 URI。 FrontChannelLogoutUri = "https://localhost:5002/signout-oidc", // 允许URI在注销后重定向到的退出 PostLogoutRedirectUris = { "https://localhost:5002/Home/Index" }, AllowedScopes = { "ApiOne", IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, "rc.bc" }, // 将所有声明放在 id标记中,允许有很多Claims AlwaysIncludeUserClaimsInIdToken = true, // 获取或设置一个值,该值指示是否允许脱机访问. 默认值为 false。 AllowOfflineAccess = true, // 指定是否需要同意屏幕(默认值为 false) RequireConsent = false, }, ``` tn>在`Startup`的`ConfigureServices`中添加授权登录地址。 ```csharp services.ConfigureApplicationCookie(config => { config.Cookie.Name = "IdentityServer.Cookie"; // 设置Cookie名称 config.LoginPath = "/IdentityCodeAuth/Login"; // 设置登录地址 }); ``` tn>创建用户实例 ![](https://img.tnblog.net/arcimg/hb/4925edfced0c497491cd61372a43c6b1.png) ```csharp public class LoginViewModel { /// <summary> /// 用户名 /// </summary> public string Username { get; set; } /// <summary> /// 密码 /// </summary> public string Password { get; set; } /// <summary> /// 返回连接 /// </summary> public string ReturnUrl { get; set; } } ``` tn>在添加授权地址后我们也应该添加相应的控制器,并给对应的用户模型附上用户名与返回地址。`returnUrl`参数表示服务器跳转到客户端时的链接。 ```csharp public class IdentityCodeAuthController : Controller { // 用户界面使用提供的服务与 IdentityServer进行通信。 private readonly IIdentityServerInteractionService _interaction; public IdentityCodeAuthController( IIdentityServerInteractionService interaction, ) { _interaction = interaction; } [HttpGet] public async Task<IActionResult> Login(string returnUrl) { var vm = new LoginViewModel() { ReturnUrl = returnUrl }; // 获取上下文的内容 var context = await _interaction.GetAuthorizationContextAsync(returnUrl); // 判断用户名是否存在 vm.Username = context?.LoginHint; return View(vm); } } ``` tn>创建出所对应的view,里面放了一个表单 ```html @model LoginViewModel <form> <input type="hidden" asp-for="ReturnUrl" /> <div> <label>Username</label> <input asp-for="Username" /> </div> <div> <label>Password</label> <input asp-for="Password" /> </div> <div> <button type="submit">Sign In</button> </div> </form> ``` tn>接着我们来运行一下,通过点击客户端首页上的`Privacy`,然后成功跳转到了授权服务器的登录页面。完成了A项。 ![](https://img.tnblog.net/arcimg/hb/c0046be89f164b91ae22ec3e04df63bd.png) ![](https://img.tnblog.net/arcimg/hb/f41afbaadd554f259c8dba388e98be1e.png) tn>接着我们添加登录时需要处理的代码,我们准备放到`IdentityCodeAuth`控制器中的`Login`页面进行处理。处理时登录成功,拿到授权验证码到跳转到客户端的页面,就完成了B与C两项。 ```html <form asp-controller="IdentityCodeAuth" asp-action="Login" method="post"> ``` ```csharp /// <summary> /// ValidateAntiForgeryToken 防伪 /// </summary> /// <param name="vm"></param> /// <returns></returns> [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Login(LoginViewModel vm) { // 获取上下文的内容 var context = await _interaction.GetAuthorizationContextAsync(vm.ReturnUrl); if (ModelState.IsValid) { var result = await _signInManager.PasswordSignInAsync(vm.Username, vm.Password, false, false); // 判断登录是否成功 if (result.Succeeded) { // 登录成功 return Redirect(vm.ReturnUrl); } else if (result.IsLockedOut) { // 如果登录失败就执行如下。。。 // 不做处理就会直接回到Login页面中。。。 } } return View(vm); } ``` tn>接着我们尝试登录试一下 ![](https://img.tnblog.net/arcimg/hb/3724553cb47f47f2b577604a40c71067.png) ![](https://img.tnblog.net/arcimg/hb/1342e5d640c041ed8ace1f8f9c722666.png) tn>最后我们看到访问需要授权接口时拿去到了Access Token,并访问到了接口。 >### 退出授权 tn>在`_Layout.cshtml`页面上`Privacy`后面添加一行判断是否登录的代码;如果登录了,则显示登录按钮。(同样的我们这里Logout方法并不存在,所以需要添加Home控制器下的Logout方法) ```html @if (User.Identity.IsAuthenticated) { <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Logout">Logout</a> </li> } ``` ```csharp public async Task<IActionResult> Logout() { return SignOut("Cookies", "oidc"); } ``` tn>接着在授权服务器上`Startup.cs`添加登出的地址。(因为你再客户端上登出了,在没有授权服务器上退出) ```csharp services.ConfigureApplicationCookie(config => { config.Cookie.Name = "IdentityServer.Cookie"; // 设置Cookie名称 config.LoginPath = "/IdentityCodeAuth/Login"; // 设置登录地址 config.LogoutPath = "/IdentityCodeAuth/Logout"; // 设置退出地址 }); ``` tn>在`IdentityCodeAuth`控制器下添加退出的`Logout`方法 ```csharp [HttpGet] public async Task<IActionResult> Logout(string logoutId) { await _signInManager.SignOutAsync(); var logoutRequest = await _interaction.GetLogoutContextAsync(logoutId); if (string.IsNullOrEmpty(logoutRequest.PostLogoutRedirectUri)) { return RedirectToAction("Index", "WeatherForecast"); } return Redirect(logoutRequest.PostLogoutRedirectUri); } ``` tn>运行测试一下。 ![](https://img.tnblog.net/arcimg/hb/4711e23eb6b84b0cb241eb5db2925f91.png) ![](https://img.tnblog.net/arcimg/hb/d77af9612b6b47ba8268af007cc14374.png) >### 注册模块 tn>在授权服务器上创建`RegisterViewModel.cs`注册实例,添加注册方法(Register)与对应的视图 >RegisterViewModel.cs ```csharp public class RegisterViewModel { [Required] public string Username { get; set; } [Required] [DataType(DataType.Password)] public string Password { get; set; } [Required] [DataType(DataType.Password)] [Compare("Password")] public string ConfirmPassword { get; set; } public string ReturnUrl { get; set; } } ``` >IdentityCodeAuthController.cs ```csharp /// <summary> /// 注册页面 /// </summary> /// <param name="returnUrl"></param> /// <returns></returns> [HttpGet] public IActionResult Register(string returnUrl) { return View(new RegisterViewModel { ReturnUrl = returnUrl }); } ``` >Register.cshtml ```html @model RegisterViewModel @* 注册失败的错误消息 *@ @if (ViewData["Message"] != null ) { <div style="color:red"> @ViewData["Message"].ToString() </div> } <form asp-controller="IdentityCodeAuth" asp-action="Register" method="post"> <input type="hidden" asp-for="ReturnUrl" /> <div> <label>Username</label> <input asp-for="Username" /> <span asp-validation-for="Username"></span> </div> <div> <label>Password</label> <input asp-for="Password" /> <span asp-validation-for="Password"></span> </div> <div> <label>Confirm Password</label> <input asp-for="ConfirmPassword" /> <span asp-validation-for="ConfirmPassword"></span> </div> <div> <button type="submit">Register In</button> </div> </form> <a asp-controller="IdentityCodeAuth" asp-action="Login" asp-route-returnUrl="@Model.ReturnUrl">Back to Login</a> ``` >为登录页面添加注册标签 Login.cshtml ```html <a asp-controller="IdentityCodeAuth" asp-action="Register" asp-route-returnUrl="@Model.ReturnUrl">Register</a> ``` ![](https://img.tnblog.net/arcimg/hb/672253a3bce947d48fb99cfe30b4c5fc.png) >接着我们在`IdentityCodeAuthController`添加处理注册方法 ```csharp /// <summary> /// 验证注册 /// </summary> /// <param name="vm"></param> /// <returns></returns> [HttpPost] public async Task<IActionResult> Register(RegisterViewModel vm) { string Message = ""; try { //验证模型是否有效 //判断两次密码是否一致 if (!ModelState.IsValid) { throw new Exception("验证模型失败"); } //创建用户 var user = new IdentityUser(vm.Username); var result = await _userManager.CreateAsync(user, vm.Password); if (result.Succeeded) { //登录用户 await _signInManager.SignInAsync(user, false); return Redirect(vm.ReturnUrl); } } catch (Exception ex) { Message = ex.Message; } ViewData["Message"] = Message; return View(vm); } ``` >当我们验证不通过时,会显示错误消息,通过则会直接登录返回到客户端。 ![](https://img.tnblog.net/arcimg/hb/cd63ad71276445e69d294eeac0364e43.png) ![](https://img.tnblog.net/arcimg/hb/92122c137a6c4ab4b1605fbd0d90f47f.png) >### 删除cookie声明映射 tn>我们发现有些Cookie声明映射是多余的不需要的,如:amr、s_hash,我们可以在MVC客户端中的`Startup.cs`中AddOpenIdConnect委托里将其删除。 ```csharp //删除cookie声明映射 options.ClaimActions.DeleteClaim("amr"); options.ClaimActions.DeleteClaim("s_hash"); ``` ![](https://img.tnblog.net/arcimg/hb/75eb088e67c448398bbc9b1b2003c53a.png) >### 刷新Token tn>在这之前呢我们先把授权服务器的Access Token设置为1分钟,并且把Api的验证Token的时间也为1分钟。二者缺一不可。在此之前请在mvc客户端引用好`IdentityModel`。 >AiDaSi.OcDemo.Authenzation --> Config.cs ```csharp new Client { ClientId = "client_id_mvc", ClientName = "ASP.NET Core MVC Client", AllowedGrantTypes = GrantTypes.Code, ClientSecrets = { new Secret("mvc_secret".Sha256()) }, // Species允许 URI返回令牌或授权代码到 RedirectUris = { "https://localhost:5002/signin-oidc" }, // 为基于HTTP前端通道的注销指定客户端的注销 URI。 FrontChannelLogoutUri = "https://localhost:5002/signout-oidc", // 允许URI在注销后重定向到的退出 PostLogoutRedirectUris = { "https://localhost:5002/Home/Index" }, AllowedScopes = { "ApiOne", IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, "rc.bc" }, // 允许浏览器通过 (如果js等SPA需要通过验证) // AllowAccessTokensViaBrowser = true, // 将所有声明放在 id标记中,允许有很多Claims AlwaysIncludeUserClaimsInIdToken = true, // 获取或设置一个值,该值指示是否允许脱机访问. 默认值为 false。 AllowOfflineAccess = true, // 设置Token时间为60秒,除了这里要设置之外也需要在api资源中设置验证过期时间 AccessTokenLifetime = 60, // 指定是否需要同意屏幕(默认值为 false) RequireConsent = false, }, ``` >ApIDemo1 --> Startup.cs ```csharp services .AddAuthentication("Bearer") .AddJwtBearer("Bearer", config => { config.Authority = "https://localhost:7200"; // 授权服务器地址 //确定自己是哪个资源(资源名称) config.Audience = "ApiOne"; config.RequireHttpsMetadata = false; // 是否使用https进行通信 //取消验证用户以及验证角色 config.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters() { ValidateIssuer = true, ValidateAudience = false, //每间隔1分钟去检查Token是否有效 ClockSkew = TimeSpan.FromMinutes(1), //要求运行有超时时间 RequireExpirationTime = true }; }); ``` tn>启动项目,我们通过 jwt.io 解析到过期时间(exp),相隔1分钟后我们再次刷新页面。 ![](https://img.tnblog.net/arcimg/hb/f990e5d403234c4cab62214d53b3a17f.png) ![](https://img.tnblog.net/arcimg/hb/a6d49ee620da46b59c0c94450b5c0325.png) ![](https://img.tnblog.net/arcimg/hb/3550d467989541d6a7e1e70acfe82bd2.png) tn>我们发现Api请求不了了,下面我们将添加刷新Token的代码;由于`exp`是Unix时间戳,所以这里我们创建一个`TimeHelper.cs`工具类 ![](https://img.tnblog.net/arcimg/hb/b49ba90c3a964073aaa9cd7fb2861080.png) ```csharp public static class TimeHelper { //将unix时间戳转换成系统时间 public static DateTime unixtime(this string time) { DateTime dtStart = TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1)); long lTime = long.Parse(time + "0000000"); TimeSpan toNow = new TimeSpan(lTime); DateTime dtResult = dtStart.Add(toNow); return dtResult; } //将系统时间转换成unix时间戳 public static long timeunix2(this DateTime dt) { DateTimeOffset dto = new DateTimeOffset(dt); return dto.ToUnixTimeSeconds(); } //将系统时间转换成unix时间戳 public static DateTime unixtime2(this double d) { System.DateTime time = System.DateTime.MinValue; System.DateTime startTime = TimeZone.CurrentTimeZone.ToLocalTime(new System.DateTime(1970, 1, 1)); time = startTime.AddMilliseconds(d); return time; } } ``` tn>在`HomeController.cs`控制器中引用`AiDaSi.OcDemo.MVC.Helper`的命名空间,并添加`RenewTokensAsync`方法,对Token进行刷新。获取到新Token后我们有两种不同的方法对Token进行更新。 ```csharp private async Task<string> RenewTokensAsync() { var client = _httpClientFactory.CreateClient(); var disco = await client.GetDiscoveryDocumentAsync("https://localhost:7200"); if (disco.IsError) { // 我们这里将Cookie清空掉 foreach (var item in Request.Cookies) { Response.Cookies.Delete(item.Key); } // 报错 return await Task.FromResult(disco.Error); // throw new Exception(disco.Error); } var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken) // 刷新token的操作 var tokenResponse = await client.RequestRefreshTokenAsync(new RefreshTokenRequest { Address = disco.TokenEndpoint, ClientId = "client_id_mvc", ClientSecret = "mvc_secret", RefreshToken = refreshToken }); #region 第一种写法 if (tokenResponse.IsError) { // 我们这里将Cookie清空掉 foreach (var item in Request.Cookies) { Response.Cookies.Delete(item.Key); } return await Task.FromResult(tokenResponse.Error); // 报错 // throw new Exception(tokenResponse.Error); } var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResponse.ExpiresIn); var tokens = new[] { new AuthenticationToken { Name = OpenIdConnectParameterNames.IdToken, Value = tokenResponse.IdentityToken }, new AuthenticationToken { Name = OpenIdConnectParameterNames.AccessToken, Value = tokenResponse.AccessToken }, new AuthenticationToken { Name = OpenIdConnectParameterNames.RefreshToken, Value = tokenResponse.RefreshToken }, new AuthenticationToken { Name = "expires_at", Value = expiresAt.ToString("o", CultureInfo.InvariantCulture) } }; // 获取身份认证的结果,包含当前的pricipal和properties var currentAuthenticateResult = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); // 把新的tokens存起来 currentAuthenticateResult.Properties.StoreTokens(tokens); // 登录 await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, currentAuthenticateResult.Principal, currentAuthenticateResult.Properties); return tokenResponse.AccessToken; #endregion #region 第二种写法 //下面将修改上下文 var authInfo = await HttpContext.AuthenticateAsync("Cookies"); authInfo.Properties.UpdateTokenValue("access_token", tokenResponse.AccessToken); authInfo.Properties.UpdateTokenValue("id_token", tokenResponse.IdentityToken); authInfo.Properties.UpdateTokenValue("refresh_token", tokenResponse.RefreshToken); //二次认证(更新token) await HttpContext.SignInAsync("Cookies", authInfo.Principal, authInfo.Properties); #endregion } ``` tn>在`Privacy`方法中对Token失效进行验证 ```csharp [Authorize] public async Task<IActionResult> Privacy() { var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken); var idToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken); var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken); var code = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.Code); ViewData["accessToken"] = accessToken; ViewData["idToken"] = idToken; ViewData["refreshToken"] = refreshToken; ViewData["code"] = code; // 获取接口数据 var httpClient = _httpClientFactory.CreateClient(); //httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); httpClient.SetBearerToken(accessToken); // 验证Token是否失效 string tokenStr = accessToken; var handler = new JwtSecurityTokenHandler(); var payload = handler.ReadJwtToken(tokenStr).Payload; var expclaim = payload.Claims.FirstOrDefault(x=>x.Type == "exp"); DateTime dateTime = expclaim.Value.unixtime(); int compNum = DateTime.Compare(DateTime.Now, dateTime); //判断当前时间是否大于token的过期时间,如果有就刷新token,这样就能达到无缝衔接 if (compNum > 0) { await RenewTokensAsync(); return RedirectToAction(); } var Result = await httpClient.GetAsync("http://localhost:5280/WeatherForecast"); if (Result.IsSuccessStatusCode) { ViewData["Apione"] = await Result.Content.ReadAsStringAsync(); } return View(); ``` tn>由于场景很难模拟,大家可以打断点自行测试。 ![](https://img.tnblog.net/arcimg/hb/ae14a64524fb497c8ed924f7659719c7.png) ![](https://img.tnblog.net/arcimg/hb/225bbfdcb2a7454b8500d24efe8d096b.png) 接下来将更新`javascript`的客户端...
欢迎加群讨论技术,1群:677373950(满了,可以加,但通过不了),2群:656732739
👈{{preArticle.title}}
👉{{nextArticle.title}}
评价
{{titleitem}}
{{titleitem}}
{{item.content}}
{{titleitem}}
{{titleitem}}
{{item.content}}
尘叶心繁
这一世以无限游戏为使命!
博主信息
排名
6
文章
6
粉丝
16
评论
8
文章类别
.net后台框架
166篇
linux
17篇
linux中cve
1篇
windows中cve
0篇
资源分享
10篇
Win32
3篇
前端
28篇
传说中的c
4篇
Xamarin
9篇
docker
15篇
容器编排
101篇
grpc
4篇
Go
15篇
yaml模板
1篇
理论
2篇
更多
Sqlserver
4篇
云产品
39篇
git
3篇
Unity
1篇
考证
2篇
RabbitMq
23篇
Harbor
1篇
Ansible
8篇
Jenkins
17篇
Vue
1篇
Ids4
18篇
istio
1篇
架构
2篇
网络
7篇
windbg
4篇
AI
18篇
threejs
2篇
人物
1篇
嵌入式
2篇
python
13篇
HuggingFace
8篇
pytorch
9篇
opencv
6篇
最新文章
最新评价
{{item.articleTitle}}
{{item.blogName}}
:
{{item.content}}
关于我们
ICP备案 :
渝ICP备18016597号-1
网站信息:
2018-2024
TNBLOG.NET
技术交流:
群号656732739
联系我们:
contact@tnblog.net
欢迎加群
欢迎加群交流技术