Go的Gin框架拦截器实现登录认证结合JWT实现会话记录

news2024/12/24 20:20:42

JWT

jwt全称 Json web token,是一种认证和信息交流的工具。

授权:这是使用JWT最常见的场景。一旦用户登录,每个后续请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。

信息交流:JSON Web令牌是在各方之间安全传输信息的好方法。

https://jwt.io/

JWT包含三个部分,标题,有效载荷,签名。因此jwt的格式为Header.Payload.Signature。如下是一个JWT生成的token

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InhpYW94dSIsImV4cCI6MTY4MTUyNzcyNSwibmJmIjoxNjgxNTI3OTA1fQ.zOKqaUl2Z9BzuOIB9P0GmoHqAkHLp7O6yMy4lQ6FJ9U

标题通常由两部分组成:令牌的类型(JWT)和使用的签名算法(如HMAC SHA256或RSA)。

{
"alg": "HS256",
"typ": "JWT"
}

alg 表示签名的算法,默认是 HMAC SHA256(写成 HS256)
typ 表示这个 token 的类型,类型为 “JWT”
这个JSON被Base64Url编码以形成JWT的第一部分即生成header

荷载通常是用户信息和附加数据的声明

{
 "sub": "1234567890",
 "name": "xiaoxu",
 "admin": true
}

对有效载荷进行Base64Url编码,以形成JSON Web令牌的第二部分。

签名部分,主要是对 Header 和 payload 的签名,防止数据被窜改。签名还需要一个密钥secret,该密钥仅保存在服务器,签名的算法就是 Header 中指定的签名算法。

在这里插入图片描述

JWT工作模式

在身份验证中,当用户使用其凭据成功登录时,将返回JSON Web Token令牌。每当用户想要访问受保护的路由或资源时,用户代理应该发送JWT,通常在Authorization头中使用Bearer模式。

Authorization: Bearer <token>

如果通过HTTP头发送JWT令牌,则应尽量防止它们变得太大。有些服务器不接受超过8 KB的头文件。

JWT认证的一般流程为:

  1. 登录认证返回token
  2. 每次访问次元携带token
  3. token过期或失效重新登录认证

JWT在服务端返回token的一般流程为:

  1. 对Header实现base64编码
  2. 对Payload实现basse64编码
  3. 使用签名并使用加密算法对Header和Payload实现加密,返回Signature,即token。

JWT优势

基于服务器的身份认证,基于session+cookie的会话保持技术,session认证通过后需要将用户的session数据保存在内存中,随着认证用户的增加,内存开销会大;CORS多个终端访问同一数据时会出现禁止访问的情况;用户容易受到CSRF攻击;不利于集群部署,多个集群的session内存共享实现复杂;不利于反向代理,代理服务器也需要session共享。

基于Token的身份认证,基于Token的身份认证是无状态的,服务器直接从token中解析有用信息,不会存储任何用户信息,token文件较小利于网络传输。

OAuth2是一种授权和认证框架 ,JWT是一种认证协议。

JWT规则生成Token

  1. 下载并引入jwt-go包并引入JWT
//下载依赖
go get -u github.com/dgrijalva/jwt-go

//引入依赖
import "github.com/dgrijalva/jwt-go"

  1. 构造Payload数据格式,配置有用信息
//Pyaload
type MyClaim struct {
	Username string `json:"username"`
	jwt.StandardClaims
}

jwt.StandardClaims是jwt包下的结构体,定义了Payload的一些标准预定义信息,用户自定义其余信息需要通过继承该类实现,即通过结构体嵌套实现。

type StandardClaims struct {
 Audience  string `json:"aud,omitempty"`(受众,即接受 JWT 的一方)
 ExpiresAt int64  `json:"exp,omitempty"`(所签发的JWT的过期时间)
 Id        string `json:"jti,omitempty"`(JWT的Id)
 IssuedAt  int64  `json:"iat,omitempty"`(签发时间)
 Issuer    string `json:"iss,omitempty"`(JWT的签发者)
 NotBefore int64  `json:"nbf,omitempty"`(JWT的生效时间)
 Subject   string `json:"sub,omitempty"`(主题)
}

在自定义负载仅仅添加了Username一项。

  1. 依据JWT的Header和Payload和加密算法生成JSON Web Token
//定义签名
// secret签名
var mySignatureSecret []byte = []byte("!@#qwe")



