Deepzz's Blog https://deepzz.com 数字游民 & Indie Hacker zh-CN Tue, 19 Mar 2024 08:18:44 +0000 2023年终总结 - 焦虑的一年 https://deepzz.com/post/2023-year-end.html https://deepzz.com/post/2023-year-end.html#comments https://deepzz.com/post/2023-year-end.html 该篇总结是我第一次严格意义上的年终总结,也到了必须要进行总结的时候。总结经验,反思过往!

相较于以往,每年的这个时候都会看到很多人在对自己进行总结。对于自己,是一个性格比较随意的一个人,总结不总结的也无所谓,只要内心对自己有要求,有思考就行。但对于很多事和物,没有纸笔的记录很快就会忘却,2023年对于我来说是挺焦虑的一年,希望记录下对这些事情,以作反省。

先说说自己,性格是一个比较独立的一个人,不想去处理那些复杂的人际关系,不喜欢,也是浪费时间(当然这肯定是不对的,大家不要学我)。简简单单的就挺好,没有利益,没有冲突,有兴趣的就一块玩,没有的我们下次找机会。平时个人想法也挺多的,就是执行力需要提高。

从标题可以反映出我2023焦虑的一年,可以从工作上和生活中来进行总结。

工作上

我目前在一家安全相关的公司从事产品研发的工作,公司不大但老板有技术背景,对技术比较看重。当时过去主要工作内容是数字身份产品的研发(以创业的心态),包括IDaaS平台(OAuth2/OIDC,SAML,LDAP等)、无密码身份认证(WebAuthn/FIDO2)、协同签名和S/MIME相关的产品。

由于各种原因吧,如产品方向和研发方案,经过3年的产品研发,最终的产出很少或者说没有应用场景。个人挺苦恼的,毕竟作为研发研发出没有产生价值的产品真的很没有成就感,产品没价值就代表研发没价值。

2023最主要是将我们的产品通过国密L2认证,这也是痛苦的根源,目前也仅通过一半。起初从已通过小伙伴处得知过认证还是比较容易,检测人员有提供部分指导。自从我们从接手项目开始,整个文档或者说产品的方案就不满足检测要求,才知道有多难。整个过程下来文档修改过不下百遍,在一遍遍修改和沟通下才艰难完成部分项目的认证,期间还收到整改通知。

总结下来:

  1. 对检测要求和产品设计的不了解(过检产品由他人研发),沟通和功能修改过程也挺难的。
  2. 不能汲取公司内部已过产品的经验,该过检产品非标品,有很大不确定性。仅能部分参考。
  3. 检测机构为北京国密检测局且今年格外严格。

我认为这个过程对我来说是焦虑的,首先它不是的我强项(产品研发),其次它的认证过程是我不能够把控的。当一件事情不在我的控制范围内的时候,充满了不确定性,就像一个快断线风筝,指不定什么时候就失败了,它就会让我变得焦虑。我的性格不容许有这样的事情发生。

同时,2023年较年初定下的目标完成了不到50%,未能达成年初计划,其中有研发方向的变化,有人员的变化。

不过今年公司产研的变化让我看到希望,从最初无产品经理到目前三名产品经理。现在产品的立项也较之前更严谨,不再是一股脑拍板觉得这个可以做,这个方向我看到了机会,今天行明天不行。我相信在产品经理的介入下,通过调研用数据说话,更多人的参与立项讨论,专业的事交给专业的人,以后会更加规范,做的产品会更有价值。

从根本上来说,利用每个人的长处去做事情一定是事半功倍的,有时候静下心来好好思考,方向比忙得不可开交更加重要,观察与发现比我觉得更合适。2023年工作上的变化就是没有变化,现在看来,除了埋头干活或许有意提升自身的价值更加重要。

生活中

今年生活上最大的变化是我们家里的天使宝宝降临,给我们整个家庭带来了很多变化。

宝宝出生前,家里就开始囤各种婴儿用品,什么婴儿床、婴儿车、小玩具、纸尿裤、抱被什么的,各种各样,以前我们小时候咋就没这么多玩意。同时两边的父母也过来帮着带带孩子煮个饭照顾我们的饮食,不然两个人确实转不过来。

孩子出生后本以为大人可以轻松一点,谁知孩他妈还堵奶,最严重的是最后还发炎了,痛的恼火得很。最后找通乳师通了五六次才消下去(新生孩母亲一定要注意这个问题)。孩子每几个小时就需要喂一次奶,有时候还一直哭闹,这让你睡眠和精力严重不足。但这就是人生需要经历的一件事,孩子有时候一个笑容就会把你治愈。加油吧,少年!

今年给自己找了一个锻炼的理由,一狠心给自己添置了一台公路车,国庆节第一次就去骑行了95公里的绿道,那感觉很酸爽。身边的朋友也一起购置了,以后跟这大神一起拉链。

2024计划

个人比较向往独立开发者,有经验的朋友的可以带带新人。

装了一年多的房子打算2024年初入住。由于房子不是在工作附近,每次装修或安装什么东西就得安排时间,一来二去整个房子很多细节都没有弄好,不过有什么办法只有妥协,下一次不就有经验了嘛。

年前和朋友聊了许久,感觉每个人都有每个人的苦恼,都过得不容易,比如为了下一代怎么怎么的。要我说,有很多苦恼都是自己给自己的压力。只有先把自己安排好,才能安排别人。一辈子很短,快乐向上的活下去!

2024年,给自己立一些Flag:

  • 产品突破0的销售额。
  • 读10本书,无论什么书。
  • 骑行1000公里。
  • 开始运营小红书和Twitter,粉丝数突破5000。

2024让自己发生点变化。

本文链接:https://deepzz.com/post/2023-year-end.html参与评论 »

]]>
Mon, 01 Jan 2024 22:49:00 +0800
如何玩转智能家居 - 组建局域网,访问家庭网络 https://deepzz.com/post/openwrt-and-wireguard-connect-homenet-anywhere.html https://deepzz.com/post/openwrt-and-wireguard-connect-homenet-anywhere.html#comments https://deepzz.com/post/openwrt-and-wireguard-connect-homenet-anywhere.html 接上篇文章,继续折腾家庭网络:如何玩转智能家居 - 网络如何智能openwrt+v2ray

更新说明

  • 2023.02.24:R2S不再作为主路由,采用旁路由接入

本篇文章解决问题是:如何通过配置wireguard实现从世界任何地方优雅的访问家庭网络。我们在家庭网络使用场景汇总一定会遇到以下情况,人在外:

  1. 想要访问家庭网络中的NAS服务,获取一些资料。
  2. 想要下载一部资源,添加下载任务,一回家就可使用
  3. 全屋智能家居,某一个设备出现故障,想要直接接入家庭网络查看原因修复
  4. 等等…

这些场景的基本需求是:安全性一定要有保障,能够在外网连接到家庭网络(不是简单的内网穿透,而是组成大局域网)。

答案不言而喻,组建vpn。那么问题来了,用什么vpn,如何连接到家庭网络:

  • 没有公网IP,现在IPv4已经耗尽,想要从运营商那获得公网IP还是很困难的,而且IPv6目前还不是很普及。
  • 家庭内部网络安全性脆弱,需要防火墙来保护,一般的不应该将家庭网络中的服务暴露在公网中,这样会有极大的安全隐患。

本篇文章的目的是:通过vpn的形式使得在外网如同访问家庭内部局域网一样,让外部设备与家庭网络中的设备形成一个局域网。

那为什么选择wireguard:

  • 开源:https://github.com/WireGuard
  • 简单:代码简单,仅4000余行,原理简单,部署简单。
  • 安全:支持最新加密技术,Curve25519、ChaCha20等。
  • 性能:WireGuard 虚拟网由于是100%内核处理,省去了用户进程和内核交互的开销,因此性能优越,具体表现为吞吐量高。

网络架构: homenet-design

博主目前设计的网络架构如上图,不过本篇文章只讲 光猫<->AC<->R2S 这样的链路:

光猫:192.168.1.1
AC(主路由):192.168.2.1
R2S(旁路由):192.168.2.100

这里的R2S我们作为旁路由来使用,什么是旁路由请自行搜索资料。简而言之,家庭内大多数网络均走AC主路由,部分走旁路由(翻墙),旁路由的(折腾)挂掉不会导致整个家里网络中断。

由于博主家庭网络没有公网IP,动态IP也没有,因此这里找了一台云服务器作为中转(非UDP打洞,中转方式会消耗云服务器流量,请悉知)。当然也可以用基于wireguard现成的方案,如tailscale,免费应该够用(有设备限制)。

提前准备

这里假设你已经准备好已经准备好openwrt路由器。我们还需要准备:

一个公网IP:或是家里自带公网IP+DDNS,或是购买一台公网服务器VPS
一个域名:可选,如果你的公网IP是动态的,那就需要域名且必须通过DDNS

FRP

因为博主家里没有公网IP,那么只能通过这种内网穿透的方式实现,将wireguard的udp端口通过代理的方式暴露到外网,如果有公网IP跳至下一步。

1、在vps上安装frps服务端,请到 frp/releases 下载对应架构的最新版本即可,解压到服务器。

服务端配置:

[common]
bind_port = 7000

token = 768f72bc664ad2ad7c9edccf65523fd7

其中 token 是用来鉴权客户端的,请重新生成。bind_port 指定 frps 监听的端口,云服务器需要放开该端口的访问。

2、在openwrt安装frpc客户端,请到 System->Software 搜索 frpc 安装。 software-frpc

完成后进行配置:假设这里我们选择端口 51820/udp 作为wireguard的通信端口,IP 10.0.0.1作为wireguard的网关IP。 frpc-service

我这边的 /etc/config/frpc 配置如下,你可对应着在网页上进行修改:

config init
  option stdout '1'
  option stderr '1'
  option respawn '1'
  option user 'root'
  option group 'root'

config conf 'common'
  option server_port '7000'
  option tls_enable 'false'
  option server_addr '<server_ip>'
  option token '768f72bc664ad2ad7c9edccf65523fd7'

config conf 'ssh'
  option name 'wireguard'
  option type 'udp'
  option use_encryption 'false'
  option use_compression 'false'
  option local_ip '10.0.0.1'
  option local_port '51820'
  option remote_port '51820'

记得将 server_ip 替换成frps的地址,如果不出意外的,openwrt中的frpc已经跑起来了。

WireGuard

WireGuard是点对点的网络,每个节点既可以做服务器,又可以做客户端。我们这里将部署在openwrt中的wireguard节点称为服务端,在手机或电脑端部署的wireguard节点称为客户端。更多安装方式:https://www.wireguard.com/install/

整体步骤如下:

  1. LAN口设置(旁路由)
  2. 安装wireguard

LAN口设置

1、编辑LAN口,设置上游网关信息: interfaces-lan

lan-settings

2、配置DNS: lan-dns-settings

3、关闭SYN-flood保护,开启动态IP伪装 wireguard-firewall

安装wireguard

1、在openwrt中安装wireguard建议通过System->Software进行安装:

software-wireguard

安装完成,重启路由器。同时你也会看到 Status->WireGuardwireguard-status

配置服务端

需要了解细节的朋友可以参考官方 Quick Start,我们这里通过openwrt进行wireguard服务端配置。

1、点击 Network->Interface 中的 Add new interface 创建接口: wireguard-interface

2、接口名称建议 wg0,协议选择 WireGuard VPN 进行创建: wireguard-create

3、一般设置:

  • Private Key 是WireGuard节点的私钥,可通过 Generate Key Pair 生成,也可手动生成:

    # 通过ssh登录到openwrt,执行
    $ wg genkey | tee privatekey | wg pubkey > publickey
    $ cat privatekey publickey
    
  • Listen Port 监听的端口,这里设置为 51820 与 frpc 配置对应即可。

  • IP Addresses 是WireGuard接口的私有IP网段,这里设置为 10.0.0.1/24 与 frpc 配置对应,不要与其它内网IP段冲突。

wireguard-general-settings

4、防火墙设置,有 vpnvpn,无则选 lanwireguard-firewall-settings

最后 Save,保存即可,后续再步骤添加 Peers

客户端配置

首先,通过这里的安装方式进行客户端的安装:https://www.wireguard.com/install/,我这里以iOS举例。

1、点击 创建隧道->手动创建wireguard-ios-home

2、填写基本信息,主要填写下面三项,其它默认:

  • 名称,这里随便填
  • 公私钥,这里点击 生成密钥对 随机生成即可。
  • 局域网IP地址,即客户端节点IP,这个地址要设置为符合 10.0.0.1/24

wireguard-ios-settings1

3、通过 添加节点 添加服务器信息:

  • 公钥,填写服务端的公钥。
  • 对端,服务端地址+端口,由于家里没有公网,这里走的frp,所以填的是公网vps代理地址。
  • 路由的IP地址(段),这里填写需要走wireguard网络的IP段,如:192.168.100.0/24,192.168.2.0/0,10.0.0.0/0。这样当开启wireguard vpn之后访问这些IP就会走vpn网络。

wireguard-ios-settings2

到了这一步,客户端已经完成配置了。但是服务端还没有,因为wireguard本质上是一个P2P通信的软件,我们还要将客户端公钥信息配置到服务端才行。

配置Peer

在openwrt配置wireguard peer信息。

wireguard-interface-edit

填写刚才配置的客户端信息,Save 保存即可: wireguard-peer-settings

现在在手机上开启 vpn,尝试访问一下路由器地址 192.168.100.1 是否可行。 wireguard-ios-vpn

FAQ

1、客户端如何访问openwrt的下一级路由的服务,如这里如何访问 192.168.2.x 服务:

因为路由器作为openwrt的下一级路由,openwrt是不知道如何到达 192.168.2.x 这个网段的。因此需要添加一条静态路由: wireguard-static-route

2、如何所有流量都走wireguard,并且可以通过之前设置的v2ray进行科学上网:

请参考:https://www.wireguard.com/netns/#routing-all-your-traffic

3、不用开启端口吗,很多教程上面需要开启路由器端口?

开启端口,一般是指我们路由器有公网IP,通过ddns方式对外暴露端口的时候需要开启。

相关地址

[1] https://github.com/WireGuard

[2] https://www.wireguardconfig.com/

本文链接:https://deepzz.com/post/openwrt-and-wireguard-connect-homenet-anywhere.html参与评论 »

]]>
Sun, 20 Nov 2022 23:45:00 +0800
如何玩转智能家居 - 如何自由上网openwrt+v2ray https://deepzz.com/post/router-openwrt-v2ray-tproxy.html https://deepzz.com/post/router-openwrt-v2ray-tproxy.html#comments https://deepzz.com/post/router-openwrt-v2ray-tproxy.html 本篇文章仅作为个人技术研究记录,请勿用于非法用途。

更新说明

  • 2024.01.30:重新整理文档,基于现有模式。
  • 2023.7.19:添加nftables配置。

玩转智能家居,网络很关键,本篇文章我们来聊一聊怎样实现科学上网。其具体的体现就是,路由器帮我们智能的识别我们想要访问的网站,通过分析判断,将流量通过不同的路径进行转发。使用场景大家可以自己想象,这里不做过多赘述。

本次的实战是:在安装了openwrt的软路由上(主路由),通过v2ray+vps实现的翻墙。本次相关软硬件如下:

  • OpenWrt:22.03.2+,该版本之后使用nftables,下面同时也给出iptables配置。
  • V2ray:4.44.0,当前OpenWrt版本能安装的最高版本。

提前准备

一个路由器:博主使用软路由R2S+16GTF卡(读卡器),当然其它软路由也可或者安装了openwrt的路由器。

一个vps(墙外):用于安装v2ray服务端,帮助翻墙。

v2ray有多种传输方式,这里选用了KCP + wechat-video方式。

安装openwrt

软路由首先要刷好openwrt的系统,如何刷,我这边以R2S为例:

1、安装刷机软件,因为用过树莓派刷机软件挺不错,推荐给大家:https://www.raspberrypi.com/software/raspberrypi-home.png

2、 下载openwrt系统,在这里可以查询你的设备支持的固件 https://firmware-selector.openwrt.org/,记得选择版本号。

3、将TF卡连接至电脑,打开树莓派刷机软件,选择“使用自定义镜像“,选择刚才下载的openwrt系统文件,进行烧录: raspberrypi-image

一旦烧录完成,将TF插入r2s,插电即可启动。

修改网关IP

我们修改openwrt网关IP的目的是为了防止IP地址冲突,默认情况下openwrt使用的是 192.168.1.1,这与大多数光猫或路由器冲突。

将openwrt接入到现有网络,通过路由器或光猫后台查询到该openwrt的IP地址。直接访问该IP地址,密码为空,登录。我这里是更改为 192.168.100.1。选择 Network->Interfacesopenwrt-home

点击 Lan 口的 Edit 按钮,将 IPv4 address 修改为你想设置的地址,保存: openwrt-interface-edit

openwrt-interfaces

点击 Save & Apply 之后,等待片刻,即可使用新的地址 192.168.100.1 访问openwrt。

开启IP转发

本方案中openwrt需要开启IP转发才能作为网关使用:

$ echo net.ipv4.ip_forward=1 >> /etc/sysctl.conf && sysctl -p

执行后将出现 net.ipv4.ip_forward=1 的提示。

安装v2ray

如何安装v2ray?我们需要在vps上和openwrt上安装并配置v2ray的服务端和客户端。

vps服务器

服务器安装可以参考 Github 这个项目一键安装,需要提前切换到root。

