Rails REST API 如何在服务器端暂存数据,以便下次请求使用?

harryyoung · 2016年10月13日 · 最后由 bindiry 回复于 2016年11月01日 · 5325 次阅读

我的应用场景是这样的:

用户进行某项操作,需要先验证,验证通过完成操作创建记录。服务器端先将验证码发送到客户端,客户端将数据连同用户输入的验证码一起发回,和服务器端保存的验证码比对,正确则继续,否则失败。

遇到的问题是: 我在使用 RAILS 5 API,REST API 不建议在服务器端保存状态,默认的 rails 5 api 中 session 都没有启用,所以不能这样暂存验证码;由于用户可能放弃这项操作,不能先创建记录,否则会产生大量冗余数据,所以操作是验证后新建记录,所以不能先将验证码存到数据库中。另外,我在想将验证码做成 token 发送给客户端,客户端再发回来,但感觉也不是很好。

所以,请问,针对这种情况有什么比较好的处理方法?谢谢。

def session
  env[Rack::Session::Abstract::ENV_SESSION_KEY]
end

没有办法,可以加一层 memcache 缓存,所有的 REST API 都可以访问

使用 rails 做 api,你还是马上搭个缓存吧。用缓存的过期机制可以帮你省很多事的!

用 JWT 机制。这有个例子:https://github.com/nsarno/knock

@mimosa 谢谢你的回答,不太懂 Rack,感觉那还是 Session。 @winnie 不是用户验证功能,用户验证用 JWT 确实很好。

@nouse @breeze 我同意比较好的解决方案就是缓存,不过我怎么感觉本质上和 session/cookie 差不多了。Session 也是一种临时存储;为了识别用户,缓存数据的时候我还是要给用户发一个 key,他下次请求还是要带着这个 key,对比 cookie。所以现在都有点怀疑了,Session/cookie 怎么就不 REST 了。

这个问题本质上是:服务端要在若干个 HTTP 请求的工作流中,记住用户相关的验证码数据(验证码) ,方便做后续处理(校验)。这必须把数据临时存放在某个地方。

我能想到的一个解决方法是用 redis 存储验证码,key 设置为用户 id 相关的,value 设置成验证码,再设置一个过期时间(比如 5 分钟)。这样流程就变成:

  • 客户端进入某页面,服务端渲染页面的时候生成验证码,并存入 redis。
  • 客户端输入正确的验证码,提交数据(我很好奇客户如何知道正确的验证码是什么?)
  • 服务端从请求中获得验证码,跟 redis 中的验证码对比,通过则创建数据,然后把 redis 的数据删掉。

更进一步,如果服务端需要记住的验证码是 用户 + 设备 唯一的,则可以考虑服务端为每个创建操作生成一个 uuid,这样服务端记录的 key 是 用户 id + 随机 uuid ,其他部分思路相同就不多说了。

关于 JWT,它是 明文 的,把中间一段抠出来用 base64 解码就可以获得原始数据。它的签名算法只是为了防止数据被中间人篡改,而不是加密数据。显然这是不能用于验证码这种敏感数据的。

突然想到个类似 JWT 的办法,不需要额外的存储系统和类库,就是 ActiveSupport::MessageEncryptor 。它也是把数据 base64 化成一段字符串,但它可以用一个 key 去加密。key 你可以存在任何地方,比如 secrets.yml 里。这可能是成本最低的方案了。

@darkbaby123 jwt 是可以加密的。gem jwt 中 encode 方法默认使用 HS256 算法加密。

我最终使用了 Redis 缓存+jwt 的方式。服务器端先将验证码保存在缓存中,key 是 SecureRandom.urlsafe_base64,然后将 key 放入 jwt 加密发送到客户端。客户端请求的时候发送验证码(注:短信/邮件发给他的)和 token,然后用解密出来的 key 从缓存中取出验证码和客户端发来的比较,通过则继续。。。

一直用 redis+token

@flowerwrong 可以理解为这是在炫耀吗😄

#12 楼 @harryyoung 😄,没懂你。炫耀啥?

@flowerwrong nothing, just joking. 想我辛苦折腾了好几天。😭

jwt 也可以用 token 来来解密,解密正确就算是通过不可以吗

#10 楼 @harryyoung 重新看了一下,不行。拿官方例子来说:

require 'jwt'
require 'base64'

payload = { data: 'test' }
token = JWT.encode payload, 'my$ecretK3y', 'HS256'
# => "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.ZxW8go9hz3ETCSfxFxpwSkYg_602gOPKearsf6DsxgY"

# 取 xxx.yyy.zzz 的中间段,base64 decode 。无需 secret key
Base64.decode64 token.split('.')[1]
# => "{\"data\":\"test\"}"

encode 不是 encrypt。你说的那些算法只是用来数字签名的,目的是确保 token 携带的数据没有被篡改。

顺便说一句,在浏览器端解码 token 并不是一件困难的事情。比如 auth0 的这个 angular-jwt 。因为它做的就是解码 token 获取数据的事情,所以 API 设计就没有任何 key 让你传。给个 token 它就把数据告诉你。

楼主揭示了一个很重要的事情,REST API 非常依赖缓存。我们的 API 一半以上的时间耗费在查询资源和权限验证了,这个时候就觉得还是 RPC 好,几个 REST 请求完全可以一个 RPC 搞定。但是 RPC 的组合性不好,一个接口只有一个用户。

@darkbaby123 你说的对,JWT 确实不适合这种场景,还是自己加密一个 token 比较合适。

基于 session.id 存放在 Memcached 里面

感觉 7# @darkbaby123 的回复应该靠谱。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号