山河永寂

山河永寂 查看完整档案

杭州编辑杭州电子科技大学  |  会计学 编辑滴滴  |  iOS 工程师 编辑 blog.parsedge.com 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

山河永寂 发布了文章 · 2017-10-30

数据库随笔

数据库随笔

背景

目前软件开发行业中,无论是移动端开发还是后端开发,基本上都会碰到数据库的开发,这里就谈谈笔者对于数据库的感想

冲突

在移动端亦或是后端开发中,很多时候,我们会感觉到无论是 ORM 还是其他方案,都会存在着一些缺点,其实这来源于数据库本身和开发语言本身的冲突,现代化的语言基本上都是面向对象开发,面向对象是从软件工程基本原则(如耦合、聚合和封装)的基础上发展起来的,而关系数据库则是从数学理论发展而来的,两套理论存在显著的区别。
数据库最初的时候就是起源于文件,一个程序打开一个文件,然后将一些数据存入文件中,最后关闭文件,从这个需求开发,发展出了数据库。目前主流关系型数据库主要有两种,一种是基于网络传输的数据库,一种则是嵌入式数据库,这两种数据库区别就在于是否存在完善的权限管理和是否服务端代码嵌入到了程序中。
无论是哪种关系型数据库,开发者都是通过 SQL 语言和其打交道,主流的 ORM 技术实际上只是由数据库中查询数据,按条读取结果集中每一个字段,然后装配成对象,或者需要把对象的每一个属性拆出来,拼凑成SQL字符串,再提交数据库。

ORM 技术

广义上,ORM指的是面向对象的对象模型和关系型数据库的数据结构之间的相互转换。 狭义上,ORM可以被认为是,基于关系型数据库的数据存储,实现一个虚拟的面向对象的数据访问接口。这里指的 ORM 是指 Hibernate 这样的框架,至于 ORM 框架的好处就不说了,ORM 一般情况下,一个持久化类和一个表对应,类的每个实例对应表中的一条记录,类的每个属性对应表的每个字段。 但是,这本身 SQL 语言是存在冲突的,我们先来拆分一下要求

  1. 数据库需要做 Migration(比如建表,增加字段,删除字段,描述默认值,增加索引,增加约束等)
  2. 表和表通过关系产生联系
  3. SQL 语句可以让你只选择自己需要的列
  4. SQL 语句参数和结果不保证类型安全
  5. 结果可能是虚的,比如存储过程和视图

面向对象和上面的行为实际上是格格不入的,比如将表抽象为一个类,每个类的实例是一行数据,看起来确实很完美,但是其实存在一个隐藏的缺陷。

  1. 首先,主要的数据库操作,基本都是 CRUD 组成,一般来说,增加、删除、更新语法都是差不多的,很容易将其抽象,但是查询就不简单了,对数据的关注度不一样,所需要的查询语句就不同,在针对查询的方面,ORM 很难覆盖全部的情况。
  2. 其次,面向对象中,存在着继承,如果两个对象存在继承关系,那如何设计表结构,

目前的主流 ORM 技术有好多种,Hibernate 是一种比较典型的如上面所述的 ORM 技术,Mybatis 则是一种半自动化的 ORM 技术,这里讲一讲 Mybatis

  1. Mybatis 不存在 Migration 功能,必须要通过第三方功能支持
  2. Mybatis 只做了 SQL 到对象的映射,类本身不代表数据表,而转化出的对象都以 POJO 的形式提供。这实际上是一种字段绑定

也就是说,ORM 本身,需要做两件事

  1. 映射表的本身和迁移
  2. 属性和数据库的字段绑定

Mybatis 由于过于依赖 SQL,因此如果将底层数据库进行替换,则会导致很大的迁移工作量,Hibernate 就不存在这个问题,但是 Hibernate 确实不够灵活,刚才也讲到了 SQL 查询的问题,因此 Hibernate 如果要做到灵活,实在是代价太大了。两者各有优缺点。

ORM 的意义

ORM 主要有两个意义

  1. 防止注入,保证 SQL 安全
  2. 抽象 OOP,便于开发

除了上面这两种 ORM 以外,还有一种更加轻量级的技术方案,就是提供一整套类似于函数调用方式来写 SQL 查询,借助 IDE 的代码提示和编译器语法检查,来保证安全,这种方案是灵活度最高的。但是更加繁琐
在针对防注入方面,通常做法是使用绑定参数和预处理语句,避免字符串的拼接,或者采用手段,防止传入的单引号提前截断 SQL

针对查询的问题的想法

由于数据库查询存在这么大的问题,而且需要保证 SQL 安全,笔者有两条思路

  1. 封装常用的操作,覆盖大多数场景
  2. 提供安全的底层手段,解决特殊查询情况
查看原文

赞 0 收藏 0 评论 0

山河永寂 赞了文章 · 2017-04-29

使用 OAuth 2 和 JWT 为微服务提供安全保障

Part 1 - 理论相关

作者 freewolf

关键词

微服务Spring CloudOAuth 2.0JWTSpring SecuritySSOUAA

写在前面

作为从业了十多年的IT行业和程序的老司机,今天如果你说你不懂微服务,都不好意思说自己的做软件的。SOA喊了多年,无人不知,但又有多少系统开发真正的SOA了呢?但是好像一夜之间所有人都投入了微服务的怀抱。

作为目前最主流的“微服务框架”,Spring Cloud发展速度很快,成为了最全面的微服务解决方案。不管什么软件体系,什么框架,安全永远是不可能绕开的话题,我也把它作为我最近一段时间研究微服务的开篇。

老话题!“如何才能在微服务体系中保证安全?”,为了达成目标,这里采用一个简单而可行方式来保护Spring Cloud中服务的安全,也就是建立统一的用户授权中心。

这里补充说一下什么是Authentication(认证)Authorization(鉴权),其实很简单,认证关心你是谁,鉴权关心你能干什么。举个大家一致都再说的例子,如果你去机场乘机,你持有的护照代表你的身份,这是认证,你的机票就是你的权限,你能干什么。

学习微服务并不是一个简单的探索过程,这不得学习很多新的知识,其实不管是按照DDD(Domain Driven Design)领域驱动设计中领域模型的方式,还是将微服务拆分成更小的粒度。都会遇到很多新的问题和以前一直都没解决很好的问题。随着不断的思考,随着熟悉Facebook/GitHub/AWS这些机构是如何保护内部资源,答案也逐渐浮出水面。

为了高效的实现这个目标,这里采用OAuth 2JWT(JSON Web Tokens)技术作为解决方案,

为什么使用OAuth 2

尽管微服务在现代软件开发中还算一个新鲜事物,但是OAuth 2已经是一个广泛使用的授权技术,它让Web开发者在自己提供服务中,用一种安全的方式直访问Google/Facebook/GitHub平台用户信息。但在我开始阐述细节之前,我将揭开聚焦到本文真正的主题:云安全

那么在云服务中对用户访问资源的控制,我们一般都怎么做呢?然我举一些大家似乎都用过的但又不是很完美的例子。

我们可以设置边界服务器或者带认证功能的反向代理服务器,假设所有访问请求都发给它。通过认证后,转发给内部相应的服务器。一般在Spring MVC Security开发中几乎都会这样做的。但这并不安全,最重要的是,一旦是有人从内部攻击,你的数据毫无安全性。

其他方式:我们为所有服务建立统一的权限数据库,并在每次请求前对用户进行鉴权,听起来某些方面的确有点愚蠢,但实际上这确实是一个可行的安全方案。

更好的方式: 用户通过授权服务来实现鉴权,把用户访问Session映射成一个Token。所有远程访问资源服务器相关的API必须提供Token。然后资源服务器访问授权服务来识别Token,得知Token属于哪个用户,并了解通过这个Token可以访问什么资源。

这听起来是个不错的方案,对不?但是如何保证Token的安全传输?如何区分是用户访问还是其他服务访问?这肯定是我们关心的问题。

所以上述种种问题让我们选择使用OAuth 2,其实访问Facebook/Google的敏感数据和访问我们自己后端受保护数据没什么区别,并且他们已经使用这样的解决方案很多年,我们只要遵循这些方法就好了。

OAuth 2是如何工作的

如果你了解OAuth 2相关的原理,那么在部署OAuth 2是非常容易的。
让我们描述下这样一个场景,“某App希望获得TomFacebook上相关的数据”

OAuth 2 在整个流程中有四种角色:

  • 资源拥有者(Resource Owner) - 这里是Tom

  • 资源服务器(Resource Server) - 这里是Facebook

  • 授权服务器(Authorization Server) - 这里当然还是Facebook,因为Facebook有相关数据

  • 客户端(Client) - 这里是某App

Tom试图登录Facebook某App将他重定向到Facebook的授权服务器,当Tom登录成功,并且许可自己的Email和个人信息被某App获取。这两个资源被定义成一个Scope(权限范围),一旦准许,某App的开发者就可以申请访问权限范围中定义的这两个资源。

