做人呢最重要的就是开心

最近正在迁移自己的小项目,项目之前是基于 Laravel5.5 开发的。整个用户登陆也是基于框架的 Auth 包认证的。其中用户密码这块也是用到了 PHP 内置的函数 password_hash,用它进行密码加密。而且 PHP 默认使用的 PASSWORD_BCRYPT 算法。在使用 Go 的迁移过程中需要认证密码,还需要兼容 password_hash, 所以就把这个过程记录下来。使用下面的例子来说明如何使用 GO bcrypt 包来对你的密码进行 hash 和 salt 加密

对于这个例子,我将创建一个控制台应用程序,用于演示如何获取用户输入的密码并使用它生成 salt 哈希值。 完成此操作后,我将通过比较密码与其散列版本来验证密码是否正确。

获取用户输入的密码

开始我们先创建一个可以在控制台读取用户输入的的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
func getPwd() []byte {
fmt.Println("Enter a password")
var pwd string

// 读取用户输入
_, err := fmt.Scan(&pwd)
if err != nil {
log.Println(err)
}
return []byte(pwd)

}
`

Hash & Salt 用户的密码

现在我们可以使用 Go 的 bcrypt 包提供的GenerateFromPassword(password []byte, cost int)([]byte, error)方法对用户的密码进行 hash 和 salt 加密了。

GenerateFromPassword 方法以给定 cost 值返回密码的 Bcrypt 算法的 Hash 值,如果提供的 cost 值小于 Mincost 的话,将会默认使用 DefaultCost 代替

使用 GenerateFromPassword 函数的一个优势就是我们不需要自己来编写函数来生成 Salt,因为它会为我们自动生成一个 Salt。

下面的函数使用 GenerateFromPassword 生成 salted 哈希值,该哈希值作为字节切片返回。 然后我们将字节切片作为字符串返回,以便我们可以将 salted 哈希存储在数据库中作为用户密码。

1
2
3
4
5
6
7
func hashAndSalt(pwd []byte) string {
hash, err := bcrypt.GenerateFromPassword(pwd, bcrypt.MinCost)
if err != nil {
log.Println(err)
}
return string(hash)
}

目前我们做了什么

到目前为止,我们已经创建了一个接受来自控制台的用户输入并将其作为字节切片返回的函数。 然后,
我们再创建一个可以接收用户输入并返回 salted 哈希值的函数。下面就是代码事例。

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

import (
"fmt"
"log"
"golang.org/x/crypto/bcrypt"
)

func main() {
for {
pwd := getPwd()
hash := hashAndSalt(pwd)
fmt.Println("Salted Hash", pwd)
}
}

如果你运行上面的代码,将会得到下面的结果

1
2
3
> $ Enter a password
> $ foobar
> Salted Hash $2a$10$...........

这里需要的注意的是我使用 for 循环调用函数,直到我强制停止它。对于那些不熟悉 GO 的人来讲,这个就和其他语言的 while (true){}是一样的效果。

验证密码

最后一件事儿就是需要验证密码的正确性来登陆我们的系统,我们可以使用 bcrypt 包提供的CompareHashAndPassword(hashedPassword, password []byte) error函数

CompareHashAndPassword 将 bcrypt 哈希密码与其纯文本进行比较。 成功时返回nil,失败时返回错误

我们使用CompareHashAndPassword函数来创建另一个返回 bool 值的函数让我们知道密码是否匹配。

1
2
3
4
5
6
7
8
9
10
func comparePasswords(hashedPwd string, plainPwd []byte) bool {

byteHash := []byte(hashedPwd)
err := bcrypt.CompareHashAndPassword(byteHash, plainPwd)\
if err != nil {
log.Println(err)
return false
}
return true
}

更新 Main 函数

我们现在可以更新我们的主要功能,以便我们能够输入密码,获取其盐渍哈希,然后再次输入密码,并查明我们的第二个密码是否与我们输入的第一个密码相匹配。
我们现在修改一个 main 函数,当我们输入密码的时候,获取 salted 哈希值,然后再次输入密码,来检查我们的密码是否匹配。

1
2
3
4
5
6
7
8
9
func main() {
for {
pwd := getPwd()
hash := hashAndSalt(pwd)
pwd2 := getPwd()
pwdMatch := comparePasswords(hash, pwd2)
fmt.Println("Passwords Match?", pwd)
}
}

全部代码

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
48
49
package main

import (
"fmt"
"log"
"golang.org/x/crypto/bcrypt"
)

func main() {
for {
// 输入密码 获取 hash 值
pwd := getPwd()
hash := hashAndSalt(pwd)
// 再次输入密码验证
pwd2 := getPwd()
pwdMatch := comparePasswords(hash, pwd2)
fmt.Println("Passwords Match?", pwd)
}
}

func getPwd() []byte {
fmt.Println("Enter a password")
var pwd string
_, err := fmt.Scan(&pwd)
if err != nil {
log.Println(err)
}
return []byte(pwd)
}


func hashAndSalt(pwd []byte) string {
hash, err := bcrypt.GenerateFromPassword(pwd, bcrypt.MinCost)
if err != nil {
log.Println(err)
}
return string(hash)
}

func comparePasswords(hashedPwd string, plainPwd []byte) bool {
byteHash := []byte(hashedPwd)

err := bcrypt.CompareHashAndPassword(byteHash, plainPwd)
if err != nil {
log.Println(err)
return false
}
return true
}

以上便是 GO 转 php 的加密函数的过程,如果有任何错误或者不当的地方欢迎进行改进。

自定义认证头

项目使用的tymon/jwt-auth包作为 token 的认证,过程中需要迁移项目,因为之前公司的 token 头部使用自定义的,并且他们还修改了包的头信息。就是下面头部信息。

1
2
3
4
5
6
7
class AuthHeaders implements ParserContract
{
// 下面这两处就是被修改的
protected $header = 'authorization';

protected $prefix = 'bearer';
}

迁移项目过程了,因为拉取了新的包,所以还要去动包的信息,这是极其不合理的行为。所以就在包中尝试找到了更好的解决办法。如果在项目迁移过程中遇到了类似的问题该如何去做呢?这里只提供了我能想到的解决办法。需要在 AppServiceProvider 中加入该方法就可以了。

1
2
3
4
5
6
7
8
protected function setAuthHeader()
{
$chain = $this->app['tymon.jwt.parser']->getChain();

$chain[0] = $chain[0]->setHeaderPrefix('项目的 token 前缀')->setHeaderName('项目的头信息 key');

$this->app['tymon.jwt.parser']->setChain($chain);
}

如果你有更好的办法可以提供,欢迎留言或者改进。

Module

自从 Go 官方从去年推出 1.11 之后,增加新的依赖管理模块并且更加易于管理项目中所需要的模块。模块是存储在文件树中的 Go 包的集合,其根目录中包含 go.mod 文件。 go.mod 文件定义了模块的模块路径,它也是用于根目录的导入路径,以及它的依赖性要求。每个依赖性要求都被写为模块路径和特定语义版本。

从 Go 1.11 开始,Go 允许在 $GOPATH/src 外的任何目录下使用 go.mod 创建项目。在 $GOPATH/src 中,为了兼容性,Go 命令仍然在旧的 GOPATH 模式下运行。从 Go 1.13 开始,模块模式将成为默认模式。

本文将介绍使用模块开发 Go 代码时出现的一系列常见操作:

  • 创建一个新模块。
  • 添加依赖项。
  • 升级依赖项。
  • 删除未使用的依赖项。

下面使用的案例都是以 GIN 模块为例。
在这之前呢,需要先设置一些环境变量:

1
2
export GO111MODULE=on
export GOPROXY=https://goproxy.io // 设置代理

创建一个新模块

你可以在 $GOPATH/src 之外的任何地方创建一个新的目录。比如:

mkdir backend && cd backend

然后初始化 go mod init backend,成功之后你会发现目录下会生成一个 go.mod 文件.

$ cat go.mod

内容如下

module backend

go 1.12

添加依赖项

创建一个文件 main.go 然后加入以下代码,这里直接 import 了 gin 的依赖包。

vim 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
}

go build 之后,会在 go.mod 引入所需要的依赖包。之后再来看看 go.mod 文件的情况。

1
2
3
4
5
6
7
8
9
10
11
12
module backend

go 1.12

require (
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 // indirect
github.com/gin-gonic/gin v1.3.0
github.com/golang/protobuf v1.3.1 // indirect
github.com/mattn/go-isatty v0.0.7 // indirect
github.com/ugorji/go v1.1.4 // indirect
gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
gopkg.in/yaml.v2 v2.2.2 // indirect

require 就是 gin 框架所需要的所有依赖包 并且在每个依赖包的后面已经表明了版本号

升级依赖项

首先我们需要查看以下我们使用到的依赖列表

1
2
3
4
5
6
7
8
9
10
11
12
> go list -m all
// 你会看到所有项目使用的依赖包
backend
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3
github.com/gin-gonic/gin v1.3.0
github.com/golang/protobuf v1.3.1
github.com/mattn/go-isatty v0.0.7
github.com/ugorji/go v1.1.4
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405
gopkg.in/go-playground/validator.v8 v8.18.2
gopkg.in/yaml.v2 v2.2.2

因为这里使用的是最新的版本,无法升级,所以这里给出一个回退的例子。将 GIN 框架的版本回退到上个版本。这里需要使用一个命令查看依赖的版本历史。

1
2
3
> go list -m -versions github.com/gin-gonic/gin
// 将会列出 Gin 版本历史
github.com/gin-gonic/gin v1.1.1 v1.1.2 v1.1.3 v1.1.4 v1.3.0

将版本更新到上个版本,这里只是个演示。

1
2
3
4
> go get github.com/gin-gonic/gin@v1.1.4 // 只需要在依赖后面加上 @version 就可以了
> go list -m all
// 看到了版本变化
github.com/gin-gonic/gin v1.1.4

或者可以使用 go mod来进行版本的切换, 这样就需要两个步骤了

1
2
> go mod edit -require="github.com/gin-gonic/gin@v1.1.4" // 修改 go.mod 文件
> go tidy //下载更新依赖

go.tidy 会自动清理掉不需要的依赖项,同时可以将依赖项更新到当前版本

使用起来这是一个很简单过程,只需要几个命令,你便可以知道依赖的版本信息,以及自由选择安装的版本,一切都变得这么简单。

删除未使用的依赖项

如果你在项目过程需要移除一些不需要的依赖,可以使用下面的命令来执行:

1
>go mod tidy

更多关于 go mod 的使用命令

1
2
3
4
5
6
7
8
9
10
11
>$ go mod
The commands are:

download download modules to local cache
edit edit go.mod from tools or scripts
graph print module requirement graph
init initialize new module in current directory
tidy add missing and remove unused modules
vendor make vendored copy of dependencies
verify verify dependencies have expected content
why explain why packages or modules are needed

结论

Go Module 是 Go 依赖管理的未来。 目前只有 1.11 和 1.12 版本支持该功能,介绍了 Go 依赖管理的功能。更多的功能会在以后补充。也欢迎补充完善。最后如果你是使用 Goland, 请移步这里Working with Go Modules 阅读关于使用 Modules 开发

捕获迭代变量

这是在学习 Go 程序设计 中遇到的一个比较重要的一个警告。这是个 Go 语言的词法作用域规则的陷阱。看完之后感觉是真的一个比较让人疑惑困惑的地方。所以特地记录一下。由标题就可以知道了,迭代变量,肯定是在 for 中遇到的问题。来看一个简单的例子说明一下这个问题所在。

看一段简单的代码, 首先是错误的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var slice []func()

func main() {
sli := []int{1, 2, 3, 4, 5}
for _, v := range sli {
fmt.Println(&v)
slice = append(slice, func(){
fmt.Println(v * v) // 直接打印结果
})
}

for _, val := range slice {
val()
}
}
// 输出 25 25 25 25 25

你可能会很奇怪为什么会出现这种情况, 结果不应该是 1, 4, 9, 16, 25 吗?其实原因是循环变量的作用域的规则限制。在上面的程序中,v 在 for 循环引进的一个块作用域内进行声明。在循环里创建的所有函数变量共享相同的变量,就是一个可访问的存储位置,而不是固定的值。(你会惊奇的发现 &v 的内存地址是一样的)

模拟一下实际的情况,假设 v 变量的地址在 0x12345678 上, for 循环在迭代过程中,所有变量值都是在这地址上迭代的。当最后调用匿名函数的时候,取值也是在这块地址上。所以最后输出的结果都是迭代的最后一个值。至少在 Go 语言中是不用质疑的。这里也是一个陷阱,如果你不清楚的话,肯定会遇到坑。那个该如何修改呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var slice []func()

func main() {
sli := []int{1, 2, 3, 4, 5}
for _, v := range sli {
temp := v // 其实很简单 引入一个临时局部变量就可以了,这样就可以将每次的值存储到该变量地址上
fmt.Println(&temp) // 这里内存地址是不同的
slice = append(slice, func(){
fmt.Println(temp * temp) // 直接打印结果
})
}

for _, val := range slice {
val()
}
}
// 输出 1, 4, 9, 16, 25 预期结果

只需要引入一个局部变量便可以解决了,这是必须的。否则你的程序将不会有可预期的结果。

捕获迭代变量

这是在学习 Go 程序设计 中遇到的一个比较重要的一个警告。这是个 Go 语言的词法作用域规则的陷阱。看完之后感觉是真的一个比较让人疑惑困惑的地方。所以特地记录一下。由标题就可以知道了,迭代变量,肯定是在 for 中遇到的问题。来看一个简单的例子说明一下这个问题所在。

看一段简单的代码, 首先是错误的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var slice []func()

func main() {
sli := []int{1, 2, 3, 4, 5}
for _, v := range sli {
fmt.Println(&v)
slice = append(slice, func(){
fmt.Println(v * v) // 直接打印结果
})
}

for _, val := range slice {
val()
}
}
// 输出 25 25 25 25 25

你可能会很奇怪为什么会出现这种情况, 结果不应该是 1, 4, 9, 16, 25 吗?其实原因是循环变量的作用域的规则限制。在上面的程序中,v 在 for 循环引进的一个块作用域内进行声明。在循环里创建的所有函数变量共享相同的变量,就是一个可访问的存储位置,而不是固定的值。(你会惊奇的发现 &v 的内存地址是一样的)

模拟一下实际的情况,假设 v 变量的地址在 0x12345678 上, for 循环在迭代过程中,所有变量值都是在这地址上迭代的。当最后调用匿名函数的时候,取值也是在这块地址上。所以最后输出的结果都是迭代的最后一个值。至少在 Go 语言中是不用质疑的。这里也是一个陷阱,如果你不清楚的话,肯定会遇到坑。那个该如何修改呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var slice []func()

func main() {
sli := []int{1, 2, 3, 4, 5}
for _, v := range sli {
temp := v // 其实很简单 引入一个临时局部变量就可以了,这样就可以将每次的值存储到该变量地址上
fmt.Println(&temp) // 这里内存地址是不同的
slice = append(slice, func(){
fmt.Println(temp * temp) // 直接打印结果
})
}

for _, val := range slice {
val()
}
}
// 输出 1, 4, 9, 16, 25 预期结果

只需要引入一个局部变量便可以解决了,这是必须的。否则你的程序将不会有可预期的结果。

复现

问题是在我修改后台的时候出现,因为最近要给我的后台管理换后台皮肤,所以要重写一下后台页面。换完之后呢,发现每次请求页面都特么的长,然后就打打开 Chrome 的调试查看, 发现 Waiting(TTFb) 时间特别长,几乎所有占据了所有的请求时间。

调试

  • 切换浏览器,一开始我以为是浏览器问题,所以切换了 Firefox, 发现结果是一样

  • 域名访问的问题,我是通过本地 ip 地址访问,切换到 Apache 服务器之后发现结果也是一样

  • 怀疑是静态资源加载,发现静态资源加载都非常的快,排除

    基本猜测结束发现,我发现什么无法解决问题,查看 tp 的日志文件,发现了问题,在 Mysql Db Connect 的时候,连接时间非常的长,长达 1s 以上,问题就出现在这里。为什么会出现这种情况呢?

    我发现 host 的地址不是 127.0.0.1 而是 localhost

解决

host 文件添加

1
127.0.0.1 localhost

为什么会这样呢?只需要两个步骤就可以看出来了。在没有配置上述配置之前

1
ping localhost

你会发现他指向的是 ipv6 ::1 的地址, 问题就是出在这里。走的是 IPV6。改成 IPV4 的地址就可以了

最近公司要做个微信分享功能,记录下来以便后来复制粘贴,哈哈哈。
原谅我,我在做这个之前从网上的确复制粘贴了,最后发现都没法用,哈哈,只能自己看接口文档了。在这之前,首先你要获取 appid 和 appSecret,然后还要配置 JS 安全域名,还有就是 IP 白名单,这些我就不说了,有了微信公众号后台轻松搞定

后端

我使用的是世界上最好的语言,不说大家都知道是什么了吧!哈哈哈,我就直接上代码了,注意点在代码里面看。注意项目用的thinkphp 框架,所以 Cache 根据实际使用,不能一股脑的粘贴啊

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128

use Think\Cache;

class WeChat
{
// 你的 appid
private $appId = '';
// 秘钥
private $appSecret = '';
// 获取 token 的 url
private $getAccessTokenUrl = 'https://api.weixin.qq.com/cgi-bin/token';
// 获取 jsticket 的 url
private $getJsTicketUrl = 'https://api.weixin.qq.com/cgi-bin/ticket/getticket';

private $cacheLiefTime = 3600;

private $cacheKey = 'ticket';

/**
* 获取签名
*
* @time at 2018年12月10日
* @param $url
* @return array
*/
public function getSignature($url)
{
$nonceStr = $this->createNonceStr(18);
$timeStamp = time();
$params = [
'jsapi_ticket' => $this->getJsTicket(),
'noncestr' => $nonceStr,
'timestamp' => $timeStamp,
];
// 只有唯一的注意点,就是不要把 url 放在 http_build_query 函数里面,它会转义
// 验证你的签名请到这里 http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=jsapisign
$signature = sha1(http_build_query($params) . '&url=' . $url);

return ['noncestr' => $nonceStr, 'timestamp' => $timeStamp, 'signature' => $signature, 'appid' => $this->appId];
}

/**
* 获取 TOKEN
*
* @time at 2018年12月10日
* @return void
*/
private function getAccessToken()
{
$params = [
'grant_type' => 'client_credential',
'appid' => $this->appId,
'secret' => $this->appSecret,
];

$res = $this->httpGet($this->getAccessTokenUrl .'?'.http_build_query($params));
$res = json_decode($res, true);
if (isset($res['errcode'])) {
return false;
}

return $res['access_token'];

}

/**
* 获取签名
*
* @time at 2018年12月10日
* @return void
*/
private function getJsTicket()
{
$ticket = cache($this->cacheKey);

if ($ticket) {
return $ticket;
}

$accessToken = $this->getAccessToken();

$params = [
'access_token' => $accessToken,
'type' => 'jsapi',
];

if (!$accessToken) {
return false;
}
$res = $this->httpGet($this->getJsTicketUrl .'?'.http_build_query($params));
$res = json_decode($res, true);

if (isset($res['errcode']) && $res['errcode']) {
return false;
}
Cache::remember($this->cacheKey, $res['ticket'], $this->cacheLiefTime);

return $res['ticket'];
}

private function httpGet($url)
{
$curl = curl_init();
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_TIMEOUT, 500);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($curl, CURLOPT_URL, $url);
$res = curl_exec($curl);
curl_close($curl);

return $res;
}

private function createNonceStr($n)
{
if (!is_numeric($n)) {
return false;
}
$str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
$nonceStr = '';
$strLen = strlen($str);
for ($i = 1; $i < intval($n); $i++) {
$nonceStr .= $str[rand(0, $strLen-1)];
}
return $nonceStr;
}
}

前台

引入 http://res2.wx.qq.com/open/js/jweixin-1.4.0.js 网上都是 1.2.0,抄的我好累

前端代码
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
// 注意微信文档提到 url 不允许带 # 后面的内容,所以就分割吧, 还有下面你看一下就懂了吧。
// 微信好多接口都换了,使用也和网上的一点也不一样了,所以如果当你看到这篇文章文章使用不能成功的时候,建议你去看微信文档, 不要再抄这个了。还有就是调试期间把 debug 开下来。
var url = window.location.href.split('#')[0];
$.post("{:url('Index/getWxConfig')}", {"url": url}, function (response){
var params = {}
params.debug = false;
params.appId = response.data.appid;
params.timestamp = response.data.timestamp;
params.nonceStr = response.data.noncestr;
params.signature = response.data.signature;
params.jsApiList = ['updateAppMessageShareData','updateTimelineShareData'];
wx.config(params);
wx.ready(function () {
wx.updateAppMessageShareData({
title: '我就是不信',
desc: '微信分享成功了没',
link: url,
imgUrl: 'http://aaaa.gaiwenkeji.com/data/upload/qrcode/7.png?id=1544524256',
type: 'link',
dataUrl: '',
});
wx.updateTimelineShareData({
title: '我相信了, 能不信吗',
link: url,
imgUrl: 'http://aaaa.gaiwenkeji.com/data/upload/qrcode/7.png?id=1544524256',
})
})

以上便是微信分享的所有东西,没有什么坑。坑就是我抄网上的。还有就是签名 URL 转义的,其他就没咯。

补充

2018/12/24 新的微信接口在安卓系统会无效,建议使用老接口,IOS 上可以继续使用新的接口

代理

由于最近使用 vps 连续被封禁,不知是否是因为切换到 SSR 的缘故?我从网络上看到很多关于 SSR 被封禁的情况,好像是由于墙不需要去特意破解什么加密,而是根据 SSR 某些特征可以大概率查封。对于我而言,我还是信服这种说法,因为在我切换到 SSR 之后不到三天我的两台服务器连续被封禁了,这让我不知所措。因为在这之前没去好好了解实际情况,我面对这种情况真的显得很无奈,好好地两台 VPS 居然都被封了,只好忍痛又买了一台。

发现 V2ray

在论坛上看到很多人也遇到过这种情况,很多都推荐使用 V2ray,新的协议,伪装程度高,各种优点。而且现在 SS 和 SSR 停止更新了,从 V2ray github 上来看,更新很频繁,而且功能也在不停地增加。
对于这个时候的我,其实已经不在乎速度了,更加在乎的是安全和长久,因为我只是用来查查资料,基本很少看视频,对于速度要求不高。当然现在已经搭建的这台代理速度还是可以的,油管的 1080 很流畅。而且好几个人在用。

方案

本身 v2ray 服务支持很多种方案,具体有多少我都忘了。有兴趣的可以去查查博客。这里我只介绍我正在使用方案,也是大家普遍认可而且安全比较高的方案 。

v2ray + nginx + websocket + tls

当前你首先必须拥有一台 VPS,推荐肯定就是搬瓦工的 KVM 架构的了。安装请选择带有 BBR 的系统。BBR 是由 GOOGLE 提出的一个开源 TCP 拥塞控制的算法。这里有一篇文章作了详细的介绍,有兴趣的可以看看。还是你必须拥有一个域名,这是必须的。如果没有,那你可以先关闭该页面,申请完再来看了,或者找一下其他的 v2ray 的解决方案了。
这套方案的思路就是利用 nginx 作为中转来做伪装。相比于 SSR 的直连,在 v2ray 客户端发出 ws 的请求到 nginx,然后利用 nginx 的代理直接转发到 v2ray 的服务上面。这样就可以将请求伪装成正常 ws 的请求 。

安装 (Centos7 系统)

安装 v2ray

我还是推荐一键脚本,这也是由官方维护的,所以我认为你没必要再去自己搞什么编译啥的,也没多大意义。目前还是能成功安全起来为主。以后有兴趣了再去搞搞也还行。执行下面的命令就可以了

bash <(curl -L -s https://install.direct/go.sh)

使用以下命令来控制 v2ray 服务

1
systemctl start|restart|stop|status v2ray

其配置文件放在 /etc/v2ray/config.json,是一个 Json 文件,在你安装完之后会有一个初始化的 Json 文件,文件会提供三个必要的信息 IP、端口(Port)、id(UUID), 尤其是这个 UUID,很重要。客户端会使用到,必须保持和服务端一致。当然你可以自己生成所需要的 UUID,到这个网站 Online UUID Generator 生成就可以了。

执行 systemctl start v2ray 然后执行 systemctl status v2ray 之后,如果看到绿色的 active(running) 就说明启动成功了,这里就先将所有服务先安装好,配置在最后一块配置就行了。

安装 nginx

1
2
3
4
5
6
7
8
9
10
11
yum -y install gcc gcc-c++ automake pcre pcre-devel zlib zlib-devel open openssl-devel

wget http://nginx.org/download/nginx-1.14.1.tar.gz

tar zxvf nginx-1.14.1.tar.gz

cd nginx-1.14.1

./configure --prefix=/usr/local/nginx --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --user=nginx --group=nginx --with-http_ssl_module --with-http_v2_module --with-http_dav_module --with-http_stub_status_module --with-threads --with-file-aio

make && make install

五部 Nginx 就安装好了。最后加一下软连接

1
ln -s /usr/local/nginx/sbin/nginx /usr/bin/

安装证书

这里假设你已经拥有了域名,并且增加了 A 解析到该台 VPS。下面我们申请证书,证书过程也很简单。安装两个脚本便可以了。这里利用 Certbot 来申请。执行下面的命令

1
2
3
yum install epel-release
yum install -y certbot
curl https://get.acme.sh | sh

安裝成功后执行 source ~/.bashrc 以确保脚本所设置的命令別名生效。
如果安装报错,那么可能是因为系统缺少 acme.sh 所需要的依赖项, acme.sh 的依赖项主要是 netcat(nc), 我们通过以下命令来安装这些依赖,然后重新安装一遍

1
yum -y install netcat

生成证书之前,请确保宿主机上的 80 端口没有被占用,否则会安装失败

~/.acme.sh/acme.sh –issue -d mydomain.me –standalone -k ec-256

-k 表示秘钥长度,后面的值可以是 ec-256ec-384、2048、3072、4096、8192,带有 ec 表示生成的是 ECC 证书,沒有则是 RSA 证书。在安全性上 256 位的 ECC 证书等同于 3072 位的 RSA 证书。

Let’s Encrypt 的证书有效期只有 3 個月,因此需要 90 天至少要更新一次证书,acme.sh脚本会每 60 天自动更新证书,。也可以手动更新。

手动更新 ECC 证书,执行:

sudo ~/.acme.sh/acme.sh –renew -d mydomain.com –force –ecc

如果是 RSA 证书则执行:

sudo ~/.acme.sh/acme.sh –renew -d mydomain.com –force

配置

v2ray服务端配置

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
{
"log": {
"access": "/var/log/v2ray/access.log",
"error": "/var/log/v2ray/error.log",
"loglevel": "debug"
},
"inbound": {
"listen": "127.0.0.1",
"port": ***, // 你的实际端口号
"protocol": "vmess",
"settings": {
"clients": [
{
"id": ### uuid,
"alterId": 64,
"level": 1
}
]
},
"streamSettings": {
"network": "ws",
"security": "auto",
"wsSettings": {
"path": "path", // 自定义
"headers": {
"Host": "yourDomain" // 你的域名地址
}
}
}
},
"outbound": {
"protocol": "freedom",
"settings": { }
},
"outboundDetour": [
{
"protocol": "blackhole",
"settings": { },
"tag": "blocked"
}
],
"routing": {
"strategy": "rules",
"settings": {
"rules": [
{
"type": "field",
"ip": [
"0.0.0.0/8",
"10.0.0.0/8",
"100.64.0.0/10",
"127.0.0.0/8",
"169.254.0.0/16",
"172.16.0.0/12",
"192.0.0.0/24",
"192.0.2.0/24",
"192.168.0.0/16",
"198.18.0.0/15",
"198.51.100.0/24",
"203.0.113.0/24",
"::1/128",
"fc00::/7",
"fe80::/10"
],
"outboundTag": "blocked"
}
]
}
}
}

nginx 配置

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
server {
# 禁用不需要的请求方式 以下只允许 get、post
if ($request_method !~ ^(POST|GET)$) {
return 444;
}

listen 80;
server_name yourDomain; #注:填写自己的域名
return 301 yourDomain;
}

upstream v2ray {
server 127.0.0.1:端口; #注:v2ray后端监听地址、端口
keepalive 2176; # 链接池空闲链接数
}

map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
#要开启 HTTP/2 注意nginx版本
#可以使用 nginx -V 检查
listen 443 ssl http2 backlog=1024 so_keepalive=120s:60s:10 reuseport; # backlog是nginx 监听队列 默认是511 使用命令 ss -tnl查看(Send-Q);
#设置编码
charset utf-8;

#证书配置
ssl_certificate path; #注:填写自己证书路径
ssl_certificate_key path; #注:填写密钥路径

ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
ssl_session_tickets off;

# https://nginx.org/en/docs/http/ngx_http_ssl_module.html
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
#openssl ciphers
#注:懒人配置 https://mozilla.github.io/server-side-tls/ssl-config-generator/
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;

#安全设定
#屏蔽请求类型
if ($request_method !~ ^(POST|GET)$) {
return 444;
}
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options nosniff;
add_header Strict-Transport-Security max-age=15 always;
root /var/www/html;

# Add index.php to the list if you are using PHP
index index.html index.htm index.php ;

server_name yourDomain; #注: 将domain.Name 替换成你的域名


location /path { #注:修改路径,上述服务端 ws 配置的 path
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; #此处与<map>对应
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_requests 25600;
keepalive_timeout 300 300;
proxy_buffering off;
proxy_buffer_size 8k;

#后端错误重定向
proxy_intercept_errors on;
error_page 400 = "yourDomain";
if ($http_host = "yourDomain" ) {
proxy_pass http://127.0.0.1:端口; // 设置 v2ray 监听的地址
}
}
}

客户端推荐使用 v2rayN, 这是一款 window 界面的产品,配置很简单。

补充

  • v2rayN 这款客户端无法代理 go get,原因不详,wireshark 无法抓到包,本身的 shadowsocks 客户端正常
  • 添加 shadowsocks 服务
1
2
3
4
5
6
7
8
9
10
11
"inboundDetour": [
{
"protocol": "shadowsocks",
"port": 10086, // 自行设置
"settings": {
"method": "aes-256-cfb", // 和客户端设置的加密方式一样,自行设置
"password": "****", // 密码设置
"udp": false
}
}
],

最后

以上便是安装的全部过程,可能过程有疏漏,也可能你在配置的过程中最后访问不了,问题有很多,可能是版本,也可能是环境问题。遇到问题请自行 google。
还有千万不要 粘贴复制 粘贴复制 粘贴复制

跨域

什么是跨域?为何会出现跨域问题?

首先了解一下什么叫做域?简单的定义就是 协议、域名、端口都相同叫做域,缺一不可,还有这里的域并不是单指域名。出于安全考虑,浏览器规定了一种同源策略。这里的源可以理解为域。
同源策略 的产生是处于安全考虑的。我们都知道,以 Http 协议为例,Http 协议是无状态的,所以我们为了保留某些用户的信息采用了 Cookie 方法,现在的 H5 还有 LocalStorage 。设想一下如果你登录了我的博客,必然会在头部信息中带有登录 Cookie 信息,如果没有同源策略的话,你访问其他其他网站的时候就会带上 cookie 信息,访问对方是恶意网站的话,该恶意网站可以通过访问我博客的接口完全获取你的 个人信息或者修改你的信息。

现实中还是需要突破这个策略需求的,所以 W3C 制定了 CORS 标准,全称是”跨域资源共享”(Cross-origin resource sharing)。

跨域请求共享

目前主流浏览器都是支持 CORS 标准的,所以当你进行跨域请求的时候,浏览器会自动完成整个过程。但是服务器端并不会,所以需要的解决的就是服务器端的问题。这里还有一点注意的就是浏览器会多发出一次 OPTIONS 请求,这个称作预检,用以判断实际发送的请求是否安全,服务端是不需要理会的。

两种请求

简单请求
1
2
3
4
5
6
7
8
9
10
1. 请求方法是以下三种方法之一:
1. get
2. post
3. head
2. HTTP 的头信息不超出以下几种字段:
1. Accept
2. Accept-Language
3. Content-Language
4. Last-Event-ID
5. Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

满足以上两种条件的都称之为简单请求,其他的则是非简单请求。浏览器对于这种请求的处理方式也是不一样的。

简单请求响应

对于简单请求,浏览器发出跨域请求的时候,只会简单的在头部加上 Origin 字段,在 Origin 指定的域名服务器响应中,当然是允许跨域的服务器,会多出几个响应字段,都是以 Access-Control- 开头的字段。

Access-Control-Allow-Origin

该字段是必须的。它的值要么是请求时 Origin 字段的值,要么是一个 *,表示接受任意域名的请求。

Access-Control-Allow-Credentials

该字段可选。它的值是一个布尔值,表示是否允许发送 Cookie 。默认情况下,Cookie 不包括在 CORS 请求之中。设为 true,即表示服务器明确许可,Cookie 可以包含在请求中,一起发给服务器。这个值也只能设为 true,如果服务器不要浏览器发送 Cookie,删除该字段即可。

Access-Control-Expose-Headers

该字段可选CORS 请求时,XMLHttpRequest 对象的 getResponseHeader() 方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在 Access-Control-Expose-Headers 里面指定。

非简单请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是 PUTDELETE,或者 Content-Type 字段的类型是application/json
除了简单请求的 Origin 字段外,还有两个特殊的头信息字段:

Access-Control-Request-Method

该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法,上例是 PUT

Access-Control-Request-Headers

该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段。

非简单请求响应

当服务器发出非简单请求的时候,服务段会响应一下几个头信息:

Access-Control-Allow-Methods

该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。

Access-Control-Allow-Headers

如果浏览器请求包括 Access-Control-Request-Headers 字段,则 Access-Control-Allow-Headers 字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在”预检”中请求的字段。

Access-Control-Allow-Credentials

该字段与简单请求时的含义相同。

Access-Control-Max-Age

该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

Lumen 处理跨域请求

回到标题的问题,如果来解决 Lumen 的跨域请求问题。了解 CORS 标准之后,如果你使用 Lumen 框架是基于 restful api 的话,就避免不了 PUTDELETE 请求。
需要建立一下几个响应字段

1
2
3
4
Access-Control-Allow-Origin: '*' // 允许的域名
Access-Control-Allow-Methods: '*' // 允许的方法
Access-Control-Allow-Headers: 'Content-Type, Authorization, X-Requested-With'
Access-Control-Max-Age: '' // 如果你不需要每次都要预请求的话可以添加该字段

建立一个 CORS 中间件文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace App\Http\Middleware;

use Closure;

class CorsMiddleware
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{

$response = $next($request);
$response->header('Access-Control-Allow-Origin', '*');
$response->header('Access-Control-Allow-Methods', '*');
$response->header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
return $response;
}
}

boostrap\app.php 文件加入中间件

1
2
3
    $app->middleware([
App\Http\Middleware\CorsMiddleware::class,
]);

这样就可以解决跨域问题了

最近想搞一下前后端分离,所以就基于 Vue 想做一下基础的后台管理,后端就选用了 Lumen5.7.*,因为解决方案多,方便。认证就在此基础使用了 Json Web Token(JWT)。这里记录一些使用过程。

下载 Jwt 包

修改 composer.json

1
2
3
"require": {
"tymon/jwt-auth": "1.0.*"
},

composer update

配置

修改 bootstrap 目录下 app.php 文件

1
2
3
4
5
6
7
8
9
 $app->withFacades();

$app->withEloquent();

$app->routeMiddleware([
'auth' => App\Http\Middleware\Authenticate::class,
]);
$app->register(App\Providers\AppServiceProvider::class);
$app->register(App\Providers\AuthServiceProvider::class);

打开这五行注释。然后进入到 AppServiceProvider 服务注入文件, 注册 Jwt 服务

1
2
3
4
5
public function register()
{
// Register Jwt Service
$this->app->register(\Tymon\JWTAuth\Providers\LumenServiceProvider::class);
}

其次需要增加 Jwt Auth 认证配置。所以在根目录下增加配置文件目录 config,然后增加 auth.php 配置文件,内容从 laravel\lumen-framework\config\auth.php 复制过来。配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
return [

'defaults' => [
'guard' => env('AUTH_GUARD', 'api'),
],

'guards' => [
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],

'providers' => [
//
'users' => [
'driver' => 'eloquent',
'model' => \App\User::class,
],

'passwords' => [
//
],
];

除了这里,还需要看一下包提供的配置。然后创建一个 jwt.php 配置文件就可以了。 最后执行一下命令,生成秘钥

1
php artisan jwt:secret

使用

门面的服务驱动使用的 eloquent, 所以需要实现 Tymon\JWTAuth\Contracts\JWTSubject 接口

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
class User extends Model implements AuthenticatableContract, AuthorizableContract, JWTSubject
{
use Authenticatable, Authorizable;

/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email',
];

/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = [
'password',
];

/**
* Get the identifier that will be stored in the subject claim of the JWT.
*
* @return mixed
*/
public function getJWTIdentifier()
{
return $this->getKey();
}

/**
* Return a key value array, containing any custom claims to be added to the JWT.
*
* @return array
*/
public function getJWTCustomClaims()
{
return [];
}
}

创建一个控制器 LoginController,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Illuminate\Http\Request;
use Tymon\JWTAuth\JWTAuth;

class LoginController extends Controller
{
public function login(Request $request, JWTAuth $jwt)
{
if (! $token = $jwt->attempt($request->only('email', 'password'))) {
return response()->json(['user_not_found'], 404);
}

return response()->json(compact('token'));
}
}

添加路由

1
$router->post('login', 'LoginController@login');

这个要说一下,你的 Users 表结构可以直接用 Laravel Auth,认证实际也是基于 Auth 实现的,所以这一块很轻松。 测试用 Postman 请求一下,可以看到返回的结果。应该是这样的。

1
2
3
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3Q6ODAwMFwvbG9naW4iLCJpYXQiOjE1NDI2ODExMzgsImV4cCI6MTU0MjY4NDczOCwibmJmIjoxNTQyNjgxMTM4LCJqdGkiOiJHZGp5bFV1ZlpHdzM2cTlPIiwic3ViIjo4LCJwcnYiOiI4N2UwYWYxZWY5ZmQxNTgxMmZkZWM5NzE1M2ExNGUwYjA0NzU0NmFhIn0.oZwsvsY8kfDvcs6ct_IGLzU8JWPDUbTTb6u8OTntsl4"
}

上述便是该包使用的过程,如果有疑问或者遗漏之处请指正。Jwt 文档地址