p99延迟是什么?

Go1.14发布时,Sameer Ajmani 再说明其性能提升时提到一个 p99 延迟,查了资料,但是最近突然想起来又忘记了,这里记录下来。

原文是说:

Some data: improved loaded gRPC server throughput by 15% & reduced p99 latency by 36%; reduced worst-case tail latency by up to 2,000x (429 ms ⇒ 192 us) in application benchmx; reduced latency 25% & improved throughput 20% in application benchmx; improved microbenchmx up to 15%

这里提到 Go1.14下 gRPC p99 延迟降低了 36%。

那什么 p99 延迟?简单而言就是一段时间内,所有请求中最快的 99% 请求平均延迟,同理p95就是快的95%的请求延迟。p99延迟能直观的衡量服务器性能指标。

StackFlow 的一个答案举了一个例子:

1
2
3
4
5
6
7
8
9
Latency    Number of requests
1s 5
2s 5
3s 10
4s 40
5s 20
6s 15
7s 4
8s 1

7秒内完成了 99 个请求,那么 p99 就是 7秒。

Rust Iterator 迭代器

在 Rust 中很多常用数据结构,例如Vector、HashMap 都内置了迭代器,消费这些结构直接只使用 for...in 的结构即可。

1
2
3
4
let v = vec![1, 2, 3];
for i in v {
println!("{}", i);
}

如果要自定义一个迭代器,只要实现 Iterator trait 即可,每次遍历都是运行 next 方法,直到返回 None。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pub struct Counter {
pub count: usize,
}

impl Iterator for Counter {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
self.count += 1;
if self.count < 6 {
Some(self.count)
} else {
None
}
}
}

如下所示,可以直接使用 for 进行遍历。

1
2
3
4
5
6
fn main() {
let c = Counter { count: 0 };
for i in c {
println!("{}", i);
}
}

不过你可能会发现,这里的变量 c 是不可变的,但是我们定义的 Iterator.next 需要是可变的。

但是这个编译器没有报错,可以正常运行,那这怎么回事?

事实上 for...in 只是一个语法糖,上面的代码转化为运行的代码是下面这个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
let c = Counter { count: 0 };
match IntoIterator::into_iter(c) {
mut iter => loop {
let next;
match iter.next() {
Some(val) => next = val,
None => break,
};
let x = next;
let () = {
println!("{}", x);
};
},
};
}

这里 c 转化成 iterator::IntoIterator 并调用 into_iter() 方法,由于 Counter 没有实现 Copy trait,c 也会移动到 iter 可变变量上,所以这里就可以修改内部变量。

内部会不断循环调用,知道 next 方法返回 None。

所以 for...in 在类型没有实现 Copy 的时候会移动变量,后续也不能再使用这个变量。

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let c = Counter { count: 0 };
for i in c {
println!("{}", i);
}
println!("{}", c.count); // Error
// borrow of moved value: `c`
// value borrowed here after moverustc(E0382)
// main.rs(19, 14): value moved here
// main.rs(18, 9): move occurs because `c` has type `Counter`, which does not implement the `Copy` trait
// main.rs(19, 14): consider borrowing to avoid moving into the for loop
}

如果我们为 Counter 加上 Copy 和 Clone trait,那么这里就不会报错了,但是由于 for 循环仅仅复制了 c 数据,所以最后还是打印值为 0

1
2
3
4
5
6
7
8
9
10
11
12
#[derive(Copy, Clone)]
pub struct Counter {
pub count: usize,
}

fn main() {
let c = Counter { count: 0 };
for i in c {
println!("{}", i);
}
println!("{}", c.count); // 打印为 0
}

Iterator 内置了很多高阶函数, 类如 map/reduce 也都支持。

1
2
3
4
5
pub fn main() {
let a = vec![1, 2, 3, 4, 5];
let b: Vec<i32> = a.iter().map(|&x| x * 2).collect();
assert_eq!(b, vec![2, 4, 6, 8, 10]);
}

这里需要两点,一是迭代器是惰性的,链式调用的最后需要使用 collect() 进行收集;二是消费迭代器不能自动类型推导,需要手动定义类型,这里给变量定义类型或使用 turbofish,另外这个类型需要满足 FromIterator<T>,一般而言用 Vec<T> 即可。

1
2
3
4
5
pub fn main() {
let a = [1, 2, 3];
let _ = a.iter().map(|&x| x * 2).collect::<Vec<i32>>();
let _: Vec<i32> = a.iter().map(|&x| x * 2).collect();
}

对于 reduce 方法,在 rust 中名称为 fn fold<B, F>(self, init: B, f: F) -> B

比如计算从 1 到 3 的和,初始值为 1。

1
2
3
4
5
pub fn main() {
let a = [1, 2, 3];
let sum = a.iter().fold(1, |acc, x| acc + x);
assert_eq!(sum, 7);
}

如果是计算和的话,还有更简单的方法,使用内置的 sum 方法:

1
2
3
4
5
pub fn main() {
let a = [1, 1, 2, 3];
let sum: i32 = a.iter().sum();
assert_eq!(sum, 7);
}

也有计算阶乘的内置方法 product()

1
2
3
4
5
6
fn factorial(n: u32) -> u32 {
(1..=n).product()
}
assert_eq!(factorial(0), 1);
assert_eq!(factorial(1), 1);
assert_eq!(factorial(5), 120);

Rust 中的 From 和 Into trait

Rust 中 From trait 定义一个类型如何转换为另一个类型的过程,还有些类似于构造函数。比如最常见的可以将 str 转换为 String

1
2
let my_str = "hello";
let my_string = String::from(my_str);

再比如将一个原始 i32 数字转化为 Number 类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use std::convert::From;

#[derive(Debug)]
struct Number {
value: i32,
}

impl From<i32> for Number {
fn from(item: i32) -> Self {
Number { value: item }
}
}

fn main() {
let num = Number::from(30);
println!("My number is {:?}", num);
}

如果一个类型实现了 From,那么也自动实现了 Into trait,Into 实际上是 From 的逆运算。由于类型可能有多个 From 实现,那么需要具体声明返回值类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use std::convert::From;

#[derive(Debug)]
struct Number {
value: i32,
}

impl From<i32> for Number {
fn from(item: i32) -> Self {
Number { value: item }
}
}

fn main() {
let int = 5;
let num: Number = int.into();
println!("My number is {:?}", num);
}

在密码学库中,也可以定义私钥到公钥的转换。如下所示,ed25519 的私钥生成中,从私钥获取公钥的过程直接由 let pk: PublicKey = (&sk).into(); 一步完成。

具体实现则由 From trait 实现,这样让代码逻辑中的转换实现流程更清晰了。

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
/// An ed25519 keypair.
#[derive(Debug, Default)] // we derive Default in order to use the clear() method in Drop
pub struct Keypair {
/// The secret half of this keypair.
pub secret: SecretKey,
/// The public half of this keypair.
pub public: PublicKey,
}
impl KeyPair {
pub fn generate<R>(csprng: &mut R) -> Keypair
where
R: CryptoRng + RngCore,
{
let sk: SecretKey = SecretKey::generate(csprng);
let pk: PublicKey = (&sk).into();

Keypair{ public: pk, secret: sk }
}
}

/// An EdDSA secret key.
#[derive(Default)] // we derive Default in order to use the clear() method in Drop
pub struct SecretKey(pub(crate) [u8; SECRET_KEY_LENGTH]);

impl AsRef<[u8]> for SecretKey {
fn as_ref(&self) -> &[u8] {
self.as_bytes()
}
}

/// An ed25519 public key.
#[derive(Copy, Clone, Default, Eq, PartialEq)]
pub struct PublicKey(pub(crate) CompressedEdwardsY, pub(crate) EdwardsPoint);

impl<'a> From<&'a SecretKey> for PublicKey {
/// Derive this public key from its corresponding `SecretKey`.
fn from(secret_key: &SecretKey) -> PublicKey {
let mut h: Sha512 = Sha512::new();
let mut hash: [u8; 64] = [0u8; 64];
let mut digest: [u8; 32] = [0u8; 32];

h.input(secret_key.as_bytes());
hash.copy_from_slice(h.result().as_slice());

digest.copy_from_slice(&hash[..32]);

PublicKey::mangle_scalar_bits_and_multiply_by_basepoint_to_produce_public_key(&mut digest)
}
}

代码引用地址:

  1. https://doc.rust-lang.org/rust-by-example/conversion/from_into.html
  2. https://github.com/dalek-cryptography/ed25519-dalek/blob/master/src/secret.rs

GET 请求可不可以附带 body?

正常的认知中,GET 是不可以使用 body 的,如果要使用 body,可以转化成 querystring。

查了些资料,发现规范并没有限制,但是实际使用是有很大区别的。

使用 Go 实现一个 Server,然后使用 curl(v7.54.0),nodejs(v13.5.0),go(v1.13.5) 来实验:

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

import (
"fmt"
"io/ioutil"
"net/http"
)

func main() {
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
data, _ := ioutil.ReadAll(req.Body)
fmt.Println(req.Method)
fmt.Println(req.Header)
fmt.Println(string(data))
fmt.Println()
})
_ = http.ListenAndServe(":8000", nil)
}

使用 Go client

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

import (
"io"
"io/ioutil"
"net/http"
"strings"
)

func main() {
var url = "http://localhost:8000/"
var payload = strings.NewReader("hello,world")

req, err := http.NewRequest(http.MethodGet, url, payload)
if err != nil {
panic(err)
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()

_, _ = io.Copy(ioutil.Discard, resp.Body)
}

Server 正常打印。

使用 curl

1
curl --location --request GET 'localhost:8000' --data-raw 'hello,world'

Server 正常打印,另外 Content-Type 也自动加上了 application/x-www-form-urlencoded。

使用 nodejs 的话,就有很大区别,GET 不会传递 body,而 POST 会。

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
// nodejs version v13.5.0
var https = require('http');

var options = {
'method': 'POST',
'hostname': 'localhost',
'port': 8000,
'path': '/'
};

var req = https.request(options, function (res) {
var chunks = [];
res.on("data", function (chunk) {
chunks.push(chunk);
});

res.on("end", function (chunk) {
var body = Buffer.concat(chunks);
console.log(body.toString());
});

res.on("error", function (error) {
console.error(error);
});
});

req.write("hello,world");

req.end();

切换到 postman 实现,postman 是支持 GET with body 的。

solidity 重入攻击和预防

solidity 执行是单线程事务执行的,所以没有并发也就没有了竞态,所以怎么可以进行重入攻击呢?

如下所示代码,警告⚠️:下面代码有严重bug!

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
pragma solidity ^0.7.0;

interface IWallet {
function deposit() external payable;
function withdraw(uint _amount) external;
function balanceOf(address _address) external view returns (uint256);
}

contract Victim is IWallet {
mapping(address => uint256) public balances;

function deposit() public payable override {
balances[msg.sender] += msg.value;
}

function balanceOf(address _address) public view override returns (uint256){
return balances[_address];
}

function withdraw(uint _amount) public override {
require(balances[msg.sender] >= _amount);
(bool success,) = msg.sender.call{value: _amount}(new bytes(0));
require(success);
balances[msg.sender] -= _amount;
}
}

这是一个钱包,之前存取,看起没有什么问题,但是如果我们withdraw发送到一个合约里面,我们可以利用合约重新调用 withdraw 就可以耗尽钱包里所有的钱。

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
contract Hacker {
address payable public owner;
IWallet public wallet;

constructor (IWallet _wallet) {
owner = msg.sender;
wallet = _wallet;
}

function deposit() public payable {
wallet.deposit{value: msg.value}();
}

function withdraw() public payable {
require(msg.sender == owner);
wallet.withdraw(wallet.balanceOf(address(this)));
}

fallback() external payable {
if (msg.sender == address(wallet)){
uint balance = wallet.balanceOf(address(this));
if (msg.sender.balance >= balance){
wallet.withdraw(balance);
}
}
}

function flush() public {
require(msg.sender == owner);
selfdestruct(owner);
}
}

问题出在哪里?Victim.withdraw 仅开始检查了余额,然后最后才扣减余额,那么我们就可以再目标合约 fallback 里面重新调用 Victim.withdraw 直到耗尽合约内的钱。

不仅如此,耗尽之后,调用栈结束后没有检查溢出,Hacker 合约的钱凭空变的更多!

1
balances[msg.sender] -= _amount;

最简单的解决方式是校验完余额然后立即扣减余额,这样重入的时候余额就不会检查失败。

1
2
3
4
5
6
function withdraw(uint _amount) public override {
require(balances[msg.sender] >= _amount);
balances[msg.sender] -= _amount; // 转账之前进行扣减
(bool success,) = msg.sender.call{value: _amount}(new bytes(0));
require(success);
}

除此之外,我们可以设计锁机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
contract Victim is IWallet {
mapping(address => uint256) public balances;
bool private locked;

modifier Mutex {
require(!locked, "locked!");
locked = true;
_;
locked = false;
}

function withdraw(uint _amount) public override Mutex {
require(balances[msg.sender] >= _amount);
balances[msg.sender] -= _amount;
(bool success,) = msg.sender.call{value: _amount}(new bytes(0));
require(success);
}
}

Go中可比较性和有序性

什么是可比较性,简单来说就是可以使用比较运算符的类型具有可比较性。

比较运算符又分为等于、大于、小于等形式。可以使用等于和不等的那么就代表此类型具有可比较性。可以使用大于和小于的的类型,那么就说明此类型具有有序性。

一般而言:

  • 布尔类型具有可比较性。两个布尔值相等,那么说明二者都是 true 或 false。
  • 整型和浮点型这个大家都知道,同时具有可比较性和有序性。

可比较性有个前提,比较运算符两侧是可以互相赋值的,int 和 float64 都具有可比较性,但是二者不可以进行比较。

那么字符串呢?

字符串可以进行比较,也具有有序性,两个字符串相比较,会按照字节顺序一个接一个的按照字典序比较,如果都相同那么就相同。如果不相同,怎么比较大小?

不是按照长度,而是按照相同位置的大小决定。如下所示: a1 是小于 b 的。

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

import (
"fmt"
)

func main() {
var a = "a"
var b = "b"
var a1 = "a1"

fmt.Println(a < b, a < a1, a1 < b) // true true true
}

指针是具有可比较性,两个指针如果指向同一个值,那么就相等,但是不具有有序性。

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

import (
"fmt"
)

func main() {
var i int

var x = &i
var y = &i

fmt.Println(x == y) // true
}

channel 也具有可比较性,如果两个 channel 相等,那么代表二者创建时使用了同一个 make 或者二者都是 nil。

值得注意的是两个 nil 的 channel 如果不是相同类型,那是不可以比较的,我们上述所述的可比较行和有序性都建立在二者是具有相同的可赋值性。

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

import (
"fmt"
)

func main() {
var i chan int
var j chan int
var x chan string

fmt.Println(i == j) // false
fmt.Println(i == x) // 不能编译
}

数组是可以比较的,如果两个数组元素是可比较的,且具有相同长度和相同值,那么二者就是相等的,但数组不具有有序性。

slice 不同于数组,不能进行比较。

接口也具有可比较性,如果两个接口如果相等,那么具有相同的动态类型和值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
)

