xyxsw
文章34
标签3
分类0
DN11 使用 EBGP 重分发 IBGP OSPF(同as内部peer)

DN11 使用 EBGP 重分发 IBGP OSPF(同as内部peer)

DN11 使用 EBGP 重分发 IBGP OSPF(同as内部peer)

连接 wg peer

1.本地机器

# wg_aws.conf
[Interface]
# 你的私钥
PrivateKey = <PRIVATEKAY>
# 你开的端口号(注意防火墙)
ListenPort = <PORT>
PostUp = /sbin/ip addr add dev %i <your ip> peer <peer ip>
# 不创建路由(由路由协议接管)
Table = off

[Peer]
# 你对端的ip和端口(外网机器)
Endpoint = <ENDPOINT>:<PORT>
# 你对端的公钥(外网机器)
PublicKey = <PUBLICKEY> 
AllowedIPs = 0.0.0.0/0

2.外网机器

# wg_ts_home.conf
[Interface]
# 你的私钥
PrivateKey = <PRIVATEKAY>
# 你开的端口号(注意防火墙)
ListenPort = <PORT>
PostUp = /sbin/ip addr add dev %i <your ip> peer <peer ip>
# 不创建路由(由路由协议接管)
Table = off
# wg 默认的 MTU 是 1420 (这里主要排除外网机器 MTU 异常的问题,洞里打洞不要配这个)
MTU = 1420

[Peer]
# 你对端的ip和端口(本地机器)
Endpoint = <ENDPOINT>:<PORT>
# 你对端的公钥(本地机器)
PublicKey = <PUBLICKEY> 
AllowedIPs = 0.0.0.0/0

bird配置

创建

/etc/bird.conf
/etc/bird/ospf.conf
/etc/bird/ibgp.conf
/etc/bird/bgp.conf

这四个文件

内网 openwrt 和外网机器上几乎需要相同的这四个文件(唯一不同的是 bgp neighbor 那里)

文中的 172.16.3.254、172.16.3.91 改为你本机(你的本地机器或者外网机器)的隧道ip
文中的 4211112243 改为你的 asn (别整出俩 as 号就行)

/etc/bird.conf

log syslog all;
debug protocols all;

router id 172.16.3.254;

ipv4 table BGP_table;
ipv4 table OSPF_table;
ipv4 table IBGP_table;

protocol device{

}

protocol kernel{
    ipv4 {
        export all;
        import none;
    };
}

include "/etc/bird/ospf.conf";
include "/etc/bird/ibgp.conf";
include "/etc/bird/bgp.conf";

protocol pipe PIPE_OSPF_BGP{
    table BGP_table;
    peer table OSPF_table;
    import all;
    export none;
}

protocol pipe PIPE_BGP_MASTER4{
    table master4;
    peer table BGP_table;
    import filter {
        if source = RTS_STATIC then reject;
        krt_prefsrc = 172.16.3.254;
        accept;
    };
    export none;
}

protocol pipe PIPE_IBGP_BGP {
    table BGP_table;
    peer table IBGP_table;
    import filter {
        if source = RTS_OSPF then reject;
        accept;
    };
    export filter {
        if source = RTS_STATIC then reject;
        accept;
    };
}

# 宣告 172.16.3.0/24 段
protocol static {
    ipv4 {
        table BGP_table;
        import all;
        export none;
    };

    # 只是为了让BGP的路由表里出现这条路由,不要担心 reject
    # 这个动作其实无所谓,reject 这个动作并不会被发到其他 AS
    # 后续将在导出到 master4 的时候删除这条路由,本地也不会有这一条
    # 请修改为你自己要宣告的段
    route 172.16.3.0/24 reject;
}

/etc/bird/ibgp.conf

template bgp ibgp_peers {
    local as 4211112243;
    source address 172.16.3.254;
    multihop;
    ipv4 {
        next hop self;
        igp table OSPF_table;
        table IBGP_table;
        import all;
        export all;
    };
}

protocol bgp ibgp_aws from ibgp_peers {
    neighbor 172.16.3.91 internal;
}

bgp.conf

template bgp BGP_peers {
    local 172.16.3.254 as 4211112243;

    ipv4 {
        table BGP_table;
        import all;
        export filter {
            if source ~ [RTS_STATIC, RTS_BGP, RTS_OSPF] then accept;
            reject;
        };
    };
}

# collector 配置
protocol bgp collect_self {
    local as 4211112243;
    multihop;
    neighbor 172.16.255.1 as 4211110101;
    ipv4 {
        add paths tx;
        table BGP_table;
        import none;
        export filter {
            if source ~ [RTS_BGP,RTS_STATIC] then accept;
        };
    };
}

# 如下的 neighbor 配置应按照 wg 实际链接数量为准
# 从模板定义一个BGP邻居
# protocol bgp protocol 名称 from 模板名称
protocol bgp baimeow from BGP_peers {
   neighbor 172.16.4.254%baimeow as 4220084444;
}

# 如下为 如果你有物理链路的 BGP 配置 (没有的别抄)
# protocol bgp Meva_phy from BGP_peers {
#     source address 172.16.3.1;
#     neighbor 172.16.3.97%'br-lan' as 4211111024;
# 
#     ipv4 {
#     import filter {
#             if bgp_path ~ [= 4211111024 =] then bgp_local_pref = 120;
#             bgp_next_hop = 172.16.3.97;
#             accept;
#         };
#     };
# }

ospf.conf

protocol ospf v2 ospf_aws {
        ipv4 {
                table OSPF_table;
                export all;
                import all;
        };
        area 0.0.0.0 {
                interface "wg_aws" {
                        cost 20;
                        type ptp;
                };
                interface "lo" {
                        type bcast;
                };
        };
}
go-zero api文件生成项目框架 | 青训营笔记

go-zero api文件生成项目框架 | 青训营笔记

go-zero api文件生成项目框架

数据结构

.api文件是go-zero自创的文件格式,和protobuf的语法有很大不同,但好在不难理解。具体语法介绍可看官网文档:go-zero.dev/docs/tasks/…

先编写接口定义,然后直接一次性使用 goctl 生成代码。

api文件支持将一个数据结构嵌入另一个结构中,便于编写统一的响应格式。

首先定义空请求结构和基础的响应结构:

syntax = "v1"  
  
type Empty {  
}  
  
type BasicResponse {  
    StatusCode int32 `json:"status_code"`  
    StatusMsg string `json:"status_msg"`  
}

type关键字也支持代码块,只需要用type()包裹,就可以省去关键词。下面定义了注册和登录的请求和响应结构。

type (  
    RegisterRequest struct {  
        Username string `form:"username"`  
        Password string `form:"password"`  
    }  

    RegisterResponse struct {  
        BasicResponse  
        UserId int64 `json:"user_id"`   
    }  
)  
  
type (  
    LoginRequest struct {  
        Username string `form:"username"`  
        Password string `form:"password"`  
    }  

    LoginResponse struct {  
        BasicResponse  
        UserId int64 `json:"user_id"`  
    }  
)

最后还要定义一个获取用户信息的接口。因为本文不涉及创作、社交方面的业务逻辑实现,所以User结构其实没什么可以返回的信息。之后将其嵌入响应类型的user字段中:

type (  
    User {  
        Id int64 `json:"id"`  
        Name string `json:"name"`  
    }  
    
    GetUserInfoRequest struct {  
        UserId int64 `form:"user_id"`   
    }  

    GetUserInfoResponse struct {  
        BasicResponse  
        User User `json:"user"`  
    }  
)

定义接口路由

把所有api路由配置好

@server(  
    group: app  
)  
service app {  
    @handler Ping  
    get /ping (Empty) returns (BasicResponse)  
}  
  
@server(  
    group: user  
    prefix: /douyin/user  
)  
service app {  
    @handler Register  
    post /register (RegisterRequest) returns (RegisterResponse)  
  
    @handler Login  
    post /login (LoginRequest) returns (LoginResponse)  
  
    @handler GetUserInfo  
    get / (GetUserInfoRequest) returns (GetUserInfoResponse)  
}

以下几点需要注意:

  1. 如果service代码块有多个,要求后面的名称(即“app”)相同
  2. 形如装饰器的@server代码块可以做代码分组、路由前缀、鉴权、中间件等多种配置。
    • 这里把路由分成app和user两组,之后生成时会分组放在文件夹里
    • 给user组设置了/douyin/user的路由器前缀,就不用重复写了
    • 鉴权和中间件配置下文会提到
  3. 每个接口需要一个handler,其实就是给函数起名
  4. 接口定义的格式是 <HTTP方法> <子路径> (<请求数据结构>) returns (<响应数据结构>)

创建项目模板

这次不是新建示例项目,而是根据现有的api声明创建

安装 goctl

https://go-zero.dev/docs/tasks/installation/goctl

$ GO111MODULE=on go install github.com/zeromicro/go-zero/tools/goctl@latest

安装 protoc

https://go-zero.dev/docs/tasks/installation/protoc

$ goctl env check --install --verbose --force
goctl api go --api app.api --dir=. --style=goZero 

这会在当前目录生成项目文件。之后进入目录安装依赖:

go mod tidy

测试Ping接口

Ping接口的逻辑在internal/logic/app/pingLogic.go,打开文件并编辑handler函数:

func (l *PingLogic) Ping(req *types.Empty) (resp *types.BasicResponse, err error) {  
    return &types.BasicResponse{
        StatusCode: 0,
        StatusMsg:  "pong",
    }, nil
}

这里的写法和rpc微服务有点区别。注意到传入的resp是一个指针,所以需要手动创建一个响应结构体并设置键值。

这样就算完成Ping接口了,可以运行服务。

gorm 进阶 | 青训营笔记

gorm 进阶 | 青训营笔记

gorm

关于简单的 gorm 总结 可以看上一篇文章 gorm 初体验 | 青训营笔记

软删除

Gorm提供了软删除的能力,需要在结构体中定义一个Deleted字段,此时再调用Delete删除函数,则会生成update语句,并将deleted字段赋值为当前删除时间。

type Product struct {
    ID      uint
    Code    string
    Price   uint
    Deleted gorm.DeletedAt
}

再次执行删除操作:

go
db.Delete(&Product{}, 1)

生成的sql为:

UPDATE `product` SET `deleted`='2023-01-30 22:22:22.202' WHERE `product`.`id` = 1 AND `product`.`deleted` IS NULL

查询被软删除的操作,要使用Unscoped函数:

db.Unscoped().First(&product, 2)

有了DeleteAt字段后,删除操作已经变成了更新操作,那么想要物理删除怎么办?也是使用Unscoped函数:

db.Unscoped().Delete(&Product{}, 1)

Gorm事务

Gorm提供了BeginCommitRollback方法用于使用事务。

// 开启事务
tx := db.Begin()
if err := tx.Create(&Product{Code: "D32", Price: 100}).Error; err != nil {
    // 出现错误回滚
    tx.Rollback()
    return
}
if err := tx.Create(&Product{Code: "D33", Price: 100}).Error; err != nil {
    // 出现错误回滚
    tx.Rollback()
    return
}
// 提交事务
tx.Commit()

在开启事务后,调用增删改操作是应该使用开启事务返回的tx而不是db

Gorm还提供了Transaction函数用于自定提交事务,避免用户漏写CommitRollback

if err = db.Transaction(func(tx *gorm.DB) error {
    if err := db.Create(&Product{Code: "D55", Price: 100}).Error; err != nil {
        return err
    }
    if err := db.Create(&Product{Code: "D56", Price: 100}).Error; err != nil {
        return err
    }
    return nil
}); err != nil {
    return
}

这种写法,当出现错误时会自动进行Rollback,当正常执行时会自动Commit

Hook

当我们想在执行增删改查操作前后做一些额外的操作时,可以使用Gorm提供的Hook能力。

Hook是在创建、查询、更新、删除等操作之前、之后自动调用的函数,如果任何Hook返回错误,Gorm将停止后续的操作并回滚事务。

Hook会开启默认事务,所以会带来了一些性能损失。

性能提高

对于写操作(创建、更新、删除),为了确保数据的完整性,Gorm会将他们封装在事务内运行,但是这样会降低性能,可以使用SkipDefaultTransaction关闭默认事务。

使用PrepareStmt缓存预编译语句可以提高后续调用的速度。

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  SkipDefaultTransaction: true,
  PrepareStmt: true,
})

Gorm生态

Gorm拥有非常丰富的扩展生态,下面列举一些常用的扩展。

GORM 配置

GORM 提供的配置可以在初始化时使用

type Config struct {
  SkipDefaultTransaction   bool
  NamingStrategy           schema.Namer
  Logger                   logger.Interface
  NowFunc                  func() time.Time
  DryRun                   bool
  PrepareStmt              bool
  DisableNestedTransaction bool
  AllowGlobalUpdate        bool
  DisableAutomaticPing     bool
  DisableForeignKeyConstraintWhenMigrating bool
}

跳过默认事务

为了确保数据一致性,GORM 会在事务里执行写入操作(创建、更新、删除)。如果没有这方面的要求,您可以在初始化时禁用它。

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  SkipDefaultTransaction: true,
})

命名策略

GORM 允许用户通过覆盖默认的NamingStrategy来更改命名约定,这需要实现接口 Namer

type Namer interface {
    TableName(table string) string
    SchemaName(table string) string
    ColumnName(table, column string) string
    JoinTableName(table string) string
    RelationshipFKName(Relationship) string
    CheckerName(table, column string) string
    IndexName(table, column string) string
}

默认 NamingStrategy 也提供了几个选项,如:

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  NamingStrategy: schema.NamingStrategy{
    TablePrefix: "t_",   // table name prefix, table for `User` would be `t_users`
    SingularTable: true, // use singular table name, table for `User` would be `user` with this option enabled
    NoLowerCase: true, // skip the snake_casing of names
    NameReplacer: strings.NewReplacer("CID", "Cid"), // use name replacer to change struct/field name before convert it to db name
  },
})

NowFunc

更改创建时间使用的函数

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  NowFunc: func() time.Time {
    return time.Now().Local()
  },
})

DryRun

生成 SQL 但不执行,可以用于准备或测试生成的 SQL

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  DryRun: false,
})

PrepareStmt

PreparedStmt 在执行任何 SQL 时都会创建一个 prepared statement 并将其缓存,以提高后续的效率

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  PrepareStmt: false,
})

禁用嵌套事务

