Vert.x web模块(八) 一个真实案例

到目前为止,您已经学习了如何使用Oauth2处理程序,但是您会注意到,对于每个请求,您都需要进行身份验证。这是因为处理程序没有状态,并且在示例中没有应用状态管理。

尽管对于面向API的端点建议没有状态,例如,对于面向用户的端点使用JWT(我们稍后将介绍这些),我们可以将身份验证结果存储在会话中。为了实现这一点,我们需要一个类似以下代码段的应用程序:

OAuth2Auth authProvider =
  GithubAuth
    .create(vertx, "CLIENTID", "CLIENT SECRET");
// We need a user session handler too to make sure
// the user is stored in the session between requests
router.route()
  .handler(SessionHandler.create(LocalSessionStore.create(vertx)));
// we now protect the resource under the path "/protected"
router.route("/protected").handler(
  OAuth2AuthHandler.create(
      vertx,
      authProvider,
      "http://localhost:8080/callback")
    // we now configure the oauth2 handler, it will
    // setup the callback handler
    // as expected by your oauth2 provider.
    .setupCallback(router.route("/callback"))
    // for this resource we require that users have
    // the authority to retrieve the user emails
    .withScope("user:email")
);
// Entry point to the application, this will render
// a custom template.
router.get("/").handler(ctx -> ctx.response()
  .putHeader("Content-Type", "text/html")
  .end(
    "<html>\n" +
      "  <body>\n" +
      "    <p>\n" +
      "      Well, hello there!\n" +
      "    </p>\n" +
      "    <p>\n" +
      "      We're going to the protected resource, if there is no\n" +
      "      user in the session we will talk to the GitHub API. Ready?\n" +
      "      <a href=\"/protected\">Click here</a> to begin!</a>\n" +
      "    </p>\n" +
      "    <p>\n" +
      "      <b>If that link doesn't work</b>, remember to provide your\n" +
      "      own <a href=\"https://github.com/settings/applications/new\">\n" +
      "      Client ID</a>!\n" +
      "    </p>\n" +
      "  </body>\n" +
      "</html>"));
// The protected resource
router.get("/protected").handler(ctx -> {
  // at this moment your user object should contain the info
  // from the Oauth2 response, since this is a protected resource
  // as specified above in the handler config the user object is never null
  User user = ctx.user();
  // just dump it to the client for demo purposes
  ctx.response().end(user.toString());
});

Oauth2与JWT混用

一个提供者使用jwt tokens作为访问token,在想混合基于客户端认证和API认证,这是符合RFC6740标准且十分有用的。例如说,你有一个应用,想给一些HTML文档一些保护,但是也想这些页面被API消费。在这种情况下,API不能方便执行OAthus2要求的转发握手请求。倮可以在握手前使用提供的token.

只要提供配置支持JWT,这个处理会被处理器自动处理。

在现实中指的是,你的API可以通过Authorization头设为"Bearer BASE64_ACCESS_TOKEN"可以访问你保护的资源。

WebAuthn

我们的在线生活依赖于过时而脆弱的密码观念。密码是恶意用户与您的银行帐户或社交媒体帐户之间的密码。密码难以维护;很难将它们存储在服务器上(密码被盗)。它们很难记住,或者不告诉别人(网络钓鱼攻击)。

但还有更好的办法!一个无密码的世界,它是W3C和FIDO联盟在您的浏览器上运行的标准。

WebAuthn是一种API,它允许服务器使用公钥密码而不是密码来注册和验证用户,这是一种在身份验证设备(例如yubikey令牌或手机)的帮助下以用户可访问的方式使用密码的API。

该协议要求至少在路由器上安装第一个回调:

  1. /webauthn/response 执行所有验证的回调URL

  2. /webauthn/login 允许用户开始登录流程的URL(可选的,但是没有此URL将不能登录)

  3. /webauthn/register 允许用户注册一个新身份的URL(可选,如果注册数据已经被存储,可以不需要)

一个保护应用程序的例子:

WebAuthn webAuthn = WebAuthn.create(
    vertx,
    new WebAuthnOptions()
      .setRelyingParty(new RelyingParty().setName("Vert.x WebAuthN Demo"))
      // What kind of authentication do you want? do you care?
      // # security keys
      .setAuthenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM)
      // # fingerprint
      .setAuthenticatorAttachment(AuthenticatorAttachment.PLATFORM)
      .setUserVerification(UserVerification.REQUIRED))
  // where to load the credentials from?
  .authenticatorFetcher(fetcher)
  // update the state of an authenticator
  .authenticatorUpdater(updater);

// parse the BODY
router.post()
  .handler(BodyHandler.create());
// add a session handler
router.route()
  .handler(SessionHandler
    .create(LocalSessionStore.create(vertx)));