$ bash <(curl -L https://raw.githubusercontent.com/v2fly/fhs-install-v2ray/master/install-release.sh)

安装的相关文件地址会打印出来,如下:

installed: /usr/local/bin/v2ray
installed: /usr/local/share/v2ray/geoip.dat
installed: /usr/local/share/v2ray/geosite.dat
installed: /usr/local/etc/v2ray/config.json
installed: /var/log/v2ray/
installed: /var/log/v2ray/access.log
installed: /var/log/v2ray/error.log
installed: /etc/systemd/system/v2ray.service
installed: /etc/systemd/system/v2ray@.service

设置开机自启并启动v2ray服务:

$ systemctl enable v2ray; systemctl start v2ray

修改配置文件 /usr/local/etc/v2ray/config.json

{
  "log": {
    "access": "/var/log/v2ray/access.log",
    "error": "/var/log/v2ray/error.log",
    "loglevel": "info"
  },
  "inbounds": [
    {
      "port": 8545,
      "protocol": "vmess",
      "settings": {
        "clients": [
          {
            "id": "<客户端鉴权uuid>",
            "alterId": 0
          }
        ]
      },
      "streamSettings": {
        "network": "kcp",
        "security": "none",
        "tlsSettings": {},
        "tcpSettings": {},
        "httpSettings": {},
        "kcpSettings": {
          "mtu": 1350,
          "tti": 50,
          "uplinkCapacity": 100,
          "downlinkCapacity": 100,
          "congestion": false,
          "readBufferSize": 2,
          "writeBufferSize": 2,
          "header": {
            "type": "wechat-video"
          }
        },
        "wsSettings": {},
        "quicSettings": {}
      }
    }
  ],
  "outbounds": [
    {
      "protocol": "freedom",
      "settings": {}
    },
    {
      "protocol": "blackhole",
      "settings": {},
      "tag": "block"
    }
  ]
}

这里的uuid是作为鉴权使用,随便生成 $ v2ctl uuid,只需要保证客户端uuid填写一致即可。然后执行以下命令:

$ service v2ray restart # 重启v2ray服务

vps相关的防火墙端口记得开。如果进行调试,可以将日志等级调整为 Debug,并手动启动v2ray服务:v2ray -c /usr/local/etc/v2ray/config.json

OpenWrt

在openwrt上安装v2ray,由于openwrt上未使用bash,故我们不能够使用上面的脚本进行安装,而是采用opkg的方式,请参考 https://github.com/kuoruan/openwrt-v2ray 安装v2ray-core。

也可以直接在openwrt网页上操作,一样的: openwrt-software

openwrt-dpkg

保存后,然后点击 Update lists,完成后搜索v2ray进行安装即可: openwrt-software-search

注意,这里可能由于网络的问题,安装过程较长时间,请耐心等候,或多次尝试。

通过SSH登录到openwrt:ssh root@192.168.100.1,编辑新建配置文件 /etc/config/v2ray

{
    "log": {
        "loglevel": "info"
    },
    "inbounds": [
        {
            "listen": "0.0.0.0",
            "port": 30000,
            "protocol": "dokodemo-door",
            "settings": {
                "network": "tcp,udp",
                "followRedirect": true
            },
            "sniffing": {
                "enabled": true,
                "destOverride": [
                    "http",
                    "tls"
                ]
            },
            "streamSettings": {
                "sockopt": {
                    "tproxy": "tproxy",
                    "mark": 2
                }
            },
            "tag": "transparent"
        },
       {
            "listen": "0.0.0.0",
            "port": 30443,
            "protocol": "socks",
            "settings": {
                "auth": "noauth",
                "udp": false
            }
        },
        {
            "listen": "0.0.0.0",
            "port": 30080,
            "protocol": "http",
            "settings": {
                "timeout": 360
            }
        }
    ],
    "outbounds": [
        {
            "streamSettings": {
                "network": "kcp",
                "security": "none",
                "kcpSettings": {
                    "downlinkCapacity": 20,
                    "writeBufferSize": 1,
                    "readBufferSize": 1,
                    "header": {
                        "type": "wechat-video"
                    },
                    "uplinkCapacity": 50,
                    "congestion": false,
                    "tti": 20,
                    "mtu": 1350
                }
            },
            "tag": "proxy",
            "mux": {
                "concurrency": 8,
                "enabled": false
            },
            "settings": {
                "vnext": [
                    {
                        "users": [
                            {
                                "level": 0,
                                "security": "aes-128-gcm",
                                "alterId": 0,
                                "id": "<客户端鉴权uuid>"
                            }
                        ],
                        "address": "<vps服务器地址>",
                        "port": 8545
                    }
                ]
            },
            "protocol": "vmess"
        },
        {
            "protocol": "freedom",
            "settings": {
                "domainStrategy": "UseIP"
            },
            "streamSettings": {
                "sockopt": {
                    "mark": 2
                }
            },
            "tag": "direct"
        },
        {
            "protocol": "blackhole",
            "settings": {
                "response": {
                    "type": "none"
                }
            },
            "tag": "adblock"
        },
        {
            "protocol": "dns",
            "streamSettings": {
                "sockopt": {
                    "mark": 2
                }
            },
            "tag": "dns-out"
        }
    ],
   "dns": {
        "servers": [
            {
                "address": "223.5.5.5",
                "port": 53,
                "domains": [
                    "geosite:cn",
                    "ntp.org"
                ],
                "expectIPs": [
                    "geoip:cn"
                ]
            },
            {
                "address": "114.114.114.114",
                "port": 53,
                "domains": [
                    "geosite:cn",
                    "ntp.org"
                ],
                "expectIPs": [
                    "geoip:cn"
                ]
            },
            {
                "address": "1.1.1.1",
                "port": 53,
                "domains": [
                    "geosite:geolocation-!cn",
                    "geosite:speedtest"
                ]
            },
            {
                "address": "8.8.8.8",
                "port": 53,
                "domains": [
                    "geosite:geolocation-!cn",
                    "geosite:speedtest"
                ]
            },
            "localhost"
        ]
    },
    "routing": {
        "domainStrategy": "IPOnDemand",
        "rules": [
            {
                "type": "field",
                "inboundTag": [
                    "transparent"
                ],
                "port": 53,
                "network": "udp",
                "outboundTag": "dns-out"
            },
            {
                "type": "field",
                "inboundTag": [
                    "transparent"
                ],
                "port": 123,
                "network": "udp",
                "outboundTag": "direct"
            },
            {
                "type": "field",
                "ip": [
                    "223.5.5.5",
                    "114.114.114.114"
                ],
                "outboundTag": "direct"
            },
            {
                "type": "field",
                "ip": [
                    "8.8.8.8",
                    "1.1.1.1"
                ],
                "outboundTag": "proxy"
            },
            {
                "type": "field",
                "domain": [
                    "geosite:category-ads-all"
                ],
                "outboundTag": "adblock"
            },
            {
                "type": "field",
                "protocol": [
                    "bittorrent"
                ],
                "outboundTag": "direct"
            },
            {
                "type": "field",
                "ip": [
                    "geoip:private",
                    "geoip:cn"
                ],
                "outboundTag": "direct"
            },
            {
                "type": "field",
                "domain": [
                    "geosite:cn"
                ],
                "outboundTag": "direct"
            }
        ]
    }
}

不对配置做过多解释,你需要替换掉配置中需要修改的部分 <客户端鉴权uuid>vps服务器IP。执行 v2ray -c /etc/config/v2ray,等待启动成功。

此时,我们已经可以进行智能上网了,当然这里还没有实现 透明代理。验证方式,通过指定代理:

$ http_proxy=192.168.100.1:30080 curl -Lv google.com # 墙外测试
$ http_proxy=192.168.100.1:30080 curl -Lv baidu.com # 墙内测试

透明代理

这里我们的软路由是作为旁路由使用的,因此需要实现全局的智能上网(透明代理),所有连接到该路由的设备都可以智能的选择网络路线。透明代理原理是通过 nftables 的路由转发等功能。

完成安装后,请ssh到openwrt。iptables 执行:

# 设置策略路由
ip rule add fwmark 1 table 100 
ip route add local 0.0.0.0/0 dev lo table 100


# 代理局域网设备
iptables -t mangle -N V2RAY
# 目标网关所在网段请求直连, 执行:
#  ip address | grep -w "inet" | awk '{print $2}'
iptables -t mangle -A V2RAY -d 10.0.0.0/8 -j RETURN
iptables -t mangle -A V2RAY -d 100.64.0.0/10 -j RETURN
iptables -t mangle -A V2RAY -d 127.0.0.0/8 -j RETURN
iptables -t mangle -A V2RAY -d 169.254.0.0/16 -j RETURN
iptables -t mangle -A V2RAY -d 172.16.0.0/12 -j RETURN
iptables -t mangle -A V2RAY -d 192.0.0.0/24 -j RETURN
# 目标地址为组播IP/E类地址/广播IP直连
iptables -t mangle -A V2RAY -d 224.0.0.0/4 -j RETURN
iptables -t mangle -A V2RAY -d 240.0.0.0/4 -j RETURN
iptables -t mangle -A V2RAY -d 255.255.255.255/32 -j RETURN

iptables -t mangle -A V2RAY -d 192.168.100.0/24 -p tcp ! --dport 53 -j RETURN
iptables -t mangle -A V2RAY -d 192.168.100.0/24 -p udp ! --dport 53 -j RETURN
iptables -t mangle -A V2RAY -m mark --mark 2 -j RETURN

# 给 UDP 打标记 1,转发至 30000 端口
iptables -t mangle -A V2RAY -p udp -j TPROXY --on-port 30000 --tproxy-mark 1
# 给 TCP 打标记 1,转发至 30000 端口
iptables -t mangle -A V2RAY -p tcp -j TPROXY --on-port 30000 --tproxy-mark 1
# 应用规则
iptables -t mangle -A PREROUTING -j V2RAY


# 代理网关本机
iptables -t mangle -N V2RAY_MASK 
# 目标网关所在网段请求直连
# 执行: ip address | grep -w "inet" | awk '{print $2}'
iptables -t mangle -A V2RAY_MASK -d 10.0.0.0/8 -j RETURN
iptables -t mangle -A V2RAY_MASK -d 100.64.0.0/10 -j RETURN
iptables -t mangle -A V2RAY_MASK -d 127.0.0.0/8 -j RETURN
iptables -t mangle -A V2RAY_MASK -d 169.254.0.0/16 -j RETURN
iptables -t mangle -A V2RAY_MASK -d 172.16.0.0/12 -j RETURN
iptables -t mangle -A V2RAY_MASK -d 192.0.0.0/24 -j RETURN
# 目标地址为组播IP/E类地址/广播IP直连
iptables -t mangle -A V2RAY_MASK -d 224.0.0.0/4 -j RETURN 
iptables -t mangle -A V2RAY_MASK -d 240.0.0.0/4 -j RETURN
iptables -t mangle -A V2RAY_MASK -d 255.255.255.255/32 -j RETURN

iptables -t mangle -A V2RAY_MASK -d 192.168.100.0/24 -p tcp ! --dport 53 -j RETURN
iptables -t mangle -A V2RAY_MASK -d 192.168.100.0/24 -p udp ! --dport 53 -j RETURN
iptables -t mangle -A V2RAY_MASK -m mark --mark 2 -j RETURN
# 给流量打标记, 重路由
iptables -t mangle -A V2RAY_MASK -p tcp -j MARK --set-mark 1
iptables -t mangle -A V2RAY_MASK -p udp -j MARK --set-mark 1
# 应用规则
iptables -t mangle -A OUTPUT -j V2RAY_MASK


# DIVERT 规则, 避免已有连接的包二次通过 TPROXY
iptables -t mangle -N DIVERT
iptables -t mangle -A DIVERT -j MARK --set-mark 1
iptables -t mangle -A DIVERT -j ACCEPT
iptables -t mangle -I PREROUTING -p tcp -m socket -j DIVERT

nftbles 执行:

#!/usr/sbin/nft -f

# 清空已有规则
flush ruleset

# 局域网的地址
define RESERVED_IP = {
  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,
  224.0.0.0/4,
  240.0.0.0/4,
  255.255.255.255/32,
}

# 代理设备
table ip v2ray {
  # 局域网
  chain prerouting {
    type filter hook prerouting priority 0; policy accept;
    ip daddr $RESERVED_IP return

    meta l4proto tcp ip daddr 192.168.0.0/16 return # 直连局域网
    ip daddr 192.168.100.0/24 tcp dport != 53 return
    ip daddr 192.168.100.0/24 udp dport != 53 return
    meta mark 2 return # 直连流量
    meta l4proto { tcp, udp } meta mark set 0x1 tproxy to :30000 accept # 转发至v2ray
  }

  # 网关本机 
  chain output {
    type route hook output priority 0; policy accept;
    ip daddr $RESERVED_IP return

    meta l4proto tcp ip daddr 192.168.0.0/16 return # 直连局域网
    ip daddr 192.168.100.0/24 tcp dport != 53 return
    ip daddr 192.168.100.0/24 udp dport != 53 return
    meta mark 2 return # 直连流量
    meta l4proto { tcp, udp } meta mark set 0x1 accept # 重路由至prerouting
  }
}

# DIVERT规则
table ip filter {
  chain divert {
    type filter hook prerouting priority -150; policy accept;
    meta l4proto tcp socket transparent 1 meta mark set 1 accept
  }
}

请注意,这里有代理局域网设备和网关本机的配置,可自行选者。这里 192.168.100.0/24 需要换成你openwrt路由器的IP地址段。进行测试:

$ curl -Lv google.com # 墙外测试
$ curl -Lv baidu.com # 墙内测试

如果执行完上面的 iptablesnftables 命令之后,路由器的访问出现问题,如ssh连不上openwrt,可通过重启路由器的方式解决。因为这里的规则是软写入的,在内存中,重启之后就会无效。因此,一旦你测试成功,请将配置粘贴到openwrt中的 Network->Firewall->Custom Rules 进行固化: firewall-custom-rules

FAQ

1、更多关于透明代理的原理和细节可以参考以下文章:

2、openwrt如何实现v2ray后台运行,开机自启,解决 too many open files

编辑新建 /etc/init.d/v2ray

#!/bin/sh /etc/rc.common

USE_PROCD=1
START=99

NAME=v2ray
PROG=/usr/bin/v2ray

start_service() {
        procd_open_instance $NAME
        procd_set_param command "$PROG" -config /etc/config/v2ray

        procd_set_param file /etc/config/v2ray

        procd_set_param limits core="unlimited"
        procd_set_param limits nofile="1000000 1000000"
        procd_set_param stdout 1
        procd_set_param stderr 1
        # respawn automatically if something died, be careful if you have an alternative process supervisor
        # if process dies sooner than respawn_threshold, it is considered crashed and after 5 retries the service is stopped
        # /etc/init.d/your_service reload will restart the daemon if these files have changed
        procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5}

        procd_close_instance
}

reload_service() {
        stop
        start
}

添加执行权限 $ chmod +x /etc/init.d/v2ray。可执行:

$ /etc/init.d/v2ray start/stop/restart # 启动/停止/重启

参考文章

本文链接:https://deepzz.com/post/router-openwrt-v2ray-tproxy.html参与评论 »

]]>
Sat, 12 Nov 2022 11:29:00 +0800
如何玩转智能家居 - 网络组网方案实施 https://deepzz.com/post/smart-home-networking-design.html https://deepzz.com/post/smart-home-networking-design.html#comments https://deepzz.com/post/smart-home-networking-design.html 接上篇:如何玩转智能家居 - 家庭组网方案选择

当我们已经选择好家庭组网方案之后:AC(PoE)+AP。那么问题来了:实际该如何操作,如何布线,需要考虑什么?

就我来说,可能考虑这些因素:

  1. 弱电箱位置,要不要挪动,什么位置才是最佳
  2. 强弱电的走向,如何才不会被影响
  3. 我们的AC应该放在哪里,AC为什么需要POE
  4. 我们的AP应该放在哪里,要放几个
  5. 到底需不需要弄一个类似家庭机房的东西,放哪,有什么用
  6. 网线怎么弄,什么地方要放几根,用几类网线
  7. 普通家庭上全屋光纤有用吗

确定需求

确定需求前,我们确定下当下的环境:

  1. 电信宽带目前只能达到千兆
  2. 大多数家庭以WiFi上网为主,台式机上网较少
  3. 不考虑全屋光纤,目前成本较高(土豪请忽略),因为直接支持光纤设备极少,需要光电转换
  4. 全屋必须网络全覆盖,必须实现网络无缝漫游

以上,我们得出:全屋采用超六类网线即可满足未来需求,超六类网线可以达到万兆网络,往后看还是能够满足的。全屋主要是以WiFi为主,那么部署AP时,如果不需要网线则不牵网线,节省成本。

我们还有有哪些需求需要解决呢,确定以下信息:

  • 是否需要IPTV:需要,至少客厅需要一台电视
  • 哪些设备需要网线:电视,台式电脑,Xbox,AP
  • 是否需要科学上网:需要,长期会查询外文资料
  • 是否使用NAS:需要,NAS可以帮助存储照片,视频,音乐等,能够称为智能家居的后端存储
  • 是否需要万兆网络:需要,内网一定要快,至少保证访问NAS的速度,剪辑视频需要
  • 是否需要内网穿透:需要,我需要在外控制家里设备
  • 预算有多少,4k+

以上,我们得出:

  1. 客厅至少需要三根网线:电视或电视盒子,Xbox游戏机,AP
  2. 书房至少需要三根网线:台式电脑,NAS,AP
  3. 每个卧室至少需要一根网线:AP或作备用
  4. 需要一个万兆交换机,实现内网有线万兆
  5. 软路由来一套,自由上网用
  6. 向电信要IPv4或外网服务器来一套

那么综合以上信息,我确定我是需要一个机柜的:将所有的网络硬件都放在一个位置,把NAS也放过去,最后将该机柜放到定制柜里完美隐藏。

设计方案

接下来是设计布网方案,这需要实际根据户型图来操作。大家可以参考我的这张设计图: homenet-design

基本思路就是有一个总的网络机柜,网络从弱电箱出来直接到网络机柜,后续的网络全部走机柜出去。由于博主要实现内网穿透,故画了阿里云上去,如果你们不需要可不参考(也可以走wireguard/tailscale打洞方案)。

以上图仅是设计参考用,实际在实施过程中有些许变化。然后就有了这张图: homenet-design-part2

这张图AP酌情减少了一个,具体根据实际情况来定。

当你确定好方案之后,一定要第一时间与电工师傅进行核对,确认方案的可行性。博主就是这样,想当然的将弱电箱挪到鞋柜处,所有网络设备都放到鞋柜,谁知强电与弱电在一个垂直面上,不行。所以改到了餐柜处,当时也是打得我措手不及,方案是调整了又调整。

选定设备

基于以上种种结论,可以开始选择设备了。

机柜

首先,选机柜。tb上的机柜也有现成的,而且非常结实,有兴趣的朋友可以看这里:简易开放式机柜 open-trial-cabinet

但是,自购机柜的话需要考虑机柜放置位置。比如放鞋柜,那么尺寸一定要合适。还有就是机柜出最好叫木工师傅帮助打一个散热孔。

现成的对于我来说不够折腾,本着极客的原则,博主开始学习网友的购买零件自己组装机柜。具体可参考这里: cabinet-list

lizhu-jijia

lizhu-jijia 从知乎上看到第一眼就爱上这个配置,和我的计划也非常符合。于是我也采购了一套,不过散热还是比较贵的。等房子装修好了晒图。

选AC+AP

由于是面板AP,选择面就比较少了。最开始相中了华为的H6,不过这玩意是贴上去的,不够美观。于是叫朋友推荐了一套TP-Link的套装,省事也省心。

TP-LINK TL-R4010GP-AC PoE ACTP
TP-LINK TL-XAP3002GI-PoE

这两款设备,我是在某鱼上购买的全新,应该不会翻车,有需求的朋友可以去某鱼上搜索下。

这款AC是集PoE交换机一体的:

  • 双WAN口接入
  • 9个千兆网口,2WAN+8LAN
  • 内置无线控制器,可统一管理TP-LINK AP产品
  • 所有LAN口支持标准PoE供电,整机输出功率高达120W
  • IPSec/PPTP/L2TP VPN,远程通信更安全
  • 接入认证(Web认证、短信认证、PPPoE服务器)
  • 上网行为管理(移动APP管控/桌面应用管控/网站过滤/网页安全)

这款AP是支持Wi-Fi6:

  • 新一代Wi-Fi 6 (802.11ax)技术
  • 11AX 2.4G/5G 双频并发,无线速率高达2976Mbps
  • 国标86壳体,外观优雅,出墙厚度仅9.4mm
  • 802.3af/at标准PoE网线供电,无需外接电源
  • 支持TP-LINK易展功能,简单按下按键,即可供TP-LINK易展设备无线接入
  • 频谱导航,引导双频用户优先连接5GHz频段,使2.4GHz 和5GHz 两个频段负载均衡,保障网络性能
  • FIT模式支持自动射频调优
  • FIT模式支持智能漫游
  • 支持弱信号设备剔除、禁止弱信号设备接入功能
  • 胖瘦一体,可以根据不同应用环境选择不同模式
  • 支持TP-LINK 商用网络云平台集中管理
  • 支持TP-LINK 商云APP远程查看/管理
  • 整机尺寸(mm):88*86*45.4,出墙厚度9.4mm

看中这款AP的原因也是它的出墙厚度仅9.4mm,与普通插座面板差不多的高度。实现了完美的隐藏。

参考

https://www.zhihu.com/question/26596786/answer/279309998

本文链接:https://deepzz.com/post/smart-home-networking-design.html参与评论 »

]]>
Thu, 23 Jun 2022 22:27:00 +0800
如何玩转智能家居 - 家庭组网方案选择 https://deepzz.com/post/home-networking.html https://deepzz.com/post/home-networking.html#comments https://deepzz.com/post/home-networking.html 为什么会有家庭组网这个说法?其根本原因是想要实现全屋网络覆盖。结合时下流行的智能家居,相辅相成。

目前,大多数家庭的网络仅靠那么一个路由器支撑,但路由器的网络覆盖范围往往都会有局限,就比如信号穿堵墙就会有很大的衰减,家里的角落甚至有时搜索不到信号。那么一般我们的解决方案有哪些呢?

  1. 换一个更好更强的路由器,天线更多,窗墙能力更强,4根的,6根的,越多越强
  2. 使用WiFi放大器,将信号进行增强。一个不够用两个,两个不够用多个
  3. 路由器桥接模式,当前路由器信号不好的时候切换成信号好的
  4. 家庭组网方案:电力猫、有线/无线Mesh、AC(POE)+AP

前面三种有各种各样的缺点,比如别墅这种大空间用一个再NB的路由器也是覆盖不全的;WiFi放大器这种东西网速会有损耗,稳定性得不到保证;路由器桥接,由于SSID不同,设备不会自动切换路由器,只能手动切换,不方便。因此,前三种方案均不推荐。

我们这里着重说家庭组网方案。这几种方案都能解决全屋网络覆盖的问题,具体选择哪种需根据情况来决定。

电力猫

电力猫用的传输技术是正交频分复用(OFDM),简单点说就是把互联网信号和电力信号叠加到一起。 power-cat

我们平时用的电都是交流电,频率是50HZ,电力猫就是在交流电的基础上叠加上互联网的信号。因为互联网的信号频率都很高(10MHz甚至更高)所以如果采用合适的手段,是不会对电力传输产生影响的。

不过要注意,电力猫有以下缺点:

  • 必须在同一电表下,不在同一电表下将不会进行数据传输
  • 如果电力线采用三相供电设计,电力猫的作用就会受到严重限制,速度会降低
  • 品质低下的电力猫存在散热和噪音问题
  • 电力猫的稳定性容易受到滤波产品、充电器、大功率电器的干扰

一般的,如果家里现有的网络布线中没有预埋网线且不能够增加网线的情况下建议采用这种方式组网。

无线/有线Mesh

什么Mesh组网?又称网格网络,即家里的网络通过节点组建的方式实现网格化,网络中所有的节点都互相连接,并且每一个节点至少连接其他两个节点,所有的节点之间形成一个整体的网络。 mesh-networking

整个Mesh网络中有一个主节点,主节点用来进行节点信息的同步。

组网后,会生成一种网状网络,不同接入点可以以星状、树状、串联和总线方式,混合组网。在这个网络中,SSID统一,无线设备还可以自由寻找信号最好的节点去连接传输数据,用户手持设备在不同节点间,穿梭时无线网络是无缝切换的,实现较好的漫游效果,漫游过程中,数据丢包,延时,抖动越低,网络质量越好。

mesh-networking

Mesh组网分无线组网和有线组网(有线/无线是指Mesh网络中节点的连接方式):

1、无线组网

优点是可以不受空间的限制,可以在需要增加节点的时候随意增加,实现家庭的网络全覆盖。

缺点也是有的,就比如说所有节点均需要的一个供电插座,摆放位置,对于强迫症患者来说是致命的。

2、有线组网

优点是可以通过主路由POE供电方式(POE供电是通过网线为节点供电)和面板子路由形式进行隐藏式安装,美观,比如华为H6。

缺点是后期不能够随意增加节点,因为网口的位置和个数都是固定,如果前期没有提前规划好就会比较痛苦。

AC(POE)+AP

AC是指无线控制器(Wireless AccessPoint Controller),是一种网络设备,用来集中化控制局域网内可控的无线AP,是一个无线网络的核心,负责管理无线网络中的所有无线AP,对AP管理包括:下发配置、修改相关配置参数、射频智能管理、接入安全控制等。

AP即无线访问接入点(Wireless AccessPoint),传统有线网络中的HUB。AP相当于一个连接有线网和无线网的桥梁,其主要作用是将各个无线网络客户端连接到一起,然后将无线网络接入以太网,从而达到网络无线覆盖的目的。

目前该组网方式最常用于企业等大型对网络要求较高的商业场所,该组网方式稳定,简单。目前也用于对网络要求较高的家庭组网中。我们这里聊一聊AC+AP组网的特点。

由于AC+AP组网方式中,一般为有线组网,我们这里也只讨论该组网形式。

1、对于AC

常接触到关键词是POE(Power Over Ethernet),一般还是建议采用POE为AP供电的方式,这样更加方便。

2、对于AP

分为面板AP和吸顶AP。面板AP是一种隐藏式安装形式,这种必须要使用POE供电方式才行,非常美观,但覆盖范围相较于吸顶AP会稍微逊色,100平建议2-3个。吸顶AP安装在吊顶处,覆盖范围较广,100平安装1-2个就好。

由于目前我家是新装修,所以博主采用:AC(POE)+AP:

啊,之前买的 479GPE 不能放进弱电箱。重新买了个4010GP,它有磁铁可以直接粘在弱电箱上。

FTTR

FTTR(Fiber To The Room)即光纤到室的说法。FTTR组网方案采用了一个主光猫,多个从光猫的方式进行网络组网,光猫与光猫之间通过光纤进行连接,由于光纤比较细小,即使显示布线也不会太影响美观: fiber-to-the-room

有兴趣的朋友可以去了解下。我认为FTTR是未来十年发展的一种趋势,但现其高昂的布设成本劝退了我,待后期方案成熟,将网线替换成光纤也是可行的。

下文将介绍博主具体组网方案和思考。

参考文档

[1] https://wenku.baidu.com/view/b001ec4f33687e21af45a953.html
[2] ​https://www.zhihu.com/question/410166038/answer/1989571138
[3] https://zhuanlan.zhihu.com/p/296788149

本文链接:https://deepzz.com/post/home-networking.html参与评论 »

]]>
Wed, 13 Apr 2022 13:24:00 +0800
如何玩转智能家居 - 灯控方案选择 https://deepzz.com/post/smart-home-lighting-control-scheme.html https://deepzz.com/post/smart-home-lighting-control-scheme.html#comments https://deepzz.com/post/smart-home-lighting-control-scheme.html 作为玩转智能家居的第一篇,今天我们了解一下智能家居中的灯光控制系统。

最近准备装修房子,作为互联网从业者一直对科技产品有浓厚兴趣,喜欢折腾。所以新家最好有一个专用家庭机柜,软路由、万兆交换机、AC+CP,全屋光纤,都给它搞上。最重要的还是要有套全屋智能家居,进行自动化场景控制。家庭组网及全屋智能我会在这个系列一一实践,总结经验为大家踩坑,哈哈。

目前来说想要实现全屋智能,两种方式:

  1. 全屋智能方案提供商,你只需要提要求,方案和实施均交给他们(省心但价格较贵)。
  2. 自己折腾,动手设计,这需要有一定的折腾能力和专业知识(心累)。

开始全屋智能系列之初,你首先要确定的是选择一个全屋智能生态,选生态涉及到你能够搭配智能设备的丰富程度。比如你选了苹果生态,但是家里的灯光是不支持 HomeKit 控制的,这就陷入两难,就很烦(当然这也是有解决办法的,后续文章说到)。

  • 米家生态,有完善的智能家居生态链,配套完整,且设备性价比高(推荐)
  • 华为生态,HarmonyOS支持,目前主要由官方提出的解决方案为主,其它配套较少
  • 苹果生态,HomeKit支持,结合 iPhone 体验超棒,但国内支持产品较少(备选)
  • Matter生态,由全球大厂组成的IoT物联网联盟,实现厂厂互联,不过目前还在起步阶段

其它生态,老牌家电厂商也在推自己的智能家居系统与玩法,不过感觉是和自己玩,个人认为智能家居最终还是会实现统一或互连。我这里选择米家生态作为全屋智能解决方案,自己折腾。后续结合 HomeAssistant 上苹果生态,体验应该是非常棒。

本篇文章作为装修系列中的灯光控制篇。

基础知识

先来普及下电相关的知识。电线分(GB/T6995-2008):

  • 地线(E),黄绿色(双色线),用于有金属外壳电器,防止触电,直接接地,三孔插座会用到。
  • 零线(N),蓝色或绿色、黑色
  • 火线(L),红色或棕色,火线与地线,零线电压差220V。开关接在火线上。L1灯连接线。

我们一般就会接触到零线与火线。

开关

在智能设备出现之前,就一种开关:机械开关。智能设备出现之后,为了让原来不智能的设备变智能,于是智能开关出现了。这里普及一下开关的种类。

开关又分几开几控,几开即该开关面板上有几个开关控制按钮,几控即该开关上有几个接触点,如下图: single-open-double-charged

开关往上拨会接通一根线,往下拨又会接通一更线。常见的应用就是,一个灯能够被多个开关控制。如一般卧室就是双控。

理解了开关的说法,我们来讲一讲典型的开关案例:

1、单火开关,即普通开关,这是最常见的开关。开关里只有两根线,一根进,一根出。 single-line-switch

2、零火开关,即智能开关(忽略普通开关),智能开关是需要一直通电的,因此需要三根线,两进,一出。 zero-line-switch

