0%

很多离职的小伙伴都喜欢吐槽前公司是个方法论公司,大佬喜欢大谈方法论,晋升答辩除了工作内容之外,有一项考核就是影响力价值观输出。这样有什么问题呢?作为曾经的一线研发,我觉得问题在于这些抽象上,概念上,甚至鸡汤般的东西,有时候不能解决基层员工的痛点。方法论是个好东西,是我们工作经验的提炼,总结,淡淡方法论对于怎么高效工作,管理团队有指导意义。但是对于基层员工,他们每天面临着公司最脏,最累,最烦琐的工作,有些人又是讨好型人格不懂拒绝,于是就很容易陷入穷忙的死循环。这时候你跟他们大谈方法论,说要注意工作的轻重缓急,及时汇报,懂得拒绝,不要事情都一个人抗,学会减少重复劳动巴拉巴拉。但是要知道,工作都是上面安排的,他已经是最一线的员工,没人能转手了,这种情况下其实已经分身乏术。过于注重方法论输出的另一个问题就是容易产生骗子,以晋升为例,来自各部门的评委们在几十分钟的答辩内,是否真的能真实了解一个人工作产出?对于那些晋升成功的,和他合作的同事心里真实评价如何?脉脉上吐槽做着低P的工作,像个高P一样高谈阔论这种事,肯定是存在的。看着别人没多少产出就能顺利晋级,那些每天累死累活的人会怎么想,既没有加薪,又没有晋升,干的又不开心,负面能量不断累积,想来想去也只有愤然离职这一条路。最终就是铁打的领导流水的兵,然后领导继续给新员工灌输方法论,新员工受不了后继续跑路。
所以我觉得,方法论也好,鸡汤也罢,这都是管理团队提高效率需要的技能。但是落到具体员工身上上,老板们也需要关注下下面人真正的痛点是什么,应该怎么改善。毕竟鸡汤也只是偶尔补补身子用的,一日三餐才是大多数人面临的人生问题。
当然,如果把底下人当成几年一换的耗材,上面说的就都是废话了。

只要是接触过各种开放平台的开发者,对于OAuth概念肯定不陌生。但是由于OAuth流程比较复杂,对于刚接触的人来说,容易云里雾里。我之前工作上接触OAuth比较多,本文以OAuth2.0的RFC文档为基础,结合自己以前一些工作上的经验,系统地梳理一下OAuth2.0规范。

What is OAuth

关于OAuth的定义,维基百科是这么说的:

OAuth is an open standard for access delegation, commonly used as a way for Internet users to grant websites or applications access to their information on other websites but without giving them the passwords. This mechanism is used by companies such as Amazon, Google, Facebook, Microsoft and Twitter to permit the users to share information about their accounts with third party applications or websites.

O == Open, Auth == Authorization(授权), not Authentication(认证).

首先要明确的是,OAuth是一种授权协议,而非认证协议。通过它,用户可以授权第三方应用访问自己保存在资源服务器器上的资源。当然,如果这些资源是账号信息,第三方服务器也可以基于OAuth实现类似SSO的单点登录,完成登录认证。

OAuth历史

上面这张图基本涵盖了OAuth诞生的相关历史进程。

在2006年,Twitter在开发他们自己的OpenID实现,而当时Ma.gnolia网站需要一个使用OpenID授权访问他们网站资源的方案,双方会面讨论后发现当时并没有一个统一的标准API实现这件事。

上面功能的实现者们于2007年成立了OAuth讨论组,撰写并公布了最早的开放授权(OAuth)草案。这个草案后来得到了Google的关注,最终也一起参与了规范的制定。

在2007年10月,OAuth1.0草案公布。

在2008年11月的IETF第73次会议上,OAuth得到广泛支持,IETF正式为它成立了一个工作组。

2010年,编号为RFC-5849的OAuth1.0 RFC文档发表。

在2012年,OAuth2.0 的RFC-6749, 和Bearer Token 的 RFC-6750相继发表。大多数互联网应用都以此作为授权标准。需要注意的是OAuth2.0与OAuth1.0并不兼容。

虽然IETF的RFC意为征求意见稿(Request for Comment),但是目前它已经是开放授权的事实标准。

本文后续的一些内容,提炼自IETF的RFC文档,并结合我自己工作中的一些经验总结。

一些概念

了解OAuth2.0之前,我们先熟悉几个概念。

角色

OAuth2.0 把整个流程中的参与者分为4种角色:

  1. Resource Owner:资源拥有者,通常是我们网站/应用的用户。
  2. Client secret:与Client id 配对的密钥,格式各家实现不用,保证安全性即可。在进行OAuth授权流程时,Client必须提供Client id与 Client secret。如果Client secret发生泄露,出于安全考虑,Authorization Server一般允许注册方重新生成secret.
  3. Client:客户端,一般指第三方应用程序,即资源使用方。比如豆瓣注册时,需要用户的微信头像做豆瓣头像,此时豆瓣就是Client。
  4. Authorization Server:授权服务器,对Client进行授权时验证客户端,用户合法性的节点。Resource Server 和 Authorization Server可能是同一个(比如资源是账号数据时)也可能不同。

几个术语

首先,Client 想要得到Authorization Server 的授权,需要先注册。比如各种开放平台,需要先由开发者提供网站地址,应用名称,默认重定向地址等信息,才能为其颁发合法的Client id 和 Client Secret 进行OAuth授权。

  1. Client id:是 Client 在Authorization Server注册的标志,格式各家实现不同,但是需要全局唯一。一般注册后不会改变,也有实现方喜欢叫App id。
  2. Client secret:与Client id 配对的密钥,格式各家实现不用,保证完全性即可。在进行OAuth授权流程时,Client必须提供Client id与 Client secret。如果Client secret发生泄露,处于安全考虑,Authorization Server一般允许注册方重新生成secret.
  3. User-Agent:一般指用户浏览器,或者APP。
  4. Access token:是完成授权流程后,Client得到的票据,访问Resource Owner的资源时,需要对其进行验证。认证失败Authorization Server将引导Client重新进行OAuth流程。
  5. Refresh token:类似 AccessToken 的票据,用于刷新Access token(不需要重新走OAuth流程)。Refresh token 是可选项,不一定要实现。

熟悉这些概念后,我们开始介绍OAuth2.0定义的标准授权流程。

OAuth2.0 Flow

以下几种OAuth Flow,摘选自RFC相关文档,详情请参考最后引用链接。

为覆盖各种场景,OAuth2.0划分了4种授权流程:

  1. Authorization Code:授权码模式,因为需要在各个节点往返三次,俗称3 leg。
  2. Implicit:隐式授权,相对于授权码模式做了简化。
  3. Resource Owner Password Credentials:密码认证模式。
  4. Client Credentials:客户端认证模式。

下面详细介绍这几种模式。

Authorization Code Grant

下图描述了一个完整的 Authorization Code 模式授权流程,Client与其他角色的交互通过User-Agent,这里 Client 包含前端和后端服务器。

+----------+
| 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)
  1. 步骤A:用户在通过User-Agent(浏览器)使用Client时,Client需要访问用户Resource Owner的资源,此时发起了OAuth流程。Client携带客户端认证信息(Client id)、请求资源的范围、本地状态,重定向地址等重定向到Authorization Server,用户看到授权确认页面。
  2. 步骤B:用户认证并确认授权信息,Authorization Server判断用户是否合法来进行下一步授权或者返回错误。
  3. 步骤C:如果用户合法且同意授权,Authorization Server使用第一步Client提交的重定向地址重定向浏览器,并携带授权码和之前Client提供的本地状态信息。
  4. 步骤D:Client 使用授权码找Authorization Server交换access token(处于安全性考虑,一般由Client 的服务端发起),为了严格验证,这一步除了携带授权码,还需要前面使用的重定向地址。
  5. 步骤E:Authorization Server 验证Client提交的授权码是否有效,重定向地址是否与步骤C匹配。如果验证通过,将返回access token和refresh token(可选)给Client。

得到 access token后,Client可以在token失效前,访问Resource Server得到已授权的用户资源。OAuth2.0在Client与Resource Server之间,设置了一个授权层(authorization layer),Client 通过得到的授权令牌访问资源,对于资源访问权限、时效在颁发令牌时控制。

流程中几个步骤涉及到的接口:

重定向授权页(步骤A)

请求例子:

GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com

参数说明:

Parameter Description
response_type 表示授权类型,必选项,此处的值固定为”code”
client_id 表示客户端的ID,必选项
redirect_uri 表示重定向URI,可选项。如果不提供,Authorization Server会使用Client注册时的重定向URI进行重定向。
scope 表示申请的权限范围,可选项,多个scope值用空格分开
state 表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。建议使用。