在一个事务中使用 Transaction 方法,GORM 会使用 SavePoint(savedPointName)RollbackTo(savedPointName) 为你提供嵌套事务支持,你可以通过 DisableNestedTransaction 选项关闭它

DisableAutomaticPing

在完成初始化后,GORM 会自动 ping 数据库以检查数据库的可用性,若要禁用该特性,可将其设置为 true

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  DisableAutomaticPing: true,
})

DisableForeignKeyConstraintWhenMigrating

AutoMigrateCreateTable 时,GORM 会自动创建外键约束,若要禁用该特性,可将其设置为 true

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  DisableForeignKeyConstraintWhenMigrating: true,
})
go字节三件套 | 青训营笔记

go字节三件套 | 青训营笔记

Gorm、Kitex、Hertz:三件套介绍与基本用法

Gorm

Gorm是Golang中广受欢迎的ORM(对象关系映射)框架,已经发展数十年,具有强大的功能和出色的性能。

ORM框架用于将面向对象的概念与数据库中的表相对应,简化数据操作过程。在Golang中,自定义的结构体与数据库表一一对应,结构体的实例对应表中的一条记录。

基本用法

定义结构体:在Gorm中,定义结构体来映射数据库表。

type User struct {
  ID    uint   `gorm:"primary_key"`
  Name  string `gorm:"type:varchar(100)"`
  Email string `gorm:"type:varchar(100);uniqueIndex"`
}

增加数据:使用Create方法来新增数据条目。

db.Create(&User{Name: "John", Email: "john@example.com"})

查询数据:使用Find进行数据查找,可以通过Where和Or条件构建查询条件。

var user User
db.Where("name = ?", "John").First(&user)

更新数据:使用Update进行数据更新,可以使用Model结合Update或Updates方法来更新列。

db.Model(&user).Update("Name", "John Updated")

删除数据:使用Delete进行数据删除操作,根据是否包含gorm.deletedat字段执行物理或逻辑删除。

db.Delete(&user)

Kitex

https://www.cloudwego.io/zh/docs/kitex/overview/

Kitex是字节开发的高性能Golang微服务RPC框架,具备出色的可扩展性和高性能。
服务端

func main() {
  srv := kitex.NewServer(new(YourServiceImpl), 
                          kitex.WithServiceAddr(":8888"))
  srv.Run()
}

客户端

func main() {
  client := NewYourServiceClient("127.0.0.1:8888", kitex.WithTransportProtocol(protocol.TRPC))
  // 调用RPC方法
}

高性能:Kitex具有优异的性能表现,适用于高负载的微服务场景。
可扩展:支持多协议,并且有丰富的开源扩展库,可以满足各种需求。

Hertz

https://www.cloudwego.io/zh/docs/hertz/overview/

Hertz是字节开发的HTTP框架,结合了其他开源框架的优点,同时满足字节跳动内部的需求,具有高可用性、高性能和高扩展性。

package main

import (
    "context"

    "github.com/cloudwego/hertz/pkg/app"
    "github.com/cloudwego/hertz/pkg/app/server"
    "github.com/cloudwego/hertz/pkg/common/utils"
    "github.com/cloudwego/hertz/pkg/protocol/consts"
)

func main() {
    h := server.Default()

    h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
            ctx.JSON(consts.StatusOK, utils.H{"message": "pong"})
    })

    h.Spin()
}

高可用性:Hertz框架被设计为具有高度的稳定性和可用性,适用于大规模的应用场景。
高性能:框架在性能方面表现出色,适合处理高并发请求。
高扩展性:Hertz框架允许根据业务需求进行定制和扩展,以满足复杂应用的要求。

网站安全漏洞 | 青训营笔记

网站安全漏洞 | 青训营笔记

什么是漏洞

漏洞是指在网站或网络应用中存在的安全弱点,它们可能由于各种原因而产生,包括编码错误、配置不当或第三方组件的安全性不足。了解一个网站的基本构成是理解漏洞的第一步。网站通常由以下几个关键组成部分构成,而攻击者通常会尝试从这些部分找出弱点进行攻击:

  • 前端:使用的技术可能包括HTML、CSS、JavaScript、Vue.js、React.js等。
  • 网关:例如nginx或Apache,负责处理进出的网络流量。
  • 后端:可能是用Go、Java、Node.js、Python等语言编写的。
  • 前后端交互:通常通过HTTP或WebSocket进行。

常见的安全事件

由安全漏洞引发的攻击事件不仅可能导致数据泄露、服务瘫痪、成果失窃和系统劫持,还可能带来以下更为严重的后果:

  • 法律责任:泄露用户数据可能导致法律诉讼和巨额罚款。
  • 品牌声誉受损:一次严重的安全事件可能永久性地影响公司的声誉。
  • 业务中断:服务中断可能导致用户流失和收入减少。
  • 内外部信任度下降:员工和合作伙伴也可能因为安全事件而对企业失去信任。

网站攻击者的意图

确切地知道攻击者的意图可以帮助企业更有效地防御威胁。除了政治、经济和竞争等原因外,还有以下可能的攻击动机:

  • 社会工程攻击:通过人为操作或欺骗,使内部人员泄露敏感信息。
  • 黑帽SEO:通过各种手段来操纵搜索引擎排名。
  • 资源占用:攻击者可能只是为了消耗目标网站的资源。
  • 纯粹的破坏欲:有些攻击者可能只是出于破坏的乐趣。

红蓝对抗

红蓝对抗是一种模拟现实世界网络攻防场景的方法,通常用于评估和提升组织的安全防护能力。在这一模式中,红军(Red Team)模拟攻击者,寻找系统的弱点和漏洞;蓝军(Blue Team)则扮演防御方,负责检测和阻止这些攻击。除此之外,还有所谓的“紫军”(Purple Team),负责协调红蓝两方,以确保双方都从对抗中获得最大的学习价值。

攻击方式

网站攻击通常可以分为两大类:针对客户端的攻击和针对服务端的攻击。

  • 针对客户端的攻击:这类攻击直接影响网站的终端用户,例如通过XSS(跨站脚本)或CSRF(跨站请求伪造)等方式。
  • 针对服务端的攻击:这类攻击目标是网站后端的服务器或数据库,例如通过SQL注入或命令执行等方式。 每一种攻击都有其自己的防护机制和修补方案,因此对这些攻击手段有深入的了解是非常重要的。

服务端漏洞

服务端漏洞通常比客户端漏洞更为严重,因为它们直接影响到整个系统的安全性。其中,第三方组件漏洞是一个常见但经常被忽视的问题。例如,一些流行的开源库或框架可能存在已知的安全问题,如果开发人员没有及时更新这些组件,就可能导致整个系统的安全性受到威胁。

SQL注入漏洞

SQL注入是一种常见但危险的攻击手段,它出现在应用程序未能正确地处理用户输入的场合。更准确地说,攻击者通过输入特定的SQL代码片段,使得原SQL查询逻辑被改变,从而达到非法获取数据或执行特定操作的目的。防范SQL注入的基础是编码规范,包括但不限于使用预编译SQL语句、参数化查询,以及进行严格的输入验证。

命令执行

