OAuth2简介
OAuth2也称为"第三方登录"。即使用第三方网站进行身份认证。并获取其在第三方网站的身份数据。OAuth2是一种被广泛使用的身份认证方式,网上的介绍的文章也很多,大家可以参考阮一峰写的3篇介绍文档(https://www.ruanyifeng.com/blog/2019/04/oauth_design.html)。
网上提供第三方认证的平台很多,包括(QQ, 微信,GitHub, gitee,华为,…),大家可以常考: (https://blog.csdn.net/taiyi7627/article/details/115392886), 介绍比较强全。目前看国内提供的平台,个人开发者使用,都是需要审核的,不过目测并不难通过。本文选择使用GitHub的第三方认证平台做为案例,主要是因为个人有现成账号,且不需要审核,而且网上介绍的文章也非常多,缺点也很明显,国内访问非常不稳定,国外平台都一样,无奈。当然做技术的也是有办法的,这类话题不适合拿出来讨论,反正各显神通自己克服吧。
应用注册
要使用第三方平台,首先需要在第三方平台注册你的应用,其中最重要的是认证回调URL(“Authorization callback URL”),这个地址是你的应用程序接收第三方平台认证结果的URL,这个URL后续是可以修改的。
注册成功后,你需要点"Cenerate a new client secret",创建客户端密码。至此,应用注册完成。后续要修改:点你头像,Settings -> Developer settings -> OAuth Apps。
Vert.x OAuth2
Vert.x提供了一个开箱即用的组件"vertx-auth-oauth2"来支持OAuth2认证,要使用,首先需要将vertx-auth-oauth2依赖添加到的项目中:
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-auth-oauth2</artifactId>
<version>4.5.10</version>
</dependency>
vertx-auth-oauth2最重要的是OAuth2Auth类(OAuth2 auth provider),OAuth2Auth提供的三种OAuth 2.0认证方式支持:
- Authorization Code Flow. // 授权码方式
- Password Credentials Flow. // 密码方式
- Client Credentials Flow. // 客户端凭证方式
Github使用的是授权码(Authorization Code)方式,程序第一步就是根据你的注册信息,实例化OAuth2Auth:
static final String redirectUri = "http://xxx.xxx.xxx.xxx:8080/oauth2/callback";
OAuth2Auth oauth2 = OAuth2Auth.create(vertx, new OAuth2Options()
.setClientId("Ov23li2FQxeipjGZqYbx")
.setClientSecret("a5779ce...387646c")
.setSite("https://github.com/login")
.setTokenPath("/oauth/access_token")
.setAuthorizationPath("/oauth/authorize")
);
要使用第三方认证,我们需要(根据注册的信息)生成第三方平台认证URL,并将这个链接放到你的应用的登录页面中:
static final String state = "any-random-12";
static final List<String> scopes = Arrays.asList("notifications");
String authorizationUri = oauth2.authorizeURL(new OAuth2AuthorizationURL()
.setRedirectUri(redirectUri)
.setScopes(scopes)
.setState(state));
// 在OAuth2.0中, state参数是一个可选的字段,用于防止跨站请求伪造(CSRF)攻击,并且可以用来保持请求的上下文。
// 生成的URL格式如下:
// https://github.com/login/oauth/authorize?state=any-random-12&scope=notifications&response_type=code&client_id=Ov23li2FQxeipjGZqYbx&redirect_uri=https%3A%2F%2Fptop.only.wip.la%3A443%2Fhttp%2Fxxx.xxx.xxx.xxx%3A8080%2Foauth2%2Fcallback
// 将生成的第三方认证URL,放到"通过Github登录"超链接中,客户点击会跳转到GitHub的登录页面。
router.route("/").handler(routingContext -> indexPage(routingContext, authorizationUri));
static void indexPage(RoutingContext routingContext, String authorizationUri) {
StringBuilder html = new StringBuilder();
html.append("<html><header><meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\"></header><body>")
.append("<h2>Vert.x OAuth2 sample.</h2><br/>")
.append("<a href=\"" + authorizationUri + "\">通过Github登录</a> || ")
.append("<a href=\"/logout\">logout</a>").append("</body></html>");
routingContext.response().end(html.toString());
}
用户点登录链接,将跳转到Github登录页面,认证通过后,需要用户并授权(获取在Github的用户信息):
通过后,Github通过调用你预留的Authorization callback URL,并通过URL参数返回将授权码返回给你的应用:
// http://xxx.xxx.xxx.xxx:8080/oauth2/callback?code=d1ba8....db5a76&state=any-random-12
// 通过回调的授权码(code),可以获取用户在github的用户信息;
// 通过回调state, 可以验证是否是你发起的认证请求。
router.route("/oauth2/callback").handler(routingContext -> oauth2Callback(routingContext, oauth2, vertx));
static void oauth2Callback(RoutingContext routingContext, OAuth2Auth oauth2, Vertx vertx) {
HttpServerRequest request = routingContext.request();
LOGGER.info(request.absoluteURI());
String code = request.getParam("code"); //
String stateCallback = request.getParam("state");
if(Objects.equals(state, stateCallback)) {
LOGGER.info("State OK.");
} else {
LOGGER.info("State Mismatch.");
}
Oauth2Credentials credentials = new Oauth2Credentials()
.setCode(code)
.setRedirectUri(redirectUri)
.setFlow(OAuth2FlowType.AUTH_CODE);
oauth2.authenticate(credentials).onSuccess(user -> {
LOGGER.info(user.principal().toString());
LOGGER.info(user.attributes().toString());
JsonObject principal = user.principal();
String accessToken = principal.getString("access_token");
String tokenType = principal.getString("token_type");
// 通过Vert.x Client使用access_token获取用户在Github上的用户信息。
// 需要vertx-web-client依赖
WebClientOptions options = new WebClientOptions().setConnectTimeout(3000).setSsl(true).setTrustAll(true);
WebClient client = WebClient.create(vertx, options);
// API文档: https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28
client.get(443, "api.github.com", "/user")
.putHeader("content-type", "application/vnd.github+json")
.putHeader("Authorization", tokenType + " " + accessToken)
.send()
.onSuccess(result -> {
// 成功获取用户在Github上的用户信息,你可以用其中某些信息做为索引,与改用户在你的应用中的用户信息或权限进行关联
// 本例将获取的用户信息,做为的属性,创建一个新的User,并关联到RoutingContext中。
JsonObject body = result.bodyAsJsonObject();
LOGGER.info(body.encodePrettily());
User newUser = new UserImpl(principal, body);
routingContext.setUser(newUser);
// 跳转到应用的其他页,使用redirect似乎接收不到User, 猜测可能是Github跳转不算同一个请求。
//routingContext.redirect("/hello");
routingContext.reroute("/hello");
}).onFailure(error -> {
LOGGER.log(Level.SEVERE, "", error);
});
}).onFailure(ex -> {
LOGGER.log(Level.SEVERE, "OAuth认证失败。", ex);
});
}
通过hello页面,展示用户登录信息。
router.route("/hello").handler(routingContext -> helloPage(routingContext));
static void helloPage(RoutingContext routingContext) {
StringBuilder html = new StringBuilder().append(
"<html><header><meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\"></header><body>");
User user = routingContext.user();
if (user != null) {
String login = user.get("login");
String name = user.get("name");
// long id = user.get("id");
// String email = user.get("email");
html.append("<p> Hello ~, " + name + "(" + login + "):</p>")
.append("<pre>" + user.attributes().encodePrettily() + "</pre>");
} else {
html.append("<p> 你没有登录。</p>");
}
html.append("<p><a href=\"/\">返回首页</a></p></body></html>");
routingContext.response().end(html.toString());
}
执行效果如下:
案例比较简单不上传了,下面是完整代码:
package vertx.web;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.User;
import io.vertx.ext.auth.impl.UserImpl;
import io.vertx.ext.auth.oauth2.OAuth2Auth;
import io.vertx.ext.auth.oauth2.OAuth2AuthorizationURL;
import io.vertx.ext.auth.oauth2.OAuth2FlowType;
import io.vertx.ext.auth.oauth2.OAuth2Options;
import io.vertx.ext.auth.oauth2.Oauth2Credentials;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.client.WebClientOptions;
public class Oauth2 {
static final Logger LOGGER = Logger.getLogger(Oauth2.class.getName());
static final String redirectUri = "http://xxx.xxx.xxx.xxx:8080/oauth2/callback";
//static final String redirectUri = "http://localhost:8080/oauth2/callback";
static final String state = "any-random-12";
static final List<String> scopes = Arrays.asList("notifications");
public static void main(String[] args) throws UnsupportedEncodingException {
Vertx vertx = Vertx.vertx();
OAuth2Auth oauth2 = OAuth2Auth.create(vertx, new OAuth2Options()
.setClientId("Ov23li2FQxeipjGZqYbx")
.setClientSecret("a5779c....387646c")
.setSite("https://github.com/login")
.setTokenPath("/oauth/access_token")
.setAuthorizationPath("/oauth/authorize")
);
String authorizationUri = oauth2.authorizeURL(new OAuth2AuthorizationURL()
.setRedirectUri(redirectUri)
.setScopes(scopes)
.setState(state));
//LOGGER.info(URLDecoder.decode(authorizationUri, StandardCharsets.UTF_8.toString()));
HttpServer server = vertx.createHttpServer();
Router router = Router.router(vertx);
router.route().failureHandler(Oauth2::errorHandler);
router.route("/oauth2/callback").handler(routingContext -> oauth2Callback(routingContext, oauth2, vertx));
router.route("/").handler(routingContext -> indexPage(routingContext, authorizationUri));
router.route("/hello").handler(routingContext -> helloPage(routingContext));
server.requestHandler(router).listen(8080);
}
static void oauth2Callback(RoutingContext routingContext, OAuth2Auth oauth2, Vertx vertx) {
HttpServerRequest request = routingContext.request();
LOGGER.info(request.absoluteURI());
String code = request.getParam("code");
String stateCallback = request.getParam("state");
if(Objects.equals(state, stateCallback)) {
LOGGER.info("State OK.");
} else {
LOGGER.info("State Mismatch.");
}
Oauth2Credentials credentials = new Oauth2Credentials()
.setCode(code)
.setRedirectUri(redirectUri)
.setFlow(OAuth2FlowType.AUTH_CODE);
oauth2.authenticate(credentials).onSuccess(user -> {
LOGGER.info(user.principal().toString());
LOGGER.info(user.attributes().toString());
JsonObject principal = user.principal();
String accessToken = principal.getString("access_token");
String tokenType = principal.getString("token_type");
WebClientOptions options = new WebClientOptions().setConnectTimeout(3000).setSsl(true).setTrustAll(true);
WebClient client = WebClient.create(vertx, options);
// https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28
client.get(443, "api.github.com", "/user")
.putHeader("content-type", "application/vnd.github+json")
.putHeader("Authorization", tokenType + " " + accessToken)
.send()
.onSuccess(result -> {
JsonObject body = result.bodyAsJsonObject();
LOGGER.info(body.encodePrettily());
User newUser = new UserImpl(principal, body);
routingContext.setUser(newUser);
//routingContext.redirect("/hello");
routingContext.reroute("/hello");
}).onFailure(error -> {
LOGGER.log(Level.SEVERE, "", error);
});
}).onFailure(ex -> {
LOGGER.log(Level.SEVERE, "OAuth认证失败。", ex);
});
}
static void errorHandler(RoutingContext routingContext) {
Throwable exception = routingContext.failure();
int statusCode = routingContext.statusCode();
HttpServerRequest request = routingContext.request();
String method = request.method().name();
String uri = request.absoluteURI();
LOGGER.log(Level.SEVERE, method + " " + uri + ", statusCode: " + statusCode, exception);
HttpServerResponse response = routingContext.response();
response.setStatusCode(statusCode);
JsonArray errorArray = new JsonArray().add(new JsonObject().put("code", statusCode))
.add(new JsonObject().put("message", exception.getMessage()));
JsonObject respObj = new JsonObject().put("error", errorArray);
response.end(respObj.toString());
}
static void helloPage(RoutingContext routingContext) {
StringBuilder html = new StringBuilder().append(
"<html><header><meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\"></header><body>");
User user = routingContext.user();
if (user != null) {
String login = user.get("login");
String name = user.get("name");
// long id = user.get("id");
// String email = user.get("email");
html.append("<p> Hello ~, " + name + "(" + login + "):</p>")
.append("<pre>" + user.attributes().encodePrettily() + "</pre>");
} else {
html.append("<p> 你没有登录。</p>");
}
html.append("<p><a href=\"/\">返回首页</a></p></body></html>");
routingContext.response().end(html.toString());
}
static void indexPage(RoutingContext routingContext, String authorizationUri) {
StringBuilder html = new StringBuilder();
html.append("<html><header><meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\"></header><body>")
.append("<h2>Vert.x OAuth2 sample.</h2><br/>")
.append("<a href=\"" + authorizationUri + "\">通过Github登录</a> || ")
.append("<a href=\"/logout\">logout</a>").append("</body></html>");
routingContext.response().end(html.toString());
}
}