原创

SpringBoot-校验前后端代码版本是否一致-前后端版本-提示用户刷新页面

1. 需求背景

系统前后端发布后,用户如果未刷新页面,提交的接口数据可能就不符合后端最新的代码逻辑,此时需要告知用户刷新页面。

2. 实现思路

仅在前端需要set Authorization header头的接口中(可以共用shiro.anons配置,不用再梳理api接口)增加一个Api-Version的头,Api-Version的值每次迭代和后端约定即可。

以下场景需要推荐修改Api-Version的值,发布后:
有删减接口
接口的出入参结构发生变化
前端数据处理逻辑的变化

前端样式或提示文案等变化可不修改Api-Version的值,具体问题具体分析。

3. 代码

// WebMvcConfig.java

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    // 注入拦截器
    @Autowired
    private List<HandlerInterceptor> interceptorList = Collections.emptyList();


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        for (HandlerInterceptor handlerInterceptor : interceptorList) {
            logger.info("add interceptor " + handlerInterceptor.getClass());
            registry.addInterceptor(handlerInterceptor);
        }
        registry.addInterceptor(cleanInterceptor);
        registry.addInterceptor(new HandlerInterceptor() {


            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
///
                return true;
            }

            @Override
            public void postHandle(HttpServletRequest request, HttpServletResponse response,
                                   Object handler, ModelAndView modelAndView) throws Exception {

            }

            @Override
            public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                                        Object handler, Exception ex) throws Exception {

            }
        });
    }

    }

// VersionCheckInterceptor.java

package cn.jiangjiesheng.interceptor;

import cn.jiangjiesheng.common.constants.Constants;
import cn.jiangjiesheng.core.utils.IpUtil;
import cn.jiangjiesheng.exception.GnException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.PathMatcher;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;

/**
 * 轻量化实现 校验前后版代码版本是否一致
 */
@Order(Ordered.HIGHEST_PRECEDENCE + 3)
@Component
@Slf4j
public class VersionCheckInterceptor extends AbstractExcludeInterceptor {

    @Value("${api.version:}")
    private String currentReleaseApiVersion;

    @Value("#{'${shiro.anons:}'.split(',')}")
    private List<String> shiroAnonUrlList;

    @Autowired  //注入 AntPathMatcher.java
    private PathMatcher pathMatcher;

    private final static String HEADER_API_VERSION = "Api-Version";
    private final static String AUTHORIZATION = "Authorization";
    private final static String ERROR_API_VERSION_TIP_MSG = "当前系统已升级(最新版本%s),请先备份当前页面数据,然后刷新页面重试即可";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        checkCurrentApiVersion(request);
        return super.preHandle(request, response, handler);
    }

    /**
     * 校验前后端版本是否一致
     * api.version
     *
     * @param request
     */
    private void checkCurrentApiVersion(HttpServletRequest request) {
        //未配置当前发行版直接放行
        if (StringUtils.isBlank(currentReleaseApiVersion)) {
            return;
        }
        // request.getServletContext().getContextPath(); // /xxxx
        // request.getContextPath(); // /xxxx
        // request.getServletPath();// /pis/login

        String servletPath = request.getServletPath();
        if (StringUtils.isBlank(servletPath)) {
            return;
        }
             //TODO 删除
        System.err.println("拦截的url:" + servletPath);
        //当前请求是否属于 不拦截的url
        if (checkIsDoNotCheckVersionUrl(request, servletPath)) {
            return;
        }

        String apiVersion = request.getHeader(HEADER_API_VERSION);
        if (StringUtils.isBlank(apiVersion) || !StringUtils.equals(apiVersion, currentReleaseApiVersion)) {
            log.warn("当前客户端请求的前端页面未刷新到最新版本,ip: {},ua: {} ", IpUtil.getIpAddress(request), request.getHeader("user-agent"));
            throw new GnException(String.format(ERROR_API_VERSION_TIP_MSG, currentReleaseApiVersion));
        }
    }

    /**
     * 当前请求是否属于 不拦截的url
     *
     * @param servletPath
     * @return
     */
    private boolean checkIsDoNotCheckVersionUrl(HttpServletRequest request, String servletPath) {
        List<String> doNotCheckUrlList = getDoNotCheckVersionUrl();

        String authorization = request.getHeader(AUTHORIZATION);
        if (StringUtils.isBlank(authorization)) {
            // 没带authorization头的,一定是匿名的接口,直接返回,提交效率
          return true;
        }

         // 带了authorization头的,也有可能是匿名的接口

        // 快速精确匹配一次
        if (doNotCheckUrlList.contains(servletPath)) {
            return true;
        }
        //再模糊匹配一次
        return doNotCheckUrlList.stream().filter(urlPattern -> urlPattern.contains("*"))
                .anyMatch(urlPattern -> pathMatcher.match(urlPattern, servletPath));

    }

    /**
     * 不拦截的url或url pattern,支持 **
     *
     * @return
     */
    private List<String> getDoNotCheckVersionUrl() {

        // 支持redis
        // 免登陆的接口 + 给第三方调用的 (guest/gateway/*)
    //      比如 搜索 guest"  /guest /guest/gateway  /guest/wvp-alarm-get  /bis/guest /bss/guest
        //
        // 综合: 读取 shiro.anons 配置 即可
        // return Lists.newArrayList("/xxxx/pis/login", "/xxxx/bos/dataFlag/*");
        return shiroAnonUrlList;
    }

}

// application.properties

# 最新迭代的版本
api.version=v20240815

# 匿名访问的url
shiro.anons=/monitor/*,/pis/login,/pis/login/slide,/login,/v1/**,/v2/**,/**/guest/**,/webjars/**,/captcha/**,/swagger**/**,/doc.html,/**/*.html,/**/*.htm,*.ico,/dictExcel/*,/api/**,/doc/**,/dist/**,/test/*,/inspection/*,/hugeScreen/**
正文到此结束
本文目录