//实例化负载payload
c := MyClaim{
	Username: "xiaoxu",
	StandardClaims: jwt.StandardClaims{
		NotBefore: time.Now().Unix() + 60,  //JWT的生效时间
		ExpiresAt: time.Now().Unix() - 120, //签发JWT的过期时间
	},
}


//生成Header
//Header一般都是如下的json使用默认即可不用专门实例化,因此直接默认即可
/*
	{
		"alg": "HS256",
		"typ": "JWT"
	}
*/


//生成token

//返回未加密signature
sensitiveToken := jwt.NewWithClaims(jwt.SigningMethodHS256, c)

//利用secret签名对token加密
signature, err := sensitiveToken.SignedString(mySignatureKey)

通过上述步骤得到signature即最终的token。所有代码如下:

package main

import (
	"fmt"
	"github.com/dgrijalva/jwt-go"
	"time"
)

//Pyaload

type MyClaim struct {
	Username string `json:"username"`
	jwt.StandardClaims
}

// secret签名
var mySignatureKey []byte = []byte("!@#qwe")

func main() {
	/*
		engine := gin.Default()
		engine.GET("/", func(context *gin.Context) {
			context.String(200, "Hello World")
		})
		engine.Run("127.0.0.1:80")
	*/

	//实例化负载payload
	c := MyClaim{
		Username: "xiaoxu",
		StandardClaims: jwt.StandardClaims{
			NotBefore: time.Now().Unix() + 60,  //JWT的生效时间
			ExpiresAt: time.Now().Unix() - 120, //签发JWT的过期时间
		},
	}

	//Header就是默认也是不用专门实例化
	/*
		{
			"alg": "HS256",
			"typ": "JWT"
		}
	*/

	//返回未加密signature
	sensitiveToken := jwt.NewWithClaims(jwt.SigningMethodHS256, c)

	//利用secret签名对token加密
	signature, err := sensitiveToken.SignedString(mySignatureKey)
	if err != nil {
		panic(err)
	}
	fmt.Println(signature)
}

生成token

在这里插入图片描述

解密Token

//解密Token
token1, _ := jwt.ParseWithClaims(signature, &MyClaim{}, func(token *jwt.Token) (interface{}, error) {
	return mySignatureKey, nil
})

fmt.Println(token1)
fmt.Println(token1.Claims)
fmt.Println(token1.Claims.(*MyClaim).Username)

解密后得到jwt.Token对象,从该对象可以获取Header,Payload,Signature(claims)等信息。

在这里插入图片描述
jwt.ParseWithClaims()方法用于解密Token,第一个参数为生成的token,第二个参数为自定义Payload的结构体类型,第三个参数为一个方法返回签名。

解密后得到的jwt.Token对象一般包含以下几个变量。
在这里插入图片描述

成员变量描述
Claimsjwt表头和负载加密生成的不完整Token,此时还未对签名加密,也就是回到之前的sensitiveToken := jwt.NewWithClaims(jwt.SigningMethodHS256, c)此步骤。一般通过断言转化为自定义Payload结构体
SignedString(key inteface{})该方法是传入签名对签名在加密生成完整的token
Header返回表头
Method返回加密的算法
Valid验证token是否过期

另外需要注意的是,定义的负载必须是公共的即结构体名和成员名首字母都必须大写,不然无法将为加密的*TokenClaims类型断言成自定义的结构体类型。如下,如果将字段改为小写在同包下的测试包都无法生成和解析Token。

在这里插入图片描述

拦截器

Go的拦截器,可以在方法执行前后先执行拦截器的方法,通过拦截器放行或拦截方法。在Gin框架中,路由方法都是若干个,因此在执行路由后的逻辑时,可以将任意个拦截器方法当作参数传递给从而实现对无逻辑的放行和控制。

func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes

如上所示,gin.Default().GET()方法中,第一个参数为路由地址,其后为若干拦截器方法,对于拦截器方法type HandlerFunc func(*Context)其实就是实现了gin.context的方法的重命名函数。

因此任意以gin.context为参数的方法均为拦截器方法。

//定义权限认证中间件

func Certification() gin.HandlerFunc {
	return func(context *gin.Context) {
		context.Set("username", "xiaoxu")
		if context.PostForm("username") != "xiaoxu" {
			context.String(200, "用户名错误!")
			context.Abort()
		} else {
			context.Next()
		}
	}
}
//gin的路由调用拦截器

import "github.com/gin-gonic/gin"

