Vert.x,认证与授权 - 基于OAuth 2的身份认证

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());
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值