3 选 1

IdentityServer 4

本来 IdentityServer 4 一直都是首选的, 但在 2020 年他们决定成立公司, IdentityServer 5 就开始收费了.

The Future of IdentityServer

Azure Active Directory B2C

Azure 的 SASS

ASP.NET Core 6 and Authentication Servers

ASP.NET Core Team 明确表示他们不会投入任何资源去研发类似 IdentityServer 的东西, 虽然 ASP.NET Core 5.0 开始, SPA 的 template 是依赖 IdentityServer 4 的,

并且 6.0 也会依赖, 7.0 会有替代, 但绝对没有计划要做类似的或者辅助类似的 Library, 原因是 M$ 要卖 Azure 产品.

OpenIddict Core

Github 地址

这个是目前跟 IdentityServer 4 最靠近的替代库了. 这篇主要介绍这个

关于这 3 者的对比可以参考 :

Token-Based Security: 3 Possible Alternatives To IdentityServer

ASP.NET Core 6 and authentication servers: the real bait and switch is not the one you think (OpenIddict Core 作者写的)

前言

OpenIddict 是很小的项目, 算是 1 个人维护的吧 (不过作者也挺厉害的, 一直用心维护着)

它没有 IdentityServer 那么好的封装, 文档, 教程. 但是对于开发人员来说, 要搞起来还是挺容易的.

这里只是大概记入一些 OpenIddict 对应的关系和解释, 并不会 step by step 的教, 要整个搞起来建议跟着 refer 做.

主要参考:

Step by step tutorial (client = Postman)

Step by step tutorial (client = Angular)

项目源码

官网文档

各种 Samples

Config 的一些细节

Startup.cs 说起

OpenIddict 推荐使用 EF Core 做 (好像是可以换 Storage, 但我就是用 EF Core 的所以也没有去研究其它的)

使用 UseOpenIddict extension 来进行 setup

这里和 IdentityServer 4 的 setup 方式不太一样哦. Library 如果要结合 EF Core 的话, 我目前觉得 IdentityServer 4 的方式会比较正确. 就是使用分开的 Context Migrations

OpenIddict 用的方式是 ReplaceService

通过 ReplaceService, OpenIddict 就可以拦截到 EntityModelBuilder

不过有一点要注意哦, ReplaceService 只能调用一次而已. 如果项目本身有用到或者其它 Library 也有用到, 是会坏掉的. 所以我还是觉得 Library 应该要向 IdentityServer 4 那样,使用不同的 DbContext 来管理.

接下来就是 AddOpenIddict()

AddQuartz 是因为 OpenIddict 需要定时去 Database 清楚过期的 Access Token, 所以需要依赖一个 Server Task Library, 而它选了 Quartz ... 竟然不选 Hangfire...

注: v3.7 后 UseMicrosoftDependencyInjectionJobFactory 已经不需要了。

然后是 .AddCore 负责 setup EF Core 和 Quartz

接着是 .AddServer 就是 autho server 的主要 config 了

里面有:

Token 的寿命

支持的 Flow

各种 Endpoint URL

签名用到的钥匙 (非对称加密 X.509).

如果想从 Azure Key Vault 拿 certificate 不能使用 Async 哦.

OpenIddict 作者的回复: Make the ConfigureServices method async in Startup.cs

Token 加密用到的钥匙 (我目前 autho 和 resource server 是同一台, 所以这里我选择用对称加密, 一把钥匙就好了, 如果是分开的情况那么 autho server 就要有 resource server 的公钥, 之前的文章有讲过)

它的钥匙也是支持 key rotation 的, 和 IdentityServer 类似. 你放多多把钥匙进去, 它选来用

最后的 UseDataProtection 是指 access token, refresh token 的加密, identity token 只能用 JWT 哦. (我看官网好像是推荐使用 Data Protection, 我觉得也挺好的, 毕竟 Identity Cookie 也是用 Data Protection)

接下来是 resource server 的 config (我的 autho 和 resource 是同一台, 所以是在一起 setup)

UseLocalServer 是因为我的 autho 和 resource 是同一台, 如果是分开的话, resource server 需要验证签名, 所以还需要 link 去 autho discover 发现 server 的公钥.