重定向回Client(步骤C)

请求例子:

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

参数说明:

Parameter Description
code 表示授权码,必选项。该码的有效期应该很短,通常设为10分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端ID和重定向URI,是一一对应关系。
state 表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。建议使用。

从 Authorization Server 获取token(步骤D)

请求例子:

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

grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb

参数说明:

Parameter Description
grant_type 表示使用的授权模式,必选项,此处的值固定为”authorization_code”。
code 表示上一步获得的授权码,必选项。
redirect_uri 表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。
client_id Client在Authorization Server注册后得到的client_id,必选项。

Authorization Server 返回token(步骤E)

响应结果例子:

1
2
3
4
5
6
7
8
9
10
11
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter":"example_value"
}

参数说明:

Parameter Description
access_token 表示访问令牌,必选项。
token_type 表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。
expires_in 表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
refresh_token 表示更新令牌,用来获取下一次的访问令牌,可选项。
scope 表示权限范围,如果与客户端申请的范围一致,此项可省略。

Implicit Grant

Implicit 授权的流程如下图,与 Authorization Code 相比,少了返回授权码这一步,Authorization Server直接返回token至Client的前端,Client方面没有后端参与。图中的Web-Hosted Client Resource可以认为是Client的前端资源容器,比如前端服务器,APP等。

+----------+
| 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 |
|         |
+---------+
  1. 步骤A:与 Authorization Code流程类似,Client携带客户端认证信息(Client id 和 Secret)、请求资源的范围、本地状态,重定向地址等重定向到Authorization Server,用户看到授权确认页面。
  2. 步骤B:用户认证并确认授权信息,Authorization Server判断用户是否合法来进行下一步授权或者返回错误。
  3. 步骤C:如果用户合法且同意授权,Authorization Server使用第一步Client提交的重定向地址重定向浏览器,并将token携带在URI Fragment中一并返回。
  4. 步骤D:User-Agent 顺着重定向指示向Web-Hosted Client Resource 发起请求(按RFC2616该请求不包含Fragment)。User-Agent 在本地保留Fragment信息。
  5. 步骤E:Web-Hosted Client Resource 返回一个网页(通常是带有嵌入式脚本的HTML),该网页能够提取URI中的Fragment和其他参数。
  6. 步骤F:在User-Agent中使用上一步提供的脚本提取URL中的token。
  7. 步骤G:User-Agent传送token给Client。

Implicit 比起 Authorization Code 来说,少了Client使用授权码换Token的过程,而是直接把token提供给User-Agent让Client提取。整个流程中使用URL传递token,不需要Client的服务端参与,且没有严格验证Client信息,安全性欠佳。使用这个方式授权,需要在安全性和便利性之间做好权衡。

流程中几个步骤涉及到的接口:

重定向授权页(步骤A)

请求例子:

GET /authorize?response_type=token&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com

参数说明:

Parameter Description
response_type 表示授权类型,此处的值固定为”token”,必选项。
client_id 表示客户端的ID,必选项
redirect_uri 表示重定向URI,可选项。如果不提供,Authorization Server会使用Client注册时的重定向URI进行重定向。
scope 表示申请的权限范围,可选项,多个scope值用空格分开
state 表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。

携带token重定向回Client(步骤C)

请求例子:

HTTP/1.1 302 Found
Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA&state=xyz&token_type=example&expires_in=3600

参数说明:

Parameter Description
access_token 表示访问令牌,必选项。
token_type 表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。
expires_in 表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
scope 表示权限范围,如果与客户端申请的范围一致,此项可省略。

Implicit Grant 不严格验证Client,因此这里不提供 refresh_token(以防Client不经用户同意,使用refresh_token不断得到授权)。同时Implicit Grant 的access_token 是通过url的hash返回的,不会在网络上传输,但是还是存在泄漏的可能(如User-Agent本身不安全)。

Resource Owner Password Credentials Grant

这种授权方式其实是常见的用户名密码认证方式。使用这种授权的Client必须是高度可信的,比如操作系统。只有当其他的流程不能使用时,才启用这种方式,同时Authorization Server必须特别关注Client确保不会出现安全问题。整个过程中,Client不得保存用户的密码(只能由Client来保证,所以Client必须是高度可信的)。

+----------+
| Resource |
|  Owner   |
|          |
+----------+
     v
     |    Resource Owner
    (A) Password Credentials
     |
     v
+---------+                                  +---------------+
|         |>--(B)---- Resource Owner ------->|               |
|         |         Password Credentials     | Authorization |
| Client  |                                  |     Server    |
|         |<--(C)---- Access Token ---------<|               |
|         |    (w/ Optional Refresh Token)   |               |
+---------+                                  +---------------+
  1. 步骤A:resource owner 提供给Client用户名密码。
  2. 步骤B:Client直接使用用户名密码向Authorization Server进行认证,并请求token。
  3. 步骤C:Authorization Server认证Client信息和用户名密码,验证通过后返回token。

流程中几个步骤涉及到的接口:

Client提交用户名密码请求token(步骤B)

请求例子:

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

grant_type=password&username=johndoe&password=A3ddj3w

参数说明:

Parameter Description
grant_type 表示授权类型,此处的值固定为”password”,必选项。
username 表示用户名,必选项。
password 表示用户的密码,必选项。
scope 表示权限范围,可选项。

Authorization Server返回token信息(步骤C)

响应例子:

1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter":"example_value"
}

这里的响应参数跟Authorization Code 模式是一样的。

Client Credentials Grant

该模式是Client 访问实现与Authorization Server约定好的资源。Client以自己的名义,而不是以用户的名义,向Authorization Server进行认证。严格地说,Client Credentials 模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向Client注册,Client以自己的名义要求Authorization Server提供服务,其实不存在授权问题。

+---------+                                  +---------------+
|         |                                  |               |
|         |>--(A)- Client Authentication --->| Authorization |
| Client  |                                  |     Server    |
|         |<--(B)---- Access Token ---------<|               |
|         |                                  |               |
+---------+                                  +---------------+
  1. 步骤A:Client 向Authorization Server进行身份认证,并请求token。
  2. 步骤B:Authorization Server 对 Client信息进行认证,有效则发放token。

流程中几个步骤涉及到的接口:

Client申请token(步骤A)

请求例子:

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

grant_type=client_credentials

参数说明:

Parameter Description
grant_type 表示授权类型,此处的值固定为”client_credentials”,必选项。
scope 表示权限范围,可选项。

这一步Authorization Server 必须验证Client。

Authorization Server返回token信息(步骤B)

响应例子:

1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter":"example_value"
}

这里的响应参数跟Authorization Code 模式也是一样的。

PKCE(Proof Key for Code Exchange)

随着无服务端移动应用或SPA的流行,IETF针对Implicit授权提出了优化方案,在RFC-6749的四种Flow之外另外定义了一种更安全的PKCE模式(RFC-7636)。
PKCE的流程大概如下:

                                          +-------------------+
                                          |   Authz Server    |
+--------+                                | +---------------+ |
|        |--(A)- Authorization Request ---->|               | |
|        |       + t(code_verifier), t_m  | | Authorization | |
|        |                                | |    Endpoint   | |
|        |<-(B)---- Authorization Code -----|               | |
|        |                                | +---------------+ |
| Client |                                |                   |
|        |                                | +---------------+ |
|        |--(C)-- Access Token Request ---->|               | |
|        |          + code_verifier       | |    Token      | |
|        |                                | |   Endpoint    | |
|        |<-(D)------ Access Token ---------|               | |
+--------+                                | +---------------+ |

这里引入了几个新的变量:t_m(摘要算法),code_verifier,code_challenge(即图中经过算法t_m计算后得到的t(code_verifier)参数)

  1. Client随机生成一串字符并作URL-Safe的Base64编码处理, 结果用作code_verifier。
  2. 将这串字符通过SHA256哈希,并用URL-Safe的Base64编码处理,结果用作code_challenge。
  3. Client使用把code_challenge,请求Authorization Server,获取Authorization Code。(步骤A)
  4. Authorization Server 认证成功后,返回Authorization Code(步骤B)。
  5. Client 把Authorization Code 和code_verifier请求Authorization Server,换取Access Token。
  6. Authorization Server 返回 token。(步骤D)

由于中间人不能由code_challenge逆推code_verifier,因此即使中间人截获了code_challenge, Authorization Code等,也无法换取Access Token, 避免了implicit模式的安全问题。

流程中几个步骤涉及到的接口:

Client重定向授权页(步骤A)

请求例子:

https://{authorizationServerDomain}/oauth2/default/v1/authorize?client_id=0oabygpxgk9l
XaMgF0h7&response_type=code&scope=openid&redirect_uri=yourApp%3A%2Fcallback&st
ate=state-8600b31f-52d1-4dca-987c-386e3d8967e9&code_challenge_method=S256&code_
challenge=qjrzSW9gMiUgpUvqgEPE4_-8swvyCtfOVvg55o5S_es

response_type,client_id,redirect_uri,scope,state 跟implicit 模式是一样的。重点看下其他几个参数。

参数说明:

Parameter Description
code_verifier 一串用来加密的 43 位到 128 位的随机字符串。由 A-Z,a-z,0-9,还有符号 -._~ 生成。
code_challenge 由 code_verifier 来生成,如果设备支持加密,则加密方式为:BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))。如果不支持,则直接使用 code_verifier。
code_challenge_method 生成 code_challenge 所用方法,分为 SHA256 和 plain。前者是指 SHA256 方法加密生成,后者是指直接使用 code_verifier,即不加密。

Authorization Server返回token信息(步骤B)

响应例子:

1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter":"example_value"
}

这里的响应参数跟Authorization Code 模式也是一样的。

Token

对于token(Access Token和Refresh Token)需要使用什么样的格式,其实没有硬性要求,不同平台有不同的实现方式。这里列举两种常见的token规范,Bearer Token和JWT。

Bearer Token

OAuth 诞生时就已经定义了两种token格式:Bearer Token 和 Mac Token,Mac 主要使用在无https的环境下,由于OAuth2.0已经要求所有参与者必须使用HTTPS,所以Mac格式不在我们今天讨论范围。Bearer Token由RFC-6750定义。

Bearer Token 格式用BNF范式表示就是:

b64token    = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"="
credentials = "Bearer" 1*SP b64token

换成程序员比较容易理解的正则表达式就是:

b64token = [0-9a-zA-Z-._~+/]+=
credentials =  Bearer\s([0-9a-zA-Z-._~+/]+=)+

所以所谓的Bearer Token就是以数字、大小写字母、破折号、小数点、下划线、波浪线、加号、正斜杠、等号结尾组成的Base64编码字符串。在HTTP传输过程中,需要以’Bearer ‘作为前缀标识。

Bearer Token 的三种传输方式

RFC-6750定义了三种传输Bearer Token 的方式,优先级依次递减:

Authorization Request Header Field(使用HTTP Header的Authorization字段传递)

GET /resource HTTP/1.1
Host: server.example.com
Authorization: Bearer mF_9.B5f-4.1JqM

Form-Encoded Body Parameter(使用表单参数传递)

POST /resource HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded

access_token=mF_9.B5f-4.1JqM

URI Query Parameter(使用URI参数传递)

GET /resource?access_token=mF_9.B5f-4.1JqM HTTP/1.1
Host: server.example.com

由于Cookie容易被CSRF攻击,不建议采用cookie的方式传输token。
尽量不要用URI参数的方法,因为浏览器历史记录、服务器日志等可能泄露URI上的机密信息。

JWT

JWT(JSON Web Token)是近几年移动端常用的token,它可以直接将一些信息编码传递,对客户端更友好。使用JWT有以下有点:

  1. 验证token 不需要另外的缓存或者数据库,通过约定好的加密方式解密就行。
  2. 因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。
  3. 因为有了payload部分,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。
    便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。
  4. 它不需要在服务端保存会话信息, 所以它易于应用的扩展。

使用JWT也必须注意一些问题:

  1. 不应该在jwt的payload部分存放敏感信息,因为该部分是客户端可解密的部分。
  2. 保护好secret私钥,该私钥非常重要。
  3. 如果可以,请使用https协议传递JWT。

JWT也有自己的RFC规范RFC-7519,这里简单介绍一下它的格式。详细请参考文末的RFC链接。

JWT的格式很简单,一个JWT字符串分为Header,Payload,Signature三部分,他们的原始字符串经过编码后由小数点分隔连接起来。

Header记录着token类型和摘要算法,这里的明文最后要经过Base64URL编码:

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

Payload记录着业务信息和用户数据(非敏感),字段可以根据需求自定义,处于安全性考虑,实现方会再加上expire过期时间字段控制生命周期。这里的明文同样也要经过Base64URL编码:

1
2
3
4
5
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}

Signature是Header和Payload经过摘要算法处理后的签名信息,使用的摘要算法需要同Header中alg属性一致,这里是HS256。secret是加密需要的密钥,使用对称加密算法的话密钥泄漏影响较大。如果使用非对称加密算法(如RSA256),使用的是公钥验证签名,风险就小很多:

1
2
3
4
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

连接编码后的三个部分,就得到一个JWT字符串:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM

所以当Server端颁发JWT后,Client就可以根据约定好的secret,摘要算法验证Signature并提取Payload信息。

OAuth 面临的安全问题

OAuth2.0 作为一个授权协议,安全问题尤为重要。OAuth大规模应用的这些年来,主要的安全问题可以分为以下几类:

  1. Client Authentication(客户端错误认证),作为Client的开发者,必须保护好自己的client_id client_secret,谨防盗用。
  2. Code or Token Steal(票据窃取),OAuth 是票据协议,无法区分使用票据的人是否合法。所以作为Authorization Server,必须对token的失效机制做好控制(如合理的失效时间,限制Code只能用一次,允许用户管理自己已授权的token)。作为Client,必须确保用户授权的token不被采集(最常见的问题就是在log中记录access token)
  3. Cross-Site Request Forgery(CSRF攻击),Authorization Code Grant模式流程较长,存在CSRF隐患。
  4. Authorization Code Redirection URI Manipulation(重定向地址篡改),重定向地址篡改是钓鱼网站常用的攻击手段。

前面两种是任何认证授权系统都需要考虑的安全问题,这里重点介绍下后面两种跟OAuth流程比较相关的安全问题。

CSRF攻击

在OAuth2.0流程中实施CSRF攻击的流程如下:

原理

  1. 攻击者预先准备好使用自己账号授权生成的 authorization code 的回调地址,引诱用户点击。
  2. 用户点击后,变成使用攻击者的账号完成Oauth 流程得到token。
  3. 在的第三方app绑定账号的场景,攻击者就可以使用自己的账号完成OAuth登陆用户的第三方app。

防范措施

要防止这样的攻击其实很容易,使用RFC规范中推荐的state参数即可,但是由于增加了开发工作量,很多开发者使用OAuth2.0时,经常忽略这个参数。具体细节如下:

  1. 在 Authorization Code Grant或者implicit Grant流程的第一步,调用/authorize接口时,带上state参数,state的值由Client指定,生成规则需保证足够随机又有一定业务含义,他人无法轻易假冒。
  2. Client 需要保存state参数。
  3. 在Authorization Server 认证成功重定向回Client时,会将state原样带回,此时Client需要验证state参数是否一致。

Authorization Code 流程重定向地址篡改

对重定向地址检查也是一个时常被忽略的安全弱点。

原理

  1. 对于一个正常的第三方Client应用A,攻击者自己也作为一个Client,伪造一个应用A的/authorize请求的链接,其中redirect_uri指向的是攻击者的Client。
  2. 攻击者诱导用户点击伪造的链接,发起OAuth2.0的Authorization Code Grant流程。
  3. 用户完成认证后,Authorization Server 携带Code重定向回攻击者Client。
  4. 攻击者准备一个自己的Code,将上一步应用A的Code替换,伪造一条应用A的回调请求返回给应用A。(此时Code被替换成了攻击者的Code)
  5. 应用A的Client在不清楚Code被替换的情况下,继续完成Authorization Code Grant流程,使用攻击者的Code换取Access Token。

此时用户走完OAuth流程,但是在应用A上得到的却是攻击者帐号的授权。大家会觉得,这样有什么问题,又不是用户的授权泄漏。这种攻击方式可以针对绑定帐号的场景,比如用户本来要将豆瓣帐号与微博帐号关联,使用微博的OAuth授权来登陆豆瓣。而被这样钓鱼以后,自己的豆瓣帐号绑定的是攻击者的微博帐号,此时攻击者就可以用他的微博帐号登陆用户的豆瓣帐号了。

防范方法

  1. Client 注册时,需要开发者提供域名与Client绑定。
  2. Authorization Server对/authorize 接口验证的redirect_uri 参数验证,确认与Client注册时提供的域名一致。
  3. Authorization Server对/access_token 接口的redirect_uri 参数进行验证,保证与Client发起 /authorize请求时的redirect_uri一致。
  4. 对于Authorization Server的Code换token接口,可以要求Client提供client_id和secret,校验此时的code是否产生自同一个client_id。

