Skip to content

Instantly share code, notes, and snippets.

@lqt0223
Last active September 7, 2022 07:56
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lqt0223/21708835af14147c9aed740c57cb53dd to your computer and use it in GitHub Desktop.
Save lqt0223/21708835af14147c9aed740c57cb53dd to your computer and use it in GitHub Desktop.
04 Google OAuth2.0 in Java with no library

使用Java 无类库完成Google OAuth2.0验证

对于我们这种初学者来说,涉及到网络的编程学问真的很多。如果需要了解每一个网络请求或服务器的细节的话,协议、安全验证、跨域、请求头的每项的意义之类,需要了解的东西真的非常多。

这篇博文,我想从很简单的角度出发,总结一下网络编程中最基本的两项任务ーー建立一个服务器、发起一个请求ーー在Java中的实现。

以实现一个实际所需要的功能来进行学习,是很有效的学习方法。所以这一次我选择的课题就是:用Java建立一个服务器端程序,用它来完成Google的OAuth2.0验证。

这个小课题帮我GET到的Java小技能主要有以下这些,还是收获挺大的。

  • HttpURLConnection发起请求
  • org.json解析JSON
  • HttpServer创建服务器,设定好路由
  • 各种读取/写入文件流,网络请求流

Google OAuth2.0验证简介

Google OAuth2.0验证是调用Google API所需的验证方式之一,验证时用户会被重定向到Google的“同意使用您的信息”页面,用户点击同意后,开发者所开发的应用便会得到授权,可以使用用户的个人信息。开发者可以通过调用Google API(需要加上服务器返回的access token)来获得自己需要的数据。

针对这次的例子(服务器端程序进行Google OAuth2.0验证),大家可以先看看这个链接,Using OAuth 2.0 for Web Server Applications,了解一下这一验证的步骤。简单来说,完成验证需要6步:

  1. 前期准备:在Google API Console(控制台)网站上新建一个project,并为它创建一个OAuth client ID类型的Credentials。
    1. Client ID 的用途,选择 Web application
    2. 创建的Credentials中会自动包含两个字段:client_id和client_secret。
    3. Credentials中还有一个字段叫做redirect_uris,这个需要自己设置。在网页上的"Authorized redirect URIs"中,填入类似 http://yourhost/oauth2callback 的地址形式。
    4. (因为大家一般是在本地服务器上开发,所以建议设置成http://127.0.0.1:8000/oauth2callback ,其中端口以及后面的path任意)。
    5. Credentials的内容,可以点击控制台上的"Download JSON"下载,以后需要读取这些敏感的字段时,可以从该JSON文件中提取。
  2. 确认好访问作用域(Access scopes),这个是Google自己的概念,大意是说自己需要调用具体哪种Google API,每种Google API都有一个对应的网址来表示这个作用域。详情可见OAuth 2.0 Scopes for Google APIs
  3. 当用户运行你的服务器程序时,将用户重定向至Google的OAuth2.0验证服务器,这时浏览器中会载入Google提供的验证页面(名叫consent screen)。
  4. 用户点击确认后,Google的服务器会向你在第1-2步中设置好的一个redirect_uri发回一个请求,请求的querystring中第一个参数叫做authorization code。这个不是最终的access token,但可以通过此code拿到token。
  5. 你的服务器需要以POST方式,设置好需要的参数后,再一次向Google的验证服务API发起请求,以拿到最后的access token。
  6. 拿到access token就可以愉快地使用Google的各种API啦。

开始实现

上面的机制简直看得人头晕,不过这些都是人为造成的。这次的重点在于如何手工地实现这些步骤,虽然这其中只包含了几个比较基础的创建服务器、发起请求的操作,但对于我来说足够作为练习了。

Google为了方便开发者使用它的OAuth2.0验证,还提供了各种语言的API客户端类库(Google API Client Library),把GAE,安卓,Web前端应用,Web服务器应用等环境下的Google API的验证和调用都封装得十分完善。

但这次为了学习怎么样“人肉”完成验证,我们先绕过这个库,有兴趣的小伙伴请参考API Client Library for Java,以及其他语言的版本。

步骤1、2

是在Google API Console的页面上的操作以及查阅相关资料,这里不再赘述。

步骤3、4、5、6

先看代码。

// GoogleOAuthDemo.java

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import org.json.JSONException;
import org.json.JSONObject;
import sun.net.www.http.PosterOutputStream;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.URL;
import java.util.HashMap;

public class GoogleOAuthDemo {

    private static HashMap clientSecretDataMap;
	
  	// 读取client_secret.json文件中的内容
    private static HashMap<String, String> readClientSecretJSON(String filePath){
        // 读文件内容用的变量
        BufferedReader br = null;
        String line;
        String fileContent = "";

        // 解析JSON用的变量
        JSONObject jsonObject;
        Object parsed;

        // 返回用的变量,一个HashMap
        HashMap<String, String> map = new HashMap<String, String>();

        try{
            br = new BufferedReader(new FileReader(filePath));
        }catch (FileNotFoundException e){
            e.printStackTrace();
        }

        try{
            while((line = br.readLine()) != null) {
                fileContent += line;
            }
        }catch (IOException e) {
            e.printStackTrace();
        }

        try{
            parsed = new JSONObject(fileContent).get("web");
            jsonObject = (JSONObject)parsed;
            String clientId = jsonObject.getString("client_id");
            String clientSecret = jsonObject.getString("client_secret");
            String redirectURI = jsonObject.getJSONArray("redirect_uris").getString(0);
            try{
                map.put("client_id", clientId);
                map.put("client_secret",clientSecret);
                map.put("redirect_uri",redirectURI);
            }catch (NullPointerException e){
                e.printStackTrace();
            }

        }catch (JSONException e){
            e.printStackTrace();
        }
        return map;
    }

    public static void main(String[] args) {
        // 读取json文件
        final String CLIENT_SECRET_JSON_PATH = "src/main/java/client_secret.json";
        clientSecretDataMap = readClientSecretJSON(CLIENT_SECRET_JSON_PATH);

        try{
            // 在端口8000上运行服务器
            HttpServer server = HttpServer.create(new InetSocketAddress(8000),50);
          
            // 为两个/path设置好对应的回调方法,类似于其他服务器框架说的“路由”或“映射”
          	// 首先设置开始验证的入口 /oauth2
            server.createContext("/oauth2", new HttpHandler(){
                public void handle(HttpExchange httpExchange) throws IOException {
                    handleAuth(httpExchange);
                }
            });
          
            // 然后设置一个入口处理Google服务器传回的请求
            server.createContext("/oauth2callback", new HttpHandler() {
                public void handle(HttpExchange httpExchange) throws IOException {
                    handleAuthCallback(httpExchange);
                }
            });
            // 启动服务器
            server.start();
            System.out.println("Server running at 8000...");
        }catch (IOException e){
            e.printStackTrace();
        }
    }
	
  	//步骤3,重定向
    private static void handleAuth(HttpExchange httpExchange){
        // 拼接好重定向到Google consent page的地址
        final String GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
        final String YOUTUBE_SCOPE = "https://www.googleapis.com/auth/youtube.force-ssl";
        String googleRedirectURL = GOOGLE_AUTH_URL
                + "?scope=" + YOUTUBE_SCOPE
                + "&response_type=code"
                + "&client_id=" + clientSecretDataMap.get("client_id")
                + "&redirect_uri=" + clientSecretDataMap.get("redirect_uri");

        // 设置返回头中的Location字段,设置返回码为302,完成重定向
        try{
            httpExchange.getResponseHeaders().set("Location", googleRedirectURL);
            httpExchange.sendResponseHeaders(302,-1);
            // 记得需要手动关闭这次httpExchange
            httpExchange.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

  	//步骤4,获取 authorization code
    private static void handleAuthCallback(HttpExchange httpExchange){
        // 从Google服务器的传回的请求中取出 authorization code
        String code = httpExchange.getRequestURI().getQuery().substring(5);

        // 这一次我们需要向Google的另一个API发一个HTTPS POST请求,换到access token
        final String GOOGLE_GET_TOKEN_API_URL = "https://www.googleapis.com/oauth2/v4/token";
        String paramString = "code=" + code
                + "&client_id=" + clientSecretDataMap.get("client_id")
                + "&client_secret=" + clientSecretDataMap.get("client_secret")
                + "&redirect_uri=" + clientSecretDataMap.get("redirect_uri")
                + "&grant_type=authorization_code";

        //步骤5 发出POST请求
        HttpURLConnection con;
        try{
            URL url = new URL(GOOGLE_GET_TOKEN_API_URL);
            con = (HttpURLConnection)url.openConnection();

            // 为方便debug打印一行提示
            System.out.println("Posting...");
            con.setRequestMethod("POST");

            // POST方法,需要写入POST到服务器的数据时,记得设置setDoOutput为true
            con.setDoOutput(true);
            PosterOutputStream pos = (PosterOutputStream)con.getOutputStream();
            pos.write(paramString.getBytes());

            // 处理返回结果
            // 因为GFW,我并不能每次都拿到正确的结果,因此这个实现暂时无法进行下去
            // 这一次我只把正确的结果打印出来
            InputStream is = con.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            String line;
            String responseText = "";
            while((line = br.readLine()) != null){
                responseText += line;
            }

            // 结果是一个JSON的字符串
            // 未完成的步骤6 
          	// 如果你的网络情况良好,可以试试进一步解析这个JSON,并拿到access token,最终调用Google API
            System.out.println(responseText);
            con.disconnect();

        }catch (IOException e){
            e.printStackTrace();
        }
        httpExchange.close();
    }
}
@yangleimiao
Copy link

仿照您的文章尝试了一下,并把步骤6也实现啦,谢谢您的文章的指导~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment