0%

我们在开发微服务的时候,多半会采用 OAuth2 进行授权认证。 本文将介绍如何通过状态随机数来防止可能的 CSRF 攻击。

CSRF

CSRF 攻击

OAuth2 中的 CSRF 攻击

OAuth2 授权过程中有以下几个参数:

  • response_type=code
  • client_id
  • redirect_uri

这几个参数都是公开的,所有用户都使用相同的值。 攻击者也无法控制 redirect_uri,因为这个会配置在授权服务器的合法地址列表中。

攻击者能控制的是可选参数 state,如果授权过程中提供了 state,那么授权成功后会原封不动的返回 state。 授权流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sequenceDiagram
autonumber
participant A as Webapp
participant C as Passport
participant B as Backend

A ->> +C: redirect /login?...&state=value
C ->> C: login
C -->> -A: redirect /?code=...&state=value

A -->> +C: /TOKEN?code=...
C -->> -A: access_token

A ->> B: Authorization: Bearer <access_token>

state 本身不容易受到任何攻击,但是 Web 应用程序可能会实现自定义逻辑,这种逻辑使用攻击者有机可乘。 攻击流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sequenceDiagram
autonumber
participant A as Webapp
participant C as Passport
Actor D as Attacker
participant B as Backend

D ->> +C: redirect /login?...&state=value
C ->> C: login
C -->> -A: redirect /?code=...&state=value

A -->> +C: /TOKEN?code=...
C -->> -A: access_token

A ->> A: read state

A ->> B: CSRF Attack
Read more »

什么是 CSRF 攻击

CSRF(Cross-Site Request Forgery)的全称是跨站请求伪造,也被称为 One Click Attack 或者 Session Riding,通常缩写为 CSRF 或者 XSRF。 CSRF 的中文名称尽管听起来像跨站脚本攻击(XSS),但它与XSS非常不同,并且攻击方式几乎相左。 XSS 利用站点内的信任用户,而 CSRF 则通过伪装来自受信任用户的请求来攻击受信任的网站。 与 XSS 攻击相比,CSRF 攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比 XSS 更具危险性。

我们可以这么理解 CSRF 攻击:攻击者借助浏览器自动封装 cookie 来伪造请求,以你的名义进行某些非法操作。 CSRF 能够使用你的账户发送邮件,获取你的敏感信息,甚至盗走你的账户购买商品等。 CSRF 攻击其实是利用了 web 中用户身份认证验证的一个漏洞: 简单的身份验证仅仅能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。

想要达成 CSRF 攻击,需要两个条件:

  1. 用户先登陆受信任的站点 A,并产生 cookie 信息
  2. 在 cookie 未失效的情况下,访问危险网站 B

CSRF 攻击流程

1
2
3
4
5
6
7
8
9
10
11
12
13
sequenceDiagram
autonumber
Actor User as User (Browser)
participant A as Web A (Trusted)
participant B as Web B (Hacked)

User ->> +A: 用户登陆网站 A
A -->> -User: 登陆成功,并生成 cookie 信息

User ->> +B: 浏览危险网站 B
B -->> -User: 伪造对网站 A 的请求 R(可能是一个危险请求)
User -->> +A: 在用户不知情的情况下对网站 A 发起请求 R,请求会自动携带 cookie
A -->> -User: 网站 A 执行请求 R
Read more »

Lerna 是一个管理工具,用于管理包含多个软件包(package)的 JavaScript 项目。

Lerna 有两种工作模式:

  • Fixed/Locked mode (default): 所有的包共用一个版本号。
  • Independent mode: 使用 --independent, -i 参数初始化项目,每个包单独指定版本号。

Getting Started

初始化项目:

1
2
3
$ mkdir lerna-demo && cd $_
$ npm install -D lerna
$ npx lerna init

目录结构如下:

1
2
3
4
lerna-demo/
|-- packages/
|-- package.json
|-- lerna.json

创建包:

1
$ npx lerna create @lerna-demo/package1

最终目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
lerna-demo/
|-- packages/
|-- package1/
|-- __tests__/
|-- package1.test.js
|-- lib/
|-- package1.js
|-- package.json
|-- README.md
|-- package.json
|-- lerna.json

当然,除了可以使用 lerna create <pkg-name> 创建 package 之外,还可以使用其他 cli 工具自行创建 package

