《优化接口设计的思路》系列:第二篇—接口用户上下文的设计与实现

简介: 大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。作为一名从业已达六年的老码农,我的工作主要是开发后端Java业务系统,包括各种管理后台和小程序等。在这些项目中,我设计过单/多租户体系系统,对接过许多开放平台,也搞过消息中心这类较为复杂的应用,但幸运的是,我至今还没有遇到过线上系统由于代码崩溃导致资损的情况。这其中的原因有三点:一是业务系统本身并不复杂;二是我一直遵循某大厂代码规约,在开发过程中尽可能按规约编写代码;三是经过多年的开发经验积累,我成为了一名熟练工,掌握了一些实用的技巧。

前言

大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。

作为一名从业已达六年的老码农,我的工作主要是开发后端Java业务系统,包括各种管理后台和小程序等。在这些项目中,我设计过单/多租户体系系统,对接过许多开放平台,也搞过消息中心这类较为复杂的应用,但幸运的是,我至今还没有遇到过线上系统由于代码崩溃导致资损的情况。这其中的原因有三点:一是业务系统本身并不复杂;二是我一直遵循某大厂代码规约,在开发过程中尽可能按规约编写代码;三是经过多年的开发经验积累,我成为了一名熟练工,掌握了一些实用的技巧。

考虑到文字太过寡淡,我先上一张图

在Spring Boot中,默认情况下,每个请求到达时都会分配一个单独的线程来处理,而且请求的发起人也不一定都是同一个人,所以一个请求对应一个用户上下文,并且要求线程隔离,即不同线程的用户上下文互不影响,最后用户上下文还需要随着线程的结束而删除。
本文我会从用户上下文如何构建、如何使用、如何删除这三个方面解释接口用户上下文的设计与实现。

本文参考项目源码地址:summo-springboot-interface-demo

一、接口用户上下文的构建、使用、清除

1. 利用Filter拦截到每一个请求

由于接口散落在各个Controller中,且绝大部分接口都是需要这个用户上下文的(注:也不排除不需要用户上下文的接口存在),所以这里需要统一入口进行创建、销毁。看起来可以使用AOP的方式来实现,
不过这里有一个更合适的方案,利用SpringBoot自带的Filter【javax.servlet.Filter】来实现。

实现起来非常简单,我这边自定义了一个WebFilter,代码如下:

WebFilter.java

package com.summo.filter;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.summo.context.GlobalUserContext;
import com.summo.context.UserContext;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class WebFilter implements Filter {
   
   

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
   
   
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
        throws IOException, ServletException {
   
   
        try {
   
   
            //获取本次接口的唯一码
            String token = java.util.UUID.randomUUID().toString().replaceAll("-", "").toUpperCase();
            MDC.put("requestId", token);
            //获取请求头
            HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
            HttpServletResponse httpServletResponse = (HttpServletResponse)servletResponse;
            log.info("当前请求链接为:[{}]", httpServletRequest.getRequestURL());
            //设置用户上下文
            UserContext userContext = new UserContext();
            userContext.setUserId(1L);
            GlobalUserContext.setUserContext(userContext);
            //执行doFilter,这行一定要加,否则程序会中断掉
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        } catch (Exception e) {
   
   
            log.error("do doFilter exception", e);
        } finally {
   
   
            GlobalUserContext.clear();
            MDC.remove("requestId");
        }
    }

    @Override
    public void destroy() {
   
   
        Filter.super.destroy();
    }
}

这段代码的核心方法是:public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
我们可以在这个方法里面获取到ServletRequest和ServletResponse,这两个类能获取到代表着我们可以操作整个请求过程,这里如何确定当前请求的用户?下面有一张流程图供大家参考:

还有一种做法是使用JWT来当做用户token,因为JWT本身就可以存储一些信息,所以我们就不需要去缓存用户信息了,直接解析JWT即可,这种做法在分布式应用中很常见。

2. 获取当前请求的线程

上面已经获取到用户信息了,现在需要将用户信息放入用户上下文中,但由于请求的发起人不一定都是同一个人,所以一个请求对应着一个用户上下文,也即一个线程设置一个上下文。那么这里就需要获取到当前线程才能设置上下文。

获取当前线程有很多办法,这里推荐使用阿里巴巴开源的TTL框架(TransmittableThreadLocal)来实现,功能强大且用法简单。

引入方法如下:

<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>transmittable-thread-local</artifactId>
  <version>2.11.1</version>
</dependency>

使用方法如下:

 private static final TransmittableThreadLocal<UserContext> USER_HOLDER = new TransmittableThreadLocal<>();

直接new一个对象就行,而且支持泛型。

3. 用户上下文生命周期管理

对于用户上下文的生命周期管理需要定义3个方法:

  • 设置上下文用户信息;
  • 获取上下文用户信息
  • 清除上下文用户信息

    以上方法均为静态方法。

下面是一个简单的例子:
GlobalUserContext.java

package com.summo.context;

import com.alibaba.ttl.TransmittableThreadLocal;

public class GlobalUserContext {
   
   

    private static final TransmittableThreadLocal<UserContext> USER_HOLDER = new TransmittableThreadLocal<>();

    /**
     * 设置上下文用户信息
     *
     * @param user 用户信息
     */
    public static void setUserContext(UserContext user) {
   
   
        USER_HOLDER.set(user);
    }

    /**
     * 获取上下文用户信息
     */
    public static UserContext getUserContext() {
   
   
        return USER_HOLDER.get();
    }

    /**
     * 清除上下文用户信息
     */
    public static void clear() {
   
   
        USER_HOLDER.remove();
    }
}

UserContext.java

package com.summo.context;

import lombok.Data;

@Data
public class UserContext {
   
   

    /**
     * 用户ID
     */
    private Long userId;

}

调用方式如下:

设置上下文用户信息:GlobalUserContext.setUserContext(userContext);
获取上下文用户信息:GlobalUserContext.getUserContext();
清除上下文用户信息:GlobalUserContext.clear();

4. 用户上下文的使用

获取用户上下文很方便,调用GlobalUserContext.getUserContext();就行了,这里我主要讲一下用户上下文的使用场景。

a. 身份认证

可以将用户的身份认证信息(如用户名、密码、权限等)保存在用户上下文中,在需要进行鉴权的地方进行验证。

b. 用户日志记录

正如《优化接口设计的思路》系列:第三篇—在用户使用系统过程中留下痕迹 的方法三.

c. 防止接口数据越权

举个例子,比如有些业务需要获取当前登录用户的信息、当前登录用户的收藏、当前登录用户的浏览记录,这样的接口总不能在接口上传一个userId吧?真要这样干了,非得给安全骂死。。。
利用用户上下文的话,接口就可以不用传递任何参数获取到当前用户的userId,实现你的需求啦。

d. 跨服务调用

在分布式系统中,可以将用户上下文信息传递给其他服务,以保持用户的一致性和连贯性。

e. 监控和统计

可以将用户上下文中的信息用于系统的监控和统计,如请求的处理时间、请求的次数等。

5. 用户上下文的删除

删除很简单,调用GlobalUserContext.clear();即可,详情可见WebFilter.java内容。

二. 用户登录&认证

上面主要是说怎么获取到接口请求的用户以及怎么设置用户上下文,但没说用户身份是什么时候确认的以及怎么确认的,这里说一下常见做法。
想要确认用户信息就不得不提到用户登录&认证这套东西了,登录的方式非常多,简单的有账号密码登录、手机验证码登录,复杂的就是单点登录、三方授权登录如微信扫码、支付宝扫码等。虽然方式多,但是结果都一样的:确认当前用户身份

当前用户身份确认好之后,系统一般会根据当前用户信息生成一个唯一的并带有时效性的token,放入下一次请求的cookie中。等到下一次请求来的时候,我们就可以从cookie中获取这个token,利用这个token获取这个用户的信息。

由于用户认证情况太多,这里我就不贴代码了,上面是账号密码登录用户认证的的时序图,供大家参考。

