为失败而设计

为故障和失败做设计 先来看一个笑话: QA工程师走进酒吧,要了一杯啤酒,要了0杯啤酒,要了999999999杯啤酒,要了一只蜥蜴,要了-1杯啤酒,要了一个sfdeljknesv,酒保从容应对,QA工程师 很满意。接下来,一名顾客来到了同一个酒吧,问厕所在哪,酒吧顿时起了大火,然后整个建筑坍塌了。 事实上,无论我们事先做多么详尽的计划,我们还是会失败。而且大多数时候,失败、故障都会从一个我们无法预期的角度发生,令人猝不及防。 如果没有办法避免失败(事先要考虑到这么多的异常情况不太现实,而且会投入过多的精力),那么就需要设计某种机制,使得当发生这种失败时系统可以将损失降低到最小。 另一方面,系统需要具备从灾难中回复的能力。如果由于某种原因,服务进程意外终止了,那么一个watchdog机制就会非常有用,比如supervisord就可以用来保证进程意外终止之后的自动启动。 [program:jigsaw] command=java -jar /app/jigsaw.jar startsecs=0 stopwaitsecs=0 autostart=true autorestart=true stdout_logfile=/var/log/jigsaw/app.log stderr_logfile=/var/log/jigsaw/app.err 在现实世界中,设计一个无缺陷的系统显然是不可能的,但是通过努力,我们还是有可能设计出具有弹性,能够快速失败,从失败中恢复的系统来。 错误无可避免 令人担心的错误处理 我们先来看两个代码片段,两段代码都是在实现一个典型的Linux下的Socket服务器: int main (int argc, char *argv[]) { int serversock; struct sockaddr_in server; serversock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) setup_sockaddr(&server); bind(serversock, (struct sockaddr *) &server, sizeof(server)); listen(serversock, MAXPENDING) //... } 如果加上现实中可能出现的各种的处理,代码会变长一些: int main (int argc, char *argv[]) { int serversock; struct sockaddr_in server; if (argc != 2) { fprintf(stderr, "USAGE: server <port>\n"); exit(-1); } if ((serversock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) { fprintf(stderr, "Failed to create socket\n"); exit(-1); } setup_sockaddr(&server); if (bind(serversock, (struct sockaddr *) &server, sizeof(server)) < 0) { fprintf(stderr, "Failed to bind the server socket\n"); exit(-1); } if (listen(serversock, MAXPENDING) < 0) { fprintf(stderr, "Failed to listen on server socket\n"); exit(-1); } //....

May 17, 2016 · 2 min · 邱俊涛 | Juntao Qiu

会话与API安全 - 下

前后端分离之后 前后端分离之后,在部署上通过一个反向代理就可以实现动静态分离,跨域问题的解决等。但是一旦引入鉴权,则又会产生新的问题。通常来说,鉴权是对于后台API/API背后的资源的保护,即未经授权的用户不能访问受保护资源。 要实现这个功能有很多种方式,在应用程序之外设置完善的安全拦截器是最常见的方式。不过有点不够优雅的是,一些不太纯粹的、非功能性的代码和业务代码混在同一个代码库中。 另一方面,各个业务系统都可能需要某种机制的鉴权,所以很多企业都会搭建SSO机制,即Single Sign-On。这样可以避免人们在多个系统创建不同账号,设置不同密码,不同的超时时间等等。如果SSO系统已经先于系统存在了很久,那么新开发的系统完全不需要自己再配置一套用户管理机制了(一般SSO只会完成鉴权中鉴别的部分,授权还是需要各个业务系统自行处理)。 本文中,我们使用基础设施(反向代理)的一些配置,来完成保护未授权资源的目的。在这个例子中,我们假设系统由这样几个服务器组成: 系统组成 这个实例中,我们的系统分为三部分 kanban.com:8000(业务系统前端) api.kanban.com:9000(业务系统后端API) sso.kanban.com:8100 (单点登录系统,登陆界面) 前端包含了HTML/JS/CSS等资源,是一个纯静态资源,所以本地磁盘即可。后端API则是一组需要被保护的API(比如查询工资详情,查询工作经历等)。最后,单点登录系统是一个简单的表单,用户填入用户名和密码后,如果登录成功,单点登录会将用户重定向到登录前的位置。 我们举一个具体场景的例子: 未登录用户访问http://kanba.com:8000/index.html 系统会重定向用户到http://sso.kanban.com:8100/sso?return=http://kanba.com:8000/index.html 用户看到登录页面,输入用户名、密码登录 用户被重定向回http://kanba.com:8000/index.html 此外,index.html页面上的app.js对api.kanban.com:9000的访问也得到了授权 环境设置 简单起见,可以通过修改/etc/hosts文件来设置服务器环境: 127.0.0.1 sso.kanban.com 127.0.0.1 api.kanban.com 127.0.0.1 kanban.com nginx及auth_request 反向代理nginx有一个auth_request的模块。在一个虚拟host中,每个请求会先发往一个内部location,这个内部的location可以指向一个可以做鉴权的Endpoint。如果这个请求得到的结果是200,那么nginx会返回用户本来请求的内容,如果返回401,则将用户重定向到一个预定义的地址: server { listen 8000; server_name kanban.com; root /usr/local/var/www/kanban/; error_page 401 = @error401; location @error401 { return 302 http://sso.kanban.com:8100/sso?return=$scheme://$http_host$request_uri; } auth_request /api/auth; location = /api/auth { internal; proxy_pass http://api.kanban.com:9000; proxy_pass_request_body off; proxy_set_header Content-Length ""; proxy_set_header X-Original-URI $request_uri; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; if ($http_cookie ~* "w3=(\w+)") { set $token "$1"; } proxy_set_header X-KANBAN-TOKEN $token; } } 比如上面这个例子中,auth_request的URL为/api/auth,它是一个内部的location,外部无法访问。在这个locaiton中,请求会被转发到http://api....

May 12, 2016 · 2 min · 邱俊涛 | Juntao Qiu

会话与API安全 - 上

保护你的API 在大部分时候,我们讨论API的设计时,会从功能的角度出发定义出完善的,易用的API。而很多时候,非功能需求如安全需求则会在很晚才加入考虑。而往往这部分会涉及很多额外的工作量,比如与外部的SSO集成,Token机制等等。 这篇文章会以一个简单的例子,从应用程序和部署架构上分别讨论几种常见的模型。这篇文章是这个系列的第一篇,会讨论两个简单的主题: 基于Session的用户认证 基于Token的RESTful API(使用Spring Security) 使用Session 由于HTTP协议本身是无状态的,服务器需要某种机制来区分每个请求。比如在返回给客户的响应中加入一些ID,客户端再次请求时带上这个ID,这样服务器就可以区分出来每个请求,并完成事务性的操作(完成订单的创建,更新,商品派送等等)。 在多数Web容器中,这种机制通过Session来实现。Web容器会为每个首次请求创建一个Session,并将Session的ID以浏览器Cookie的方式返回给客户端。客户端(常常是浏览器)在后续的请求中带上这个Session的ID来表明自己的身份。这种机制同样被用在了鉴权方面,用户登录系统之后,系统分配一个Session ID给他。 除非Session过期,或者用户从客户端的Cookie中主动删了Session ID,否则在服务器端来看,用户的信息会和这个Session绑定起来。后台系统也可以随时知道请求某个资源的真实用户是谁,并以此来判断该用户时候真的有权限这么做。 HttpSession session = request.getSession(); String user = (String)session.getAttribute("user"); if(user != null) { // } Session的问题 这种做法在小规模应用中工作良好,随着用户的增多,企业往往需要部署多台服务器形成集群来对外提供服务。在集群模式下,当某个节点挂掉之后,由于Session默认是保存在部署Web容器中的,用户会被误判为未登录,后续的请求会被重定向到登陆页面,影响用户体验。 这种将应用程序状态内置的方法已经完全无法满足应用的扩展,因此在工程实践中,我们会采用将Session外置的方式来解决这个问题。即集群中的所有节点都将Session保存在一个公用的键值数据库中: @Configuration @EnableRedisHttpSession public class HttpSessionConfig { } 上面这个例子是在Spring Boot中使用Redis来外置Session。Spring会拦截所有对HTTPSession对象的操作,后续的对Session的操作,Spring都会自动转换为与后台的Redis服务器的交互,从而避免节点挂掉之后Session丢失的问题。 spring.redis.host=192.168.99.100 spring.redis.password= spring.redis.port=6379 如果你跟我一样懒的话,直接启动一个redis的docker container就可以: $ docker run --name redis-server -d redis 这样,多个应用共享这一个实例,任何一个节点的终止、异常都不会产生Session的问题。 基于Token的安全机制 上面说到的场景中,使用Session需要额外部署一个组件(或者引入更加复杂的Session同步机制),这会带来另外的问题,比如如何保证这个节点的高可用,除了Production环境之外,Staging和QA环境也需要这个组件的配置、测试和维护。 很多项目现在会采用另外一种更加简单的方式:基于Token的安全机制。即不使用Session,用户在登陆之后,会获得一个Token,这个Token会以HTTP Header的方式发送给客户,同样,客户再后续的资源请求中也需要带着这个Token。通常这个Token还会有过期时间的限制(比如只能使用1周,一周之后需要重新获取)。 基于Token的机制更加简单,和RESTful风格的API一起使用更加自然,相较于传统的Web应用,RESTful的消费者可能是人,也可能是Mobile App,也可能是系统中另外的Service。也就是说,并不是所有的消费者都可以处理一个登陆表单! Restful API 我们通过一个实例来看使用Spring Security保护受限制访问资源的场景。 对于Controller: @RestController @RequestMapping("/protected") public class ProtectedResourceController { @RequestMapping("/{id}") public Message getOne(@PathVariable("id") String id) { return new Message("Protected resource "+id); } } 我们需要所有请求上都带有一个X-Auth-Token的Header,简单起见,如果这个Header有值,我们就认为这个请求已经被授权了。我们在Spring Security中定义这样的一个配置:...

May 10, 2016 · 2 min · 邱俊涛 | Juntao Qiu