从 JSON Web 令牌到单点登录第 1 部分:创建令牌

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

这是一系列文章中的第一篇,这些文章提供了使用 JSON Web 令牌 (JWT) 标准 1 构建单点登录的方法。在这篇文章中,您将学习如何实现基于用户凭据生成加密令牌的身份验证服务。此加密令牌可用于访问共享同一顶级域的其他站点。假设每个用户都有一个帐户,用于访问多个站点。如果您不熟悉 JWT,我强烈建议您阅读“JSON Web 令牌剖析”。 2个

认证服务

身份验证服务的主要目标是执行用户的实际身份验证并启动令牌的创建。用户将通过 HTTPS 使用 POST 方法将电子邮件地址和密码发送到预定义的 URL,例如 https://dzone.com/services/auth 。为了简单起见,这些凭据将由 servlet 的 doPost() 方法处理。


 @WebServlet("/auth")
public class AuthServlet extends HttpServlet {

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // retrieve the email and password from the request String email = request.getParameter("email"); String password = request.getParameter("password");

// authenticate user User user = new User(email, password); // the authenticate() method code was omitted because it’s domain specific boolean authenticated = new AuthService().authenticate(user);

if(authenticated) { String token = null; TokenService tokenService = new TokenService(); try { token = tokenService.generateToken(user); } catch (NoSuchAlgorithmException | JOSEException | IOException e) { // TODO handle exceptions } // TODO store token and send response } else { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } } }

此时,令牌已准备好存储并且应该已发送响应。接下来讨论令牌存储的细节。

生成加密的 JWT

正如上面代码中突出显示的那样,令牌生成由 TokenService 类处理。它的实现基于 Nimbus JOSE + JWT 3 库,该库同时支持 JSON Web 签名 (JWS) 和 JSON Web 加密 (JWE) 标准。要使用这两个标准,您需要一个密钥来对令牌进行签名和加密。下面的代码片段向您展示了如何使用 javax.crypto 包中的 KeyGenerator 4 类生成密钥。一旦生成了密钥,就获得了它的主要编码格式。令牌的签名和加密需要此格式。


 @WebServlet("/auth")
public class AuthServlet extends HttpServlet {

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // retrieve the email and password from the request String email = request.getParameter("email"); String password = request.getParameter("password");

// authenticate user User user = new User(email, password); // the authenticate() method code was omitted because it’s domain specific boolean authenticated = new AuthService().authenticate(user);

if(authenticated) { String token = null; TokenService tokenService = new TokenService(); try { token = tokenService.generateToken(user); } catch (NoSuchAlgorithmException | JOSEException | IOException e) { // TODO handle exceptions } // TODO store token and send response } else { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } } }

现在密钥已准备就绪,是时候创建令牌了。这可以通过定义一组构成令牌有效负载的声明来完成。


 @WebServlet("/auth")
public class AuthServlet extends HttpServlet {

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // retrieve the email and password from the request String email = request.getParameter("email"); String password = request.getParameter("password");

// authenticate user User user = new User(email, password); // the authenticate() method code was omitted because it’s domain specific boolean authenticated = new AuthService().authenticate(user);

if(authenticated) { String token = null; TokenService tokenService = new TokenService(); try { token = tokenService.generateToken(user); } catch (NoSuchAlgorithmException | JOSEException | IOException e) { // TODO handle exceptions } // TODO store token and send response } else { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } } }

generateToken() 方法首先定义六个声明,其中四个用于验证令牌的有效性,而其余两个是特定于应用程序的。这两个自定义声明用于保存用户的电子邮件地址和密码。这组声明在以下代码片段中使用,它是 generateToken() 方法的一部分,用于创建签名和加密的令牌。


 @WebServlet("/auth")
public class AuthServlet extends HttpServlet {

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // retrieve the email and password from the request String email = request.getParameter("email"); String password = request.getParameter("password");

// authenticate user User user = new User(email, password); // the authenticate() method code was omitted because it’s domain specific boolean authenticated = new AuthService().authenticate(user);

if(authenticated) { String token = null; TokenService tokenService = new TokenService(); try { token = tokenService.generateToken(user); } catch (NoSuchAlgorithmException | JOSEException | IOException e) { // TODO handle exceptions } // TODO store token and send response } else { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } } }

这种紧凑形式的令牌用于验证共享同一顶级域的站点上的用户身份。下一节将简要讨论用于在站点之间共享令牌的方法。

加密 JWT 的存储

如前所述,在“身份验证服务”部分下的 doPost() 方法中,需要存储令牌并作为响应的一部分发回。这是必需的,因为调用身份验证服务的客户端在将来与其他站点通信时需要令牌。此处使用的方法是使用安全 cookie 来存储令牌。此 cookie 将由浏览器随每个请求一起发送到共享同一顶级域的任何站点。下面是创建安全 cookie 的代码:


 @WebServlet("/auth")
public class AuthServlet extends HttpServlet {

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // retrieve the email and password from the request String email = request.getParameter("email"); String password = request.getParameter("password");

// authenticate user User user = new User(email, password); // the authenticate() method code was omitted because it’s domain specific boolean authenticated = new AuthService().authenticate(user);

if(authenticated) { String token = null; TokenService tokenService = new TokenService(); try { token = tokenService.generateToken(user); } catch (NoSuchAlgorithmException | JOSEException | IOException e) { // TODO handle exceptions } // TODO store token and send response } else { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } } }

下一步是什么?

在本系列的下一篇文章中,您将学习如何检索令牌、验证其内容以及访问其持有的声明集。

参考

  1. JSON 网络令牌 (JWT) RFC 7519 https://tools.ietf.org/html/rfc7519
  2. JSON Web 令牌剖析 https://scotch.io/tutorials/the-anatomy-of-a-json-web-token
  3. Nimbus JOSE + JWT 库 http://connect2id.com/products/nimbus-jose-jwt
  4. 密钥生成器 API http://docs.oracle.com/javase/7/docs/api/javax/crypto/KeyGenerator.html