// security handler
WebAuthnHandler webAuthNHandler = WebAuthnHandler.create(webAuthn)
  .setOrigin("https://192.168.178.74.xip.io:8443")
  // required callback
  .setupCallback(router.post("/webauthn/response"))
  // optional register callback
  .setupCredentialsCreateCallback(router.post("/webauthn/register"))
  // optional login callback
  .setupCredentialsGetCallback(router.post("/webauthn/login"));

// secure the remaining routes
router.route().handler(webAuthNHandler);

这个应用程在后端是不安全的,但是有一些代码需要在客户端执行。需要一些样板代码,使用以下两个功能:

/**
* Converts PublicKeyCredential into serialised JSON
* @param  {Object} pubKeyCred
* @return {Object}            - JSON encoded publicKeyCredential
*/
var publicKeyCredentialToJSON = (pubKeyCred) => {
 if (pubKeyCred instanceof Array) {
   let arr = [];
   for (let i of pubKeyCred) { arr.push(publicKeyCredentialToJSON(i)) }

   return arr
 }

 if (pubKeyCred instanceof ArrayBuffer) {
   return base64url.encode(pubKeyCred)
 }

 if (pubKeyCred instanceof Object) {
   let obj = {};

   for (let key in pubKeyCred) {
     obj[key] = publicKeyCredentialToJSON(pubKeyCred[key])
   }

   return obj
 }

 return pubKeyCred
};

/**
* Generate secure random buffer
* @param  {Number} len - Length of the buffer (default 32 bytes)
* @return {Uint8Array} - random string
*/
var generateRandomBuffer = (len) => {
 len = len || 32;

 let randomBuffer = new Uint8Array(len);
 window.crypto.getRandomValues(randomBuffer);

 return randomBuffer
};

/**
* Decodes arrayBuffer required fields.
*/
var preformatMakeCredReq = (makeCredReq) => {
 makeCredReq.challenge = base64url.decode(makeCredReq.challenge);
 makeCredReq.user.id = base64url.decode(makeCredReq.user.id);

 return makeCredReq
};

/**
* Decodes arrayBuffer required fields.
*/
var preformatGetAssertReq = (getAssert) => {
 getAssert.challenge = base64url.decode(getAssert.challenge);

 for (let allowCred of getAssert.allowCredentials) {
   allowCred.id = base64url.decode(allowCred.id)
 }

 return getAssert
};

这些函数将帮助你与服务器交互,不需要更多东西。让我们用一个用户登录:

// using the functions defined before...
getGetAssertionChallenge({name: 'your-user-name'})
.then((response) => {
 // base64 must be decoded to a JavaScript Buffer
 let publicKey = preformatGetAssertReq(response);
 // the response is then passed to the browser
 // to generate an assertion by interacting with your token/phone/etc...
 return navigator.credentials.get({publicKey})
})
.then((response) => {
 // convert response buffers to base64 and json
 let getAssertionResponse = publicKeyCredentialToJSON(response);
 // send information to server
 return sendWebAuthnResponse(getAssertionResponse)
})
.then((response) => {
 // success!
 alert('Login success')
})
.catch((error) => alert(error));

// utility functions

let sendWebAuthnResponse = (body) => {
 return fetch('/webauthn/response', {
   method: 'POST',
   credentials: 'include',
   headers: {
     'Content-Type': 'application/json'
   },
   body: JSON.stringify(body)
 })
   .then(response => {
     if (!response.ok) {
       throw new Error(`Server responded with error: ${response.statusText}`);
     }
     return response;
   })
};

let getGetAssertionChallenge = (formBody) => {
 return fetch('/webauthn/login', {
   method: 'POST',
   credentials: 'include',
   headers: {
     'Content-Type': 'application/json'
   },
   body: JSON.stringify(formBody)
 })
   .then(response => {
     if (!response.ok) {
       throw new Error(`Server responded with error: ${response.statusText}`);
     }
     return response;
   })
   .then((response) => response.json())
};

上面的示例已经涵盖了66%的API,其中包括3个端点中的2个。最后一个端点是用户注册。用户注册是将新密钥注册到服务器凭据存储并映射到用户的过程,当然,在客户端创建了一个私钥并与服务器关联,但该密钥从未离开硬件令牌或您的手机安全芯片。

要注册用户并重用上面已经定义的大多数功能,请执行以下操作:

/* Handle for register form submission */
getMakeCredentialsChallenge({name: 'myalias', displayName: 'Paulo Lopes'})
.then((response) => {
 // convert challenge & id to buffer and perform register
 let publicKey = preformatMakeCredReq(response);
 // create a new secure key pair
 return navigator.credentials.create({publicKey})
})
.then((response) => {
 // convert response from buffer to json
 let makeCredResponse = window.publicKeyCredentialToJSON(response);
 // send to server to confirm the user
 return sendWebAuthnResponse(makeCredResponse)
})
.then((response) => {
 alert('Registration completed')
})
.catch((error) => alert(error));

// utility functions

let getMakeCredentialsChallenge = (formBody) => {
 return fetch('/webauthn/register', {
   method: 'POST',
   credentials: 'include',
   headers: {
     'Content-Type': 'application/json'
   },
   body: JSON.stringify(formBody)
 })
   .then(response => {
     if (!response.ok) {
       throw new Error(`Server responded with error: ${response.statusText}`);
     }
     return response;
   })
   .then((response) => response.json())
};

警告

由于API的安全性,浏览器将不允许您在纯文本HTTP上使用此API。所有请求都必须通过HTTPS。

警告

WebAuthN需要HTTPS和有效的TLS证书,您也可以在开发期间使用自签名证书。

一次性密码(Multi-Factor Authentication)

Vert.x还支持多因素身份验证。使用MFA有两个选项:

  • HOTP-基于哈希的一次性密码
  • TOTP-基于时间的一次性密码

提供程序之间的用法是相同的,因此存在一个处理程序,允许您在构造函数级别选择所需的模式。

此处理程序的行为如下:

如果在请求中没有用户,则假设没有执行先前的认证。这意味着请求将立即以状态代码401终止。

如果用户存在并且对象缺少匹配类型(hotp/totp)的属性mfa,则请求将被重定向到验证url(如果提供),否则将终止。此类url应提供输入代码的方式,例如:

<html>
<head>
 <meta charset="UTF-8">
 <title>OTP Authenticator Verification Example Page</title>
</head>
<body>
<form action="/otp/verify" method="post" enctype="multipart/form-data">
 <div>
   <label>Code:</label>
   <input type="text" name="code"/><br/>
 </div>
 <div>
   <input type="submit" value="Submit"/>
 </div>
</form>
</body>
</html>

用户输入有效code后,请求重定到初始URL或者到/根路径(在没有原URL的情况下)

当然,这个流程假设认证器应用或者设备已经配置好。为了配置一个新的应用或设备,下面是一个HTML页面的例子:

<html>
<head>
 <title>OTP Authenticator Registration Example Page</title>
</head>
<body>
 <p>Scan this QR Code in Google Authenticator</p>
 <img id="qrcode">
 <p>- or enter this key manually -</p>
 <span id="url"></span>

 <script>
 const key = document.getElementById('url');
 const qrcode = document.getElementById('qrcode');

 fetch(
   '/otp/register',
   {
     method: 'POST',
     headers: {
       'Accept': 'application/json'
     }
   })
   .then(res => {
     if (res.status === 200) {
       return res;
     }
     throw new Error(res.statusText);
   })
   .catch(err => console.error(err))
   .then(res => res.json())
   .then(json => {
     key.innerText = json.url;
     qrcode.src =
       'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' +
       encodeURIComponent(json.url);
   });
 </script>
</body>
</html>

本例中重要的一点是,脚本向配置的注册回调发出POST请求。再次,如果请求中没有已经认证的用户,则该回调将返回状态代码401。成功后,JSON文档将返回一个url和一些额外的元数据。该url应用于配置验证器,方法是在应用程序上手动输入或呈现QR(二维码)码。二维码的渲染可以在后端或前端完成。为了简单起见,本示例使用GoogleChartsAPI在浏览器上呈现它。

最后,这是如何在vert中使用处理程序。vert.x应用程序:

router.post()
  .handler(BodyHandler.create());
// add a session handler (OTP requires state)
router.route()
  .handler(SessionHandler
    .create(LocalSessionStore.create(vertx))
    .setCookieSameSite(CookieSameSite.STRICT));

// add the first authentication mode, for example HTTP Basic Authentication
router.route()
  .handler(basicAuthHandler);

final OtpAuthHandler otp = OtpAuthHandler
  .create(TotpAuth.create()
    .authenticatorFetcher(authr -> {
      // fetch authenticators from a database
      // ...
      return Future.succeededFuture(new io.vertx.ext.auth.otp.Authenticator());
    })
    .authenticatorUpdater(authr -> {
      // update or insert authenticators from a database
      // ...
      return Future.succeededFuture();
    }));

otp
  // the issuer for the application
  .issuer("Vert.x Demo")
  // handle code verification responses
  .verifyUrl("/verify-otp.html")
  // handle registration of authenticators
  .setupRegisterCallback(router.post("/otp/register"))
  // handle verification of authenticators
  .setupCallback(router.post("/otp/verify"));

// secure the rest of the routes
router.route()
  .handler(otp);

// To view protected details, user must be authenticated and
// using 2nd factor authentication
router.get("/protected")
  .handler(ctx -> ctx.end("Super secret content"));

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值