3、凌动开关,也是普通机械开关,开关通过弹簧实现回弹(单向按键),使得灯一直通电。因此它需要搭配凌动灯来使用(关键还是在灯),该开关通过对开和关的电脉冲算法实现灯的亮灭。

目前市面上还有一种单火智能开关,同样是智能开关那么必须一直通电在线。它是如何实现的呢?它是通过调节“可变电阻”实现“灯泡”的开启和关闭的或者内置电池(总之需要使得开关处于在线状态),我认为这只是一种妥协的做法。还有就是无线智能开关,原理上均类似。

以上说的开关均是单键开关,多控开关线路会相应增加。

1、普通灯,不做过多描述

2、智能灯,可以通过手机等智能设备和开关控制的灯。

3、凌动灯,支持凌动功能的灯

好了,现在已经基本了解了灯控相关的知识啦,我们来说一说实际的方案。

方案

总的来说,智能灯控就是开关、灯的组合:

  1. 智能灯+智能开关,不推荐。容易出现冲突,智能开关控制开,智能灯控制关,到底开还是关傻傻分不清楚。
  2. 智能灯+普通开关,不推荐。开关得一直在开启状态,通过智能手机控制,容易出现开关断掉设备不在线状态。
  3. 普通灯+智能开关,次选
  4. 普通灯(改凌动)+凌动开关,次选
  5. 凌动灯+凌动开关,首选

可以看到,博主中意的还是凌动方案。凌动方案既可以满足现房智能改造,也可以满足新房装修线路造价问题,同时还满足对智能不感冒的老年人使用习惯。如果家里没有老人,推荐直接不设计开关,绝对给力。

普通灯+智能开关

由于是智能开关,因此开关盒里有必要再加一根零线。如果你的家已经装修好请确认是否预留零线。 zero-line-seitch2

当然如果你基于很多因素考虑,这种方案最适合你的家庭,在没有零线预留的情况你可以采用单火智能开关也是一种不错的选择。不过这种方案可能对灯有一定的需求,比如日光灯就不可以。 single-line-switch2

不过目前这种方案有两个缺点:

  • 实现双控有难度,即在门口开灯,在床头关灯。
  • 布线成本增加(需要增加零线)

普通灯(改凌动)+凌动开关

结论,这种方案可以和凌动灯方案实现全屋凌动统一方案。这种方案推荐不想过多增加成本,现有灯具不换情况下的用户使用。

这里涉及两个地方的改造。

1、改造灯

凌动开关只是一个普通机械开关,重点还是在控制灯的组件上。该方案需要一个叫灵动通断器的东西,这个不是凌动官方出的,是民间根据凌动方案适配的。所以对此介意的同学可以绕道了。

它的原理就是,将凌动灯拆分成两块:普通灯+灵动通断器。一些灵动通断器的功能还是很多的,如蓝牙,蓝牙Mesh,还有开关随意贴可用。 lingdong-tongduanqi

还有个选择就是,将现有灯换成凌动灯。据我观察,现在支持凌动功能的灯还是蛮多的,如客餐厅吸顶灯,卧室吸顶灯,吊灯等。

2、普通开关改凌动开关

如果家里开关是普通开关,可以通过添加一个弹簧的形式改成凌动开关。普通开关改凌动开关相关可以到某宝搜索,不过推荐购买原装凌动开关更安全稳定。

凌动灯+凌动开关

原生支持凌动,如果家里有条件完全可以采用这样方案。上面所有方案的缺点,都完美的解决了。不管你是正在装修还是已经装修好,完全不需要做任何特殊线路改动(原普通灯光线路),换上凌动开关和凌动灯就可以了。 lingdong

博主目前采用:

  • 普通灯(改凌动)+凌动开关
  • 凌动灯+凌动开关

两种方案结合的形式,毕竟不是所有灯都支持凌动功能,你喜欢的灯不一定都支持凌动功能。

最后,其实所有方案都不是固定的,当你对智能灯控了解到一定程度之后,你可以有自己的方案,组合和场景是多种多样的。

本文链接:https://deepzz.com/post/smart-home-lighting-control-scheme.html参与评论 »

]]>
Thu, 31 Mar 2022 14:26:00 +0800
如何玩转智能家居 - HomeAssistant接触 https://deepzz.com/post/homeassistant-concept.html https://deepzz.com/post/homeassistant-concept.html#comments https://deepzz.com/post/homeassistant-concept.html 接上篇文章:如何玩转HomeAssistant - HA介绍

如何安装 HA,上篇文章说到了一些,本篇文章实战安装 HA,并介绍 HA 内部概念。通过两种方式安装:

  • 镜像安装
  • Docker安装

如果大家有更多需求,可以到官方站点查看:Installation

镜像运行

镜像安装,相当于安装一个操作系统,我们需要提前准备好虚拟机或者一个小型的设备(如树莓派或NUC等等)。

我这里准备的是 VirtualBox Linux 环境来进行安装 HassOS,镜像下载地址在 Github 上:home-assistant/operating-system

1. Create a new virtual machine
2. Select “Other Linux (64Bit)
3. Select “Use an existing virtual hard disk file”, select the VDI file from above
4. Edit the “Settings” of the VM and go “System” then Motherboard and Enable EFI
5. Then “Network” “Adapter 1” Bridged and your adapter.

virtualbox-install-ha

Docker运行

容器运行对于了解过的人应该是比较喜欢的一种方式。我们这是使用 docker 做演示。

首先,我们得有一个安装 docker 环境的机器:docker安装,选择一个合适的 HA 镜像版本。运行方式很简单:

docker run --init -d \
  --name homeassistant \
  --restart=unless-stopped \
  -e TZ=Asia/Shanghai \
  -v /PATH_TO_YOUR_CONFIG:/config \
  --network=host \
  homeassistant/home-assistant:stable

替换 PATH_TO_YOUR_CONFIG 为本地配置路径,容器运行起来之后会监听 :8123 端口,启动过程可能会耗点时间。通过访问 http://<host>:8123 即可。

HA 内在

HA 初次接触是一个新鲜事物,它的内部有很多概念,如集成、设备、服务和实体等。提前理解这些概念有助于我们快速上手,玩转 HA。

设备和服务

集成是 HA 中重要的概念,串联着整个系统。那么什么是集成?

集成可以说集成设备,集成服务,或者说设备和服务是集成的一种抽象体现。而实体(Entity)是通过集成(如 lightswitch 等)进行标准化的(如小米灯通过 light集成 集成了 Entity)。标准化实体附加了用于控制的服务。

实体将 HA 的内部工作抽象化。作为集成商,不必担心服务或状态机的工作方式。相反,可以扩展实体类并为要集成的设备类型实现必要的属性和方法。

integrating-devices-services

  1. Device Integration(ie. hue)将使用此配置来建立与设备/服务的连接。它将转发 Config Entry(传统使用发现助手)以在其各自的集成(lightswitch)中设置实体。device Integration 还可以为未标准化的事物注册自己的 Services。这些服务在集成的域下发布 hue.activate_scene

  2. Entity Integration(i.e. light)负责定义抽象实体类和服务来控制实体。

  3. Entity Component 帮助程序负责将配置分发到平台,转发发现并收集用于服务调用的实体以。

  4. Entity Platform 帮助程序管理该平台的所有实体,并在必要时轮询它们以获取更新。添加实体时,entity platform 负责将实体注册到设备和实体注册表中。

  5. Integration Platform(i.e. hue.light)使用配置来查询外部设备/服务,并创建要添加的实体。integration platform 还可以注册实体服务。这些服务将在设备集成的所有实体上进行实体集成(即所有 Hue light 实体)。这些服务在设备集成域下发布。

1、实体与 HA Core 交互

从实体基类集成的集成实体类负责获取数据并处理服务调用。如果禁用了轮询,则它还负责告知 HA 数据何时可用。

entity-core-interaction

实体基类(由实体集成定义)负责格式化数据并将其写入状态机。

实体注册表将为 unavailable 当前未由实体对象支持的任何注册实体写入状态。

2、实体数据层次

entity-data-hierarchy

删除,禁用或重新启用任何对象,下面的所有对象都将进行相应调整。

什么是实体

什么是实体,实体注册表(Entity Registry)?我认为是 HA 中智能设备所能划分的最小单元,也可以理解为控制单元,如空气净化器中的温度传感器上报视作一个实体。

每个实体均有 Unique ID,该ID不能被用户更改,否则造成数据不一致情况。如果一个设备只有一个ID,但提供多个实体,我们可以这样标识 {unique_id}-{sensor_type}

切记 Unique ID 必须全局唯一,且不可变,一般用 MAC 地址。

什么是设备

什么是设备,设备注册表(Device Registry)?HA 中的设备代表具有自己的控制单元的物理设备,它位于一个特定的地理区域,通常由一个或多个实体表示。举个例子,一台空气净化器是一台设备,它所包含的温度、湿度和PM2.5传感器(控制单元)所暴露的我们可以认为是实体。

但是一个实体(如温度传感器)如果拆解出来,也可以是一个独立的设备,这里更多的其实就是一个从属关系的划分(设备可以视作实体,实体可以视作设备),具体的实体或设备的划分自行考虑。配置实体、实体、设备之间的关系如下图:

ha-device-entity

Config Entry 配置了指定的 Entry,该 Entry 可能关联着某个 Device。一个设备通常有如下属性:

属性 描述
id HA 生成的唯一ID
name 设备的名称
connections connetion_type, connection_identifier的集合
identifiers 标识符集合,外界的设备识别号,如序列号
manufacturer 设备制造商
model 设备模型
suggested_area 建议设备区域
config_entries 联接该设备的实体
sw_version 设备防火墙版本
via_device 设备与 HA 之间路由消息的设备标识符
area_id 区域ID
entry_type 实体类型,None 或者 “service”

通过设备注册表来进行管理。

什么是区域

什么是区域,区域注册表(Area Registry)?区域应该是最好理解的,它用来定义区域,如客厅,卧室A,厨房等,代表了一个具体的物理位置,它可以帮助我们归集和标记设备的具体区域。

通过区域注册表来进行管理。

什么是Blueprint

蓝图,是可重复使用的自动化,可以轻松共享。您可以从 Github 和社区论坛导入其他用户的 Blueprint。

什么是自动化

其描述很清晰,为智能家居指定自动化规则。即在什么情况下想要使得智能家居做出什么样的反应。

什么是场景

定格一组设备的状态,日后即可一键恢复。也就是说在实际使用过程中,你可能有一个固定的场景或者模式,比如家庭影院。

什么是脚本

执行一系列动作,相当于指定流水线。可以自行考虑考虑

什么是lovelace

相当于是一个主题,你可以在这个主题上做自己的定制。

HA 初识

当 HA 运行起来之后,你可以通过 <ip>:8123 进行访问,默认会根据浏览器进行对应语言的显示(如中文),通过一些简单的配置就可以开启我们的智能家居之旅了。

首先,需要我们初始化一个账户,也就是管理员账户,《创建账户》。

然后,设置 HA 的名称,选择时区和单位等,至于定位可能不准需要自己选择,《下一步》。

然后,HA 会自动发现你网路中的相关设备和服务,如果现在不是很清楚可以直接跳过,《完成》。

现在,我们进入 HA 首页,你的折腾开始了:

ha-home

  • 概览,也就是仪表盘,我们后续会将我们的智能设备通过卡片的形式添加到这里。可以在这里直接控制设备和查看设备状态。
  • 地图,能够显示相关位置信息,如人员、家的定位等
  • 日志,记录 HA 中发生相关事件
  • 历史,查看历史的事件信息
  • 媒体浏览器,查看一些硬件设备上的媒体
  • 开发者工具,可以帮助我们做一些调试动作
  • 配置,我们使用最多的,如集成、自动化等,和配置文件关系密切
  • 通知,一些通知,告警等
  • 人员,个人设置,如密码修改,主题等

配置文件

HA 的运行离不开配置,现在有两种配置方式,一是通过 HA 网页 UI 进行配置(需要相应的集成适配),二是通过 HA 配置文件进行配置。这里我们直接介绍配置文件:

├── automations.yaml                       # 自动化
├── blueprints                             # 蓝图
│   └── automation
│       └── homeassistant
│           ├── motion_light.yaml
│           └── notify_leaving_zone.yaml
├── configuration.yaml                     # 主配置文件
├── deps                                   # 相关依赖
├── groups.yaml                            # 分组
├── home-assistant_v2.db                   # sqlite3数据库
├── scenes.yaml                            # 场景
├── scripts.yaml                           # 脚本
├── secrets.yaml                           # 密钥
└── tts                                    # 文字转语音记录

默认情况下 HA 会自动创建上面的配置文件。具体的使用方式在后续的实践过程我们一一熟悉。

但想要玩转 HA 我们还会接触到更多的配置,如用来保存第三方开发组件的 custom_components 文件夹等(HA 会默认加载)。当然为了方便管理,我们也可以自己组织文件夹的组成。然后在 configuration.yaml 进行启用:

# Configure a default setup of Home Assistant (frontend, api, etc)
default_config:

# Text to speech
tts:
  - platform: google_translate

group: !include groups.yaml
automation: !include automations.yaml
script: !include scripts.yaml
scene: !include scenes.yaml

!include 用来指定该字段内容的外部文件,而 !include_dir_merge_named 则用来指定文件夹。

之后的文章,我们通过一个个实战来熟悉 HA 的配置和使用,玩转 HA。

本文链接:https://deepzz.com/post/homeassistant-concept.html参与评论 »

]]>
Mon, 12 Apr 2021 11:05:00 +0800
如何玩转智能家居 - HomeAssistant介绍 https://deepzz.com/post/homeassistant-introduce.html https://deepzz.com/post/homeassistant-introduce.html#comments https://deepzz.com/post/homeassistant-introduce.html HomeAssistant是什么

HomeAssistant,简称 HA,是一款基于 Python 的智能家居开源系统,支持众多品牌的智能家居设备,可以轻松实现设备的语音控制、自动化等。

初次接触 HA 是很早之前,当时被那种万物互联所震撼,智能之家的想法吸引。随着自己的成长,有空间去做这些事。也是时候进入 HA 的世界了,为后面的物联网之家打下基础。

推荐学习站点:

HA 的安装

初次接触 HA,卧槽,根本不知道如何下手。那我们还是从相关概念开始。

HA 的安装方式有很多,大家可以参考这里:installation。简单说 HA 的安装方式有4种:

  • HA Operating System,又称 HassOS,提供了一个支持多平台的最小化的 HA 操作系统,底层基于 Docker 容器引擎,通过容器化部署的 Supervisor 容器化控制 HA Core 及相关应用。由于是 OS,因此需要独立设备或者虚拟机进行安装。官方推荐
  • HA Container,基于容器技术独立安装 HA Core,如 Docker。免去了各种环境的不统一困扰。需要在支持容器化技术的系统上使用。
  • HA Supervised,前身 Hass.io,是一个基于容器化的系统,通过容器化部署的 Supervisor 容器化控制 HA Core 及相关应用。HA 提供了 手动安装 脚本。较于 HA Operating System 最大的区别就是可以安装在已有的操作系统上,其它都一样。
  • HA Core,手动安装在 Python 虚拟环境中,纯手动,可能需要一定的折腾能力。

官方建议使用专用干净系统来运行 HA。即用一台独立的硬件来装 HassOS,如果不知道怎么选择,可以使用 Raspberry Pi 和 HassOS。如果有现有系统且支持 Docker,用 HA Container 也不错,如 NAS。

HA 的衍生

围绕着 HA,衍生了很多插件、系统,各种名词,眼花缭乱。新人接触,很容易被整懵,不适合新人学习。这里我把我所接触到的关于 HA 的衍生介绍给大家,希望大家尽快入门。

0、Hass 是什么?
Hass 简单来说就是 HA 实际运行中,为我们服务的程序实例,具体可参考 HA 机制

1、Hassbian 是什么?
类 Raspberry Pi 操作系统 raspbian,那么内置了 HA 可安装在 Raspberry Pi 的操作系统就称呼为 hassbian,这是一个统称。

具体的系统可以到这里:操作系统

2、HassIO (hass.io)是什么?
是一款 HA 特别定制的高集成度、图形化操作、易上手的系统。系统由 ResinOS 及 Docker 驱动,旨在简化 HA 的配置操作。(现在已经没有这个说法了,称作 supervised,合并到 操作系统)。

架构是这样的: ha-hassio-arch

HassIO 通过 docker 将 HA 部署起来,管理着 HA,并通过 HA 的 API 进行有效交互。同时提供了一套插件标准 Add-ons,该标准规范了 HassIO 的容器编写规范。

3、HomeBridge 是什么?
简称 HB。通过模拟 iOS HomeKit API,架设一个桥梁,使得不支持 HomkeKit 的智能设备接入 HomeKit。

HA 与 HB 是两个完全独立的系统,各自可以独立运行。HB 的主要目的是桥接不支持 HomeKit 的智能设备,桥接完成后我们可以通过 iOS 家庭 进行管理。或者接入 HA 自动化管理。

目前 HA 已经内置了 Bridge,通过 配置 -> 集成 -> 添加集成 -> 搜索HomeKit 即可。不过据说,HB 可以支持更多的智能设备,且它的 UI 做得比较棒。当然两者都可以自行定制 UI,不过得自我折腾。

4、HACS(Home Assistant Community Store) 是什么?
可以理解为 HACS 是一个为 HA 提供强大 UI 的商店,用于页面美化以及个性化自定义的。里面也支持了很多强大的插件,大家可以自己慢慢体会。

5、Node-RED 是什么?
Node-RED是一种编程工具,用于以新颖有趣的方式将硬件设备、API 和在线服务连接在一起。

它提供了一个基于浏览器的编辑器,可以轻松地使用面板中广泛的节点连接流,这些节点可以在一次单击中部署到运行时。 node-red-example

总之想要将这些东西玩起来还是需要费一定功夫的,如果没有人指导个人觉得比较难入门。

HA 的机制

HA 是运行在 Python3 环境中的,核心进程是:Core,目前版本通过日期形式定义:最新版本

如果按照分层概念来看的话,可以分为三层:操作系统、运行环境、Hass。 homeassistant-full-arch

而 HA 又主要分为三大块:HA CoreIntegration 和配置文件 configuration.yml,它们组成了完整的 HA。

1、HA Core 是整个系统的大脑,相当于操作系统的内核,用来决策和调度相关资源。它有四个核心部分:

  • 事件总线(Event Bug),HA 的心脏,负责监听和触发事件
  • 状态机(State Machine),跟踪事物的状态,并 state_changed 在状态更改后触发事件
  • 服务注册表(Service Registry),在事件总线上侦听 call_service 事件,并允许其他代码注册服务
  • 计时器(Timer),time_changed 每1秒在事件总线上发送一个事件

ha_architecture

2、Integration 集成,Core 作为大脑,总得有可利用的手脚吧,集成就是如此。

HA Core 通过集成进行扩展。每个集成都负责 HA 中的特定域。集成可以侦听或触发事件,提供服务并维护状态。集成由组件(基本逻辑)和平台(与其它集成集成的位)组成。集成是用 Python 编写的,可以实现 Python 能够提供的所有优势。开箱即用的 HA 提供了许多 内置集成component-interaction

HA 中定义了4种集成类型:

  • 定义物联网域,该集成定义了物联网设备的 特定类别,如灯 light,它定义了什么数据和格式在 HA 可以使用。另外已定义的 域列表。如果要建立一个新的域,还可以在 这里讨论
  • 定义与外部设备和服务互动,该集成定义了在 HA 中如何通过已定义的《物联网域》与外部设备和服务进行交互,如灯 light,一个例子是 Philips 灯可以作为灯的实体在 HA 进行交互,更多信息:实体体系结构
  • 表示虚拟/计算数据点,该集成表示基于虚拟数据的实体。如 input_booleanIntegration,即虚拟交换机。或者通过 HA 中其它数据做处理输出,如 templateIntegrationutility_meterIntegration
  • 由用户或响应事件触发的动作,该集成提供了家庭自动化逻辑的小片段,可在您的房屋中执行常见任务。最受欢迎的一种是 automation集成,它允许用户通过配置格式创建自动化。如 fluxIntegration,它可以根据太阳落山来控制灯光。

3、configuration.yml 配置文件是用来帮助我们完成上述功能的,当然也可以通过 HA front 网页 UI 进行操作。

具体配置详细,请参考下篇文章。

本文链接:https://deepzz.com/post/homeassistant-introduce.html参与评论 »

]]>
Fri, 26 Mar 2021 11:31:00 +0800
跨省异地换工作,社保、医保、公积金转移 https://deepzz.com/post/interprovincial-job-change.html https://deepzz.com/post/interprovincial-job-change.html#comments https://deepzz.com/post/interprovincial-job-change.html 在我们已经确定要异地换工作的情况下,我们在当地缴纳的社保和公积金应该怎么办?如何转移?转移的政策如何?如何确定转移成功?

是的,这个问题一定是需要考虑的。当我们在同一城市换工作,你个人的社保和公积金账户是不会变的,新公司这边可以直接为你接续缴纳。但如果是异地,新公司会确定你是否在该地有账户,无则重新开户,所有年限从零开始。

这里以博主自身经历:上海徐汇 -> 成都高新为例。

知识普及

首先想要普及一些常识,公司一般为员工缴纳五险一金:工伤保险、失业保险、生育保险、医疗保险、养老保险和住房公积金。其中工伤保险、失业保险、生育保险属于“当期交纳当期享受”的险种,养老保险、医疗保险和住房公积金属于累计缴纳,当缴纳到一定年限,相当可观。

因此,我们知道只有养老保险、医疗保险和住房公积金可以进行异地转移。

下面介绍下这几个保险,缴纳比例(上海为例)。具体的缴纳基数可以咨询所在公司的HR。

工伤保险

全额由公司缴纳,0.16-1.52%。该险种是指在工作中或在规定的特殊情况下,遭受意外伤害或患职业病导致暂时或永久丧失劳动能力以及死亡时,劳动者或其遗属从国家和社会获得物质帮助的一种社会保险制度。

就比如说职业病,上下班途中事故都是在该保险所保的范畴。

失业保险

公司缴纳0.5%,个人缴纳0.5%。该险种是指在非个人意愿情况下失业,且未再就业。从而获得补助的一种保险制度。一般每个城市要求员工连续缴纳一年以上才能享受该制度。

如上海政策:

  • 缴费1-5年,可领取最长12个月
  • 缴费5-10年,可领取最长18个月
  • 缴费10年以上,可领取最长24个月

如果失业且暂时未能找到合适工作,可以通过申请失业保险缓解经济压力,同时失业保险基金会为你缴纳医疗保险保障你可以继续享受医保待遇(医保断缴会享受不到医保待遇)。

医疗保险+生育保险

公司缴纳10%,个人缴纳2%。医保是最有用的保险,也是大家比较常用到的,如门诊支付,医保报销等,具体的医保政策可以查看当地档案。医保有两个账户:个人账户和统筹账户。个人缴纳全部划入个人账户,公司缴纳的一般20%划入个人账户。

医保有个连续缴费的说法,连续缴费15年可终身享受医保待遇。停缴次月不再享受医保待遇,停保超3月将重新计算连续缴费周期(不要断缴,影响报销额度)。另外,医保账户的钱是不会清零的哦。

而我们能转移的只有个人账户。

养老保险

公司缴纳16%,个人缴纳8%。简单说就是退休后我们领的退休工资,多缴多得,长缴多得。

有个公式:

  • 每月到手的养老金 = 基础养老金 + 个人账户养老金;
  • 基础养老金 = 退休时上年度在岗职工月平均工资 × (1+本人平均缴费指数(比例)) ÷ 2 × 缴费年限 × 1%;
  • 个人账户养老金 = 个人账户累计储蓄额(含利息) ÷ 计发月数。

退休时能领多少养老金与个人缴费年限、缴费基数、个人账户余额以及养老金领取地的职工平均工资等密切相关。

同样的条件下,一线城市的社会平均工资要高于其它地方,那么在一线城市领取的养老金要比其他地方高得多。

所以说,并不是换了城市工作就一定要转移社保。如果你在大城市缴纳了十几年的社保,已满足在当地退休的条件,因为某些原因需要回老家工作,此时的社保可以考虑不转移。

