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

前言

大家好!我是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获取这个用户的信息。

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

点赞
收藏

评论区

加载中...

相关推荐

灵感乍现!造了个与众不同的Dubbo注册中心扩展轮子

hello大家好呀,我是小楼。作为一名基础组件开发,服务好每一位业务开发同学是我们的义务(KPI)。客服群里经常有业务开发同学丢来一段代码、一个报错,而我们,当然要微笑服务,耐心解答。有的问题,凭借多年踩坑经验,一眼就能看出;有的问题,看一眼代码也能知道原因,但有的问题,还真就光凭看是看不出来的,这时,只能下载代码,本地跑跑看了。熟悉我的朋友都知道,我从事d

低代码开发平台 | 低代码的衍生历程、优势及未来趋势

通过简单的拖拉拽操作,而不用编写复杂的代码,实现少写代码或者不写代码,就能快速高效完成业务目标。低代码平台演进1.低代码概念低代码是无需编码(0代码)或通过少量代码就可以快速生成应用程序的开发平台。通过可视化进行应用程序开发的方法,具有不同经验水平的开发人员可以通过图形化的用户界面,使用拖拽组件和模型驱动的逻辑来创建网页和移动应用程序。2.低代码衍生历

Spring Boot快速开发企业级Admin管理后台

Erupt可快速的构建管理页面,零前端代码、零CURD、自动建表,仅需单个类文件简洁的注解配置,即可快速开发企业级Admin管理后台!后台管理系统非常重要,但开发存在一定的痛点,如:开发效率低、界面不美观、交互不理想、工作量重复、存在安全漏洞、后端研发被迫写前端代码等。我是程序汪Erupt提供企业级中后台管理系统的全栈解决方案,提供超多业务组

Sql Server之旅——第一站 那些给我们带来福利的系统视图

本来想这个系列写点什么好呢,后来想想大家作为程序员,用的最多的莫过于数据库了,但是事实上很多像我这样工作在一线的码农,对sql都一知半解,别谈优化和对数据库底层的认识了,我也是这样。。。一:那些系统视图1\.系统视图是干什么呢?从名字上看就知道,系统视图嘛?猜的不错的话,就是存放一些sqlserver系统的一些信息,

Hibernate纯sql查询结果和该sql在数据库直接查询结果不一致

问题:今天在做一个查询的时候发现一个问题,我先在数据库实现了我需要的sql,然后我在代码中代码:selectdistinctd.id,d.name,COALESCE(c.count_num,0),COALESCE(c.count_fix,0),COALESCE(c

PHP+MySql实现图书管理系统

这个图书管理系统是我学完PHP时写的一个练手项目,功能参考了自己学校的图书管理系统。为了锻炼自己的动手能力以及加深对代码的理解,前端和后端均由自己完成,前端使用了一些基本的框架(毕竟我主攻后端开发方向),后端大部分要用到的功能都是自己从底层实现并封装,基本没有用到第三方框架。总体来说还是比较简陋的,在某些地方可能存在缺陷或者漏洞。项目地址