对于上面提到的CSRF和钓鱼攻击,Client方面如果增加一些授权成功后的提示给用户(比如平台成功与xxx帐号绑定),可以避免用户无意识地授权的情况发生。上面的例子只是简单展示了OAuth授权中需要开发者关注的安全细节,关于OAuth安全想要了解更多,可以参考文末的OAuth安全指南。

小结

  1. OAuth2.0 规范将参与者划分为 Resource Owner,Resource Server,Client,Authorization Server四种角色。
  2. RFC-6749定义了四种OAuth2.0 Grant Flow:Authorization Code Grant,Implicit Grant,Resource Owner Password Credentials Grant,Client Credentials Grant。其中前两种是比较常用的OAuth2.0授权模式。
  3. 对于移动端APP或者SPA应用,可以考虑使用PKCE模式减少Implicit Grant的安全风险。
  4. 对于Token的格式,建议使用Bearer Token或者JWT。
  5. 由于OAuth2.0的Flow步骤较长,不管是Client端还是Authorization Server端,在使用OAuth2.0的时候,最好严格按照RFC规范执行,可以最大程度地减少安全隐患。同时也要注意业界关于OAuth漏洞的披露,及时修复漏洞。

参考链接

  1. OAuth2.0: https://oauth.net/2/
  2. OAuth2.0(RFC-6749): https://tools.ietf.org/html/rfc6749
  3. PKCE(RFC-7636): https://tools.ietf.org/html/rfc7636
  4. Bearer token(RFC-6750): https://tools.ietf.org/html/rfc6750
  5. JWT(RFC-7519): https://tools.ietf.org/html/rfc7519
  6. 乌云平台(备份)OAuth安全指南: http://drops.xmd5.com/static/drops/papers-1989.html

最近把去年出版的 Effective Java 3rd Edition 的新章节读完了,把笔记统一整理一下。

Lambdas and Streams

Item 42: Prefer lambdas to anonymous class

Java 8 以前,模板方法、函数方法基本是用匿名类实现:

1
2
3
4
5
6
7
// before JDK1.8  use anonymous object
// Anonymous class instance as a function object - obsolete!
Collections.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});

Java 8 开始,可以使用lambda表达式:

1
2
3
4
5
6
7
// before JDK1.8  use anonymous object
// Anonymous class instance as a function object - obsolete!
Collections.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});

Java 8 中的 lambda 表达式时基于函数式接口(Function Interface),它就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。函数式接口可以被隐式转换为
lambda 表达式。

比如 上面Collections.sort中的 Comparator 变量,它的函数式接口定义为:

1
2
3
4
5
6
7
8
 */
@FunctionalInterface
public interface Comparator<T> {
public static <T> void sort(List<T> list, Comparator<? super T> c) {
list.sort(c);
}
// other method …
}

所以从Java 8 开始,不要创建一个匿名内部类作为函数对象,除非函数式方法入参没有定义函数式接口。

Item 43: Prefer method references to lambdas

Java 8 开始提供一种比 lambda 更简洁的方式作为函数对象:方法引用(method references)

方法引用和 lambda 表达式对比:

1
2
3
4
// method reference
map.merge(key, 1, Integer::sum);
// lambda
map.merge(key, 1, (count, incr) -> count + incr);

目前方法引用唯一用途是用来简化 lambda 表达式,用方法名来代替 lambda。

方法引用的几种形式:

引用静态方法

ContainingClass::staticMethodName

例子: String::valueOf,对应的Lambda:(s) -> String.valueOf(s)

比较容易理解,和静态方法调用相比,只是把.换为::

引用特定对象的实例方法

containingObject::instanceMethodName

例子: x::toString,对应的Lambda:() -> this.toString()

与引用静态方法相比,都换为实例的而已。

引用特定类型的任意对象的实例方法

ContainingType::methodName

例子: String::toString,对应的Lambda:(s) -> s.toString()

太难以理解了。难以理解的东西,也难以维护。建议还是不要用该种方法引用。

实例方法要通过对象来调用,方法引用对应Lambda,Lambda的第一个参数会成为调用实例方法的对象。

引用构造函数

ClassName::new

例子: String::new,对应的Lambda:() -> new String()

构造函数本质上是静态方法,只是方法名字比较特殊。

有些情况下,使用方法引用的代码 会比 lambda 更长

比如这个在 GoshThisClassNameIsHumongous 类中的方法:

用方法引用

1
service.execute(GoshThisClassNameIsHumongous::action);

等价的 lambda 是:

1
service.execute(() -> action());

下面是各种方法引用的例子:

Method ref Type Example Lambda Equivalent
Static Integer::parseInt str -> Inter.parseInt(str)
Bound Instant.now()::isAfter Instant then = Instant.now();
t -> then.isAfter(t)
Unbound String::toLowerCase str -> str.toLowerCase()
Class Constructor TreeMap<K, V> :: new () -> new TreeMap<K, V>
Array Constructor int[] :: new len -> new int[len]

: Bound unbound reference method 用法语义比较模糊,感觉用 Lambda 描述更清楚。

总而言之,当方法引用更简单明了时,就用它,否则就用 lambda。

Item 44: Favor the use of standard functional interfaces

JDK 包中已经默认提供了多种函数式接口,所以尽量使用自带的而不是自己写函数式接口。

比如下面的例子,我们自定义了一个函数式接口,需要让重写的类有一一个 remove 的默认方法,入参是 map自己的引用和eledest元素,使用fuction interface可以先定义一个接口:

1
2
3
4
// Unnecessary functional interface; use a standard one instead. 
@FunctionalInterface interface EldestEntryRemovalFunction<K,V>{
boolean remove(Map<K,V> map, Map.Entry<K,V> eldest);
}

其实这个接口,JDK已经有默认实现,就是BiPredicate 接口。

标准函数式接口分类

Java 自带的函数式接口分为几类:Operator,Predicate,Function,Supplier,Consumer。

  1. Operator 接口表示入参和返回值是同一种类型的函数,比如下面代表一元、二元运算的接口:

    1
    2
    3
    4
    5
    @FunctionalInterface
    public interface UnaryOperator<T> extends Function<T, T> {
    static <T> UnaryOperator<T> identity() {
    return t -> t;
    }
1
2
3
4
5
6
7
8
9
10
11
12
@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
}

public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
}
}
  1. Predicate 有些地方叫做谓词函数,用来表示返回值是boolean 的函数,如:

    1
    2
    3
    4
    5
    @FunctionalInterface
    public interface Predicate<T> {
    boolean test(T t);
    ...
    }
  2. Function 类型的函数接口表示入参和返回值不同类型的函数,意为将参数T传给一个函数,返回R。即 R = Function(T),比如:

    1
    2
    3
    4
    @FunctionalInterface
    public interface BiFunction<T, U, R> {
    R apply(T t, U u);
    }
  3. Supplier 类型的接口,表示一个没有入参,有返回值的函数,例如:

    1
    2
    3
    4
    @FunctionalInterface
    public interface Supplier<T> {
    T get();
    }

    Supplier 常用于 Stream计算中new 对象。

  4. Consumer 类型的函数接口表示接受一个泛型参数,但是不返回数据,如:

    1
    2
    3
    4
    5
    @FunctionalInterface
    public interface Consumer<T> {
    void accept(T t);
    ...
    }

@FunctionalInterface 注解

@FunctionalInterface 有点像 @Override 注解,注释告诉编译器这是个函数式接口,没有任何功能上的作用。只有一个抽象方法的接口,不使用这个注解也是函数式接口。

需要函数式接口时,先看一下Java 提供的标准接口能否满足,不满足再自己实现,实现时最好遵循标准函数式接口的定义风格。

Item 45: Use streams judiciously

一些 stream API 的概念

stream API 里有两种抽象概念:

  1. the stream: 表示Java 中的各种集合
  2. stream pipe-line: 表示对于这些集合的多重计算操作,有0个或多个 intermediate operations 一个 terminal
    operation 组成。 intermediate operations 从一个stream传到另一个stream,terminal operation 对接最后一个 intermediate operations,对stream做最后一次操作(一般是排序、打印、转换集合结果等操作)。

Stream pipelines 是懒式(lazily)执行的,只有当碰到 terminal operations 时,前面所有的 intermediate operations 才会执行。不带 terminal operations的stream操作是静默的,永远不会执行。

stream api 是流式的。

默认情况下stream pipleline 是串行(squentially)执行的

过度使用 stream 让程序难以阅读和维护

在没有显示类型的情况下,合适的参数命名是保持stream 流有良好可读性的关键。

避免使用 stream去处理字符(char)数据

例如:

"Hello world!".chars().forEach(System.out::print);"Hello world!".chars()返回的是int[]数组的stream,所以输出的不是 Hello world!而是 7210110810811132119i11111410810033。因为stream API中只支持 int long double三种primitive ype 的stream,没有char的stream。