func main() {
var x interface{} = (*int)(nil)
var y interface{} = (*float64)(nil)

fmt.Println(x == y) // false

var i interface{} = 1
var j interface{} = 1
fmt.Println(i == j) // true

var a interface{}
var b interface{}
fmt.Println(a == b) // true
}

如果接口底层类型是 slice 这种不可比较的会发生?目前1.14的编译器还无法编译时判断,这里会造成运行时 panic

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

import "fmt"

func main() {
var x interface{} = []int{1}
var y interface{} = []int{1}

// panic: runtime error: comparing uncomparable type []int
fmt.Println(x == y)
}

如果底层类型不同,但是都具有可比较性的两个接口值进行互相比较,那么不会报错,而是返回 false。

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

import "fmt"

func main() {
var x interface{} = 1
var y interface{} = 1.0

fmt.Println(x == y) // false

}

一个接口值和一个非接口值,也是可以比较的,只要接口值的底层类型和值与非接口值相同,那么二者相等。

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

import "fmt"

func main() {
var x interface{} = 1
var y interface{} = 1.0

fmt.Println(x == 1, y == "string") // true, false
}

如果结构体所有字段都是可比较的,那么也是可比较的,如果两个结构体的非空字段都是相等的,那么二者相等。

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

import "fmt"

func main() {
var x = struct{ Name string }{"string"}
var y = struct{ Name string }{"string"}
fmt.Println(x == y) // true

var a = struct{ _ int }{100}
var b = struct{ _ int }{200}
fmt.Println(a == b) // true
}

slice/map/function 都不能进行比较,但是可以和 nil 进行比较。

可比较性是非常重要的知识点,因为 map 类型的 key 必须是可比较的,所以有些面试题会出 map 的 key 可以是哪些类型,尤其会问,key 是否可以是 slice 和 interface{}。

Solidity v0.6.0 主要功能和破坏性更新示例

重载关键字

和 C++ 中面向对象继承类似,Solidity 提供了 virtual 和 override 关键字。继承抽象合约或者合约接口需要加上 overide 关键字。

1
2
3
4
5
6
7
8
9
abstract contract Feline {
function utterance() public virtual returns (bytes32);
function running() public {
}
}

contract Cat is Feline {
function utterance() public override returns (bytes32) { return "miaow"; }
}

回退函数更改

1
2
3
4
5
6
7
8
9
10
contract Wallet {
// 5.0 中用于接收以太坊的回退函数
function () external payable { }

// 6.0 需要更改为这样
receive() external payable {}

// 或者这样,如不接收 ETH 则 payable 为可选
fallback() external payable {}
}

异常捕获

当进行外部非低级调用时,异常会冒泡到上层,v6.0 加入 try…catch 可以捕获到下层异常。

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
pragma solidity ^0.6.0;

interface DataFeed { function getData(address token) external returns (uint value); }

contract FeedConsumer {
DataFeed feed;
uint errorCount;
function rate(address token) public returns (uint value, bool success) {
// Permanently disable the mechanism if there are
// more than 10 errors.
require(errorCount < 10);
try feed.getData(token) returns (uint v) {
return (v, true);
} catch Error(string memory /*reason*/) {
// This is executed in case
// revert was called inside getData
// and a reason string was provided.
errorCount++;
return (0, false);
} catch (bytes memory /*lowLevelData*/) {
// This is executed in case revert() was used
// or there was a failing assertion, division
// by zero, etc. inside getData.
errorCount++;
return (0, false);
}
}
}

try 后接外部调用或者合约创建的表达式。当前版本仅支持 string 的错误类型,后续会增加更多错误类型,具体可以参考 try…catch 文档

其它更新

  1. array.push() 不再返回新长度
  2. 增加数组容量不可使用 array.length++,需要改成 array.push()
  3. 减少数组容量不可使用 array.length–,需要改成 array.pop()
  4. 可以使用 payable(address) 强制转化 address 为 address payable
  5. struct 和 enum 可以在文件全局声明
  6. 更多更新可以参考文档或者changelog

Go1.14 重要新特性

语言变更

内嵌接口方法集允许部分重叠

接口(interface)是方法集合,方法必须有唯一的非空名称。

之前的接口规范:一个接口 T 可以在方法集中使用接口 E,这里的 E 称之为 T 的内嵌接口,使得 E 中的所有公有和私有方法被添加到接口 T 中。

所以如下所示的 ReadWriteCloser 多个内嵌接口拥有同名 Close 方法是不允许的。

1
2
3
4
type ReadWriteCloser interface {
io.ReadCloser
io.WriteCloser
}

Go1.14 现在支持这种语法形式。

现在的接口规范:一个接口 T 可以在方法集中使用接口 E,这里的 E 称之为 T 的内嵌接口,T 的方法集是 T 中明确声明的方法以及T内嵌接口中方法的结合。这个方法集包含所有公有和私有方法,同名的方法必须拥有相同的函数签名。

以下为规范的示例:

显式重复多次声明相同的方法依然是不被允许的

1
2
3
4
type I interface {
m()
m() // 非法,m()已经显式声明
}

在 Go1.14 之前是允许内嵌多个空接口

1
2
type E interface {}
type I interface { E; E } // 总是合法

在 Go1.14 之后允许内嵌多个非空接口:

1
2
type E interface { m() }
type I interface { E; E } // 现已合法