func main() {

	engine := gin.Default()
	engine.GET("/", Certification(), func(context *gin.Context) {
		context.String(200, "Hello World")
	})

	engine.GET("/index", Certification(), func(context *gin.Context) {
		context.String(200, "welcome index !")
	})

	engine.GET("/test", Certification(), func(context *gin.Context) {
		context.String(200, "welcome test")
	})
	engine.Run("127.0.0.1:80")
}

当请求地址为携带正确额用户名和密码时,就会出现 ”用户名错误“ 的字样!

在这里插入图片描述

当携带正确用户名时据可以访问任意路径

在这里插入图片描述

注意表单类型为form-datatext类型,而x-www-form-urlencoded时key-value类型。

上面案例实现了拦截器对资源的拦截,接下来实现数据的查询模拟用户登录,使用sql数据库,orm框架为gorm。

//下载mysql驱动
go get -u gorm.io/driver/mysql

//gorm框架
go get -u gorm.io/gorm

//引入依赖
import (
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)
//构建如下的数据库表

mysql> select * from user;
+----+---------+----------+----------+
| id | user    | password | role     |
+----+---------+----------+----------+
|  1 | xiaoxu  | 123      | admin    |
|  2 | zhansan | 123      | personal |
|  3 | lisi    | 123      | role     |
+----+---------+----------+----------+
3 rows in set (0.01 sec)

//结构体映射数据库表
//定义数据表映射结构体

type User struct {
	Id       int    `json:"id"`
	User     string `json:"user"`
	Password string `json:"password"`
	Role     string `json:"role"`
}
//数据库驱动程序返回数据库操作对象

//数据库驱动
func ConnMysql() *gorm.DB {
	datasource := "root:root@tcp(127.0.0.1:3306)/user?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(datasource), &gorm.Config{})
	if err != nil {
		fmt.Println("error connect mysql", err)
	}
	return db
}
//主函数映入数据库对象(略)
import "xxx/../db"


//重构拦截器,添加查询数据库操作
func Certification() gin.HandlerFunc {
	return func(context *gin.Context) {
		//fmt.Println(context.PostForm("username"))
		var user db.User
		db.ConnMysql().Where("user = ?", context.PostForm("username")).Find(&user)
		//fmt.Println("user----------", user)

		//判断用户是否存在
		if user.User == "" {
			context.String(200, "用户名不存在!")
			context.Abort()
			//判断用户密码
		} else if user.Password != context.PostForm("password") {
			context.String(200, "用户名或密码错误!")
			context.Abort()
		} else {
			//fmt.Println("password-----------", user.Password)
			context.Next()
		}

	}
}

效果如下

请添加图片描述

Gin结合JWT实现认证

上一节已经实现了登录认证,那么又遇到一个新的问题,如果同一时间间隔内出现了多个用户登录系统,如A用户在CSDN上登录了自己的账号发布了自己的文章,在同一时间间隔内登录了B,C两个用户,那么发布的文章如何标识为A用户的呢?

这就需要用到会话技术了,在之前的系统中是用的Session技术,服务端会话技术,将用户信息记录在服务器端,每个用户都自己的独立的状态。

在session中存储用户的信息,如A用户的用户名,那么A用户发布文章时将A用户的用户名和文章一起存储到数据库,就可以区分该文章是A用户的了。但是随着用户数量的增加,同时时间间隔内突增的访问用户会加大服务的负载。

因此出现了JWT的新技术,用于用户会话额记录,在第一节介绍中JWT完美的取代了SEESION,成为主流的认证和会话记录技术。

拦截器章节实现了用户登录,本章节将介绍如何利用JWT实现会话记录。

//下载引入go-jwt依赖

//下载依赖
go get -u github.com/dgrijalva/jwt-go

//引入依赖
import "github.com/dgrijalva/jwt-go"

项目结构体如下

在这里插入图片描述

//JWTConfig包的源码

import (
	"github.com/dgrijalva/jwt-go"
	"time"
)

// MyPayload 定义负载继承jwt的标准负载
type MyPayload struct {
	Username string
	jwt.StandardClaims
}

// 定义secret签名
var signatureKey []byte = []byte("!@#qwe")

// MakeUserToken 生成加密token
func MakeUserToken(user string) string {
	//传入用户信息生成负载实例
	payload := MyPayload{
		Username: user,
		StandardClaims: jwt.StandardClaims{
			NotBefore: time.Now().Unix() + 10,
			ExpiresAt: time.Now().Unix() - 10,
		},
	}

	//生成加密Signature
	token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, payload).SignedString(signatureKey)
	if err != nil {
		panic(err)
	}
	return token
}

