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的全局通用处理参考示意代码

// axios请求前置处理,请求之前,将token放到header中
axios.interceptors.request.use(request => {
  request.headers["token"] = localStorage.getItem("token");
  return request;
});
// axios请求后置处理
axios.interceptors.response.use(
    function(response) {
      let data = response.data;
      return new Promise(resolve => {
        if (data.code === "401") {
          if(localStorage.getItem("token")!=null && localStorage.getItem("token")!==""){
            Modal.error({
              title: "提示",
              content: data.message
                  ? data.message
                  : "您的账号已在其他地方登录,点击确定重新登录!",
              onOk: () => {
                sessionStorage.clear();
                localStorage.clear();
                window.parent.postMessage("refresh", "*");
              }
            });
          }else{
            window.parent.postMessage("refresh", "*");
          }
        } else {
          resolve(response);
        }
      }).catch(error => {
        console.log(error);
      });
    },
    function(error) {
      // 对响应错误做点什么
      console.log(error);
      const res = error.response;
      if (res && res.status === 401) {
        //token失效状态码
        // Message.warning("登陆失效,请您重新登陆!");
        message.warn("登陆失效,请您重新登陆!");
        //刷新当前页面
        window.parent.postMessage("refresh", "*");
        return new Promise(() => {});
      } else {
        return Promise.reject(error);
      }
    }
);
//router中的全局处理
router.beforeEach(async (to, from, next) => {
  let token = localStorage.getItem("token");
  if (to.name === "login") {
    if (token === "" || token == null) {
      next();
    } else {
      next({ name: "index" });
    }
  } else {
    if (token === "" || token == null) {
      //登录失效,跳转到登录
      next({ name: "login" });
    } else {
      if (to.matched.length === 0) {
        //没有匹配的路由,跳转到404
        next({ name: "404" });
      } else {
        next();
      }
    }
  }
});

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

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

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

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

       主要作用就是清空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的类,然后重载它的一些方法,并将这个对象向后传递,这样在后哦面流程中就可以使用我们放进去的数据的了。 先看下重写的类,参考代码如下:

public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {
    private String bodyJsonStr;
    public BodyReaderHttpServletRequestWrapper(HttpServletRequest request,String bodyJsonStr) throws IOException {
        super(request);
        this.bodyJsonStr = bodyJsonStr;

    }
    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream bais = new ByteArrayInputStream(bodyJsonStr.getBytes("utf-8"));
        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return bais.read();
            }
            @Override
            public boolean isFinished() {
                return false;
            }
            @Override
            public boolean isReady() {
                return false;
            }
            @Override
            public void setReadListener(ReadListener listener) {
            }
        };
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }

    public String getBodyJsonStr() {
        return bodyJsonStr;
    }

    public void setBodyJsonStr(String bodyJsonStr) {
        this.bodyJsonStr = bodyJsonStr;
    }
}

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

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

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
	HttpServletRequest req = (HttpServletRequest) request;
	HttpServletResponse rep = (HttpServletResponse) response;

	BufferedReader streamReader = new BufferedReader(new InputStreamReader(req.getInputStream(), "UTF-8"));

	StringBuilder responseStrBuilder = new StringBuilder();
	String inputStr;
	while ((inputStr = streamReader.readLine()) != null)
		responseStrBuilder.append(inputStr);
	if(responseStrBuilder!=null && responseStrBuilder.length()>0){
		JSONObject jsonObject = JSONObject.parseObject(responseStrBuilder.toString());
		
		jsonObject.put("userId","123");
		jsonObject.put("name","test");
		try
		{
			chain.doFilter(new BodyReaderHttpServletRequestWrapper(req,jsonObject.toJSONString()),rep);
			return;
		}catch (Exception e){
			e.printStackTrace();
			log.error("包装参数失败,失败原因:{}",e.getMessage());
		}
	}
	chain.doFilter(req, rep);
}

form-data

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

public class ParameterRequestWrapper extends HttpServletRequestWrapper
{