住房公积金

公司和个人缴纳相同比例,5%-7%之间。住房公积金缴纳比例由公司决定,基数有些公司是工资全额,有些是定额。找工作可以特意看看,一般来说住房公积金缴得越多越好。

用于购房,住房公积金贷款能够贷得比较低的利率,一般是三点几,商业贷款现在已经上六了。如果不贷款还可以用于支付房屋首付款。

用于租房,一般城市租房可通过住房公积金支付租金。

用于建房,翻建或大修住房,公积金缴费人有建造自有住房,或翻修、大修自有住房需求时,可以申请一次性提取住房公积金,由于支付翻建、建房、大修住房费用

重大疾病,公积金缴费人的家庭成员如果不幸患重大疾病,那么可以申请提取公积金用于支付医疗费用,帮助减轻负担

退休、离休、出国定居等,可以申请一次性提取公积金。对于退休人员来说,一次性提取的公积金是一笔可观的养老金。

养老保险转移

首先你得在新就业地办理参保手续,然后才能申请转移。你可以登录 上海一网通办

1、搜索:基本养老保险,找到《基本养老保险转往外省市缴费凭证》 endowment-insurance.jpg

2、填写申请信息,等待3日(一般很快)

3、前往我的主页,下载缴费凭证: endowment-insurance02

然后交给公司的HR让她帮你办理社保转移即可。

另一种方式,如果你已经办理过电子社保卡,可直接在开通过电子社保卡的APP上进行申请,如各个银行APP(包括掌上12333): endowment-insurance03

上述两种,在社保转移申请成功之后,可以在开通电子社保卡的APP(包括掌上12333)上查看转移进度: endowment-insurance04

这里我遇到的情况是,公司HR也是通过电子发函方式帮我申请的。我也能在电子社保卡的APP查看到申请记录,但是该进度迟迟未能更新。于是我通过转移接续电话咨询得知,上海那边由于系统还在更换当中,电子发函没有提示,所以一直没有处理我的申请。

医疗保险转移

我这边了解到,社保能够通过公司帮忙进行转移,但医保必须得自己到社保局申请。目前成都这边已经将这些工作下发到各个街道办事处了,可以提前了解你所处的街道办事处。

首先你得在新就业地办理参保手续,然后才能申请转移。你可以登录 上海一网通办

1、搜索:基本医疗保障,找到《基本医疗保障参保(合)凭证(发函用)》 endowment-insurance.jpg

2、填写申请信息,等待3日(一般很快)

3、同上,前往我的主页,下载缴费凭证

4、将凭证打印,到街道办事处进行办理转移接续

这里我遇到的情况是,基本社保和医保转移是同时申请的。但医保很无赖,没有进度,也不好查询。每每前往街道办事处,得知的都是一些还在进行中的说话,没有一个人告诉我需要等到社保转移成功之后才行。

后续社保转移成功之后,医保转移也是迟迟没有成功。多次打街道办电话也不知道具体的原因,我也是很无赖,我甚至从上海那边拿到了挂号信的单号。那就等呗,将近一个月时间总算是成功了。所以医保这块转移还是只有等。

公积金转移

公积金转移的规则是,必须要在新参保地缴纳六个月以上才能够进行转移。

转移需要自行到公积金服务大厅申请。申请的过程很简单,需要一些资料:

  • 新参保地的公司名称
  • 新参保地的公积金账号
  • 愿参保地的公积金账号
  • 愿参保地的单位名称(填写封存的账户名称)

公积金申请接续是体验最好的,填写一个转移接续单就好,转移过程大概2个工作日,且转移成功会有短信提醒。目前成都公积金管理中心需要先预约才能前往办理业务,切记。

参考文档

[1] https://zhuanlan.zhihu.com/p/152012584
[2] https://zhuanlan.zhihu.com/p/106685043

本文链接:https://deepzz.com/post/interprovincial-job-change.html参与评论 »

]]>
Mon, 22 Mar 2021 18:00:00 +0800
Go1.16初体验,怎样使用 //go:embed https://deepzz.com/post/how-to-use-go-embed.html https://deepzz.com/post/how-to-use-go-embed.html#comments https://deepzz.com/post/how-to-use-go-embed.html 2月16日,Go1.16版本发布了。对于我们普通开发者来说,本次版本发布了一些有趣的特性,这里列举了重要的几点:

  • 新增了embed包,在编译时通过使用 //go:embed 指令可以进行嵌入文件的访问,即将文件嵌入到二进制包中。
  • 增加了对 macOS ARM64 的支持(Apple silicon)。
  • 默认开启 Go modules。
  • 修复了一些bug和改进一些问题,如构建速度提升25%,内存使用量降低15%。
  • io/util 包被弃用,所有方法被移至 ioos 包。

具体详细的发布日志移步:go1.16,本篇文章关注的是如何使用://go:embed

embed功能说明

embed能帮我们做什么?一句话概括就是将静态资源文件嵌入到编译的二进制文件中。

这样做有什么优势?个人认为比较重要是保证一个应用的完整性。比如:

  1. 比如一个Web应用,包含了很多image和html,一般情况下我们需要将所有的文件和编译好的二进制文件拷贝到同一机器。如果是分布式应用还会带来更多的拷贝过程。当然如果使用如docker容器镜像方式,是会简化拷贝过程,但也会增加一些负担,如:打包过程。
  2. 比如一个App应用,本身会携带很多如音频、图片小文件。一般情况下在安装过程中我们需要将许许多多的小文件进行拷贝,我们知道磁盘I/O瓶颈比较大的,安装时间长会给用户带来不好的体验。
  3. 比如一个游戏应用。
  4. 比如一个WebAssembly应用等等。

当然上面举的例子只是从一些方面来考虑,具体的打包部署方式需要综合考虑多个因素,如当前公司的自动化运维体系。

embed能帮我们保证一个应用的整体性和完整性,我觉得对于强迫症的开发者来说一定是个福利,哈哈。下面来看看embed的使用方法。

embed基础用法

通过 官方文档 我们知道embed嵌入的三种方式:string、bytes和FS(File Systems)。

//go:embed 基本用法是:

package main

import "embed"

//go:embed hello.txt
var s string

//go:embed hello.txt
var b []byte

//go:embed hello.txt
//go:embed assets
var f embed.FS

func main() {
  print(s)

  print(string(b))

  data, _ := f.ReadFile("hello.txt")
  print(string(data))
}

1、导入 embed 包,如果没有使用 embed.FS 需要显示的导入:

import _ "embed"

2、匹配文件 //go:embed <匹配模式> <匹配模式>...,匹配模式符合 path.Match 方式。

(1)匹配模式是相对位置,如:

├── assets
│   ├── .gitkeep
│   ├── _home.html
│   └── index.html
├── hello.txt
└── main.go

匹配 index.html 则使用 //go:embed assets/index.html 即可,不能使用 ...(如./hello.txt)。
(2)可以匹配多个,以空格隔开,如 //go:embed hello.txt assets/index.html。也可以重复,避免匹配长度过长:

//go:embed hello.txt
//go:embed assets/index.html
var f embed.FS

(3)[]bytestring 只能匹配单个文件。如果文件名称有空格可使用双引号 " 或者反引号 ``。 (4)如果//go:embed assets匹配的是一个目录,那么该目录中所有文件都将递归的嵌入,除了以.开头的文件。 (5)匹配目录中的所有内容,使用统配*,包括以.` 开头的文件。
3、匹配的变量只能是全局变量。

embed进阶用法

Go1.16 为了对 embed 的支持也添加了一个新包 io/fs。两者结合起来可以像之前操作普通文件一样。

常规文件操作

如通过 embed 进行常规的文件目录读取,文件递归遍历等:

//go:embed hello.txt
//go:embed hello.txt assets/*
var f embed.FS

...

entries, err := f.ReadDir(".")
if err != nil {
  panic(err)
}
for _, entry := range entries {
  info, err := entry.Info()
  if err != nil {
    panic(err)
  }
  fmt.Println(info.Name(), info.Size(), info.IsDir())
}

Web文件系统

通过原生go http服务,我们将静态资源文件嵌入到二进制中,做静态文件服务器:

package main

import (
  "embed"
  "net/http"
)

//go:embed hello.txt assets/*
var f embed.FS

func main() {
  http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(f))))

  http.ListenAndServe(":8080", nil)
}

通过常见的Web服务框架,提供文件的访问:

package main

import (
  "embed"
  "net/http"

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

//go:embed hello.txt assets/*
var f embed.FS

func main() {
  e := gin.Default()

  e.StaticFS("/static/", http.FS(f))
  e.Run(":8080")
}

其它web框架各自可以试试。

模版操作

通过 embed 方式嵌入模版,渲染模版:

├── main.go
└── tmpl
    ├── en.tmpl
    └── zh.tmpl
package main

import (
    "embed"
    "fmt"
    "html/template"
    "net/http"
)

//go:embed tmpl/*.tmpl
var f embed.FS

func main() {
  t, err := template.ParseFS(f, "tmpl/*.tmpl")
  if err != nil {
    panic(err)
  }

  // /hello?lang=xx.tmpl
  http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
    r.ParseForm()

    t.ExecuteTemplate(w, r.FormValue("lang"), nil)
  })

  http.ListenAndServe(":8080", nil)
}

版本嵌入

通常我们需要将我们的版本打包到二进制文件中,以便确定我们的版本信息。embed 之前我们可以采取通过 -ldflags 的方法将版本动态的赋值到变量。现在,我们可以通过 embed 方式赋值啦。

# version_dev.go
// +build !prod

package main

var version string = "dev"
# version_prod.go
// +build prod

package main

import (
  _ "embed"
)

//go:embed version.txt
var version string

执行命令:

$ go run .
Version "dev"

$ go run -tags prod .
Version "0.0.1"

参考文档

[1] https://pkg.go.dev/embed
[2] How to Use //go:embed

本文链接:https://deepzz.com/post/how-to-use-go-embed.html参与评论 »

]]>
Fri, 19 Feb 2021 14:27:00 +0800
10 分钟理解什么是 OpenID Connect(OIDC) 协议 https://deepzz.com/post/what-is-oidc-protocol.html https://deepzz.com/post/what-is-oidc-protocol.html#comments https://deepzz.com/post/what-is-oidc-protocol.html 什么是 OIDC

OIDC是一个OAuth2上层的简单身份层协议。它允许客户端验证用户的身份并获取基本的用户配置信息。OIDC使用JSON Web Token(JWT)作为信息返回,通过符合OAuth2的流程来获取,更多详细 10 分钟理解什么是 OAuth 2.0 协议

OAuth2与资源访问和共享有关,而OIDC与用户身份验证有关。

其目的是为您提供多个站点的登录名。每次需要使用OIDC登录网站时,都会被重定向到登录的OpenID网站,然后再回到该网站。例如,如果选择使用Google帐户登录Auth0,这就使用了OIDC。成功通过Google身份验证并授权Auth0访问您的信息后,Google会将有关用户和执行的身份验证的信息发送回Auth0。此信息在JWT中返回,包含ID Token或者Access Token。

JWT包含Claims,它们是有关实体(通常是用户)的Claims(例如名称或电子邮件地址)和其他元数据。OIDC规范定义了一组标准的权利要求。这组标准声明包括姓名,电子邮件,性别,出生日期等。但是,如果要获取有关用户的信息,并且当前没有最能反映此信息的标准声明,则可以创建自定义声明并将其添加到令牌中。

较OAuth2,OIDC有一些不同的概念:

  • OpenID Provider(OP),实现OIDC的OAuth2授权服务器
  • Relying Party(RP),使用OIDC的OAuth2客户端
  • End-User(EU),用户
  • ID Token,JWT格式的授权Claims
  • UserInfo Endpoint,用户信息接口,通过ID Token访问时返回用户信息,此端点必须为HTTPS

协议流程

从理论上来讲,OIDC协议遵循以下步骤:

  1. RP发送认证请求到OP
  2. OP验证End-User并颁发授权
  3. OP用ID Token(通常是Access Token)进行响应
  4. RP携带Access Token发送请求到UserInfo Endpoint
  5. UserInfo Endpoint返回End-User的Claims
+--------+                                   +--------+
|        |                                   |        |
|        |---------(1) AuthN Request-------->|        |
|        |                                   |        |
|        |  +--------+                       |        |
|        |  |        |                       |        |
|        |  |  End-  |<--(2) AuthN & AuthZ-->|        |
|        |  |  User  |                       |        |
|   RP   |  |        |                       |   OP   |
|        |  +--------+                       |        |
|        |                                   |        |
|        |<--------(3) AuthN Response--------|        |
|        |                                   |        |
|        |---------(4) UserInfo Request----->|        |
|        |                                   |        |
|        |<--------(5) UserInfo Response-----|        |
|        |                                   |        |
+--------+                                   +--------+

ID Token

这里预先解释ID Token的含义,OIDC对OAuth2进行的主要扩展(用户用户身份验证)就是ID Token,为JWT格式。其中包含授权服务器对用户验证的Claims和其它请求的Claims。

在ID Token中,以下Clams适用于使用OIDC的所有OAuth2:

  • iss,必须,发行机构Issuer,大小写敏感的URL,不能包含query参数
  • sub,必须,用户身份Subject,Issuer为End-User分配的唯一标识符,大小写敏感不超过255 ASCII自符
  • aud,必须,特别的身份Audience,必须包含OAuth2的client_id,大小写敏感的字符串/数组
  • exp,必须,iat到期时间Expire,参数要求当前时间在该时间之前,通常可以时钟偏差几分钟,unix时间戳
  • iat,必须,JWT颁发时间Issuer at time,unix时间戳
  • auth_time,End-User验证时间,unix时间戳。当发出max_age或auth_time Claims时,必须。
  • nonce,用于将Client session和ID Token关联,减轻重放攻击,大小写敏感字符串
  • acr,可选,Authentication Context Class Reference,0 End-User不符合ISO/IEC 28115 level 1,不应该授权对任何货币价值的资源访问。大小写敏感的字符串。
  • amr,可选,Authentication Methods References,JSON字符串数组,身份验证的表示符,如可能使用了密码和OTP身份验证方式
  • azp,可选,Authorized party,被授权方。如果存在必须包含OAuth2的Client ID,仅当ID Token有单个Audience且与授权方不同时,才需要此Claim

ID Token可能包含其它Claims,任何未知的Claims都必须忽略。ID Token必须使用JWS进行签名,并分别使用JWS和JWE进行可选的签名和加密,从而提供身份验证、完整性、不可抵赖性和可选的机密性。如果对ID Token进行了加密,则必须先对其签名,结果是一个嵌套的JWT。ID Token不能使用nonce作为alg值,除非所使用的响应类型没有从Authorization Endpoint返回任何ID Token(如Authorization Code Flow),并且客户端在注册时显示请求使用nonce。

授权

身份验证遵循以下三种方式;授权码方式(response_type=code)、隐式方式(response_type=id_token token或response_type=id_token)、混合方式。

下表是三种方式的特征:

属性 授权码 隐式 混合
Token从authorization端点返回 no yes no
Token从token端点返回 yes no no
Token未显示给浏览器 yes no no
能够验证客户端 yes no yes
可以刷新Token yes no yes
一次交流 no yes no
服务器到服务器 yes no no

response_type对应的身份验证方式:

response_type 方式
code 授权码
id_token 隐式
id_token token 隐式
code id_token 混合
code token 混合
code id_token token 混合

除了由OAuth2定义的“response_type”之外,所有code均在 OAuth2多种响应类型编码实践。

注意OAuth2为隐式类型定义token的响应类型,但OIDC不会使用此响应类型,因为不会返回ID Token。

授权码方式

使用授权码方式时,所有Token从Token端点返回。授权码将授权code返回给客户端,然后客户端可以将其直接交换为ID Token和Access Token。这样的好处是不会向User-Agent及可能访问User-Agent的其它恶意应用公开任何Token。授权服务器还可以在交换Access Token的授权code之前对客户端进行身份验证。授权code适用于可以安全的维护其自身和授权服务器之间的客户端机密的客户端。

执行以下步骤:

  1. 客户端(RP)准备一个包含所需请求参数的身份验证请求
  2. 客户端将(RP)请求发送到授权服务器(OP)
  3. 授权服务器(OP)对用户(EU)进行身份验证
  4. 授权服务器(OP)获得用户同意/或授权
  5. 授权服务器(OP)使用授权码将用户发送回客户端(RP)
  6. 客户端(RP)使用Token Endpoint的授权码来请求响应
  7. 客户端(RP)收到响应,该响应Body中包含ID Token和Access Token
  8. 客户端(RP)验证ID Token并检索用户的标识符

授权请求

授权服务器(OP)的authorization端点需要支持GET和POST方法,GET采用Query String序列化,POST采用Form序列化。OIDC采用OAuth2的授权码流程参数:

  • scope,必须,OIDC必须包含openid的scope参数
  • response_type,必须,同OAuth2
  • client_id,必须,同OAuth2
  • redirect_uri,必须,同OAuth2
  • state,可选,同OAuth2

如:

HTTP/1.1 302 Found
Location: https://openid.c2id.com/login?
          response_type=code
          &scope=openid
          &client_id=s6BhdRkqt3
          &state=af0ifjsldkj
          &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb

授权响应

OP收到验证请求后,需要对请求参数做严格的验证:

  1. 验证OAuth2的相关参数
  2. 验证scope是否有openid参数,如果没有则为OAuth2请求
  3. 验证所有必须的参数是否都存在
  4. 如果sub是被要求了,必须尽在由子值标识的最终用户与活动session通过身份验证的情况下积极响应。不得使用不用用户的ID Token或Access Token响应,即使这些用户与授权服务器由活动session。如果支持claims,则可以使用id_token_hint发出请求。

验证通过后引导EU进行身份认证并同意授权。完成后,会重定向到RP指定的回调地址,并携带code和state相关参数:

HTTP/1.1 302 Found
Location: https://client.example.org/cb?
          code=SplxlOBeZQQYbYS6WxSbIA
          &state=af0ifjsldkj

获取Token

RP使用上一步获得的code请求token端点,然后就可以获得响应Token,其中除了OAuth2规定的数据外,还会附加一个id_token的字段,如:

POST /token HTTP/1.1
Host: openid.c2id.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW

grant_type=authorization_code
 &code=SplxlOBeZQQYbYS6WxSbIA
 &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb

成功后,OP会返回带有ID Token的JSON数据:

  HTTP/1.1 200 OK
  Content-Type: application/json
  Cache-Control: no-store
  Pragma: no-cache

  {
   "access_token": "SlAV32hkKG",
   "token_type": "Bearer",
   "refresh_token": "8xLOxBtZp8",
   "expires_in": 3600,
   "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.ewogImlzc
     yI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5
     NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZ
     fV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5Nz
     AKfQ.ggW8hZ1EuVLuxNuuIJKX_V8a_OMXzR0EHR9R6jgdqrOOF4daGU96Sr_P6q
     Jp6IcmD3HP99Obi1PRs-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CJ
     NqeGpe-gccMg4vfKjkM8FcGvnzZUN4_KSP0aAp1tOJ1zZwgjxqGByKHiOtX7Tpd
     QyHE5lcMiKPXfEIQILVq0pc_E2DzL7emopWoaoZTF_m0_N0YzFC6g6EJbOEoRoS
     K5hoDalrcvRYLSrQAZZKflyuVCyixEoV9GfNQC3_osjzw2PAithfubEEBLuVVk4
     XUVrWOLrLl0nx7RkKU8NXNHq-rvKMzqg"
  }

在拿到这些信息后,需要对id_token及access_token进行验证。验证成功就可以通过UserInfo端点获取用户信息了。

验证Token

授权服务器必须验证Token的有效性:

  • 根据RFC6749
  • 验证ID Token规则
  • 验证Access Token规则

UserInfo

获取用户信息,客户端(RP)可以通过GET或POST请求通过UserInfo Endpoint获取用户信息。

GET /userinfo HTTP/1.1
Host: openid.c2id.com
Authorization: Bearer SlAV32hkKG

请求成功:

{
   "sub"                     : "alice",
   "email"                   : "alice@wonderland.net",
   "email_verified"          : true,
   "name"                    : "Alice Adams",
   "given_name"              : "Alice",
   "family_name"             : "Adams",
   "phone_number"            : "+359 (99) 100200305",
   "profile"                 : "https://c2id.com/users/alice",
   "https://c2id.com/groups" : [ "audit", "admin" ]
}

隐式授权

隐式授权,所有Token都从授权端点返回。主要由浏览器中使用脚本语言实现的客户机使用。访问Token和ID Token直接返回给客户端,授权服务器不执行客户端身份验证。

  1. 客户端(RP)携带认证参数发送请求到授权服务器(OP)
  2. 授权服务器(OP)验证用户并得到用户批准
  3. 授权服务器(OP)携带用户相关信息+ID Token/Access Token返回到客户端(RP)
  4. 客户端(RP)验证ID Token和检索用户标识符

授权请求

  • response_type,必须,’id_token token’或’id_token’。无Access Token使用’id_token’
  • redirect_uri,必须,OP处登记的重定向地址
  • nonce,必须,隐式授权必须

授权响应

  • access_token,如果response_type是id_token可以不反回
  • token_type,固定为Bearer,
  • id_token,必须,ID Token
  • state
  • expires_in,可选,Access Token到期时间(s)

之后就可以拿着ID Token

混合授权

是上面两种模式的混合。可选response_type有:code id_token,code token,code id_token token。

参考资料

[1] https://openid.net/specs/openid-connect-core-1_0.html
[2] https://www.jianshu.com/p/be7cc032a4e9
[3] https://demo.c2id.com/oidc-client/

本文链接:https://deepzz.com/post/what-is-oidc-protocol.html参与评论 »

]]>
Fri, 18 Sep 2020 16:18:00 +0800
上海之行,告一段落 https://deepzz.com/post/my-shanghai-life.html https://deepzz.com/post/my-shanghai-life.html#comments https://deepzz.com/post/my-shanghai-life.html 是时候离开上海了,脑袋里生出这个念头的时候已是一年前。

上海,中国最发达的城市之一,是很多人实现梦想的地方,这里有很多之最,吸引这无数年轻人在这里奋斗。这里为外来的我们提供了非常多的便利,基本政策非常好。可,成功毕竟是少数人的。

还记得,最初:无知小伙,怀揣着见一见大城市的心情来到了上海,见一见曾经的上海滩,看一看儿时记忆里的东方之珠。几年过去的今天,小伙已然成为00后口中的大叔,成为了前浪。仿佛应该让出位置给更多的年轻人机会,哈哈。

四年时间,让我学到了很多,成长了很多,也让我对自己有一个清楚的认识,更加确定未来的方向。从来不敢想象能够在上海安家,毕竟以现在的工资比较渺茫,离开是必然。和在一起工作了四年的小伙伴已经磨合的非常好,一个眼神都知道是不是有需要交流的地方。决定离开的那天,不是很舒服,离别是一件让人伤感的事情,况且是一个奋斗过四年的地方。

因为个人种种原因,不得不回成都。决定了,那就洒洒脱脱的离开。非常感谢公司和领导对我的支持与帮助,感谢我们能够相遇,共事。感谢我们有机会能够成为朋友,相知。今天的分别不舍,希望我们依然可以相遇,莫要断了这份情。

离开前,躺在床上脑袋时常回想这些年在上海的际遇。画面一张张闪过,是上海呀:一座现代化的大都市。这里有著名的外滩,有享誉世界的陆家嘴,有中国内地唯一的迪士尼。有经常去的电影院,有熟悉的话剧院,有好玩的密室逃脱,各种沉浸式的表演。更多的是我那朝夕相处了四年的小伙伴呐。

途径地铁,看着匆匆走过的人群,和我曾经一样,每一位都是为着自己能够在这个城市立足而努力着,奋斗着,不甘平庸的年轻人。加油,年轻人!我先撤。

是的,今天我已经回到成都,一个生我养我的城市。这里没有了熟悉的公司,熟悉的同事,熟悉的周边商场。同时,面对的是未知,些许担忧:不知道即将面对什么工作,与什么样的同事相处,公司前景如何。不过,信心总是有的,这几年不是白练的。

收获

上海之行,收获颇丰,无论工作还是生活。