// 解密token
func ParserUserToken(token string) string {
	//解密后jwt.Token对象,从该对象可以获取Header,Payload,Signature(claims)等信息
	unsafeToken, _ := jwt.ParseWithClaims(token, &MyPayload{}, func(token *jwt.Token) (interface{}, error) {
		return signatureKey, nil
	})
	user := unsafeToken.Claims.(*MyPayload).Username
	return user
}

引入之后应该可以使用JWTConfig包下的生成token和解析token的方法。
在这里插入图片描述

由于会话记录也是全局的,所以在全局拦截器引入jwt,将jwt认证嵌入并实现登陆后的会话记录。

实现代码如下:

  1. jwt生成token与解析token的代码
package jwtconfig

import (
	"github.com/dgrijalva/jwt-go"
	"time"
)

// MyPayload 定义负载继承jwt的标准负载
type MyPayload struct {
	Username string
	jwt.StandardClaims
}

// 定义secret签名
var signatureKey []byte = []byte("!@#qwe")

// MakeUserToken 生成加密token
func MakeUserToken(user string) string {
	//传入用户信息生成负载实例
	payload := MyPayload{
		Username: user,
		StandardClaims: jwt.StandardClaims{
			NotBefore: time.Now().Unix() + 10,
			ExpiresAt: time.Now().Unix() - 10,
		},
	}

	//生成加密Signature
	token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, payload).SignedString(signatureKey)
	if err != nil {
		panic(err)
	}
	return token
}

// 解密token
func ParserUserToken(token string) (*MyPayload, error) {
	//解密后jwt.Token对象,从该对象可以获取Header,Payload,Signature(claims)等信息
	unsafeToken, err1 := jwt.ParseWithClaims(token, &MyPayload{}, func(token *jwt.Token) (interface{}, error) {
		return signatureKey, nil
	})

	//将负载转化为结构体
	claims, ok := unsafeToken.Claims.(*MyPayload)

	if ok && unsafeToken.Valid {
		return claims, nil
	} else {
		return claims, err1
	}

	/*
		//验证token是否有效
		if unsafeToken.Valid {
			//错误判断并返回错误信息
			if err1 != nil {
				return "未携带有效token", unsafeToken.Claims
			} else if ve, ok := err1.(*jwt.ValidationError); ok {
				if ve.Errors&jwt.ValidationErrorMalformed != 0 {
					return "无效token", nil
				} else if ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 {
					return "token已过期", nil
				} else {
					return "", nil
				}
			}
		}
	*/

}
  1. 路由jwt验证代码
package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"go-jwt/db"
	"go-jwt/jwtconfig"
)

func main() {

	engine := gin.Default()
	engine.POST("/login", Certification(), func(context *gin.Context) {
		user := context.PostForm("user")
		fmt.Println(jwtconfig.MakeUserToken(user))
		context.String(200, "welcome register")
	})
	engine.GET("/", JWTHandler(), func(context *gin.Context) {
		context.String(200, "Hello World")
	})

	engine.GET("/test1", JWTHandler(), func(context *gin.Context) {
		context.String(200, "welcome index !")
	})

	engine.GET("/test2", JWTHandler(), func(context *gin.Context) {
		context.String(200, "welcome test")
	})
	engine.Run("127.0.0.1:80")
}

//定义权限认证中间件

func Certification() gin.HandlerFunc {
	return func(context *gin.Context) {
		//fmt.Println(context.PostForm("username"))
		var user db.User
		db.ConnMysql().Where("user = ?", context.PostForm("username")).Find(&user)
		//fmt.Println("user----------", user)
		//判断用户是否存在
		if user.User == "" {
			context.String(500, "用户名不存在!")
			context.Abort()
			//判断用户密码
		} else if user.Password != context.PostForm("password") {
			context.String(500, "用户名或密码错误!")
			context.Abort()
		} else {
			//fmt.Println("password-----------", user.Password)
			context.Next()
		}

	}
}

//jwt拦截器

func JWTHandler() gin.HandlerFunc {
	return func(context *gin.Context) {

		//引入jwt实现登录后的会话记录,登录会话发生登录完成之后
		//header获取token
		token := context.Request.Header.Get("token")
		if token == "" {
			context.String(302, "请求未携带token无法访问!")
			context.Abort()
		}
		//解析token
		claims, err := jwtconfig.ParserUserToken(token)
		if claims == nil || err != nil {
			context.String(401, "未携带有效token或已过期")
			context.Abort()
		} else {
			//context.Set("user", claims.Username)
			context.Next()

		}
	}
}