同时解密 Token 也是需要 config 钥匙.

然后是 add test data, 这里的 test data 是指 client infomation

production 的情况下, client 数据 should be 已经在数据库里面了.

在来看看 client 是怎样输入的

 更新 2021-12-15: 补上很重要的 Logout

这里用 Insomnia (类似 Postman) 来做测试

上半段注释掉的是 for client credentials flow 的, 下面是 authorization code flow + PKCE(前后端分离 web app 的 flow)

Insomnia.rest 是 client redirect URL,

scope API 就是 resource server

openid 是要求返回 identity token

offline_access 就是返回 refresh token

具体测试画面长这样

setting 关掉 validate certificates (因为本地测试使用 self signed certificate)

不关掉会有 error

PKCE 不需要 client secret 但需要 code_verifier 和 code_challenge. (Insomnia 内置了)

所有的 token endpoint 我们需要自己做 (IdentityServer 4 是有封装的, OpenIddict 没有)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using OpenIddict.Validation.AspNetCore; namespace AuthorizationServer.Controllers
{
[ApiController]
public class AuthorizationController : ControllerBase
{
private readonly SignInManager<User> _signInManager;
public AuthorizationController(
SignInManager<User> signInManager
)
{
_signInManager = signInManager;
} [HttpGet("~/connect/authorize")]
[HttpPost("~/connect/authorize")]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); // Retrieve the user principal stored in the authentication cookie.
var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme); // If the user principal can't be extracted, redirect the user to the login page.
if (!result.Succeeded)
{
return Challenge(
authenticationSchemes: IdentityConstants.ApplicationScheme,
properties: new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
});
} // Create a new claims principal
var claims = new List<Claim>
{
// 'subject' claim which is required
new Claim(OpenIddictConstants.Claims.Subject, result.Principal.Identity.Name),
new Claim("some claim", "some value").SetDestinations(OpenIddictConstants.Destinations.AccessToken),
new Claim(OpenIddictConstants.Claims.Email, "some@email").SetDestinations(OpenIddictConstants.Destinations.IdentityToken)
}; var claimsIdentity = new ClaimsIdentity(claims, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); // Set requested scopes (this is not done automatically)
claimsPrincipal.SetScopes(request.GetScopes()); // Signing in with the OpenIddict authentiction scheme trigger OpenIddict to issue a code (which can be exchanged for an access token)
return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
} [HttpPost("~/connect/token")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); ClaimsPrincipal claimsPrincipal; if (request.IsClientCredentialsGrantType())
{
// Note: the client credentials are automatically validated by OpenIddict:
// if client_id or client_secret are invalid, this action won't be invoked. var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); // Subject (sub) is a required field, we use the client id as the subject identifier here.
identity.AddClaim(OpenIddictConstants.Claims.Subject, request.ClientId ?? throw new InvalidOperationException()); // Add some claim, don't forget to add destination otherwise it won't be added to the access token.
identity.AddClaim("some-claim", "some-value", OpenIddictConstants.Destinations.AccessToken); claimsPrincipal = new ClaimsPrincipal(identity); claimsPrincipal.SetScopes(request.GetScopes());
} else if (request.IsAuthorizationCodeGrantType())
{
// Retrieve the claims principal stored in the authorization code
claimsPrincipal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
} else if (request.IsRefreshTokenGrantType())
{
// Retrieve the claims principal stored in the refresh token.
claimsPrincipal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
} else
{
throw new InvalidOperationException("The specified grant type is not supported.");
} // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
} [Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
[HttpGet("~/connect/userinfo")]
public async Task<IActionResult> Userinfo()
{
var claimsPrincipal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal; return Ok(new
{
Name = claimsPrincipal.GetClaim(OpenIddictConstants.Claims.Subject),
Occupation = "Developer",
Age = 43
});
} [Authorize]
[HttpPost("~/connect/logout")]
public ActionResult LogOut()
{
// Ask ASP.NET Core Identity to delete the local and external cookies created
// when the user agent is redirected from the external identity provider
// after a successful authentication flow (e.g Google or Facebook).
_signInManager.SignOutAsync(); // Returning a SignOutResult will ask OpenIddict to redirect the user agent
// to the post_logout_redirect_uri specified by the client application or to
// the RedirectUri specified in the authentication properties if none was set.
return SignOut(
OpenIddictServerAspNetCoreDefaults.AuthenticationScheme
);
}
}
}