工作中,有幸结识了一群优秀的小伙伴,四年的相处中,我们的默契可以说达到了灵魂阶段,非常融洽。每个人都有各自的特点,优点,从他们身上学到很多:坚持、乐观、专业、刻苦。相信我们还会再见,来成都,我请客!

生活中,从一名单身狗,到有了女朋友,到最后成婚,有了家庭。相信会越来越好!同时,也收获了一身负债,哈哈。

成长

自我感觉最深的是:学会了如何思考。几年前,是一个比较冲动的人,遇事沉不住气,意气风发。现在稍微好多了,或许是岁月磨平了棱角。

更多的是一个角色的转变,从而自己的心态也跟着转变。如处理生产事故,需要在极短的时间内找出问题,并且修复它,这需要你有扎实的技术基础和心态。如你带新人,你需要细致耐心的去引导他如何更好的处理问题,解决问题,以致最后学会如何思考问题。等等,你需要的更快成长。

等你技术到了一个瓶颈的时候,思考的问题也发生了转变。是继续在技术上深耕,还是转向人员的管理。技术上如何精进?更多的时候是自己的迷茫,就像是你成为了一个新人需要人带。所以,换个环境,感受一下不同?人员的管理,你会不会有内心的惶恐,前一步还安安心心打酱油,后一步就得学着如何规划:事与人,学着如何去处理部门与部门之间的沟通,人与人之间的沟通,人与事的合理安排。

很庆幸,在老东家学到了不少(虽只是皮毛),这对我自己来说已是一次不可多得的历练。相信,学以致用,学有所成。

现在

现在,已然成为一名新人,一个新的环境。

很多时候,新环境能够激发人类的一些潜质,一些新的东西会使你迸发出新的思维。你能够学习的东西更多,遇到的难题也会更多,需要掌握技术的深度与广度也更高。

同时,挑战意味着机遇,学着如何在挑战中成长才是我们需要关注的地方。加油,兄die!

所以,让自己偶尔处于一个不太舒适的环境或许遇见不一样的你。

未来

未来已来!大多数时候能够从现在的你窥见未来的你。一句老话说得好:努力不一定成功,但不努力一定不会成功。所有成功都是来之不易,点滴积累,最要坚持。

如果此时此刻的你还不知道坚持什么,可以和我交流。

如果此时此刻的你还不知道为什么坚持,可以和我交流。

如果此时此刻的你想要放弃,卧槽,那你一定不会看到这句。

致: 感谢我们已经经历过的,享受我们正在经历的,期待我们即将经历的!

上海

爱上海,爱生活! img_0930.jpeg

本文链接:https://deepzz.com/post/my-shanghai-life.html参与评论 »

]]>
Tue, 01 Sep 2020 23:33:00 +0800
nginx server 如何写,如何写 nginx 配置 https://deepzz.com/post/how-to-write-nginx-server.html https://deepzz.com/post/how-to-write-nginx-server.html#comments https://deepzz.com/post/how-to-write-nginx-server.html 本篇文章主要介绍 nginx server 虚拟服务器如何配置。其中包括的一些例子我会已保存到 nginx 。尽可能的想要整理一份比较完整的配置说明,避免找寻资料的麻烦。博主也尽可能的保证本篇文章的准确性,如有失误,请告知。

通过 $ nginx -V,你可以看到 nginx 的编译配置信息:

$ nginx -V
nginx version: nginx/1.17.3
built by gcc 8.3.0 (Debian 8.3.0-6)
built with OpenSSL 1.1.1c  28 May 2019
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules --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 --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads ...

其中可以看到 --prefix=/etc/nginx,nginx 安装时会把相关数据文件写入到该目录,如我们的配置文件 --conf-path

每次更改 nginx 的配置文件,你需要执行一下操作:

# 验证配置文件的正确性
$ nginx -T

# 重新加载配置文件
$ nginx -s reload

基础概念

这里推荐 nginx 的官方文档地址:http://nginx.org/en/docs/

文档中涵盖了各个模块的配置用法,以及默认值,可以填写的上下文位置。 nginx-module-doc

目前 nginx 支持多种服务类型:

http
mail
stream
google perftools

我们 着重介绍 http 服务。其它服务基本知识点都能涵盖到。

安装完 nginx ,我们先来看一看 nginx 的默认配置 /etc/nginx/nginx.conf,当然可能与你的默认配置不同,不过大同小异:

# worker以什么身份运行
user  nginx; // default nobody

# worker进程个数,一般为 CPU 个数,也可选 auto
worker_processes  1; # default 1

# 每个worker可打开的描述符限制
worker_rlimit_nofile 8192;

# 错误日志保存路径和级别
error_log  /var/log/nginx/error.log warn;

# 进程pid保存路径
pid        /var/run/nginx.pid;

# 指定dns服务器
resolver 10.0.0.1;

events {
    # 每个worker最大连接数
    worker_connections  1024; # default 1024
}

# http 服务定义
http {
    # 加载 mime 类型
    include       /etc/nginx/mime.types;
    # 定义默认数据类型
    default_type  application/octet-stream;
    # 日志格式
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    # 访问日志
    access_log  /var/log/nginx/access.log  main;
    # 是否调用sendfile函数(zero copy 方式)来输出文件,如果磁盘IO重负载应用,可设置为off
    sendfile        on;
    # 此选项允许或禁止使用socke的TCP_CORK的选项,此选项仅在使用sendfile的时候使用
    #tcp_nopush     on;

    keepalive_timeout  65;

    # 代理相关设置
    # proxy_connect_timeout 90;
    # proxy_read_timeout 180;
    # proxy_send_timeout 180;
    # proxy_buffer_size 256k;
    # proxy_buffers 4 256k;
    # proxy_busy_buffers_size 256k;
    # proxy_temp_file_write_size 256k;

    # tcp_nodelay on;
    
    # gzip 压缩
    #gzip  on;

    # 加载其它配置,这样我们在 conf.d 下写的文件才会生效
    include /etc/nginx/conf.d/*.conf;
}

加载配置 /etc/nginx/conf.d,才能让我们的配置生效:

# 加载其它配置
include /etc/nginx/conf.d/*.conf;

一般的,如果是小站点不用去修改默认配置。当流量到达一定程度,需要进行适当优化。

内置变量

内置变量,nginx 各个模块都将请求的一些参数进行变量化,通过 $ + 变量名 即可使用。每个模块或多或少都有自己的变量。着重介绍下核心模块的 内置变量

# 通过arg_<name>的方式可取出相关参数,若请求 /foo?name=Tony&age=2,则 arg_name=tony arg_age=2
$arg_name
$args
# 客户端IP地址二进制
$binary_remote_addr
# 发送到客户端的字节数,不包括响应头
$body_bytes_sent
# 发送给客户端字节数
$bytes_sent
# 连接序列号
$connection
# 当前已经连接的请求书
$connection_requests
# Content-Length 请求头
$content_length
# Content-Type 请求头
$content_type
# cookie 名称
$cookie_name
# 当前请求的 root 或 alias 的值
$document_root
# 与 $uri 相同
$document_uri
# 优先级:请求行中的 host name,请求头中的 Host,请求匹配的 server name
$host
# host name
$hostname
# 任意请求头字段。变量名的最后一部分是转换为小写的字段名,用下划线替换破折号
$http_name
# 如果连接在 SSL 模式下运行,则为 on,否则为空字符串
$https
# ? 后如果请求行有参数,或者空字符串
$is_args
# 设置此变量可以限制响应速度
$limit_rate
# 当前时间(秒),分辨率为毫秒
$msec
# nginx 版本号
$nginx_version
# 当前 worker 进程号
$pid
# 如果是 pipelined 则为 p,否则为 . 
$pipe
# 代理协议头中的客户端地址,否则为空字符串,代理协议之前必须通过在listen指令中设置 proxy_protocol 参数来启用
$proxy_protocol_addr
# 来自代理协议头的客户端端口,否则为空字符串,代理协议之前必须通过在listen指令中设置 proxy_protocol 参数来启用
$proxy_protocol_port
# 与 $args 相同
$query_string
# 与当前请求的 root 或 alias 指令值对应的绝对路径名,所有符号链接都解析为实际路径
$realpath_root
# 客户端地址
$remote_addr
# 客户端端口
$remote_port
# 使用 Basic auth 的用户名
$remote_user
# 完整的请求行
$request
# 请求体,当将请求体读入内存缓冲区时,proxy_pass、fastcgi_pass、uwsgi_pass和scgi_pass指令处理的位置可以使用变量的值
$request_body
# 具有请求主体的临时文件的名称
$request_body_file
# 如果请求完成则为 OK,否则为空
$request_completion
# 当前请求的文件路径,基于 root 或 alias 和请求 URI
$request_filename
# 由16个随机字节生成的惟一请求标识符,以十六进制表示
$request_id
# 请求长度(包括请求行、头和请求体)
$request_length
# 请求方法,如 GET 或 POST
$request_method
# 请求处理时间,从客户端读取第一个字节以来的时间
$request_time
# 若请求 /foo?a=1&b=2,则 request_uri=/foo?a=1&b=2
$request_uri
# 如 http 或 https
$scheme
# 任意响应报头字段,变量名的最后一部分是转换为小写的字段名,用下划线替换破折号
$sent_http_name
# 响应结束时发送的任意字段,变量名的最后一部分是转换为小写的字段名,用下划线替换破折号
$sent_trailer_name
# 接受请求的服务器的地址
$server_addr
# 接受请求的 server 名称
$server_name
# 接受请求的 server 端口
$server_port
# 请求协议,如 HTTP/1.0 或 HTTP/1.1 或 HTTP/2.0
$server_protocol
# 响应状态
$status
$tcpinfo_rtt,$tcpinfo_rttvar,$tcpinfo_snd_cwnd,$tcpinfo_rcv_space
# 本地时间ISO 8601标准格式
$time_iso8601
# 通用日志格式的本地时间
$time_local
# 若请求 /foo?a=1&b=2,则 uri=/foo
$uri
# 用户代理
$http_user_agent
# cookie
$http_cookie

你还可以通过自定义变量指令 set 进行变量的定义。

server定义

server 即虚拟服务,它用来描述我们站点一些访问规则。需要填写在 http 标签中,可定义多个,如:

http {
    server {
        ...
    }
    server {
        ...
    }
    ...
}

一个常见的 server 的定义:

resolver 10.0.0.1;

# 负载均衡
upstream dynamic {    
    zone upstream_dynamic 64k;

    server backend1.example.com      weight=5;
    server backend2.example.com:8080 fail_timeout=5s slow_start=30s;
    server 192.0.2.1                 max_fails=3;
    server backend3.example.com      resolve;
    server backend4.example.com      service=http resolve;

    server backup1.example.com:8080  backup;
    server backup2.example.com:8080  backup;
}

# http服务
server {
    listen 80;
    server_name example.com www.example.com;
    location / {
        rewrite https://$host; # 重定向到https
    }
}

# https 服务
server {
    listen 443 ssl; # 监听端口
    server_name example.com www.example.com; # 匹配域名

    # ssl证书
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers         AES128-SHA:AES256-SHA:RC4-SHA:DES-CBC3-SHA:RC4-MD5;
    ssl_certificate     /usr/local/nginx/conf/cert.pem;
    ssl_certificate_key /usr/local/nginx/conf/cert.key;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 10m;

    # 静态服务
    location / {
        root /usr/share/nginx/html;
        index index.html index.htm;
    }
    # 反向代理
    location /api {
        proxy_pass http://dynamic;
        health_check;
    }
}

下面就让我们来详细解释下。

http_upstream_module

http_upstream_moduleupstream 说白了就是做负载均衡,它可以帮助我们定义一组相同服务的别名,如backend,当请求到来的时候可以通过相关策略帮我们选一组服务提供响应。

目前只能被 proxy_passfastcgi_passuwsgi_passscgi_passmemcached_passgrpc_pass 使用。

形式如下:

upstream <name> { # 命名
    server <address> [parameters]; # 服务
    server <address> [parameters];
    ...
}

[parameters] 参数可选以下值:

  • weight=number,default 1,设置 server 的权重
  • max_conns=number,default 0,限制 server 的活跃连接数,0 代表不限制
  • max_fails=number,default 1,设置在 fail_timeout 时间内失败的最大次数,可由 proxy_next_upstreamfastcgi_next_upstreamuwsgi_next_upstreamscgi_next_upstreammemcached_next_upstreamgrpc_next_upstream 指定下组 upstream,0 值代表不启用
  • fail_timeout=time,default 10s,设置多长时间判定无连接服务器失败
  • backup,标记 server 为备用 server,当 primary server 不可用时启用
  • down,标记 server 下线不可用
  • resolve,用来监视与服务器域名对应IP地址的更改,它会自动更改上游配置,upstream 必须驻留在共享内存中,必须写在 http 标签中。
http {
  resolver 10.0.0.1;

  upstream u {
    zone ...;
    ...
    server example.com resolve;
  }
}
  • route=string,设置 server 路由名称
  • server=name,
  • slow_start=time,慢启动,server 非正常状态恢复到正常需要的时间
  • drain,设置为 drain 模式

其它负载均衡设置:

  • zone name [size],设置共享内存的名称和大小
  • state file
  • hash key [consistent],负载均衡方式,key 可以为文本,变量,或其组合
  • ip_hash,负载均衡方式,根据IP地址范围分布 server,用 IPv4 前三个8位或整个IPv6
  • keepalive connections,设置到上游 server 保持最大空闲连接
upstream memcached_backend {
  server 127.0.0.1:11211;
  server 10.0.0.2:11211;

  keepalive 32;
}

server {
  ...

  location /memcached/ {
    set $memcached_key $uri;
    memcached_pass memcached_backend;
  }
}
  • keepalive_requests number,设置最大请求连接数
  • keepalive_timeout timeout,连接超时时间
  • ntlm,允许使用NTLM身份验证代理请求
upstream http_backend {
  server 127.0.0.1:8080;

  ntlm;
}

server {
  ...

  location /http/ {
    proxy_pass http://http_backend;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    ...
  }
}
  • least_conn,负载均衡方式,将请求传给活跃连接数最少的 server
  • least_time header | last_byte [inflight],负载均衡方式,将请求传给平均响应时间和活跃连接数最少的 server
  • queue number [timeout=time],队列缓存,当选择不到 server 处理请求时放入队列,如果队列满,返回502
  • random [two [method]],负载均衡方式,
  • sticky,会话关联,同一客户端请求将会被传给同一 upstream 的同一 server
  # cookie
  upstream backend {
    server backend1.example.com route=a;
    server backend2.example.com route=b;

    sticky cookie srv_id expires=1h domain=.example.com path=/;
  }
  # route
  map $cookie_jsessionid $route_cookie {
    ~.+\.(?P<route>\w+)$ $route;
  }

  map $request_uri $route_uri {
    ~jsessionid=.+\.(?P<route>\w+)$ $route;
  }

  upstream backend {
    server backend1.example.com route=a;
    server backend2.example.com route=b;

    sticky route $route_cookie $route_uri;
  }
  # learn
  upstream backend {
    server backend1.example.com:8080;
    server backend2.example.com:8081;

    sticky learn
      create=$upstream_cookie_examplecookie
      lookup=$cookie_examplecookie
      zone=client_sessions:1m;
  }

listen

listen 监听设置,来看一看可选参数:

默认 listen *:80 | *:8000;
listen address[:port] [default_server] [ssl] [http2 | spdy] [proxy_protocol] [setfib=number] [fastopen=number] [backlog=number] [rcvbuf=size] [sndbuf=size] [accept_filter=filter] [deferred] [bind] [ipv6only=on|off] [reuseport] [so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt]];
listen port [default_server] [ssl] [http2 | spdy] [proxy_protocol] [setfib=number] [fastopen=number] [backlog=number] [rcvbuf=size] [sndbuf=size] [accept_filter=filter] [deferred] [bind] [ipv6only=on|off] [reuseport] [so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt]];
listen unix:path [default_server] [ssl] [http2 | spdy] [proxy_protocol] [backlog=number] [rcvbuf=size] [sndbuf=size] [accept_filter=filter] [deferred] [bind] [so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt]];

真的多,可平时也没用几个,举例:

listen 127.0.0.1:8000;
listen 127.0.0.1; # 如果只指定地址,默认监听 80
listen 8000;
listen *:8000;
listen localhost:8000;
listen 127.0.0.1 default_server accept_filter=dataready backlog=1024;

# IPv6
listen [::]:8000;
listen [::1];

# unix socket
listen unix:/var/run/nginx.sock;

其它参数说明:

  • default_server,如果指定,server 将会成为默认 server
  • ssl,开启 ssl 模式,即 https
  • http2,正常情况开启 http2 都应该开始 ssl,但 nginx 也支持不开启 ssl 下的 http2 协议
  • spdy,和 http2 一样,建议开启 ssl
  • setfib=number,监听套接字设置关联的路由表FIB (SO_SETFIB选项)。这目前只适用于FreeBSD
  • fastopen=number,为监听套接字启用“TCP Fast Open”(1.5.8),并限制尚未完成三方握手的连接队列的最大长度
  • backlog=number
  • rcvbuf=size,接受 buffer 的大小(SO_CRCVBUF)
  • sndbuf=size,发送 buffer 的大小(SO_SNDBUF)
  • accept_filter=filter,可选 dataready 和 httpready,在 accept() 前过滤
  • deferred,指示在Linux上使用deferred accept() (TCP_DEFER_ACCEPT套接字选项)
  • bind,标记指定 address:port 单独的绑定
  • ipv6only on|off,只接受 IPv6 连接
  • reuseport
  • so_keepaliv on|off|[keepidle]:[keepintv1]:[keepcnt],”TCP keepalive” 开关

server_name

server_name,设置虚拟主机的名称。

形式如下:

默认值 server_name "";
server_name name ...;

例1,穷举域名

server {
    server_name example.com www.example.com;
}

例2,通配符写法

server {
    server_name example.com *.example.com www.example.*;
}

例3,这种写法满足例1

server {
    server_name .example.com;
}

例4,正则表达式,以 ~ 开头

server {
    server_name www.example.com ~^www\d+\.example\.com$;
}

例5,正则表达式捕获

server {
    server_name ~^(www\.)?(.+)$;

    location / {
        root /sites/$2;
    }
}

server {
    server_name _;

    location / {
        root /sites/default;
    }
}

例6,正则表达式变量

server {
    server_name ~^(www\.)?(?<domain>.+)$;

    location / {
        root /sites/$domain;
    }
}

server {
    server_name _;

    location / {
        root /sites/default;
    }
}

例7,与空名称使用

server {
    server_name www.example.com "";
}

如果当一个名称匹配多个 server 的是时候,匹配优先级如下:

  1. 确切的名称
  2. 以 * 开头的最长的通配符名称
  3. 以 * 结尾的最长通配符名称
  4. 第一个匹配的正则表达式

更多匹配规则请查阅:http://nginx.org/en/docs/http/server_names.html

location

location 是用来干嘛的,它是用来根据 URI 进行配置设置的,如:

server {
    listen 80;
    server_name example.com;

    location / { # 普通请求网页
        root /usr/share/nginx/html;
        index index.html index.htm;
    }

    location /api { # API请求代理
        proxy_pass http://dynamic;
        health_check;
    }
}

形式如下:

location [ = | ~ | ~* | ^~ ] uri { ... }
  • none,如果没有修饰符,则将该位置解释为前缀匹配。这意味着给定的位置将根据请求URI的开头进行匹配,以确定匹配
  • =,代表精确匹配,完全相等即匹配
  • ~,区分大小写的正则表达式匹配
  • ~*,不区分大小写的正则表达式匹配
  • ^~,普通字符匹配,如果该选项匹配,只匹配该选项

nginx 的匹配过程如下:

  1. 精确匹配 =,如果匹配成功,搜索停止
  2. 前缀匹配,最长位置匹配,如果该匹配具有 ^~,搜索停止
  3. 正则匹配,按配置文件中定义的顺序进行匹配。
  4. 如果第3条规则产生匹配的话,结果被使用。否则,使用第2条规则的结果。

让我们通过一个例子来了解下匹配规则:

location = / {
    [ configuration A ]
}

location / {
    [ configuration B ]
}

location /documents/ {
    [ configuration C ]
}

location ^~ /images/ {
    [ configuration D ]
}

location ~* \.(gif|jpg|jpeg)$ {
    [ configuration E ]
}

请求 / 将会匹配 A,请求 /index.html 将会匹配 B,请求 /documents/document.html 将会匹配 C,请求 /images/1.gif 将会匹配 D,请求 /documents/1.jpg 将会匹配 E。

ssl mode

ssl 模式可以让我们站点启用 HTTPS,具体详细请参考 http_ssl_module

想要开启 ssl 模式,需要在 listen 关键字处添加上 ssl,如:

server {
    listen              443 ssl;
    server_name         example.com;

    ssl_certificate     example.com.rsa.crt;
    ssl_certificate_key example.com.rsa.key;

    ssl_certificate     example.com.ecdsa.crt;
    ssl_certificate_key example.com.ecdsa.key;

    ...
}

上面的例子是部署双证书,当某一证书因某种原因失效不至于导致站点不能访问。下面来看看参数解释:

  • ssl_buffer_size size,default 16k,发送数据的缓冲区的大小
  • ssl_certificate file,PEM 格式证书文件
  • ssl_certificate_key file,PEM 格式私钥文件
  • ssl_ciphers ciphers,default HIGH:!aNULL:!MD5,ssl套件 openssl ciphers
  • ssl_client_certificate file,用于验证客户端证书的 CA 文件
  • ssl_crl file,用于验证客户端证书的吊销文件
  • ssl_dhparam file,为DHE密码指定具有DH参数的文件
  • ssl_early_data on|off,default on
  • ssl_ecdh_curve curve,default auto,为ECDHE密码指定一条曲线
  • ssl_password_file file,私钥密码文件
  • ssl_prefer_server_ciphers on|off,是否启用服务器套件偏好
  • ssl_protocols [SSLv2] [SSLv3] [SSLv3] [TLSv1] [TLSv1.1] [TLSv1.2] [TLSv1.3],default TLSv1 TLSv1.1 TLSv1.2,可选的ssl协议
  • ssl_session_cache off|none|[builtin[:size]] [shared:name:size],default none,设置 session cache 的类型和大小
ssl_session_cache builtin:1000 shared:SSL:10m;
  • ssl_session_ticket_key file,设置一个文件,其中包含用于加密和解密TLS会话票据的密钥
ssl_session_ticket_key current.key;
ssl_session_ticket_key previous.key;

随机一个 AES256(80),AES128(40)
openssl rand 80 > ticket.key
  • ssl_session_tickets on|off,default on,是否启用 session ticket
  • ssl_session_timeout time,default 5m,超时时间
  • ssl_stapling on|off,default off,ocsp 装订 ssl_stapling on; resolver 192.0.2.1;
  • ssl_stapling_file file
  • ssl_stapling_responder url
  • ssl_stapling_verify on|off,default off
  • ssl_trusted_certificate file,指定验证客户端证书的 CA 文件
  • ssl_verify_client on|off|optional|optional_no_ca,default off,是否验证客户端证书
  • ssl_verify_depth number,default 1,设置客户端证书链的验证深度

相关变量

$ssl_cipher,已建立连接使用的 ciphers
$ssl_ciphers,客户端支持的 ciphers
$ssl_client_escaped_cert,urlencoded 客户端证书
$ssl_client_fingerprint,SHA1指纹
$ssl_client_i_dn,issuer DN
$ssl_client_i_dn_legacy,同上,1.11.6之后使用
$ssl_client_raw_cert,PEM格式客户端证书
$ssl_client_s_dn,subject DN
$ssl_client_s_dn_legacy,同上,1.11.6之后使用
$ssl_client_serial,客户端证书序列号
$ssl_client_v_end,客户端证书结束时间
$ssl_client_v_remain,剩余多少天
$ssl_client_v_start,证书开始时间
$ssl_client_verify,客户端证书是否验证成功,"SUCCESS" 或 "FAILED:reason" 或 "NONE"
$ssl_curves,客户端支持的曲线
$ssl_early_data
$ssl_protocol,连接使用的协议
$ssl_server_name,从 SNI 获取的 server name
$ssl_session_id,连接的 session id
$ssl_session_reused,session是否重用,"r" 重用,"." 没有

其它模块

其它模块你需要根据文档及编译信息判断该模块是否默认编译在 nginx 中,并且版本是否匹配:

ngx_http_access_module
ngx_http_addition_module
ngx_http_api_module
ngx_http_auth_basic_module
ngx_http_auth_jwt_module
ngx_http_auth_request_module
ngx_http_autoindex_module
ngx_http_browser_module
ngx_http_charset_module
ngx_http_dav_module
ngx_http_empty_gif_module
ngx_http_f4f_module
ngx_http_fastcgi_module
ngx_http_flv_module
ngx_http_geo_module
ngx_http_geoip_module
ngx_http_grpc_module
ngx_http_gunzip_module
ngx_http_gzip_module
ngx_http_gzip_static_module
ngx_http_headers_module
ngx_http_hls_module
ngx_http_image_filter_module
ngx_http_index_module
ngx_http_js_module
ngx_http_keyval_module
ngx_http_limit_conn_module
ngx_http_limit_req_module
ngx_http_log_module
ngx_http_map_module
ngx_http_memcached_module
ngx_http_mirror_module
ngx_http_mp4_module
ngx_http_perl_module
ngx_http_proxy_module
ngx_http_random_index_module
ngx_http_realip_module
ngx_http_referer_module
ngx_http_rewrite_module
ngx_http_scgi_module
ngx_http_secure_link_module
ngx_http_session_log_module
ngx_http_slice_module
ngx_http_spdy_module
ngx_http_split_clients_module
ngx_http_ssi_module
ngx_http_ssl_module
ngx_http_status_module
ngx_http_stub_status_module
ngx_http_sub_module
ngx_http_upstream_module
ngx_http_upstream_conf_module
ngx_http_upstream_hc_module
ngx_http_userid_module
ngx_http_uwsgi_module
ngx_http_v2_module
ngx_http_xslt_module

nginx完整配置

一份给新人的 nginx 完整配置:https://github.com/deepzz0/nginx

参考链接

[1] http://nginx.org/en/docs/

本文链接:https://deepzz.com/post/how-to-write-nginx-server.html参与评论 »

]]>
Sun, 18 Aug 2019 02:30:00 +0800
她和他的第一次出国自由行 - 巴厘岛 https://deepzz.com/post/we-are-traveled-in-bali.html https://deepzz.com/post/we-are-traveled-in-bali.html#comments https://deepzz.com/post/we-are-traveled-in-bali.html 前一篇文章:她和他的第一次出国自由行 - 出行模版

她姓陈,他也姓陈,她和他走到了一起,于是他们的旅程开始了。

她是个活泼开朗的人,喜欢所有美好的东西,向往自由。她做事直接,雷厉风行却也做事细心,知书达理。 他是个典型的程序员,理性睿智,性格沉稳。但同时也有着一颗走出去的心,喜欢有挑战的东西,登山,漂流。

一天某人说想要去巴厘岛旅行,他说我也要去。于是,她以后的计划里都不再是她一个人,她要和她爱的人一同出发啦。说干就干,她开始做攻略了,购买有关巴厘岛旅行相关的书,泡在马蜂窝看各种攻略,安装各种旅行APP。她说这是她第一次做这么详细的攻略,也是第一次出国自由行,她怕她做的攻略不完美。可她怎会知道,他正认真的看着她做攻略的样子,认真,笃定。很感谢她带给他的这种感觉,这是幸福的味道。

经过多日奋战及最后的实践,我们得出了如下时间计划: bali-time

旅行预算

愉快的旅行必定少不了精心的预算环节,毕竟大多数人还是需要考虑花多少钱的问题。因为机票和酒店的费用因淡旺季的原因起伏很大,这里就不列举了。

  • 一日游项目 2(人)x¥570x2(天)=¥2280,只计划了前两天的一日游项目。一日游一般包酒店接送,包午餐。
  • 吃喝 2(人)x2(餐)x¥75x6(天)+¥200=¥2050,平均一餐¥150左右,外加¥200的上浮。去除了一日游的午餐,故少一天。
  • 包车 ¥350x4(天)=¥1400,计划有4天自由行(包含小费),想去哪就去哪。
  • 门票 2(人)x¥50x4(天)=¥400,包车自由行的花需要自己开销门票。

根据上面的预算,我们总共大概需要¥6100。因为我们一日游是通过网上订票,因此我们只在国内(中国银行)兑换了$500,兑换美金的原因是在兑换印尼盾的时候可能汇率稍微高些(也没太注意),人民币也可以在当地兑换。总共身上携带现金共$500+¥1000。另外巴厘岛的ATM是支持银联卡的。

而事实上,我们的实际花销其实是和计划差不多的。就是,在吃的问题上吃了一次大餐(被坑),当时是去金巴兰看日落,身上又刚去兑换了印尼盾。身揣巨款不得虚,我们的司机直接就把我们拉到一个海鲜店里。尼玛,乡巴佬没吃过大龙虾啊,尝试一下!事后才感觉坑。。。

行前准备

一定要做如下准备,却保你的旅行畅通无忧,安全前行。

  1. 签证,目的地是否需要签证,巴厘岛免签。确保签证下来之后再继续下面的安排。
  2. 预定机票,根据你们的计划时间提前预定机票。国际旅行确保是否转机,行李是否直挂。
  3. 预定酒店,最好提前预定酒店,免得到时候像无头苍蝇乱撞,麻烦。最好根据旅行行程预定游玩景点附近的酒店,省的奔波,辛苦。
  4. 下载APP,提前想好我们在国外可能遇到的问题,做好准备。 bali-app
    • 导航,谷歌地图。国外貌似谷歌地图才好使
    • 翻译,谷歌翻译百度翻译。是在不知道怎么表达的时候派上用场。
    • 旅行,携程飞猪马蜂窝,可以在上面定一日游,找司机等。
    • 出行,UberGrabGo-Jek,东南亚的滴滴,美团,好用得很。
    • 民宿,AirbnbAgoda,用来预定特色民宿,很不错。
  5. 网络通讯,一定要保持网络通畅,出门在外不容易啊。 bali-sim
    • 国际漫游,如果不嫌贵可以申请开通国际漫游服务。
    • 当地电话卡,去淘宝购买当地电话卡,非常方便。
    • 租赁WIFI,让我们随时都有网可以上。需要先预定,然后在机场借还。
  6. 相关资料,可以打印需要的重要资料备份,以防手机丢失或没电。

当然还有我们自己的行李了,巴厘岛属于热带,太阳非常的毒辣,一定要注意防嗮。不然要脱两层皮。每年10月 - 次年3月为雨季,注意防雨。

实际行程

总体来说我们的行程安排的不是很满,有快有慢,这才是节奏。毕竟,旅行的最终意义在于感受与理解。看到了什么,理解了什么,要有所得才好啊。

出发前,前往双流机场T1取了租借的随身Wifi,托运行李。因为是到新加坡转机,行李直挂,所以我们拿到了两程的机票。接下来就是长达4个多小时的飞行时间。到达新加坡之后,衣服要换成夏装。可以逛逛这边的免税店,她说有的卖得比国内便宜几百块。有需要的朋友也可以提前在网上订购,等回程的时候直接去取就可以了。接着就是从新加坡到巴厘岛啦,期间会让你填一张海关申请表: bali-customs

Day1

大概在早上10点,我们到达伍拉赖机场(这个机场有好多名字)。在机场兑换了$200,事实证明机场兑换稍微少那么几十块。一出机场,好家伙,一股热气袭来。哇,从冬天直接到夏天了。

当天下着小雨,不过没有影响到我们的心情,因为有司机直接接我们去酒店。她在微信上联系的,到库塔的海滩馨乐酒店120k。 bali-kuta-hotel

不过酒店要下午2点才能入住。没办法,我们只好将行李寄放在酒店,出去逛了逛。顺便吃个午餐(路边小店),这里的午餐还不错啊,挺实惠,能吃饱。 bali-kuta-food

整体而言,这个酒店是本次旅行最普通的,但也是我认为是最舒适的。酒店位置靠近库塔沙滩,只间隔一条马路。处在市中心,有大型商场,酒吧等娱乐场所。最重要的是提供早餐,早餐啊!特别棒。

入住之后,稍微整理了下。还是稍微睡个觉,休息一下。准备明天的行程。之后就是出去觅食啦。酒店后面是一条比较传统的老街,有各种手工作品,赏心悦目啊。

不过,对于雨季的担心还是不可避免啊,天呐。 bali-wheather

Day2

今天我们报了一个Penida岛深度游(不浮潜)。报一日游的好处就是自己不用找车,酒店专车接送,包午餐,有印尼范的中文导游。不好的地方就是线路一定,时间有限,一个点只能停留一个小时左右。

要到岛上需要先到码头坐40分钟左右的船(Fast Boat),期间有一段时间会有比较大的浪,可能会晕船,最好向导游要个袋子。之后是坐一个小时的车上山。整个过程只能用颠簸来形容,还非常的热,火辣(我们有理由相信天气预报不准)。期间你可以睡一觉哦,但一定要抓紧,弯很大,车很快。 bali-penida

bali-broken-bench

经过一路颠簸,你就会来到达传说中iPhone壁纸拍摄的地方。遗憾的是我们没有足够的时间,更加深一步的了解这个地方。

回到酒店,我们就约莫着可以吃晚饭了。累了一天还是好好犒劳自己,在大众点评上找了一家网红,猫途鹰上的评价也还不错。说走就走,拐来拐去的还是把它找到了,不过要排队,很多人。 bali-kuta-fatchow

Day3

本来今天我们是打算去滑翔的,考虑到天气的问题,万一不能滑翔呢?要不调到后面再去把。这一调直接让我们错过了最想去的滑翔伞项目。

今天我们睡到自然醒,吃完早餐,懒洋洋的呆了会。下午包了半天的辆车,自由行。路线是:情人崖 -> Suluban bench -> 金巴兰日落。

可能因为对情人崖景点不太了解的缘故,我们没走几步,拍了一些照片就没继续了。其实前面还有很长的一段路程。也没见着传说中的猴子。 bali-qingrenya2

bali-qingrenya

之后,我们就去了离这里比较近的Suluban bench,真的超级棒。这里的人很少,是个小众景点,但美得很出众。 bali-suluban

晚上我们打车去了一个叫Warung Ipang的餐厅,这里真的是性价比超高,推荐。 bali-delicious

bali-delicious1

Day4

今天我们没有计划,有时候没有计划就是最好的计划。睡觉睡到自然醒,然后到传统市场逛逛,转转,感受一下慢生活。傍晚夕阳西下,弥补一下金巴兰没有看到日落的遗憾。尽管这里的沙滩有很多渣子,不过依然有美丽的日落。旁边还有踢沙滩足球的印尼孩童,美哉。

We’re at kuta beach.

bali-kuta-bench

bali-kuta-bench1

bali-kuta-bench2

Day5

今天我们要离开海滩馨乐酒店,去往我们的下一个很有特色的民宿,所以我们不得不带上我们的行李出发,包个车是不错的选择。

今天选择的第一个地方是印尼盾上印的一个 Temple,恰巧碰上了一个什么活动,挺有特色的。 bali-temple

之后,去了她心心念的 Ins 上看到的双子瀑布。 bali-twin-waterfall

最后,来到了拍照很美的Wanagiri Hidden Hill。这里门票真的贵,不过拍出来的照片真的美。面朝高山湖泊,我心似大海。 bali-hill

bali-hill2

Yes, 今天陈同志安排的是一个非常有特色的民宿圆顶屋。看似简单,内在却很丰富,这里包含了厕所,洗浴,以及多人玩耍的地,不过都是开放式的哦。屋子是建在田中间的,很有乡村气息,晚上能听到各种虫鸣,也有比较大胆的动物进来偷吃我们的零食,胆大!

不过这个圆顶屋是真的不好找,我们司机带我们险些走错路。 bali-dome1

bali-dome

Day6

今天,我们还得换地方。原本可以定两个晚上,可是后面日期都被预定出去了,罢了,换一个地方。

今天我们去了巴厘岛必打卡的地方Bali Swing。这里的服务员厉害的哦,见哪国人说哪国话,中文说得是溜溜的。一进Bali Swing就齐声高喊Welcome to Bali Swing,很棒。对了,一定要体验那根最长的秋千,爽。 bali-swing

接下来是去了一个瀑布,不过这个瀑布因为前一天晚上下雨的缘故,很浑浊。我们待了一会就转战下个点。

这会,我们来到了德格拉朗梯田,我们没有见到网上那些生长茂密的农作物,只瞧见了小苗。想要说的是,这里的民风有点不好,可能是作为游客的我们打扰到他们了吧。要进入到梯田,首先需要在坡上的小店吃东西,才能进去。等到我们走了一段路,又有个大妈搭了个草棚,堵在那要钱,讲了讲价,最终让我们进去了。没想到的是,在往梯田上走的时候还有个老哥要钱,那必须是没钱啊。一看我们要走,立马说免费还和我们聊了聊天,打听我们是哪里人。中国人是冤大头吗! bali-terrace

到了晚上,我们住的还是一个民宿,类似于圆顶屋,不过是一个独立的院子,有大门,圆顶屋下还有个水池养着鱼。这个民宿挺大的,有好几个独立的院子。院子外面房东弄了个无边的泳池,以及餐厅(对,就是提供早餐)。

晚上,我们又开始了我们的觅食之旅。不走出去不知道,一走出去吓一跳,这里就是民宿窝啊,这一遍全是民宿,连餐厅都能够开到民宿里。如果能生活在这个地方多好,好不悠闲自在。

Day7

今天迎来了我们的终极挑战Campuhan Ridge Walk,乌布坎普罕山脊徒步。说是徒步,那就是晒太阳,太阳太大了,没走几步,汗如雨下啊。期间遇到很多外国友人,还有带娃的,冲啊。 bali-hike

实际上,这条徒步路线非常出名,据说排名世界前三。本来我是下决心要走完几公里的,某人不知道咋想的,看到后面的路景色不美了就不走了。计划三个小时完成的徒步之旅,一个小时就搞定了,这导致我们还有很多时间。我们的司机推荐我们去漂流,好像是提前准备好的,汗。

漂流的话最好是提前预定,这样才好安排人手。我们去的时候因为只有我们俩,最后漂流的时候加上教练也就仨,没有人多那么刺激。不过咱也没虚过谁,以仨人之力挑战其它5~7人皮划艇,玩得不亦乐乎。阿勇河上有很多小瀑布,我们教练就是自嗨型的人,直接下去把我们推到瀑布下,哇,透心凉。

阿勇河很长,大概要漂流2个小时,感觉还是不过瘾啊,哈哈。

晚上是我们在巴厘岛住的最后一晚了,选择了网红酒店阿卡萨别墅,就好好的享受最后一晚把。晚上我们又去了Warang Ipang,回味一下。我们还定了一个漂浮早餐,感受一下。

bali-breakfast-float

不过就个人感觉而言,该别墅不推荐。屋里什么都要付费,厕所像是没有清洗过一样,不太干净。最后我们想打个前台电话,翻遍了手册,没找到有用的信息(因为里面全是付费清单),清单还翻译成了中文。最后还是按酒店常规配置按0拨过去的。

Day8

最后一天也贼精彩。

我们享受了我们的漂浮早餐。满满的活力,出发,回家。

司机一大早就到酒店,接上我们就直接去机场了。8天的巴厘岛之行就这样结束了。可事情没完…

抵达新加坡,我们需要去领取她朋友在网上订购的化妆品。因为时间的问题,我们大概只有1个多小时的逗留时间。当时的时间是非常紧张的,再加上网络的问题,非常着急。iShop柜台在哪?怎么又有T1和T2?我们先去T1还是T2?怎么去T1?我们拿着网站给的地图,着急的寻找着。最后还是到柜台咨询才找到方向,然后决定先去T1,到了T1柜台处,一询问:天,原来每个航站楼都有一个柜台,他们会根据你的航班将物品放在指定的iShop柜台。

好家伙,我们再返回T2,赶紧跑到柜台。当时就震惊了,这么多人。没办法,只能抱歉的插个位置了,对服务人员说航班即将起飞,能否帮我们先处理。很nice的小姐姐,帮我们一一核对清单。登上飞往成都的航班。

还掉WIFI,巴厘岛之行圆满落幕!

本文链接:https://deepzz.com/post/we-are-traveled-in-bali.html参与评论 »

]]>
Wed, 06 Mar 2019 22:57:00 +0800
她和他的第一次出国自由行 - 出行模版 https://deepzz.com/post/our-travel-template.html https://deepzz.com/post/our-travel-template.html#comments https://deepzz.com/post/our-travel-template.html 第一次出国自由行,心里难免有些慌张,哈。希望在这里记录下本次出行的经验,做一个模版供下次出行参考。

三要素

和写小说一样,地点人物时间是我认为最重要的三个要素。

地点,首先确定地点,有些地方真的值得前去。

  • 安全,安全永远放在第一位。
  • 目的,美景、美食、美人,总之这个地方一定要有吸引你的地方。
  • 交通,包车,租车,还是公共交通。当地交通的便利性决定了我们旅行的欢乐值。
  • 物价,当地物价,基于当地物价及预算需要确定是否前去。
  • (国际)语言,英语,日语…语言是我们交流的工具,也是我们重要的考虑因素。

人物,其次是人物,有同样目的地的人有时更容易达成目标。

  • 人数,总共多少人,关系着定机票、门票、酒店、包车大小。
  • 关系,人物关系,房间是安排亲子房还是其它,以及本次旅行的尴尬度。
  • 年龄,年龄大小,决定了本次游玩的项目危险程度,疲劳程度,或者安排分支行程。

时间,最后确定时间,时间是开始也是结尾。

  • 旅行天数,旅行时间一定要安排好,不能匆忙,一般5天以上为最佳。
  • 出发日期,确定出发日期便于定机票,酒店及其它日程安排。根据当地天气及人物假期恰当安排。

三者都确定好之后,我们需要根据这些情况做一个大概的预算。然后着手我们的旅行了。

提前准备

哈,旅行之前我们是需要准备好一些东西的。等一切准备就绪,带着人和行李就可以了。

机票,提前定好机票。国际旅行一定确定是否转机,行李是否直挂。

酒店,为了不每天的奔波及找酒店。有一个好的行程安排是轻松旅行的关键。酒店最好定在景点的周边,这样节省时间体力。

APP安装,旅行(飞猪,携程,马蜂窝),地图(谷歌,高德),民宿(airbnb,途家),出行(滴滴,Uber,grab),翻译(谷歌,百度),

(国际)资料,护照复印件,旅行线路图,打印酒店订单。

(国际)电话,当地大使馆紧急联络方式(地址,电话,邮件等)。当地紧急电话(报警,救护车,火警等)。

(国际)货币,是到当地兑换还是提前国内兑换(参考汇率)。一般可以先在国内兑换美元,再到当地用美元兑现当地货币。

所有东西都准备好了,就等着人带着你的行李出发啦。

物料准备

当我们的资料准备好了之后,机票、酒店均已定好。那么现在只却人过去了,当然人也得带着自己的随身行李了。

必备证件,切记一定把自己的证件带全,最好放在一个专用的包里。

  • 身份证
  • 学生证
  • 驾驶证
  • 银行卡
  • (国际)护照
  • (国际)信用卡
  • (国际)驾驶证

日常衣物,请根据当地气候天气携带日常衣物。

常用物品:

  • 墨镜
  • 内衣裤(一次性)
  • 拖鞋(一次性)

春夏:

  • T恤
  • 短裤
  • 防嗮衫
  • 牛仔裤
  • 泳装
  • 袜子(船袜,短袜)
  • 帽子(草帽,鸭舌帽)
  • 连衣裙
  • 吊带
  • 鞋(运动鞋,小白鞋等)

秋冬:

  • 羽绒服
  • 毛衣
  • 秋衣秋裤
  • 围巾
  • 手套
  • 雪地靴
  • 帽子
  • 口罩
  • 长袜

洗漱用品,最好携带洗漱用品,很多酒店民宿都不提供。

  • 牙刷牙膏
  • 一次性毛巾
  • 洗发水、护发素、沐浴露
  • 剃须刀

护肤用品,现在皮肤娇贵得很,一定记得携带护肤用品。

  • 水、乳、霜、精华
  • 面膜
  • 防嗮霜(喷雾)
  • 洗面奶、卸妆油
  • 化妆品等

备用药品,出门在外,总有磕磕碰碰,随身携带些药品备用。

  • 创口贴
  • 感冒药
  • 消炎药
  • 晕船药
  • 藿香正气胶囊
  • 驱蚊药

设备器材,携带什么样的设备代表你旅行的目的,嘿嘿。

  • 手机(&防水套)
  • 相机(&三脚架)
  • Kindle
  • iPad
  • 雨伞
  • 充电宝
  • 充电器(手机,相机,电脑等)
  • 电脑
  • 对讲机
  • (国际)电源转换插头(欧标、美标等)
  • (国际)变压器,国内是220v,不过充电器一般都是有适配范围。

(国际)网络通讯,一定有随时能通信的方式。

  • (国际)手机漫游,开通手机漫游业务。
  • (国际)购买当地电话卡,这将是一个非常方便的做法。
  • (国际)WiFi 租赁,考虑方便性,可以租赁个。

行程安排

提前计划好旅行的行程,拒绝盲目。一般建议定好前两天的行程,然后根据状态选择后面的行程。

如:

  • 2-10,双流T1-樟宜T2 23:20-04:10
  • 2-11,樟宜T2-伍拉赖I 06:55-09:35
    • 库塔海滩馨乐庭酒店
  • 2-12,佩尼达深度游
    • 库塔海滩馨乐庭酒店
  • 2-13,……
  • 2-14,……
  • 2-15,伍拉赖l-樟宜 13:00-15:40
    • 樟宜T2-双流T1 17:54-22:20

bali-time

本文链接:https://deepzz.com/post/our-travel-template.html参与评论 »

]]>
Sun, 03 Mar 2019 15:09:00 +0800
Go 测试,go test 工具的具体指令 flag https://deepzz.com/post/the-command-flag-of-go-test.html https://deepzz.com/post/the-command-flag-of-go-test.html#comments https://deepzz.com/post/the-command-flag-of-go-test.html 接上篇文章 Go 单元测试,基准测试,http 测试。本篇文章介绍 Go 测试工具 go test,包括各种子命令、参数之类的内容。你可以通过 go test -h 查看帮助信息。

其基本形式是:

go test [build/test flags] [packages] [build/test flags & test binary flags]

执行 go test 命令,它会在 *_test.go 中寻找 test 测试benchmark 基准examples 示例 函数。测试函数必须以 TestXXX 的函数名出现(XXX 为以非小写字母开头),基准函数必须以 BenchmarkXXX 的函数名出现,示例函数必须以 ExampleXXX 的形式。三种函数类似下面的签名形式:

// test 测试函数
func TestXXX(t *testing.T) { ... }

// benchmark 基准函数
func BenchmarkXXX(b *testing.B) { ... }

// examples 示例函数,其相关命名方式可以查看第一篇文章
func ExamplePrintln() {
    Println("The output of\nthis example.")
    // Output: The output of
    // this example.
}
或
func ExamplePerm() {
    for _, value := range Perm(4) {
        fmt.Println(value)
    }

    // Unordered output: 4
    // 2
    // 1
    // 3
    // 0
}

更多请查看 go help testfunc

go test 命令还会忽略 testdata 目录,该目录用来保存测试需要用到的辅助数据。

go test 有两种运行模式:

1、本地目录模式,在没有包参数(例如 go testgo test -v)调用时发生。在此模式下,go test 编译当前目录中找到的包和测试,然后运行测试二进制文件。在这种模式下,caching 是禁用的。在包测试完成后,go test 打印一个概要行,显示测试状态、包名和运行时间。

2、包列表模式,在使用显示包参数调用 go test 时发生(例如 go test mathgo test ./... 甚至是 go test .)。在此模式下,go 测试编译并测试在命令上列出的每个包。如果一个包测试通过,go test 只打印最终的 ok 总结行。如果一个包测试失败,go test 将输出完整的测试输出。如果使用 -bench-v 标志,则 go test 会输出完整的输出,甚至是通过包测试,以显示所请求的基准测试结果或详细日志记录。

下面详细说明下 go test 的具体用法,flag 的作用及一些相关例子。需要说明的是:一些 flag 支持 go test 命令和编译后的二进制测试文件。它们都能识别加 -test. 前缀的 flag,如 go test -test.v,但编译后的二进制文件必须加前缀 ./sum.test -test.bench=.

有以下测试文件 sum.go:

package sum

func Sum(a, b int) int {
    return a + b
}

sum_test.go 内容:

package sum

import (
    "flag"
    "fmt"
    "testing"
    "time"
)

var print bool

func init() {
    flag.BoolVar(&print, "p", false, "print test log")
    flag.Parse()
}

func TestSum(t *testing.T) {
    val := Sum(1, 2)
    if print {
        fmt.Println("sum=", val)
    }
}

// -bench 基准测试
func BenchmarkSum(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Sum(i, i+1)
    }
}

// -timeout 测试
func TestSumLongTime(t *testing.T) {
    time.Sleep(time.Second * 2)
    Sum(1, 2)
}

// 子测试
func TestSumSubTest(t *testing.T) {
    t.Run("1+2", func(t *testing.T) {
        val := Sum(1, 2)
        t.Log("1+2=", val)
    })
    t.Run("2+3", func(t *testing.T) {
        val := Sum(2, 3)
        t.Log("2+3=", val)
    })
}

// 子测试,无具体子测试
func TestSumSubTest2(t *testing.T) {
    val := Sum(2, 3)
    t.Log("no subtest=", val)
}

// 并发测试
func TestSumParallel(t *testing.T) {
    t.Parallel()
    Sum(1, 2)
}

func TestSumParallel2(t *testing.T) {
    t.Parallel()
    Sum(1, 2)
}

test flag

以下 flag 可以跟被 go test 命令使用:

  • -args:传递命令行参数,该标志会将 -args 之后的参数作为命令行参数传递,最好作为最后一个标志。
  $ go test -args -p=true
  • -c:编译测试二进制文件为 [pkg].test,不运行测试。
  $ go test -c && ./sum.test -p=true
  • -exec xprog:使用 xprog 运行测试,行为同 go run 一样,查看 go help run
  • -i:安装与测试相关的包,不运行测试。
  $ go test -i
  • -o file:编译测试二进制文件并指定文件,同时运行测试。
  go test -o filename

test/binary flag

以下标志同时支持测试二进制文件和 go test 命令。

  • -bench regexp:通过正则表达式执行基准测试,默认不执行基准测试。可以使用 -bench .-bench=.执行所有基准测试。
  $ go test -bench=.
  $ go test -c
  $ ./sum.test -test.bench=.
  • -benchtime t:每个基准测试运行足够迭代消耗的时间,time.Duration(如 -benchtime 1h30s),默认 1s。
  $ go test -bench=. -benchtime 0.1s
  $ ./sum.test -test.bench=. -test.benchtime=1s
  • -count n:运行每个测试和基准测试的次数(默认 1),如果 -cpu 指定了,则每个 GOMAXPROCS 值执行 n 次,Examples 总是运行一次。
  $ go test -bench=. -count=2
  $ ./sum.test -test.bench=. -test.count=2
  • -cover:开启覆盖分析,开启覆盖分析可能会在编译或测试失败时,代码行数不对。
  $ go test -bench=. -cover
  • -covermode set,count,atomic:覆盖分析的模式,默认是 set,如果设置 -race,将会变为 atomic。
    • set,bool,这个语句运行吗?
    • count,int,该语句运行多少次?
    • atomic,int,数量,在多线程正确使用,但是耗资源的。
  • -coverpkg pkg1,pkg2,pkg3:指定分析哪个包,默认值只分析被测试的包,包为导入的路径。
  # sum -> $GOPATH/src/test/sum
  $ go test -coverpkg test/sum
  • -cpu 1,2,4:指定测试或基准测试的 GOMAXPROCS 值。默认为 GOMAXPROCS 的当前值。
  • -list regexp:列出与正则表达式匹配的测试、基准测试或 Examples。只列出顶级测试(不列出子测试),不运行测试。
  $ go test -list Sum
  • -parallel n:允许并行执行通过调用 t.Parallel 的测试函数的最大次数。默认值为 GOMAXPROCS 的值。-parallel 仅适用于单个二进制测试文件,但go test命令可以通过指定 -p 并行测试不同的包。查看 go help build
  $ go test -run=TestSumParallel -parallel=2
  • -run regexp:只运行与正则表达式匹配的测试和Examples。我们可以通过 / 来指定测试子函数。go test Foo/A=,会先去匹配并执行 Foo 函数,再查找子函数。
  $ go test -v -run TestSumSubTest/1+
  • -short:缩短长时间运行的测试的测试时间。默认关闭。
  $ go test -short
  • -timeout d:如果二进制测试文件执行时间过长,panic。默认10分钟(10m)。
  $ go test -run TestSumLongTime -timeout 1s
  • -v:详细输出,运行期间所有测试的日志。
  $ go test -v

analyze flag

以下测试适用于 go test 和测试二进制文件:

  • -benchmem:打印用于基准的内存分配统计数据。
  $ go test -bench=. -benchmem
  $ ./sum.test -test.bench -test.benchmem
  • -blockprofile block.out:当所有的测试都完成时,在指定的文件中写入一个 goroutine 阻塞概要文件。指定 -c,将写入测试二进制文件。
  $ go test -v -cpuprofile=prof.out
  $ go tool pprof prof.out
  • -blockprofilerate n:goroutine 阻塞时候打点的纳秒数。默认不设置就相当于 -test.blockprofilerate=1,每一纳秒都打点记录一下。
  • -coverprofile cover.out:在所有测试通过后,将覆盖概要文件写到文件中。设置过 -cover。
  • -cpuprofile cpu.out:在退出之前,将一个 CPU 概要文件写入指定的文件。
  • -memprofile mem.out:在所有测试通过后,将内存概要文件写到文件中。
  • -memprofilerate n:开启更精确的内存配置。如果为 1,将会记录所有内存分配到 profile。
  $ go test -memprofile mem.out -memprofilerate 1
  $ go tool pprof mem.out
  • -mutexprofile mutex.out:当所有的测试都完成时,在指定的文件中写入一个互斥锁争用概要文件。指定 -c,将写入测试二进制文件。
  • -mutexprofilefraction n:样本 1 在 n 个堆栈中,goroutines 持有 a,争用互斥锁。
  • -outputdir directory:在指定的目录中放置输出文件,默认情况下,go test 正在运行的目录。
  • -trace trace.out:在退出之前,将执行跟踪写入指定文件。

本文链接:https://deepzz.com/post/the-command-flag-of-go-test.html参与评论 »

]]>
Sun, 20 May 2018 13:53:00 +0800
Go 单元测试,基准测试,http 测试 https://deepzz.com/post/study-golang-test.html https://deepzz.com/post/study-golang-test.html#comments https://deepzz.com/post/study-golang-test.html 对我们程序员来说,如何提高代码质量一定是我们的重中之重。不仅需要你能够写得一手的业务代码,还需要做的是如何保证你的代码质量。测试用例便是一个非常好的用来提高我们代码质量的工具。

通过测试,我们能够及时的发现我们程序的设计逻辑错误,并能够给接手项目的其它程序员同学理解函数有帮助。

本篇文章主要介绍 Go 语言中的 testing 包。它要求我们以 *_test.go 新建文件,并在文件中以 TestXxx 命名函数。然后再通过 go test [flags] [packages] 执行函数。

$ ls
db.go
db_test.go

$ cat db_test.go
package db

import "testing"

func TestGetUser(t *testing.T) {
    user, err := GetUser("test@example.com")
    if err != nil {
        t.Fatal(err)
    }
    t.Log(user)
}

它也为我们提供了三种类型的函数:测试函数 T、基准测试函数 B、实例函数 Example。

Test 测试

函数测试,其基本签名是:

func TestName(t *testing.T){
    // ...
}

测试函数的名字必须以 Test 开头,可选的后缀名必须不以小写字母开头,一般跟我们测试的函数名。

类型 testing.T 有以下方法:

// 打印日志。对于测试,会在失败或指定 -test.v 标志时打印。对与基准测试,总是打印,避免因未指定 -test.v 带来的测试不准确
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})


// 标记函数失败,继续执行该函数
func (c *T) Fail()
// 标记函数失败,调用 runtime.Goexit 退出该函数。但继续执行其它函数或基准测试。
func (c *T) FailNow()
// 返回函数是否失败
func (c *T) Failed() bool


// 等同于 t.Log + t.Fail
func (c *T) Error(args ...interface{})
// 等同于 t.Logf + t.Fail
func (c *T) Errorf(format string, args ...interface{})


// 等同于 t.Log + t.FailNow
func (c *T) Fatal(args ...interface{})
// 等同于 t.Logf + t.FailNow
func (c *T) Fatalf(format string, args ...interface{})


// 将调用函数标记标记为测试助手函数。
func (c *T) Helper()

// 返回正在运行的测试或基准测试的名称
func (c *T) Name() string

// 用于表示当前测试只会与其他带有 Parallel 方法的测试并行进行测试。
func (t *T) Parallel()

// 执行名字为 name 的子测试 f,并报告 f 在执行过程中是否失败
// Run 会阻塞到 f 的所有并行测试执行完毕。
func (t *T) Run(name string, f func(t *T)) bool


// 相当于 t.Log + t. SkipNow
func (c *T) Skip(args ...interface{})
// 将测试标记为跳过,并调用 runtime.Goexit 退出该测试。继续执行其它测试或基准测试
func (c *T) SkipNow()
// 相当于 t.Logf + t.SkipNow
func (c *T) Skipf(format string, args ...interface{})
// 报告该测试是否是忽略
func (c *T) Skipped() bool

Benchmark 测试

函数测试,其基本签名是:

func BenchmarkName(b *testing.B){
    // ...
}

测试函数的名字必须以 Benchmark 开头,可选的后缀名必须不以小写字母开头,一般跟我们测试的函数名。

B 类型有一个参数 N,它可以用来只是基准测试的迭代运行的次数。基准测试与测试,基准测试总是会输出日志。

type B struct {
    N int
    // contains filtered or unexported fields
}

基准测试较测试多了些函数:

func (c *B) Log(args ...interface{})
func (c *B) Logf(format string, args ...interface{})
func (c *B) Fail()
func (c *B) FailNow()
func (c *B) Failed() bool
func (c *B) Error(args ...interface{})
func (c *B) Errorf(format string, args ...interface{})
func (c *B) Fatal(args ...interface{})
func (c *B) Fatalf(format string, args ...interface{})
func (c *B) Helper()
func (c *B) Name() string
func (b *B) Run(name string, f func(b *B)) bool
func (c *B) Skip(args ...interface{})
func (c *B) SkipNow()
func (c *B) Skipf(format string, args ...interface{})
func (c *B) Skipped() bool


// 打开当前基准测试的内存统计功能,与使用 -test.benchmem 设置类似,
// 但 ReportAllocs 只影响那些调用了该函数的基准测试。
func (b *B) ReportAllocs()

// 对已经逝去的基准测试时间以及内存分配计数器进行清零。对于正在运行中的计时器,这个方法不会产生任何效果。
func (b *B) ResetTimer()
例:
func BenchmarkBigLen(b *testing.B) {
    big := NewBig()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        big.Len()
    }
}

// 以并行的方式执行给定的基准测试。RunParallel 会创建出多个 goroutine,并将 b.N 个迭代分配给这些 goroutine 执行,
// 其中 goroutine 数量的默认值为 GOMAXPROCS。用户如果想要增加非CPU受限(non-CPU-bound)基准测试的并行性,
// 那么可以在 RunParallel 之前调用 SetParallelism。RunParallel 通常会与 -cpu 标志一同使用。
// body 函数将在每个 goroutine 中执行,这个函数需要设置所有 goroutine 本地的状态,
// 并迭代直到 pb.Next 返回 false 值为止。因为 StartTimer、StopTimer 和 ResetTimer 这三个函数都带有全局作用,所以 body函数不应该调用这些函数;
// 除此之外,body 函数也不应该调用 Run 函数。
func (b *B) RunParallel(body func(*PB))
例:
func BenchmarkTemplateParallel(b *testing.B) {
    templ := template.Must(template.New("test").Parse("Hello, {{.}}!"))
    b.RunParallel(func(pb *testing.PB) {
        var buf bytes.Buffer
        for pb.Next() {
            buf.Reset()
            templ.Execute(&buf, "World")
        }
    })
}


// 记录在单个操作中处理的字节数量。 在调用了这个方法之后, 基准测试将会报告 ns/op 以及 MB/s
func (b *B) SetBytes(n int64)

// 将 RunParallel 使用的 goroutine 数量设置为 p*GOMAXPROCS,如果 p 小于 1,那么调用将不产生任何效果。
// CPU受限(CPU-bound)的基准测试通常不需要调用这个方法。
func (b *B) SetParallelism(p int)

// 开始对测试进行计时。
// 这个函数在基准测试开始时会自动被调用,它也可以在调用 StopTimer 之后恢复进行计时。
func (b *B) StartTimer()

// 停止对测试进行计时。
func (b *B) StopTimer()

Example 测试

示例函数可以帮助我们写一个示例,并与输出相比较:

func ExampleHello() {
    fmt.Println("hello")
    // Output: hello
}

func ExampleSalutations() {
    fmt.Println("hello, and")
    fmt.Println("goodbye")
    // Output:
    // hello, and
    // goodbye
}

// 无序输出 Unordered output
func ExamplePerm() {
    for _, value := range Perm(4) {
        fmt.Println(value)
    }
    // Unordered output: 4
    // 2
    // 1
    // 3
    // 0
}

关于示例函数我们需要知道:

  • 函数的签名需要以 Example 开头
  • 输出的对比有有序(Output)和无序(Unordered output)两种
  • 如果函数没有输出注释,将不会被执行

官方给我们的命名的规则是:

// 一个包的 example
func Example() { ... }
// 一个函数 F 的 example
func ExampleF() { ... }
// 一个类型 T 的 example
func ExampleT() { ... }
// 一个类型 T 的方法 M 的 example
func ExampleT_M() { ... }

// 如果以上四种类型需要提供多个示例,可以通过添加后缀的方式
// 后缀必须小写
func Example_suffix() { ... }
func ExampleF_suffix() { ... }
func ExampleT_suffix() { ... }
func ExampleT_M_suffix() { ... }

子测试

上面我们也说到了 Test 和 Benchmark 的 Run 方法,它用来执行子测试。

func TestFoo(t *testing.T) {
    // <setup code>
    t.Run("A=1", func(t *testing.T) { ... })
    t.Run("A=2", func(t *testing.T) { ... })
    t.Run("B=1", func(t *testing.T) { ... })
    // <tear-down code>
}

每个子测试可以用一个唯一的名字表示:顶级测试的名称和传递给 Run 的名称序的组合,用 / 分隔。

go test -run ''      # 运行所有测试
go test -run Foo     # 匹配 Foo 相关的顶级测试,如 TestFooBar
go test -run Foo/A=  # 匹配 Foo 相关的顶级测试, 并匹配子测试 A=
go test -run /A=1    # 匹配所有顶级测试, 并匹配它们的子测试 A=1

子测试也可以用来控制并行性。父级测试只有在完成所有子测试后才能完成。在这个例子中,所有的测试都是相互平行的,并且只与对方一起运行,而不管可能定义的其它顶级测试:

func TestGroupedParallel(t *testing.T) {
    for _, tc := range tests {
        tc := tc // capture range variable
        t.Run(tc.Name, func(t *testing.T) {
            t.Parallel()
            ...
        })
    }
}

运行直到并行子测试完成才会返回,这提供了一种在一组并行测试后进行清理的方法:

func TestTeardownParallel(t *testing.T) {
    // This Run will not return until the parallel tests finish.
    t.Run("group", func(t *testing.T) {
        t.Run("Test1", parallelTest1)
        t.Run("Test2", parallelTest2)
        t.Run("Test3", parallelTest3)
    })
    // <tear-down code>
}

Main 测试

有时候我们也需要从主函数开始进行测试:

func TestMain(m *testing.M)

例:
func TestMain(m *testing.M) {
    // call flag.Parse() here if TestMain uses flags
    os.Exit(m.Run())
}

HTTP 测试

Go 语言目前的 web 开发是比较多的,那么在我们对功能函数有了测试之后,HTTP 的测试又该怎样做呢?

Go 的标准库为我们提供了一个 httptest 的库,通过它就能够轻松的完成 HTTP 的测试。

1、测试 Handle 函数

package main

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

var HandleHelloWorld = func(w http.ResponseWriter, r *http.Request) {
    io.WriteString(w, "<html><body>Hello World!</body></html>")
}

func main() {
    req := httptest.NewRequest("GET", "http://example.com/foo", nil)
    w := httptest.NewRecorder()
    HandleHelloWorld(w, req)

    resp := w.Result()
    body, _ := ioutil.ReadAll(resp.Body)

    fmt.Println(resp.StatusCode)
    fmt.Println(resp.Header.Get("Content-Type"))
    fmt.Println(string(body))
}

2、TLS 服务器?

package main

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

func main() {
    ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Hello, client")
    }))
    defer ts.Close()

    client := ts.Client()
    res, err := client.Get(ts.URL)
    if err != nil {
        log.Fatal(err)
    }

    greeting, err := ioutil.ReadAll(res.Body)
    res.Body.Close()
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("%s", greeting)
}

3、常用的 HTTP 框架又如何测试?

// Package main provides ...
package main

import (
    "fmt"
    "net/http"
    "net/http/httptest"

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

func main() {
    engine := gin.Default()
    engine.GET("/hello", func(c *gin.Context) { c.String(http.StatusOK, "Hello") })
    engine.GET("/world", func(c *gin.Context) { c.String(http.StatusOK, "world") })

    req := httptest.NewRequest(http.MethodGet, "/hello", nil)
    w := httptest.NewRecorder()

    engine.ServeHTTP(w, req)

    fmt.Println(w.Body.String())
}

本文链接:https://deepzz.com/post/study-golang-test.html参与评论 »

]]>
Wed, 09 May 2018 23:31:00 +0800
10 分钟理解什么是 OAuth 2.0 协议 https://deepzz.com/post/what-is-oauth2-protocol.html https://deepzz.com/post/what-is-oauth2-protocol.html#comments https://deepzz.com/post/what-is-oauth2-protocol.html 什么是 OAuth 2.0

OAuth 2.0 是一个行业的标准授权协议。OAuth 2.0 专注于简化客户端开发人员,同时为 Web 应用程序,桌面应用程序,手机和客厅设备提供特定的授权流程。

它的最终目的是为第三方应用颁发一个有时效性的令牌 token。使得第三方应用能够通过该令牌获取相关的资源。常见的场景就是:第三方登录。当你想要登录某个论坛,但没有账号,而这个论坛接入了如 QQ、Facebook 等登录功能,在你使用 QQ 登录的过程中就使用的 OAuth 2.0 协议。

如果你想了解更多,其官方网址为:https://oauth.net/2/。下面我们来了解下 OAuth 协议的基本原理

角色

首先需要介绍的是 OAuth 2.0 协议中的一些角色,整个授权协议的流程都将围绕着这些角色:

  • resource owner,资源所有者,能够允许访问受保护资源的实体。如果是个人,被称为 end-user。
  • resource server,资源服务器,托管受保护资源的服务器。
  • client,客户端,使用资源所有者的授权代表资源所有者发起对受保护资源的请求的应用程序。如:web网站,移动应用等。
  • authorization server,授权服务器,能够向客户端颁发令牌。
  • user-agent,用户代理,帮助资源所有者与客户端沟通的工具,一般为 web 浏览器,移动 APP 等。

可能有些朋友依然不太理解,这里举例说明:假如我想要在 coding.net 这个网站上用 github.com 的账号登录。那么 coding 相对于 github 就是一个客户端。而我们用什么操作的呢?浏览器,这就是一个用户代理。当从 github 的授权服务器获得 token 后,coding 是需要请求 github 账号信息的,从哪请求?从 github 的资源服务器。

协议流程

oauth2-roles

上图详细的描述了这四个角色之间的步骤流程:

  • (A) Client 请求 Resource Owner 的授权。授权请求可以直接向 Resource Owner 请求,也可以通过 Authorization Server 间接的进行。
  • (B) Client 获得授权许可。
  • © Client 向 Authorization Server 请求访问令牌。
  • (D) Authorization Server 验证授权许可,如果有效则颁发访问令牌。
  • (E) Client 通过访问令牌从 Resource Server 请求受保护资源。
  • (F) Resource Server 验证访问令牌,有效则响应请求。
     +--------+                               +---------------+
     |        |--(A)- Authorization Request ->|   Resource    |
     |        |                               |     Owner     |
     |        |<-(B)-- Authorization Grant ---|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(C)-- Authorization Grant -->| Authorization |
     | Client |                               |     Server    |
     |        |<-(D)----- Access Token -------|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(E)----- Access Token ------>|    Resource   |
     |        |                               |     Server    |
     |        |<-(F)--- Protected Resource ---|               |
     +--------+                               +---------------+

                     Figure 1: Abstract Protocol Flow

授权

一个客户端想要获得授权,就需要先到服务商那注册你的应用。一般需要你提供下面这些信息:

  • 应用名称
  • 应用网站
  • 重定向 URI 或回调 URL(redirect_uri)

重定向 URI 是服务商在用户授权(或拒绝)应用程序之后重定向用户的地址,因此也是用于处理授权代码或访问令牌的应用程序的一部分。在你注册成功之后,你会从服务商那获取到你的应用相关的信息:

  • 客户端标识 client_id
  • 客户端密钥 client_secret

client_id 用来表识客户端(公开),client_secret 用来验证客户端身份(保密)。

授权类型

OAuth 2.0 列举了四种授权类型,分别用于不同的场景:

  • Authorization Code(授权码 code):服务器与客户端配合使用。
  • Implicit(隐式 token):用于移动应用程序或 Web 应用程序(在用户设备上运行的应用程序)。
  • Resource Owner Password Credentials(资源所有者密码凭证 password):资源所有者和客户端之间具有高度信任时(例如,客户端是设备的操作系统的一部分,或者是一个高度特权应用程序),以及当其他授权许可类型(例如授权码)不可用时被使用。
  • Client Credentials(客户端证书 client_credentials):当客户端代表自己表演(客户端也是资源所有者)或者基于与授权服务器事先商定的授权请求对受保护资源的访问权限时,客户端凭据被用作为授权许可。

下面来具体说说这四种授权。注意重定向一定要用 302。

授权码模式

该方式需要资源服务器的参与,应用场景大概是:

  1. 资源拥有者(用户)需要登录客户端(APP),他选择了第三方登录。
  2. 客户端(APP)重定向到第三方授权服务器。此时客户端携带了客户端标识(client_id),那么第三方就知道这是哪个客户端,资源拥有者确定(拒绝)授权后需要重定向到哪里。
  3. 用户确认授权,客户端(APP)被重定向到注册时给定的 URI,并携带了第三方给定的 code。
  4. 在重定向的过程中,客户端拿到 code 与 client_idclient_secret 去授权服务器请求令牌,如果成功,直接请求资源服务器获取资源,整个过程,用户代理是不会拿到令牌 token 的。
  5. 客户端(APP)拿到令牌 token 后就可以向第三方的资源服务器请求资源了。
     +----------+
     | Resource |
     |   Owner  |
     |          |
     +----------+
          ^
          |
         (B)
     +----|-----+          Client Identifier      +---------------+
     |         -+----(A)-- & Redirection URI ---->|               |
     |  User-   |                                 | Authorization |
     |  Agent  -+----(B)-- User authenticates --->|     Server    |
     |          |                                 |               |
     |         -+----(C)-- Authorization Code ---<|               |
     +-|----|---+                                 +---------------+
       |    |                                         ^      v
      (A)  (C)                                        |      |
       |    |                                         |      |
       ^    v                                         |      |
     +---------+                                      |      |
     |         |>---(D)-- Authorization Code ---------'      |
     |  Client |          & Redirection URI                  |
     |         |                                             |
     |         |<---(E)----- Access Token -------------------'
     +---------+       (w/ Optional Refresh Token)

   Note: The lines illustrating steps (A), (B), and (C) are broken into
   two parts as they pass through the user-agent.

                     Figure 3: Authorization Code Flow

具体说明,这里以 coding 和 github 为例。当我想在 coding 上通过 github 账号登录时:

1、GET 请求 点击登录,重定向到 github 的授权端点:

https://github.com/login/oauth/authorize?
  response_type=code&
  client_id=a5ce5a6c7e8c39567ca0&
  redirect_uri=https://coding.net/api/oauth/github/callback&
  scope=user:email
字段 描述
response_type 必须,固定为 code,表示这是一个授权码请求。
client_id 必须,在 github 注册获得的客户端 ID。
redirect_uri 可选,通过客户端注册的重定向 URI(一般要求且与注册时一致)。
scope 可选,请求资源范围,多个空格隔开。
state 可选(推荐),如果存在,原样返回给客户端。

返回值:

https://coding.net/api/oauth/github/callback?code=fb6a88dc09e843b33f
字段 描述
code 必须。授权码
state 如果出现在请求中,必须包含。

授权错误

第一种,客户端没有被识别或错误的重定向 URI,授权服务器没有必要重定向资源拥有者到重定向URI,而是通知资源拥有者发生了错误。

第二种,客户端被正确地授权了,但是其他某些事情失败了。这种情况下下面地错误响应会被发送到客户端,包括在重定向 URI 中。

https://coding.net/api/oauth/github/callback?
  error=redirect_uri_mismatch&
  error_description=The+redirect_uri+MUST+match+the+registered+callback+URL+for+this+application.&
  error_uri=https%3A%2F%2Fdeveloper.github.com%2Fapps%2Fmanaging-oauth-apps%2Ftroubleshooting-authorization-request-errors%2F%23redirect-uri-mismatch
字段 描述
error 必须,必须是预先定义的错误码: 错误码
error_description 可选,错误描述
error_uri 可选,指向可解读错误的 URI
state 必须,如果出现在授权请求中

2、POST 请求 获取令牌 token,当获取到授权码 code 后,客户端需要用它获取访问令牌:

https://github.com/login/oauth/access_token?
  client_id=a5ce5a6c7e8c39567ca0&
  client_secret=xxxx&
  grant_type=authorization_code&
  code=fb6a88dc09e843b33f&
  redirect_uri=https://coding.net/api/oauth/github/callback

出于安全考虑 client_id 和 client_secret 可以通过 HTTP Basic 认证:Authorization: Basic YTVjZTVhNmM3ZThjMzk1NjdjYTA6eHh4eA==

字段 描述
grant_type 必须,固定为 authorization_code/refresh_token。
code 必须,上一步获取到的授权码。
redirect_uri 必须(如果请求/authorize接口有),完成授权后的回调地址,与注册时一致。
client_id 必须,客户端标识。
client_secret 必须,客户端密钥。

返回值:

{
  "access_token":"a14afef0f66fcffce3e0fcd2e34f6ff4",
  "token_type":"bearer",
  "expires_in":3920,
  "refresh_token":"5d633d136b6d56a41829b73a424803ec"
}
字段 描述
access_token 这个就是最终获取到的令牌。
token_type 令牌类型,常见有 bearer/mac/token(可自定义)。
expires_in 失效时间。
refresh_token 刷新令牌,用来刷新 access_token。

3、获取资源服务器资源,拿着 access_token 就可以获取账号的相关信息了:

curl -H "Authorization: token a14afef0f66fcffce3e0fcd2e34f6ff4" https://api.github.com/user

4、POST 请求 刷新令牌

我们的 access_token 是有时效性的,当在获取 github 用户信息时,如果返回 token 过期:

https://github.com/login/oauth/access_token?
  client_id=a5ce5a6c7e8c39567ca0&
  client_secret=xxxx&
  redirect_uri=https://coding.net/api/oauth/github/callback&
  grant_type=refresh_token&
  refresh_token=5d633d136b6d56a41829b73a424803ec
字段 描述
redirect_uri 必须
grant_type 必须,固定为 refresh_token
refresh_token 必须,上面获取到的 refresh_token

返回值:

{
  "access_token":"a14afef0f66fcffce3e0fcd2e34f6ee4",
  "token_type":"bearer",
  "expires_in":3920,
  "refresh_token":"4a633d136b6d56a41829b73a424803vd"
}

refresh_token 只有在 access_token 过期时才能使用,并且只能使用一次。当换取到的 access_token 再次过期时,使用新的 refresh_token 来换取 access_token

  +--------+                                           +---------------+
  |        |--(A)------- Authorization Grant --------->|               |
  |        |                                           |               |
  |        |<-(B)----------- Access Token -------------|               |
  |        |               & Refresh Token             |               |
  |        |                                           |               |
  |        |                            +----------+   |               |
  |        |--(C)---- Access Token ---->|          |   |               |
  |        |                            |          |   |               |
  |        |<-(D)- Protected Resource --| Resource |   | Authorization |
  | Client |                            |  Server  |   |     Server    |
  |        |--(E)---- Access Token ---->|          |   |               |
  |        |                            |          |   |               |
  |        |<-(F)- Invalid Token Error -|          |   |               |
  |        |                            +----------+   |               |
  |        |                                           |               |
  |        |--(G)----------- Refresh Token ----------->|               |
  |        |                                           |               |
  |        |<-(H)----------- Access Token -------------|               |
  +--------+           & Optional Refresh Token        +---------------+

               Figure 2: Refreshing an Expired Access Token

[旧版] 隐式模式

该方式一般用于移动客户端或网页客户端。隐式授权类似于授权码授权,但 token 被返回给用户代理再转发到客户端(APP),因此它可能会暴露给用户和用户设备上的其它客户端(APP)。此外,此流程不会对客户端(APP)的身份进行身份验证,并且依赖重定向 URI(已在服务商中注册)来实现此目的。

基本原理:要求用户授权应用程序,然后授权服务器将访问令牌传回给用户代理,用户代理将其传递给客户端。

     +----------+
     | Resource |
     |  Owner   |
     |          |
     +----------+
          ^
          |
         (B)
     +----|-----+          Client Identifier     +---------------+
     |         -+----(A)-- & Redirection URI --->|               |
     |  User-   |                                | Authorization |
     |  Agent  -|----(B)-- User authenticates -->|     Server    |
     |          |                                |               |
     |          |<---(C)--- Redirection URI ----<|               |
     |          |          with Access Token     +---------------+
     |          |            in Fragment
     |          |                                +---------------+
     |          |----(D)--- Redirection URI ---->|   Web-Hosted  |
     |          |          without Fragment      |     Client    |
     |          |                                |    Resource   |
     |     (F)  |<---(E)------- Script ---------<|               |
     |          |                                +---------------+
     +-|--------+
       |    |
      (A)  (G) Access Token
       |    |
       ^    v
     +---------+
     |         |
     |  Client |
     |         |
     +---------+

   Note: The lines illustrating steps (A) and (B) are broken into two
   parts as they pass through the user-agent.

                       Figure 4: Implicit Grant Flow

1、同样以 coding 和 github 为例:

https://github.com/login/oauth/authorize?
  response_type=token&
  client_id=a5ce5a6c7e8c39567ca0&
  redirect_uri=https://coding.net/api/oauth/github/callback&
  scope=user:email
字段 描述
response_type 必须,固定为 token。
client_id 必须。当客户端被注册时,有授权服务器分配的客户端标识。
redirect_uri 可选。由客户端注册的重定向URI。
scope 可选。请求可能的作用域。
state 可选(推荐)。任何需要被传递到客户端请求的URI客户端的状态。

返回值:

https://coding.net/api/oauth/github/callback#
  access_token=a14afef0f66fcffce3e0fcd2e34f6ff4&
  token_type=token&
  expires_in=3600
字段 描述
access_token 必须。授权服务器分配的访问令牌。
token_type 必须。令牌类型。
expires_in 推荐,访问令牌过期的秒数。
scope 可选,访问令牌的作用域。
state 必须,如果出现在授权请求期间,和请求中的 state 参数一样。

授权错误和上面一样

2、用户代理提取令牌 token 并提交给 coding

3、coding 拿到 token 就可以获取用户信息了

curl -H "Authorization: token a14afef0f66fcffce3e0fcd2e34f6ff4" https://api.github.com/user

[旧版] 资源所有者密码模式

用户将其服务凭证(用户名和密码)直接提供给客户端,该客户端使用凭据从服务获取访问令牌。如果其它方式不可行,则只应在授权服务器上启用该授权类型。此外,只有在客户端受到用户信任时才能使用它(例如,它由服务商自有,或用户的桌面操作系统)。

     +----------+
     | Resource |
     |  Owner   |
     |          |
     +----------+
          v
          |    Resource Owner
         (A) Password Credentials
          |
          v
     +---------+                                  +---------------+
     |         |>--(B)---- Resource Owner ------->|               |
     |         |         Password Credentials     | Authorization |
     | Client  |                                  |     Server    |
     |         |<--(C)---- Access Token ---------<|               |
     |         |    (w/ Optional Refresh Token)   |               |
     +---------+                                  +---------------+

            Figure 5: Resource Owner Password Credentials Flow

POST 请求 密码凭证流程

https://oauth.example.com/token?grant_type=password&username=USERNAME&password=PASSWORD&client_id=CLIENT_ID
字段 描述
grant_type 必须,固定为 password。
username 必须,UTF-8 编码的资源拥有者用户名。
password 必须,UTF-8 编码的资源拥有者密码。
scope 可选,授权范围。

返回值:

{ 
  "access_token"  : "...",
  "token_type"    : "...",
  "expires_in"    : "...",
  "refresh_token" : "...",
}

如果授权服务器验证成功,那么将直接返回令牌 token,改客户端已被授权。

客户端模式

这种模式只需要提供 client_idclient_secret 即可获取授权。一般用于后端 API 的相关操作。

     +---------+                                  +---------------+
     |         |                                  |               |
     |         |>--(A)- Client Authentication --->| Authorization |
     | Client  |                                  |     Server    |
     |         |<--(B)---- Access Token ---------<|               |
     |         |                                  |               |
     +---------+                                  +---------------+

                     Figure 6: Client Credentials Flow

POST 请求 客户端凭证流程:

https://oauth.example.com/token?grant_type=client_credentials&client_id=CLIENT_ID&client_secret=CLIENT_SECRET
字段 描述
grant_type 必须。必须设置到客户端证书中。
scope 可选。授权的作用域。

返回值

{ 
  "access_token"  : "...",
  "token_type"    : "...",
  "expires_in"    : "...",
}

如果授权服务器验证成功,那么将直接返回令牌 token,改客户端已被授权。

参考网站

[1] https://developers.douban.com/wiki/?title=oauth2

[2] https://www.digitalocean.com/community/tutorials/an-introduction-to-oauth-2

[3] https://tools.ietf.org/html/rfc6749

[4] https://oauth.net/2/

本文链接:https://deepzz.com/post/what-is-oauth2-protocol.html参与评论 »

]]>
Thu, 08 Mar 2018 14:15:00 +0800
DNS 一站到家之 CAA 记录 https://deepzz.com/post/what-is-caa-record-in-dns.html https://deepzz.com/post/what-is-caa-record-in-dns.html#comments https://deepzz.com/post/what-is-caa-record-in-dns.html 自 2017.04 CAB 论坛通过投票决定:2017.09.08 起,所有 CA 机构颁发 SSL 证书前必须对颁发的域名进行 CAA 检测。CAA 就时不时出现在技术人员的眼前。那么 CAA 记录是什么?又有什么作用呢?如何添加 CAA 记录?

什么是 CAA

CAA(Certification Authority Authorization)是一种 DNS 记录,它被定义在 RFC6844,其目的是用来指定域名允许哪个证书颁发机构(CA)为其颁发证书。防止钓鱼攻击者使用该域名申请 SSL 证书。

它们还提供了一种方法来指示通知规则,以防有人从未经授权的 CA 颁发证书。在没有 CAA 记录的情况下,所有 CA 均可为该域名颁发证书。当然如果存在 CAA 记录,CA 必须遵守规则,只能是在记录列表中的 CA 才被允许。

CAA记录可以为整个域或特定主机名设置策略。CAA 记录也被子域继承,因此 CAA 记录集 example.com 也将适用于任何子域,例如 subdomain.example.com(除非被覆盖)。CAA 记录可以控制发行单域名证书,通配符证书或同时。

CAA 记录格式

CAA 记录由以下元素组成:

标签 描述
flag 0-255 之间的无符号整数
tag 用来表示关键标志,RFC 有定义
value 与 tag 关联的值

CAA 记录的规范的表示法是:

CAA <flags> <tag> <value>

RFC 目前定义了 3 个可用的 tag:

  • issue:明确授权单个证书颁发机构颁发主机名的证书(任何类型)。
  • issuewild:明确授权单个证书颁发机构为主机名颁发通配符证书(只有通配符)。
  • iodef:指定认证机构可以向其报告策略违规的URL或邮箱。

我们先来尝试看看 CAA 到底是什么样的:

$ dig dnsimple.com caa 

; <<>> DiG 9.11.2 <<>> dnsimple.com caa
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 26969
;; flags: qr rd ra; QUERY: 1, ANSWER: 5, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;dnsimple.com.          IN  CAA

;; ANSWER SECTION:
dnsimple.com.       3600    IN  CAA 0 issue "amazonaws.com"
dnsimple.com.       3600    IN  CAA 0 issue "comodoca.com"
dnsimple.com.       3600    IN  CAA 0 iodef "mailto:ops@dnsimple.com"
dnsimple.com.       3600    IN  CAA 0 issue "letsencrypt.org"
dnsimple.com.       3600    IN  CAA 0 issuewild "comodoca.com"

;; Query time: 176 msec
;; SERVER: 223.5.5.5#53(223.5.5.5)
;; WHEN: Sun Feb 04 13:18:35 CST 2018
;; MSG SIZE  rcvd: 204

请注意这里的 dig 的版本,较低的版本是不支持 CAA 记录的查询的,请悉知。另外,dig 是不支持继承的查询方式。

查询规则

通过查看 CAB 论坛的帖子来看,他们清晰的定义了 CAA 记录的查询规则,具体如下:

名称 含义
CAA(X) 查询标签 X 返回的结果。
P(X) 是 DNS 层次结构中位于 X 之上的 DNS 标签。
A(X) 在标签 X 处指定的 CNAME 或 DNAME 记录的目标。
  • 如果 CAA(X) 不为空,则 R(X) = CAA(X),否则
  • 如果 A(X) 不为空,并且 CAA( A(X) ) 不为空,那么 R(X) = CAA( A(X) ),否则
  • 如果 X 不是顶级域名,那么 R(X) = R( P(X) ),否则
  • R(X) 为空

因此,当节点处的搜索返回 CNAME 记录时,CA 将沿着 CNAME 记录链到达其目标。如果目标标签含 CAA 记录,则返回。否则,CA 继续在节点 X 的父节点处进行搜索。

注意,搜索不包括 CNAME 记录的目标的父级(除非 CNAME 指向自己的路径)。

为了防止资源耗尽攻击,CA 应该限制被接受的 CNAME 链的长度。但 CA 必须处理包含 8 个或更少的 CNAME 记录的 CNAME 链。

这里举例说明。例如,如果我们想要限制颁发 example.com 给 Let’s Encrypt 证书颁发机构的 SSL 证书,我们应该添加以下 CAA 记录:

example.com.  CAA 0 issue "letsencrypt.org"

如果我们想允许 Let’s Encrypt 和 Comodo,我们应该添加 2 个 CAA 记录,每个 CA 记录一个:

example.com.  CAA 0 issue "comodoca.com"
example.com.  CAA 0 issue "letsencrypt.org"

如果我们想让 Let’s Encrypt 并且 Comodo 只用于通配符,那么我们可以使用 issuewild

example.com.  CAA 0 issue "letsencrypt.org"
example.com.  CAA 0 issuewild "comodoca.com"

请注意,issuewild 的存在将覆盖 issue。因此,Let’s Encrypt 不允许发出通配符证书(不管他们不支持这种类型的证书)。

最后,要获得违反政策的通知,您可以添加一个带有 iodef 标记的记录,其中包含要通知的电子邮件地址:

example.com.  CAA 0 iodef "mailto:example@example.com"

如前所述,记录是由子主机名继承的。我们来看一个子域配置的例子:

example.com.        CAA 0 issue "letsencrypt.org"
alpha.example.com.  CAA 0 issue "comodoca.com"
beta.example.com.   CAA 0 issue "letsencrypt.org"
beta.example.com.   CAA 0 issue "comodoca.com"

在上面的示例中,Let’s Encrypt 是 example.com 域的默认 CA。但是,只有 Comodo 才能颁发证书 alpha.example.com。Comodo 和 Let’s Encrypt 都可以颁发证书 beta.example.com。那么 foo.example.com 呢?因为没有记录存在 foo.example.com,但有一个记录 example.com,在这种情况下,只有 Let’s Encrypt 将被允许发出 foo.example.com

相关工具

  1. myssl:如果你想查看某个域名是否添加 CAA 记录,你可以到这里检测(实现链式的查询)。
  2. sslmate:CAA 记录自助生成,如果你不知道 CAA 记录是什么样,你可以在这里生成,然后添加到你的 DNS 上。

FAQ

1、为什么我添加的 CAA 记录没有生效?
检查域名是否添加 CNAME。按照 RFC 文档来说,CNAME 是不能与其它任何记录同时存在的,但有些 DNS 服务商自己实现的 DNS 服务器并没有遵守这项规则,导致一些记录不生效。

2、有哪些支持 CAA 记录的 DNS 服务商?
到 2018 年,已经有非常多的 DNS 服务商支持 CAA 记录:

本文链接:https://deepzz.com/post/what-is-caa-record-in-dns.html参与评论 »

]]>
Sun, 04 Feb 2018 13:58:00 +0800
docker volume 容器卷的那些事(二) https://deepzz.com/post/the-docker-volumes-permissions.html https://deepzz.com/post/the-docker-volumes-permissions.html#comments https://deepzz.com/post/the-docker-volumes-permissions.html 如果你读了docker volume 容器卷的那些事(一),我想应该不会遇到下面这些问题的,毕竟是具有指导意义的。本篇文章的内容依旧是有关 volume 的内容,主要讲诉的是如何解决非 root 用户下的文件映射问题。博主将自己常遇到的一些问题总结如下。

事情要从博主使用 prometheus 说起。当时博主使用的执行脚本类似下面这种:

$ docker run --rm \
    --name prometheus \
    -p 9090:9090 \
    -v "$(pwd)"/data:/prometheus \
    prom/prometheus:v2.0.0

应该是在其版本 2.0.0 之前,博主使用 prometheus 一切正常。突然有一天冒出这样的错误:

level=info ts=2017-12-22T12:40:09.154479277Z caller=main.go:314 msg="Starting TSDB"
level=error ts=2017-12-22T12:40:09.154587496Z caller=main.go:323 msg="Opening storage failed" err="open DB in /prometheus: open /prometheus/872424405: permission denied"

什么情况!发生了什么?没有权限?明明没有该执行脚本,不应该的啊。这才想起来咱刚刚更新过 prometheus 镜像的版本(该版本优化很大,故及时跟进)。没办法,看看它的 Dockerfile 更新了什么 #Use user nobody in Dockerfile。在 Dockerfile 中明显的看到:

USER       nobody

从以前的 root 用户切换到了 nobody 用户(为了安全考虑)。

而我们映射的目录:

drwxr-sr-x    2 root     root            40 Dec  5 02:41 data/

看到我们的 data 目录的拥有者依然是 root 用户,权限的问题必然出现了。

那么,如果你依然固执的要这样做(不使用命名容器卷)。这里提供了几种解决的办法,供参考。

在某些情况下,即使使用下面方法也不能达到效果,可能你需要尝试关闭 SELinux:setenforce 0(临时关闭)

更改目录拥有者

是的,非常容易的想到,既然这个映射出来的文件夹所有者不是 nobody,我给它改成 nobody 不就可以了吗?

首先,我们找到 nobody 用户的 id:

# 找到它的原始镜像执行命令。
$ docker run --rm quay.io/prometheus/busybox cat /etc/passwd

...
nobody:x:65534:65534:nobody:/home:/bin/false

发现,其 id 为 65534(其实这些用户uid是约定的),执行如下命令:

$ sudo chown -R 65534 data

$ ls -al data
drwxr-sr-x    3 65534   root            60 Dec 22 12:59 data/

可以看到 data 目录的所有者已经改为了 uid 为 65534 的用户。再次执行运行 prometheus 的脚本,成功。

Data Container

是的,你可以使用 Data Container 的方式进行容器卷的共享,这样也能够解决权限的问题。其基本运行方式是:

# 声明一个容器卷 /data,并在 /data 目录下新建 a.txt 文件
$ docker run --name data_container -v /data alpine touch /data/a.txt

# 挂载容器卷,查看 /data 目录下的内容
$ docker run --volumes-from container_name alpine ls /data
a.txt

当执行第二条命令时,你会看到了 a.txt 文件,说明挂载数据容器成功了。

需要说明的是,最好用同一个镜像运行数据容器,这样才能保证两者的 UID 一致,然也会出现权限问题。数据容器应该是执行一条命令就退出。

再把前面 prometheus 的例子拿来实践一下。首先,在 prometheus 的 Dockerfile 中我们看到:

# 声明容器卷
VOLUME     [ "/prometheus" ]
...
# 入口
ENTRYPOINT [ "/bin/prometheus" ]

原来 prom/prometheus 镜像就声明了一个容器卷,那么我们就不必再多次一举了。但我们需要覆盖 ENTRYPOINT 指令。

$ docker run --name data_container --entrypoint="" prom/prometheus:v2.0.0 ls

然后再次执行:

$ docker run --rm \
    --name prometheus \
    -p 9090:9090 \
    --volumes-from data_container \
    prom/prometheus:v2.0.0

成功。

切换用户

有没有更好的方式去实现呢?有的,这种方式较第一种优点是自动化,不需要手动更改文件权限。具体流程是:

  1. 切换为 root 用户。
  2. 更改目录权限到当前非 root 用户。
  3. 用 gosu 以非 root 用户执行命令。

这里需要自行书写 Dockerfile 构建镜像。具体实现类似下面,新建 Dockerfile:

FROM prom/prometheus:v2.0.0
USER root

RUN mkdir -p /usr/local/bin \
  && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.10/gosu-amd64" \
  && chmod +x /usr/local/bin/gosu 
 
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD [ "--config.file=/etc/prometheus/prometheus.yml", \
      "--storage.tsdb.path=/prometheus", \
      "--web.console.libraries=/usr/share/prometheus/console_libraries", \
      "--web.console.templates=/usr/share/prometheus/consoles" ]

其中 entrypoint.sh 的内容如下,它的目的就是将我们的目录的权限改成非 root 用户的权限:

#!/bin/sh

chown -R nobody /prometheus
gosu nobody prometheus "$@"

这里提到了 gosu 工具,用它来替换 sudo 从某种意义上解决了信号传递和 TTY 的问题,更多详情到其项目首页。

然后我们构建镜像,执行最初的运行脚本,成功。我们查看下映射到宿主机上的目录:

$ ls -al data
drwxr-sr-x    3 nobody   root            80 Jan 11 11:09 data

# 进入容器查看进程
$ ps
PID   USER     TIME   COMMAND
    1 root       0:00 {entrypoint.sh} /bin/sh /entrypoint.sh ...
    6 nobody     0:00 prometheus --config.file=/etc/prometheus/prometheus.yml ...

注意,standard_init_linux.go:195: exec user process caused "exec format error" 得到这个错误,可能是你没有指定运行 entrypoint.sh 的 shebang。指定如 #!/bin/sh 即可。

参考文章

[1] https://denibertovic.com/posts/handling-permissions-with-docker-volumes/
[2] https://segmentfault.com/a/1190000004527476

本文链接:https://deepzz.com/post/the-docker-volumes-permissions.html参与评论 »

]]>
Sun, 07 Jan 2018 19:46:00 +0800