前段时间用ruby实现了新浪微博的简易Oauth的客户端,对aouth协议有了一个大概的了解。
完成服务器端的实现,纯属自己一个的加深学习aouth的想法,嘿嘿. 验证支持basic,oauth,xauth
接收下用到的controller
OauthController 负责对用户aouth验证和发放accessToken
Oauth_base_controller 所有需要aouth验证的controller的父类,对子类的所有方法进行权限验证
一个帮助类
OauthUtil 负责字符串的加密和拼接
OauthController提供三个对外方法:
request_token
authorize
access_token
具体方法含义,对应oauth验证的每个url。
具体代码如下
# coding: utf-8
# HTTP 400 Bad Request
# o Unsupported parameter
# o Unsupported signature method
# o Missing required parameter
# o Duplicated OAuth Protocol Parameter
# HTTP 401 Unauthorized
# o Invalid Consumer Key
# o Invalid / expired Token
# o Invalid signature
# o Invalid / used nonce
class OauthController < Oauth_base_controller
TEST_APP_KEY = "123456"
TEST_APP_SECRET = "654321"
TEST_OAUTH_TOKEN = "QWERTY"
TEST_OAUTH_TOKEN_SECRET ="YUIOP"
TEST_OAUTH_VERIFIER = "ASDFG"
TEST_ACCESS_TOKEN = "HJKLG"
TEST_ACCESS_TOKEN_SECRET = "ZXCVB"
protect_from_forgery :except => [:request_token,:authorize,:access_token]
skip_before_filter :auth
def request_token
oauth_signature = params["oauth_signature"]
puts "=======================params======================",params.inspect
render :json=>%({"error":400,"detail":"need signature"}),:status => 400,:callback=>params[:callback] and return if oauth_signature.blank?
oauth_signature = CGI::unescape oauth_signature
oauth_params = {
:oauth_consumer_key => params["oauth_consumer_key"],
:oauth_timestamp => params["oauth_timestamp"],
:oauth_nonce => params["oauth_nonce"],
:oauth_version => params["oauth_version"] || "1.0",
:oauth_signature_method => "HMAC-SHA1"
}
oauth_params[:oauth_callback] = params["oauth_callback"] if params["oauth_callback"]
oauth_params[:oauth_body_hash] = params["oauth_body_hash"] if params["oauth_body_hash"]
httpmethod = request.method.to_s
base_uri = "http://#{request.headers["HTTP_HOST"]}/oauth/request_token"
key = "#{TEST_APP_SECRET}&"
create_base_string = OauthUtil.create_oauth_signature(httpmethod.upcase, base_uri, oauth_params, key)
puts "===============oauth_signature,create_base_string",oauth_signature,create_base_string
render :json=>%({"error":401,"detail":"Invalid signature"}),:status => 401,:callback=>params[:callback] and return if create_base_string != oauth_signature
render :text => "oauth_token=#{TEST_OAUTH_TOKEN}&oauth_token_secret=#{TEST_OAUTH_TOKEN_SECRET}" if params["oauth_callback"].nil?
render :text => "oauth_token=#{TEST_OAUTH_TOKEN}&oauth_token_secret=#{TEST_OAUTH_TOKEN_SECRET}&oauth_callback_confirmed=true" if params["oauth_callback"]
end
def authorize
@token = params[:oauth_token]
@oauth_callback = params[:oauth_callback]
render :text => "no token error" and return if @token.blank?
if request.get?
@name = "测试应用"
render :action => "authorize"
return
end
if request.post?
username = params[:username]
password = params[:password]
if username.nil? || password.nil? || username != "test" || password !="test"
flash[:notice] = "用户名或密码错误"
render :action => "authorize"
return
else
render :text => "授权已完成" and return if params[:oauth_callback].blank?
callback = OauthUtil.callback_url(CGI::unescape(params[:oauth_callback]), @token, TEST_OAUTH_VERIFIER)
redirect_to callback
return
end
end
return :text=>""
end
def access_token
oauth_signature = params["oauth_signature"]
render :json=>%({"error":400,"detail":"need signature"}),:status => 400,:callback=>params[:callback] and return if oauth_signature.blank?
oauth_signature = CGI::unescape oauth_signature
puts "=======================params======================",params.inspect
## for oauth
oauth_params = {
:oauth_consumer_key => params["oauth_consumer_key"],
:oauth_token => params["oauth_token"],
:oauth_timestamp => params["oauth_timestamp"],
:oauth_nonce => params["oauth_nonce"],
:oauth_version => params["oauth_version"] || "1.0",
:oauth_signature_method => "HMAC-SHA1"
}
## for xauth
oauth_params = {
:x_auth_username => params["x_auth_username"],
:x_auth_password => params["x_auth_password"],
:x_auth_mode => "client_auth",
:oauth_consumer_key => params["oauth_consumer_key"],
:oauth_timestamp => params["oauth_timestamp"],
:oauth_nonce => params["oauth_nonce"],
:oauth_version => "1.0",
:oauth_signature_method => "HMAC-SHA1"
} if xauth?
render :json=>%({"error":403,"detail":"unsupport XAuth"}),:status => 403,:callback=>params[:callback] and return if xauth?
render :json=>%({"error":401,"detail":"Invalid signature"}),:status => 401,:callback=>params[:callback] and return if params["x_auth_username"] != "test" || params["x_auth_password"] != "test"
oauth_params[:oauth_body_hash] = params["oauth_body_hash"] if params["oauth_body_hash"]
oauth_params[:oauth_verifier] = params["oauth_verifier"] if params["oauth_verifier"]
httpmethod = request.method.to_s
base_uri = "http://#{request.headers["HTTP_HOST"]}/oauth/access_token"
key = "#{TEST_APP_SECRET}&#{TEST_OAUTH_TOKEN_SECRET}"
key = "#{TEST_APP_SECRET}&" if xauth? ## for xauth
create_base_string = OauthUtil.create_oauth_signature(httpmethod.upcase, base_uri, oauth_params, key)
puts "===============oauth_signature,create_base_string",oauth_signature,create_base_string
render :json=>%({"error":401,"detail":"Invalid signature "}),:status => 401,:callback=>params[:callback] and return if create_base_string != oauth_signature
render :text => "oauth_token=#{TEST_ACCESS_TOKEN}&oauth_token_secret=#{TEST_ACCESS_TOKEN_SECRET}"
end
def xauth?
params["x_auth_username"] || params["x_auth_password"]
end
end
其中OauthUtil类源码如下
# coding: utf-8
require "uri"
class OauthUtil
class << self
## 生成base_string 字符串
def base_string(httpmethod,base_uri,request_params)
base_str = httpmethod + "&" + CGI::escape(base_uri) + "&"
base_str += request_params.to_a.sort{|a,b| a[0].to_s <=> b[0].to_s}.map{|param| CGI::escape(param[0].to_s) + "%3D"+ CGI::escape(param[1].to_s)}.join("%26")
puts "===========base_string===========",base_str
base_str
end
## see http://stackoverflow.com/questions/1959486/digest-hmac-is-part-of-ruby-standard-lib
def digest(value,key)
puts "===========digest_key===========",key
signature = Base64.encode64 OpenSSL::HMAC.digest("SHA1", key, value)
puts "===========signature===========",signature.strip
signature.strip
end
def create_oauth_signature(httpmethod,base_uri,request_params,key)
create_base_string = base_string httpmethod,base_uri,request_params
digest create_base_string,key
end
def callback_url(url,oauth_token,oauth_verifier)
begin
uri = URI::parse(url)
rescue Exception
return ""
end
query_string = uri.query
query_string = "&#{query_string}" if query_string
"#{uri.scheme}://#{uri.host}:#{uri.port}#{uri.path}?oauth_token=#{oauth_token}&oauth_verifier=#{oauth_verifier}#{query_string}"
end
end
end
涉及到的erb页面authorize.html.erb页面代码如下
是否要授权<%=@name%>应用,使用你在本网站的部分功能?
<form action="/oauth/authorize" method="post">
<input type="hidden" value="<%=@token%>" name="oauth_token"/>
<input type="hidden" value="<%=@oauth_callback%>" name="oauth_callback"/>
账号:<input type="text" name="username"></input><br />
密码:<input type="password" name="password"></input><br />
<input type="submit" value="授权"/>
</form>
如果不想授权,请关闭此页面。
<p><%=flash[:notice]%></p>
Oauth_base_controller 主要方法为auth,为子类提供验证,具体源码如下:
# coding: utf-8
class Oauth_base_controller < ApplicationController
before_filter :handle_headers_oauth_string
before_filter :auth
## 进行验证
def auth
authenticate = request.headers["AUTHORIZATION"] || request.headers["HTTP_AUTHORIZATION"]
if authenticate.blank?
auth_oauth
else
authenticate_method = authenticate[0,5]
authenticate_body = authenticate[5,authenticate.size - 5]
case authenticate_method
when "Basic"
@name_pwd = Base64.decode64(authenticate_body.strip)
puts "name_pwd",@name_pwd
render :json=>%({"error":401,"detail":"authenticate format error"}),:status => "401",:callback=>params[:callback] and return false if @name_pwd.split(":").size != 2
auth_basic
else "OAuth"
auth_oauth
end
end
end
## 要移除的相关非oauth计算签名参数
def except_other_params
except_params "realm",params["realm"]
except_params "oauth_signature",params["oauth_signature"]
except_params "action",params["action"]
except_params "controller",params["controller"]
end
## 将非oauth计算签名所需参数移动到except_params中去
def except_params(key,value)
@except_params = {} if @except_params.nil?
@except_params[key] = value
params.delete key.to_s
params.delete key.to_sym
end
private
## 进行 Basic 验证
def auth_basic
username = @name_pwd.split(":")[0]
password = @name_pwd.split(":")[1]
render :json=>%({"error":401,"detail":"authenticate fail"}),:status => "401",:callback=>params[:callback] and return false if username != "test" || password != "test"
end
## 进行oauth 验证
def auth_oauth
params["oauth_version"] ||= "1.0"
puts "=======================params======================",params.inspect
oauth_signature = params["oauth_signature"]
render :json=>%({"error":400,"detail":"need signature"}),:status => "400",:callback=>params[:callback] and return if oauth_signature.blank?
oauth_signature = CGI::unescape oauth_signature
except_other_params
httpmethod = request.method.to_s
base_uri = "http://#{request.headers["HTTP_HOST"]}#{request.path}"
key = "#{app_secret_by_oauth_consumer_key params["oauth_consumer_key"]}&#{token_secret_by_access_token params["oauth_token"]}"
create_base_string = OauthUtil.create_oauth_signature(httpmethod.upcase, base_uri, params, key)
puts "===============oauth_signature,create_base_string",oauth_signature,create_base_string
render :json=>%({"error":401,"detail":"Invalid signature"}),:status => "401",:callback=>params[:callback] and return if create_base_string != oauth_signature
end
## 查找app_key 对应的secret
def app_secret_by_oauth_consumer_key(app_key)
"654321"
end
## 查找access_token 对应的secret
def token_secret_by_access_token(access_token)
"ZXCVB"
end
## 整理header里面的oauth 的参数到params里面去
def handle_headers_oauth_string
authenticate = request.headers["AUTHORIZATION"] || request.headers["HTTP_AUTHORIZATION"]
return true if authenticate.blank?
oauth_method = authenticate[0,5]
return true if oauth_method == "Basic"
render :json=>%({"error":401,"detail":"http_headers content error"}),:status => "401",:callback=>params[:callback] and return false if oauth_method != "OAuth"
oauth_body = authenticate[5,authenticate.size - 5]
oauth_body.split(",").each do |header_param|
next if header_param.split("=").size != 2
k = header_param.split("=")[0].strip
v = header_param.split("=")[1].strip.gsub(/\"/,"")
params[k] = v
end
end
end
使用方式为,继承Oauth_base_controller,然后子类中的所有方法则都要进行验证后才能访问,如:
# coding: utf-8
class OauthTestController < Oauth_base_controller
def index
puts "=======================",request.headers.inspect
render :text => request.headers.inspect
end
end
当访问这个index方法的时候,会进行oauth或basic验证,如果通过则返回客户端的请求头字符串,否则返回相应的验证失败代码