目录
相关文章
|
存储 监控 小程序
《优化接口设计的思路》系列:第三篇—留下用户调用接口的痕迹
大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。 作为一名从业已达六年的老码农,我的工作主要是开发后端Java业务系统,包括各种管理后台和小程序等。在这些项目中,我设计过单/多租户体系系统,对接过许多开放平台,也搞过消息中心这类较为复杂的应用,但幸运的是,我至今还没有遇到过线上系统由于代码崩溃导致资损的情况。这其中的原因有三点:一是业务系统本身并不复杂;二是我一直遵循某大厂代码规约,在开发过程中尽可能按规约编写代码;三是经过多年的开发经验积累,我成为了一名熟练工,掌握了一些实用的技巧。
196 0
《优化接口设计的思路》系列:第三篇—留下用户调用接口的痕迹
|
网络协议 芯片
|
10月前
|
人工智能 编解码 机器人
NVILA:英伟达开源视觉语言大模型,高效处理高分辨率图像和长视频
NVILA是英伟达推出的视觉语言大模型,旨在高效处理高分辨率图像和长视频,同时保持高准确性。该模型通过“扩展-压缩”策略和多种优化技术,在多个领域如机器人导航和医疗成像中展现出广泛的应用潜力。
363 13
NVILA:英伟达开源视觉语言大模型,高效处理高分辨率图像和长视频
|
监控 小程序 Java
《优化接口设计的思路》系列:第五篇—接口发生异常如何统一处理
大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。
891 0
《优化接口设计的思路》系列:第五篇—接口发生异常如何统一处理
|
域名解析 监控 安全
接口开放太麻烦?试试阿里云API网关吧
我在[多方合作时,系统间的交互是怎么做的?](https://www.cnblogs.com/wlovet/p/17466812.html)这篇文章中写过一些多方合作时接口的调用规则和例子,然而,接口开放所涉及的安全、权限、监控、流量控制等问题,可不是简简单单就可以解决的,这一般需要专业的开放平台来支撑。但为了开放几个接口就要做一个开放平台,实在是不合算。为此阿里云为了解决这类需求推出了一款强大的工具——API网关。本文将介绍阿里云API网关的特点和优势,以及如何使用API网关来简化接口开放的过程。
622 0
接口开放太麻烦?试试阿里云API网关吧
|
Java Maven Spring
查看springboot版本支持最高的java版本
截至最近更新,Spring Boot 3.0及以上版本支持的最高Java版本为Java 17。鉴于技术的不断演进,建议直接参考Spring Boot的官方文档获取最准确的支持信息,因为这些版本兼容性可能会随着新版本的发布而有所变化。选择与你的Spring Boot版本相匹配的Java版本,可以确保充分利用框架特性,同时保证项目的稳定性和前瞻性。
776 0
|
缓存 NoSQL 前端开发
《优化接口设计的思路》系列:第六篇—接口防抖(防重复提交)的一些方式
本文探讨了后端开发中的接口防抖策略,作者是一名有六年经验的Java开发者,分享了如何防止重复提交导致的问题。防抖主要用于避免用户误操作或网络波动引起的多次请求,作者提出理想防抖机制应具备正确性、响应速度、易集成和用户反馈。文章详细分析了哪些接口需要防抖(如用户输入、按钮点击、滚动加载)以及如何识别重复接口,提出了使用共享缓存和分布式锁两种实现方式,并展示了基于Redis的Java代码示例。作者通过注解实现请求锁,并提供了测试截图证明防抖效果。然而,实现完全幂等性还需要业务层面的补充措施。
1011 8
|
存储 缓存 Java
《优化接口设计的思路》系列:第四篇—接口的权限控制
大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。 作为一名从业已达六年的老码农,我的工作主要是开发后端Java业务系统,包括各种管理后台和小程序等。在这些项目中,我设计过单/多租户体系系统,对接过许多开放平台,也搞过消息中心这类较为复杂的应用,但幸运的是,我至今还没有遇到过线上系统由于代码崩溃导致资损的情况。这其中的原因有三点:一是业务系统本身并不复杂;二是我一直遵循某大厂代码规约,在开发过程中尽可能按规约编写代码;三是经过多年的开发经验积累,我成为了一名熟练工,掌握了一些实用的技巧。
277 0
《优化接口设计的思路》系列:第四篇—接口的权限控制
|
存储 前端开发 测试技术
Android 官方架构中的 UseCase 该怎么写?
Android 官方架构中的 UseCase 该怎么写?
356 0
|
前端开发 Cloud Native Java
JSP 技术从问世到淘汰,它到底经历了什么?
JSP 技术从问世到淘汰,它到底经历了什么?
728 0