+--------+                               +---------------+
|        |--(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 ---|               |
+--------+                               +---------------+

Tom允许了权限请求,再次通过重定向返回某App,重定向返回时携带了一个Access Token(访问令牌),接下来某App就可以通过这个Access TokenFacebook直接获取相关的授权资源(也就是Email和个人信息),而无需重新做Tom相关的鉴权。而且每当Tom登录了某App,都可以通过之前获得的Access Token,直接获取相关授权资源。

到目前为止,我们如何直接将以上内容用于实际的例子中?OAuth 2十分友好,并容易部署,所有交互都是关于客户端和权限范围的。

  • OAuth 2中的客户端权限范围和我们平时的用户和权限是否相同?

  • 我需要将授权映射到权限范围中或将用户映射到客户端中?

  • 为什么我需要客户端?

你也许在之前在类似的企业级开发案例中尝试映射过相关的角色。这会很棘手!

任何类型的应用都提供用户登录,登录结果是一个Access Token,所有的之后的API调用都将这个Access Token加入HTTP请求头中,被调用服务去授权服务器验证Access Token并获取该Token可访问的权限信息。这样一来,所有服务的访问都会请求另外的服务来完成鉴权。

权限范围和角色,客户端和用户

OAuth 2中,你可以定义哪个应用(网站、移动客户端、桌面应用、其他)可以访问那些资源。这里只有一个尺寸,来自哪里的哪个用户可以访问那些数据,当然也是哪个应用或者服务可以访问那些资源。换一种说法,权限范围就是控制那些端点对客户端可见,或者用户根据他的权限来获取相关的数据。

在一个在线商店中,前端可以看做一个客户端,可以访问商品、订单和客户信息,但后端可以关于物流和合同等,另一方面,用户可以访问一个服务但并不是全部的数据,这可以是因为用户正在使用Web应用,当他不能的时候,其他用户却可以。服务之间的访问时我们要讨论的另一个维度。如果你熟悉数学,我可以说在OAuth 2中,客户端-权限范围关系是线性独立于用户-权限关系。

为什么是JWT

OAuth 2并不关心去哪找Access Token和把它存在什么地方的,生成随机字符串并保存Token相关的数据到这些字符串中保存好。通过一个令牌端点,其他服务可能会关心这个Token是否有效,它可以通过哪些权限。这就是用户信息URL方法,授权服务器为了获取用户信息转换为资源服务器。

当我们谈及微服务时,我们需要找一个Token存储的方式,来保证授权服务器可以被水平扩展,尽管这是一个很复杂的任务。所有访问微服务资源的请求都在Http Header中携带Token,被访问的服务接下来再去请求授权服务器验证Token的有效性,目前这种方式,我们需要两次或者更多次的请求,但这是为了安全性也没什么其他办法。但扩展Token存储会很大影响我们系统的可扩展性,这是我们引入JWT(读jot)的原因。

+-----------+                                     +-------------+
|           |       1-Request Authorization       |             |
|           |------------------------------------>|             |
|           |     grant_type&username&password    |             |--+
|           |                                     |Authorization|  | 2-Gen
|  Client   |                                     |Service      |  |   JWT
|           |       3-Response Authorization      |             |<-+
|           |<------------------------------------| Private Key |
|           |    access_token / refresh_token     |             |
|           |    token_type / expire_in / jti     |             |
+-----------+                                     +-------------+

简短来说,响应一个用户请求时,将用户信息和授权范围序列化后放入一个JSON字符串,然后使用Base64进行编码,最终在授权服务器用私钥对这个字符串进行签名,得到一个JSON Web Token,我们可以像使用Access Token一样的直接使用它,假设其他所有的资源服务器都将持有一个RSA公钥。当资源服务器接收到这个在Http Header中存有Token的请求,资源服务器就可以拿到这个Token,并验证它是否使用正确的私钥签名(是否经过授权服务器签名,也就是验签)。验签通过,反序列化后就拿到OAuth 2的验证信息。

验证服务器返回的信息可以是以下内容:

  • access_token - 访问令牌,用于资源访问

  • refresh_token - 当访问令牌失效,使用这个令牌重新获取访问令牌

  • token_type - 令牌类型,这里是Bearer也就是基本HTTP认证

  • expire_in - 过期时间

  • jti - JWT ID

由于Access TokenBase64编码,反编码后就是下面的格式,标准的JWT格式。也就是HeaderPayloadSignature三部分。

{ 
  "alg":"RS256",
  "typ":"JWT"
}
{
  "exp": 1492873315,
  "user_name": "reader",
  "authorities": [
    "AURH_READ"
  ],
  "jti": "8f2d40eb-0d75-44df-a8cc-8c37320e3548",
  "client_id": "web_app",
  "scope": [
    "FOO"
  ]
}
&:lƧs)ۡ-[+
F"2"Kآ8ۓٞ:u9ٴ̯ޡ 9Q32Zƌ޿$ec{3mxJh0DF庖[!뀭N)㥔knVVĖV|夻ׄE㍫}Ŝf9>'<蕱굤Bۋеϵov虀DӨ8C4K}Emޢ    YVcaqIW&*uʝub!׏*Ť\՟-{ʖX܌WTq

使用JWT可以简单的传输Token,用RSA签名保证Token很难被伪造。Access Token字符串中包含用户信息和权限范围,我们所需的全部信息都有了,所以不需要维护Token存储,资源服务器也不必要求Token检查。

+-----------+                                    +-----------+
|           |       1-Request Resource           |           |
|           |----------------------------------->|           |
|           | Authorization: bearer Access Token |           |--+
|           |                                    | Resource  |  | 2-Verify
|  Client   |                                    | Service   |  |  Token
|           |       3-Response Resource          |           |<-+
|           |<-----------------------------------| Public Key|
|           |                                    |           |
+-----------+                                    +-----------+

所以,在微服务中使用OAuth 2,不会影响到整体架构的可扩展性。淡然这里还有一些问题没有涉及,例如Access Token过期后,使用Refresh Token到认证服务器重新获取Access Token等,后面会有具体的例子来展开讨论这些问题。

如果您感兴趣,后面还会有实现部分,敬请期待~

由于 http://asciiflow.com/ 流程图使用中文就无法对齐了,本文中流程图都是英文了~

查看原文

赞 63 收藏 270 评论 37

山河永寂 发布了文章 · 2017-04-29

kubernetes 实战[1]

转载自笔者的博客

kubernetes 容器平台分析

Docker 容器算是目前最火的云计算产品了,因为它解决了很多运维和开发上的痛点问题,比如抹平了开发和生产的环境区别,甚至可以做到在生产环境使用 RHEL,而开发使用 Ubuntu,也能平滑部署,但是想要真正的将其投放到生产环境中,实际上还有很多问题亟待解决。而 kubernetes 就是这样一个 Best Practise

生产环境容器化的需求

脱离了业务环境的架构都是耍流氓,想要将容器真正落地,就需要真正分析其需求,这里就整理了一下容器化平台的需求

  1. 存储

  2. 网络

  3. 容器编排和服务发现

  4. 负载均衡

  5. 日志收集

  6. 认证授权

  7. 资源配额

  8. 分布式服务

可以看到,真正想要部署一个容器平台实际上需要解决的问题是十分繁多的,Docker 只是解决了最根本的容器分发和运行,但是 kubernetes 却解决了上面的大部分问题,这里就一一讲述一下。

存储

容器都是无状态的,很容易就被杀死,然后重新启动,因此存储是重中之重,Docker 自身提供了名为数据卷的存储方案,但是实际上没什么用,因为它支持的最好的就是本地存储,挂在本地路径,这样的强依赖是不可能做到生产环境的分布式服务的,除非每一台容器节点都持有一份同样的文件存储,但是这样会大大的消耗存储空间,而且 Docker 在文件权限管理等方面也有很多问题。kubernetes 提出的方案是通过持久化存储链,实际上就是做了个抽象层插件,各方可以开发自己的插件用于支持各类存储工具,目前已经支持 ceph、glusterfs 等主流的分布式文件系统了,但是这些分布式文件系统在部署上都很麻烦,甚至有性能的损失,因此使用 nas 作为存储是最便捷性能最好的。

网络

网络也是容器平台的重点问题,因为不可能依靠一个容器提供所有的服务,比如一个容器提供了数据库和 web 服务,而且这样也不利于解耦,因此容器之间需要通过网络通信,用过 Docker 自身网络的都知道,Docker 实际上是通过网桥来实现容器之间网络通信的,默认设置是无法做到跨节点网络,但是可以通过设置 flannel 产生一个 SDN,然后 Docker 使用此网络作为容器的网络,这样便能做到跨界点通信,kubernetes 则定义了一套 CNI 标准,只要符合这套标准就能让 kubernetes 使用 SDN,而目前来说已经有很多软件定义网络实现了这套协议,比如 flannel、weave,不过目前而言最好用的还是 flannel。

容器编排和服务发现

容器之间存在依赖必然需要编排功能,这也是 kubernetes 重点解决的问题,在容器编排上,kubernetes 有 pod、controller 概念,pod 可以认为是抽象的容器,它有可变的 ip,并且容易被杀死重启。controller 则是控制 pod 运作的控制器,replication controller、deployment、statefulsets、daemon sets,各有各的用法,比如 deployment 能够做到水平伸缩。在服务发现上面,kubernetes 有 pod、service 概念,看到这里相信大家都会有疑问,pod ip 可变总不能每次都要手动改程序或者配置才能访问吧,实际上 kubernetes 提供的 service 就是用来解决这个问题的,service 是一套虚拟的 ip,service 通过 selector 选择器挑选出自己身后的 pod,这样就能做到提供稳固的 ip 接口,其他容器无需关心数据库的 ip,只需要关心数据库这个 service 就可以。service 本身的 ip 则不是通过 SDN 产生的,而是通过 iptables 导流,将其导入到实际的 pod 中,但是这样子依旧存在一个问题,就是 service 的 ip 同样需要提前知道,这时候就需要服务发现出马了,kubernetes 自身提供了一套简单好用的 DNS 发现机制,kube-dns 将 service 注册到 dns 中,通过 dns 就能解析得到 service 对应的 ip。

负载均衡

负载均衡实际上就是需要一个前端负载均衡器,将流量统一导入到不同的容器中,kubernetes 的方案是通过 ingress 定义规则,然后根据规则产生模板配置,就比如 nginx 作为负载均衡器,产生配置后 reload 就能生效,但是目前官方提供的 ingress controller 容器镜像存在着一个问题,当 ingress 定义一个 TCP/UDP 四层负载均衡转发的时候,nginx 容器则必须修改容器部署,因为需要绑定主机端口。因此目前最好的方案还是通过云服务器提供商的负载均衡器,比如 GCE、AWS 提供的负载均衡器。

日志收集

日志收集实际上不光是收集容器通过标准输出标准错误产生的日志,还需要收集容器运行时信息,比如内存、cpu 占用等信息,这里不细讲了,因为无论是 Docker 还是 kubernetes 都提供了收集方案,不过 kubernetes 更加灵活好用,并且收集的信息更全面,连容器节点的信息都能收集

认证授权

kuberentes 有几大组件 apiserver、controller manager、scheduler、proxy、kubelet,所有的组件都通过 apiserver 通信和管理,因此需要通过认证和授权来防止非法操作,在这上面 kubernetes 提供了很多方案,比如 basic auth、bearar token、keystone 等,但是真正能投入生产环境使用的,只有 OpenID Connector,不知道这种认证授权的可以自行谷歌,更糟的是,官方甚至没有提供部署方案,需要自行研发 OpenID Connector 服务器并且部署下去。CoreOS 倒是开源了一套 dex 系统,但是这玩意实际上也不靠谱,照样需要研发力量的支持,从这上面就决定了 kubernetes 高门槛的准入标准。

资源配额

kubernetes 资源配额方案非常丰富,无论是存储配额还是内存甚至是 cpu 限额,都可以通过 yaml 文件定义

分布式服务

分布式服务是大规模运行容器平台的关键,因为容器平台必然是部署在多节点上的,而 kubernetes 天生就是为分布式部署开发的,apiserver、controller manager、scheduler 实际上就是主节点,而 proxy 和 kubelet 则是每个 slave 节点都需要的。不过目前主流的做法包括 kubeadm 半自动部署方案都是让主节点通过 kubelet 在容器中运行 apiserver 等东西。

总结

想要在生产环境大规模应用容器化技术,看似开源了产品,但是 kubernetes 本质上是一个半成品,甚至连自动化部署方案都不成熟,并且需要研发力量的支持才能真正运行起来,以笔者个人的意见来说,kubernetes 实际上并非是一个面向企业终端用户的产品,而是一个面向云计算厂商的半成品,它真正的用法应当是云计算公司提供自动化节点部署方案,云计算平台提供 SDN 、负载均衡和分布式存储,甚至可能的话,让云计算厂商提供一套管理控制 web 界面,并且做好认证授权系统,kube-dashboard 这个官方提供的 web ui 界面实际上功能是全了,但是认证授权功能的缺失则导致普通用户很难部署使用,或者说 kube-dashboard 本来就不是为了普通用户部署而开发的,而是为了提供给厂商做二次开发准备。kube-dashboard 则是负责定义 web 管理界面应该有哪些功能。

查看原文

赞 3 收藏 6 评论 0

山河永寂 赞了文章 · 2017-04-29

使用JWT保护你的Spring Boot应用 - Spring Security实战

使用JWT保护你的Spring Boot应用 - Spring Security实战

作者 freewolf

原创文章转载请标明出处

关键词

Spring BootOAuth 2.0JWTSpring SecuritySSOUAA

写在前面

最近安静下来,重新学习一些东西,最近一年几乎没写过代码。整天疲于奔命的日子终于结束了。坐下来,弄杯咖啡,思考一些问题,挺好。这几天有人问我Spring Boot结合Spring Security实现OAuth认证的问题,写了个Demo,顺便分享下。Spring 2之后就没再用过Java,主要是xml太麻烦,就投入了Node.js的怀抱,现在Java倒是好过之前很多,无论是执行效率还是其他什么。感谢Pivotal团队在Spring boot上的努力,感谢Josh Long,一个有意思的攻城狮。

我又搞Java也是为了去折腾微服务,因为目前看国内就Java程序猿最好找,虽然水平好的难找,但是至少能找到,不像其他编程语言,找个会世界上最好的编程语言PHP的人真的不易。

Spring Boot

有了Spring Boot这样的神器,可以很简单的使用强大的Spring框架。你需要关心的事儿只是创建应用,不必再配置了,“Just run!”,这可是Josh Long每次演讲必说的,他的另一句必须说的就是“make jar not war”,这意味着,不用太关心是Tomcat还是Jetty或者Undertow了。专心解决逻辑问题,这当然是个好事儿,部署简单了很多。

创建Spring Boot应用

有很多方法去创建Spring Boot项目,官方也推荐用:

start.spring.io可以方便选择你要用的组件,命令行工具当然也可以。目前Spring Boot已经到了1.53,我是懒得去更新依赖,继续用1.52版本。虽然阿里也有了中央库的国内版本不知道是否稳定。如果你感兴趣,可以自己尝试下。你可以选Maven或者Gradle成为你项目的构建工具,Gradle优雅一些,使用了Groovy语言进行描述。

打开start.spring.io,创建的项目只需要一个Dependency,也就是Web,然后下载项目,用IntellJ IDEA打开。我的Java版本是1.8。

这里看下整个项目的pom.xml文件中的依赖部分:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

所有Spring Boot相关的依赖都是以starter形式出现,这样你无需关心版本和相关的依赖,所以这样大大简化了开发过程。

当你在pom文件中集成了spring-boot-maven-plugin插件后你可以使用Maven相关的命令来run你的应用。例如mvn spring-boot:run,这样会启动一个嵌入式的Tomcat,并运行在8080端口,直接访问你当然会获得一个Whitelabel Error Page,这说明Tomcat已经启动了。

创建一个Web 应用

这还是一篇关于Web安全的文章,但是也得先有个简单的HTTP请求响应。我们先弄一个可以返回JSON的Controller。修改程序的入口文件:

@SpringBootApplication
@RestController
@EnableAutoConfiguration
public class DemoApplication {

    // main函数,Spring Boot程序入口
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    // 根目录映射 Get访问方式 直接返回一个字符串
    @RequestMapping("/")
    Map<String, String> hello() {
      // 返回map会变成JSON key value方式
      Map<String,String> map=new HashMap<String,String>();
      map.put("content", "hello freewolf~");
      return map;
    }
}

这里我尽量的写清楚,让不了解Spring Security的人通过这个例子可以了解这个东西,很多人都觉得它很复杂,而投向了Apache Shiro,其实这个并不难懂。知道主要的处理流程,和这个流程中哪些类都起了哪些作用就好了。

Spring Boot对于开发人员最大的好处在于可以对Spring应用进行自动配置。Spring Boot会根据应用中声明的第三方依赖来自动配置Spring框架,而不需要进行显式的声明。Spring Boot推荐采用基于Java注解的配置方式,而不是传统的XML。只需要在主配置 Java 类上添加@EnableAutoConfiguration注解就可以启用自动配置。Spring Boot的自动配置功能是没有侵入性的,只是作为一种基本的默认实现。

这个入口类我们添加@RestController@EnableAutoConfiguration两个注解。
@RestController注解相当于@ResponseBody@Controller合在一起的作用。

run整个项目。访问http://localhost:8080/就能看到这个JSON的输出。使用Chrome浏览器可以装JSON Formatter这个插件,显示更PL一些。

{
  "content": "hello freewolf~"
}

为了显示统一的JSON返回,这里建立一个JSONResult类进行,简单的处理。首先修改pom.xml,加入org.json相关依赖。

<dependency>
    <groupId>org.json</groupId>
    <artifactId>json</artifactId>
</dependency>

然后在我们的代码中加入一个新的类,里面只有一个结果集处理方法,因为只是个Demo,所有这里都放在一个文件中。这个类只是让返回的JSON结果变为三部分:

  • status - 返回状态码 0 代表正常返回,其他都是错误

  • message - 一般显示错误信息

  • result - 结果集

class JSONResult{
    public static String fillResultString(Integer status, String message, Object result){
        JSONObject jsonObject = new JSONObject(){{
            put("status", status);
            put("message", message);
            put("result", result);
        }};
        return jsonObject.toString();
    }
}

然后我们引入一个新的@RestController并返回一些简单的结果,后面我们将对这些内容进行访问控制,这里用到了上面的结果集处理类。这里多放两个方法,后面我们来测试权限和角色的验证用。

@RestController
class UserController {

    // 路由映射到/users
    @RequestMapping(value = "/users", produces="application/json;charset=UTF-8")
    public String usersList() {

        ArrayList<String> users =  new ArrayList<String>(){{
            add("freewolf");
            add("tom");
            add("jerry");
        }};

        return JSONResult.fillResultString(0, "", users);
    }

    @RequestMapping(value = "/hello", produces="application/json;charset=UTF-8")
    public String hello() {
        ArrayList<String> users =  new ArrayList<String>(){{ add("hello"); }};
        return JSONResult.fillResultString(0, "", users);
    }

    @RequestMapping(value = "/world", produces="application/json;charset=UTF-8")
    public String world() {
        ArrayList<String> users =  new ArrayList<String>(){{ add("world"); }};
        return JSONResult.fillResultString(0, "", users);
    }
}

重新run这个文件,访问http://localhost:8080/users就看到了下面的结果:

{
  "result": [
    "freewolf",
    "tom",
    "jerry"
  ],
  "message": "",
  "status": 0
}

如果你细心,你会发现这里的JSON返回时,Chrome的格式化插件好像并没有识别?这是为什么呢?我们借助curl分别看一下我们写的两个方法的Header信息.

curl -I http://127.0.0.1:8080/
curl -I http://127.0.0.1:8080/users

可以看到第一个方法hello,由于返回值是Map<String, String>,Spring已经有相关的机制自动处理成JSON:

Content-Type: application/json;charset=UTF-8

第二个方法usersList由于返回时String,由于是@RestControler已经含有了@ResponseBody也就是直接返回内容,并不模板。所以就是:

Content-Type: text/plain;charset=UTF-8

那怎么才能让它变成JSON呢,其实也很简单只需要补充一下相关注解:

@RequestMapping(value = "/users", produces="application/json;charset=UTF-8")

这样就好了。

使用JWT保护你的Spring Boot应用

终于我们开始介绍正题,这里我们会对/users进行访问控制,先通过申请一个JWT(JSON Web Token读jot),然后通过这个访问/users,才能拿到数据。

关于JWT,出门奔向以下内容,这些不在本文讨论范围内:

JWT很大程度上还是个新技术,通过使用HMAC(Hash-based Message Authentication Code)计算信息摘要,也可以用RSA公私钥中的私钥进行签名。这个根据业务场景进行选择。

添加Spring Security

根据上文我们说过我们要对/users进行访问控制,让用户在/login进行登录并获得Token。这里我们需要将spring-boot-starter-security加入pom.xml。加入后,我们的Spring Boot项目将需要提供身份验证,相关的pom.xml如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.7.0</version>
</dependency>

至此我们之前所有的路由都需要身份验证。我们将引入一个安全设置类WebSecurityConfig,这个类需要从WebSecurityConfigurerAdapter类继承。

@Configuration
@EnableWebSecurity
class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // 设置 HTTP 验证规则
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 关闭csrf验证
        http.csrf().disable()
                // 对请求进行认证
                .authorizeRequests()
                // 所有 / 的所有请求 都放行
                .antMatchers("/").permitAll()
                // 所有 /login 的POST请求 都放行
                .antMatchers(HttpMethod.POST, "/login").permitAll()
                // 权限检查
                .antMatchers("/hello").hasAuthority("AUTH_WRITE")
                // 角色检查
                .antMatchers("/world").hasRole("ADMIN")
                // 所有请求需要身份认证
                .anyRequest().authenticated()
            .and()
                // 添加一个过滤器 所有访问 /login 的请求交给 JWTLoginFilter 来处理 这个类处理所有的JWT相关内容
                .addFilterBefore(new JWTLoginFilter("/login", authenticationManager()),
                        UsernamePasswordAuthenticationFilter.class)
                // 添加一个过滤器验证其他请求的Token是否合法
                .addFilterBefore(new JWTAuthenticationFilter(),
                        UsernamePasswordAuthenticationFilter.class);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 使用自定义身份验证组件
        auth.authenticationProvider(new CustomAuthenticationProvider());

    }
}

先放两个基本类,一个负责存储用户名密码,另一个是一个权限类型,负责存储权限和角色。

class AccountCredentials {

    private String username;
    private String password;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

class GrantedAuthorityImpl implements GrantedAuthority{
    private String authority;

    public GrantedAuthorityImpl(String authority) {
        this.authority = authority;
    }

    public void setAuthority(String authority) {
        this.authority = authority;
    }

    @Override
    public String getAuthority() {
        return this.authority;
    }
}

在上面的安全设置类中,我们设置所有人都能访问/POST方式访问/login,其他的任何路由都需要进行认证。然后将所有访问/login的请求,都交给JWTLoginFilter过滤器来处理。稍后我们会创建这个过滤器和其他这里需要的JWTAuthenticationFilterCustomAuthenticationProvider两个类。

先建立一个JWT生成,和验签的类

class TokenAuthenticationService {
    static final long EXPIRATIONTIME = 432_000_000;     // 5天
    static final String SECRET = "P@ssw02d";            // JWT密码
    static final String TOKEN_PREFIX = "Bearer";        // Token前缀
    static final String HEADER_STRING = "Authorization";// 存放Token的Header Key

  // JWT生成方法
    static void addAuthentication(HttpServletResponse response, String username) {

    // 生成JWT
        String JWT = Jwts.builder()
                // 保存权限(角色)
                .claim("authorities", "ROLE_ADMIN,AUTH_WRITE")
                // 用户名写入标题
                .setSubject(username)
                // 有效期设置
                        .setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME))
                // 签名设置
                        .signWith(SignatureAlgorithm.HS512, SECRET)
                        .compact();

