REST API 安全认证研究
Posted sp42a
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了REST API 安全认证研究相关的知识,希望对你有一定的参考价值。
REST API 安全认证研究
概述
对外网暴露的 RESTful API,由于是无状态的,如果不做认证,那就相当于裸奔的,任何人都可以调用,随意调用,这样是极不安全的。下面就 RESTful API 的安全性方案进行了一些研究。(但是首先建议,核心系统的 API 不对外网暴露,只允许内网调用,而且不建议做成 HTTP RESTful 形式。如果非要使用 RESTful API 对外网暴露接口,那么请看下面)。
RESTful API 的安全性,包括了如下三个方面:
a) 对客户端做身份认证
b) 各种安全防护措施
c) 身份认证之后的授权
1、对客户端做身份认证
面向最终用户的 API,采用 HTTPS + OAuth 2 的方式来认证比较好。面向服务器的API,则比较简单,方式比较多。
2、各种安全防护措施
包括:防止敏感数据泄露,防篡改,防重放攻击,防 DDoS 攻击。应对方案包括:采用HTTPS;DDoS 流量清洗;对敏感数据部分加盐再加密传输;在请求中增加一次性的 Token。
3、身份认证之后的授权,由应用服务器端根据业务需求自己实现,比如安全框架 Shiro、SpringSecurity 等。
参考资料
- http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html
- http://open.weibo.com/wiki/index.php/Weibo-JS_V2
- https://developer.github.com/v3/
考虑两种黑客攻击情况: 1、针对 HTTP 的网络劫持;2、针对 HTTPS 的中间人攻击+SSLstrip。防御措施,要点如下:1、全站采用 HTTPS;2、客户端、Web 端的数据单向加密传输。
采用全站 HTTPS 后,能避免绝大多数的攻击。而针对 HTTPS 的攻击,难度很大(目前市面上对 HTTPS 成功的攻击还是比较少见),如果不是非常非常核心的数据,是可以不考虑特殊保护的。针对 HTTPS 的进一步保护手段,是采用数据加密,对客户端和 Web 端用户发送的数据,进行加密再传输,即使被黑客获取了,也无法篡改和利用。
客户端身份认证方案
客户端会话认证
两种大的方案:
- 基于服务器端(分布式)Session 技术
- 基于JWT(Json Web Token)技术
通常来说,针对Web 浏览器端的应用,基本用(a)方案,而手机 App 端的应用,一般用(b)方案。
分布式 Session 技术
基本上都是用 Redis 来存储 Session 信息。Session 的方案比较常规,不做过多说明。
JWT技术(基于token的鉴权机制)参见《JWT技术-基于token的鉴权机制》一文。
OAuth 2.0认证
关于 HTTPS 的安全性问题
首先来说,从目前全世界的报道来看,HTTPS 数据加密应该是足够安全的,黑客很难破解。但是,对 HTTPS 的应用,仍然可以变相的攻击:重放攻击。
首先,模拟一个重放攻击的例子,如下:
常规登录流程:
- 前端web页面用户输入账号、密码,点击登录。
- 请求提交之前,web端首先通过客户端脚本如javascript对密码原文进行md5加密。
- 提交账号、md5之后的密码
- 请求提交至后端,验证账号与密码是否与数据库中的一致,一致则认为登录成功,反之失败。
但是,如果监听者截取到了登录信息:
http://****/login.do?method=login&password=md5&userid=登录账号
把它重放一下,即可冒充你的身份登录系统。解决思路如下:
- 把这个 url 请求做成一次性使用的,第二次(重放)请求就无效了。
- 每次请求传输一个 token 值,第二次如果还是一样的 token,就无效。这个 token 的生成算法或者密码数据是保密的,监听者无法得知算法或者密码,从而无法仿制 token。
方案一
1、如果是客户端APP,可以采用某种算法和密匙,将密码数据、时间截、随机数等加在一起生成token,然后再发送给服务器端验证,服务器验证通过后会记录下这个token,保存一段时间(比如24小时)。如果网络监听者再次发送一样的请求和token,则服务器端直接拒绝。由于网络监听者是不知道token生成算法或密匙的,如果他随意修改了token,服务器端根据密码数据、时间截、随机数等计算出来的token,就和接收到的token不一致,就拒绝这次访问。
举个实际例子,假设第一次请求数据:
DATA: {password:"ds8fds7", score: 10}
RS-TOKEN: AAAAAAAAAA (timestamp + nonce)
如果黑客原封不动将这个请求再发一遍,显然这个 token 是会被拒绝的。所以要重新生成token,但是黑客不知道
方案二
2、也可以在调用这个API之前,先向服务器获取一个随机码(称之为盐值),在客户端和服务器端各保存一份,并且设置一个有效时间,客户端提交请求时,将md5之后的密码数据与该随机码拼接后,再次执行md5,然后提交(提交的token=md5(md5(密码数据)+随机码)),后端也会计算token,然后对比,对比成功则删除从服务器端这个随机码,监听者无法再次使用它进行登录。网络监听者即使再次提交这个请求,但是后端已经删除了随机码,所以无法通过。(这个方案的劣势在于,每次请求之前,都要先获取一个随机码,第二次请求时要携带上这个随机码)
3、针对方案一,如果不是客户端APP,而是WEB浏览器,算法和密匙是写在JavaScript里面的,怎么保证算法和密匙不被泄露?js是可以被拿到的,js函数可以被黑客执行,他可以调用js函数向服务器端发起请求。为避免这种情况,应该让网络监听者无法拿到可用的js,即使拿到了,也要限制js的执行,具体方案还有待研究,我考虑到两点,1是JavaScript混淆压缩,让js很难被反编译识别(目前来说是可以做到的),2是只允许js在指定环境下才能执行,例如有指定cookie或浏览器内存数据时才能执行,而cookie数据或内存数据需要从后端获取,js动态生成,且当该js生成的时候就携带一个标识,当js初始化时,就执行一次后端验证,验证通过该js可以执行,否则这个js不能执行。
Cookie 与 CSRF(XSRF)
本文是《Session和Cookie原理》的续篇。在上一篇中,详细介绍了 Cookie 的原理。下面介绍,如何Cookie的跨站共享,及CSRF(Cross-Site Request Forgery,跨网站请求伪造)攻击。请先阅读《》一文,以便对跨网站请求有一个初步了解。在这篇文章中提到的一点十分关键:
Fetch 与 CORS 的一个有趣的特性是,可以基于 HTTP cookies 和 HTTP 认证信息发送身份凭证。一般而言,对于跨域
XMLHttpRequest 或 Fetch 请求,浏览器不会发送身份凭证信息。如果要发送凭证信息,需要设置 XMLHttpRequest
的某个特殊标志位(withCredentials = true)。
另外,注意到,在同一个浏览器当前打开的多个 Tab 页网站,无论是否为同一个网站,Cookie 都是共享可见的(注意:这个共享,不是说每个网站的脚本可以访问别的网站的Cookie,而是说,所有的Cookie,在浏览器发送HTTP请求认证时,都会携带过去)(另外,1. 对于网站的JS,能否访问自己的Cookie,要看这个Cookie是否为httpOnly的。2. 如果要访问别的网站的Cookie,除非是在Cookie的domain允许的子域,否则无法访问其他网站的Cookie)(另外,Cookie虽然是共享的,可以发送给服务器端,但是HTTP请求本身有访问控制,如果不是同一个网址发来的请求,并且服务器没有设置Access-Control-*项目的选项来允许跨站请求的话,请求会被拒绝的)
注意:“HTTP跨站请求” 和 “Cookie的跨站共享”,其实是两码事情。之所以会同时提到它们,是因为 我们经常会用到 HTTP的跨站请求,然而 HTTP的跨站请求,由于存在 Cookie的跨站共享,就可以导致 CSRF 攻击。
由于 Cookie这个共享机制,浏览器当前Tab页网站,都可以携带Cookie发送给后端服务器(对于跨域的AJAX请求,需要设置withCredentials = true,否则请求不会携带Cookie,对于非跨域的请求,或者跨域的非AJAX请求–比如通过form表单提交,浏览器会自动携带上Cookie),这样后端服务器,是无法根据Cookie来识别请求发送自哪个地方、哪个网站,有可能是网站A发出的,也可能是网站B发出的。所以当前打开的网站A可以给后端发一个请求AAA,网站B也可以发送这个请求,并且有一样的Cookie,如果只是根据Cookie来认证的话,那么从网站A和网站B发送的请求AAA都是有效的。
如果你登录网站A(网站A的后端服务器支持跨域访问)为管理员,执行了一个入库操作(http://A.com/api2),在没有退出网站A管理员身份的情况下,同时又打开了钓鱼网站B,那么网站B就可以用你的网站A管理员身份执行AJAX,操作入库等,它只要知道ajax的url即可。(如果你感兴趣,很容易就可以模拟出来这种效果,打开网站A并登录,然后打开网站B,在B的JS中执行网站A的ajax请求,甚至先打开网站B,在网站B的JS中隔1秒执行一次某个AJAX,随时等待用户登录,一旦用户登录这个AJAX操作就会执行成功)【注意,跨域请求用AJAX的话,后端服务器要设置“Access-Control-Allow-Origin”,否则不会存在CSRF攻击,且如果要携带Cookie信息的话,AJAX必须设置“withCredentials: true”,但是如果不用AJAX请求,比如使用form表单请求,则不会受到限制(亲测),下面给出我的测试方法】
测试方法:
第一次,在 B 网站调用 A 系统的 API:
$.getJSON('http://A.com/api2',function(result){
console.log(result)
});
由于是跨域的AJAX请求,而且B系统服务器端又没有设置“Access-Control-Allow-Origin
”,所以报错了:
Failed to load http://A.com/api2: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access.
createError.js?2d83:16 Uncaught (in promise) Error: Network Error
at createError (createError.js?2d83:16)
at XMLHttpRequest.handleError (xhr.js?b50d:87)
Warn:xhr.js?b50d:178 Cross-Origin Read Blocking (CORB) blocked cross-origin response http://A.com/api2 with MIME type application/json. See https://www.chromestatus.com/feature/5629709824032768 for more details.
但奇怪的是,在网络请求里面可以看到这个 api 调用,且显示的 HTTP CODE 为 200,但没有数据返回,应该是由于服务器端没有设置“Access-Control-Allow-Origin
”,所以直接返回空的响应(response)。
第二次,我们不用 AJAX 请求,我们用 form表单提交请求:
<script type="text/javascript">
$(document).ready(function () {
$("#ddd").submit();
});
</script>
<div>
<form id="ddd" action="http://B.com/api2" method="get" target="nm_iframe">
<input type="text" name="keywords" />
<input type="text" name="pageNum" value="1" />
<input type="submit" value="Submit" />
</form>
<iframe id="id_iframe" name="nm_iframe" style="display:none;"></iframe>
</div>
结果不出所料,请求成功了,正常返回数据,于是我们成功模拟了一次 CSRF 攻击。基于以上分析得出结论:
大多数的CSRF攻击,都不会基于AJAX请求,而是通过form表单等请求来实现,因为这样可以不用关心服务器端是否设置了“Access-Control-Allow-Origin”。
明白 CSRF 原理之后,就很容易想到 防御方案:
1、服务器端,设置允许 HTTP 跨站请求时,范围权限要尽量小,可以指定某站可以跨站访问,其他站点发来的请求就会被拒绝,还可以指定某个 api url 支持跨站请求,其他所有url都不允许跨站请求。(但是这种服务器端的设置,只能避免AJAX请求的CSRF攻击,没办法防御 form 表单提交的请求)
2、为了抵御来自form表单的跨站请求,服务器端应该对每次请求进行身份校验,就像JWT鉴权那样,每次请求的header中放一个token,如果没有token则拒绝请求,由于token是在本站的cookie或者localStorage中,跨站的form表单请求是拿不到token的,也就无法发起有效的请求了。(而且 JWT方案不仅能防范 CSRF攻击,还能做登录、请求鉴权、身份识别的作用,但是JWT方案又会遇到 XSS 脚本攻击的可能,当然XSS也有解决方案,而且XSS攻击难度要大一些)
参考资料(写得很好,推荐阅读):https://en.wikipedia.org/wiki/Cross-site_request_forgery。这篇参考资料的“Cookie-to-header token” 方案,就是我上面推荐的方案的一种实现方式,即把cookie里面的认证信息(比如sessionid),放一份在header中,可以存一样的,也可以HMAC签名一下。
Cookie跨域
上一篇文章已经说了Cookie原理及“domain-域”的概念,Cookie是不能跨域访问的,如果要想解决Cookie跨域的问题,可以另辟蹊径,达到目的,比如nginx反向代理,反向代理之后其实就是同一服务器下,不存在跨域了,还比如jsonp。本文不详细讨论,参考:https://www.cnblogs.com/1020182600HENG/p/7121148.html
跨域请求Access-Control问题及相关理论大全
本文涉及三个核心知识:
- CORS 及 HTTP 的 Access-Control
- 浏览器的 preflight request
- HTTP 的 OPTIONS 方法的作用
及一个故事(我为什么三个小时没查出 CORS 失败)。先别急,必须来弄懂上面的三个知识。
第一个,CORS 及 HTTP 的 Access-Control,推荐看下面这两篇文章:
讲解如下:
- CORS 的来源及背景:出于安全原因,浏览器限制了从脚本启动的跨域 HTTP 请求。并在 Fetch 规范中定义了 CORS;
- CORS 的实现途径:浏览器的 XMLHttpRequest 或 Fetch API 的调用;
- CORS 的底层实现方法:使用携带 CORS 信息的 HTTP 标头(例如,Request 使用 Origin: https://foo.example,Response使用
Access-Control-Allow-Origin: *
); - 具体实现上,分两种情况:一种是简单请求(GET、HEAD、POST,且 Header 符合 CORS 安全列出的请求标头),第二种是非简单请求。对于后者,浏览器在发送正式请求之前,会发起一个 preflight request(预请求),这个 HTTP 请求的 Method 类型为 OPTIONS。如下图所示:可以看到,针对这个 POST 调用,浏览器发起了两次 HTTP 请求,第一次 Method 为 OPTIONS。Response 返回的信息有:
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Access-Control-Max-Age
给出以秒为单位的值,该值表示对预请求的响应可以缓存多长时间而无需发送另一个预检请求。在这种情况下,86400 秒是24小时。请注意,每个浏览器都有一个最大内部值,当 Access-Control-Max-Age
较大时,该内部值优先。
5、带Cookie身份验证信息(credentials)的请求
XMLHttpRequest or Fetch 有能力在 Cookie 中携带 credentials 信息以实现 HTTP Authentication,但默认情况下,跨域请求不会携带 credentials 信息(进一步说,不会携带任何 Cookie),但是可以设置使其支持跨域传递 Cookie:
const invocation = new XMLHttpRequest();
invocation.withCredentials = true; // 在send()前设置
invocation.send();
注意:服务器端 Access-Control-Allow-Credentials = true时,参数Access-Control-Allow-Origin 的值不能为“*”,否则将请求失败(取决于所使用的API)。这是针对Cookie头,如果是通过Header做认证的则不存在这个问题,换句话说,上面所说的是不携带任何cookie,但是仍然可以携带自定义的header。另外,CORS Response中设置的Cookie受到“第三方Cookie政策”约束。
第二个,浏览器的 preflight request,上文已经说明,此处总结一下:
参见:https://www.jianshu.com/p/b55086cbd9af
为什么要发预检请求
我们都知道浏览器的同源策略,就是出于安全考虑,浏览器会限制从脚本发起的跨域HTTP请求,像XMLHttpRequest和Fetch都遵循同源策略。浏览器限制跨域请求一般有两种方式:
- 浏览器限制发起跨域请求
- 跨域请求可以正常发起,但是返回的结果被浏览器拦截了
一般浏览器都是第二种方式限制跨域请求,那就是说请求已到达服务器,并有可能对数据库里的数据进行了操作,但是返回的结果被浏览器拦截了,那么我们就获取不到返回结果,这是一次失败的请求,但是可能对数据库里的数据产生了影响。为了防止这种情况的发生,规范要求,对这种可能对服务器数据产生副作用的 HTTP 请求方法,浏览器必须先使用 OPTIONS 方法发起一个预检请求,从而获知服务器是否允许该跨域请求:如果允许,就发送带数据的真实请求;如果不允许,则阻止发送带数据的真实请求。
什么时候发预检请求
HTTP请求包括: 简单请求 和 非简单请求(需预检的请求)。具体定义参见前文描述。
第三个,HTTP的OPTIONS方法的作用,总结如下:
大家知道,HTTP请求方法并不是只有GET和POST。据RFC2616标准(现行的HTTP/1.1)得知,通常有以下8种方法:OPTIONS、GET、HEAD、POST、PUT、DELETE、TRACE和CONNECT。
1、OPTIONS官方定义
OPTIONS方法是用于请求获得由Request-URI标识的资源在请求/响应的通信过程中可以使用的功能选项。通过这个方法,客户端可以在采取具体资源请求之前,决定对该资源采取何种必要措施,或者了解服务器的性能。该请求方法的响应不能缓存。
如果这个OPTIONS请求包含一个正文(有Content-Length或Transfer-Encoding存在),则必须有Content-Type来指定媒体类型。虽然规范里没有定义这种正文的用法,但是HTTP将来的扩展可能会用它来查询服务器上更详细的信息。不支持该扩展的服务器可以忽略该请求正文。
如果该URI是一个星号(“”),OPTIONS请求将试图应用于服务器,而不是某个指定资源。由于服务器的通信选项通常依赖于资源,所以此“”请求只能作为“ping”或者“no-op”方法;或者用来测试服务器的性能。例如,用来测试HTTP/1.1代理。
如果该URI不是星号,则只能用来获取该资源通信中可用的选项。得到的200响应应该包含一个头域,指明服务器实现的和适用于该资源的可选特征(如:Allow),可能还包括该规范尚未定义的扩展。如果有响应正文,则应包含关于通信选项的信息。本规范没有定义该正文格式,但可能在HTTO将来的扩展中定义。可以利用内容协商来选择合适的响应格式。如果没有响应正文,响应必须包含Content-Length,并且值为“0”。
请求头的Max-Forwards用来请求特定代理。当代理收到一个允许URI转发的OPTIONS请求,则检查Max-Forwards。如果Max-Forwards值为0,则不能转发该消息;相反,代理会将自己的通信选项去响应。如果 Max-Forwards 是正整数,代理转发请求的时候会将该值减1。如果请求中没有 Max-Forwards,转发的请求也不会有。
简而言之,OPTIONS请求方法的主要用途有两个:
- 获取服务器支持的HTTP请求方法;也是黑客经常使用的方法。
- 用来检查服务器的性能。例如:AJAX进行跨域请求时的预检,需要向另外一个域名的资源发送一个HTTP OPTIONS请求头,用以判断实际发送的请求是否安全。
最后,一个故事:我为什么三个小时没查出CORS失败?CORS问题我很清楚,解决办法也一清二楚。但这次我尝试了所有办法却无效!后端各种策略都设置了,全都是允许。但是前端仍然报错:
Access to XMLHttpRequest at **from origin ** has been blocked by CORS policy
前端用了 MockJS,在 XMLHttpRequest 中有 MockHttpRequest 字样,担心是MockJS影响,于是去掉了MockJS换成了正式API,然而还是报错。网上有人说是Chrome浏览器参数影响,设置了一个参数,还是报错。
后端代码有些复杂,用到了Keycloak和Tomcat Server底层代码逻辑,担心是服务器问题。这个环境太复杂了,影响环节多了,检查和测试起来就特别麻烦。最终,测试一种直接在HTTP Response中设置Access-Control标头的方法时,才发现,是我那个Controller方法用了response.reset()
,把Spring-web的CorsConfiguration 配置给清除了。后端代码如下:
@Bean // 拦截器
public FilterRegistrationBean<CorsFilter> corsFilterRegistration() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod(CorsConfiguration.ALL);
config.addAllowedOrigin(CorsConfiguration.ALL);
config.addAllowedHeader(CorsConfiguration.ALL);
config.applyPermitDefaultValues();
...
}
// Controller
@RequestMapping(value = "/by-type", method= {RequestMethod.POST, RequestMethod.GET})
public void code(HttpServletRequest request, HttpServletResponse response) throws IOException {
...
response.reset();
response.setHeader("Content-Disposition", "attachment; filename=\\"project.zip\\"");
response.addHeader("Content-Length", "" + data.length);
response.setContentType("application/octet-stream; charset=UTF-8");
IOUtils.write(data, response.getOutputStream());
}
可以看到,Filter 拦截器虽然设置了 CORS,但是 Controller 调用了 response.reset()
将 header 清空了!!!除了使用 Spring-web 的 CorsConfiguration 配置 filter,也可以自己实现 filter,参考代码如下:
@Value("${xy.cors-white-list}")
private String whiteList;
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
String origin = request.getHeader("origin");
response.setHeader("Access-Control-Allow-Origin", Tools.asList(whiteList.split(",")).contains(origin) ? origin : "-");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, PATCH, DELETE, PUT, OPTIONS");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "*");
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
} else {
chain.doFilter(req, res);
}
}
后端写法(PHP为例)参见:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Server-Side_Access_Control
总结
当问题非常复杂的时候,可能有很多因素影响,容易迷惑人,怎么办呢? 解决问题的思路是:弄清楚问题其中的一个或几个本质,抓住这些点不放,深入分析,看能不能突破其中一个线索。也就是说,认清楚问题的本质,并始终相信,真相就在某个细节上,只是你还没发现。一旦你掌握了本质,并且你确认那是本质,应该相信自己的判断。没有什么灵异事件,没有什么好奇怪的,确定本质及解决问题的正确方向,再寻找证据。
以上面的故事为例,首先,我没有抓住问题本质——报错信息:Access to XMLHttpRequest at **from origin ** has been blocked by CORS policy
,因为对这个报错信息的来源和底层原因不够确定,导致我排查问题时走偏了(胡乱试了很多种方法),如果理解其本质,就知道,这一定是后端 Origin 的设置与前端不匹配,所以问题一定在后端(这就是一个准确的判断,虽然不知道具体的原因是什么,但是可以判断出一定是后端的问题,进一步说,一定是后端 Response 中没有设置 Access-Control-Allow-Origin
,能理解并坚持这一个原则,也就离发现真相不远了)。
以上是关于REST API 安全认证研究的主要内容,如果未能解决你的问题,请参考以下文章