相关文章
从数据库层谈安全措施
这篇文章是通过对话的形式展现,也是我第一次尝试这样的风格。不知道阅读起来的感觉和以前 这种感觉哪种好,希望多多指点。
正文
老A: 老大交代下来任务了,要写一套接口服务,用来查询订单的。主要接口有这么几个,登录、注册、查询用户订单、添加用户订单、删除用户订单、查询用户信息、修改用户信息、注销。小Z,这个就交给你做了,先把接口文档写一下。ヽ(ー_ー)ノ
小Z: 好的没问题,交给我吧。(▽)
十分钟之后·····
小Z :接口文档好了,老A你看一下。(▽)
登录接口 | |
---|---|
接口地址 | /login.do |
请求方法 | post |
请求参数 | username /password |
返回内容 | User对象 |
注册接口 | |
---|---|
接口地址 | /regist.do |
请求方法 | post |
请求参数 | username /password /nickname |
返回内容 | User对象 |
查询用户订单 | |
---|---|
接口地址 | /orderList.do |
请求方法 | get |
请求参数 | userId |
返回内容 | OrderList对象 |
添加用户订单 | |
---|---|
接口地址 | /addOrder.do |
请求方法 | post |
请求参数 | Order对象 |
返回内容 | 成功/失败 |
删除用户订单 | |
---|---|
接口地址 | /removeOrder.do |
请求方法 | post |
请求参数 | orderId |
返回内容 | 成功/失败 |
查询用户信息 | |
---|---|
接口地址 | /getUser.do |
请求方法 | get |
请求参数 | userId |
返回内容 | User对象 |
修改用户信息 | |
---|---|
接口地址 | /editUser.do |
请求方法 | post |
请求参数 | User对象 |
返回内容 | User对象 |
注销 | |
---|---|
接口地址 | /logout.do |
请求方法 | post |
请求参数 | userId |
返回内容 | 成功/失败 |
老A看完后,语重心长的对小Z说:“写接口,不光要考虑功能的实现,你还得考虑一下安全方面啊!”ヽ(ー_ー)ノ
小Z道:“我也考虑到了,请求使用HTTPS,提交的用post方法,获取的用get方法,这样应该比较安全吧。” !!!∑(゚Д゚ノ)ノ
老A:“那我问你,你的登录和后面的接口有什么关系?” o(´^`)o
小Z:“登录进来获取到user对象,user里有userId,通过userId请求后面的数据。不登录是没有的” ─=≡Σ(((つ•̀ω•́)つ
老A:“那么如果我知道UserId,是不是意味着我不需要登录就可以获取到数据了?” <( ̄ ﹌  ̄)>
小Z:“不登录你怎么获取?” (〃‘▽’〃)
老A:“userId是不是不变的,那我上次登录过后我记录下来UserId,下次再用可不可以?我换个电脑还用这个userId,是不是还能获取到数据?”
(▼ヘ▼#)
小Z:“好像是这么个情况,那我该怎么办呢?” ⊙(・◇・)?
老A:“不是好像,就是这么个情况!登录请求不是仅仅为了验证密码返回用户信息的,而是登录要管理起来后面所有跟用户权限有关的接口,不登录是不能用的。”
“我们登陆接口中可以加上一个token验证机制,就是说,如果登录成功,我们随机生成一串字符,把这个字符作为key,关联存储user对象。然后我们返回这个key。之后的所有请求都需要携带这个key,后面的请求过来后,先通过key查找缓存中是否有User对象。如果有,说明已经登录过了,就有权限访问,反之则没有权限访问。”
“同时,由于我们使用了key关联user对象,那么我们也不需要返回user对象。后面的请求也不需要携带user对象中的信息过来。因为我们缓存中已经存储了user对象,通过key查了出来,我们直接使用查出来的user对象作为参数就可以了。这样也避免了user对象的泄露。”
“key也可以添加有效时间的信息,比如说我们可以记录上一次请求key的时间,然后下一次请求就有了一个现在的时间。如果说登录操作有效时间是2小时,那么就可以拿现在的时间减去上一次请求的时间,得到一个差值,然后比较这个差值是否小于2小时,如果小于的话则是在有效时间内,允许请求。同时更新一下最后一次请求时间为当前时间,下一次请求继续判断。如果是超时了,就可以将缓存的user给删掉,直接返回错误,重新登录。用户退出也是一样的,删除掉缓存的user对象,那么以后的请求就找不到user对象,就不允许请求了”
小Z:“不愧是老A,我醍醐灌顶,豁然开朗。我甘拜下风,五体投地。对您的崇拜犹如滔滔江水连绵不绝” (。ᴗ。)
老A:“别拍马屁了,还不快回去改接口” (▼ヘ▼#)
小Z:“好的师傅,马上就去”
又十分钟后。。。
小A:“我的接口改完了,再看一下吧”
登录接口 | |
---|---|
接口地址 | /login.do |
请求方法 | post |
请求参数 | username /password |
返回内容 | token |
注册接口 | |
---|---|
接口地址 | /regist.do |
请求方法 | post |
请求参数 | username /password /nickname |
返回内容 | 成功 |
查询用户订单 | |
---|---|
接口地址 | /orderList.do |
请求方法 | get |
请求参数 | token |
返回内容 | OrderList对象 |
添加用户订单 | |
---|---|
接口地址 | /addOrder.do |
请求方法 | post |
请求参数 | Order对象、token |
返回内容 | 成功/失败 |
删除用户订单 | |
---|---|
接口地址 | /removeOrder.do |
请求方法 | post |
请求参数 | orderId、token |
返回内容 | 成功/失败 |
查询用户信息 | |
---|---|
接口地址 | /getUser.do |
请求方法 | get |
请求参数 | token |
返回内容 | User对象 |
修改用户信息 | |
---|---|
接口地址 | /editUser.do |
请求方法 | post |
请求参数 | User对象、token |
返回内容 | User对象 |
注销 | |
---|---|
接口地址 | /logout.do |
请求方法 | post |
请求参数 | token |
返回内容 | 成功/失败 |
老A:用户登录验证是解决了,但这还不够安全。
小Z:“这还不够安全?那还缺什么呢?”
老A:“你怎么知道给你发消息的就是就是真的用户呢?或者说,你怎么知道给你接口进行请求的,是你想让请求的客户端呢?”
小Z:“这个··· 我还真不知道请求是不是用户发的,我用请求工具填这些参数也是可以请求成功的,也就是说请求是可以被伪造的。”
老A:“是的,你这脑袋还算灵光”
小Z:“那该如何解决呢?”
老A:“我们可以使用验签机制,来防止参数的伪造篡改”
“验签的机制是,将参数进行某个规则的加密,这个规则只有你的服务端和客户端知道,参数中除了正常的请求参数外,多了一个验签参数 sign。后台拿到请求参数后,可以将参数用同样的方法加密,然后与sign比对,如果一致就认为没有被篡改。”
小Z:“那如果别人直接拿抓包的数据进行请求,参数不变,那不是没什么作用吗?他甚至都不用知道加密规则是什么”
老A:“孺子可教也,知道思考了。为了应对这个问题,我们可以在请求中再加一个参数,时间戳”
小Z:“我明白了。请求参数和时间戳一起加密,因为时间戳是变化的,所以说加密出来的结果也是变化的。同时请求参数中带着时间戳参数,服务端可以根据时间戳来限制单次请求的有效时间,比如说单次请求1分钟内有效。那么之后的请求,就算原封不动的拿过来,因为时间戳的限制,不是当时的请求就是无效的,获取不到数据。然而要修改参数中的时间戳,又跟加密验签sign不一致,也得不到返回。这一招真是高啊”
老A:“还有更高的呢。如果说黑客通过你的参数和解密后的结果,得到了加密规则,你咋办?”
老A:“拿到原文和密文,想要破译加密规则也不是不可能的事情。那么如果我们呈现出来的不是原文,那是不是就更安全一些了?”
小Z:“您快别绕关子了,到底是该怎么做?”
老A:“你也不多动动脑袋。服务端个客户端都是我们自己的程序,或者说都是我们授权的。那么我就可以在服务端设置一个参数secret。然后我们把secret告诉给客户端。当然不是走网络 请求,而是通过线下的方式,比如说发邮件啊啥的。然后客户端会把secret存在自己本地,服务器也存一个secret。加密的时候,将secret拼接在要加密的字符串上,secret本身不进行网络传输。这样,在原文中就有一个不可见的字符,别人通过网络也拿不到真正的原文。这样破解起来就不容易了”
小Z:“老A你真是天才啊!”
老A:“什么天才,我也是慢慢学的。这也不是我第一个发明的。”
小Z:“那么我们还可以设置加密秘钥,服务端和客户端知道,也不参与传输。这样也更安全。而且加密不用选择对称加密,采用MD5这种非对称加密就可以,因为服务器不需要解密,服务器只用通过相同的加密规则得到一个相同的结果就可以了”
老A:“学会举一反三了,不错不错。其实 我们也可以产生动态秘钥,用两个渠道传输。比如说以前银行会用一个动态令牌,你要登录网银就需要动态令牌的验证码。”
小Z:“其实现在的手机验证码也感觉像是这种,通过短信这个渠道去发送验证码,也提高了安全性。”
老A:“不错嘛,还想到了手机短信。不过一般是这种操作只有在登录的时候使用。因为虽然是安全,但是用户操作麻烦。为了提高用户的便捷性,只有在关键时刻才会使用,比如登录,比如转账这些。”
小Z:“难怪呢,这下对于 安全又了更深刻的认识。”
老A:“还有一些能够提高安全的你可以做的,那就是传输内容加密和混淆。”
小Z:“加密和混淆?”
老A:“对。你看你现在的接口,参数名别人一看就知道是啥,username这不就是用户名?你的请求参数不也直接明文,别人一眼就看到你传的值?你返回的内容User,别人不也就看到了用户信息?别人也不需要仿造请求,只是进行一个拦截,就看到了数据。”
小Z:“说的对啊,这么重要的事情给忘记了,这就需要双方进行一个对称加密规则,和名称定义了。对称加密的秘钥同样的也需要线下放到客户端和服务端两边。”
老A:“哈哈哈,脑袋转的挺快”
小Z:“那我回去改改,您再来看”
叕十分钟后
登录接口 | |
---|---|
接口地址 | /login.do |
请求方法 | post |
请求参数 | u 用户名/n 密码 |
返回内容 | 加密的json字符 token |
注册接口 | |
---|---|
接口地址 | /regist.do |
请求方法 | post |
请求参数 | s 用户名/ a 密码 /e 昵称 |
返回内容 | 成功 |
查询用户订单 | |
---|---|
接口地址 | /orderList.do |
请求方法 | get |
请求参数 | tn |
返回内容 | 加密的json字符 OrderList对象 |
添加用户订单 | |
---|---|
接口地址 | /addOrder.do |
请求方法 | post |
请求参数 | 加密的json字符 Order对象、tn |
返回内容 | 成功/失败 |
删除用户订单 | |
---|---|
接口地址 | /removeOrder.do |
请求方法 | post |
请求参数 | od 订单id、tn |
返回内容 | 成功/失败 |
查询用户信息 | |
---|---|
接口地址 | /getUser.do |
请求方法 | get |
请求参数 | tn |
返回内容 | 加密的json字符 User对象 |
修改用户信息 | |
---|---|
接口地址 | /editUser.do |
请求方法 | post |
请求参数 | 加密的json字符 User对象、tn |
返回内容 | 成功 |
注销 | |
---|---|
接口地址 | /logout.do |
请求方法 | post |
请求参数 | tn |
返回内容 | 成功/失败 |
小Z:“这下够安全了吧”
老A:“这是比之前安全等级高了很多,但不能说百分百安全。比如以前的SQL注入,就是直接将SQL写到参数中,让查询结果直接返回。当然现在是很少了,也不能说没有。我们都会对入参和返回值做处理,很少有直接返回的情况,你应该没这么干吧?”
小Z:“我当然知道,我的Dao层是用的MyBatis,传参用的还是 # 号,#号 就是为了防止SQL注入的”
老A:“那你知道为什么#号能防止SQL注入么?”
小Z:“我还真不知道,您给我讲讲吧”
老A:“唉,看样子你知道SQL注入这回事,但也是没真正明白。以后换了Hibernate或者其他的,你该咋办?你这还只是考虑java开发,安全是要考虑通用性的。你看上面的接口,是不是并不关心你用什么语言实现?”
小Z:“哎呀,您老先别教训我了,我这如饥似渴的小眼神瞅着您,您快说说。”
老A:“SQL注入,说白了就是在参数中写SQL语句,然后后台服务操作数据库时,参数中的SQL语言直接当SQL运行了,就查出来了数据。其实防范也很简单,就是不让参数当SQL语句执行,而是当成字符串。mybatis的#号,实际上就是将传入的参数在SQL中显示为字符串,这样SQL语句就不会被执行。同时,返回的结果也需要进行处理,不能直接返回,这样就避免了SQL注入”
小Z:“原来如此啊”
老A:“除了考虑接口数据的安全外,还需要考虑稳定性、可靠性。这也是安全要考虑的事情。因为不是所有的黑客攻击都是冲着你的数据来的,也有就是冲着你破坏你来的。让你的服务失效,不能用。比如DDoS攻击,就是 通过大规模的访问,让你的服务拥挤不堪,最后崩溃。这种情况下就该考虑限制请求和提高服务器并发量等去解决了”
小Z:“原来安全有这么多事情啊”
老A:“安全方面,还有很多事情呢,慢慢学吧”