Read more »

Using middleware

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func main() {
// Creates a router without any middleware by default
r := gin.New()

// Global middleware
// Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release.
// By default gin.DefaultWriter = os.Stdout
r.Use(gin.Logger())

// Recovery middleware recovers from any panics and writes a 500 if there was one.
r.Use(gin.Recovery())

// Per route middleware, you can add as many as you desire.
r.GET("/benchmark", MyBenchLogger(), benchEndpoint)

// Authorization group
// authorized := r.Group("/", AuthRequired())
// exactly the same as:
authorized := r.Group("/")
// per group middleware! in this case we use the custom created
// AuthRequired() middleware just in the "authorized" group.
authorized.Use(AuthRequired())
{
authorized.POST("/login", loginEndpoint)
authorized.POST("/submit", submitEndpoint)
authorized.POST("/read", readEndpoint)

// nested group
testing := authorized.Group("testing")
testing.GET("/analytics", analyticsEndpoint)
}

// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
Read more »

XML/JSON/YAML/ProtoBuf rendering

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
func main() {
r := gin.Default()

// gin.H is a shortcut for map[string]interface{}
r.GET("/someJSON", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
})

r.GET("/moreJSON", func(c *gin.Context) {
// You also can use a struct
var msg struct {
Name string `json:"user"`
Message string
Number int
}
msg.Name = "Lena"
msg.Message = "hey"
msg.Number = 123
// Note that msg.Name becomes "user" in the JSON
// Will output : {"user": "Lena", "Message": "hey", "Number": 123}
c.JSON(http.StatusOK, msg)
})

r.GET("/someXML", func(c *gin.Context) {
c.XML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
})

r.GET("/someYAML", func(c *gin.Context) {
c.YAML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
})

r.GET("/someProtoBuf", func(c *gin.Context) {
reps := []int64{int64(1), int64(2)}
label := "test"
// The specific definition of protobuf is written in the testdata/protoexample file.
data := &protoexample.Test{
Label: &label,
Reps: reps,
}
// Note that data becomes binary data in the response
// Will output protoexample.Test protobuf serialized data
c.ProtoBuf(http.StatusOK, data)
})

// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}

Query and post form

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
router := gin.Default()

router.POST("/post", func(c *gin.Context) {
id := c.Query("id")
page := c.DefaultQuery("page", "0")
name := c.PostForm("name")
message := c.PostForm("message")

fmt.Printf("id: %s; page: %s; name: %s; message: %s", id, page, name, message)
})
router.Run(":8080")
}
Read more »

Grouping routes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
router := gin.Default()

// Simple group: v1
v1 := router.Group("/v1")
{
v1.POST("/login", loginEndpoint)
v1.POST("/submit", submitEndpoint)
v1.POST("/read", readEndpoint)
}

// Simple group: v2
v2 := router.Group("/v2")
{
v2.POST("/login", loginEndpoint)
v2.POST("/submit", submitEndpoint)
v2.POST("/read", readEndpoint)
}

router.Run(":8080")
}
Read more »

Gin 是使用 Go 实现的 HTTP Web 框架。

安装:

1
$ go get -u github.com/gin-gonic/gin

编写 main.go 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

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

func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080
}

运行:

1
2
# run main.go and vist http://0.0.0.0:8080/ping on browser
$ go run main.go

Jupyter Notebook 是基于网页的用于交互计算的应用程序。其可被应用于全过程计算:开发、文档编写、运行代码和展示结果。

JupyterLab 是 Jupyter 主打的最新数据科学生产工具,某种意义上,它的出现是为了取代 Jupyter Notebook。 不过不用担心 Jupyter Notebook 会消失,JupyterLab 包含了 Jupyter Notebook 所有功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
version: '3.7'

services:
jupyter:
# https://jupyter-docker-stacks.readthedocs.io/en/latest/using/selecting.html
image: jupyter/datascience-notebook
restart: always
labels:
- traefik.http.routers.jupyter.rule=Host(`jupyter.yourdomain.com`)
- traefik.http.routers.jupyter.entrypoints=websecure
- traefik.http.routers.jupyter.service=jupyter
- traefik.http.services.jupyter.loadbalancer.server.port=8888
environment:
- JUPYTER_ENABLE_LAB=true
volumes:
- data:/home/jovyan

volumes:
data: