Turbolinks Deivse 登录相关

我们想要在使用devise登录的时候实现用在form提交时候使用remote: true来提交表单。表单提交通过之后使用Turbolinks跳转到其他页面。
如果有验证错误则在当前页面显示验证错误。

为什么要这样做? 因为在Turbolinks-ios中无法用html方法提交表单。提交表单之后的跳转也必须由turbolinks来完成。

首先准备好需要render的js模版。

1
2
3
4
5
6
7
8
9
10
11
12
13
<% if resource && resource.errors.empty? %>
<% flash[:success] = "登录成功"; %>
Turbolinks.visit('<%=@redirect_to %>',{ action: "replace" });
<% else %>
$('#headFlash').html(`
<div class="alert alert-warning alert-dismissible fade in" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<span><%= flash[:alert] %></span>
</div>
`);
<% end %>

我们需要overwrite devise的sessionsController中的create方法。

1
2
3
4
5
6
7
8
9
class Users::SessionsController < Devise::SessionsController

def create
self.resource = warden.authenticate!(auth_options)
sign_in(resource_name, resource)
yield resource if block_given?
@redirect_to = after_sign_in_path_for(resource)
end
end

注意需要在router配置中标明我们进行了Devise::SessionsController的overwrite。

跑一下。登录成功,跳转。

故意输错密码,登录失败,但是直接server端返回了401,而并没有渲染create.js.erb模版。

这个是为什么?

重点在这里。warden.authenticate!(auth_options) warden的默认策略直接跳过下面的步骤直接返回给我们401了。我们需要告诉warden我们期待的是什么处理。

这里需要对oauth_options中的recall 进行overwrite。

注意到使用remote: true默认的request的format是:js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Users::SessionsController < Devise::SessionsController

respond_to :js
def create
self.resource = warden.authenticate!(auth_options)
sign_in(resource_name, resource)
yield resource if block_given?
@redirect_to = after_sign_in_path_for(resource)
end

def failure
render :create
end
protected
def auth_options
{ scope: resource_name, recall: "#{controller_path}#failure" }
end
end

故意输错一下试一试,特么为什么还是直接返回401而没有使用我们定义的failure方法?
去看一下devise initializer里面配置的说明。

1
2
3
4
5
6
7
8
9
# ==> Navigation configuration
# Lists the formats that should be treated as navigational. Formats like
# :html, should redirect to the sign in page when the user does not have
# access, but formats like :xml or :json, should return 401.
# If you have any extra navigational formats, like :iphone or :mobile, you
# should add them to the navigational formats lists.
#
# The "*/*" below is required to match Internet Explorer requests.
# config.navigational_formats = ['*/*', :html]

那么就是说只要config.navigational_formats = ['*/*', :html, :js]就ok了。

添加之后再试一下,还是不行。搜索一番。据说要添加

1
config.http_authenticatable_on_xhr = false

这特么是什么鬼。看一下devise default的failure app。

1
2
3
4
5
6
7
8
9
def respond
if http_auth?
http_auth
elsif warden_options[:recall]
recall
else
redirect
end
end

我们看到只有http_auth? 为false我们才会调用recall。

1
2
3
4
5
6
7
def http_auth?
if request.xhr?
Devise.http_authenticatable_on_xhr
else
!(request_format && is_navigational_format?)
end
end

可以看到我们只有设置Devise.http_authenticatable_on_xhr 为 false才使得验证失败时候的处理落到我们自己定义的recall中。

以上。

Turbolinks-ios中使用Devise进行登录

在Turbolinks-ios使用Devise的网页页面进行登录需要注意将表单变成ajax形式提交。

Rails部分

  • Rails的页面中,中使用ajax提交登录表单。

  • overwrite devise的session的create方法。结束时候渲染create.js.erb模版。

  • create.js.erb中使用Turbolinks.visit(after_url, {action: "replace"})方法转向登录后的页面。

Swift 部分

  • delegate页面中的turbolinks的链接时候对应的处理。
1
2
3
4
5
6
7
8
9
10
11
extension ViewController: SessionDelegate {
func session(_ session: Session, didProposeVisitToURL URL: URL, withAction action: Action) {
visit(URL: URL as NSURL, action: action)
}

func session(_ session: Session, didFailRequestForVisitable visitable: Visitable, withError error: NSError) {
let alert = UIAlertController(title: "Error", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
present(alert, animated: true, completion: nil)
}
}
  • visit函数中按传来的action进行navigationController stack的替换。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func visit(URL: NSURL, action: Action) {
let visitable = WebViewController()
visitable.visitableURL = URL as URL!
switch action {
case .Advance:
pushViewController(visitable, animated: true)
break
case .Replace:
popViewController(animated: false)
pushViewController(visitable, animated: false)
break
default:
pushViewController(visitable, animated: true)
break
}
session.visit(visitable)
}

要特别注意的是处理replace的时候我们需要把登录页面从navigationController的历史里替换掉,否则我们登录之后点击返回上一页会返回登录页面的快照。
另外注意animated: false,这样用户就看不到我们替换的过程。