命令执行漏洞通常发生在应用需要与操作系统进行交互时。与SQL注入类似,这一漏洞也常常是由于不正确的输入处理引起的。攻击者可能会尝试插入恶意命令或参数,以此来控制或破坏目标系统。防护措施包括使用安全的API进行命令调用、对用户输入进行严格的验证和过滤,以及使用最小权限原则来限制应用程序对系统资源的访问。

越权漏洞

越权漏洞是授权控制不当导致的一类安全问题,它们通常分为三类:

  • 未授权:用户无需任何授权即可访问受限制的资源。
  • 水平越权:用户访问同一权限级别但不属于自己的资源。
  • 垂直越权:低权限用户访问高权限用户的资源。 越权问题的根本在于不恰当的权限控制和会话管理,因此相应的防护措施应从这两方面入手。

SSRF攻击

服务端请求伪造(SSRF)是一种利用目标服务器作为中间人来发起请求的攻击。这样做可能会暴露原本不可达的内部网络资源。SSRF通常利用的是应用程序或服务器配置中的不当输入验证或URL解析缺陷。防范措施包括限制从服务器发出的请求类型和目标,以及对所有外部输入进行严格的验证。

文件上传漏洞

文件上传漏洞是一种常见的安全风险,尤其是在允许用户上传文件的应用中。如果没有适当的安全措施,攻击者可能会上传含有恶意代码的文件,这些文件一旦被执行,可能会导致严重的安全后果。因此,除了基本的文件类型检测和大小限制外,更高级的防护措施,如内容扫描和安全存储策略,也是非常必要的。

客户端漏洞

客户端漏洞主要影响网站的最终用户,而不是服务器或数据库。这些漏洞通常涉及到如何处理从客户端(通常是Web浏览器)发来的数据和请求。例如,开放重定向是一种常见的客户端漏洞,攻击者通过这种漏洞可以将用户重定向到恶意网站。修复这类问题通常需要在客户端和服务器端都进行适当的输入验证。

跨站脚本(XSS)攻击

跨站脚本(XSS)是一种在目标网站上执行恶意脚本的攻击方式。它利用的是网站没有对用户提交的数据进行适当的过滤和转义。根据攻击的具体形式,XSS攻击可以进一步分为存储型、反射型和DOM型三类。每种类型的XSS攻击都有其独特的防御方法,如输入过滤、输出编码和内容安全策略(CSP)。

跨站请求伪造(CSRF)

跨站请求伪造(CSRF)是一种攻击手法,攻击者通过某种方式诱导用户点击一个链接或按钮,从而在用户不知情的情况下,以该用户的身份执行非预期的操作。防御CSRF攻击的常见方法包括使用CSRF令牌、设置SameSite属性等。

总结

在这篇文章中,我们列举了网站安全漏洞的多个方面,从基础概念到各种类型的安全漏洞,以及如何防范这些漏洞。重要的是,无论是服务端还是客户端,安全漏洞都可能存在,并且都需要得到妥善的处理。

网站安全是一个涉及多个层面的复杂问题,需要开发者、运维人员、以及安全团队共同努力来维护。只有全方位、多层次的安全防护措施,才能有效地减少安全风险,保护网站和用户数据的安全。

go架构 | 青训营笔记

go架构 | 青训营笔记

什么是架构

主要针对互联网服server系统(类似网站)来定义架构:架构是系统的骨架,支撑和链接各个部分,包括组件、连接件、约束规范,以及指导这些内容设计与演化的原理。

  • 组件:类似应用服务,独立模块、数据库、nginx等等、

  • 连接件:分布式调用、进程间调用、调用使用http协议还是tcp协议、组件之间的交互关系、

  • 约束规范: 定规则做限制:例如设计原则、编码规范等等。

组件

组件是架构的构建块,包括独立模块、数据库、Nginx等。

例如,Go语言中可以定义一个组件,如一个HTTP服务器:

package main

import (
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, World!"))
    })
    http.ListenAndServe(":8080", nil)
}

连接件

连接件处理组件之间的交互关系,例如分布式调用、进程间调用、使用的协议等。

约束规范

约束规范定义了架构中的规则和限制,例如设计原则和编码规范。

几种典型架构

单机架构

单机架构就是把所有功能都实现在一个进程中,并部署在一台机器上。

单机架构的优点是非常简单,缺点是会遇到并发处理的难题,而且运维需要停服。

当时计算机软硬件性能比较基础,这个问题引起人们对网络套接字优化和服务端调度的关注,这个词语也随着时代的发展逐渐变化,如C10M代表的是处理1000万个并发连接。现在的计算机已经有足够的性能和优秀的软件来支撑单机数百万连接了,然而并发问题仍然是后端架构设计中不能略过的关键问题。

单体架构

单体架构是对单机架构的改进。既然单机处理能力有限,就可以采用分布式部署的模式设置多台单机,再设置一个分流环节,将请求转发给各个单体服务器来处理。

单体架构的优点是可以实现水平扩容,处理能力不够时只需要简单添加新的单机即可;运维不需要停服,可以逐个对单机维护不影响其他单机的运行。单体架构下,每台机器的功能是一致的,因此它没有解决这两个缺点:

  1. 职责太多,开发效率低:每台机器都要实现系统的完整功能,不能专注于某个功能的开发。
  2. 爆炸半径大: 爆炸半径是指爆炸可能造成损害的区域范围。在生产环境中指的就是部署的服务故障时可能影响的软件功能范围。单体架构下每一个单机下线,都会造成所有功能的处理承载量下降,因此爆炸半径覆盖了软件多数功能。

垂直应用架构

为了解决单体架构的问题,垂直应用架构提供了一种思路,将系统按照服务拆分任务,分配给不同的服务器,从而实现职责的简单划分,一定程度上提高开发和运维的效率。不过在实践中一个服务往往是大量功能点的集合,仍然存在功能琐碎的问题

更加高级的架构

SOA、微服务架构

SOA(Service-Oriented Architecture)架构具有两个特性:

  1. 将应用的不同功能单元抽象为服务,从而细分职责
  2. 定义服务之间的通信标准,确保服务之间的数据流通

微服务架构则是SOA架构去中心化的演进方向,旨在减少服务之间的沟通消耗,避免SOA服务内部集中沟通导致的过度中心化问题,他实现了水平切分,减少了跨级别调用链,让每个服务直接负责的上下级减少。

这类架构在解决之前的弊端时,也会引入新的问题:

  • 数据一致性问题:不同服务如何确保数据同步
  • 高可用问题:服务之间如何进行可靠合作
  • 治理问题:一个服务出现问题时,要如何容灾
  • 解耦和过微问题:过度细分会导致运维成本提高,要如何权衡?

事件驱动架构

事件驱动架构(EDA)是一种异步架构,其中生产者生成事件,并由一个或多个消费者消费。这允许解耦生产者和消费者,并实现高度可扩展和灵活的系统。

Apache Kafka
Apache Kafka是一种流行的事件流平台。您可以使用Go编写Kafka的生产者和消费者。

如下是示例代码

package main

import (
    "github.com/confluentinc/confluent-kafka-go/kafka"
)