        // 将 JWT 写入 body
        try {
            response.setContentType("application/json");
            response.setStatus(HttpServletResponse.SC_OK);
            response.getOutputStream().println(JSONResult.fillResultString(0, "", JWT));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

  // JWT验证方法
    static Authentication getAuthentication(HttpServletRequest request) {
        // 从Header中拿到token
        String token = request.getHeader(HEADER_STRING);

        if (token != null) {
            // 解析 Token
            Claims claims = Jwts.parser()
                    // 验签
                    .setSigningKey(SECRET)
                    // 去掉 Bearer
                    .parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
                    .getBody();

            // 拿用户名
            String user = claims.getSubject();

            // 得到 权限(角色)
            List<GrantedAuthority> authorities =  AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get("authorities"));

            // 返回验证令牌
            return user != null ?
                    new UsernamePasswordAuthenticationToken(user, null, authorities) :
                    null;
        }
        return null;
    }
}

这个类就两个static方法,一个负责生成JWT,一个负责认证JWT最后生成验证令牌。注释已经写得很清楚了,这里不多说了。

下面来看自定义验证组件,这里简单写了,这个类就是提供密码验证功能,在实际使用时换成自己相应的验证逻辑,从数据库中取出、比对、赋予用户相应权限。

// 自定义身份认证验证组件
class CustomAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 获取认证的用户名 & 密码
        String name = authentication.getName();
        String password = authentication.getCredentials().toString();

        // 认证逻辑
        if (name.equals("admin") && password.equals("123456")) {

            // 这里设置权限和角色
            ArrayList<GrantedAuthority> authorities = new ArrayList<>();
            authorities.add( new GrantedAuthorityImpl("ROLE_ADMIN") );
            authorities.add( new GrantedAuthorityImpl("AUTH_WRITE") );
            // 生成令牌
            Authentication auth = new UsernamePasswordAuthenticationToken(name, password, authorities);
            return auth;
        }else {
            throw new BadCredentialsException("密码错误~");
        }
    }

    // 是否可以提供输入类型的认证服务
    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

下面实现JWTLoginFilter 这个Filter比较简单,除了构造函数需要重写三个方法。

  • attemptAuthentication - 登录时需要验证时候调用

  • successfulAuthentication - 验证成功后调用

  • unsuccessfulAuthentication - 验证失败后调用,这里直接灌入500错误返回,由于同一JSON返回,HTTP就都返回200了

class JWTLoginFilter extends AbstractAuthenticationProcessingFilter {

    public JWTLoginFilter(String url, AuthenticationManager authManager) {
        super(new AntPathRequestMatcher(url));
        setAuthenticationManager(authManager);
    }

    @Override
    public Authentication attemptAuthentication(
            HttpServletRequest req, HttpServletResponse res)
            throws AuthenticationException, IOException, ServletException {

        // JSON反序列化成 AccountCredentials
        AccountCredentials creds = new ObjectMapper().readValue(req.getInputStream(), AccountCredentials.class);

        // 返回一个验证令牌
        return getAuthenticationManager().authenticate(
                new UsernamePasswordAuthenticationToken(
                        creds.getUsername(),
                        creds.getPassword()
                )
        );
    }

    @Override
    protected void successfulAuthentication(
            HttpServletRequest req,
            HttpServletResponse res, FilterChain chain,
            Authentication auth) throws IOException, ServletException {

        TokenAuthenticationService.addAuthentication(res, auth.getName());
    }


    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {

        response.setContentType("application/json");
        response.setStatus(HttpServletResponse.SC_OK);
        response.getOutputStream().println(JSONResult.fillResultString(500, "Internal Server Error!!!", JSONObject.NULL));
    }
}

再完成最后一个类JWTAuthenticationFilter,这也是个拦截器,它拦截所有需要JWT的请求,然后调用TokenAuthenticationService类的静态方法去做JWT验证。

class JWTAuthenticationFilter extends GenericFilterBean {

    @Override
    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain filterChain)
            throws IOException, ServletException {
        Authentication authentication = TokenAuthenticationService
                .getAuthentication((HttpServletRequest)request);

        SecurityContextHolder.getContext()
                .setAuthentication(authentication);
        filterChain.doFilter(request,response);
    }
}

现在代码就写完了,整个Spring Security结合JWT基本就差不多了,下面我们来测试下,并说下整体流程。

开始测试,先运行整个项目,这里介绍下过程:

  • 先程序启动 - main函数

  • 注册验证组件 - WebSecurityConfigconfigure(AuthenticationManagerBuilder auth)方法,这里我们注册了自定义验证组件

  • 设置验证规则 - WebSecurityConfigconfigure(HttpSecurity http)方法,这里设置了各种路由访问规则

  • 初始化过滤组件 - JWTLoginFilterJWTAuthenticationFilter 类会初始化

首先测试获取Token,这里使用CURL命令行工具来测试。

curl -H "Content-Type: application/json" -X POST -d '{"username":"admin","password":"123456"}'  http://127.0.0.1:8080/login

结果:

{
  "result": "eyJhbGciOiJIUzUxMiJ9.eyJhdXRob3JpdGllcyI6IlJPTEVfQURNSU4sQVVUSF9XUklURSIsInN1YiI6ImFkbWluIiwiZXhwIjoxNDkzNzgyMjQwfQ.HNfV1CU2CdAnBTH682C5-KOfr2P71xr9PYLaLpDVhOw8KWWSJ0lBo0BCq4LoNwsK_Y3-W3avgbJb0jW9FNYDRQ",
  "message": "",
  "status": 0
}

这里我们得到了相关的JWT,反Base64之后,就是下面的内容,标准JWT