如果不同的内嵌接口提供了同名方法,这些方法必须拥有相同的签名,否则就是非法的:

1
2
3
type E1 interface { m(x int) bool }
type E2 interface { m(x float32) bool }
type I interface { E1; E2 } // 非法:E1.m 和 E2.m 同名但是方法签名不同

Go命令变更

自动支持 vendor

如果当前 main 包基目录下包含一个 vendor 目录,并且 go.mod 显式说明仅支持 go1.14 或更高版本,那么所有的 go 命令都会显式的加入 -mod=vendor。另外一个新的命令行参数 -mod=mod,在没有 vendor 目录的时候会从模块本地缓存中读取。

当默认或者显式 -mod=vendor 的时候,go命令会校验 vendor/modules.txt 文件和 go.mod 声明的是一致的。

支持非TLS的私有仓库

在很久之前可以使用 go get -insecure 下载非 TLS 的私有仓库包,但是 go module 后不支持这种方式。

Go1.14现在支持声明环境变量的方式支持非安全连接的私有包,类似 GOPRIVATE 只需要定义逗号分割的 GOINSECURE 环境变量即可。

运行时变更

更快的 defer,而且是几乎零开销。现在 defer 可以在对性能至关重要的代码中使用,而无需担心新能开销。

Go 协程现在可以异步抢占,减少调度程序死锁或延迟垃圾回收的可能性。

timer 在调度中减少锁使用和上下文切换,性能提升明显。详情可以参考 《Go夜读 Go time.Timer 源码分析(Go1.14)》。

标准库变更

  1. 新包 hash/maphash 提供非密码学安全的哈希算法
  2. tls 移除 SSLv3 支持,TLS 1.3 默认开启

解决macOS Catalina (v10.15)无法编译c++扩展库的问题

升级到 macos v10.15 之后,nodejs无法正常编译 c++ 库,node-gyp 文档给了解决方案,本篇是其的简短翻译。

确认 Xcode 命令行工具是否安装

  1. /usr/sbin/pkgutil --packages | grep CL 应该返回数据
  2. /usr/sbin/pkgutil --pkg-info com.apple.pkg.CLTools_Executables 应该返回 version: 11.0.0 或之后版本。

如果都正常返回数据,那么你应该重新安装 node-gyp。

否则就需要安装 xcode 或者 xcode-common-line,第二个文件较小但可能不解决问题,那么就需要在 app store 安装 Xcode (大概8GB) 。

安装后,打开 xcode,选择 Preferences > Locations ,如果 Command Line Tool 为空,那么需要先选择一个。

截屏2020-02-19下午8 04 35

然后可以重新尝试安装 c++ 库。

基于x509的认证授权技术

认证方式有很多,比如使用用户名密码的BasicAuth,使用 AccessToken 的 OAuth 2.0 等等,还有一个这篇文章要写的基于 X509 的认证方式,这个不太常见,目前我就在k8s api server中见到。

认证的本质就是获取并确定请求方的身份。回想一下在 TLS 中确认身份的情况,在握手中服务端要返回给客户端证书,客户端要检验服务端提供的证书合法性,校验通过后再进行通信。

大多数的使用场景都是客户端检验服务端证书,但是有没有服务端要求校验客户端证书的?有,并且是TLS 标准,所以基于此我们就可以实现基于 X509 的认证方式。

这就比一般的 TLS 握手多了一个过程,服务端要求客户端必须提供证书并进行校验,校验通过再进行之后的握手,这个互相认证的流程也叫做 mTLS。

在 x509 v3 证书中有一个 ExtKeyUsage 字段,是一个数组,按照最小授权权限原则,对于 Server 而言,这里可以选择服务端认证,而 Client 选择客户端认证即可。

1
2
3
4
5
6
const (
ExtKeyUsageAny ExtKeyUsage = iota
ExtKeyUsageServerAuth // 服务端认证
ExtKeyUsageClientAuth // 客户端认证
// .... 这里省略其它扩展选项
)

这种方式在内网中使用极为方便,如果我们要求访问认证有过期时间,那么也不需要在数据库系统中记录过期时间,只要颁发的证书设置 NotAfter 字段即可。

至此,我们保证通信两端都是信任CA颁发的。不过还需要获取证书端的具体身份信息,这个在证书内也有提供。

