前段时间用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验证,如果通过则返回客户端的请求头字符串,否则返回相应的验证失败代码