Gin 框架之jwt 介绍与基本使用
一.JWT 介绍
Json web token
(JWT
), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519
)- 该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景
- JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源
- 也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密
二.JWT认证与session认证的区别
2.1 基于session认证流程图
服务器需要存储用户的token信息
2.2 基于jwt认证流程图
服务端不需要存储用户token, 都存在客户端
三. JWT 的构成
JWT就是一段字符串, 由三段信息构成, 三段信息文本使用.
(点) 拼接就构成了JWT字符串 :
eyJhbGciOiJIUzI1sNiIsIn.eyJzdWIiOiIxMjRG9OnRydWV9.TJVArHDcEfxjoYZgeFONFh7HgQ
- 第一部分我们称它为头部 :
header
- 第二部分我们称其为载荷 :
payload
(类似于飞机上承载的物品) - 第三部分是签证 :
signature
3.1 header : 头部
头部,JWT 的元数据,也就是描述这个 token 本身的数据,一个 JSON 对象。由两部分组成 :
- 声明类型(当前令牌名称)
- 声明加密算法
// 定义头部信息
header := map[string]interface{}{
"alg": "HS256", // 声明加密算法,可以根据需要修改
"typ": "JWT", // 声明类型
}
将头部使用base64
编码构成第一部分 (base64
编码方法, 该编码可以对称解码)
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
)
func main() {
// 定义头部信息
header := map[string]interface{}{
"alg": "HS256", // 声明加密算法,可以根据需要修改
"typ": "JWT", // 声明类型
}
// 将头部信息序列化为JSON格式字符串
headerBytes, err := json.Marshal(header)
if err != nil {
fmt.Println("JSON encoding error:", err)
return
}
headerStr := base64.RawURLEncoding.EncodeToString(headerBytes)
fmt.Println(headerStr)
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
}
3.2 payload : 负载
存放用户有效信息的地方,一个 JSON 对象, 这些有效信息包含三个部分:
- 标准中注册的声明
- 公共的声明
- 私有的声明
3.2.1 标准中注册的声明 (建议但不强制使用)
iss
: JWT签发者sub
: JWT所面向的用户aud
: 接收JWT的一方exp
: JWT的过期时间,这个过期时间必须要大于签发时间nbf
: 定义在什么时间之前,该JWT都是不可用的iat
: JWT的签发时间jti
: JWT的唯一身份标识,主要用来作为一次性token,从而回避时序攻击
3.2.2 公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息。但不建议添加敏感信息,因为该部分在客户端可解密。
3.2.3 私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
3.2.4 定义一个payload
除了上面的字段, 你自己也可以添加自己想要的字段, 需要注意的是:这些信息是不加密的, 所以最好不要存敏感信息
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
)
func main() {
// 定义Payload信息
payload := map[string]interface{}{
"sub": "1234567890", // 主题,表示该JWT的所有者
"name": "John Doe", // 自定义声明,可以根据需要添加其他声明
"iat": 1516239022, // 签发时间,表示JWT的签发时间,一般为当前时间的时间戳
"exp": 1516239022 + 3600, // 过期时间,表示JWT的过期时间,一般为签发时间加上有效期,以秒为单位
"roles": []string{"admin", "user"}, // 自定义声明,可以存储用户角色等信息
}
// 将Payload信息序列化为JSON格式字符串
payloadBytes, err := json.Marshal(payload)
if err != nil {
fmt.Println("JSON encoding error:", err)
return
}
payloadStr := base64.RawURLEncoding.EncodeToString(payloadBytes)
fmt.Println(payloadStr) // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
}
然后将其进行base64
加密,得到JWT
的第二部分。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
3.3 signatrue : 签名
signature
是根据 header
和 token
生成, 由三部分构成 :
- base64 编码后的 header
- base64 编码后的 payload
- secret : 秘钥 (只有服务端知道)
这个部分需要将base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了JWT的第三部分。
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
)
func main() {
// 定义头部信息
header := map[string]interface{}{
"alg": "HS256",
"typ": "JWT",
}
// 定义Payload信息
payload := map[string]interface{}{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516239022 + 3600,
"roles": []string{"admin", "user"},
}
// 将头部信息序列化为JSON格式字符串
headerBytes, err := json.Marshal(header)
if err != nil {
fmt.Println("JSON encoding error:", err)
return
}
headerStr := base64.RawURLEncoding.EncodeToString(headerBytes)
// 将Payload信息序列化为JSON格式字符串
payloadBytes, err := json.Marshal(payload)
if err != nil {
fmt.Println("JSON encoding error:", err)
return
}
payloadStr := base64.RawURLEncoding.EncodeToString(payloadBytes)
// 定义秘钥
secret := "your-secret-key" // 替换为实际的秘钥
// 生成签名
signature := generateSignature(headerStr, payloadStr, secret)
fmt.Println(signature) // C-94Wc6olGK6CEbkA9Xj0ogDQIFdPsEefZKCZrz_fvA
// 生成的签名字符串
}
func generateSignature(headerStr, payloadStr, secret string) string {
// 构造要签名的数据
dataToSign := headerStr + "." + payloadStr
// 使用HMAC-SHA256算法生成签名
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(dataToSign))
signatureBytes := h.Sum(nil)
// 对签名进行base64编码
signature := base64.RawURLEncoding.EncodeToString(signatureBytes)
return signature
}
3.4 得到 token
算出签名之后, 把 header、payload、signatrue 三部分使用 .
(点) 拼接成一个大字符串, 然后返回给客户端让其存储
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
)
func main() {
// 定义头部信息
header := map[string]interface{}{
"alg": "HS256",
"typ": "JWT",
}
// 定义Payload信息
payload := map[string]interface{}{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516239022 + 3600,
"roles": []string{"admin", "user"},
}
// 将头部信息序列化为JSON格式字符串
headerBytes, err := json.Marshal(header)
if err != nil {
fmt.Println("JSON encoding error:", err)
return
}
headerStr := base64.RawURLEncoding.EncodeToString(headerBytes)
// 将Payload信息序列化为JSON格式字符串
payloadBytes, err := json.Marshal(payload)
if err != nil {
fmt.Println("JSON encoding error:", err)
return
}
payloadStr := base64.RawURLEncoding.EncodeToString(payloadBytes)
// 将base64加密后的header和payload拼接起来
dataToSign := headerStr + "." + payloadStr
// 定义秘钥
secret := "your-secret-key" // 替换为实际的秘钥
// 生成签名
signature := generateSignature(dataToSign, secret)
// 最终的JWT字符串
jwtToken := dataToSign + "." + signature
fmt.Println(jwtToken)
// 最终生成的JWT字符串
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTYyNDI2MjIsImlhdCI6MTUxNjIzOTAyMiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZXMiOlsiYWRtaW4iLCJ1c2VyIl0sInN1YiI6IjEyMzQ1Njc4OTAifQ.C-94Wc6olGK6CEbkA9Xj0ogDQIFdPsEefZKCZrz_fvA
}
func generateSignature(dataToSign, secret string) string {
// 使用HMAC-SHA256算法生成签名
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(dataToSign))
signatureBytes := h.Sum(nil)
// 对签名进行base64编码
signature := base64.RawURLEncoding.EncodeToString(signatureBytes)
return signature
}
注意:secret
是保存在服务器端的,JWT的签发生成也是在服务器端的,secret
就是用来进行JWT的签发和JWT的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个 secret
,那就意味着客户端是可以自我签发JWT了。
四.base64 编码和解码的使用
首先 base64 是一种编码方式, 并非加密方式; 它跟语言无关, 任何语言都能使用 base64 编码&解码
4.1 base64 编码
// 定义一个信息字段
dic := map[string]interface{}{"id": 1, "name": "jarvis", "age": "male"}
// 将其序列化成json格式字符串
jsonBytes, err := json.Marshal(dic)
if err != nil {
fmt.Println("JSON encoding error:", err)
return
}
jsonStr := string(jsonBytes)
// 将json格式字符串encode再使用base64编码成一串Bytes格式编码
base64Str := base64.StdEncoding.EncodeToString([]byte(jsonStr))
fmt.Println([]byte(base64Str))
// [101 121 74 112 90 67 73 54 73 68 69 115 73 67 50 70 109 90 121 66 67 74 112 73 106 111 103 73 109 70 48 105 71 108 112 77 97 86 120 73 106 111 103 73 109 116 65 87 120 108 73 106 111 103 73 109 116 65 87 120 108 73 106 111 103 73 109 116 65 87 120 108 73 106 111 103 73 61]
fmt.Println(base64Str)
// eyJhZ2UiOiJtYWxlIiwiaWQiOjEsIm5hbWUiOiJqYXJ2aXMifQ==
4.2 base64 解码
// 替换为你的 base64 编码字符串
base64Str := "eyJhZ2UiOiJtYWxlIiwiaWQiOjEsIm5hbWUiOiJqYXJ2aXMifQ=="
// base64 解码
decodedBytes, err := base64.StdEncoding.DecodeString(base64Str)
if err != nil {
fmt.Println("Base64 decoding error:", err)
return
}
// JSON 反序列化
var dic map[string]interface{}
err = json.Unmarshal(decodedBytes, &dic)
if err != nil {
fmt.Println("JSON decoding error:", err)
return
}
fmt.Println(dic)
// map[age:male id:1 name:jarvis]
五.JWT 的本质原理
/*
1)jwt分三段式:头.体.签名 (head.payload.sgin)
2)头和体是可逆加密,让服务器可以反解出user对象;签名是不可逆加密,保证整个token的安全性的
3)头体签名三部分,都是采用json格式的字符串,进行加密,可逆加密一般采用base64算法,不可逆加密一般采用hash(md5)算法
4)头中的内容是基本信息:公司信息、项目组信息、token采用的加密方式信息
{
"company": "公司信息",
...
}
5)体中的内容是关键信息:用户主键、用户名、签发时客户端信息(设备号、地址)、过期时间
{
"user_id": 1,
...
}
6)签名中的内容时安全信息:头的加密结果 + 体的加密结果 + 服务器不对外公开的安全码 进行md5加密
{
"head": "头的加密字符串",
"payload": "体的加密字符串",
"secret_key": "安全码"
}
*/
5.1 签发
根据登录请求提交来的 账号 + 密码 + 设备信息 签发 token
- 用基本信息存储 json 字典, 采用 base64 编码得到头字符串
- 用关键信息存储 json 字典,采用 base64 编码得到体字符串
- 用头、体编码的字符串再加安全码信息(secret)存储 json 字典, 采用 header 中指定的算法加密得到签名字符串
- 最后形成的三段字符串用 . 拼接成
token
字符串返回给前台
5.2 校验
根据客户端带 token 的请求 反解出 user 对象
- 将 token 按
.
(点) 拆分为三段字符串, 第一段编码后的头字符串一般不需要做任何处理 - 第二段编码后的体字符串, 要解码出用户主键, 通过主键从 User 表中就能得到登录用户, 过期时间和设备信息都是安全信息, 确保 token 没过期, 且是同一设备来的
- 再将第一段 + 第二段 + 服务器安全码使用header中指定的不可逆算法加密, 与第三段 签名字符串进行对比校验, 通过后才能代表第二段校验得到的 user 对象就是合法的登录用户
5.3 jwt认证开发流程(重点)
用账号密码访问登录接口,登录接口逻辑中调用签发
token
算法,得到token
,返回给客户端,客户端自己存到cookies
中。校验
token
的算法应该写在中间件中,所有请求都会进行认证校验,所以请求带了token
,就会反解出用户信息。
六、Gin 框架中使用jwt
6.1 安装JWT库
使用Gin框架时,你可以选择一个适用于Go语言的JWT库。一个流行的选择是github.com/dgrijalva/jwt-go
库。
go get -u github.com/golang-jwt/jwt/v5
6.2 导入库
在你的Go代码中导入github.com/golang-jwt/jwt/v5
和github.com/gin-gonic/gin
。
import (
"github.com/golang-jwt/jwt/v5"
"github.com/gin-gonic/gin"
)
6.3 使用JWT 鉴权认证
6.3.1 JWT中间件开发
JWT中间件: 创建一个JWT中间件,它将用于保护需要身份验证的路由。
package middleware
import (
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"net/http"
"strings"
"webook/internal/web"
)
// LoginJWTMiddlewareBuilder JWT 登录校验
type LoginJWTMiddlewareBuilder struct {
paths []string
}
func NewLoginJWTMiddlewareBuilder() *LoginJWTMiddlewareBuilder {
return &LoginJWTMiddlewareBuilder{}
}
// IgnorePaths 忽略的路径
func (l *LoginJWTMiddlewareBuilder) IgnorePaths(path string) *LoginJWTMiddlewareBuilder {
l.paths = append(l.paths, path)
return l
}
func (l *LoginJWTMiddlewareBuilder) Build() gin.HandlerFunc {
// 用 Go 的方式编码解码
return func(ctx *gin.Context) {
// 不需要登录校验的
for _, path := range l.paths {
if ctx.Request.URL.Path == path {
return
}
}
// 用 JWT 来校验
tokenHeader := ctx.GetHeader("Authorization")
if tokenHeader == "" {
// 没登录
ctx.AbortWithStatus(http.StatusUnauthorized)
return
}
segs := strings.Split(tokenHeader, " ")
if len(segs) != 2 {
// 没登录,有人瞎搞
ctx.AbortWithStatus(http.StatusUnauthorized)
return
}
tokenStr := segs[1]
claims := &web.UserClaims{}
// ParseWithClaims 里面,一定要传入指针
// 这里的95osj3fUD7fo0mlYdDbncXz4VD2igvf0 代表的是签发的时候的key,并且key 要和签发的时候一样
token, err := jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (interface{}, error) {
return []byte("95osj3fUD7fo0mlYdDbncXz4VD2igvf0"), nil
})
if err != nil {
// 没登录
ctx.AbortWithStatus(http.StatusUnauthorized)
return
}
// token 验证不通过
if token == nil || !token.Valid {
// 没登录
ctx.AbortWithStatus(http.StatusUnauthorized)
return
}
// 将用户信息存储到上下文中
ctx.Set("claims", claims)
}
}
6.3.2 使用JWT中间件
使用JWT中间件: 在需要身份验证的路由上使用JWT中间件。
func initWebServer() *gin.Engine {
ser := gin.Default()
ser.Use(cors.New(cors.Config{
//AllowOrigins: []string{"*"},
//AllowMethods: []string{"POST", "GET"},
AllowHeaders: []string{"Content-Type", "Authorization"},
// 允许跨域访问的响应头,不加这个前端拿不到token响应头
ExposeHeaders: []string{"x-jwt-token"},
// 是否允许你带 cookie 之类的东西
AllowCredentials: true,
AllowOriginFunc: func(origin string) bool {
if strings.HasPrefix(origin, "http://localhost") {
// 你的开发环境
return true
}
return strings.Contains(origin, "http://你的公司域名.com")
},
MaxAge: 12 * time.Hour,
}))
// 注册登录校验中间件以及不要登录校验的路径
ser.Use(middleware.NewLoginJWTMiddlewareBuilder().
IgnorePaths("/users/signup").
IgnorePaths("/users/login").Build())
return ser
}
6.3.3 生成JWT token
生成JWT token: 在用户登录成功后,你可以生成JWT并将其返回给客户端。
// UserClaims 自定义的声明结构体并内嵌 jwt.StandardClaims
type UserClaims struct {
jwt.RegisteredClaims
// 声明你自己的要放进去 token 里面的数据
Uid int64
// 后续需要什么字段,就在这里添加
}
func (u *UserHandler) LoginJWT(ctx *gin.Context) {
type LoginReq struct {
Email string `json:"email"`
Password string `json:"password"`
}
var req LoginReq
if err := ctx.Bind(&req); err != nil {
return
}
user, err := u.svc.Login(ctx, req.Email, req.Password)
if err == service.ErrInvalidUserOrPassword {
ctx.String(http.StatusOK, "用户名或密码不对")
return
}
if err != nil {
ctx.String(http.StatusOK, "系统错误")
return
}
// 步骤2
// 在这里用 JWT 设置登录态
// 生成一个 JWT token
// 将用户信息存储到token中
claims := UserClaims{
Uid: user.Id,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
tokenStr, err := token.SignedString([]byte("95osj3fUD7fo0mlYdDbncXz4VD2igvf0"))
if err != nil {
ctx.String(http.StatusInternalServerError, "系统错误")
return
}
ctx.Header("x-jwt-token", tokenStr)
fmt.Println(user)
ctx.String(http.StatusOK, "登录成功")
return
}
6.3.4 访问路由签发token
我们通过接口调试工具访问路由127.0.0.1:8080/users/login
签发用户token
,header
中就会有X-Jwt-Token
这个字段以及生成的token 对应值。
6.3.4 通过 token 鉴权获取用户信息
在平时开发中,我们一般不会直接传user_id 过来,一般是通过token来获取用户信息,比如我们需要查询用户信息,之前我们已经将用户ID放入到token中了,直接通过c, _ := ctx.Get("claims")
来获取我们存放的用户信息,以下是具体代码;
func (u *UserHandler) ProfileJWT(ctx *gin.Context) {
c, _ := ctx.Get("claims")
// 你可以断定,必然有 claims
//if !ok {
// // 你可以考虑监控住这里
// ctx.String(http.StatusOK, "系统错误")
// return
//}
// ok 代表是不是 *UserClaims
claims, ok := c.(*UserClaims)
if !ok {
// 你可以考虑监控住这里
ctx.String(http.StatusOK, "系统错误")
return
}
fmt.Println("当前用户ID为:", claims.Uid)
ctx.String(http.StatusOK, "查询成功")
}
最后我们只需要访问路由:127.0.0.1:8080/users/profile
,在header
中加入token
即可。
Gin 框架之jwt 介绍与基本使用的更多相关文章
- Gin 框架 - 安装和路由配置
目录 概述 Gin 安装 路由配置 推荐阅读 概述 看下 Gin 框架的官方介绍: Gin 是一个用 Go (Golang) 编写的 web 框架. 它是一个类似于 martini 但拥有更好性能的 ...
- gin框架教程三:JWT的使用
JWT介绍 JWT (JSON Web Token) 是一种规范.这个规范允许我们使用JWT在用户和服务器之间安全传递信息. JWT的组成: jwt分3个部分,Header 头部.Payload 载荷 ...
- Gin框架介绍及使用
Gin是一个用Go语言编写的web框架.它是一个类似于martini但拥有更好性能的API框架, 由于使用了httprouter,速度提高了近40倍. 如果你是性能和高效的追求者, 你会爱上Gin. ...
- Gin框架介绍与使用
Gin // 初识 Gin框架 //下载(可能会下载不全.缺什么get什么即可) //go get -u -v github.com/gin-gonic/gin package main import ...
- 在gin框架中使用JWT
在gin框架中使用JWT JWT全称JSON Web Token是一种跨域认证解决方案,属于一个开放的标准,它规定了一种Token实现方式,目前多用于前后端分离项目和OAuth2.0业务场景下. 什么 ...
- 基于gin框架和jwt-go中间件实现小程序用户登陆和token验证
本文核心内容是利用jwt-go中间件来开发golang webapi用户登陆模块的token下发和验证,小程序登陆功能只是一个切入点,这套逻辑同样适用于其他客户端的登陆处理. 小程序登陆逻辑 小程序的 ...
- Go语言基础之20--web编程框架之Gin框架
一.Gin框架介绍 1.1 简介 A. 基于httprouter开发的web框架. http://github.com/julienschmidt/httprouter B. 提供Martini风格的 ...
- ABP开发框架前后端开发系列---(2)框架的初步介绍
在前面随笔<ABP开发框架前后端开发系列---(1)框架的总体介绍>大概介绍了这个ABP框架的主要特点,以及介绍了我对这框架的Web API应用优先的一些看法,本篇继续探讨ABP框架的初步 ...
- Go最火的Gin框架简单入门
Gin 介绍 Gin 是一个 Golang 写的 web 框架,具有高性能的优点,,基于 httprouter,它提供了类似martini但更好性能(路由性能约快40倍)的API服务.官方地址:htt ...
- 基于gin框架搭建的一个简单的web服务
刚把go编程基础知识学习完了,学习的时间很短,可能还有的没有完全吸收.不过还是在项目中发现知识,然后在去回顾已学的知识,现在利用gin这个web框架做一个简单的CRUD操作. 1.Go Web框架的技 ...
随机推荐
- SpringBoot 开发环境热部署
开发修改代码后,无需重启idea的服务. 1 模块中添加依赖 <dependency> <groupId>org.springframework.boot</groupI ...
- 聊聊损失函数1. 噪声鲁棒损失函数简析 & 代码实现
今天来聊聊非常规的损失函数.在常用的分类交叉熵,以及回归均方误差之外,针对训练样本可能存在的数据长尾,标签噪声,数据不均衡等问题,我们来聊聊适用不同场景有针对性的损失函数.第一章我们介绍,当标注标签存 ...
- 创建QUERY报表
一.SQ02创建信息集 该事务代码用于查询需要的表,及表之间的关联关系 首先设置查询区域,标准区域中所建立的信息集仅在当前客户端使用,全局区域中建立的信息集可以跨client 创建信息集 选择基础表关 ...
- AtCoder ABC 164 (D~E)
比赛链接:Here ABC水题, D - Multiple of 2019 (DP + 分析) 题意: 给定数字串S,计算有多少个子串 \(S[L,R]\) ,满足 \(S[L,R]\) 是 \(2 ...
- KB专题:区间DP专辑
自从多校后心憔悴啊,发现DP还是太水了,有一场的区间DP竟然不会做,咳,果然是赤裸裸的水军. 花了几天时间写了几道区间DP的题目,大部分都是水题,然后和以前的合并起来就是KB区间DP这个8 + 1道题 ...
- 传统单节点网站的 Serverless 上云
什么是函数?刚刚考完数学没多久的我,脑里立马想到的是自变量.因变量.函数值,也就是y=f(x).当然,在计算机里,函数function往往指的是一段被定义好的代码程序,我们可以通过传参调用这个定义好的 ...
- 函数计算 HTTP 触发器支持异步,解放双手搭建 Web 服务
作者| 阿里云Serverless技术专家 澈尔 当前阿里云函数计算支持两种类型的函数:事件函数和 HTTP 函数.其中 HTTP 函数结合 HTTP 触发器,能够支持用户直接通过 HTTP 请求利用 ...
- java生成word证明文件
开发中遇到系统自动出常用证明的需求,例如某在校大学生要求学校开具在校证明,这类证明数量大格式统一,使用代码如何实现. 一.设置word模板 下图中红色部分就是要动态变化的. 模板存放位置 二.依赖破p ...
- COM组件开发-关于在开发环境下COM组件的(来自 HRESULT 的异常:0x80080005 (CO_E_SERVER_EXEC_FAILURE)) 以及 在CLR语言下可能报错 未能加载文件或程序集“Interop.xxx 的问题
1.关于在开发环境下COM组件的(来自 HRESULT 的异常:0x80080005 (CO_E_SERVER_EXEC_FAILURE)) 开发环境下,COM组件注册的文件 不一定是你自己现在程序调 ...
- 浅谈 Docker 网络:单节点多容器
1.同网段多容器访问 这一节将对 Docker 多容器网络进行讨论,构建容器网络示意图如下: