内置 TokenManager

工具类

在使用 Tio-boot 框架开发 Web 应用程序时,安全性是一个重要的考虑因素。本文将介绍如何使用 Tio-boot 框架实现基于 JWT 的 Token 认证。我们将探讨如何配置拦截器,生成和验证 JWT Token,并管理用户的登录状态。

Tio-boot 的工具类库 Tio-utils 内置了 JwtUtilsTokenManager 两个关键组件。

1. JWT 工具类 JwtUtils

JwtUtils 类用于生成和验证 JWT Token,并从 Token 中提取用户信息。其核心功能包括:

  • 创建 JWT Token:通过用户 ID 和密钥生成 Token,并指定有效期。
  • 验证 JWT Token:检查 Token 的有效性和是否过期。
  • 解析用户 ID:从 Token 的 payload 部分解析用户 ID。

方法说明

  • createToken(String key, Map<String, Object> payloadMap):

    • 参数:
      • key: 密钥,用于签名 JWT。
      • payloadMap: 包含用户信息和过期时间的 Map。
    • 返回值: 返回生成的 JWT 字符串。
  • verify(String key, String token):

    • 参数:
      • key: 密钥,用于验证 JWT。
      • token: 待验证的 JWT 字符串。
    • 返回值: true 如果验证成功且未过期,否则返回 false
  • parseUserIdLong(String token):

    • 参数:
      • token: JWT 字符串。
    • 返回值: 返回解析出的用户 ID。

2. Token 管理器 TokenManager

TokenManager 类负责管理用户的登录状态。它使用 ITokenStorage 接口存储 Token,并提供登录、注销、验证登录状态等功能。

方法说明

  • login(Object userId, String tokenValue):

    • 参数:
      • userId: 用户 ID。
      • tokenValue: 生成的 JWT Token。
    • 返回值: 无返回值,但会将 userIdtokenValue 存储在 ITokenStorage 中。
  • isLogin(String key, String token):

    • 参数:
      • key: 密钥,用于验证 JWT。
      • token: JWT 字符串。
    • 返回值: true 如果用户已登录且 Token 有效,否则返回 false
  • logout(Object userId):

    • 参数:
      • userId: 用户 ID。
    • 返回值: 无返回值,但会将该 userId 的登录状态移除。

使用 Tio-boot 框架实现基于 JWT 的 Token 认证

1. 前置准备

确保您的项目中已经引入了 Tio-boot 框架,并且数据库中有一个用户表,例如 tio_boot_admin_system_users,该表存储了用户的基本信息,如用户名、密码等。

CREATE TABLE tio_boot_admin_system_users (
    id BIGINT NOT NULL,
    username VARCHAR(30) NOT NULL,
    password VARCHAR(100) NOT NULL DEFAULT '',
    nickname VARCHAR(30) NOT NULL,
    signature VARCHAR(200),
    title VARCHAR(50),
    group_name VARCHAR(50),
    tags JSON,
    notify_count INT DEFAULT 0,
    unread_count INT DEFAULT 0,
    country VARCHAR(50),
    access VARCHAR(20),
    geographic JSON,
    address VARCHAR(200),
    remark VARCHAR(500),
    dept_id BIGINT,
    post_ids VARCHAR(255),
    email VARCHAR(50) DEFAULT '',
    phone VARCHAR(11) DEFAULT '',
    sex SMALLINT DEFAULT 0,
    avatar VARCHAR(512) DEFAULT '',
    status SMALLINT NOT NULL DEFAULT 0,
    login_ip VARCHAR(50) DEFAULT '',
    login_date TIMESTAMP WITHOUT TIME ZONE,
    creator VARCHAR(64) DEFAULT '',
    create_time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updater VARCHAR(64) DEFAULT '',
    update_time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
    deleted SMALLINT  NOT NULL DEFAULT 0,
    tenant_id BIGINT NOT NULL DEFAULT 0,
    PRIMARY KEY (id),
    UNIQUE (username)
);

2. 登录与注销接口

接下来,我们实现登录和注销的 API 接口。用户登录成功后,生成 JWT Token 并存储在 TokenManager 中。

package com.litongjava.admin.handler;

import java.util.HashMap;
import java.util.Map;

import com.jfinal.kit.Kv;
import com.litongjava.admin.costants.AppConstant;
import com.litongjava.admin.services.LoginService;
import com.litongjava.admin.vo.LoginAccountVo;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.model.body.RespBodyVo;
import com.litongjava.model.token.AuthToken;
import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.HttpResponse;
import com.litongjava.tio.http.server.model.HttpCors;
import com.litongjava.tio.http.server.util.CORSUtils;
import com.litongjava.tio.http.server.util.Resps;
import com.litongjava.tio.utils.json.Json;
import com.litongjava.tio.utils.jwt.JwtUtils;
import com.litongjava.tio.utils.token.TokenManager;

public class ApiLoginHandler {
  public HttpResponse account(HttpRequest request) {

    HttpResponse httpResponse = TioRequestContext.getResponse();
    CORSUtils.enableCORS(httpResponse, new HttpCors());

    String bodyString = request.getBodyString();
    LoginAccountVo loginAccountVo = Json.getJson().parse(bodyString, LoginAccountVo.class);

    RespBodyVo respVo;
    // 1.登录
    LoginService loginService = Aop.get(LoginService.class);
    Long userId = loginService.getUserIdByUsernameAndPassword(loginAccountVo);

    if (userId != null) {

      // 2. 设置过期时间payload
      long tokenTimeout = (System.currentTimeMillis() + 3600000) / 1000;

      // 3.创建token
      AuthToken authToken = JwtUtils.createToken(AppConstant.SECRET_KEY, new AuthToken(userId, tokenTimeout));
      TokenManager.login(userId, authToken.getToken());

      Kv kv = new Kv();
      kv.set("token", authToken.getToken());
      kv.set("tokenTimeout", tokenTimeout);
      kv.set("type", loginAccountVo.getType());
      kv.set("status", "ok");

      respVo = RespBodyVo.ok(kv);
    } else {
      Map<String, String> data = new HashMap<>(1);
      data.put("status", "false");
      respVo = RespBodyVo.fail().data(data);

    }

    return Resps.json(httpResponse, respVo);
  }

  public HttpResponse outLogin(HttpRequest request) {
    HttpResponse httpResponse = TioRequestContext.getResponse();
    CORSUtils.enableCORS(httpResponse, new HttpCors());
    Long userIdLong = TioRequestContext.getUserIdLong();
    //remove
    TokenManager.logout(userIdLong);

    return Resps.json(httpResponse, RespBodyVo.ok());
  }

  /**
   * 因为拦击器已经经过了验证,判断token是否存在即可
   * @param request
   * @return
   */
  public HttpResponse validateLogin(HttpRequest request) {
    HttpResponse httpResponse = TioRequestContext.getResponse();
    CORSUtils.enableCORS(httpResponse, new HttpCors());

    Long userIdLong = TioRequestContext.getUserIdLong();
    boolean login = TokenManager.isLogin(userIdLong);
    return Resps.json(httpResponse, RespBodyVo.ok(login));

  }
}

方法说明

  • account(HttpRequest request):

    • 参数:
      • request: 包含用户登录信息的 HTTP 请求。
    • 返回值: 返回登录结果的 HttpResponse,包括生成的 JWT Token 和过期时间。
  • outLogin(HttpRequest request):

    • 参数:
      • request: 包含用户登出信息的 HTTP 请求。
    • 返回值: 返回登出结果的 HttpResponse

3. 登录的业务逻辑

package com.litongjava.admin.services;

import org.apache.commons.codec.digest.DigestUtils;

import com.litongjava.admin.vo.LoginAccountVo;
import com.litongjava.db.activerecord.Db;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class LoginService {
  public Long getUserIdByUsernameAndPassword(LoginAccountVo loginAccountVo) {
    // digest
    String password = DigestUtils.sha256Hex(loginAccountVo.getPassword());
    log.info("password:{}", password);
    String sql = "select id from tio_boot_admin_system_users where username=? and password=?";
    return Db.queryLong(sql, loginAccountVo.getUsername(), password);
  }
}

4. 配置登录处理器

package com.litongjava.admin.config;

import com.litongjava.admin.handler.ApiLoginHandler;
import com.litongjava.admin.handler.FakeAnalysisChartDataHandler;
import com.litongjava.admin.handler.GeographicHandler;
import com.litongjava.admin.handler.SystemHandler;
import com.litongjava.admin.handler.UserHandler;
import com.litongjava.annotation.AConfiguration;
import com.litongjava.annotation.AInitialization;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.tio.boot.server.TioBootServer;
import com.litongjava.tio.http.server.router.HttpRequestRouter;

@AConfiguration
public class HttpRequestHandlerConfig {

  @Initialization
  public void httpRoutes() {
    HttpRequestRouter r = TioBootServer.me().getRequestRouter();

    UserHandler userHandler = Aop.get(UserHandler.class);
    FakeAnalysisChartDataHandler fakeAnalysisChartDataHandler = Aop.get(FakeAnalysisChartDataHandler.class);
    GeographicHandler geographicHandler = Aop.get(GeographicHandler.class);
    SystemHandler systemHandler = Aop.get(SystemHandler.class);
    // 创建controller
    ApiLoginHandler apiLoginHandler = Aop.get(ApiLoginHandler.class);
    // 添加action
    r.add("/api/login/account", apiLoginHandler::account);
    r.add("/api/login/outLogin", apiLoginHandler::outLogin);
    r.add("/api/login/validateLogin", apiLoginHandler::validateLogin);
    r.add("/api/currentUser", userHandler::currentUser);
    r.add("/api/accountSettingCurrentUser", userHandler::accountSettingCurrentUser);
    r.add("/api/fake_analysis_chart_data", fakeAnalysisChartDataHandler::index);
    // upload

    r.add("/api/system/changeUserPassword", systemHandler::changeUserPassword);
    r.add("/api/geographic/province", geographicHandler::province);

  }
}

5. 身份验证拦截器

为了确保每个请求的合法性,我们需要实现一个身份验证拦截器 AuthInterceptor。该拦截器会在请求到达控制器之前验证 JWT Token。