func main() {
    producer, err := kafka.NewProducer(&kafka.ConfigMap{"bootstrap.servers": "localhost"})
    if err != nil {
        panic(err)
    }

    // 生产消息
    producer.Produce(&kafka.Message{
        TopicPartition: kafka.TopicPartition{Topic: &topic, Partition: kafka.PartitionAny},
        Value:          []byte("Message Value"),
    }, nil)

    // 消费消息
    consumer, err := kafka.NewConsumer(&kafka.ConfigMap{
        "bootstrap.servers": "localhost",
        "group.id":          "myGroup",
        "auto.offset.reset": "earliest",
    })
    consumer.SubscribeTopics([]string{"myTopic"}, nil)
    msg, err := consumer.ReadMessage(-1)
    if err == nil {
        fmt.Printf("Message on %s: %s\n", msg.TopicPartition, string(msg.Value))
    }
}

云计算

云计算是指通过软件自动化管理,提供计算资源的服务网络,是现在互联网大规模数据分析和存储的基石。

云计算的基础是虚拟化技术和编排方式,关系到资源的分配和调度。

云计算提供一系列特有的架构:

  • 基础设施即服务IaaS(Infrastructure as a Service):服务商提供互联网基础设施,比如用户只需要获得服务器、云硬盘、宽带网络,而不用关心物理的机房、数据中心、网络怎么建设。但是,用户需要自己进行服务器系统安装、环境部署和软件配置。
  • 平台即服务PaaS(Platform as a Service):服务商提供底层软件设施,比如为用户准备安装了指定操作系统、数据库软件、环境套件的主机。用户需要自己控制上层应用的部署和托管。
  • 软件即服务Saas(Software as a Service):服务商及提供基于软件的解决方案来满足客户的需求,比如用户想要一套OA办公系统,CMS内容管理系统,HRM人事管理系统等,而不用自行搭建和维护这样的服务,能够简化用户的工作。
  • 函数即服务FaaS(Function as a service):服务商提供一个平台,允许客户开发、运行和管理应用程序功能,而不用关心如何启动和部署这样的程序。这个模式是“无服务器”体系架构的方式,具有灵活性,适合构建微服务应用程序。

云原生架构

云原生技术为公司在公有云、私有云、混合云等新型动态环境中,构建和运行可弹性拓展的应用提供了可能。

云原生为公司提供了以下关键能力:

弹性资源调度

  • 服务资源调度:分配微服务、大服务使用的资源。
  • 计算资源调度:分配在线计算、离线计算使用的资源
  • 消息队列:和计算资源调度类似,提供海量的数据吞吐和大数据分析能力。

弹性存储资源

云原生让用户可以将存储资源看作服务,寻找对应的服务来满足自己的存储需求。

  • 经典存储:对象存储、大数据记录存储。
  • 关系型数据库:业务记录存储。
  • 元数据:提供服务发现能力
  • NoSQL:key-value存储,实现缓存和分布式事务。

DevOps

DevOps贯穿整个软件开发周期,使用一系列自动化流程,提高软件开发、交付的效率。DevOps提供了敏捷开发、持续集成/交付(CI/CD)的能力。

微服务架构

微服务架构实现业务功能单元的解耦,同时提供一套统一的通信标准确保服务之间的数据流通。

微服务常见的通信标准是HTTP(RESTful API)和RPC(Thrift, gRPC),需要结合性能、服务治理、协议可解释性来选择。实际场景下,常常使用微服务框架提供的通信能力,而不用自己从头实现一套。

服务网格

服务网格实现业务与治理的结构以及异构系统治理的统一化,从而提供复杂的治理能力。

服务发现

服务注册与发现 Eureka或Consul等服务可以用于服务发现。

服务注册与发现使服务可以动态地找到其他服务的位置。

// 使用Consul进行服务注册
consulConfig := consulapi.DefaultConfig()
consulConfig.Address = "localhost:8500"
client, err := consulapi.NewClient(consulConfig)
if err != nil {
    log.Fatal(err)
}

// 创建新的服务
registration := new(consulapi.AgentServiceRegistration)
registration.ID = "my-service-id"
registration.Name = "my-service"
registration.Port = 8080
registration.Tags = []string{"my-service"}

// 注册服务
agent := client.Agent()
if err := agent.ServiceRegister(registration); err != nil {
    log.Fatal(err)
}

// 服务发现逻辑...

分布式架构

分布式锁

在分布式环境中,协调多个节点的访问资源可能会变得复杂。分布式锁是一种解决方案,可以在多个节点之间同步访问。

可以使用Go语言和Redis创建分布式锁:


package main

import (
    "fmt"
    "github.com/go-redis/redis/v8"
    "golang.org/x/net/context"
)

func main() {
    rdb := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })

    ctx := context.Background()
    lockKey := "lock_key"

    // 尝试获取锁
    locked, err := rdb.SetNX(ctx, lockKey, "lock_value", 0).Result()
    if err != nil || !locked {
        fmt.Println("获取锁失败")
        return
    }

    // 执行业务逻辑...

    // 释放锁
    rdb.Del(ctx, lockKey)
    fmt.Println("业务逻辑执行完毕,锁已释放")
}

优化方式

离在线资源并池

在线业务以IO密集型为主,要求计算的实时性,但是具有潮汐性,不同时间段的压力不同。离线业务以计算密集型为主,对计算实时性不高。

把在线和离线资源放入同一个资源池进行调度,利用在线业务的潮汐性自动扩缩容,能够有效降低物理资源成本,提供更多的弹性资源,增加收益。

负载均衡

简单的http负载均衡 轮询算法

package main

import (
    "net/http"
    "net/http/httputil"
    "net/url"
)

var servers = []string{
    "http://localhost:8081",
    "http://localhost:8082",
}

var currentServer = 0

func nextServer() string {
    server := servers[currentServer]
    currentServer = (currentServer + 1) % len(servers)
    return server
}

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        server := nextServer()
        target, _ := url.Parse(server)
        proxy := httputil.NewSingleHostReverseProxy(target)
        proxy.ServeHTTP(w, r)
    })
    http.ListenAndServe(":8080", nil)
}

亲和性部署

如果两个微服务之间通信密切,则这两个服务的亲和性较高。将满足亲和性条件的容器调度到一台宿主机,让微服务中间件与服务网格通过共享内存通信,能够有效降低通信成本,提高服务可用性。

流量治理

基于微服务中间件和服务网格的流量治理,赋予微服务熔断、重试的能力,提供复杂环境下的流量调度,可以提高微服务调用容错性,增强容灾能力,还能进一步提高开发效率。

CPU水位负载均衡

结合IaaS提供的资源探针和服务网格的负载均衡能力,能够为自动扩缩容提供反馈,打平异构环境下算力的差异。

容器化与服务编排

Docker

可以使用Docker将Go应用程序打包到容器中,并确保它在任何支持Docker的平台上以相同的方式运行。

Kubernetes

Kubernetes是一种强大的容器编排工具,用于自动部署、扩展和管理容器化应用程序。Go语言是Kubernetes的主要实现语言。

总结

现代软件架构的复杂性和灵活性要求了解许多概念和技术。从单机到分布式,从单体到微服务,架构演化为满足不断增长的需求。

通过使用强大和灵活的编程语言,如Go,您可以构建符合这些需求的解决方案。从分布式锁到服务发现,从负载均衡到微服务通信,Go为构建现代、可扩展和高性能的系统提供了强大的工具和库。

不断学习和实践这些概念和技术将使您更好地设计和构建复杂的系统,满足业务目标,并应对未来的挑战。

gorm 初体验 | 青训营笔记

gorm 初体验 | 青训营笔记

gorm

gorm是Golang语言中一款性能极好的ORM库,对开发人员相对是比较友好的。接下来主要学习下gorm库的一些基本使用。

功能概览

  • 全功能 ORM
  • 关联 (Has One,Has Many,Belongs To,Many To Many,多态,单表继承)
  • Create,Save,Update,Delete,Find 中钩子方法
  • 支持 Preload、Joins 的预加载
  • 事务,嵌套事务,Save Point,Rollback To Saved Point
  • Context、预编译模式、DryRun 模式
  • 批量插入,FindInBatches,Find/Create with Map,使用 SQL 表达式、Context Valuer 进行 CRUD
  • SQL 构建器,Upsert,数据库锁,Optimizer/Index/Comment Hint,命名参数,子查询
    复合主键,索引,约束
  • Auto Migration
  • 自定义 Logger
  • 灵活的可扩展插件 API:Database Resolver(多数据库,读写分离)、Prometheus…
  • 每个特性都经过了测试的重重考验
  • 开发者友好

安装

安装

go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite

模型定义

基本定义

因为orm可以自动创建数据库,创建表,所以我们要用一个约定的形式来描述一个schema,便于他创建表,也便于代码中调用

例如

type User struct {
  Name         string
  Email        string
  Age          uint8
}

这样的一个结构体就可以描述user这个表

gorm.Model

// gorm.Model 的定义
type Model struct {
  ID        uint           `gorm:"primaryKey"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt `gorm:"index"`
}

这个约定了一些gorm的标准配置,相当于implement了这个结构体

例如

type User struct {
  gorm.Model
  Name         string
  Email        string
  Age          uint8
}

这样这个gorm.Model相当于在user表里添加了一些基础的字段

这个user最终会变成 相当于

type User struct {
  ID        uint           `gorm:"primaryKey"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt `gorm:"index"`
  Name         string
  Email        string
  Age          uint8
}

嵌入结构体

如上的gorm.Model就是一种嵌入结构体

官网的解释是:对于匿名字段,GORM 会将其字段包含在父结构体中

其他嵌入方法为:

对于正常的结构体字段,你也可以通过标签 embedded 将其嵌入,例如:

type Author struct {
    Name  string
    Email string
}

type Blog struct {
  ID      int
  Author  Author `gorm:"embedded"`
  Upvotes int32
}
// 它等效于
type Blog struct {
  ID    int64
  Name  string
  Email string
  Upvotes  int32
}

可以使用标签 embeddedPrefix 来为 db 中的字段名添加前缀
Author Author `gorm:”embedded;embeddedPrefix:author_”

字段标签

声明 model 时,tag 是可选的,gorm 支持以下这些 tag: tag 名大小写不敏感,但我们写的时候最好用驼峰命名法。

写法为 在字段后面添加飘号和gorm:

例如

ID        uint           `gorm:"primaryKey"`
标签名 说明
column 指定 database 的列名
type 列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 bool、int、uint、float、string、time、bytes 并且可以和其他标签一起使用,例如:not nullsize, autoIncrement… 像 varbinary(8) 这样指定数据库数据类型也是支持的。在使用指定数据库数据类型时,它需要是完整的数据库数据类型,如:MEDIUMINT UNSIGNED not NULL AUTO_INCREMENT
serializer 指定将数据序列化或反序列化到数据库中的序列化器, 例如: serializer:json/gob/unixtime
size 定义列数据类型的大小或长度,例如 size: 256
primaryKey 将列定义为主键
unique 将列定义为唯一键
default 定义列的默认值
precision 指定列的精度
scale 指定列大小
not null 指定列为 NOT NULL
autoIncrement 指定列为自动增长
autoIncrementIncrement 自动步长,控制连续记录之间的间隔
embedded 嵌套字段
embeddedPrefix 嵌入字段的列名前缀
autoCreateTime 创建时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoCreateTime:nano
autoUpdateTime 创建/更新时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoUpdateTime:milli
index 根据参数创建索引,多个字段使用相同的名称则创建复合索引,查看 索引 获取详情
uniqueIndex index 相同,但创建的是唯一索引
check 创建检查约束,例如 check:age > 13,查看 约束 获取详情
<- 设置字段写入的权限, <-:create 只创建、<-:update 只更新、<-:false 无写入权限、<- 创建和更新权限
-> 设置字段读的权限,->:false 无读权限
- 忽略该字段,- 表示无读写,-:migration 表示无迁移权限,-:all 表示无读写迁移权限
comment 迁移时为字段添加注释

连接数据库

官方提供的代码

import (
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
)

func main() {
  // 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
  dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
  db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
}

这里我提供一种封装方案,因为一般数据库配置写在配置文件里,防止泄露与主代码分离‘


import (
    "fmt"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/schema"
)

type Config struct {
    // 数据库配置
    DBList DBListConf
}

type DBListConf struct {
    Mysql MysqlConf
}

type MysqlConf struct {
    Address     string
    Username    string
    Password    string
    DBName      string
    TablePrefix string
}

type ServiceContext struct {
    Config Config
    DBList *DBList
}

type DBList struct {
    Mysql *gorm.DB
}

func NewServiceContext(c Config) *ServiceContext {
    return &ServiceContext{
        Config: c,
        DBList: initDB(c),
    }
}

func initDB(c Config) *DBList {
    dbList := new(DBList)
    dbList.Mysql = initMysql(c)

    return dbList
}

func initMysql(c Config) *gorm.DB {
    dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
        c.DBList.Mysql.Username,
        c.DBList.Mysql.Password,
        c.DBList.Mysql.Address,
        c.DBList.Mysql.DBName,
    )

    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
        NamingStrategy: schema.NamingStrategy{
            TablePrefix:   c.DBList.Mysql.TablePrefix, // 表名前缀
            SingularTable: true,                       // 使用单数表名
        },
        DisableForeignKeyConstraintWhenMigrating: true,
    })
    if err != nil {
        panic(err)
    }

    // 自动建表
    err = db.AutoMigrate(&model.Video{}, &model.Favorite{}, &model.Comment{})
    if err != nil {
        panic(err)
    }

    return db
}

func main() {
    // 创建配置实例
    config := Config{
        DBList: DBListConf{
            Mysql: MysqlConf{
                Address:     "Address",
                Username:    "root",
                Password:    "1111111111111",
                DBName:      "dbname",
                TablePrefix: "Prefix",
            },
        },
    }

    // 创建服务上下文
    serviceContext := NewServiceContext(config)
    
    // 在这里你可以使用 serviceContext 进行操作
    // ...

    fmt.Println("Database initialization and configuration completed.")
}

这里出现了自动建表的操作AutoMigrate 你需要把你想要创建的模型传进来自动建表

CRUD

1. C

接下来开始 crud,

假如你有一个user表,可以使用create创建多个数据

users := []*User{
    User{Name: "Jinzhu", Age: 18, Birthday: time.Now()},
    User{Name: "Jackson", Age: 19, Birthday: time.Now()},
}

result := db.Create(users) // pass a slice to insert multiple row

result.Error        // returns error
result.RowsAffected // returns inserted records count

Error相信你会接

if result.Error != nil {
    return exit
}

或者可以直接在create里

err := db.create().Error
if err != nil {
    return exit
}

2.R

根据主键检索


db.First(&user, 10)
// SELECT * FROM users WHERE id = 10;

db.First(&user, "10")
// SELECT * FROM users WHERE id = 10;

db.Find(&users, []int{1,2,3})
// SELECT * FROM users WHERE id IN (1,2,3);

这里的first为检索第一个 limit为1

检索全部对象

// Get all records
result := db.Find(&users)
// SELECT * FROM users;

result.RowsAffected // returns found records count, equals `len(users)`
result.Error        // returns error

条件检索

这样相当于拼sql语句

// Get first matched record
db.Where("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;

// Get all matched records
db.Where("name <> ?", "jinzhu").Find(&users)
// SELECT * FROM users WHERE name <> 'jinzhu';

// IN
db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users)
// SELECT * FROM users WHERE name IN ('jinzhu','jinzhu 2');

// LIKE
db.Where("name LIKE ?", "%jin%").Find(&users)
// SELECT * FROM users WHERE name LIKE '%jin%';

// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;

// Time
db.Where("updated_at > ?", lastWeek).Find(&users)
// SELECT * FROM users WHERE updated_at > '2000-01-01 00:00:00';

// BETWEEN
db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
// SELECT * FROM users WHERE created_at BETWEEN '2000-01-01 00:00:00' AND '2000-01-08 00:00:00';

高级查询 https://gorm.io/zh_CN/docs/advanced_query.html

3.U

更新 所有字段

db.Save(&User{Name: "jinzhu", Age: 100})
// INSERT INTO `users` (`name`,`age`,`birthday`,`update_at`) VALUES ("jinzhu",100,"0000-00-00 00:00:00","0000-00-00 00:00:00")

db.Save(&User{ID: 1, Name: "jinzhu", Age: 100})
// UPDATE `users` SET `name`="jinzhu",`age`=100,`birthday`="0000-00-00 00:00:00",`update_at`="0000-00-00 00:00:00" WHERE `id` = 1

更多更新方法 https://gorm.io/zh_CN/docs/update.html

4.D

删除一条记录

// Email 的 ID 是 `10`
db.Delete(&email)
// DELETE from emails where id = 10;

// 带额外条件的删除
db.Where("name = ?", "jinzhu").Delete(&email)
// DELETE from emails where id = 10 AND name = "jinzhu";

更多删除方法 https://gorm.io/zh_CN/docs/delete.html

其中的软删除感觉很有用

当调用Delete时,GORM并不会从数据库中删除该记录,而是将该记录的DeleteAt设置为当前时间,而后的一般查询方法将无法查找到此条记录。

总结

gorm是个很好用的go的orm框架,对很多数据库都兼容

性能也很好,操作起来也很简单。

go语言三个小项目 | 青训营笔记

go语言三个小项目 | 青训营笔记

猜数字

  1. 猜数字
    这个项目非常简单,它涉及到随机数的生成和用户输入操作。
    我们使用了bufio库来处理输入数据。
reader := bufio.NewReader(os.Stdin)
input, _ := reader.ReadString('\n')

简单字典

  1. 标准库strconv
    它主要用于字符和其他类型之间的转换。
strconv.Atoi(s string) int 
  1. 标准库strings
strings.TrimSuffix(s string ,  suffix string) string

删除末尾字符,如果没有就正常返回

  1. 网络库
client := &http.Client{}

初始化请求客户端

req,  err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict",  data)

构造头

req.Header.Set("authority",  "api.interpreter.caiyunai.com")

请求

bodyText,  err := io.ReadAll(resp.Body)
err = json.Unmarshal(bodyText,  &dictResponse)
  1. os库
word := os.Args[1]

获取环境参数

socks5代理

package main

import (
    "context"
    "encoding/binary"
    "fmt"
    "io"
    "net"
)

const (
    socks5Ver  = 0x05
    atypeIPV4  = 0x01
    atypeHOST  = 0x03
    cmdConnect = 0x01
)

func main() {
    // 假设在这里建立一个 net.Listener 监听 Socks5 代理请求

    for {
        client, err := acceptConnection()
        if err != nil {
            continue
        }

        // 开启一个 goroutine 处理客户端请求
        go handleSocks5Client(client)
    }
}

func acceptConnection() (net.Conn, error) {
    // 实现接收连接请求的逻辑
}

func handleSocks5Client(conn net.Conn) {
    defer conn.Close()

    // 鉴权阶段
    if err := auth(conn); err != nil {
        fmt.Println("Authentication error:", err)
        return
    }

    // 通讯阶段
    addr, err := connect(conn)
    if err != nil {
        fmt.Println("Connection error:", err)
        return
    }

    // 给客户端回包,表示连接成功
    _, _ = conn.Write([]byte{socks5Ver, 0x00, 0x00, atypeIPV4, 0, 0, 0, 0, 0, 0})

    // 开始进行数据转发
    dest, err := net.Dial("tcp", addr)
    if err != nil {
        fmt.Println("Failed to connect to destination:", err)
        return
    }
    defer dest.Close()

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    // 从客户端到目标地址的数据拷贝
    go func() {
        _, _ = io.Copy(dest, conn)
        cancel()
    }()

    // 从目标地址到客户端的数据拷贝
    go func() {
        _, _ = io.Copy(conn, dest)
        cancel()
    }()

    <-ctx.Done()
}

func auth(conn net.Conn) error {
    buf := make([]byte, 2)
    _, err := io.ReadFull(conn, buf)
    if err != nil {
        return err
    }

    ver, nmethods := buf[0], buf[1]
    methods := make([]byte, nmethods)
    _, err = io.ReadFull(conn, methods)
    if err != nil {
        return err
    }

    // 在这里根据收到的 methods 进行认证处理

    // 假设这里选择不需要认证,回包告知客户端不需要认证
    _, err = conn.Write([]byte{socks5Ver, 0x00})
    if err != nil {
        return err
    }

    return nil
}

func connect(conn net.Conn) (string, error) {
    buf := make([]byte, 4)
    _, err := io.ReadFull(conn, buf)
    if err != nil {
        return "", err
    }

    ver, cmd, _, atyp := buf[0], buf[1], buf[2], buf[3]
    if ver != socks5Ver || cmd != cmdConnect {
        return "", fmt.Errorf("Unsupported SOCKS5 command")
    }

    var addr string
    switch atyp {
    case atypeIPV4:
        buf := make([]byte, 4)
        _, err := io.ReadFull(conn, buf)
        if err != nil {
            return "", err
        }
        addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])

    case atypeHOST:
        hostSizeBuf := make([]byte, 1)
        _, err := io.ReadFull(conn, hostSizeBuf)
        if err != nil {
            return "", err
        }
        hostSize := int(hostSizeBuf[0])
        hostBuf := make([]byte, hostSize)
        _, err = io.ReadFull(conn, hostBuf)
        if err != nil {
            return "", err
        }
        addr = string(hostBuf)
    }

    portBuf := make([]byte, 2)
    _, err = io.ReadFull(conn, portBuf)
    if err != nil {
        return "", err
    }
    port := binary.BigEndian.Uint16(portBuf)

    return fmt.Sprintf("%v:%v", addr, port), nil
}

使用 SOCKS5 代理的实现,涵盖了以下主要步骤:

鉴权阶段:

解析客户端发送的版本号、支持的认证方法数量和认证方法列表。
针对支持的认证方法进行验证,返回认证成功的回包。
通讯阶段:

解析客户端发送的连接请求,包括版本号、命令类型、目标地址类型、目标地址、目标端口等信息。
根据目标地址类型和地址解析方式,获取目标地址和端口号。
向目标地址发起连接请求,并返回连接成功的回包。
进程管理:

使用 goroutine 后台开启两个服务器交互进程。
通过 context 库进行进程管理,确保进程在必要时可以被关闭。
将客户端和目标之间的数据互相拷贝,使得数据能够在两者之间传递。

四、课后个人总结:

在这节课学习的内容中,我收获了很多关于Go语言编程的知识和技能。以下是我的感想:

Go语言的简洁和高效:通过学习这些基础标准库和相关功能,我深刻感受到Go语言的简洁和高效。标准库提供了丰富的功能,让编程变得更加简单和高效。

数据类型转换和字符串处理:学习了strconv和strings包,对于数据类型转换和字符串处理有了更深入的了解。这些工具使得在处理用户输入和数据转换时更加方便。

网络编程和HTTP库:了解了Go语言中的网络编程和net/http包,我现在能够编写简单的HTTP服务器和客户端,这对于开发网络应用和服务端程序非常有用。

文件操作和系统交互:通过os包的学习,我学会了如何在Go语言中进行文件操作和与操作系统进行交互,这对于处理文件和系统配置十分重要。

代理和鉴权:学习了socks5代理和鉴权机制,我对网络代理和安全认证有了更深刻的理解,这对于开发安全性较高的应用非常重要。

上下文处理:context包的学习使我了解了在Go语言中如何优雅地处理请求上下文,更好地控制请求的流程和生命周期。

LINUX 挑战:用户和组管理

LINUX 挑战:用户和组管理

LINUX 挑战:用户和组管理

wget https://labfile.oss.aliyuncs.com/courses/2585/06.tar.gz
tar -xzf 06.tar.gz
chmod 700 ./06.sh
./06.sh
sudo userdel guest2
sudo userdel guest3
sudo userdel guest4
sudo userdel guest5

sudo rm -rf /home/guest2
sudo rm -rf /home/guest3
sudo rm -rf /home/guest4
sudo rm -rf /home/guest5

sudo groupadd guest
mkdir /tmp/tempuser
sudo usermod -u 2000 -g guest -d /tmp/tempuser guest1 
sudo mkdir /home/vsftpduser
sudo useradd vsftpduser -d /home/vsftpduser -s /usr/sbin/nologin 

sudo chown vsftpduser /home/vsftpduser
sudo chgrp vsftpduser /home/vsftpduser
sudo chmod 744 /home/vsftpduser
sudo useradd mag
sudo useradd dev
sudo useradd test

sudo mkdir /srv/projectx/mag
sudo mkdir /srv/projectx/dev
sudo mkdir /srv/projectx/test

sudo usermod -G dev mag
sudo chown dev:dev /srv/projectx
sudo chmod 751 /srv/projectx

sudo chmod o+rx /srv/projectx/dev
sudo chmod o+rx /srv/projectx/test
cat /etc/passwd | grep bash | cut -d ':' -f 1,3 > users.txt
sudo useradd stu
sudo passwd stu
123456
123456

sudo visudo
:21
# 按i往里写
stu ALL=(shiyanlou:shiyanlou) /bin/touch
# 退出按:wq
sudo passwd dev
123456
123456

sudo touch /etc/sudoers.d/dev
sudo chmod 777 /etc/sudoers.d/dev
vim /etc/sudoers.d/dev
# 按i往里写
dev  ALL=(root:root) /usr/sbin/useradd
# 退出按:wq
Web 开发安全 | 青训营笔记

Web 开发安全 | 青训营笔记

这是我参与「第五届青训营」伴学笔记创作活动的第 17 天

课程重点

  • Web 相关的攻击介绍

  • Web 相关的防御介绍

详细介绍

XSS (Cross-Site Scripting)

主要是由于盲目信任用户输入,直接将用户输入渲染出来,导致了攻击脚本的植入

特点:

  1. 通常难以从 UI 上感知
  2. 窃取用户信息,例如 cookie 和 token

常见的会导致XSS的代码:

document.write
el.innerHTML = anyString

总结来说,所有能够渲染dom结构的函数,都有可能导致XSS攻击。

比如论坛场景中,用户提交的内容包含恶意script标签

fetch("/submit", {
    body: JSON.stringify({
        id: "1",
        content: `<script>alert("xss");</script>`
    })
})

导致代码被插入到帖子中,所有用户访问页面都会执行恶意脚本

XSS 能够分成下面四类

Stored XSS 直接将恶意脚本储存到了数据库中,导致之后的所有访问均会携带恶意脚本,危害很大

Reflected XSS` 反射型,不储存数据,仅仅从URL传入脚本导致攻击,例如 `/path?param=<script>alert('xss')</script>

DOM-based XSS 不由服务器参与,攻击的发起和执行都在浏览器,常见于前端框架中

Mutation-based XSS 利用浏览器的特性,不同浏览器会有区别,例如

<noscript><p title="</noscript><img src=x onerror=alert(1)>">

CSRF (Cross-site request forgery)

特点:

  1. 在用户不知情的前提下
  2. 利用用户权限(cookie)
  3. 构造指定HTTP请求,窃取或修改用户敏感信息

例子:

用户没有访问银行网页,但是访问了一个带有攻击内容的网页,该网页尝试请求银行接口,由于用户登录过银行,接口携带cookie,导致请求成功,使得用户受到损失。

SQL 注入 (SQL Injection)

在请求参数中构造恶意字符串,拼接SQL语句,导致服务器执行了特定的SQL语句,造成数据库内容泄露

例子:

sql.query(`
    SELECT a, b, c FROM table
    WHERE username = ${username}
    AND form_id = ${form_id}