    private Map<String, String[]> params = new HashMap<String, String[]>();


    @SuppressWarnings("unchecked")
    public ParameterRequestWrapper(HttpServletRequest request)
    {
        // 将request交给父类,以便于调用对应方法的时候,将其输出,其实父亲类的实现方式和第一种new的方式类似
        super(request);
        //将参数表,赋予给当前的Map以便于持有request中的参数
        this.params.putAll(request.getParameterMap());
    }

    //重载一个构造方法
    public ParameterRequestWrapper(HttpServletRequest request, Map<String, Object> extendParams)
    {
        this(request);
        addAllParameters(extendParams);//这里将扩展参数写入参数表
    }

    @Override
    public String getParameter(String name)
    {//重写getParameter,代表参数从当前类中的map获取
        String[] values = params.get(name);
        if (values == null || values.length == 0)
        {
            return null;
        }
        return values[0];
    }

    @Override public Map<String, String[]> getParameterMap()
    {
        return params;
    }

    @Override public Enumeration<String> getParameterNames()
    {
        return new Vector<String>(params.keySet()).elements();
    }

    @Override
    public String[] getParameterValues(String name)
    {
        return params.get(name);
    }


    public void addAllParameters(Map<String, Object> otherParams)
    {//增加多个参数
        for (Map.Entry<String, Object> entry : otherParams.entrySet())
        {
            addParameter(entry.getKey(), entry.getValue());
        }
    }


    public void addParameter(String name, Object value)
    {//增加参数
        if (value != null)
        {
            if (value instanceof String[])
            {
                params.put(name, (String[]) value);
            }
            else if (value instanceof String)
            {
                params.put(name, new String[]{(String) value});
            }
            else
            {
                params.put(name, new String[]{String.valueOf(value)});
            }
        }
    }
}

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

       具体使用的时候如下:

ParameterRequestWrapper requestWrapper = new ParameterRequestWrapper((HttpServletRequest)request);
Map<String,Object>      rMap           = new HashMap<>();
rMap.put("userId","1");
rMap.put("useName","zhangsan");
requestWrapper.addAllParameters(rMap);
chain.doFilter(requestWrapper, rep);

后记

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

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
过滤器是Spring Boot中的一个重要组件,可以在HTTP请求被处理之前或响应被发送回客户端之前对请求和响应进行拦截和处理,从而实现一些功能,比如IP过滤。 下面是一个简单的IP过滤器实现: 1. 创建一个名为IpFilter的类,实现javax.servlet.Filter接口: ``` @Component public class IpFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; String ipAddress = req.getRemoteAddr(); // 实现IP过滤逻辑 if (!"127.0.0.1".equals(ipAddress)) { HttpServletResponse res = (HttpServletResponse) response; res.sendError(HttpServletResponse.SC_UNAUTHORIZED, "IP地址不允许访问!"); return; } chain.doFilter(request, response); } } ``` 2. 在Spring Boot应用口类中添加注解@EnableWebMvc和@ComponentScan,以启用Spring MVC和扫描过滤器类: ``` @EnableWebMvc @ComponentScan(basePackages = "com.example.filter") @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ``` 3. 在Spring Boot应用的配置文件中,添加过滤器的配置: ``` @Bean public FilterRegistrationBean<Filter> ipFilter() { FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>(); registration.setFilter(new IpFilter()); registration.addUrlPatterns("/*"); registration.setName("ipFilter"); registration.setOrder(1); return registration; } ``` 在以上代码中,我们创建了一个FilterRegistrationBean类型的Bean,使用setFilter方法设置要使用的过滤器类,使用addUrlPatterns方法设置要拦截的URL模式,使用setName方法设置过滤器名称,使用setOrder方法设置过滤器执行顺序。 通过以上步骤,我们就成功地实现了一个IP过滤器。需要注意的是,我们可以根据实际需求,修改过滤器的实现逻辑,比如可以从请求头中获取IP地址,或者通过配置文件设置允许访问的IP地址列表等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值