package com.litongjava.admin.services;

import com.litongjava.admin.costants.AppConstant;
import com.litongjava.tio.utils.hutool.StrUtil;
import com.litongjava.tio.utils.token.TokenManager;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class AuthService {
  public Long getIdByToken(String authorization) {
    log.info("authorization:{}", authorization);
    if (StrUtil.isBlank(authorization)) {
      return null;
    }
    String[] split = authorization.split(" ");

    Long userId = 0L;
    if (split.length > 1) {
      String idToken = split[1];
      userId = TokenManager.parseUserIdLong(AppConstant.SECRET_KEY, idToken);
    } else {
      userId = TokenManager.parseUserIdLong(AppConstant.SECRET_KEY, authorization);
    }
    return userId;
  }
}
package com.litongjava.admin.inteceptor;

import com.litongjava.admin.services.AuthService;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.HttpResponse;
import com.litongjava.tio.http.common.HttpResponseStatus;
import com.litongjava.tio.http.common.RequestLine;
import com.litongjava.tio.http.server.intf.HttpRequestInterceptor;

public class AuthInterceptor implements HttpRequestInterceptor {

  private Object body = null;

  public AuthInterceptor() {
  }

  public AuthInterceptor(Object body) {
    this.body = body;
  }

  @Override
  public HttpResponse doBeforeHandler(HttpRequest request, RequestLine requestLine, HttpResponse responseFromCache) {
    String authorization = request.getHeader("authorization");

    AuthService authService = Aop.get(AuthService.class);
    Long userId = authService.getIdByToken(authorization);

    if (userId != null) {
      TioRequestContext.setUserId(userId);
      return null;
    }

    HttpResponse response = TioRequestContext.getResponse();
    response.setStatus(HttpResponseStatus.C401);

    if (body != null) {
      response.setJson(body);
    }
    return response;
  }

  @Override
  public void doAfterHandler(HttpRequest request, RequestLine requestLine, HttpResponse response, long cost) throws Exception {
  }
}

方法说明

  • doBeforeHandler(HttpRequest request, RequestLine requestLine, HttpResponse responseFromCache):
    • 参数:
      • request: 当前的 HTTP 请求。
      • requestLine: 请求行信息。
      • responseFromCache: 缓存的响应信息。
    • 返回值: 返回 HttpResponse,如果验证通过,返回 null,否则返回错误响应。

6. 拦截器配置

拦截器配置是关键步骤,它将拦截所有需要保护的路由,并允许特定的路由绕过验证。

package com.litongjava.admin.config;
// 导入必要的类和注解

import com.litongjava.admin.inteceptor.AuthInterceptor;
import com.litongjava.annotation.AConfiguration;
import com.litongjava.annotation.AInitialization;
import com.litongjava.tio.boot.http.interceptor.HttpInterceptorModel;
import com.litongjava.tio.boot.http.interceptor.HttpInteceptorConfigure;
import com.litongjava.tio.boot.server.TioBootServer;

@AConfiguration
public class InterceptorConfiguration {

  @Initialization
  public void config() {
    // 登录 拦截器实例
    AuthInterceptor authTokenInterceptor = new AuthInterceptor();
    HttpInterceptorModel model = new HttpInterceptorModel();
    model.setInterceptor(authTokenInterceptor);

    // 拦截所有路由
    model.addblockeUrl("/**");

    // 设置例外路由
    // index
    model.addAlloweUrls("", "/");
    // user
    model.addAlloweUrls("/register/*", "/api/login/account", "/api/login/outLogin");
    // app
    HttpInteceptorConfigure serverInteceptorConfigure = new HttpInteceptorConfigure();
    serverInteceptorConfigure.add(model);
    // 将拦截器配置添加到 Tio 服务器
    TioBootServer.me().setServerInteceptorConfigure(serverInteceptorConfigure);
  }
}

7. 登录请求示例

curl 'http://localhost:10004/api/login/account' \
  -H 'Accept: application/json, text/plain, */*' \
  --data-raw '{"username":"admin","password":"admin","autoLogin":true,"type":"account"}'
curl 'http://localhost:10004/api/validateLogin' \
  -H 'Accept: application/json, text/plain, */*' \
  -H 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MjQ5OTQ0NDIsInVzZXJJZCI6MX0=.9We8596ZexVSH4Su8cx4zpgD9XzEQLKM42wpWrSr8j0'
curl 'http://localhost:10004/sejie-download-admin/api/login/outLogin' \
  -X 'POST' \
  -H 'Accept: application/json, text/plain, */*' \
  -H 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MjQ5OTQ0NDIsInVzZXJJZCI6MX0=.9We8596ZexVSH4Su8cx4zpgD9XzEQLKM42wpWrSr8j0'

结论

通过以上步骤,我们成功在 Tio-boot 框架中实现了基于 JWT 的用户认证。使用 JwtUtils 类生成和验证 Token,通过 TokenManager 管理用户的登录状态,并配置了拦截器来确保请求的安全性。这个方法能够有效地提高系统的安全性,并且易于扩展和维护。