证书内提供了国家、地区、组织、通用名称等字段,这个就可以用作授权的身份信息。

1
2
3
4
5
6
7
8
9
type Name struct {
Country, Organization, OrganizationalUnit []string
Locality, Province []string
StreetAddress, PostalCode []string
SerialNumber, CommonName string

Names []AttributeTypeAndValue
ExtraNames []AttributeTypeAndValue // Go 1.5
}

这里 CommonName 通用名称可以视作用户身份标识符,Organization 组织名称可以视作用户组。通常情况使用这两个字段进行授权操作就足够了,一般很多场景都只需要使用 CommonName 就可以了。

这就要求颁发证书需要保证这两个字段的正确性,以及通用名称字段的唯一性。所以如果是高安全等级的场景可以在证书颁发的时候加入人工审核环节。

下面使用 Go 实现双向认证,服务端需要配置信任CA和要求客户端认证即可:

1
2
3
4
5
6
7
server := http.Server{
TLSConfig: &tls.Config{
ClientAuth: tls.RequireAndVerifyClientCert, // 客户端必须要提供证书
Certificates: []tls.Certificate{}, // 服务端证书
ClientCAs: x509.NewCertPool(), // 校验客户端证书的CA集合
},
}

这个过程主要需要配置是客户端,不过也很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
httpclient := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
Certificates: []tls.Certificate{}, // 客户端证书
RootCAs: x509.NewCertPool(), // 校验服务端证书CA集合
},
},
}

req, _ := http.NewRequest(http.MethodGet, "https://localhost:8443/anycall", nil)
resp, err := httpclient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
_, _ = io.Copy(os.StdOut, resp.body)

这样通信过程认证过程就可以在底层 TLS 握手时进行,服务端应用层“可以不再”需要进行任何配置。

1
2
3
4
5
6
7
8
9
10
11
// 授权方式示例
http.HandleFunc("/anycall", func(w http.ResponseWriter, req *http.Request) {
commonName := req.TLS.PeerCertificates[0].Subject.CommonName
if commonName != "客户端通用名称" {
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte("权限不足"))
return
}

_, _ = w.Write([]byte("get success"))
})

不过如果CA被恶意的重复颁发一个相同通用名称的证书,就会造成服务端错误的识别证书,不过可以用证书指纹判断是否与配置数据一致。

这样也造成了一定的麻烦,需要颁发证书就得修改。这个过程适用于特别特别注重安全的场景使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
http.HandleFunc("/anycall", func(w http.ResponseWriter, req *http.Request) {
clientCertificate := req.TLS.PeerCertificates[0]
commonName := clientCertificate.Subject.CommonName
if commonName != "客户端通用名称" {
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte("权限不足"))
return
}

// 多加一次证书指纹判断,另外错误的颁发证书应该进行报警
hash := sha256.Sum256(clientCertificate.Raw)
if !hmac.Equal(hash[:], []byte("HASH_AT_PRE_CONFIG")) {
_, _ = w.Write([]byte("无法匹配证书"))
return
}

_, _ = w.Write([]byte("get success"))
})

当然客户端也可以验证服务端证书指纹,不过这个有个专有名称叫做 HTTP Public Key Pinning (HPKP)

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
httpclient := &http.Client{
Transport: &http.Transport{
Dial: func(network, addr string) (net.Conn, error) {
// 第三个参数 *tls.Cofnig 按照上文说明填写
c, err := tls.Dial(network, addr, YOUR_TLS_CONFIG)
if err != nil {
return nil, err
}

// 获取HPKP并校验,视情况可只验证第一个的证书,而不是所有的证书链中所有证书
var hasOne bool
for _, certificate := range c.ConnectionState().PeerCertificates {
hash := sha256.Sum256(certificate.Raw)
if hmac.Equal(hash[:], []byte(nil)) {
hasOne = true
break
}
}

if !hasOne {
return nil, errors.New("hpkp verifies failed")
}

return c, nil
},
},
}

更具体的授权操作可以根据 RBAC 形式进行,这个和传统流程一致,就不再赘述。

如果你正在使用 gRPC,我写了一个 go-example,可以参考这个项目