只有在值得这么做的情况下,才需要使用sream重构已存在的代码或在新代码中使用sream。否则容易引入新问题。

几种适合使用stream api的场景

  1. 统一转换元素序列(如集合中的元素类型)。
  2. 过滤元素序列。
  3. 对序列元素进行单一聚合操作(如计算和,最小值,连接等)
  4. 将序列元素转化为集合,或者以特定条件为它们分组。
  5. 以指定条件搜索序列元素。

当不知道是否需要使用sream时,两种方案都尝试,看哪个更合适。

Item 46: Prefer side-effect-free functions in streams (没有副作用)

尽量把foreach 操作用于输出stream结果,而不是计算逻辑。

总的来说,stream 的流式编程是没有副作用(side-effect-free)的函数对象。

为了正确使用 stream,必须知道 collectors 操作,它用于产生输出集合。几个重要的collectors 工厂是:toList, toSet, toMap, groupingBy, joining.

Item 47: Prefer Collection to Stream as a return type

Collection 和它的子类最适合作为返回序列数据的方法的返回值类型。Stream 没有继承或实现 Interable 接口,所以它不能使用 for-each 遍历,所以当某个方法需要返回序列时,优先使用 Collection 返回而不是 Stream。因为Collection 接口不仅可以被for-each遍历,它还有一个sream方法。你的方法的调用方可能需要返回的序列结果进行sream运算,或者仅仅只要遍历访问它。

Item 48: Use caution when making streams parallel

在并发编程中,违反安全性和活跃度(liveness)的情况是没法避免的,stream 的 parallel 也不例外。

看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11

// Stream-based program to generate the first 20 Mersenne primes
// if change to parallel invoke, it will let cpu usage to 90 percent
public static void main(String[] args) {
primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
.filter(mersenne -> mersenne.isProbablePrime(50)).limit(20)
.forEach(System.out::println);
}
static Stream<BigInteger> primes() {
return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}

假如源代码是Stream.iterate,或者有使用limit(),再使用 parallel 并行化一个stream pipeline 不大可能提高性能。所以不要随意地使用 parallel 来并行化 stream 计算。

作为一个规则,在以下类型数据上使用stream parallel 比较容易获得性能收益:ArrayList, HashMap, HashSet, and ConcurrentHashMap instances; arrays; int ranges; and long ranges. 因为这些数据结构易于拆分子任务,且有较好的存储局部性(locality of reference),所以能在 stream 的并行任务上获得较好效果。

局部性原理可以参考:

  1. https://www.zhihu.com/question/25142664
  2. https://zhuanlan.zhihu.com/p/30127242

如果自己定义了 Stream,Iterable,Collection 接口的接口实现,想要在使用parallel时实现好的性能,需要重写 spliterator 方法。

在正确使用的前提下,使用 parallel 处理stream 流,可以得到近似处理器核心数的线性性能提升。某些领域,比如机器学习和数据处理,特别适合这种性能提升。

总的来说,不要尝试使用parallel处理stream,除非你有足够的理由去保证这么做能大幅提高性能并保证程序正确性。确保你的代码在使用parallel后仍然正确。

Optionals

Java 8 中新增的 Optional 容器类,它封装了可能为null的对象,强制使用方在使用时进行检查,防止NPE问题。

容器类型(collections, maps, streams, arrays, and optionals )不应该被包换到Optional对象中。

当你的方法可能没有返回值时,你应该使用Optional作为返回值类型,这样调用方必须处理没有返回值的情况,避免了NPE问题。

不可以用 Optional 去包装原始类型(Boolean, Byte, Character, Short, and Float),因为 Optional 会对其进行两次装箱(boxing)。这种情况应该直接使用 OptionalInt, OptionalLong, and OptionalDouble

不要使用Optional对象最为Map的key或者数组的元素。

因为涉及的装箱拆箱操作,对于性能要求严苛的方法,还是使用返回null的方式处理控制比较合适。

Default methods in interfaces

Java 8中引入默认方法是为了让老接口支持lambda表达式。在老接口中添加默认方法,这些接口的实现类不会在编译器报错。

try-with-resources

Item 9: Prefer try-with-resources to try-finally

Java 7 以后,应该总是使用 try-with-resources 方式而不是 try-finally 方式处理资源操作。

自动关闭资源的语法:

1
2
3
4
5
6
7
// try-with-resources - the the best way to close resources! 
static String firstLineOfFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(
new FileReader(path))) {
return br.readLine();
}
}

要使用 try-with-resources 语法,资源类必须实现 AutoCloseable 接口。

@safeVarags

混用泛型和可变参数时,可能存在安全问题:

1
2
3
4
5
6
7
8
9

// Mixing generics and varargs can violate type safety!
static void dangerous(List<String>... stringLists) {
List<Integer> intList = List.of(42);
Object[] objects = stringLists;
objects[0] = intList;
// Heap pollution
String s = stringLists[0].get(0); // ClassCastException
}

保存一个值到泛型的可变参数数组中是不安全的,编译器也会提出警告:

1
2
3
Warning look like this:
warning: [unchecked] Possible heap pollution from
parameterized vararg type List<String>

@SafeVarargs 提供了一种声明,表示该方法的作者保证该方法是类型安全的,编译器会忽略安全检查,不显示警告。

只要有混用泛型和可变参数的方法,都要声明@SafeVarargs ,同时作者必须保证方法内部不会出现上面例子的Heap pollution 的类型安全问题。

以下情况的可变参数方法是安全的:

  1. 不在可变参数数组中保存任何数据
  2. 让可变参数数组的数据对非信任代码不可见。

@SafeVarargs 只在费重载方法中有效,且java 9之前只能用静态方法。java9中添加了私有方法的使用。

刚工作的时候,看了王垠的一篇《编辑器之神和神之编辑器》,便落入了使用vim的深坑。工作了那么多年发现每个一段时间重新看vim东西总能学到新的知识,最近快速读完了《Practical Vim》,在这里把一些以前没注意的小技巧再重新整理一下。

normal mode

  • R进入逐字替换
  • f{char}搜索某个字符(比如c),按;可以跳到下一个c位置,按,跳到上一个c
  • 使用相对行号时,如何删除当前行开始向下数3行的内容? d3j
  • q/ 查看使用/搜索过的历史命令
  • q: 查看 ex 命令历史
  • m{char} 标记位置,`{char} 调整到标记位置,其中{char}小写字母是文件内标记,用大写字母是全局标记(可以在文件之间跳转)
  • 0(数字0)是复制专用寄存器,只有y命令会覆盖它,使用y命令后,”0p可以黏贴寄存器内容。覆盖无名寄存器的操作(x,c,d等)不会覆盖0寄存器

insert mode

  • <C-r>{register} 直接输出寄存器的值,比如<C-r>a是输出a寄存器内容
  • <C-r>=6*35<CR> 使用表达式寄存器=,计算6*35的值并输出

virtual mode

  • v高亮选择文本,gv重选上次高亮选区
  • 高亮选中文本后,按o可以将光标在高亮文本起始末尾跳转,方便调整高亮文本

Ex command mode

  • [range] m {address} 把[range] 范围的文本移动到{address}
  • [range] t {address} 把[range] 范围的文本复制到{address}
  • <C-r>{register} 在命令行黏贴寄存器内容
  • [range] normal {command} 使用命令行执行普通模式命令,如 % normal A; 会在所有行后面追加字符;
  • 重复上一次执行的Ex 命令,使用 @:
  • <C-d> 显示可用命令列表,Tab补全命令
  • <C-r><C-w>插入当前光标下的单词到编辑的command中

[range] 范围语法支持:

符号 地址
1 文本第一行
$ 文本的最后一行
0 虚拟行,位于文本第一行上方
. 光标所在行
‘m 位置标记m所在行
‘< 高亮选区的起始行
‘> 高亮选区的结束行
% 整个文件
  1. 允许使用+{number}-{number}对[range]进行偏移
  2. [range] 也支持使用模式匹配

Macro (宏)

录制宏:

  1. q{register} 开启录制
  2. 录入宏的内容
  3. q结束录制

调用宏:@{register}
批量调用: n@{register} ,这里n是执行宏的次数
向录制好的宏追加内容: 用 q{register大写} 开始录制,录制后的内容会追加到原来的宏后面。比如原来用 qa 录制了 dwi 操作,想要再后面加j,可以:

  1. qA 开启宏a的追加
  2. 录制j
  3. q结束录制
  4. 此时@a执行的操作就是dwij

编辑宏:
宏用的寄存器和复制黏贴的寄存器是一样的,需要编辑好录制的宏,可以先将寄存器输出,编辑后复制到寄存器中,继续用@{register}执行宏。比如原来寄存器a录制的宏是dwij,现在想要改成dwi2j,可以:

  1. 在normal mode下,”ap 黏贴出宏内容 dwij
  2. 将内容改成 dwi2j
  3. 选中文本,y”a 覆盖寄存器a内容
  4. 再执行@a,操作就是 dwi2j了

Pattern 模式匹配

vim 的 search(搜索),substitute(替换)命令都支持Pattern,也就是正则表达式匹配
默认情况下,/ 搜索模式的正则表达式,正则表达式的元字符 .*()等,都需要加\转义,也就是在默认情况下/.*搜索的是 .*字符串,而不是任意个字符

magic:
在搜索表达式前加\m使用magic功能,除了 $.*^之外的元字符要加反斜杠,如\m.*搜索的就是任意字符,若不是.*字符串

very magic:
在搜索表达式前\v使用very magic 功能,正则表达式任何元字符都不用加\,如:/\v(a.c){3}$ 就是查找行尾三个字符是a{char}c的表表达式(如abc,acc,aec)。

强制关闭magic:
如果已经set magic,要在搜索表达式里强制不用magic,就在前面加\V,比如已经开启了set magic,使用 /\V.*搜索的是.*字符串。

单词界定:
在very magic 模式下,使用<word>可以以单词为界匹配字符,而不会匹配到其他内容。比如目前有个文本:

1
2
3
4
abc aa
abcdedf
1234
123abc

/\v<abc>搜索时,只有第一行 abc 匹配,123abc不会匹配。

模式匹配边界:
一个匹配的边界通常对应于一个模式的起始与结尾。但我们可以使用元字符 \zs\ze对匹配进行裁剪,使其成为这个完整模式的一个子集(参见 :h /\zs ) 。元 字符 \zs标志着一个匹配的起始,而元字符\ze则用来界定匹配的结束。将二者相结合,我们可以定义一个特殊的模式,它们可以让我们定义一个模式匹配一个较大的文本范围,然后再收窄匹配范围。与单词定界符类似,\zs\ze 均为零宽度元字符。

vim中模式的特殊元字符 ,\zs\ze关键字示例:

必须转义的情况:

  1. 正向查找时,/必须转义。
  2. 反向查找时,?必须转义。
  3. 每次都要转义\

查找&替换
显示当前查找关键词个数: :%s///gn

将光标定位到匹配结果词尾:/lang/e

重用上次查找的模式
:%s/\va.c/123/g
等价于以下两个命令

1
2
/\v.c
%s//123/g

global 命令

:global命令的广义形式如下所示:
:g/{pattern}/[range][cmd]
:g/{re}/{cmd} 在匹配的行上执行命令,如 :g/re/d ,删除包含re字符的行
:v/{re}/{cmd} 在非匹配的行上执行命令 如 :v/re/d ,删除不包含re字符的行

今天面试别人的时候,提到了CAS,本来想要引导他说出CAS潜在的ABA问题,发现自己也没发简单的向他解释清楚,需要好好梳理下。

什么是 CAS

维基百科的解释是:

In computer science, compare-and-swap (CAS) is an atomic instruction used in multithreading to achieve synchronization. It compares the contents of a memory location with a given value and, only if they are the same, modifies the contents of that memory location to a new given value. This is done as a single atomic operation. The atomicity guarantees that the new value is calculated based on up-to-date information; if the value had been updated by another thread in the meantime, the write would fail. The result of the operation must indicate whether it performed the substitution; this can be done either with a simple boolean response (this variant is often called compare-and-set), or by returning the value read from the memory location (not the value written to it).

CAS(compare and swap),简单地说就是一种在多线程的情况下,让每个线程修改某个数据是一种原子操作。要实现CAS,有几个关键的值:

  1. 要修改的变量内存中的值V
  2. 更新变量前事先记录的期望值E,取值来自V
  3. 将要更新的值A

一个典型的CAS更新操作如下:

  1. 读取内存中的值V,赋值给E
  2. 更新变量前,比较内存值V与E
  3. 如果V==E,将V更新成A
  4. 如果V!=E,重复步骤1

如此循环,直到步骤3更新操作完成,写成伪代码就是:

1
2
3
4
5
6
7
8
9
10
do{
int e=v.getVal();
synchronized(this){
if(e==getVal()){
v.setVal(a);
break;
}
}

}while(true)

CAS在JDK中被广泛应用,比如java.util.concurrent包下面的Lock、AtomicInteger相关的类都有用到。
例如a++;这种操作在Java里面并不是原子操作(包含了值的累加和赋值两个动作),所以并发情况下竞争操作某一个变量,需要用AtomicXXX几个类。

举个例子

AtomicInteger 实现类似a++操作,使用的是它的incrementAndGet 方法,源码很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
// 此处的 compareAndSet 由usafe对象提供硬件级别的原子操作
if (compareAndSet(current, next))
return next;
}
}

Lock中对于竞争变量的CAS,也是类似的操作。

什么是ABA问题

简单地说就是,多个线程同时使用CAS更新数据时:

  1. 线程1要将数据从A变成B时(此时线程1的期待值E==’A’)
  2. 其他线程已经抢先更新了变量,把变量从A变成其他值,再变回A(如A->C->A)。
  3. 当线程1用CAS机制准备更新变量时,发现E==A,所以继续更新变量。

这样有什么问题?

虽然变量最终结果是对的,但是线程1更新变量前,变量已经经历了一系列变化才回到原值。对于某些场景,忽略变化会继续进行更新操作,会带来错误的结果。
比如:

银行账户扣费问题

某个银行账户扣款操作,由于系统故障,产生了2个线程(T1,T2)对同一账户进行扣款,正常防重逻辑应该是一个执行成功,另一个失败,但是如果使用上面的CAS操作,就是:

  1. 账户里有100元,需要扣款50元
  2. T1先完成操作,扣款50元,将账户值V改为50
  3. T2准备扣款,当时期待值E==100
  4. 此时有其他转账操作先于T2对V进行累加,比如转入50元,此时V又变成100
  5. T2进行CAS的更新操作,发现E\==V\==100,执行更新操作,又扣款50

堆栈操作问题

如果CAS中的操作,变量值V是栈顶指针,也会有同样的问题:

  1. 某个堆栈内容是:A-B-C,栈顶为A
  2. 线程1更新前,得到期望值E==A
  3. 其他线程对栈进行进行pop,push操作,pop A B,push D A,此时栈的内容为 A-D-C
  4. 此时栈顶还是A,但是内容已经改变,线程1要更新的堆栈,已经不是第2步拿到期望值E时,自己要操作的那个堆栈了

如何规避

思路也很简单,就是对得到的期望值E和变量值V,增加一个版本号(比如时间戳),对于不同时期操作产生的同一个值的V,版本号是不同的,比较E与V时,需要同时比较版本号。比如juc包的AtomicStampedReference实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

public class AtomicStampedReference<V> {
private static class Pair<T> {
final T reference; //维护对象引用
final int stamp; //用于标志版本
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
private volatile Pair<V> pair;
....
/**
* expectedReference :更新之前的原始值
* newReference : 将要更新的新值
* expectedStamp : 期待更新的标志版本
* newStamp : 将要更新的标志版本
*/
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair; //获取当前pair
return
expectedReference == current.reference && //原始值等于当前pair的值引用,说明值未变化
expectedStamp == current.stamp && // 原始标记版本等于当前pair的标记版本,说明标记未变化
((newReference == current.reference &&
newStamp == current.stamp) || // 将要更新的值和标记都没有变化
casPair(current, Pair.of(newReference, newStamp))); // cas 更新pair
}
}

总结

  1. CAS 是一种自旋锁,由一个死循环+compareAndSet 实现。
  2. CAS 存在ABA隐患,对于需要关注竞争变量变化过程(不仅仅是变量的值)的场景,ABA问题必须关注。
  3. 解决ABA问题,需要在CAS的compare过程中,增加对期望值E和当前值V版本号的判断。

9月底突发奇想地想国庆和老婆来西安玩一次,于是就匆匆忙忙订了机票和住宿,提前请了2天假29号飞到了西安。28晚上自己刚从北京出差回来,简直马不停蹄。从29号启程到4号回家,我们看了长恨歌演出,逛了西安城区、华山、陕西历史博物馆、华清池、兵马俑、西安城墙。一路上吃吃喝喝,除了在兵马俑见识了千分之一的中国人口以外,其他时候体验不算太差。整座西安城给人一种浓厚的历史氛围和民族融合带来的活力,让沿海长大的我倍感新奇。

9月底10月初的西安,让我一个南方人感受到了秋天。一天气温十几度到二十几度,早晚偏冷,白天晒着太阳也不会满头大汗,真正的秋高气爽。所以每天出门我总是穿着一件薄长袖和夹克外套,热了就把外套脱下别在腰上。值得一提的是,西安城内经常能见到穿着汉服的姑娘小伙,也许是这座城市的道路和建筑本身古色古香,不想在其他地方,走在街上看着他们,竟然不觉得突兀,反而别有一番风味,有种“这果然是长安啊”的感叹。

西安的吃,只要吃得惯的话,简直不要太棒。
6天时间内,我们逛遍了回民街永兴坊,这里的吃的价格也不算便宜,但是分量够大,随便一份面食都是一大碗,我跟老婆为了多尝几口,总是要两人吃一份。第一天老婆同学请吃羊肉泡馍,见识了如何自助徒手撕膜,撕了半个小时手都麻了,煮上羊肉粉丝汤,满满的一大海碗,吃完都快扶着墙出。路边烤羊肉串一串10块,羊肉又大又肥。面馆里的水盆羊肉、biangbiang面、油泼面……每一个都在让我跟米饭说再见。肉夹馍每家卖的都不大一样,什么都能夹,简直西安汉堡。还有永兴坊的子长煎饼,长得跟广东肠粉一样,吃一份得排半小时队。路边随处可见的石榴汁,小镜糕,大甄糕,酸梅汤,花椒酸奶(真加花椒。。),油茶(这个怂了喝不惯),凉粉,酱牛肉,卤羊蹄,羊杂汤……几天下来,自己的味蕾仿佛打开了一个新世界。

由于我们住的酒店离西安城中心挺近的,我们一有空就往钟楼附近跑。钟楼算是西安城区的中心,再加上周围14公里的西安城墙,整个城内古色古香,建筑风格很有特色,满眼望去都是中国传统的中间高两边低的那种屋顶,这种我以前只在电视剧和故宫里看到。路也很宽,在市内骑自行车很舒服,道路横平竖直,房子坐落得也很规整,整个城市颜色跟中国大多数内陆城市一样,整体偏灰色调。虽然国庆路上人多,地上也不见什么垃圾。不知道是不是来的这几天天气好,空气虽然干燥,但是空气还算清新,蓝天白云的天空配合飒爽的秋意给人感觉很好。

这几天出门一般都是打的外加摩拜,出租车司机师傅跟大多数北方司机一样,喜欢唠嗑,平易近人。西安城区的街道方方正正,看好地图的话,骑车走路都不容易迷路(当然回民街还是要多走几次才懂)。比较痛苦的是去周边景区的大巴车,去华山、兵马俑、华清池我们都报的是一日团,一大早就要坐车出发,路上快则两个小时,堵车就要三四个小时,体验不是很好,特别是从兵马俑回来的时候,我们晚上6点返程,回来已经9点半了。

这次行程也算比较紧,第一晚我们去华清池看了长恨歌演出,华山、陕西历史博物馆、兵马俑华清池我们各花了一天时间,最后一天逛了西安城墙,然后在几个晚上陆续逛了钟楼附近和回民街永兴坊,大雁塔之类的景点就没再去了。

长恨歌演出门票260多块钱,需要坐车到华清池景区看(后来想想应该在参观兵马俑华清宫的那天晚上来看比较节省时间)。第一次看这种现场的真人歌舞剧表演,比较新鲜,有种春晚现场的感觉,大体上就是用现代声光电技术演了一出杨贵妃唐玄宗的爱情故事,后面还有鹊桥相会啥的(能把一个公公儿媳扒灰的故事写的如此美好也是囧。。)。可惜我艺术功底不够深刻,只能跟周围的大叔大妈看个新鲜。

第二天一大早坐大巴来登华山,两人比较怂,选择了缆车路线,西峰上北峰下,5个小时爬了华山西峰南峰。一整块花岗岩形成的华山,挺拔陡峭,地势险峻,山上很多台阶不到半个脚宽。站在华山上,俯瞰山下,真的会让人感叹祖国大好河山,雄伟瑰丽。可惜最近上班忽略了身体锻炼,西峰爬了不到一半就气喘吁吁,追不上老婆的步伐。看来回去还是要好好锻炼,有生之年要从山底下上一次,来东峰看日出。

第三天鉴于前一天爬华山太累,就选择了市内的陕西历史博物馆,还好提前订了30块钱的大唐珍宝馆门票,免去了排长队进馆的痛苦。陕博不愧为中国博物馆的Top,文物之多,时间跨度之长令人叹为观止。

第四天我们又跟了一日团参观了华清池和兵马俑,这一天算是体验最差的一天。华清池参观什么呢,就是看看以前杨贵妃杨玄宗洗澡的澡堂子,还有西安事变老蒋被抓之前的故居,都是历史遗迹,想要知道以前的情景,只能对着景观配合导游解说发挥想象力了。下午的兵马俑参观让我体验了什么是“世界第一大坑”。据说当天下午兵马俑的客流量达到了15万,从进博物馆大门到走到展馆门口,浩浩荡荡都是人头,排队的时候前胸贴后背,整个人被后面推着走。要进一号坑之前更是痛苦,博物馆方管理太混乱,三队人马三个方向进一个门,其中不乏插队的低素质游客,大家互不相让,差点爆发肢体冲突。进坑之后人山人海,根本看不到前面,人在队伍中,不由自主地被后面人推着走,还没仔细看完兵马俑的细节,就被推着到出口了。

第五天西安城墙游算是最满意的一天,跟老婆吃完一顿早午饭,慢慢悠悠地买上一张上城墙的票,租上一辆双人自行车,开始休闲游。中午到下午这段时间城墙上人不多,西安城墙虽然不是水泥路,但是一路上没有什么坡度,也足够宽敞,3个小时车程下来,轻轻松松绕城墙骑行了一圈半,两边都是充满古城特色的房子和风景,伴着凉爽的秋意,一扫前几天旅行的疲惫。

写在旅行结束后

最后坐飞机回来前,老婆问我,西安比起以前去过的重庆和成都怎么样?我觉得除了兵马俑那天的大坑以外,西安还是让我非常喜欢的。较慢的生活节奏,好吃的各种美食,浓厚的历史底蕴,人们美好的精神面貌。甚至我都觉得,下半辈子我们应该好好锻炼身体,攒钱来西安置业养老(笑)。人生不应该只有生活工作的那三点一线,应该多一点不一样的经历。相信有生之年,我还会再来几次。

最近淘了一部二手x240,换了SSD和高分屏,装上manjaro 17 作为开发备用机,期间遇到了许多坑,在这边同一记录。

原先使用了KDE桌面,后面发现性能还是跟不上,换成了更轻量级的xfce,All is well~

制作U盘启动

下载 rufus 制作u盘启动

使用U盘安装,启动后显示“failed to load ldlinux.c32”

传统BIOS与UEFI启动的问题,在BIOS的启动选项里,将启动方式改为UEFI优先即可

更换和添加源

sudo nano /etc/pacman.d/mirrors/China

manjaro 17 中 China 文件已经内置了中国的源,建议把清华的源镜像放在第一位

sudo nano /etc/pacman.d/mirrorlist

建议把清华的源镜像放在第一位,更新列表和系统的时候速度会快

sudo nano /etc/pacman-mirrors.conf

修改 OnlyCountry = China (注意把前面的注释 # 删掉)

保存退出 ### 添加archlinuxcn

sudo nano /etc/pacman.conf

添加

[archlinuxcn]
SigLevel = Optional TrustedOnly
Server= https://mirrors.tuna.tsinghua.edu.cn/archlinuxcn/$arch 
#Server = https://mirrors.6.tuna.tsinghua.edu.cn/archlinuxcn/$arch
# only IPv6#Server = https://mirrors.4.tuna.tsinghua.edu.cn/archlinuxcn/$arch
# only IPv4#HTTP is also supported

sudo pacman -Syy 更新一下源列表,此处若出现错误,请按照终端提示,删除一个文件 (本机是类似于/var/lib*的一个db文件)

# 更新出现以下错误,只要把提示的文件删除即可
libglvnd: 文件系统中已存在 /usr/lib/libEGL.so.1  
libglvnd: 文件系统中已存在 /usr/lib/libEGL.so.1.0.0  
libglvnd: 文件系统中已存在 /usr/lib/libGL.so  
libglvnd: 文件系统中已存在 /usr/lib/libGL.so.1  
libglvnd: 文件系统中已存在 /usr/lib/libGLESv1_CM.so  
libglvnd: 文件系统中已存在 /usr/lib/libGLESv1_CM.so.1  
libglvnd: 文件系统中已存在 /usr/lib/libGLESv2.so  
libglvnd: 文件系统中已存在 /usr/lib/libGLESv2.so.2  
libglvnd: 文件系统中已存在 /usr/lib/libGLESv2.so.2.0.0  
lib32-libglvnd: 文件系统中已存在 /usr/lib32/libEGL.so  
lib32-libglvnd: 文件系统中已存在 /usr/lib32/libEGL.so.1  
lib32-libglvnd: 文件系统中已存在 /usr/lib32/libEGL.so.1.0.0  
lib32-libglvnd: 文件系统中已存在 /usr/lib32/libGL.so  
lib32-libglvnd: 文件系统中已存在 /usr/lib32/libGL.so.1  
lib32-libglvnd: 文件系统中已存在 /usr/lib32/libGLESv1_CM.so  
lib32-libglvnd: 文件系统中已存在 /usr/lib32/libGLESv1_CM.so.1  
lib32-libglvnd: 文件系统中已存在 /usr/lib32/libGLESv2.so  
lib32-libglvnd: 文件系统中已存在 /usr/lib32/libGLESv2.so.2  
lib32-libglvnd: 文件系统中已存在 /usr/lib32/libGLESv2.so.2.0.0

重新执行 sudo pacman -Syy 就没有问题了

sudo pacman -S archlinuxcn-keyring` 此步很关键,是安装archlinuxcn的GPG keys 滚动升级一下系统和软件(不建议频繁滚动升级,稳定为主)

sudo pacman -Syyu

开始更新系统了

安装 vi

pacman -Syy vi

安装 aur客户端

yaourt安装与使用 用yaourt装东西,每次sudo yaourt -S xxx 最后出现

错误: 不能使用 root 用户运行 makepkg, 
因为可能会系统造成灾难性的损坏。 
无法读取 PKGBUILD

原因: 不能用 sudo 运行 yaourt ,换句话说必须在普通用户下运行 yaourt yaourt 停止维护了,可以改用yay sudo pacman -Syy yay

让终端走代理

安装并运行 privoxy 或者 polipo, 把本地ss代理转换成http代理,然后把终端代理设置到http代理上:

export https_proxy=http://localhost:8123
export http_proxy=http://localhost:8123

终端打开的程序就会走代理了。

安装 搜狗输入法

sudo pacman -S fcitx-sogoupinyin
sudo pacman -S fcitx-im # 全部安装
sudo pacman -S fcitx-configtool # 图形化配置工具

之后就是还需要更改 ~/.xprofile

export GTK_IM_MODULE=fcitx
export QT_IM_MODULE=fcitx
export XMODIFIERS="@im=fcitx"

最后在命令行输入fcitx就可以使用了

安装日语输入法

yay -Syy fcitx-anthy

dock 软件

