Link (QR code) sharing needs

  • Function module Add the sharing link (QR code) function. You can view the details of the function module through the shared link.

  • If you open an expired link, a message is displayed indicating that the link has expired.

  • 3 Links shared by some modules can only be opened by specified users in the system. If the links are opened by non-specified users outside the system, there is a message indicating that they have no permission.

  • Supports sharing on Android/Ios/Web. Scan the QR code on Android/Ios to jump to the corresponding function module

Programming scheme

Database script
CREATE TABLE `url_share` (
  `id` varchar(32) NOT NULL COMMENT 'primary key',
  `userSn` varchar(10) NOT NULL COMMENT 'Originator of sharer',
  `expire` bigint(20) NOT NULL COMMENT 'Expiration time,-1 means permanent',
  `shareParam` longtext NOT NULL COMMENT 'Share parameters',
  `shareModule` varchar(20) NOT NULL COMMENT 'Owning module',
  `shareToken` varchar(32) NOT NULL COMMENT 'share token',
  `shareUrl` varchar(512) NOT NULL COMMENT 'Shared links',
  `shareTime` datetime NOT NULL COMMENT 'Share time'.PRIMARY KEY (`id`),
  UNIQUE KEY `ix_shareToken` (`shareToken`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Link sharing';


CREATE TABLE `url_share_userauth` (
  `id` varchar(32) NOT NULL,
  `userSn` varchar(10) NOT NULL COMMENT 'User pass',
  `shareId` varchar(32) NOT NULL COMMENT 'share id'.PRIMARY KEY (`id`),
  KEY `ix_shareId` (`shareId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Link Sharing User Authorization';
Copy the code
Interface To open Or reuse an existing interface

Agree with the client in advance to add the /share prefix to the request URI of all interfaces used for sharing links (QR codes) as the identifier of the sharing interface

1. The new/share/xxxThe interface of the

For example, existing business dependencies /doBiz interfaces need to be shared

@PostMapping("/doBiz")
public void doBiz(@RequestParam String param) throws Exception {
    helloService.doBiz(param);
}
Copy the code

Add an interface for sharing, define it as /share/doBiz, and reuse the service method

@PostMapping("/share/doBiz")
public void shareDoBiz(@RequestParam String param) throws Exception {
    helloService.doBiz(param);
}
Copy the code

Every time a new module needs to share, one or more /share/ XXX interfaces should be added to the controller layer, resulting in code duplication. Imagine that in the iteration process of different versions, there will be a need for modules to add the sharing function, and then add one or more /share/ XXX interfaces, which is difficult to accept.

2. Modify the request to reuse the existing interface

The Servlet Filter or Spring Cloud’s ZuulFilter allows us to modify the HttpServerletRequest request URI and Request before the request is actually forwarded to the ServerletDispatcher Param, here is an example of tampering with a request through Spring Cloud’s ZuulFilter.

2.1 With Front-end Conventions The format of calling service interfaces on all sharing pages is as follows:

Get(Post)  /share/doBiz...
Copy the code

2.2 Configuring the INTERFACE URI that can be accessed through /share/ XXX

If all interfaces are allowed to be exposed in the form of /share/ XXX, it is a very serious vulnerability of the system, which may cause irreparable losses to businesses sensitive to business data. We can configure interfaces that are allowed to be accessed through /share/ XXX for each module through configuration files. In this way, every time the sharing function needs to be added to a new module, only the configuration file needs to be added. For the interface that does not meet the request URI of the configuration module, skip the Filter that tampers the request parameters (address) and continue to execute.

Configuration file urshare.json

[{"htmlUrl":"http://172.16.1.133:9529/#/share"."reqUrls": ["/xxxx/task/findTaskType/**"."/xxx/task/taskDetail/**"]."module":"taskDetail"
    },
    {
        "htmlUrl":"http://172.16.1.133:9529/#/share"."reqUrls": ["/xxx/user/info/**"]."module":"userInfo"}]Copy the code
  • HtmlUrl: generated share link address prefix, the resulting is usually Shared links to http://172.16.1.133:9529/#/share? shareToken=xxx
  • ReqUrls: Pages that share links require the requested background URI address
  • Module: Sharing module

2.3 Method for matching URI rules

public static boolean pathMatchPattern(String path, List<String> patterns) {
    boolean result = false;
    for (String pattern : patterns) {
        //Spring provides a utility class for matching URI regex
        AntPathMatcher matcher = new AntPathMatcher();
        if (matcher.match(pattern, path)) {
            result = true;
            break; }}return result;
}
Copy the code
Tampering withHttpServletRequestRequest and check/share/doBizRequests the gatewayZuulFilter
@Component
public class UrlShareFilter extends ZuulFilter implements ApplicationRunner {

    private Logger logger = LoggerFactory.getLogger(UrlShareFilter.class);

    @Resource
    private RedisUtil redisUtil;

    @Resource
    private UrlShareFeignService urlShareFeignService;

    @Override
    public String filterType(a) {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder(a) {
        return 0;
    }

    @Override
    public boolean shouldFilter(a) {
        RequestContext ctx = RequestContext.getCurrentContext();
        String reqUri = ctx.getRequest().getRequestURI();
        if (reqUri.indexOf("/share/") != -1) {
            try {
                return PathUtils.pathMatchPattern(reqUri.replaceFirst("/share", EmptyUtils.EMPTY_STR), urlShareFeignService.shareReqUrls());
            } catch (Exception e) {
                logger.error("urlShareFeignService.shareReqUrls error", e);
                 return false; }}return false;
    }

    @Override
    public Object run(a) throws ZuulException {
        // Parse and validate shareToken
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        String shareToken = null;
        Map<String, String[]> queryParamMap = request.getParameterMap();
        if (EmptyUtils.isNotEmpty(queryParamMap)) {
            String[] queryParam = queryParamMap.get(Constants.SHARE_TOKEN_HEADER);
            if (EmptyUtils.isNotEmpty(queryParam)) {
                shareToken = queryParam[0];
            }
        }
        String usrToken = null;
        if (EmptyUtils.isEmpty(shareToken)) {
            sendResp(ctx, HttpStatus.UNAUTHORIZED.value(), "Sharing links is illegal");
            return false;
        } else {
            UrlShareInfo urlShareInfo = null;
            try {
                urlShareInfo = urlShareFeignService.getUrlShareInfo(shareToken);
            } catch (Exception ex) {
                sendResp(ctx, HttpStatus.INTERNAL_SERVER_ERROR.value(), "Internal service error");
                return false;
            }
            if (urlShareInfo == null) {
                sendResp(ctx, HttpStatus.UNAUTHORIZED.value(), "Share link invalid");
                return false;
            }
            if (urlShareInfo.getExpire() > 0&& urlShareInfo.getExpire() <= System.currentTimeMillis()) {
                sendResp(ctx, HttpStatus.UNAUTHORIZED.value(), "Share link expired");
                return false;
            }
            if (EmptyUtils.isNotEmpty(ctx.getRequest().getHeader(Constants.TOKEN_HEADER))) {
                usrToken = ctx.getRequest().getHeader(Constants.TOKEN_HEADER);
            }
            if (EmptyUtils.isNotEmpty(urlShareInfo.getAuthUserSns())) {
                if (usrToken == null) {
                    sendResp(ctx, HttpStatus.UNAUTHORIZED.value(), "User has no permission");
                    return false;
                } else {
                    // Share links require user permission to open
                    if(redisUtil.get(usrToken) ! =null) {
                        UserSession userSession = JSONObject.parseObject(redisUtil.get(usrToken).toString(), UserSession.class);
                        if(userSession ! =null) {
                            if(! urlShareInfo.getAuthUserSns().contains(userSession.getUserSn())) { sendResp(ctx, HttpStatus.UNAUTHORIZED.value(),"User has no permission");
                                return false; }}else {
                            sendResp(ctx, HttpStatus.UNAUTHORIZED.value(), "User has no permission");
                            return false; }}else {
                        sendResp(ctx, HttpStatus.UNAUTHORIZED.value(), "User has no permission");
                        return false; }}}}// Share requests are redirected to normal requests
        final String realToken = (usrToken == null ? Constants.QRCODE_SHARE_REDIS_KEY : usrToken);
        String url = request.getRequestURI().replaceFirst("/share", EmptyUtils.EMPTY_STR);
        ctx.setRequest(new HttpServletRequestWrapper(request) {
            @Override
            public String getRequestURI(a) {
                return url;
            }

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

            @Override
            // Set the Cookie(Token) parameter for sharing, which is used to access background interfaces
            public String getHeader(String name) {
                if (name.equals(Constants.TOKEN_HEADER) || name.equals(WpsConst.HEAD_TOKEN)) {
                    return realToken;
                }
                return super.getHeader(name); }}); Map<String, List<String>> requestQueryParams = ctx.getRequestQueryParams();if (requestQueryParams == null) {
            requestQueryParams = new HashMap<>();
        }
        requestQueryParams.remove(Constants.SHARE_TOKEN_HEADER);
        ctx.setRequestQueryParams(requestQueryParams);
        ctx.put(FilterConstants.REQUEST_URI_KEY, url);
        ctx.addZuulRequestHeader(Constants.TOKEN_HEADER, realToken);
        return true;
    }

    private void sendResp(RequestContext ctx, Integer code, String errorMsg) {
        ctx.setSendZuulResponse(false);
        try {
            ctx.setResponseStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
            ctx.getResponse().setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            ctx.getResponse().getWriter().write(JSONObject.toJSONString(ResponseResult.fail(code, errorMsg)));
        } catch(Exception e) { logger.info(logger.toString()); }}@Override
    public void run(ApplicationArguments args) throws Exception {
        // Initialize Session data for sharingredisUtil.set(Constants.QRCODE_SHARE_REDIS_KEY, Constants.QRCODE_SHARE_REDIS_VAL); }}Copy the code
  • Some interfaces need to obtain user informationApplicationRunner.runInitialize sharingCookie(Token)theSessiondata
  • shouldFilterMethod used to judge/share/doBizRequest, whether permission to pass throughUrlShareFilterTamper with the request URI and parameters to determine the logic: match the requesturlshare.jsonConfiguration of thereqUrlsOne of themuriRules. If you do, you need to passrunMethod to tamper with the request.
  • runMethods according to theshareTokenSearch the shared parameter information, such as link timeliness, validity and authorizer and verify, and then tamper withRequestUriGet rid of/shareThe prefix andrequestParam, added for sharingCookie(Token)information
  • Pay attention toUrlShareFilterThe highest priority should be configured for
Generate qr code

Use the QrCodeUtil class of the HuTool toolkit to create the QR code and return it to the client

 @PostMapping(value = "/shareUrlQrcode", produces = "application/octet-stream; charset=UTF-8")
    public void shareUrlQrcode(@RequestBody GetShareUrlParam getShareUrlParam) throws Exception   		{
        try {
            HttpServletResponse response = getResponse();
            response.setHeader("Pragma"."No-cache");
            response.setHeader("Cache-Control"."no-cache");
            response.setDateHeader("Expires".0);
            response.setContentType("image/png");
            String shareUrl = urlShareService.shareUrl(getShareUrlParam, getUserSn());
            QrConfig qrConfig = 	QrConfig.create().setWidth(500).setHeight(500).setMargin(0).setImg(ImageIO.read(ResourceUtil.getStream("qrcodelog.png")));
            QrCodeUtil.generate(shareUrl, qrConfig,"png", response.getOutputStream());
        }catch (Exception e){
            logger.error("shareUrlQrcode error,getShareUrlParam={}", getShareUrlParam, e);
            throwe; }}Copy the code

conclusion

It is best not to encrypt expiration time/authorized user information directly into requestParam parameter transfer, because the uncertainty of parameter size will lead to a very dense TWO-DIMENSIONAL code, the camera in scanning dense TWO-DIMENSIONAL code effect will become very poor.

By using shareToken, the backend sends UrlShareFilter to obtain the verification expiration time or authorized user based on the shareToken. The front end can call the interface with the shareToken parameter to get the parameter information needed to share the page. And the length of the TWO-DIMENSIONAL code link is determined, the scanning performance of the two-dimensional code is guaranteed.