{"alg":"HS512"}{"authorities":"ROLE_ADMIN,AUTH_WRITE","sub":"admin","exp":1493782240}ͽ]BS`pS6~hCVH%
ܬ)֝ଖoE5р

整个过程如下:

  • 拿到传入JSON,解析用户名密码 - JWTLoginFilterattemptAuthentication 方法

  • 自定义身份认证验证组件,进行身份认证 - CustomAuthenticationProviderauthenticate 方法

  • 盐城成功 - JWTLoginFiltersuccessfulAuthentication 方法

  • 生成JWT - TokenAuthenticationServiceaddAuthentication方法

再测试一个访问资源的:

curl -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJhdXRob3JpdGllcyI6IlJPTEVfQURNSU4sQVVUSF9XUklURSIsInN1YiI6ImFkbWluIiwiZXhwIjoxNDkzNzgyMjQwfQ.HNfV1CU2CdAnBTH682C5-KOfr2P71xr9PYLaLpDVhOw8KWWSJ0lBo0BCq4LoNwsK_Y3-W3avgbJb0jW9FNYDRQ"  http://127.0.0.1:8080/users

结果:

{
  "result":["freewolf","tom","jerry"],
  "message":"",
  "status":0
}

说明我们的Token生效可以正常访问。其他的结果您可以自己去测试。再回到处理流程:

  • 接到请求进行拦截 - JWTAuthenticationFilter 中的方法

  • 验证JWT - TokenAuthenticationServicegetAuthentication 方法

  • 访问Controller

这样本文的主要流程就结束了,本文主要介绍了,如何用Spring Security结合JWT保护你的Spring Boot应用。如何使用RoleAuthority,这里多说一句其实在Spring Security中,对于GrantedAuthority接口实现类来说是不区分是Role还是Authority,二者区别就是如果是hasAuthority判断,就是判断整个字符串,判断hasRole时,系统自动加上ROLE_到判断的Role字符串上,也就是说hasRole("CREATE")hasAuthority('ROLE_CREATE')是相同的。利用这些可以搭建完整的RBAC体系。本文到此,你已经会用了本文介绍的知识点。

代码整理后我会上传到Github

本文代码

https://github.com/freew01f/s...

参考资源

查看原文

赞 52 收藏 112 评论 63

山河永寂 发布了文章 · 2017-01-20

iOS 只使用私钥生成证书请求文件

原文来自静雅斋,转载请注明出处。

0x00

很多时候开发者经常忘记备份私钥公钥,私钥如果没有保留,就无法让多个证书共享一个私钥,而公钥没有保留则会导致无法生成 CSR,不过我们可以通过私钥重新生成公钥。也可以使用 openssl 产生 CSR 请求文件。

0x01

  1. 从钥匙串导出私钥为 private.p12

  2. 使用 openssl 转换为 pem 格式并且直接根据私钥生成公钥,这里需要输入第一步的密码

> openssl pkcs12 -in private.p12 -nocerts -nodes | openssl rsa -pubout > public.pem
  1. 导入钥匙串,由于钥匙串存在一个 bug,不能直接导入 pem 格式的密钥,因此必须通过命令行导入

> security -v import public.pem -k ~/Library/Keychains/login.keychain

注意这里得指定 login.keychain,否则会导致找不到这个导入的密钥,当然这里也可以更换为其他的钥匙串

注意,这里也有一个坑,目前钥匙串存在一个 bug,用户不能对命令行导入的密钥修改名称,除非更改密钥访问控制权限,但是这又会导致另外一个问题,就是如果改变了导入的密钥,就会只能对这个导入的密钥做一次操作,比如导出密钥或者生成 CSR 请求文件,这一次操作过后就会令密钥报废,任何操作都会报错,因此,建议在生成 CSR 文件过后就删掉这个公钥。

0x02

  1. 一样先是导出 p12 文件

  2. 将其转换为 pem 格式并且据此生成 CSR 证书请求文件

openssl req -new -key <(openssl pkcs12 -in private.p12 -nocerts -nodes -passin pass:"") > new.certSigningRequest
  1. 根据提示符要求填入各项内容(实际上苹果不看这些内容,随便填)

查看原文

赞 0 收藏 0 评论 0

山河永寂 发布了文章 · 2017-01-08

Ghost 博客升级 node6.x 问题手记

原文来自静雅斋,转载请注明出处。

0x00 状态

笔者博客是用 Ghost + PostgreSQL 搭建的,最近官方出了 RoadMap 做了 LTS 支持,因此做了 Ghost 的升级,同时也顺手把 node 升级到了 6.x 版本,本以为小版本升级轻松无压力,结果重启 Ghost 的时候直接报错

ERROR: password authentication failed for user "ghost" 
 
 error: password authentication failed for user "ghost"
    at Connection.parseE (/home/ghost/node_modules/.4.1.1@pg/lib/connection.js:534:11)
    at Connection.parseMessage (/home/ghost/node_modules/.4.1.1@pg/lib/connection.js:361:17)
    at TLSSocket.<anonymous> (/home/ghost/node_modules/.4.1.1@pg/lib/connection.js:105:22)
    at emitOne (events.js:96:13)
    at TLSSocket.emit (events.js:188:7)
    at readableAddChunk (_stream_readable.js:176:18)
    at TLSSocket.Readable.push (_stream_readable.js:134:10)
    at TLSWrap.onread (net.js:548:20)

0x01 回滚版本

碰到升级出错,第一反应就是回滚版本,已经是习惯了,但是忘记之前的版本号了,只能尝试着回滚版本,但是发现所有版本都有这个问题,开始陷入僵局(笔者忘了 node 从 4.x 被升级到了 6.x)。所以只能暂时先将数据库从 PostgreSQL 迁移到了 MySQL

0x02 分析问题

从上面的错误信息来看是由于密码认证出错的问题导致的,但是实际上密码是正确的,PostgreSQL 支持 md5 和 password 两种方式认证,password 就是明文发送,笔者使用的是 ssl 加密并且使用 md5 认证,于是乎上服务器查 PostgreSQL 日志,但是并没有发现认证出错的情况,照理来说这里已经可以排除是数据库服务器的问题了,但是笔者又多做了一步,导致后面走了弯路。笔者将密码认证方式改为 password 明文发送,就 OK 了。结果就开始怀疑 pg 库和数据库的问题了。但是摸索了半天并没有发现问题所在,然后 google 了也没有找到类似的情况,一般来说,遇到问题就 google,80% 的问题都能解决,google 不到就去 github issue 上找,如果还找不到就只有两种情况,低级 bug 或者前无古人的 bug。

0x03 线索

过了一段时间,偶然去了 npm 上面找了一下 pg 依赖包,发现这个版本已经到了 6.1.2 版本了,而 Ghost 官方依赖的版本是 4.1.1,就开始怀疑是不是 pg 版本过低的原因了。与此同时,在 Ghost 官方博客上面也找到了一条声明,表示由于大家都用 MySQL,PostgreSQL 没人能提交意见,因此准备将 PostgreSQL 降级为“二等公民”。所以就前往 pg 库的 issue 上找了一下,还真找到了两条用户关于升级 node 后的问题。升级到最新版本的 pg 依赖就解决了问题。

0x04 源码

由于是升级 node6.x 带来的问题,应该是 API 上做了改动,最终发现是 Buffer 上出现的问题,源代码如下

//password request handling
con.on('authenticationMD5Password', checkPgPass(function(msg) {
  var inner = Client.md5(self.password + self.user);
  var outer = Client.md5(inner + msg.salt.toString('binary'));
  var md5password = "md5" + outer;
  con.password(md5password);
}));

改成如下代码就成功了

//password request handling
con.on('authenticationMD5Password', checkPgPass(function(msg) {
  var inner = Client.md5(self.password + self.user);
  var outer = Client.md5(Buffer.concat([new Buffer(inner), msg.salt]));
  var md5password = "md5" + outer;
  con.password(md5password);
}));

实际上也不算是 Buffer 的问题,而是 crypto 模块现在默认认为字符串为 utf8,而不是 binary,如果没有指定 encoding,就会出现这个问题。

0x05 解决方案

既然上游代码都已经修复了这个 bug,直接升级版本就行了,然后提一个 PR 到官方就 ok。

查看原文

赞 0 收藏 0 评论 0

山河永寂 赞了文章 · 2017-01-07

如何处理 Swift 中的异步错误

作者:Olivier Halligon,原文链接,原文日期:2016-02-06
译者:ray16897188;校对:小锅;定稿:numbbbbb

在之前的一篇文章中,我介绍了如何在Swift中使用throw做错误处理。但是如果你处理的是异步流程,throw 就无法胜任,该怎么办?

throw 和异步有啥问题?

回顾下,我们可以像下面这样,在一个可能失败的函数中使用 throw 关键字:


// 定义错误类型和一个可抛出的函数
enum ComputationError: ErrorType { case DivisionByZero }
func inverse(x: Float) throws -> Float {
  guard x != 0 else { throw ComputationError.DivisionByZero }
  return 1.0/x
}
// 调用它
do {
  let y = try inverse(5.0)
} catch {
  print("Woops: \(error)")
}

但如果函数是异步的,需要等待一段时间才会返回结果,比如带着 completion block 的函数,这个时候怎么办?


func fetchUser(completion: User? /* throws */ -> Void) /* throws */ {
  let url = …
  NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) -> Void in
//    if let error = error { throw error } // 我们不能这样做, fetchUser 不能“异步地抛出”
    let user = data.map { User(fromData: $0) }
    completion(user)
  }.resume()
}
// 调用
fetchUser() { (user: User?) in
  /* do something */
}

这种情况下如果请求失败的话,你怎么 throw

  • fetchUser 函数 throw 是不合理的,因为这个函数(被调用后)会立即返回,而网络错误只会在这之后发生。所以当错误发生时再throw 一个错误就太晚了,fetchUser 函数调用已经返回。

  • 你可能想把 completion 标成 throws?但是调用 completion(user) 的代码在 fetchUser 里,不是在调用 fetchUser 的代码里。所以接受并处理错误的代码必须是fetchUser 本身,而非 fetchUser 的调用点。所以这个方案也不行。?

攻克这道难题

可以曲线救国:让 completion 不直接返回 User?,而是返回一个 Void throws -> User 的 throwing 函数,这个 throwing 函数会返回一个 User(我们把这个函数命名为 UserBuilder)。这样我们就又能 throw 了。

之后当 completion 返回这个 userBuilder 函数时,我们用 try userBuilder() 去访问里面的 User... 或者让它 throw 出错误。


enum UserError: ErrorType { case NoData, ParsingError }
struct User {
  init(fromData: NSData) throws { /* … */ }
  /* … */
}

typealias UserBuilder = Void throws -> User
func fetchUser(completion: UserBuilder -> Void) {
  let url = …
  NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) -> Void in
    completion({ UserBuilder in
      if let error = error { throw error }
      guard let data = data else { throw UserError.NoData }
      return try User(fromData: data)
    })
  }.resume()
}

fetchUser { (userBuilder: UserBuilder) in
  do {
    let user = try userBuilder()
  } catch {
    print("Async error while fetching User: \(error)")
  }
}

这样 completion 就不会直接返回一个 User,而是返回一个 User... 或抛出错误。之后你就又可以做错误处理了。

但说实话,用 Void throws -> User 来代替 User? 并不是最优雅、可读性最强的解决方案。还有其他办法吗?

介绍 Result

回到 Swift 1.0 的时代,那时还没有 throw,人们得用一种函数式的方法来处理错误。由于 Swift 从函数式编程的世界中借鉴过来很多特性,所以当时人们在 Swift 中用 Result 模式来做错误处理还是很合理的。Result 长这样1


enum Result<T> {
  case Success(T)
  case Failure(ErrorType)
}

Result 这个类型其实很简单:它要么指代一次成功 —— 附着一个关联值(associated value)代表着成功的结果 —— 要么指代一次失败 —— 有一个关联的错误。它是对可能会失败的操作的完美抽象。

那么我们怎么用它?创建一个 Result.Success 或者一个 Result.Failure,然后把作为结果的 Result2 传入 completion,最后调用 completion


func fetchUser(completion: Result<User> -> Void) {
  let url = …
  NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) -> Void in
    if let error = error {
      return completion( Result.Failure(error) )
    }
    guard let data = data else {
      return completion( Result.Failure(UserError.NoData) )
    }
    do {
      let user = try User(fromData: data)
      completion( Result.Success(user) )
    } catch {
      completion( Result.Failure(error) )
    }
  }.resume()
}

还记得 monads 么?

Result 的好处就是它可以变成一个 Monad。记得Monads么?这意味着我们可以给 Result 添加高阶的 mapflatMap 方法,后两者会接受一个 f: T->U 或者 f: T->Result<U> 类型的闭包,然后返回一个 Result<U>

如果一开始的 Result 是一个 .Success(let t),那就对这个 t 使用这个闭包,得到 f(t) 的结果。如果是一个 .Failure,那就把这个错误继续传下去:


extension Result {
  func map<U>(f: T->U) -> Result<U> {
    switch self {
    case .Success(let t): return .Success(f(t))
    case .Failure(let err): return .Failure(err)
    }
  }
  func flatMap<U>(f: T->Result<U>) -> Result<U> {
    switch self {
    case .Success(let t): return f(t)
    case .Failure(let err): return .Failure(err)
    }
  }
}

如果想要了解更多信息,我建议你去重读我写的关于 Monads 的文章,但现在长话短说,我们来修改代码:


func readFile(file: String) -> Result<NSData> { … }
func toJSON(data: NSData) -> Result<NSDictionary> { … }
func extractUserDict(dict: NSDictionary) -> Result<NSDictionary> { … }
func buildUser(userDict: NSDictionary) -> Result<User> { … }

let userResult = readFile("me.json")
  .flatMap(toJSON)
  .flatMap(extractUserDict)
  .flatMap(buildUser)

上面代码中最酷的地方:如果其中一个方法(比如 toJSON)失败了,返回了一个 .Failure,那随后这个 failure 会一直被传递到最后,而且不会被传入到 extractUserDictbuildUser 方法里面去。

这就可以让错误“走一条捷径”:和 do...catch 一样,你可以在链条的结尾一并处理所有错误,而不是在每个中间阶段做处理,很酷,不是么?

Resultthrow,再从 throwResult

问题是,Result 不包含在 Swift 标准库中,而无论怎样,还是有很多函数使用 throw 来报告同步错误(译注:synchronous errors,与异步错误 asynchronous errors 相对)。比如,在实际应用场景中从一个 NSDictionary 建立一个 User,我们可能得用 init(dict: NSDictionary) throws 构造器,而不是 NSDictionary -> Result<User> 函数。

那怎么去融合这两个世界呢?简单,我们来扩展一下 Result3


extension Result {
  // 如果是 .Success 就直接返回值,如果是 .Failure 抛出错误
  func resolve() throws -> T {
    switch self {
    case Result.Success(let value): return value
    case Result.Failure(let error): throw error
    }
  }

  // 如果表达式返回值则构建一个 .Success,否则就构建一个 .Failure
  init(@noescape _ throwingExpr: Void throws -> T) {
    do {
      let value = try throwingExpr()
      self = Result.Success(value)
    } catch {
      self = Result.Failure(error)
    }
  }
}

现在我们就可以很轻松地将 throwing 构造器转换成一个闭包,该闭包返回一个 Result


func buildUser(userDict: NSDictionary) -> Result<User> {
  // 这里我们调用了 `init` 并使用一个可抛出的尾闭包来构建 `Result`
  return Result { try User(dictionary: userDict) }
}

之后如果我们将 NSURLSession 封装到一个函数中,这个函数就会异步的返回一个 Result,我们可以按个人喜好来调整这两个世界的平衡,例如:


func fetch(url: NSURL, completion: Result<NSData> -> Void) {
  NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) -> Void in
    completion(Result {
      if let error = error { throw error }
      guard let data = data else { throw UserError.NoData }
      return data
    })
  }.resume()
}

上面的代码也调用了 completion block,往里面传了一个由 throwing closure4 创建的 Result 对象。

随后我们就可以用 flatMap 把这些都串起来,再根据实际需求决定是否进入 do...catch 的世界:


fetch(someURL) { (resultData: Result<NSData>) in
  let resultUser = resultData
    .flatMap(toJSON)
    .flatMap(extractUserDict)
    .flatMap(buildUser)

  // 如果我们想在剩下代码中回到 do/try/catch 的世界
  do {
    let user = try resultUser.resolve()
    updateUI(user: user)
  } catch {
    print("Error: \(error)")
  }
}

我承诺,这就是未来

(校对注:作者这里的标题使用了双关语,承诺的英文为 "Promise", 未来的单词为 "Future"。)(定稿注:这篇文章提到的这种模式术语就是 "Promise",因此说是双关。)

Result 很炫酷,但是既然它们的主要用途是异步函数(因为同步函数我们已经有了 throw),那何不让它也实现对异步的管理呢?

实际上已经有一个这样的类型TM,它就是 Promise(有时候也叫 Future,这两个术语很像)。

Promise 类型结合了 Result 类型(能成功或者失败)和异步性。一个 Promise<T> 既可以在一段时间后(体现了异步方面的特性)被成功T 类型的值(译注:这里的赋值英文是 fulfill,原意是履行,而 Promise 本身也有承诺的意思。Promise<T>被成功赋值,等同于承诺被履行),又可能在错误发生时被拒绝(reject)

一个 Promise 也是一个 monad。但和通常以 mapflatMap 的名字来调用它的 monadic 函数不同,按规定这两个函数都通过 then 来调用:


class Promise<T> {
  // 与 map 对应的 monad,在 Promise 通常被称为 then
  func then(f: T->U) -> Promise<U>
  // 与 flatMap 对应的 monad,在 Promise 中也被称为 then 
  func then(f: T->Promise<U>) -> Promise<U>
}

错误也通过 .error.recover 解包。在代码中,它的使用方式和你使用一个 Result 基本相同,毕竟它俩都是 monad:


fetch(someURL) // returns a `Promise<NSData>`
  .then(toJSON) // assuming toJSON is now a `NSData -> Promise<NSDictionary>`
  .then(extractUserDict) // assuming extractUserDict is now a `NSDictionary -> Promise<NSDictionary>`
  .then(buildUser) // assuming buildUser is now a `NSDictionary -> Promise<User>`
  .then {
    updateUI(user: user)
  }
  .error { err in
    print("Error: \(err)")
  }

感受到了吗,这看起来多么流畅多么优雅!这就是把一些微处理步骤精密连接起来的流(stream),而且它还替你做了异步处理和错误处理这样的脏活儿累活儿。如果在处理流程中有错误发生,比如在 extractUserDict 中出错,那就直接跳到 error 回调中。就像用 do...catch 或者 Result 一样。

fetch 中使用 Promise —— 取代 completion block 或者 Result —— 看起来应该是这样的:


func fetch(url: NSURL) -> Promise<NSData> {
  // PromiseKit 有一个便利的 `init`,会返回一个 (T?, NSError?) 闭包到 `Promise` 中
  return Promise { resolve in
    NSURLSession.sharedSession().dataTaskWithURL(url) { (data, _, error) -> Void in
      resolve(data, error)
    })
  }.resume()
}

fetch 方法会立即返回,所以就没必要用 completionBlock 了。但它会返回一个 Promise 对象,这个对象只去执行 then 里面的闭包 - 在(异步)数据延时到达、Promise这个对象被成功赋值(译注:promise is fulfilled,也是承诺被履行的意思)之后。

Observe 和 Reactive

Promise 很酷,但还有另外一个概念,可以在实现微处理步骤流的同时支持异步操作,并且支持处理这个流中任何时间任何地点发生的错误。

这个概念叫做 Reactive Programming(响应式编程)。你们之中可能有人知道 ReactiveCocoa(简写 RAC),或者RxSwift。即便它和Promises有部分相同的理念(异步、错误传递,...),它还是超越了 FuturesPromises 这个级别:Rx 允许某时刻有多个值被发送(不仅仅有一个返回值),而且还拥有其他繁多丰富的特性。

这就是另外一个全新话题了,之后我会对它一探究竟。


  1. 这是对 Result 可能的实现方式中的一种。其他的实现也许就会有一个更明确的错误类型。

  2. 在这里我调用 return completion(…) 时用了一个小花招,并没有调用 completion(...) 然后再 return 来退出函数的作用域。这个花招能成功,是因为 completion 返回一个 VoidfetchUser 也返回一个 Void(什么都不返回),而且 return Void 和单个 return 一样。这完全是个人偏好,但我还是觉得能用一行写完更好。

  3. 这段代码中,@noescape关键字的意思是throwingExpr能被保证在init函数的作用域里是被直接拿来使用 - 相反则是把它存在某个属性中以后再用。用了这个关键字你的编译器不用强迫你在传进一个闭包时在调用点使用self.或者[weak self]了,还能避免引用循环的产生。

  4. 在这里暂停一下,看看这段代码多像在开篇的时候我们写的UserBuilder的那段,感觉我们开篇时就走在了正确的路上。? 

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

查看原文

赞 1 收藏 1 评论 0

山河永寂 回答了问题 · 2016-12-21

公司老代码太乱了,要不要放弃?

题主不觉得这是对个人能力的很好锻炼吗,在一个老旧的架构上面做开发,更加体现出技术的解决能力,如何一边进行需求开发,一边拆分原有模块和重构,同时能够保证业务的顺利进行,相对于陈旧的代码,我更加偏向于把这些代码叫做“沉淀的财富”,至少,原有的代码能顺利的保证业务进行,而对一个现有业务架构的重构,实际上很考验开发者的技术水平。
PS:像 PHP JS 这样的弱类型语言,确实重构很难,因为逻辑的梳理都非常困难,不然怎么叫做“动态语言一时爽,代码重构火葬场”而且看题主的描述,这些代码实际上并没有前后端分离,或者基本的静态资源分离都没做。不过真心建议继续做下去,对个人技术提升很大的,而且我记得 PHP7 已经有强类型模式了,可以试试看!

关注 20 回答 15

山河永寂 关注了问题 · 2016-12-21

公司老代码太乱了,要不要放弃?

来公司有半年多了,公司主要做一个面向OA管理的产品,公司前后端、移动、运维、服务器什么的加起来有几十号人,后端主要用PHP写,我是负责web开发这一块的。公司产品陆陆续续已经开发有十多年了,新老交替,各种写法层出不穷,主要是一开始就是刀耕火种,代码架构上没有采用比较优秀的模式,耦合度高的离谱,没有什么章法,写起来任性随意。而且PHP你懂的,如果架构上不规划下,项目越大越难看。我进入公司后做项目都是靠硬看源代码才能解决问题,公司也没有文档什么的,每次看完都很头大,实现一个功能,最难的地方并不在功能本身,而在与要找项目原生的代码逻辑和已有的各种函数和变量,代码不遵守编码规范就不提了,前端那一块简直惨不忍睹,一个几千行的代码中有PHP、js、css、html、xml各种混杂在里面,每次做功能都要把大把的时间浪费在理清代码逻辑上,已经做了半年多了,但是想想以后每天要和这堆老代码打交道,我真心头大(决心以后一定要尽量少留坑),但按照公司的意思,这个产品太大了,彻底改是改不了了,只能进行部分微调(好吧,其实我能理解),但就个人发展而言,以后如果在这种陈旧的产品上继续开发,我也会在这上面变成一个臃肿代码生成器,毕竟自己还年轻,希望各位江湖码友能给点意见,在下多谢了。

ps:素质讨论,不要乱喷。

关注 20 回答 15

山河永寂 发布了文章 · 2016-11-20

Swift 后端开发

原文来自静雅斋,转载请注明出处。

作为一门新兴的现代化语言,Swift 可以说是苹果在开发语言上的一次集大成之作,吸收了很多语言的优点。而且苹果还期望 Swift 能在服务端开发上能发挥作用。更加诱人的是,作为一种编译型语言,有着 C++ 一般的性能,并且相比 Golang、Java 来说使用 ARC 管理内存避免了 GC 导致进程停顿。可以说 Swift 就是程序员梦寐以求的语言。

Swift 目前在后端开发的缺点

虽然 Swift 本身有很多优点,但是在后端开发上依旧任重道远,比如有以下问题:

  1. 目前只适配了 Ubuntu 下的二进制包,没有 RHEL 等其他 Linux 下的二进制包

  2. 缺乏非 Mac 系统下的 IDE 开发,这个目前看起来好像只能指望 Jetbrains 了

  3. 没有其他语言那么完善的生态系统

  4. 缺乏的文档,比如包管理系统的语法文档,必须自行查看源代码

  5. 没有交叉编译链,不能在 Mac 上面编译出 Linux 可用的二进制文件

  6. 缺乏好用的单元测试

但是这些问题目前都有方法克服,比如使用 Docker 作为承载 Swift 程序的容器,而使用 Mac 来开发 Swift 程序也不是很大的问题,因为大多数的后端开发都是用 Mac 开发的。

Docker 的作用

笔者个人认为 Docker 解决的最大的问题就是开发环境和生产环境的矛盾,对于开发人员来说,追新永远是必备素质,而测试和运维不会希望环境变更导致的问题,比如线上服务器跑的是 CentOS,而 Swift 则是必须在 Ubuntu 上运行,但是 Docker 的出现就能解决这个问题。笔者认为最适合运行在 Docker 中的就是像 Web 这样的服务,Nginx 和数据库之类的就不适合放在 Docker 中,因为它们是有状态的,而且 Docker 这样的快速消亡快速创建的模式也不适合数据库这样对数据有着严格要求的应用。当然,Kubernetes 目前推出的 petset 就很适合数据库这样的有状态的应用。

Perfect 框架

Perfect 框架是 Swift 开发的 Web 应用服务器,它支持包括 Redis、SQLite、PostgreSQL、MySQL、MongoDB、FileMaker 这样的数据库,并且能以 fastcgi 或者 Web 服务器的形式提供服务。更加美妙的是,还有高质量的中文文档。

HelloWorld

Perfect 提供了基础模板工程,可以使用以下命令下载

> git clone git@github.com:PerfectlySoft/PerfectTemplate.git

然后安装依赖

> swift package fetch

然后就能编译运行了

# 以 Debug 方式编译
> swift build
# 以 Release 方式编译
> swift build -c release

分析 HelloWorld

HelloWorld 工程依赖了 Perfect-HTTPServer 模块,然后其中有两个源文件,arguments.swiftmain.swift
main.swift

import PerfectLib
import PerfectHTTP
import PerfectHTTPServer

// Create HTTP server.
let server = HTTPServer()

// Register your own routes and handlers
var routes = Routes()
routes.add(method: .get, uri: "/", handler: {
        request, response in
        response.setHeader(.contentType, value: "text/html")
        response.appendBody(string: "<html><title>Hello, world!</title><body>Hello, world!</body></html>")
        response.completed()
    }
)

// Add the routes to the server.
server.addRoutes(routes)

// Set a listen port of 8181
server.serverPort = 8181

// Set a document root.
// This is optional. If you do not want to serve static content then do not set this.
// Setting the document root will automatically add a static file handler for the route /**
server.documentRoot = "./webroot"

// Gather command line options and further configure the server.
// Run the server with --help to see the list of supported arguments.
// Command line arguments will supplant any of the values set above.
configureServer(server)

do {
    // Launch the HTTP server.
    try server.start()
} catch PerfectError.networkError(let err, let msg) {
    print("Network error thrown: \(err) \(msg)")
}

用过 Node 的 express 框架的朋友是不是感觉很熟悉,使用事件循环处理 HTTP 请求,事实上早期的 Perfect 框架用的就是 libev 框架来处理事件循环的 HTTP 请求。
arguments.swift

import PerfectHTTPServer
import PerfectLib

#if os(Linux)
    import SwiftGlibc
#else
    import Darwin
#endif

// Check all command line arguments used to configure the server.
// These are all optional and you can remove or add arguments as required.
func configureServer(_ server: HTTPServer) {
    
    var sslCert: String?
    var sslKey: String?
    
    var args = CommandLine.arguments
    
    func argFirst() -> String {
        guard let frst = args.first else {
            print("Argument requires value.")
            exit(-1)
        }
        return frst
    }
    
    let validArgs = [
        "--sslcert": {
            args.removeFirst()
            sslCert = argFirst()
        },
        "--sslkey": {
            args.removeFirst()
            sslKey = argFirst()
        },
        "--port": {
            args.removeFirst()
            server.serverPort = UInt16(argFirst()) ?? 8181
        },
        "--address": {
            args.removeFirst()
            server.serverAddress = argFirst()
        },
        "--root": {
            args.removeFirst()
            server.documentRoot = argFirst()
        },
        "--name": {
            args.removeFirst()
            server.serverName = argFirst()
        },
        "--runas": {
            args.removeFirst()
            server.runAsUser = argFirst()
        },
        "--help": {
            print("Usage: \(CommandLine.arguments.first!) [--port listen_port] [--address listen_address] [--name server_name] [--root root_path] [--sslcert cert_path --sslkey key_path] [--runas user_name]")
            exit(0)
        }]
    
    while args.count > 0 {
        if let closure = validArgs[args.first!.lowercased()] {
            closure()
        }
        args.removeFirst()
    }
    
    if sslCert != nil || sslKey != nil {
        if sslCert == nil || sslKey == nil {
            print("Error: if either --sslcert or --sslkey is provided then both --sslcert and --sslkey must be provided.")
            exit(-1)
        }
        if !File(sslCert!).exists || !File(sslKey!).exists {
            print("Error: --sslcert or --sslkey file did not exist.")
            exit(-1)
        }
        server.ssl = (sslCert: sslCert!, sslKey: sslKey!)
    }
}

这里就很简单了,就是提供参数用于 HTTP 服务器的创建,而这个好处就是能通过参数获得更多功能。

创建自己的工程

首先使用 swift package init 命令创建工程,一般来说如下

> swift package init --type executable

然后在 Package.swift 中增加依赖,但是 Swift 目前所有的 IDE 都没有提供对 PackageDescription 模块的代码提示,估计是因为这是 Swift 內建模块。具体内容得到 Swift 源代码中可以找到。
一般来说只要增加如下内容

import PackageDescription

let package = Package(
    name: "XXXX",
    dependencies: [
        .Package(url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git", majorVersion: 2, minor: 0)
    ]
)

然后创建 main.swift 文件,并且创建 HTTPServer 侦听端口,就能创建自己的工程了。

Docker

一般来说,Docker 目前想要运行 Swift 必须使用 Ubuntu 的镜像,因为 Swift 的预编译包只提供 Ubuntu 的压缩包,但是很多 Docker 镜像存在很多问题,比如缺少支持库,所以需要作出以下修改,下面提供一个样例

FROM swiftdocker/swift:3.0.1
MAINTAINER ChasonTang <chasontang@gmail.com>

RUN apt-get update \
    && apt-get install -y uuid-dev libcurl4-openssl-dev libssl-dev \ 
    && git clone https://github.com/PerfectlySoft/PerfectTemplate /usr/src/PerfectTemplate \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
WORKDIR /usr/src/PerfectTemplate
RUN swift build -c release
CMD .build/release/PerfectTemplate --port 80

Perfect 需要 libcurl 是因为 swift build 获取依赖的时候是使用 curl 来获取代码的,uuid 是因为 Perfect 框架内置函数库所需,而 openssl 则是 Perfect 依赖的库所需。这里使用的是 git clone 的方式获取工程代码,但是也可以通过 COPY 指令复制当前目录下的文件。

查看原文

赞 1 收藏 4 评论 4

认证与成就

  • 获得 171 次点赞
  • 获得 10 枚徽章 获得 0 枚金徽章, 获得 4 枚银徽章, 获得 6 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-02-07
个人主页被 2.8k 人浏览