注意: Authen server connect/userinfo 是用·

[Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]

最后是 resource server 的 api, 大概长这样.

注意: Resource server 的 API 用的是 OpenIddictValidationAspNetCoreDefaults 和 Authen Server 的 OpenIddictServerAspNetCoreDefaults 是不同的哦.

[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]

出错放错了会报错:

An identity cannot be extracted from this request.
This generally indicates that the OpenIddict server stack was asked to validate a token for an endpoint it doesn't manage.
To validate tokens received by custom API endpoints, the OpenIddict validation handler

后端的部分大概就是这样了. 可以看出来上面这些只是 simple play around 而已.

要在项目上真的跑起来的话, 还需要搞定 :

1. client 进数据库

2. 证书 X.509

3. 登入页面和同意 (consent) 页面, assign claims

前端部分 Javascript

IdentityServer 4 有封装 coidc-client.js 的, 而 OpenIddict 自然是没有咯.

上面的例子都是使用了 Insomnia or Postman, 这里说一下 js 实现的话需要注意的几个点.

流程可以看这篇 oAuth2.0 简介 - 移动端app,

首先是 endpoint discovery well-known (https://authorization-server.com/.well-known/openid-configuration 这个是默认的 discovery url)

然后 redirect to login

然后 callback 页面 ajax post 去拿 access token

Content-Type 是 application/x-www-form-urlencoded 哦.

code_verifier 和 code_challenge

要完成这 2 个, 需要 random string (不可以用 Math.random, 不够安全, C# 也是不可以哦), SHA256, Base64URL-encoded.

在 node.js 是有 build-in 支持这些的

code_verifier:

code_challenge:

但是在 browser 却没有..., 使用 crypto-js 会导致 size 很大 . 所以比较好的方式是用 browser build-in 的接口,

参考:

Generate code_verifier and code_challenge in IE11 and modern browsers

Generating the code challenge for PKCEin OAuth 2

Proof Key for Code Exchange (PKCE)

Creating a code verifier and challenge for PKCEauth on Spotify API in ReactJS

How to calculate PCKE's code_verifier?

oauth-pkce

Example for TextEncoder,ArrayBuffer,Uint8Array (我自己写的例子)

太多种版本了... 但核心是差不多的.

js 做 random string 需要用到 window.crypto.getRandomValues

它是这样子用的

let array1 = new Uint8Array(1);
let array2 = new Uint16Array(1);
let array3 = new Uint32Array(1);
array1 = window.crypto.getRandomValues(array1); // { 0: 168 }
array2 = window.crypto.getRandomValues(array2); // { 0: 64634 }
array3 = window.crypto.getRandomValues(array3); // { 0: 2577571238 }

开一个 UintArray (8, 16, 32) 都可以, 调用 getRandom 后 array 里面就有 number. 依据 8, 16, 32 number 会不同长度.

如果 new UintArray(10) 则会有 0 到 10 的 property 咯.

有了 random number, 下一步就是把它弄成 random string. 做法很多, 比如可以写 abc...zAbc...Z1234...0, 然后 for loop 刚才的 random number 去抽 string 出来

也可以把 number toString(16) 变成 16 进制, 关键就是让这个 string 多样化一点, 到足够的 length 就可以停了.

接下来就是做 SHA256, 用到 window.crypto.subtle.digest

private sha256Async(codeVerifier: string): Promise<ArrayBuffer> {
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
return window.crypto.subtle.digest('SHA-256', data);
}

它返回的是 ArrayBuffer.

最后是 convert to Base64 URL encode

private base64UrlEncode(value: ArrayBuffer): string {
return window
.btoa(String.fromCharCode.apply(null, Array.from(new Uint8Array(value))))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}

题外话:除了转 base64 以外,转成 16 进制也很常见,这里顺便给个例子

private toHexadecimal(arrayBuffer: ArrayBuffer): string {
return Array.from(new Uint8Array(arrayBuffer)).map(b => b.toString(16).padStart(2, '0')).join('')
}

题外话: JS UUID

参考: YouTube – Stop Using The uuid Library In JavaScript

crypto 除了可以生成 random 还可以做 uuid 哦

console.log(crypto.randomUUID()); // bf28ef0b-765b-4174-8e9b-5e7ef5a1d213

支持率很好

state

state 也是要用安全的 random string, 然后一定要对比, 确保 handle code 收到的 state 是之前发出去的.

Angular 简单实现

参考: IMPLEMENTING OPENID CODE FLOW WITH PKCEUSING OpenIddict AND ANGULAR

app.ts

import { Component, ChangeDetectionStrategy } from '@angular/core';
import { HttpClient } from '@angular/common/http'; @Component({
selector: 'cp-oidc',
templateUrl: './oidc.component.html',
styleUrls: ['./oidc.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OidcComponent {
constructor(private httpClient: HttpClient) {} message = '';
refreshToken = '';
accessToken = ''; private randomString(length = 43): string {
const array = new Uint32Array(length);
window.crypto.getRandomValues(array);
return Array.from(array, number => ('0' + number.toString(16)).substr(-2))
.join('')
.substr(0, length);
} private sha256Async(value: string): Promise<ArrayBuffer> {
const encoder = new TextEncoder();
const data = encoder.encode(value);
return window.crypto.subtle.digest('SHA-256', data);
} private base64Urlncode(arrayBuffer: ArrayBuffer): string {
return window
.btoa(String.fromCharCode.apply(null, Array.from(new Uint8Array(arrayBuffer))))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
} clearLocalCodeVerifier() {
window.localStorage.removeItem('codeVerifier');
window.localStorage.removeItem('state');
} async redirectToLoginPage() {
// prettier-ignore
// eslint-disable-next-line prettier/prettier
const endpoint = await this.httpClient
.get<{ 'authorization_endpoint': string }>(
'https://192.168.1.152:44300/.well-known/openid-configuration'
)
.toPromise();
const codeVerifier = window.localStorage.getItem('codeVerifier') ?? this.randomString();
const state = window.localStorage.getItem('state') ?? this.randomString();
window.localStorage.setItem('codeVerifier', codeVerifier);
window.localStorage.setItem('state', state);
const sha256 = await this.sha256Async(codeVerifier);
const codeChallange = this.base64Urlncode(sha256);
// const codeChallange = 'IlOEZEKlN2ZxNWswqLqJVhtkp_v3Uxqmghzt7Rcguio';
const authorizationUrl = endpoint.authorization_endpoint;
const clientId = 'Angular';
const redirectUrl = 'https://192.168.1.152:4200/test/oidc/callback';
const scope = 'api openid offline_access';
window.location.href = `${authorizationUrl}?response_type=code&client_id=${encodeURIComponent(
clientId
)}&redirect_uri=${encodeURIComponent(redirectUrl)}&scope=${encodeURIComponent(
scope
)}&state=${encodeURIComponent(state)}&code_challenge=${encodeURIComponent(
codeChallange
)}&code_challenge_method=S256`;
// console.log([codeChallange, authorizationUrl, clientId, redirectUrl, scope, state]);
} requestMessage() {}
getRefreshToken() {}
}

callback.ts

import { HttpClient, HttpParams } from '@angular/common/http';
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; // eslint-disable-next-line prettier/prettier
// prettier-ignore
interface TokenResponseData {
'access_token': string;
'refresh_token': string;
'expires_in': number;
'id_token': string;
'scope': string;
'token_type': string;
} @Component({
selector: 'cp-callback',
templateUrl: './callback.component.html',
styleUrls: ['./callback.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CallbackComponent implements OnInit {
constructor(private activatedRoute: ActivatedRoute, private httpClient: HttpClient) {} private tokenResponseData!: TokenResponseData; async ngOnInit(): Promise<void> {
const localToken = window.localStorage.getItem('token');
if (localToken != null) {
this.tokenResponseData = JSON.parse(localToken);
} else {
// prettier-ignore
// eslint-disable-next-line prettier/prettier
const endpoint = await this.httpClient
.get<{ 'token_endpoint': string }>(
'https://192.168.1.152:44300/.well-known/openid-configuration'
)
.toPromise();
const tokenEndpoint = endpoint.token_endpoint;
const code = this.activatedRoute.snapshot.queryParamMap.get('code')!;
const state = this.activatedRoute.snapshot.queryParamMap.get('state')!;
const codeVerifier = window.localStorage.getItem('codeVerifier')!;
const localState = window.localStorage.getItem('state')!;
if (state !== localState) {
window.alert('state not same');
}
const redirectUrl = 'https://192.168.1.152:4200/test/oidc/callback';
const body = new HttpParams()
.set('grant_type', 'authorization_code')
.set('code', code)
.set('redirect_uri', redirectUrl)
.set('client_id', 'Angular')
.set('code_verifier', codeVerifier);
this.tokenResponseData = await this.httpClient
.post<TokenResponseData>(tokenEndpoint, body.toString(), {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.toPromise();
window.localStorage.setItem('token', JSON.stringify(this.tokenResponseData));
console.log('tokenResponseData', this.tokenResponseData);
}
} async refreshToken() {
const body = new HttpParams()
.set('refresh_token', this.tokenResponseData.refresh_token)
.set('grant_type', 'refresh_token')
.set('client_id', 'Angular');
this.tokenResponseData = await this.httpClient
.post<TokenResponseData>('https://192.168.1.152:44300/connect/token', body.toString(), {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.toPromise();
window.localStorage.setItem('token', JSON.stringify(this.tokenResponseData));
console.log('tokenResponseData', this.tokenResponseData);
} async getMessage() {
const responseData = await this.httpClient
.get('https://192.168.1.152:44300/api/message', {
headers: {
Authorization: `${this.tokenResponseData.token_type} ${this.tokenResponseData.access_token}`,
},
})
.toPromise();
console.log('message', responseData);
} async getUserInfo() {
const responseData = await this.httpClient
.get('https://192.168.1.152:44300/connect/userinfo', {
headers: {
Authorization: `${this.tokenResponseData.token_type} ${this.tokenResponseData.access_token}`,
},
})
.toPromise();
console.log('message', responseData);
} clearLocalToken() {
window.localStorage.removeItem('token');
}
}

logout 是用 form post 而不是 ajax.

<form method="post" action="https://192.168.1.152:44300/connect/logout">
<input
type="hidden"
name="post_logout_redirect_uri"
value="https://192.168.1.152:4200/signout-oidc"
/>
<button>LOGOUT</button>
</form>

OIDC – OpenIddict Core的更多相关文章

  1. Open ID Connect(OIDC)在 ASP.NET Core中的应用

    我们在<ASP.NET Core项目实战的课程>第一章里面给identity server4做了一个全面的介绍和示例的练习 ,这篇文章是根据大家对OIDC遇到的一些常见问题整理得出. 本文 ...

  2. 【ASP.NET Core分布式项目实战】(二)oauth2 + oidc 实现 server部分

    本博客根据http://video.jessetalk.cn/my/course/5视频整理(内容可能会有部分,推荐看源视频学习) 资料 我们基于之前的MvcCookieAuthSample来做开发 ...

  3. asp.net core系列 57 IS4 使用混合流(OIDC+OAuth2.0)添加API访问

    一.概述 在上篇中,探讨了交互式用户身份验证,使用的是OIDC协议. 在之前篇中对API访问使用的是OAuth2.0协议.这篇把这两个部分放在一起,OpenID Connect和OAuth 2.0组合 ...

  4. ASP.NET Core 认证与授权[4]:JwtBearer认证

    在现代Web应用程序中,通常会使用Web, WebApp, NativeApp等多种呈现方式,而后端也由以前的Razor渲染HTML,转变为Stateless的RESTFulAPI,因此,我们需要一种 ...

  5. [认证授权] 4.OIDC(OpenId Connect)身份认证授权(核心部分)

    0 目录 认证授权系列:http://www.cnblogs.com/linianhui/category/929878.html 1 什么是OIDC? 看一下官方的介绍(http://openid. ...

  6. ASP.NET Core 认证与授权[1]:初识认证

    在ASP.NET 4.X 中,我们最常用的是Forms认证,它既可以用于局域网环境,也可用于互联网环境,有着非常广泛的使用.但是它很难进行扩展,更无法与第三方认证集成,因此,在 ASP.NET Cor ...

  7. ASP.NET Core 认证与授权[3]:OAuth & OpenID Connect认证

    在上一章中,我们了解到,Cookie认证是一种本地认证方式,通常认证与授权都在同一个服务中,也可以使用Cookie共享的方式分开部署,但局限性较大,而如今随着微服务的流行,更加偏向于将以前的单体应用拆 ...

  8. 使用angular4和asp.net core 2 web api做个练习项目(三)

    第一部分: http://www.cnblogs.com/cgzl/p/7755801.html 第二部分: http://www.cnblogs.com/cgzl/p/7763397.html 后台 ...

  9. ASP.NET Core的身份认证框架IdentityServer4(9)-使用OpenID Connect添加用户认证

    OpenID Connect OpenID Connect 1.0是OAuth 2.0协议之上的一个简单的身份层. 它允许客户端基于授权服务器执行的身份验证来验证最终用户的身份,以及以可互操作和类似R ...

  10. spring cloud+dotnet core搭建微服务架构:Api授权认证(六)

    前言 这篇文章拖太久了,因为最近实在太忙了,加上这篇文章也非常长,所以花了不少时间,给大家说句抱歉.好,进入正题.目前的项目基本都是前后端分离了,前端分Web,Ios,Android...,后端也基本 ...

随机推荐

  1. Apache Kyuubi 在B站大数据场景下的应用实践

    01 背景介绍 近几年随着B站业务高速发展,数据量不断增加,离线计算集群规模从最初的两百台发展到目前近万台,从单机房发展到多机房架构.在离线计算引擎上目前我们主要使用Spark.Presto.Hive ...

  2. Java 网络编程(TCP编程 和 UDP编程)

    1. Java 网络编程(TCP编程 和 UDP编程) @ 目录 1. Java 网络编程(TCP编程 和 UDP编程) 2. 网络编程的概念 3. IP 地址 3.1 IP地址相关的:域名与DNS ...

  3. ICPC游记

    \[\Large\color{#FCAEBD}『2024ICPC河南站 游记』 \] Day 0 晚上打了场 \(ABC\),快成屎了,最后竟然还加分了. 晚上回家洗了个澡,收拾收拾东西,凌晨2点就睡 ...

  4. EasyDesktop 浏览器书签管理从未如此简单

    作为一名软件开发从业人员, 每天80%的时间都在与浏览器打交道, 一半的时间在用浏览器开发调试, 另一半时间则是在互联网上搜寻各种知识和资源. 为此, 我的浏览器书签栏存储和很多非常棒的链接, 多到2 ...

  5. [rCore学习笔记 05]第0章作业题

    作业1 略. 作业2 C语言程序 gcc编译 gcc -o main main.c 编译报错 成功产生异常 main.c: In function 'main': main.c:5:26: warni ...

  6. Java工具库——Hutool的常用方法

    Hutool-All(或简称Hutool)是一个功能强大的Java编程工具库,旨在简化Java应用程序的开发. 它提供了大量的工具类和方法,涵盖了各种常见任务,包括字符串处理.日期时间操作.文件操作. ...

  7. python none类型

    一.python中的数据类型:数值类型.序列类型.散列类型. 1.数值类型:整数型(int).浮点数(float).布尔值(bool) 2.序列类型(有序的):序列类型数据的内部元素是有顺序的,可以通 ...

  8. 【NodeJS】操作MySQL

    1.在连接的数据库中准备测试操作的表: CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `name` ...

  9. 外形最漂亮的人形机器人——通用机器人Apollo,设计为可以在任何任务和环境中与人类进行协作

    视频地址: https://www.bilibili.com/video/BV11F4m1M7ph/

  10. 【转载】大模型时代的PDF解析工具

    本文来自博客园,作者:叶伟民,转载请注明原文链接:https://www.cnblogs.com/adalovelacer/p/18092208/pdf-tools-for-large-languag ...