aur docky

高分屏像素缩放

系统设置-字体-固定字体dpi 110, 不使用缩放,很多软件在开启缩放后会模糊或其他问题。固定字体的dpi就可以达到缩放效果了。

合上盖子,按电源键挂起没有效果

经过测试,发现按电源键没产生 acpi 事件,因此不能触发电源管理,此暂不能解决。曲线救国:用fn+f9作为快捷键挂起计算机。

指纹模块

参考: https://wiki.archlinux.org/index.php/Fingerprint-gui 配置完成后su和sudo要求输入密码时,就可以用刷指纹了。 ### 运行fingerprint-gui时出现 could not open fingerprint device permission problem 是因为普通用户没有指纹设备的读写权限。 lsusb 找到指纹模块的 Bus号和Device号,比如:

Bus 002 Device 002: ID 138a:0017 Validity Sensors, Inc. Fingerprint Reader

然后赋予/dev/bus/usb/xxx/yyy 777权限 xxx表示Bus号,yyy表示Device号,上面的例子就是 /dev/bus/usb/002/002 http://home.ullrich-online.cc/fingerprint/Forum/topic.php?TopicId=20

KDE 登录界面无法使用指纹识别登录

fingerprint与kde的kdm不兼容,暂时无解。

IDEA 某些菜单乱码

http://www.cnblogs.com/lemonbar/p/3924305.html

触摸板

触摸板优化只在 KDE 环境下配置成功过,xfce下不行。

鼠标点击模拟” 下面的选项是无效

触摸板设置中,“鼠标点击模拟” 下面的选项是灰色的,导致触摸板双指点击作为右键的功能不能用。

解决方案 : pacman 安装驱动 重启即可 触摸板驱动:xf86-input-libinput
官方从17年1月开始换成 libinput驱动,xf86-input-synaptics进入低维护状态,尽量不用

具体参考 https://wiki.archlinux.org/index.php/Lenovo_ThinkPad_X240#Touchpad touchpad 一节

触摸板鼠标手势 安装 libinput-gestures
参考 http://www.cnblogs.com/xiaozhang9/p/6157934.html

自定义配置文件(x240 最多支持3点触控) ~/.config/libinput-gestures.conf

gesture swipe left 4 xdotool key super+Left # 4指左划: 切换到左侧工作区 
gesture swipe right 4 xdotool key super+Right # 4指右划: 切换到右侧工作区 

gesture swipe left 3 xdotool key alt+Left # 3指左划: 网页后退 
gesture swipe right 3 xdotool key alt+Right # 3指右划: 网页前进 
gesture swipe up 3 xdotool key super+w  # 3指上划: 显示当前桌面所有窗口 
gesture swipe down 3 xdotool key super+d # 3指下划: 显示桌面 

gesture pinch in 2 xdotool key ctrl+minus # 2指捏: 缩小 
gesture pinch out 2 xdotool key ctrl+plus # 2指张: 放大

保存配置文件 然后启动 libinput-gestures-setup start 也可以重启 libinput-gestures-setup restart 加入开机启动 libinput-gestures-setup autostart

取消自动锁屏(KDE)

系统设置–桌面行为–锁屏 中设置

没有ifconfig命令

安装net-tool

pacman -S net-tools dnsutils inetutils iproute2

##共享鼠标键盘软件 synergy 下载地址 http://www.afzaalace.com/synergy-stable-builds/
arch 可以直接用pacman下载

Synergy分为服务端和客户端,用户使用鼠标键盘的那一台机子是服务端,其他的是客户端 Arch下的服务端配置 /etc/synergy.conf

# screens 指的是操作的电脑的名字, 
section: screens  
       caixx-pc:  
       Eternity-Home:  
end  
# 配置各电脑之间的相对位置 
section: links  
       caixx-pc:  
               left = Eternity-Home  

       Eternity-Home:  
               right  = caixx-pc  

end  
# 电脑别名 
section: aliases  
       caixx-pc:  
               192.168.1.103  
       Eternity-Home:  
               192.168.1.105  
end

配置完后运行服务端 synergys -f
运行客户端 synergyc -f 服务端ip地址

xfce 桌面相关

主题: aur paper-gtk-theme-git
aur paper-icon-theme-git aur paper-icon-theme

ctrl键交换 修改 ~/.profile 增加 /usr/bin/setxkbmap -option “ctrl:swapcaps”

一些快捷键命令:

睡眠 xfce4-session-logout –suspend

休眠 xfce4-session-logout –hibernate
弹出开始菜单 xfce4-popup-whiskermenu

锁屏 xflock4

哪里设置快捷键? 全局快捷键: setting –> keyboard –> Application shortcuts 窗口相关快捷键:setting –> windows manager

打开docky有,屏幕中间有一条线: image.png-276.2kB 设置-窗口管理器微调-合成器-去掉勾选“在dock窗口下显示阴影”

锁定屏幕后一段时间再用电脑,出现黑屏,只看到鼠标: 关闭屏保试试: 在设置种的 电源管理器 里关闭所有会关闭屏幕的选项 aur light-locker-settings 在设置里关闭 light-locker

滚动背景窗口时,不改变窗口焦点

Settings —> Window Manager Tweaks —> Accessibility 取消‘raise windows when any mouse button is pressed’ 选项

Arch 上维护的常用程序列表 wiki

常用的程序都可以在上面找到: https://wiki.archlinux.org/index.php/List_of_applications_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87))

也不知道这是第几次搭博客了,新浪博客、QQ空间、百度空间、CSDN、ITEYE、OSChina、自建博客……折腾博客的经历几乎跟自己的网龄一样长。写博文这件事,也没有一直坚持下来。所以,为什么这次又开了一个博客?自己总结了以下几点:

我的工作生活,需要一个宣泄的地方。
想要再锻炼一下自己写技术博客的能力。
很不习惯熟人看到我写的东西,不会再暴露任何个人信息了。
年纪一大,不想再折腾,免费博客服务是我的首选。
So,这次直接选了hexo,再尝试一次,看看自己能做到哪一步吧。