Skip to content

authentication

liubao edited this page Jul 18, 2022 · 1 revision

进行认证和鉴权设计

Porter应用实现了一个简单的基于Token的认证机制。

注意:这个机制用于基本的开发演示,用户生成环境请谨慎使用。

传统的WEB容器都提供了会话管理,在微服务架构下,这些会话管理存在很多的限制,如果需要做到弹性扩缩容,则需要做到无状态或者使用缓存。 在porter项目中,我们使用用户中心做会话管理,可以通过login和session两个接口创建和获取会话信息。会话信息持久化到数据库中,从而实现微服务本身的无状态,微服务可以弹性扩缩容。在更大规模并发或者高性能要求的情况下,可以考虑将会话信息存储到高速缓存。

@PostMapping(path = "/login", produces = MediaType.APPLICATION_JSON_VALUE)
public SessionInfo login(@RequestParam(name = "userName") String userName,
    @RequestParam(name = "password") String password)

@GetMapping(path = "/session", produces = MediaType.APPLICATION_JSON_VALUE)
public SessionInfo getSession(@RequestParam(name = "sessionId") String sessionId)

同时新增了会话管理的数据表设计:

CREATE TABLE `T_SESSION` (
  `ID`  INTEGER(8) NOT NULL AUTO_INCREMENT COMMENT '唯一标识',
  `SESSION_ID`  VARCHAR(64) NOT NULL COMMENT '临时会话ID',
  `USER_NAME`  VARCHAR(64) NOT NULL COMMENT '用户名称',
  `ROLE_NAME`  VARCHAR(64) NOT NULL COMMENT '角色名称',
  `CREATION_TIME`  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `ACTIVE_TIME`  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最近活跃时间',
  PRIMARY KEY (`ID`)
);

会话管理和认证都在应用网关进行,鉴权则需要使用到用户信息。为了让微服务获取用户信息的时候,不至于再查询用户中心,我们利用了上下文机制,在上下文里面存储了session信息,所有的微服务都可以直接从上下文里面取到session信息,非常方便和灵活。完成这个功能有如下几个关键步骤:

  • 应用网关检查session信息,并将其存放到请求头里面。
  class AuthFilter implements GlobalFilter, Ordered {
    private final WebClient.Builder builder;

    public AuthFilter(WebClient.Builder builder) {
      this.builder = builder;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

      ServerHttpRequest request = exchange.getRequest();
      if (request.getPath().value().equals("/v1/user/login")
          || request.getPath().value().equals("/v1/user/session")
          || request.getPath().value().startsWith("/porter")) {
        return chain.filter(exchange);
      } else {
        HttpCookie cookie = request.getCookies().getFirst("session-id");
        String sessionId = cookie != null ? cookie.getValue() : null;
        if (StringUtils.isEmpty(sessionId)) {
          ServerHttpResponse response = exchange.getResponse();
          response.setRawStatusCode(403);
          return response.setComplete();
        }

        String sessionInfo = sessionCache.getIfPresent(sessionId);
        if (sessionInfo == null) {
          Mono<SessionInfo> serverSessionInfo = getAndSaveSessionInfo(sessionId);
          return serverSessionInfo.transform(si -> {
            if (si == null) {
              throw new IllegalStateException();
            }
            String sessionInfoStr = writeJson(si);
            if (sessionInfoStr == null) {
              throw new IllegalStateException();
            }
            sessionCache.put(sessionId, sessionInfoStr);

            Map<String, String> cseContext = new HashMap<>();
            cseContext.put("session-id", sessionId);
            cseContext.put("session-info", sessionInfo);
            ServerHttpRequest nextRequest = exchange.getRequest().mutate()
                .header("x-cse-context", writeJson(cseContext))
                .build();
            ServerWebExchange nextExchange = exchange.mutate().request(nextRequest).build();
            return chain.filter(nextExchange);
          }).doOnError(e -> {
            ServerHttpResponse response = exchange.getResponse();
            response.setRawStatusCode(403);
          });
        }

        Map<String, String> cseContext = new HashMap<>();
        cseContext.put("session-id", sessionId);
        cseContext.put("session-info", sessionInfo);
        ServerHttpRequest nextRequest = exchange.getRequest().mutate()
            .header(InvocationContextHolder.SERIALIZE_KEY, writeJson(cseContext))
            .build();
        ServerWebExchange nextExchange = exchange.mutate().request(nextRequest).build();
        return chain.filter(nextExchange);
      }
    }

    @Override
    public int getOrder() {
      return Ordered.HIGHEST_PRECEDENCE;
    }

    private Mono<SessionInfo> getAndSaveSessionInfo(String sessionId) {
      return this.builder.build().get()
          .uri("http://user-core/v1/user/session?sessionId=" + sessionId)
          .retrieve().bodyToMono(SessionInfo.class);
    }

    private String writeJson(Object o) {
      try {
        return JsonUtils.writeValueAsString(o);
      } catch (Exception ee) {
        LOGGER.error("Unexpected error", ee);
      }
      return null;
    }
  }
  • 给删除文件增加鉴权

在上面的步骤中,已经将会话信息设置到Context里面,file-service可以方便的使用这些信息进行鉴权操作。

@DeleteMapping(path = "/delete", produces = MediaType.APPLICATION_JSON_VALUE)
public boolean deleteFile(@RequestParam(name = "id") String id, HttpServletResponse response) {
    String session = InvocationContextHolder.getOrCreateInvocationContext().getContext("session-info");
    if (session == null) {
      response.setStatus(503);
      return false;
    } else {
      SessionInfo sessionInfo = null;
      try {
        sessionInfo = JsonUtils.readValue(session.getBytes("UTF-8"), SessionInfo.class);
      } catch (Exception e) {
        response.setStatus(503);
        return false;
      }
      if (sessionInfo == null || !sessionInfo.getRoleName().equals("admin")) {
        response.setStatus(503);
        return false;
      }
    }
    
    return fileService.deleteFile(id);
}

到这里为止,认证、会话管理和鉴权的逻辑基本已经完成了。

  • 开发JS脚本管理会话

首先需要提供登陆框,让用户输入用户名密码:

<div class="form">
    <h2>登录</h2>
    <input id="username" type="text" name="Username" placeholder="Username">
    <input id="paasword" type="password" name="Password" placeholder="Password" >
    <input type="button" value="Login" onclick="loginAction()">
</div>

实现登陆逻辑。登陆首先调用后台登陆接口,登陆成功后设置会话cookie:

function loginAction() {
     var username = document.getElementById("username").value;
     var password = document.getElementById("paasword").value;
     var formData = {};
     formData.userName = username;
     formData.password = password;

     $.ajax({
        type: 'POST',
        url: "/v1/user/login",
        data: formData,
        success: function (data) {
            setCookie("session-id", data.sessiondId, false);
            window.alert('登陆成功!');
        },
        error: function(data) {
            console.log(data);
            window.alert('登陆失败!' + data);
        },
        async: true
    });
}