• Springboot项目通过filter修改接口的入参


    目录

    前言

    技术栈

    架构设计

    前端统一门户

    后端用户中心

    UserAuthority公用依赖

    过滤器

    application/json

    form-data

    后记


    前言

           在多个子工程的微服务开发的时候,后端通常情况下都是不止一个工程,前端深知也会不止一个工程,开发的团队也许也不止一个团队。 这时候,在用户校验、权限控制、功能集成方面就会需要有一套架构方案来管控。在整体的架构方面有几个要求:

           (1)根据业务需要独立拆分新建的子工程,只需要关注业务功能的代码开发即可,不需要再关注用户、角色、权限以及集成的问题;

           (2)子系统开发的时候,只需要引入pom依赖就可以非常方便获取用户信息以及对接口服务进行鉴权处理;

           (3)前端子工程同样只需要关注实际的业务功能页面开发,不需要考虑登录、登出、用户信息获取这些问题;

           (4)集成的时候,只需要提供微服务的上层负载接口地址和前端页面的路由地址即可。

           如何做到这样松耦合?但是又能紧密依赖和协调一致呢? 

           对于后端来说,重点在于统一的规范设计、Redis分布式缓存、Filter过滤器、Maven依赖;对于前端来说,重点在于统一的axios过滤请求、路由处理、sessionStorage、自定义npm依赖包等等。

    技术栈

           前端:vue/axios/vueRouter 等等

           后端:Springboot/SpringCloud/Dubbo 等等

           中间件:Redis/Nginx 等等

    架构设计

           在软件的系统架构上,主要有以下几点:

           (1)完全的前后端分离项目

           (2)统一前端门户中心+统一后端用户中心

           (3)多个前端工程打包后放到Nginx中进行静态代理的访问

           (4)系统入口是统一门户中入口路由

           (5)通过router.beforeEach校验sessionStorage中是否有用户信息,对前端路由进行鉴权控制

           (6)通过axios的interceptors统一将sessionStorage中的token信息放入header中,并统一过滤非法请求

           (7)通过Reids存储用户信息

           (8)通过Nginx做请求转发,代替传统意义的网关

           (9)通过开发UserAuthority依赖包,让每个子工程引入该依赖来完成用户信息传递以及权限校验

           重点介绍和业务无关的两个工程,前端统一门户和后端用户中心。 

    前端统一门户

           前端统一门户主要包括登录页面、门户首页框架两部分。 其中门户首页框架是典型的标题栏加左右布局结构,左边菜单栏、右边是内容区域。

           标题栏可以根据项目规模大小再扩展下,支持logo自定义、横向一级菜单、消息中心、用户个人中心等功能。 

           左侧菜单区域是标准的菜单menu组件,菜单信息通过接口获取。 右侧内容区域正常是一个ifream,通过点击菜单中的url地址,更新ifream的src来实现。

           统一门户登录成功过之后,前端会将用户信息、token信息保存到sessionStorage中。其他前端工程中直接从storage中获取这些信息,在路由跳转和axios请求的时候,直接使用。

           如下是axios和router的全局通用处理参考示意代码

    1. // axios请求前置处理,请求之前,将token放到header中
    2. axios.interceptors.request.use(request => {
    3. request.headers["token"] = localStorage.getItem("token");
    4. return request;
    5. });
    6. // axios请求后置处理
    7. axios.interceptors.response.use(
    8. function(response) {
    9. let data = response.data;
    10. return new Promise(resolve => {
    11. if (data.code === "401") {
    12. if(localStorage.getItem("token")!=null && localStorage.getItem("token")!==""){
    13. Modal.error({
    14. title: "提示",
    15. content: data.message
    16. ? data.message
    17. : "您的账号已在其他地方登录,点击确定重新登录!",
    18. onOk: () => {
    19. sessionStorage.clear();
    20. localStorage.clear();
    21. window.parent.postMessage("refresh", "*");
    22. }
    23. });
    24. }else{
    25. window.parent.postMessage("refresh", "*");
    26. }
    27. } else {
    28. resolve(response);
    29. }
    30. }).catch(error => {
    31. console.log(error);
    32. });
    33. },
    34. function(error) {
    35. // 对响应错误做点什么
    36. console.log(error);
    37. const res = error.response;
    38. if (res && res.status === 401) {
    39. //token失效状态码
    40. // Message.warning("登陆失效,请您重新登陆!");
    41. message.warn("登陆失效,请您重新登陆!");
    42. //刷新当前页面
    43. window.parent.postMessage("refresh", "*");
    44. return new Promise(() => {});
    45. } else {
    46. return Promise.reject(error);
    47. }
    48. }
    49. );
    50. //router中的全局处理
    51. router.beforeEach(async (to, from, next) => {
    52. let token = localStorage.getItem("token");
    53. if (to.name === "login") {
    54. if (token === "" || token == null) {
    55. next();
    56. } else {
    57. next({ name: "index" });
    58. }
    59. } else {
    60. if (token === "" || token == null) {
    61. //登录失效,跳转到登录
    62. next({ name: "login" });
    63. } else {
    64. if (to.matched.length === 0) {
    65. //没有匹配的路由,跳转到404
    66. next({ name: "404" });
    67. } else {
    68. next();
    69. }
    70. }
    71. }
    72. });

           这里有个细节,就是如下这一行代码:

    window.parent.postMessage("refresh", "*");
    

           它的主要作用是子框架和父框架进行通信,如果子框集中出现鉴权失败了,要通知父框架进行页面路由跳转,直接跳转到登录页面。 在整个架构中,子框架就是ifream中业务前端独立工程,父框架是统一门户的前端,在统一门户中,关于这块消息通信的处理如下:

    1. window.addEventListener("message", function(e) {
    2. if (e.data === "refresh") {
    3. localStorage.removeItem("token");
    4. localStorage.clear();
    5. rootApp.$router.push({ name: "login" });
    6. }
    7. });

           主要作用就是清空storage,然后跳转页面到登录页面。

    后端用户中心

           用户端用户中心主要提供登录、登出、查询用户菜单权限这些接口功能。重点是是登录登出接口,登录成功之后,会将用户信息、token返回给前端,同时保存一份到redis中。

           关于redis中存储用户信息,建议将key设置为用户的userId,将value设置为一个对象,包含有token和用户基本信息,同时设置有效期。 然后token的生成规则可以通过AES可逆的加密加密解密方式来实现,解密token后,可以从token中直接split出userId,然后再拿userId到redis中获取用户信息,再校验传递过来的token和从token解析出来的用户Id从redis查到的token是否一致,进而判断请求是否合法。

    UserAuthority公用依赖

           主要作用是继承一个filter,集合白名单机制对接口进行过滤校验。 如果在白名单中,则直接放行,如果没有在白名单中,则判断header中是否有token,如果没有token或者token校验不通过则返回401鉴权不通过。 前端在统一个axios.interceptors.response中进行页面的统一跳转。 如果token校验通过了,可以在filter中通过token获取从redis中获取用户信息,然后将用户信息作为入参信息向后传递,即:我们给入参数据新增一些用户信息字段,向后传递,在后面的业务工程接口中,直接从reqBo中就能获取到用户信息了。

           这也要求所有的入参对象都要继承统一的入参Bo。 

    过滤器

           前面说了那么多,都是在整个软件的集成架构上来讨论的,我们回到本篇文章的重点,如何在Filter中对请求的入参信息进行增加。在Filter中获取入参信息包括两种方式,一种是获取InputStream,一种是getParameterMap。前者对应content-type是application/json,后者对应content-type是form-data或者application/x-www-form-urlencoded或者是get请求。 content-type不同,处理方式是不同的。 

           注:我们暂时先不考虑文件类型。 

    application/json

           正常情况下,request的getInputStream和getParameterMap返回的对象都是受保护,不允许修改的,所以就需要我们进行一些特殊处理,关于stream这种,我们需要自定义个集成HttpServletRequestWrapper的类,然后重载它的一些方法,并将这个对象向后传递,这样在后哦面流程中就可以使用我们放进去的数据的了。 先看下重写的类,参考代码如下:

    1. public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {
    2. private String bodyJsonStr;
    3. public BodyReaderHttpServletRequestWrapper(HttpServletRequest request,String bodyJsonStr) throws IOException {
    4. super(request);
    5. this.bodyJsonStr = bodyJsonStr;
    6. }
    7. @Override
    8. public ServletInputStream getInputStream() throws IOException {
    9. final ByteArrayInputStream bais = new ByteArrayInputStream(bodyJsonStr.getBytes("utf-8"));
    10. return new ServletInputStream() {
    11. @Override
    12. public int read() throws IOException {
    13. return bais.read();
    14. }
    15. @Override
    16. public boolean isFinished() {
    17. return false;
    18. }
    19. @Override
    20. public boolean isReady() {
    21. return false;
    22. }
    23. @Override
    24. public void setReadListener(ReadListener listener) {
    25. }
    26. };
    27. }
    28. @Override
    29. public BufferedReader getReader() throws IOException {
    30. return new BufferedReader(new InputStreamReader(this.getInputStream()));
    31. }
    32. public String getBodyJsonStr() {
    33. return bodyJsonStr;
    34. }
    35. public void setBodyJsonStr(String bodyJsonStr) {
    36. this.bodyJsonStr = bodyJsonStr;
    37. }
    38. }

           可以看到,我们上是重写了getInputStream,将原来的stream和新的参数合并到一块,返回出去。 实际上我们并没有真的修改原始的request参数,只不过是重新生成了一个request。

           下面贴一段具体使用时候的代码:

    1. @Override
    2. public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    3. HttpServletRequest req = (HttpServletRequest) request;
    4. HttpServletResponse rep = (HttpServletResponse) response;
    5. BufferedReader streamReader = new BufferedReader(new InputStreamReader(req.getInputStream(), "UTF-8"));
    6. StringBuilder responseStrBuilder = new StringBuilder();
    7. String inputStr;
    8. while ((inputStr = streamReader.readLine()) != null)
    9. responseStrBuilder.append(inputStr);
    10. if(responseStrBuilder!=null && responseStrBuilder.length()>0){
    11. JSONObject jsonObject = JSONObject.parseObject(responseStrBuilder.toString());
    12. jsonObject.put("userId","123");
    13. jsonObject.put("name","test");
    14. try
    15. {
    16. chain.doFilter(new BodyReaderHttpServletRequestWrapper(req,jsonObject.toJSONString()),rep);
    17. return;
    18. }catch (Exception e){
    19. e.printStackTrace();
    20. log.error("包装参数失败,失败原因:{}",e.getMessage());
    21. }
    22. }
    23. chain.doFilter(req, rep);
    24. }

    form-data

           form-data的处理方式实际上和application/json是差不多的,原理都一样,区别在于重构的类不同,需要继承HttpServletRequestWrapper重写一个类。参考代码如下:

    1. public class ParameterRequestWrapper extends HttpServletRequestWrapper
    2. {
    3. private Map params = new HashMap();
    4. @SuppressWarnings("unchecked")
    5. public ParameterRequestWrapper(HttpServletRequest request)
    6. {
    7. // 将request交给父类,以便于调用对应方法的时候,将其输出,其实父亲类的实现方式和第一种new的方式类似
    8. super(request);
    9. //将参数表,赋予给当前的Map以便于持有request中的参数
    10. this.params.putAll(request.getParameterMap());
    11. }
    12. //重载一个构造方法
    13. public ParameterRequestWrapper(HttpServletRequest request, Map extendParams)
    14. {
    15. this(request);
    16. addAllParameters(extendParams);//这里将扩展参数写入参数表
    17. }
    18. @Override
    19. public String getParameter(String name)
    20. {//重写getParameter,代表参数从当前类中的map获取
    21. String[] values = params.get(name);
    22. if (values == null || values.length == 0)
    23. {
    24. return null;
    25. }
    26. return values[0];
    27. }
    28. @Override public Map getParameterMap()
    29. {
    30. return params;
    31. }
    32. @Override public Enumeration getParameterNames()
    33. {
    34. return new Vector(params.keySet()).elements();
    35. }
    36. @Override
    37. public String[] getParameterValues(String name)
    38. {
    39. return params.get(name);
    40. }
    41. public void addAllParameters(Map otherParams)
    42. {//增加多个参数
    43. for (Map.Entry entry : otherParams.entrySet())
    44. {
    45. addParameter(entry.getKey(), entry.getValue());
    46. }
    47. }
    48. public void addParameter(String name, Object value)
    49. {//增加参数
    50. if (value != null)
    51. {
    52. if (value instanceof String[])
    53. {
    54. params.put(name, (String[]) value);
    55. }
    56. else if (value instanceof String)
    57. {
    58. params.put(name, new String[]{(String) value});
    59. }
    60. else
    61. {
    62. params.put(name, new String[]{String.valueOf(value)});
    63. }
    64. }
    65. }
    66. }

           仔细看看这个重写的类会发现,实际上就是重写了getParameter/getParameterMap等等这些方法,在这些方法内部返回的数据加上自己传递的参数。 

           具体使用的时候如下:

    1. ParameterRequestWrapper requestWrapper = new ParameterRequestWrapper((HttpServletRequest)request);
    2. Map rMap = new HashMap<>();
    3. rMap.put("userId","1");
    4. rMap.put("useName","zhangsan");
    5. requestWrapper.addAllParameters(rMap);
    6. chain.doFilter(requestWrapper, rep);

    后记

           我们在业务工程中定义所有的reqBo的时候,都将这个Bo的定义继承父类的BaseReqBo,在父类的BaseReqBo中我们可以定义那些公用的用户信息或者通用字段信息,这些信息可以在上面的filter中去赋值,这样只要业务工程依赖了UserAuthority.jar,就会自动处理用户、权限这些信息。非常的方便,而且对于实际开发的同学来说,无需关心这些细节,只需要关心业务逻辑代码处理即可。 

  • 相关阅读:
    游游现在有a个 y ,b个 o ,c个 u ,他想用这些字母拼成一个字符串。
    在Azure上使用Portal查出造成SQL注入攻击的语句
    Java常用17个工具类方法,提升开发效率的“轮子”,避免重复造轮子
    【设计模式】原型模式:猴头,我叫你一声你敢答应吗?
    【LeetCode】每日一题 2023_11_18 数位和相等数对的最大和(模拟/哈希)
    【C++编程能力提升】
    python机器人编程——用python实现一个写字机器人
    Android native层Hander原理分析
    (3)(3.5) 遥测无线电区域条例
    负载均衡、反向代理(8月26号)
  • 原文地址:https://blog.csdn.net/liangcha007/article/details/126013945