依据JWT规则加密解密Token不在赘述,这里主要阐明JWT会话逻辑,首先定义了JWTHandler()jwt拦截器,主要实现步骤是,从请求头获取Token(当然也可以放在http协议的其他位置),然后token判空,解密Token判断token是否过期是否有效,有效就放行。(这里只实现了简单的验证,官网又详细的验证)

需要注意的是登录页面进行用户名身份的验证,而其他路由实现jwt会话的验证。

案例演示如下:

请添加图片描述

如下是控制太的返回
在这里插入图片描述

未验证token前都是返回302错误状态码,http header携带token后返回200状态码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/439349.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

3.redis-事务

01-Redis事务概述 概述 Redis事务是一个单独的隔离操作&#xff1a;事务中的所有命令都会序列化放到一个队列中按顺序地执行。事务 在执行的过程中&#xff0c;不会被其他客户端发送来的命令请求所打断。 不支持ACID ①atomicity, 原子性, redis事务中的指令执行失败, 不影响后…

记一次 腾讯会议 的意外崩溃分析

一&#xff1a;背景 1. 讲故事 前段时间在用 腾讯会议 直播的时候&#xff0c;居然意外崩溃了&#xff0c;还好不是在训练营上课&#xff0c;不然又得重录了&#xff0c;崩完之后发现 腾讯会议 的 bugreport 组件会自动生成一个 minidump&#xff0c;截图如下&#xff1a; 作…

tcpdump 抓包

一、Tcpdump抓包 抓取端口为2008的数据包 抓包文件内容 抓取到的内容保存在文件中&#xff0c;可以通过wireshark分析 二、tcpdump的一些命令 tcpdump和ethereal可以用来获取和分析网络通讯活动&#xff0c;他们都是使用libpcap库来捕获网络封包的。 ​在混杂模式下他们可以监…

Oracle-主备切换问题(BUG-31747989)

背景: 用户在Oracle Rac 19.6版本通过switchover方式进行主备切换&#xff0c;在备切主完成之后&#xff0c;进行open的过程中&#xff0c;状态长时间无法完成疑似hang住。 问题: ​ Oracle Rac 19.6版本通过switchover方式进行主备切换,切换完成之后进行open&#xf…

100个开源手写人工智能算法(持续更新中)

你是否曾经对开源框架中算法的运行原理感到好奇&#xff1f;又是否曾经想过使用自己写的算法来进行机器学习&#xff1f; 现在&#xff0c;向您推荐一款基于 Python 语言的开源手写机器学习算法项目&#xff01; https://github.com/yuluxingchen/AI/ 这个项目中预计将包含了…

【10 浅学jsp】

浅学 jsp 一、jsp1. jsp介绍2. JSP执行流程3. JSP的本质还是Servlet4. JSP语法4.1 注释4.2 代码块4.3JSP表达式4.4JSP声明 5. JSP指令page 指令include 包含指令包含 其他页面taglib 引入外部标签库 6. JSP细节6.1 pageContext 页面域对象 7. Servlet四大域对象小结8. MVC模型 …

利用MFC实现一个托盘功能

文章目录 1.将主窗口隐藏起来&#xff0c;并移除任务栏图标显示2. 制作系统托盘3. 右键托盘得到信息4. 选择信息栏触发事件5. 添加开机自启动与关闭开机自启动OnSetPowerBoot与OnCancelPowerBoot 右键点击托盘&#xff0c;弹出如下的图标 1.将主窗口隐藏起来&#xff0c;并移除…

Qt开发技术:Q3D图表开发笔记(三):Q3DSurface三维曲面图介绍、Demo以及代码详解

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/130264470 各位读者&#xff0c;知识无穷而人力有穷&#xff0c;要么改需求&#xff0c;要么找专业人士&#xff0c;要么自己研究 红胖子网络科技博…

怎么批量把heic格式转化jpg,3招快速解决

怎么批量把heic格式转化jpg&#xff1f;heic是一种新型的图像文件格式&#xff0c;是苹果独家搞出来的一个图片格式&#xff0c;它小巧玲珑&#xff0c;而且图像质量超好&#xff0c;专门给iOS11系统用户用的。这种格式比老JPEG更厉害&#xff0c;不仅图片质量好&#xff0c;而…

