对于我们这种初学者来说,涉及到网络的编程学问真的很多。如果需要了解每一个网络请求或服务器的细节的话,协议、安全验证、跨域、请求头的每项的意义之类,需要了解的东西真的非常多。
这篇博文,我想从很简单的角度出发,总结一下网络编程中最基本的两项任务ーー建立一个服务器、发起一个请求ーー在Java中的实现。
以实现一个实际所需要的功能来进行学习,是很有效的学习方法。所以这一次我选择的课题就是:用Java建立一个服务器端程序,用它来完成Google的OAuth2.0验证。
这个小课题帮我GET到的Java小技能主要有以下这些,还是收获挺大的。
- HttpURLConnection发起请求
- org.json解析JSON
- HttpServer创建服务器,设定好路由
- 各种读取/写入文件流,网络请求流
Google OAuth2.0验证是调用Google API所需的验证方式之一,验证时用户会被重定向到Google的“同意使用您的信息”页面,用户点击同意后,开发者所开发的应用便会得到授权,可以使用用户的个人信息。开发者可以通过调用Google API(需要加上服务器返回的access token)来获得自己需要的数据。
针对这次的例子(服务器端程序进行Google OAuth2.0验证),大家可以先看看这个链接,Using OAuth 2.0 for Web Server Applications,了解一下这一验证的步骤。简单来说,完成验证需要6步:
- 前期准备:在Google API Console(控制台)网站上新建一个project,并为它创建一个OAuth client ID类型的Credentials。
- Client ID 的用途,选择 Web application
- 创建的Credentials中会自动包含两个字段:client_id和client_secret。
- Credentials中还有一个字段叫做redirect_uris,这个需要自己设置。在网页上的"Authorized redirect URIs"中,填入类似 http://yourhost/oauth2callback 的地址形式。
- (因为大家一般是在本地服务器上开发,所以建议设置成http://127.0.0.1:8000/oauth2callback ,其中端口以及后面的path任意)。
- Credentials的内容,可以点击控制台上的"Download JSON"下载,以后需要读取这些敏感的字段时,可以从该JSON文件中提取。
- 确认好访问作用域(Access scopes),这个是Google自己的概念,大意是说自己需要调用具体哪种Google API,每种Google API都有一个对应的网址来表示这个作用域。详情可见OAuth 2.0 Scopes for Google APIs。
- 当用户运行你的服务器程序时,将用户重定向至Google的OAuth2.0验证服务器,这时浏览器中会载入Google提供的验证页面(名叫consent screen)。
- 用户点击确认后,Google的服务器会向你在第1-2步中设置好的一个redirect_uri发回一个请求,请求的querystring中第一个参数叫做authorization code。这个不是最终的access token,但可以通过此code拿到token。
- 你的服务器需要以POST方式,设置好需要的参数后,再一次向Google的验证服务API发起请求,以拿到最后的access token。
- 拿到access token就可以愉快地使用Google的各种API啦。
上面的机制简直看得人头晕,不过这些都是人为造成的。这次的重点在于如何手工地实现这些步骤,虽然这其中只包含了几个比较基础的创建服务器、发起请求的操作,但对于我来说足够作为练习了。
Google为了方便开发者使用它的OAuth2.0验证,还提供了各种语言的API客户端类库(Google API Client Library),把GAE,安卓,Web前端应用,Web服务器应用等环境下的Google API的验证和调用都封装得十分完善。
但这次为了学习怎么样“人肉”完成验证,我们先绕过这个库,有兴趣的小伙伴请参考API Client Library for Java,以及其他语言的版本。
是在Google API Console的页面上的操作以及查阅相关资料,这里不再赘述。
先看代码。
// 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();
}
}
仿照您的文章尝试了一下,并把步骤6也实现啦,谢谢您的文章的指导~