JaguarJack's Blog

做人呢最重要的就是开心

0%

Linux 文件权限

在深入了解之前,先来理解一下文件权限的基本信息。首先创建一个信息的文件

1
2
3
> touch new_file
> ll new_file
> -rw-r--r-- 1 root root 0 4月 23 22:59 new_file

文件 new_file 拥有下面三组权限:

rw- :文件的属主,登陆用户的权限 r w 分别是读写

r– : 文件属主的所属组的权限 r 读

r–: 其他用户的权限 r 读

当然还有一个 x 可执行权限这里没有,可以通过 chmod 命令来增加。这个后面再说。我们先来说说文件权限的由来,touch 命令之后的默认权限是如何来的。要知道首先要知道有 umask 的存在。通过 umask 命令可以获取默认值。这个值很有用。

1
2
> umask
> 0022 // 这是我机器上面的值

要了解这个 umask 的是如何工作的,首先要理解一下八进制模式的安全性设置。八进制模式的安全设置先获取这三个 rwx 权限的值,然后将其转换为三位二进制值,用一个八进制值来表示。在这个二进制表示中,每个位置代表一个二进制位。例如唯一读权限 r– 转换成二进制就是 100, 那么代表的八进制就是 4。
如下表表示的这样:

权限 二进制 八进制 描述
000 0 没有任何权限
–x 0001 1 只有可执行权限
-w- 010 2 只有可写权限
-wx 011 3 只有可写可执行权限
r– 100 4 只有可读权限
r-x 101 5 只有可读可执行权限
rw- 110 6 只有可读可写权限
rwx 111 7 可读可写权限可执行

当了解对应权限以及八进制值之后,看看上面的 new_file 拥有权限是 644。那么这里有点困惑了,这个值是如何得来的呢。 umask0022 有什么意义呢?其实这个值只是个掩码。他会屏蔽掉不像授予该安全级别的权限。对于文件来说,全权限是 666,减去 umask 的值就是新创建文件的权限。 644 正是我们所期望的那样,默认屏蔽了文件所属组和其他用户 w 权限。这里说明一下文件夹的全权限是 777。如果你想屏蔽更多的权限,可以修改 umask 的值。

1
> umask 026

我们使用 ssh 链接 linux 主机时,可能出现 “Host key verification failed.“ 的提示,ssh 连接不成功。
可能的提示信息如下:

IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that the RSA host key has just been changed.
The fingerprint for the RSA key sent by the remote host is
23:00:20:83:de:02:95:f1:e3:34:be:57:3f:cf:2c:e7.
Please contact your system administrator.
Add correct host key in /home/xahria/.ssh/known_hosts to get rid of this message.
Offending key in /home/xahria/.ssh/known_hosts:8
RSA host key for localhost has changed and you have requested strict checking.
Host key verification failed.

网上很多的解决方案是:vi ~/.ssh/known_hosts 删除与想要连接的主机相关的行;或者直接删除known_hosts 这个文件。 当然这个方案也是可行的,但并非解决问题的根本办法,因为继续使用,今后还会出现这样的情况,还得再删除。
下面简单讲一下这个问题的原理和比较长久的解决方案。
OpenSSH的人都知ssh会把你每个你访问过计算机的公钥(public key)都记录在 ~/.ssh/known_hosts。当下次访问相同计算机时,OpenSSH 会核对公钥。如果公钥不同,OpenSSH 会发出警告,避免你受到DNS Hijack之类的攻击。
SSH 对主机的 public_key 的检查等级是根据 StrictHostKeyChecking 变量来配置的。默认情况下,StrictHostKeyChecking=ask。简单所下它的三种配置值:

1.StrictHostKeyChecking=no 最不安全的级别,当然也没有那么多烦人的提示了,相对安全的内网测试时建议使用。如果连接server的 key 在本地不存在,那么就自动添加到文件中(默认是known_hosts),并且给出一个警告。

2.StrictHostKeyChecking=ask  默认的级别,就是出现刚才的提示了。如果连接和key不匹配,给出提示,并拒绝登录。

3.StrictHostKeyChecking=yes  最安全的级别,如果连接与key不匹配,就拒绝连接,不会提示详细信息。

对于我来说,在内网的进行的一些测试,为了方便,选择最低的安全级别。在 .ssh/config(或者 /etc/ssh/ssh_config )中配置:

1
2
 StrictHostKeyChecking no
UserKnownHostsFile /dev/null

(注:这里为了简便,将 knownhostfile 设为 /dev/null,就不保存在 known_hosts 中了)

最近正在迁移自己的小项目,项目之前是基于 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
14
15
16
17
18
19
20
21
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](https://godoc.org/golang.org/x/crypto/bcrypt) 包提供的`GenerateFromPassword(password []byte, cost int)([]byte, error)`方法对用户的密码进行 hash 和 salt 加密了。
> GenerateFromPassword 方法以给定 cost 值返回密码的 Bcrypt 算法的 Hash 值,如果提供的 cost 值小于 Mincost 的话,将会默认使用 DefaultCost 代替
>
使用 `GenerateFromPassword` 函数的一个优势就是我们不需要自己来编写函数来生成 Salt,因为它会为我们自动生成一个 Salt。

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

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

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

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

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

$ Enter a password
$ foobar
Salted Hash $2a$10$………..

1
2
3
4
5
6
7
8
这里需要的注意的是我使用 for 循环调用函数,直到我强制停止它。对于那些不熟悉 GO 的人来讲,这个就和其他语言的 `while (true){}`是一样的效果。
### 验证密码
最后一件事儿就是需要验证密码的正确性来登陆我们的系统,我们可以使用 [bcrypt](https://godoc.org/golang.org/x/crypto/bcrypt) 包提供的`CompareHashAndPassword(hashedPassword, password []byte) error`函数

> CompareHashAndPassword 将 bcrypt 哈希密码与其纯文本进行比较。 成功时返回nil,失败时返回错误
>
我们使用`CompareHashAndPassword`函数来创建另一个返回 bool 值的函数让我们知道密码是否匹配。

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

}

1
2
3
4
5
6


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

func main() {
for {
pwd := getPwd()
hash := hashAndSalt(pwd)
pwd2 := getPwd()
pwdMatch := comparePasswords(hash, pwd2)
fmt.Println(“Passwords Match?”, pwd)
}
}

1
2
3

### 全部代码

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
12
>$ 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。
还有千万不要 粘贴复制 粘贴复制 粘贴复制