Quantcast
Channel: 簡睿隨筆
Viewing all articles
Browse latest Browse all 897

以Scribe-java實作Google OAuth 2.0的認證機制

$
0
0

OAuth是一個讓我們的程式或網站直接使用認證提供者的帳號密碼來登入的服務(詳見維基百科),例如讓使用者用Google或Facebook的帳號登入自己開發的網站,這樣使用者能最快進入到你的系統裡做基本的操作,如果需要更進一步的權限時再申請正式的帳號。最近以Java實作了OAuth 2.0,留下文章以方便日後參考。以下以Google的處理步驟為例說明。

1. 申請Google Client ID

  1. 瀏覽Google API Console
    OAuth01
  2. 點擊左側的【API Access】
    OAuth02
  3. 〔按Create an OAuth 2.0 client ID…〕
    OAuth03
  4. 輸入產品資訊;Logo圖片以網址方式輸入後按〔Update〕。輸入完畢後按〔Next〕
    OAuth04
  5. 建議按〔more options〕以顯示出更多的欄位並繼續輸入資料。【Authorized Redirect URIs】指的是Google帳號輸入正確後要轉址到那個網頁,此網頁可以是Servlet或JSP,Google會傳入「code」參數。最後按〔Create client ID〕,建立client ID時會檢查提供的轉址網址是否能存取,要能存取才能完成建立動作
    OAuth05
  6. 建立Client ID完成後會顯示彙總網頁。認證處理時需要Client ID、Client secret與Redirect URIs三個欄位的資料
    OAuth06
  7. 我們可以在【Authorized Redirect URIs】填入簡單的oauth2callback.jsp,只顯示Google傳入的code參數,以方便測試:
 
<%
  String code = request.getParameter("code");
  out.println("code=" + code);
%>

2. 安裝scribe-java程式庫

提供OAuth功能的Java套件有很多,我使用的是scribe-java,主要是因為它的介面清楚簡單,也有許多測試程式可以直接測試,同時也直接提供常見的OAuth Providor的API,如Google、Twitter、Dropbox、Evernote 等皆有對應的API可以直接叫用,甚至連Plurk(噗浪)、人人網、新浪微博等都有。

  • scribe-java下載:由https://github.com/fernandezpablo85/scribe-java下載master.zip檔,並解壓縮到你的開發環境裡

但GoogleApi.java用的是OAuth 1.0a而不是2.0,我由另一個分支下載了支援OAuth 2.0的Google2Api.java:

  • scribe-java支援Google OAuth 2.0的API下載
 
package org.scribe.builder.api;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.scribe.exceptions.OAuthException;
import org.scribe.extractors.AccessTokenExtractor;
import org.scribe.model.OAuthConfig;
import org.scribe.model.OAuthConstants;
import org.scribe.model.OAuthRequest;
import org.scribe.model.Response;
import org.scribe.model.Token;
import org.scribe.model.Verb;
import org.scribe.model.Verifier;
import org.scribe.oauth.OAuthService;
import org.scribe.utils.Preconditions;

public class Google2Api extends DefaultApi20 {

  private static final String AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=%s&redirect_uri=%s&scope=%s";

  @Override
  public String getAccessTokenEndpoint() {
    return "https://accounts.google.com/o/oauth2/token?grant_type=authorization_code";
  }

  @Override
  public String getAuthorizationUrl(OAuthConfig config) {
    Preconditions
        .checkValidUrl(config.getCallback(),
            "Must provide a valid url as callback. Google does not support OOB");
    Preconditions
        .checkEmptyString(config.getScope(),
            "Must provide a valid value as scope. Google does not support no scope");

    String _sResult = "";
    try {
      _sResult = String.format(AUTHORIZE_URL, config.getApiKey(),
          URLEncoder.encode(config.getCallback(), "UTF-8"),
          URLEncoder.encode(config.getScope(), "UTF-8"));
    } catch (UnsupportedEncodingException uee) {
      throw new IllegalStateException("Cannot find specified encoding: UTF-8");
    }

  return _sResult;
}

  @Override
  public Verb getAccessTokenVerb() {
    return Verb.POST;
  }

  @Override
  public OAuthService createService(OAuthConfig config) {
    return new GoogleOAuthService(this, config);
  }

  @Override
  public AccessTokenExtractor getAccessTokenExtractor() {
    return new GoogleJsonTokenExtractor();
  }

private static final class GoogleOAuthService implements OAuthService {
  private static final String VERSION = "2.0";

  private static final String GRANT_TYPE = "grant_type";
  private static final String GRANT_TYPE_VALUE = "authorization_code";

  private final DefaultApi20 api;
  private final OAuthConfig config;

  /**
   * Default constructor
   *
   * @param api    OAuth2.0 api information
   * @param config OAuth 2.0 configuration param object
   */
  public GoogleOAuthService(DefaultApi20 api, OAuthConfig config) {
    this.api = api;
    this.config = config;
  }

  /**
   * {@inheritDoc}
   */
  public Token getAccessToken(Token requestToken, Verifier verifier) {
    OAuthRequest request = new OAuthRequest(api.getAccessTokenVerb(),
        api.getAccessTokenEndpoint());
    request.addBodyParameter(OAuthConstants.CLIENT_ID,
        config.getApiKey());
    request.addBodyParameter(OAuthConstants.CLIENT_SECRET,
        config.getApiSecret());
    request.addBodyParameter(OAuthConstants.CODE, verifier.getValue());
    request.addBodyParameter(OAuthConstants.REDIRECT_URI,
        config.getCallback());
    if (config.hasScope())
      request.addBodyParameter(OAuthConstants.SCOPE,
          config.getScope());
    request.addBodyParameter(GRANT_TYPE, GRANT_TYPE_VALUE);
    Response response = request.send();
    return api.getAccessTokenExtractor().extract(response.getBody());
  }

  /**
   * {@inheritDoc}
   */
  public Token getRequestToken() {
    throw new UnsupportedOperationException(
        "Unsupported operation, please use 'getAuthorizationUrl' and redirect your users there");
  }

  /**
   * {@inheritDoc}
   */
  public String getVersion() {
    return VERSION;
  }

  /**
   * {@inheritDoc}
   */
  public void signRequest(Token accessToken, OAuthRequest request) {
    request.addQuerystringParameter(OAuthConstants.ACCESS_TOKEN,
        accessToken.getToken());
  }

  /**
   * {@inheritDoc}
   */
  public String getAuthorizationUrl(Token requestToken) {
    return api.getAuthorizationUrl(config);
  }
}

private static final class GoogleJsonTokenExtractor implements
    AccessTokenExtractor {
  private Pattern accessTokenPattern = Pattern
      .compile("\"access_token\"\\s*:\\s*\"(\\S*?)\"");

  public Token extract(String response) {
    Preconditions.checkEmptyString(response,
        "Cannot extract a token from a null or empty String");
    Matcher matcher = accessTokenPattern.matcher(response);
    if (matcher.find()) {
      return new Token(matcher.group(1), "", response);
    } else {
      throw new OAuthException(
          "Cannot extract an acces token. Response was: "
              + response);
    }
  }
}
}

將Google2Api.java存入scribe/main/java/org/scribe/builder裡,並撰寫測試的google_login.html:

<a href="https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=你的Client ID&redirect_uri=接收code參數的轉址網址&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email">以Google帳戶登入</a>

在瀏覽器執行google_login.html若能看到下列內容表示已能存取Google帳戶且能傳出Access Token code:
OAuth08

3. 完成最後的認證

先前的oauth2callback.jsp只將接收到的code參數顯示在網頁裡,以下是oauth2callback.jsp後續的處理:

<%@ page contentType="text/html;charset=UTF-8" %>
<%@ page import="org.scribe.builder.ServiceBuilder" %>
<%@ page import="org.scribe.builder.api.Google2Api" %>
<%@ page import="org.scribe.model.*" %>
<%@ page import="org.scribe.oauth.OAuthService" %>
<%
  String _sCode = request.getParameter("code");
  out.println("code=" + _sCode + "<br>");

  Verifier verifier = new Verifier(_sCode);
  String apiKey = "你的Client ID";
  String apiSecret = "你的Client secret";
  String redirect_uri = "你的轉址網址";

  OAuthService service = new ServiceBuilder()
      .provider(Google2Api.class)
      .apiKey(apiKey)
      .apiSecret(apiSecret)
      .callback(redirect_uri)
      .scope("https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email")
      .build();

  Token accessToken = service.getAccessToken(null, verifier);

  // Now let's go and ask for a protected resource!
  OAuthRequest req = new OAuthRequest(Verb.GET, "https://www.googleapis.com/oauth2/v1/userinfo?alt=json");
  service.signRequest(accessToken, req);
  Response resp = req.send();
  // Got it! Lets see what we found...
  String _sBody = resp.getBody();

  out.println(resp.getCode() + "<br>");
  out.println("Body=" + _sBody);
%>

resp.getBody()收到的是一個類似下列的JSON物件:

 
{ "id": "帳戶ID", "email": "電子郵件@gmail.com", "verified_email": true,
  "name": "姓名", "given_name": "名", "family_name": "姓",
  "link": "https://plus.google.com/Google+的連結",
  "picture": "https://lh3.googleusercontent.com/帳戶圖片.jpg",
  "gender": "male", "locale": "zh-TW" }

Facebook取到的JSON如下:

 
{"id":"Facebook ID","name":"姓名","first_name":"名",
  "last_name":"姓","link":"Facebook頁的連結",
  "username":"使用者代碼","bio":"描述",
  "gender":"male","email":"電子郵件","timezone":8,"locale":"zh_TW",
  "verified":true,"updated_time":"2013-02-05T03:39:27+0000"}

相關參考


##

您可能也會有興趣的類似文章

PDF Plurk Google Bookmarks del.icio.us Live Add to favorites email

The post 以Scribe-java實作Google OAuth 2.0的認證機制 appeared first on 簡睿隨筆.


Viewing all articles
Browse latest Browse all 897

Trending Articles