mirror of
https://github.com/LCTT/TranslateProject.git
synced 2024-12-26 21:30:55 +08:00
Merge remote-tracking branch 'LCTT/master'
This commit is contained in:
commit
5f0d5a6bcd
315
published/20180710 Building a Messenger App- Messages.md
Normal file
315
published/20180710 Building a Messenger App- Messages.md
Normal file
@ -0,0 +1,315 @@
|
||||
[#]: collector: (lujun9972)
|
||||
[#]: translator: (gxlct008)
|
||||
[#]: reviewer: (wxy)
|
||||
[#]: publisher: (wxy)
|
||||
[#]: url: (https://linux.cn/article-12680-1.html)
|
||||
[#]: subject: (Building a Messenger App: Messages)
|
||||
[#]: via: (https://nicolasparada.netlify.com/posts/go-messenger-messages/)
|
||||
[#]: author: (Nicolás Parada https://nicolasparada.netlify.com/)
|
||||
|
||||
构建一个即时消息应用(四):消息
|
||||
======
|
||||
|
||||
![](https://img.linux.net.cn/data/attachment/album/202010/04/114458z1p1188epequ686p.jpg)
|
||||
|
||||
本文是该系列的第四篇。
|
||||
|
||||
* [第一篇: 模式][1]
|
||||
* [第二篇: OAuth][2]
|
||||
* [第三篇: 对话][3]
|
||||
|
||||
在这篇文章中,我们将对端点进行编码,以创建一条消息并列出它们,同时还将编写一个端点以更新参与者上次阅读消息的时间。 首先在 `main()` 函数中添加这些路由。
|
||||
|
||||
```
|
||||
router.HandleFunc("POST", "/api/conversations/:conversationID/messages", requireJSON(guard(createMessage)))
|
||||
router.HandleFunc("GET", "/api/conversations/:conversationID/messages", guard(getMessages))
|
||||
router.HandleFunc("POST", "/api/conversations/:conversationID/read_messages", guard(readMessages))
|
||||
```
|
||||
|
||||
消息会进入对话,因此端点包含对话 ID。
|
||||
|
||||
### 创建消息
|
||||
|
||||
该端点处理对 `/api/conversations/{conversationID}/messages` 的 POST 请求,其 JSON 主体仅包含消息内容,并返回新创建的消息。它有两个副作用:更新对话 `last_message_id` 以及更新参与者 `messages_read_at`。
|
||||
|
||||
```
|
||||
func createMessage(w http.ResponseWriter, r *http.Request) {
|
||||
var input struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
defer r.Body.Close()
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
errs := make(map[string]string)
|
||||
input.Content = removeSpaces(input.Content)
|
||||
if input.Content == "" {
|
||||
errs["content"] = "Message content required"
|
||||
} else if len([]rune(input.Content)) > 480 {
|
||||
errs["content"] = "Message too long. 480 max"
|
||||
}
|
||||
if len(errs) != 0 {
|
||||
respond(w, Errors{errs}, http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
authUserID := ctx.Value(keyAuthUserID).(string)
|
||||
conversationID := way.Param(ctx, "conversationID")
|
||||
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
respondError(w, fmt.Errorf("could not begin tx: %v", err))
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
isParticipant, err := queryParticipantExistance(ctx, tx, authUserID, conversationID)
|
||||
if err != nil {
|
||||
respondError(w, fmt.Errorf("could not query participant existance: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if !isParticipant {
|
||||
http.Error(w, "Conversation not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var message Message
|
||||
if err := tx.QueryRowContext(ctx, `
|
||||
INSERT INTO messages (content, user_id, conversation_id) VALUES
|
||||
($1, $2, $3)
|
||||
RETURNING id, created_at
|
||||
`, input.Content, authUserID, conversationID).Scan(
|
||||
&message.ID,
|
||||
&message.CreatedAt,
|
||||
); err != nil {
|
||||
respondError(w, fmt.Errorf("could not insert message: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
UPDATE conversations SET last_message_id = $1
|
||||
WHERE id = $2
|
||||
`, message.ID, conversationID); err != nil {
|
||||
respondError(w, fmt.Errorf("could not update conversation last message ID: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
respondError(w, fmt.Errorf("could not commit tx to create a message: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err = updateMessagesReadAt(nil, authUserID, conversationID); err != nil {
|
||||
log.Printf("could not update messages read at: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
message.Content = input.Content
|
||||
message.UserID = authUserID
|
||||
message.ConversationID = conversationID
|
||||
// TODO: notify about new message.
|
||||
message.Mine = true
|
||||
|
||||
respond(w, message, http.StatusCreated)
|
||||
}
|
||||
```
|
||||
|
||||
首先,它将请求正文解码为包含消息内容的结构。然后,它验证内容不为空并且少于 480 个字符。
|
||||
|
||||
```
|
||||
var rxSpaces = regexp.MustCompile("\\s+")
|
||||
|
||||
func removeSpaces(s string) string {
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
|
||||
lines := make([]string, 0)
|
||||
for _, line := range strings.Split(s, "\n") {
|
||||
line = rxSpaces.ReplaceAllLiteralString(line, " ")
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
```
|
||||
|
||||
这是删除空格的函数。它遍历每一行,删除两个以上的连续空格,然后回非空行。
|
||||
|
||||
验证之后,它将启动一个 SQL 事务。首先,它查询对话中的参与者是否存在。
|
||||
|
||||
```
|
||||
func queryParticipantExistance(ctx context.Context, tx *sql.Tx, userID, conversationID string) (bool, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
var exists bool
|
||||
if err := tx.QueryRowContext(ctx, `SELECT EXISTS (
|
||||
SELECT 1 FROM participants
|
||||
WHERE user_id = $1 AND conversation_id = $2
|
||||
)`, userID, conversationID).Scan(&exists); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return exists, nil
|
||||
}
|
||||
```
|
||||
|
||||
我将其提取到一个函数中,因为稍后可以重用。
|
||||
|
||||
如果用户不是对话参与者,我们将返回一个 `404 NOT Found` 错误。
|
||||
|
||||
然后,它插入消息并更新对话 `last_message_id`。从这时起,由于我们不允许删除消息,因此 `last_message_id` 不能为 `NULL`。
|
||||
|
||||
接下来提交事务,并在 goroutine 中更新参与者 `messages_read_at`。
|
||||
|
||||
```
|
||||
func updateMessagesReadAt(ctx context.Context, userID, conversationID string) error {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
if _, err := db.ExecContext(ctx, `
|
||||
UPDATE participants SET messages_read_at = now()
|
||||
WHERE user_id = $1 AND conversation_id = $2
|
||||
`, userID, conversationID); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
在回复这条新消息之前,我们必须通知一下。这是我们将要在下一篇文章中编写的实时部分,因此我在那里留一了个注释。
|
||||
|
||||
### 获取消息
|
||||
|
||||
这个端点处理对 `/api/conversations/{conversationID}/messages` 的 GET 请求。 它用一个包含会话中所有消息的 JSON 数组进行响应。它还具有更新参与者 `messages_read_at` 的副作用。
|
||||
|
||||
```
|
||||
func getMessages(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
authUserID := ctx.Value(keyAuthUserID).(string)
|
||||
conversationID := way.Param(ctx, "conversationID")
|
||||
|
||||
tx, err := db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
|
||||
if err != nil {
|
||||
respondError(w, fmt.Errorf("could not begin tx: %v", err))
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
isParticipant, err := queryParticipantExistance(ctx, tx, authUserID, conversationID)
|
||||
if err != nil {
|
||||
respondError(w, fmt.Errorf("could not query participant existance: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if !isParticipant {
|
||||
http.Error(w, "Conversation not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := tx.QueryContext(ctx, `
|
||||
SELECT
|
||||
id,
|
||||
content,
|
||||
created_at,
|
||||
user_id = $1 AS mine
|
||||
FROM messages
|
||||
WHERE messages.conversation_id = $2
|
||||
ORDER BY messages.created_at DESC
|
||||
`, authUserID, conversationID)
|
||||
if err != nil {
|
||||
respondError(w, fmt.Errorf("could not query messages: %v", err))
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
messages := make([]Message, 0)
|
||||
for rows.Next() {
|
||||
var message Message
|
||||
if err = rows.Scan(
|
||||
&message.ID,
|
||||
&message.Content,
|
||||
&message.CreatedAt,
|
||||
&message.Mine,
|
||||
); err != nil {
|
||||
respondError(w, fmt.Errorf("could not scan message: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
messages = append(messages, message)
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
respondError(w, fmt.Errorf("could not iterate over messages: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
respondError(w, fmt.Errorf("could not commit tx to get messages: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err = updateMessagesReadAt(nil, authUserID, conversationID); err != nil {
|
||||
log.Printf("could not update messages read at: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
respond(w, messages, http.StatusOK)
|
||||
}
|
||||
```
|
||||
|
||||
首先,它以只读模式开始一个 SQL 事务。检查参与者是否存在,并查询所有消息。在每条消息中,我们使用当前经过身份验证的用户 ID 来了解用户是否拥有该消息(`mine`)。 然后,它提交事务,在 goroutine 中更新参与者 `messages_read_at` 并以消息响应。
|
||||
|
||||
### 读取消息
|
||||
|
||||
该端点处理对 `/api/conversations/{conversationID}/read_messages` 的 POST 请求。 没有任何请求或响应主体。 在前端,每次有新消息到达实时流时,我们都会发出此请求。
|
||||
|
||||
```
|
||||
func readMessages(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
authUserID := ctx.Value(keyAuthUserID).(string)
|
||||
conversationID := way.Param(ctx, "conversationID")
|
||||
|
||||
if err := updateMessagesReadAt(ctx, authUserID, conversationID); err != nil {
|
||||
respondError(w, fmt.Errorf("could not update messages read at: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
```
|
||||
|
||||
它使用了与更新参与者 `messages_read_at` 相同的函数。
|
||||
|
||||
* * *
|
||||
|
||||
到此为止。实时消息是后台仅剩的部分了。请等待下一篇文章。
|
||||
|
||||
- [源代码][4]
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
via: https://nicolasparada.netlify.com/posts/go-messenger-messages/
|
||||
|
||||
作者:[Nicolás Parada][a]
|
||||
选题:[lujun9972][b]
|
||||
译者:[gxlct008](https://github.com/gxlct008)
|
||||
校对:[wxy](https://github.com/wxy)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||
|
||||
[a]: https://nicolasparada.netlify.com/
|
||||
[b]: https://github.com/lujun9972
|
||||
[1]: https://linux.cn/article-11396-1.html
|
||||
[2]: https://linux.cn/article-11510-1.html
|
||||
[3]: https://linux.cn/article-12056-1.html
|
||||
[4]: https://github.com/nicolasparada/go-messenger-demo
|
@ -1,8 +1,8 @@
|
||||
[#]: collector: "lujun9972"
|
||||
[#]: translator: "lxbwolf"
|
||||
[#]: reviewer: " "
|
||||
[#]: publisher: " "
|
||||
[#]: url: " "
|
||||
[#]: reviewer: "wxy"
|
||||
[#]: publisher: "wxy"
|
||||
[#]: url: "https://linux.cn/article-12681-1.html"
|
||||
[#]: subject: "Find security issues in Go code using gosec"
|
||||
[#]: via: "https://opensource.com/article/20/9/gosec"
|
||||
[#]: author: "Gaurav Kamathe https://opensource.com/users/gkamathe"
|
||||
@ -10,20 +10,21 @@
|
||||
使用 gosec 检查 Go 代码中的安全问题
|
||||
======
|
||||
|
||||
来学习下 Golang 的安全检查工具 gosec。
|
||||
![A lock on the side of a building][1]
|
||||
> 来学习下 Go 语言的安全检查工具 gosec。
|
||||
|
||||
![](https://img.linux.net.cn/data/attachment/album/202010/04/125129bh4qxxsyqpvqjtx4.jpg)
|
||||
|
||||
[Go 语言][2]写的代码越来越常见,尤其是在容器、Kubernetes 或云生态相关的开发中。Docker 是最早采用 Golang 的项目之一,随后是 Kubernetes,之后大量的新项目在众多编程语言中选择了 Go。
|
||||
|
||||
像其他语言一样,Go 也有它的长处和短处(如安全缺陷)。这些缺陷可能会因为语言本身的限制在程序员编码不当时出现,例如,C 代码中的内存安全问题。
|
||||
像其他语言一样,Go 也有它的长处和短处(如安全缺陷)。这些缺陷可能会因为语言本身的缺陷加上程序员编码不当而产生,例如,C 代码中的内存安全问题。
|
||||
|
||||
无论它们出现的原因是什么,安全问题都应该在开发过程中尽早修复,以免在封装好的软件中出现。幸运的是,静态分析工具可以帮你批量地处理这些问题。静态分析工具通过解析用某种编程语言写的代码来找到问题。
|
||||
无论它们出现的原因是什么,安全问题都应该在开发过程的早期修复,以免在封装好的软件中出现。幸运的是,静态分析工具可以帮你以更可重复的方式处理这些问题。静态分析工具通过解析用某种编程语言写的代码来找到问题。
|
||||
|
||||
这类工具中很多被称为 linter。传统意义上,linter 更注重的是检查代码中编码问题、bug、代码风格之类的问题,不会检查安全问题。例如,[Coverity][3] 是很受欢迎的用来检查 C/C++ 代码问题的工具。然而,有工具专门用来检查源码中的安全问题。例如,[Bandit][4] 用来检查 Python 代码中的安全缺陷。[gosec][5] 用来搜寻 Go 源码中的安全缺陷。gosec 通过扫描 Go 的 AST(<ruby>抽象语法树<rt>abstract syntax tree</rt></ruby>)来检查源码中的安全问题。
|
||||
这类工具中很多被称为 linter。传统意义上,linter 更注重的是检查代码中编码问题、bug、代码风格之类的问题,它们可能不会发现代码中的安全问题。例如,[Coverity][3] 是一个很流行的工具,它可以帮助寻找 C/C++ 代码中的问题。然而,也有一些工具专门用来检查源码中的安全问题。例如,[Bandit][4] 可以检查 Python 代码中的安全缺陷。而 [gosec][5] 则用来搜寻 Go 源码中的安全缺陷。`gosec` 通过扫描 Go 的 AST(<ruby>抽象语法树<rt>abstract syntax tree</rt></ruby>)来检查源码中的安全问题。
|
||||
|
||||
### 开始使用 gosec
|
||||
|
||||
在开始学习和使用 gosec 之前,你需要准备一个 Go 语言写的项目。有这么多开源软件,我相信这不是问题。你可以在 GitHub 的 [Golang 库排行榜]][6]中找一个。
|
||||
在开始学习和使用 `gosec` 之前,你需要准备一个 Go 语言写的项目。有这么多开源软件,我相信这不是问题。你可以在 GitHub 的 [热门 Golang 仓库][6]中找一个。
|
||||
|
||||
本文中,我随机选了 [Docker CE][7] 项目,但你可以选择任意的 Go 项目。
|
||||
|
||||
@ -31,57 +32,45 @@
|
||||
|
||||
如果你还没安装 Go,你可以先从仓库中拉取下来。如果你用的是 Fedora 或其他基于 RPM 的 Linux 发行版本:
|
||||
|
||||
|
||||
```
|
||||
`$ dnf install golang.x86_64`
|
||||
$ dnf install golang.x86_64
|
||||
```
|
||||
|
||||
如果你用的是其他操作系统,请参照 [Golang 安装][8]页面。
|
||||
|
||||
使用 `version` 参数来验证 Go 是否安装成功:
|
||||
|
||||
|
||||
```
|
||||
$ go version
|
||||
go version go1.14.6 linux/amd64
|
||||
$
|
||||
```
|
||||
|
||||
运行 `go get` 命令就可以轻松地安装 gosec:
|
||||
|
||||
运行 `go get` 命令就可以轻松地安装 `gosec`:
|
||||
|
||||
```
|
||||
$ go get github.com/securego/gosec/cmd/gosec
|
||||
$
|
||||
```
|
||||
|
||||
上面这行命令会从 GitHub 下载 gosec 的源码、编译并安装到指定位置。在仓库的 README 中你还可以看到[安装工具的其他方法][9]。
|
||||
|
||||
gosec 的源码会被下载到 `$GOPATH` 的位置,编译出的二进制文件会被安装到你系统上设置的 `bin` 目录下。你可以运行下面的命令来查看 `$GOPATH` 和 `$GOBIN` 目录:
|
||||
上面这行命令会从 GitHub 下载 `gosec` 的源码,编译并安装到指定位置。在仓库的 `README` 中你还可以看到[安装该工具的其他方法][9]。
|
||||
|
||||
`gosec` 的源码会被下载到 `$GOPATH` 的位置,编译出的二进制文件会被安装到你系统上设置的 `bin` 目录下。你可以运行下面的命令来查看 `$GOPATH` 和 `$GOBIN` 目录:
|
||||
|
||||
```
|
||||
$ go env | grep GOBIN
|
||||
GOBIN="/root/go/gobin"
|
||||
$
|
||||
$ go env | grep GOPATH
|
||||
GOPATH="/root/go"
|
||||
$
|
||||
```
|
||||
|
||||
如果 `go get` 命令执行成功,那么 gosec 二进制应该就可以使用了:
|
||||
|
||||
如果 `go get` 命令执行成功,那么 `gosec` 二进制应该就可以使用了:
|
||||
|
||||
```
|
||||
$
|
||||
$ ls -l ~/go/bin/
|
||||
total 9260
|
||||
-rwxr-xr-x. 1 root root 9482175 Aug 20 04:17 gosec
|
||||
$
|
||||
```
|
||||
|
||||
你可以把 `$GOPATH` 下的 `bin` 目录添加到 `$PATH` 中。这样你就可以像使用系统上的其他命令一样来使用 gosec 命令行工具(CLI)了。
|
||||
|
||||
你可以把 `$GOPATH` 下的 `bin` 目录添加到 `$PATH` 中。这样你就可以像使用系统上的其他命令一样来使用 `gosec` 命令行工具(CLI)了。
|
||||
|
||||
```
|
||||
$ which gosec
|
||||
@ -89,8 +78,7 @@ $ which gosec
|
||||
$
|
||||
```
|
||||
|
||||
使用 gosec 命令行工具的 `-help` 选项来看看运行是否符合预期:
|
||||
|
||||
使用 `gosec` 命令行工具的 `-help` 选项来看看运行是否符合预期:
|
||||
|
||||
```
|
||||
$ gosec -help
|
||||
@ -109,17 +97,12 @@ USAGE:
|
||||
|
||||
之后,创建一个目录,把源码下载到这个目录作为实例项目(本例中,我用的是 Docker CE):
|
||||
|
||||
|
||||
```
|
||||
$ mkdir gosec-demo
|
||||
$
|
||||
$ cd gosec-demo/
|
||||
$
|
||||
$ pwd
|
||||
/root/gosec-demo
|
||||
$
|
||||
|
||||
$ git clone <https://github.com/docker/docker-ce.git>
|
||||
$ git clone https://github.com/docker/docker-ce.git
|
||||
Cloning into 'docker-ce'...
|
||||
remote: Enumerating objects: 1271, done.
|
||||
remote: Counting objects: 100% (1271/1271), done.
|
||||
@ -128,10 +111,9 @@ remote: Total 431003 (delta 384), reused 981 (delta 318), pack-reused 429732
|
||||
Receiving objects: 100% (431003/431003), 166.84 MiB | 28.94 MiB/s, done.
|
||||
Resolving deltas: 100% (221338/221338), done.
|
||||
Updating files: 100% (10861/10861), done.
|
||||
$
|
||||
```
|
||||
|
||||
代码统计工具(本例中用的是 cloc)显示这个项目大部分是用 Go 写的,恰好迎合了 gosec 的功能。
|
||||
代码统计工具(本例中用的是 `cloc`)显示这个项目大部分是用 Go 写的,恰好迎合了 `gosec` 的功能。
|
||||
|
||||
|
||||
```
|
||||
@ -140,9 +122,10 @@ $ ./cloc /root/gosec-demo/docker-ce/
|
||||
8724 unique files.
|
||||
2560 files ignored.
|
||||
|
||||
\-----------------------------------------------------------------------------------
|
||||
|
||||
-----------------------------------------------------------------------------------
|
||||
Language files blank comment code
|
||||
\-----------------------------------------------------------------------------------
|
||||
-----------------------------------------------------------------------------------
|
||||
Go 7222 190785 230478 1574580
|
||||
YAML 37 4831 817 156762
|
||||
Markdown 529 21422 0 67893
|
||||
@ -151,13 +134,11 @@ Protocol Buffers 149 5014 16562 10
|
||||
|
||||
### 使用默认选项运行 gosec
|
||||
|
||||
在 Docker CE 项目中使用默认选项运行 gosec,执行 `gosec ./...` 命令。屏幕上会有很多输出内容。在末尾你会看到一个简短的 `Summary`,列出了浏览的文件数、所有文件的总行数,以及源码中发现的问题数。
|
||||
|
||||
在 Docker CE 项目中使用默认选项运行 `gosec`,执行 `gosec ./...` 命令。屏幕上会有很多输出内容。在末尾你会看到一个简短的 “Summary”,列出了浏览的文件数、所有文件的总行数,以及源码中发现的问题数。
|
||||
|
||||
```
|
||||
$ pwd
|
||||
/root/gosec-demo/docker-ce
|
||||
$
|
||||
$ time gosec ./...
|
||||
[gosec] 2020/08/20 04:44:15 Including rules: default
|
||||
[gosec] 2020/08/20 04:44:15 Excluding rules: default
|
||||
@ -183,180 +164,166 @@ $
|
||||
|
||||
滚动屏幕你会看到不同颜色高亮的行:红色表示需要尽快查看的高优先级问题,黄色表示中优先级的问题。
|
||||
|
||||
#### 关于“假阳性”
|
||||
#### 关于误判
|
||||
|
||||
在开始检查代码之前,我想先分享几条基本原则。默认情况下,静态检查工具会基于一系列的规则对测试代码进行分析并报告出检查出来的*所有*问题。这表示工具报出来的每一个问题都需要修复吗?非也。这个问题最好的解答者是设计和开发这个软件的人。他们最熟悉代码,更重要的是,他们了解软件会在什么环境下部署以及会被怎样使用。
|
||||
在开始检查代码之前,我想先分享几条基本原则。默认情况下,静态检查工具会基于一系列的规则对测试代码进行分析,并报告出它们发现的*所有*问题。这是否意味着工具报出来的每一个问题都需要修复?非也。这个问题最好的解答者是设计和开发这个软件的人。他们最熟悉代码,更重要的是,他们了解软件会在什么环境下部署以及会被怎样使用。
|
||||
|
||||
这个知识点对于判定工具标记出来的某段代码到底是不是安全缺陷至关重要。随着工作时间和经验的积累,你会慢慢学会怎样让静态分析工具忽略非安全缺陷,使报告内容的可执行性更高。因此,要判定 gosec 报出来的某个问题是否需要修复,让一名有经验的开发者对源码做人工审计会是比较好的办法。
|
||||
这个知识点对于判定工具标记出来的某段代码到底是不是安全缺陷至关重要。随着工作时间和经验的积累,你会慢慢学会怎样让静态分析工具忽略非安全缺陷,使报告内容的可执行性更高。因此,要判定 `gosec` 报出来的某个问题是否需要修复,让一名有经验的开发者对源码做人工审计会是比较好的办法。
|
||||
|
||||
#### 高优先级问题
|
||||
|
||||
从输出内容看,gosec 发现了 Docker CE 的一个高优先级问题,它使用的是低版本的 TLS(<ruby>传输层安全<rt>Transport Layer Security<rt></ruby>)。无论什么时候,使用软件和库的最新版本都是确保它更新及时、没有安全问题的最好的方法。
|
||||
|
||||
从输出内容看,`gosec` 发现了 Docker CE 的一个高优先级问题,它使用的是低版本的 TLS(<ruby>传输层安全<rt>Transport Layer Security<rt></ruby>)。无论什么时候,使用软件和库的最新版本都是确保它更新及时、没有安全问题的最好的方法。
|
||||
|
||||
```
|
||||
[/root/gosec-demo/docker-ce/components/engine/daemon/logger/splunk/splunk.go:173] - G402 (CWE-295): TLS MinVersion too low. (Confidence: HIGH, Severity: HIGH)
|
||||
172:
|
||||
> 173: tlsConfig := &tls.Config{}
|
||||
> 173: tlsConfig := &tls.Config{}
|
||||
174:
|
||||
```
|
||||
|
||||
它还发现了一个伪随机数生成器。它是不是一个安全缺陷,取决于生成的随机数的使用方式。
|
||||
|
||||
它还发现了一个弱随机数生成器。它是不是一个安全缺陷,取决于生成的随机数的使用方式。
|
||||
|
||||
```
|
||||
[/root/gosec-demo/docker-ce/components/engine/pkg/namesgenerator/names-generator.go:843] - G404 (CWE-338): Use of weak random number generator (math/rand instead of crypto/rand) (Confidence: MEDIUM, Severity: HIGH)
|
||||
842: begin:
|
||||
> 843: name := fmt.Sprintf("%s_%s", left[rand.Intn(len(left))], right[rand.Intn(len(right))])
|
||||
> 843: name := fmt.Sprintf("%s_%s", left[rand.Intn(len(left))], right[rand.Intn(len(right))])
|
||||
844: if name == "boring_wozniak" /* Steve Wozniak is not boring */ {
|
||||
```
|
||||
|
||||
#### 中优先级问题
|
||||
|
||||
这个工具还发现了一些中优先级问题。它标记了一个通过与 tar 相关的解压炸弹这种方式实现的潜在的 DoS 威胁,这种方式可能会被恶意的攻击者利用。
|
||||
|
||||
这个工具还发现了一些中优先级问题。它标记了一个通过与 `tar` 相关的解压炸弹这种方式实现的潜在的 DoS 威胁,这种方式可能会被恶意的攻击者利用。
|
||||
|
||||
```
|
||||
[/root/gosec-demo/docker-ce/components/engine/pkg/archive/copy.go:357] - G110 (CWE-409): Potential DoS vulnerability via decompression bomb (Confidence: MEDIUM, Severity: MEDIUM)
|
||||
356:
|
||||
> 357: if _, err = io.Copy(rebasedTar, srcTar); err != nil {
|
||||
> 357: if _, err = io.Copy(rebasedTar, srcTar); err != nil {
|
||||
358: w.CloseWithError(err)
|
||||
```
|
||||
|
||||
它还发现了一个通过变量访问文件的问题。如果恶意使用者能访问这个变量,那么他们就可以改变变量的值去读其他文件。
|
||||
|
||||
|
||||
```
|
||||
[/root/gosec-demo/docker-ce/components/cli/cli/context/tlsdata.go:80] - G304 (CWE-22): Potential file inclusion via variable (Confidence: HIGH, Severity: MEDIUM)
|
||||
79: if caPath != "" {
|
||||
> 80: if ca, err = ioutil.ReadFile(caPath); err != nil {
|
||||
> 80: if ca, err = ioutil.ReadFile(caPath); err != nil {
|
||||
81: return nil, err
|
||||
```
|
||||
|
||||
文件和目录通常是操作系统安全的最基础的元素。这里,gosec 报出了一个可能需要你检查目录的权限是否安全的问题。
|
||||
|
||||
文件和目录通常是操作系统安全的最基础的元素。这里,`gosec` 报出了一个可能需要你检查目录的权限是否安全的问题。
|
||||
|
||||
```
|
||||
[/root/gosec-demo/docker-ce/components/engine/contrib/apparmor/main.go:41] - G301 (CWE-276): Expect directory permissions to be 0750 or less (Confidence: HIGH, Severity: MEDIUM)
|
||||
40: // make sure /etc/apparmor.d exists
|
||||
> 41: if err := os.MkdirAll(path.Dir(apparmorProfilePath), 0755); err != nil {
|
||||
> 41: if err := os.MkdirAll(path.Dir(apparmorProfilePath), 0755); err != nil {
|
||||
42: log.Fatal(err)
|
||||
```
|
||||
|
||||
你经常需要在源码中启动命令行工具。Go 使用内建的 exec 库来实现。仔细地分析用来调用这些工具的变量,就能发现安全缺陷。
|
||||
|
||||
|
||||
```
|
||||
[/root/gosec-demo/docker-ce/components/engine/testutil/fakestorage/fixtures.go:59] - G204 (CWE-78): Subprocess launched with variable (Confidence: HIGH, Severity: MEDIUM)
|
||||
58:
|
||||
> 59: cmd := exec.Command(goCmd, "build", "-o", filepath.Join(tmp, "httpserver"), "github.com/docker/docker/contrib/httpserver")
|
||||
> 59: cmd := exec.Command(goCmd, "build", "-o", filepath.Join(tmp, "httpserver"), "github.com/docker/docker/contrib/httpserver")
|
||||
60: cmd.Env = append(os.Environ(), []string{
|
||||
```
|
||||
|
||||
#### 低优先级问题
|
||||
|
||||
在这个输出中,gosec 报出了一个 “unsafe” 调用相关的低优先级问题,这个调用会绕开 Go 提供的内存保护。再仔细分析下你调用 “unsafe” 的方式,看看是否有被别人利用的可能性。
|
||||
|
||||
在这个输出中,gosec 报出了一个 `unsafe` 调用相关的低优先级问题,这个调用会绕开 Go 提供的内存保护。再仔细分析下你调用 `unsafe` 的方式,看看是否有被别人利用的可能性。
|
||||
|
||||
```
|
||||
[/root/gosec-demo/docker-ce/components/engine/pkg/archive/changes_linux.go:264] - G103 (CWE-242): Use of unsafe calls should be audited (Confidence: HIGH, Severity: LOW)
|
||||
263: for len(buf) > 0 {
|
||||
> 264: dirent := (*unix.Dirent)(unsafe.Pointer(&buf[0]))
|
||||
263: for len(buf) > 0 {
|
||||
> 264: dirent := (*unix.Dirent)(unsafe.Pointer(&buf[0]))
|
||||
265: buf = buf[dirent.Reclen:]
|
||||
|
||||
|
||||
|
||||
[/root/gosec-demo/docker-ce/components/engine/pkg/devicemapper/devmapper_wrapper.go:88] - G103 (CWE-242): Use of unsafe calls should be audited (Confidence: HIGH, Severity: LOW)
|
||||
87: func free(p *C.char) {
|
||||
> 88: C.free(unsafe.Pointer(p))
|
||||
> 88: C.free(unsafe.Pointer(p))
|
||||
89: }
|
||||
```
|
||||
|
||||
它还标记了源码中未处理的错误。源码中出现的错误你都应该处理。
|
||||
|
||||
|
||||
```
|
||||
[/root/gosec-demo/docker-ce/components/cli/cli/command/image/build/context.go:172] - G104 (CWE-703): Errors unhandled. (Confidence: HIGH, Severity: LOW)
|
||||
171: err := tar.Close()
|
||||
> 172: os.RemoveAll(dockerfileDir)
|
||||
> 172: os.RemoveAll(dockerfileDir)
|
||||
173: return err
|
||||
```
|
||||
|
||||
### 自定义 gosec 扫描
|
||||
|
||||
使用 gosec 的默认选项带来了很多的问题。然而,经过人工审计和随着时间推移,你会掌握哪些问题是不需要标记的。你可以自己指定排除和包含哪些测试。
|
||||
使用 `gosec` 的默认选项会带来很多的问题。然而,经过人工审计,随着时间推移你会掌握哪些问题是不需要标记的。你可以自己指定排除和包含哪些测试。
|
||||
|
||||
我上面提到过,gosec 是基于一系列的规则从 Go 源码中查找问题的。下面是它使用的完整的[规则][10]列表:
|
||||
|
||||
* G101:查找硬编码凭证
|
||||
我上面提到过,`gosec` 是基于一系列的规则从 Go 源码中查找问题的。下面是它使用的完整的[规则][10]列表:
|
||||
|
||||
- G101:查找硬编码凭证
|
||||
- G102:绑定到所有接口
|
||||
- G103:审计不安全区块的使用
|
||||
- G103:审计 `unsafe` 块的使用
|
||||
- G104:审计未检查的错误
|
||||
- G106:审计 ssh.InsecureIgnoreHostKey 的使用
|
||||
- G107: 提供给 HTTP 请求的 url 作为污点输入
|
||||
- G108: 统计端点自动暴露到 /debug/pprof
|
||||
- G109: strconv.Atoi 转换到 int16 或 int32 时潜在的整数溢出
|
||||
- G110: 潜在的通过解压炸弹实现的 DoS
|
||||
- G106:审计 `ssh.InsecureIgnoreHostKey` 的使用
|
||||
- G107: 提供给 HTTP 请求的 url 作为污点输入
|
||||
- G108: `/debug/pprof` 上自动暴露的剖析端点
|
||||
- G109: `strconv.Atoi` 转换到 int16 或 int32 时潜在的整数溢出
|
||||
- G110: 潜在的通过解压炸弹实现的 DoS
|
||||
- G201:SQL 查询构造使用格式字符串
|
||||
- G202:SQL 查询构造使用字符串连接
|
||||
- G203:在 HTML 模板中使用未转义的数据
|
||||
- G203:在HTML模板中使用未转义的数据
|
||||
- G204:审计命令执行情况
|
||||
- G301:创建目录时文件权限分配不合理
|
||||
- G302:chmod 文件权限分配不合理
|
||||
- G302:使用 `chmod` 时文件权限分配不合理
|
||||
- G303:使用可预测的路径创建临时文件
|
||||
- G304:作为污点输入提供的文件路径
|
||||
- G304:通过污点输入提供的文件路径
|
||||
- G305:提取 zip/tar 文档时遍历文件
|
||||
- G306: 写到新文件时文件权限分配不合理
|
||||
- G307: 把返回错误的函数放到 defer 内
|
||||
- G401:检测 DES、RC4、MD5 或 SHA1 的使用情况
|
||||
- G306: 写到新文件时文件权限分配不合理
|
||||
- G307: 把返回错误的函数放到 `defer` 内
|
||||
- G401:检测 DES、RC4、MD5 或 SHA1 的使用
|
||||
- G402:查找错误的 TLS 连接设置
|
||||
- G403:确保最小 RSA 密钥长度为 2048 位
|
||||
- G404:不安全的随机数源(rand)
|
||||
- G404:不安全的随机数源(`rand`)
|
||||
- G501:导入黑名单列表:crypto/md5
|
||||
- G502:导入黑名单列表:crypto/des
|
||||
- G503:导入黑名单列表:crypto/rc4
|
||||
- G504:导入黑名单列表:net/http/cgi
|
||||
- G505:导入黑名单列表:crypto/sha1
|
||||
- G601: 在 range 语句中使用隐式的元素别名
|
||||
|
||||
|
||||
- G601: 在 `range` 语句中使用隐式的元素别名
|
||||
|
||||
#### 排除指定的测试
|
||||
|
||||
你可以自定义 gosec 来避免对已知为安全的问题进行扫描和报告。你可以使用 `-exclude` 选项和上面的规则编号来忽略指定的问题。
|
||||
|
||||
例如,如果你不想让 gosec 检查源码中硬编码凭证相关的未处理的错误,那么你可以运行下面的命令来忽略这些错误:
|
||||
你可以自定义 `gosec` 来避免对已知为安全的问题进行扫描和报告。你可以使用 `-exclude` 选项和上面的规则编号来忽略指定的问题。
|
||||
|
||||
例如,如果你不想让 `gosec` 检查源码中硬编码凭证相关的未处理的错误,那么你可以运行下面的命令来忽略这些错误:
|
||||
|
||||
```
|
||||
$ gosec -exclude=G104 ./...
|
||||
$ gosec -exclude=G104,G101 ./...
|
||||
```
|
||||
|
||||
有时候你知道某段代码是安全的,但是 gosec 还是会报出问题。然而,你又不想完全排除掉整个检查,因为你想让 gosec 检查新增的代码。通过在你已知为安全的代码块添加 `#nosec` 标记可以避免 gosec 扫描。这样 gosec 会继续扫描新增代码,而忽略掉 `#nosec` 标记的代码块。
|
||||
有时候你知道某段代码是安全的,但是 `gosec` 还是会报出问题。然而,你又不想完全排除掉整个检查,因为你想让 `gosec` 检查新增的代码。通过在你已知为安全的代码块添加 `#nosec` 标记可以避免 `gosec` 扫描。这样 `gosec` 会继续扫描新增代码,而忽略掉 `#nosec` 标记的代码块。
|
||||
|
||||
#### 运行指定的检查
|
||||
|
||||
另一方面,如果你只想检查指定的问题,你可以通过 `-include` 选项和规则编号来告诉 gosec 运行哪些检查:
|
||||
|
||||
另一方面,如果你只想检查指定的问题,你可以通过 `-include` 选项和规则编号来告诉 `gosec` 运行哪些检查:
|
||||
|
||||
```
|
||||
`$ gosec -include=G201,G202 ./...`
|
||||
$ gosec -include=G201,G202 ./...
|
||||
```
|
||||
|
||||
#### 扫描测试文件
|
||||
|
||||
Go 语言自带对测试的支持,通过单元测试来检验一个元素是否符合预期。在默认模式下,gosec 会忽略测试文件,你可以使用 `-tests` 选项把它们包含进来:
|
||||
|
||||
Go 语言自带对测试的支持,通过单元测试来检验一个元素是否符合预期。在默认模式下,`gosec` 会忽略测试文件,你可以使用 `-tests` 选项把它们包含进来:
|
||||
|
||||
```
|
||||
`gosec -tests ./...`
|
||||
gosec -tests ./...
|
||||
```
|
||||
|
||||
#### 修改输出的格式
|
||||
|
||||
找出问题只是它的一半功能;另一半功能是把它检查到的问题以用户友好同时又方便工具处理的方式报告出来。幸运的是,gosec 可以用不同的方式输出。例如,如果你想看 JSON 格式的报告,那么就使用 `-fmt` 选项指定 JSON 格式并把结果保存到 `results.json` 文件中:
|
||||
|
||||
找出问题只是它的一半功能;另一半功能是把它检查到的问题以用户友好同时又方便工具处理的方式报告出来。幸运的是,`gosec` 可以用不同的方式输出。例如,如果你想看 JSON 格式的报告,那么就使用 `-fmt` 选项指定 JSON 格式并把结果保存到 `results.json` 文件中:
|
||||
|
||||
```
|
||||
$ gosec -fmt=json -out=results.json ./...
|
||||
@ -370,7 +337,7 @@ $
|
||||
"confidence": "HIGH",
|
||||
"cwe": {
|
||||
"ID": "242",
|
||||
"URL": "<https://cwe.mitre.org/data/definitions/242.html>"
|
||||
"URL": "https://cwe.mitre.org/data/definitions/242.html"
|
||||
},
|
||||
"rule_id": "G103",
|
||||
"details": "Use of unsafe calls should be audited",
|
||||
@ -381,9 +348,9 @@ $
|
||||
},
|
||||
```
|
||||
|
||||
### 用 gosec 检查容易暴露出来的问题
|
||||
### 用 gosec 检查容易被发现的问题
|
||||
|
||||
静态检查工具不能完全代替人工代码审计。然而,当代码量变大、有众多开发者时,这样的工具通常能用批量的方式帮忙找出容易暴露的问题。它对于帮助新开发者识别和在编码时避免引入这些安全缺陷很有用。
|
||||
静态检查工具不能完全代替人工代码审计。然而,当代码量变大、有众多开发者时,这样的工具往往有助于以可重复的方式找出容易被发现的问题。它对于帮助新开发者识别和在编码时避免引入这些安全缺陷很有用。
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@ -392,7 +359,7 @@ via: https://opensource.com/article/20/9/gosec
|
||||
作者:[Gaurav Kamathe][a]
|
||||
选题:[lujun9972][b]
|
||||
译者:[lxbowlf](https://github.com/lxbwolf)
|
||||
校对:[校对者ID](https://github.com/校对者ID)
|
||||
校对:[wxy](https://github.com/wxy)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||
|
@ -1,5 +1,5 @@
|
||||
[#]: collector: (lujun9972)
|
||||
[#]: translator: ( )
|
||||
[#]: translator: (gxlct008)
|
||||
[#]: reviewer: ( )
|
||||
[#]: publisher: ( )
|
||||
[#]: url: ( )
|
||||
|
@ -1,314 +0,0 @@
|
||||
[#]: collector: (lujun9972)
|
||||
[#]: translator: (gxlct008)
|
||||
[#]: reviewer: ( )
|
||||
[#]: publisher: ( )
|
||||
[#]: url: ( )
|
||||
[#]: subject: (Building a Messenger App: Messages)
|
||||
[#]: via: (https://nicolasparada.netlify.com/posts/go-messenger-messages/)
|
||||
[#]: author: (Nicolás Parada https://nicolasparada.netlify.com/)
|
||||
|
||||
构建一个即时消息应用(四):消息
|
||||
======
|
||||
|
||||
本文是该系列的第四篇。
|
||||
|
||||
* [第一篇: 模式][1]
|
||||
* [第二篇: OAuth][2]
|
||||
* [第三篇: 对话Conversations][3]
|
||||
|
||||
|
||||
在这篇文章中,我们将对端点进行编码以创建一条消息并列出它们,同时还将编写一个端点以更新参与者上次阅读消息的时间。 首先在 `main()` 函数中添加这些路由。
|
||||
|
||||
```
|
||||
router.HandleFunc("POST", "/api/conversations/:conversationID/messages", requireJSON(guard(createMessage)))
|
||||
router.HandleFunc("GET", "/api/conversations/:conversationID/messages", guard(getMessages))
|
||||
router.HandleFunc("POST", "/api/conversations/:conversationID/read_messages", guard(readMessages))
|
||||
```
|
||||
|
||||
消息进入对话,因此端点包含对话 ID。
|
||||
|
||||
### 创建消息
|
||||
|
||||
该端点使用仅包含消息内容的 JSON 主体处理对 `/api/conversations/{conversationID}/messages` 的 POST 请求,并返回新创建的消息。 它有两个副作用:更新对话 `last_message_id` 以及更新参与者 `messages_read_at`。
|
||||
|
||||
```
|
||||
func createMessage(w http.ResponseWriter, r *http.Request) {
|
||||
var input struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
defer r.Body.Close()
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
errs := make(map[string]string)
|
||||
input.Content = removeSpaces(input.Content)
|
||||
if input.Content == "" {
|
||||
errs["content"] = "Message content required"
|
||||
} else if len([]rune(input.Content)) > 480 {
|
||||
errs["content"] = "Message too long. 480 max"
|
||||
}
|
||||
if len(errs) != 0 {
|
||||
respond(w, Errors{errs}, http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
authUserID := ctx.Value(keyAuthUserID).(string)
|
||||
conversationID := way.Param(ctx, "conversationID")
|
||||
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
respondError(w, fmt.Errorf("could not begin tx: %v", err))
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
isParticipant, err := queryParticipantExistance(ctx, tx, authUserID, conversationID)
|
||||
if err != nil {
|
||||
respondError(w, fmt.Errorf("could not query participant existance: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if !isParticipant {
|
||||
http.Error(w, "Conversation not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var message Message
|
||||
if err := tx.QueryRowContext(ctx, `
|
||||
INSERT INTO messages (content, user_id, conversation_id) VALUES
|
||||
($1, $2, $3)
|
||||
RETURNING id, created_at
|
||||
`, input.Content, authUserID, conversationID).Scan(
|
||||
&message.ID,
|
||||
&message.CreatedAt,
|
||||
); err != nil {
|
||||
respondError(w, fmt.Errorf("could not insert message: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
UPDATE conversations SET last_message_id = $1
|
||||
WHERE id = $2
|
||||
`, message.ID, conversationID); err != nil {
|
||||
respondError(w, fmt.Errorf("could not update conversation last message ID: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
respondError(w, fmt.Errorf("could not commit tx to create a message: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err = updateMessagesReadAt(nil, authUserID, conversationID); err != nil {
|
||||
log.Printf("could not update messages read at: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
message.Content = input.Content
|
||||
message.UserID = authUserID
|
||||
message.ConversationID = conversationID
|
||||
// TODO: notify about new message.
|
||||
message.Mine = true
|
||||
|
||||
respond(w, message, http.StatusCreated)
|
||||
}
|
||||
```
|
||||
|
||||
首先,它将请求正文解码为具有消息内容的结构。然后,它验证内容不为空并且少于 480 个字符。
|
||||
|
||||
```
|
||||
var rxSpaces = regexp.MustCompile("\\s+")
|
||||
|
||||
func removeSpaces(s string) string {
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
|
||||
lines := make([]string, 0)
|
||||
for _, line := range strings.Split(s, "\n") {
|
||||
line = rxSpaces.ReplaceAllLiteralString(line, " ")
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
```
|
||||
|
||||
这是删除空格的函数。它迭代每一行,删除两个以上的连续空格,然后回非空行。
|
||||
|
||||
验证之后,它将启动一个 SQL 事务。首先,它查询对话中的参与者是否存在。
|
||||
|
||||
```
|
||||
func queryParticipantExistance(ctx context.Context, tx *sql.Tx, userID, conversationID string) (bool, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
var exists bool
|
||||
if err := tx.QueryRowContext(ctx, `SELECT EXISTS (
|
||||
SELECT 1 FROM participants
|
||||
WHERE user_id = $1 AND conversation_id = $2
|
||||
)`, userID, conversationID).Scan(&exists); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return exists, nil
|
||||
}
|
||||
```
|
||||
|
||||
我将其提取到一个函数中,因为稍后可以重用。
|
||||
|
||||
如果用户不是对话参与者,我们将返回一个 `404 NOT Found` 错误。
|
||||
|
||||
然后,它插入消息并更新对话 `last_message_id`。从这时起,由于我们不允许删除消息,因此 `last_message_id` 不能为 `NULL`。
|
||||
|
||||
接下来提交事务,并在 goroutine 中更新参与者 `messages_read_at`。
|
||||
|
||||
```
|
||||
func updateMessagesReadAt(ctx context.Context, userID, conversationID string) error {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
if _, err := db.ExecContext(ctx, `
|
||||
UPDATE participants SET messages_read_at = now()
|
||||
WHERE user_id = $1 AND conversation_id = $2
|
||||
`, userID, conversationID); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
在回复这条新消息之前,我们必须通知它。 这是我们将要在下一篇文章中编写的实时部分,因此我在那里留一个注释。
|
||||
|
||||
### 获取消息
|
||||
|
||||
这个端点处理对 `/api/conversations/{conversationID}/messages` 的 GET 请求。 它用一个包含会话中所有消息的 JSON 数组进行响应。 它还具有更新参与者 `messages_read_at` 的副作用。
|
||||
|
||||
```
|
||||
func getMessages(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
authUserID := ctx.Value(keyAuthUserID).(string)
|
||||
conversationID := way.Param(ctx, "conversationID")
|
||||
|
||||
tx, err := db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
|
||||
if err != nil {
|
||||
respondError(w, fmt.Errorf("could not begin tx: %v", err))
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
isParticipant, err := queryParticipantExistance(ctx, tx, authUserID, conversationID)
|
||||
if err != nil {
|
||||
respondError(w, fmt.Errorf("could not query participant existance: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if !isParticipant {
|
||||
http.Error(w, "Conversation not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := tx.QueryContext(ctx, `
|
||||
SELECT
|
||||
id,
|
||||
content,
|
||||
created_at,
|
||||
user_id = $1 AS mine
|
||||
FROM messages
|
||||
WHERE messages.conversation_id = $2
|
||||
ORDER BY messages.created_at DESC
|
||||
`, authUserID, conversationID)
|
||||
if err != nil {
|
||||
respondError(w, fmt.Errorf("could not query messages: %v", err))
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
messages := make([]Message, 0)
|
||||
for rows.Next() {
|
||||
var message Message
|
||||
if err = rows.Scan(
|
||||
&message.ID,
|
||||
&message.Content,
|
||||
&message.CreatedAt,
|
||||
&message.Mine,
|
||||
); err != nil {
|
||||
respondError(w, fmt.Errorf("could not scan message: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
messages = append(messages, message)
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
respondError(w, fmt.Errorf("could not iterate over messages: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
respondError(w, fmt.Errorf("could not commit tx to get messages: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err = updateMessagesReadAt(nil, authUserID, conversationID); err != nil {
|
||||
log.Printf("could not update messages read at: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
respond(w, messages, http.StatusOK)
|
||||
}
|
||||
```
|
||||
|
||||
首先,它以只读模式开始一个 SQL 事务。 检查参与者是否存在并查询所有消息。 在每条消息中,我们使用当前经过身份验证的用户 ID 来了解用户是否拥有该消息(`我的`)。 然后,它提交事务,在 goroutine 中更新参与者 `messages_read_at` 并以消息响应。
|
||||
|
||||
### 阅读消息
|
||||
|
||||
该端点处理对 `/api/conversations/{conversationID}/read_messages` 的 POST 请求。 没有任何请求或响应主体。 在前端,每次有新消息到达实时流时,我们都会发出此请求。
|
||||
|
||||
```
|
||||
func readMessages(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
authUserID := ctx.Value(keyAuthUserID).(string)
|
||||
conversationID := way.Param(ctx, "conversationID")
|
||||
|
||||
if err := updateMessagesReadAt(ctx, authUserID, conversationID); err != nil {
|
||||
respondError(w, fmt.Errorf("could not update messages read at: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
```
|
||||
|
||||
它使用了与更新参与者 `messages_read_at` 相同的函数。
|
||||
|
||||
* * *
|
||||
|
||||
到此为止。实时消息是后台仅剩的部分了。请等待下一篇文章。
|
||||
|
||||
[Souce Code][4]
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
via: https://nicolasparada.netlify.com/posts/go-messenger-messages/
|
||||
|
||||
作者:[Nicolás Parada][a]
|
||||
选题:[lujun9972][b]
|
||||
译者:[译者ID](https://github.com/gxlct008)
|
||||
校对:[校对者ID](https://github.com/校对者ID)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||
|
||||
[a]: https://nicolasparada.netlify.com/
|
||||
[b]: https://github.com/lujun9972
|
||||
[1]: https://nicolasparada.netlify.com/posts/go-messenger-schema/
|
||||
[2]: https://nicolasparada.netlify.com/posts/go-messenger-oauth/
|
||||
[3]: https://nicolasparada.netlify.com/posts/go-messenger-conversations/
|
||||
[4]: https://github.com/nicolasparada/go-messenger-demo
|
@ -7,32 +7,31 @@
|
||||
[#]: via: (https://nicolasparada.netlify.com/posts/go-messenger-dev-login/)
|
||||
[#]: author: (Nicolás Parada https://nicolasparada.netlify.com/)
|
||||
|
||||
Building a Messenger App: Development Login
|
||||
构建一个即时消息应用(六):仅用于开发的登录
|
||||
======
|
||||
|
||||
This post is the 6th on a series:
|
||||
本文是该系列的第六篇。
|
||||
|
||||
* [Part 1: Schema][1]
|
||||
* [Part 2: OAuth][2]
|
||||
* [Part 3: Conversations][3]
|
||||
* [Part 4: Messages][4]
|
||||
* [Part 5: Realtime Messages][5]
|
||||
* [第一篇: 模式][1]
|
||||
* [第二篇: OAuth][2]
|
||||
* [第三篇: 对话][3]
|
||||
* [第四篇: 消息][4]
|
||||
* [第五篇: 实时消息][5]
|
||||
|
||||
|
||||
我们已经实现了通过 GitHub 登录,但是如果想把玩一下这个 app,我们需要几个用户来测试它。在这篇文章中,我们将添加一个为任何用户提供登录的端点,只需提供用户名即可。该端点仅用于开发。
|
||||
|
||||
We already implemented login through GitHub, but if we want to play around with the app, we need a couple of users to test it. In this post we’ll add an endpoint to login as any user just giving an username. This endpoint will be just for development.
|
||||
首先在 `main()` 函数中添加此路由。
|
||||
|
||||
Start by adding this route in the `main()` function.
|
||||
|
||||
```
|
||||
```go
|
||||
router.HandleFunc("POST", "/api/login", requireJSON(login))
|
||||
```
|
||||
|
||||
### Login
|
||||
### 登录
|
||||
|
||||
This function handles POST requests to `/api/login` with a JSON body with just an username and returns the authenticated user, a token and expiration date of it in JSON format.
|
||||
此函数处理对 `/api/login` 的 POST 请求,其中 JSON body 只包含用户名,并以 JSON 格式返回通过认证的用户、令牌和过期日期。
|
||||
|
||||
```
|
||||
```go
|
||||
func login(w http.ResponseWriter, r *http.Request) {
|
||||
if origin.Hostname() != "localhost" {
|
||||
http.NotFound(w, r)
|
||||
@ -81,9 +80,9 @@ func login(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
```
|
||||
|
||||
First it checks we are on localhost or it responds with `404 Not Found`. It decodes the body skipping validation since this is just for development. Then it queries to the database for a user with the given username, if none is found, it returns with `404 Not Found`. Then it issues a new JSON web token using the user ID as Subject.
|
||||
首先,它检查我们是否在本地主机上,或者响应为 `404 Not Found`。它解码主体跳过验证,因为这只是为了开发。然后在数据库中查询给定用户名的用户,如果没有,则返回 `404 NOT Found`。然后,它使用用户 ID 作为主题发布一个新的 JSON Web 令牌。
|
||||
|
||||
```
|
||||
```go
|
||||
func issueToken(subject string, exp time.Time) (string, error) {
|
||||
token, err := jwtSigner.Encode(jwt.Claims{
|
||||
Subject: subject,
|
||||
@ -96,31 +95,31 @@ func issueToken(subject string, exp time.Time) (string, error) {
|
||||
}
|
||||
```
|
||||
|
||||
The function does the same we did [previously][2]. I just moved it to reuse code.
|
||||
该函数执行的操作与 [前文][2] 相同。我只是将其移过来以重用代码。
|
||||
|
||||
After creating the token, it responds with the user, token and expiration date.
|
||||
创建令牌后,它将使用用户、令牌和到期日期进行响应。
|
||||
|
||||
### Seed Users
|
||||
### 种子用户
|
||||
|
||||
Now you can add users to play with to the database.
|
||||
现在,您可以将要操作的用户添加到数据库中。
|
||||
|
||||
```
|
||||
```sql
|
||||
INSERT INTO users (id, username) VALUES
|
||||
(1, 'john'),
|
||||
(2, 'jane');
|
||||
```
|
||||
|
||||
You can save it to a file and pipe it to the Cockroach CLI.
|
||||
您可以将其保存到文件中,并通过管道将其传送到 Cockroach CLI。
|
||||
|
||||
```
|
||||
```bash
|
||||
cat seed_users.sql | cockroach sql --insecure -d messenger
|
||||
```
|
||||
|
||||
* * *
|
||||
|
||||
That’s it. Once you deploy the code to production and use your own domain this login function won’t be available.
|
||||
就是这样。一旦将代码部署到生产环境并使用自己的域后,该登录功能将不可用。
|
||||
|
||||
This post concludes the backend.
|
||||
本文也结束了所有的后端开发部分。
|
||||
|
||||
[Souce Code][6]
|
||||
|
||||
@ -130,7 +129,7 @@ via: https://nicolasparada.netlify.com/posts/go-messenger-dev-login/
|
||||
|
||||
作者:[Nicolás Parada][a]
|
||||
选题:[lujun9972][b]
|
||||
译者:[译者ID](https://github.com/译者ID)
|
||||
译者:[译者ID](https://github.com/gxlct008)
|
||||
校对:[校对者ID](https://github.com/校对者ID)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
Loading…
Reference in New Issue
Block a user