如何减少电脑内存占用或优化内存?

内存(Memory)是计算机一个重要的组成部件&#xff0c;也称为内存储器或主存储器。它可以暂时存放CPU中运算的数据&#xff0c;以及与硬盘等外部存储器交换的数据&#xff0c;是CPU和硬盘之间的桥梁。若电脑内存占用过高&#xff0c;这会影响到电脑运行的速度&#xff0c;那该如…

Redis的哈希槽分区

目录 1. 一致性算法分区的缺点2. 哈希槽分区3. Redis为什么是16384个槽 1. 一致性算法分区的缺点 可以参考一致性哈希算法分区这篇文章 2. 哈希槽分区 Redis集群中内置了16384个哈希槽。redis会根据服务器节点数量大致均等的将哈希槽映射到不同的节点 当写入一条数据&#x…

Java版本的工程项目管理系统源代码之工程项目管理系统面临的挑战

​ ​工程项目管理系统是指从事工程项目管理的企业&#xff08;以下简称工程项目管理企业&#xff09;受业主委托&#xff0c;按照合同约定&#xff0c;代表业主对工程项目的组织实施进行全过程或若干阶段的管理和服务。 ​系统定义 工程项目管理企业不直接与该工程项目的总承包…

UE4/5多人游戏详解(六、多人游戏插件的菜单,创建会话设置和加入)

目录 简单的菜单 创建新的c类&#xff1a; 这里可能出现的报错&#xff1a; 菜单设置&#xff1a; 代码&#xff1a; UI创建&#xff1a; C类中创建按钮的指针&#xff1a; 子系统创建 创建会话函数&#xff1a; 创建会话后前往大厅&#xff1a; 重载函数 变量添加…

Java 线程

线程&#xff1a;线程是进程的组成部分&#xff0c;一个进程可以拥有多个线程&#xff0c;而一个线程必须拥有一个父进程。线程可以拥有自己的堆栈&#xff0c;自己的程序计数器和自己的局部变量&#xff0c;但不能拥有系统资源。它与父进程的其他线程共享该进程的所有资源。 …

PowerShell install Docker+docker-compoer

docker 前言 Docker 是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中&#xff0c;然后发布到任何流行的 Linux或Windows 机器上&#xff0c;也可以实现虚拟化。容器是完全使用沙箱机制&#xff0c;相互之间不会有任何接口。 …

Linux搭建SVN服务器详细教程

前言 本文讲解 Linux 系统下如何搭建 SVN 服务器&#xff0c;详细说明各配置项的功能&#xff0c;最终实现可管控多个项目的复杂配置。 SVN 是 subversion 的缩写&#xff0c;是一个开放源代码的版本控制系统&#xff0c;通过采用分支管理系统的高效管理&#xff0c;实现最终集…

【转】使用Midjourney绘制小漫画

原帖地址:【Midjourney教程】设计麻瓜也能15分钟一篇小漫画 Midjourney能帮我画漫画,话不多说,下方成品图 Part 1 你想画什么 画漫画当然要有故事情节,你总得确定,你要画个啥?也就是专业人士说的画面分镜,当然咱们是“野狐禅”,就不扯的太细,太细我也不会… 由于只…

软考中级软件评测师备考攻略

软件评测师属于软考中级&#xff0c;考试虽然没有软考高级难度大&#xff0c;但是会比软考初级要难&#xff0c;所以想要通过软件评测师考试还是需要花时间去用心备考的。 一、软件评测师职业前景&#xff1a; 随着互联网技术的不断发展&#xff0c;软件评测师的市场需求也会…

【CocosCreator入门】CocosCreator组件 | Canvas(画布)组件

Cocos Creator 是一款流行的游戏开发引擎&#xff0c;具有丰富的组件和工具&#xff0c;其中的Canvas能够将游戏物体渲染到屏幕上。 目录 一、组件介绍 二、渲染模式 三、组件属性 四、组件使用 五、脚本示例 一、组件介绍 Canvas组件是Cocos Creator中重要的组件之一。在…

【Socket】之TCP数据报套接字

1. 介绍下API 1.1 ServerSocket API 这是创建TCP服务端Socket的API。 构造方法方法说明ServerSocket(int port)创建一个服务端流套接字Socket&#xff0c;并绑定到指定端口 普通方法方法说明ServerSocket.accept()开始监听指定端口&#xff08;创建时绑定的端口&#xff09…