`)

上面的这段后端代码,攻击者就能够通过构造特殊的 username 或 form_id,使得SQL语句的意义被改变

例如传入 form_id = any; DROP TABLE table;

SQL 语句拼接后变成 SELECT a,b,c FROM table WHERE xxxx AND form_id = any; DROP TABLE table;

将导致数据库被删除

SSRF (Server-Side Request Forgery)

服务端伪造请求

例子:

ctx.body = await fetch(ctx.query.callback)

导致能够通过传入的参数访问到服务器内网的相关服务

DoS (Denial of Service)

「 不搞复杂的 , 量大就完事儿了 」

通过构造特定请求,导致服务器资源被消耗,来不及响应更多请求,引发雪崩效应

例如:

  1. 耗时的同步操作
  2. 文件备份
  3. 数据库写入
  4. SQL join
  5. 循环执行逻辑

DDoS (Distributed DoS)

攻击特点

  • 直接访问 IP
  • 任意 API
  • 消耗大量带宽 ( 耗尽 )

SYN Flood

尾声

安全无小事
使用的依赖 npm package,甚至是 NodeJS 可能成为最薄弱的一环

  • 保持学习心态
    npm install 除了带来了果洞 , 还可以带来漏洞

引用

https://bytedance.feishu.cn/file/boxcn9L4YzmTK3mwE3tIBL2UVme

https://baike.baidu.com/item/XSS%E6%94%BB%E5%87%BB/954065