实现二维码(链接)分享

链接(二维码)分享需求

  • 功能模块添加分享链接(二维码)功能,通过分享出去的链接,可查看功能模块的详情。
  • 分享出去的链接在1天/3天/7天/30天/永不过期,打开过期的链接,弹出提示页面链接已过期。
  • 3某些模块分享的链接只能由系统内的指定用户打开,在其他系统外或者非指定的用户打开提示无权限。
  • 支持Android/Ios/Web端分享,在Android/Ios端内扫描二维码直接跳转至相应功能模块

程序设计方案

二维码分享 (1)

数据库脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
sql复制代码CREATE TABLE `url_share` (
`id` varchar(32) NOT NULL COMMENT '主键',
`userSn` varchar(10) NOT NULL COMMENT '发起分享人',
`expire` bigint(20) NOT NULL COMMENT '过期时间,-1代表永久',
`shareParam` longtext NOT NULL COMMENT '分享参数',
`shareModule` varchar(20) NOT NULL COMMENT '所属模块',
`shareToken` varchar(32) NOT NULL COMMENT '分享token',
`shareUrl` varchar(512) NOT NULL COMMENT '分享的链接',
`shareTime` datetime NOT NULL COMMENT '分享时间',
PRIMARY KEY (`id`),
UNIQUE KEY `ix_shareToken` (`shareToken`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='链接分享';


CREATE TABLE `url_share_userauth` (
`id` varchar(32) NOT NULL,
`userSn` varchar(10) NOT NULL COMMENT '用户通行证',
`shareId` varchar(32) NOT NULL COMMENT '分享id',
PRIMARY KEY (`id`),
KEY `ix_shareId` (`shareId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='链接分享用户授权';
接口新开Or复用已有接口

事先与客户端约定所有用于分享链接(二维码)的接口request uri前添加/share前缀作为分享接口的标识

1.新增/share/xxx的接口

比如现有业务依赖/doBiz接口,需要实现分享功能

1
2
3
4
java复制代码@PostMapping("/doBiz")
public void doBiz(@RequestParam String param) throws Exception {
helloService.doBiz(param);
}

新增一个用于分享的接口,定义为/share/doBiz,然后复用service的方法

1
2
3
4
java复制代码@PostMapping("/share/doBiz")
public void shareDoBiz(@RequestParam String param) throws Exception {
helloService.doBiz(param);
}

每有一个新模块需要分享功能,在控制层controller要增加一个或者多个/share/xxx的接口,造成代码重复。试想在不同的版本迭代过程中,都会存在模块添加分享功能的需求,到时候再去增加一个或者多个/share/xxx的接口,这是很难接受的。

2.篡改请求复用已有接口

ServletFilter或者Spring CloudZuulFilter允许我们在收到请求真正转发给ServerletDispatcher之前修改HttpServerletRequestrequest urirequest param,下面是一个通过Spring CloudZuulFilter篡改请求的例子。

2.1与前端约定所有分享页面调用业务接口的格式为:

1
java复制代码Get(Post)  /share/doBiz...

2.2配置可用于通过/share/xxx访问的接口uri

如果将所有接口都允许通过/share/xxx的形式暴露出去,这是非常严重的系统漏洞,对于业务数据敏感的业务可能会带来无法挽回的损失,我们可以通过配置文件给每个模块配置允许通过/share/xxx访问的接口,这样在每次需要给新模块添加分享功能时,仅仅需要添加配置文件,对于不符合配置模块请求uri的接口,跳过篡改请求参数(地址)的Filter继续执即可。

配置文件urshare.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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"
}
]

2.3匹配uri是否符合规则的方法

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public static boolean pathMatchPattern(String path, List<String> patterns) {
boolean result = false;
for (String pattern : patterns) {
//Spring提供的用于匹配uri正则的工具类
AntPathMatcher matcher = new AntPathMatcher();
if (matcher.match(pattern, path)) {
result = true;
break;
}
}
return result;
}
篡改HttpServletRequest请求和校验/share/doBiz请求网关ZuulFilter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
java复制代码@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() {
return PRE_TYPE;
}

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

@Override
public boolean shouldFilter() {
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() throws ZuulException {
//解析并验证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(), "分享链接不合法");
return false;
} else {
UrlShareInfo urlShareInfo = null;
try {
urlShareInfo = urlShareFeignService.getUrlShareInfo(shareToken);
} catch (Exception ex) {
sendResp(ctx, HttpStatus.INTERNAL_SERVER_ERROR.value(), "服务内部错误");
return false;
}
if (urlShareInfo == null) {
sendResp(ctx, HttpStatus.UNAUTHORIZED.value(), "分享链接无效");
return false;
}
if (urlShareInfo.getExpire() > 0&& urlShareInfo.getExpire() <= System.currentTimeMillis()) {
sendResp(ctx, HttpStatus.UNAUTHORIZED.value(), "分享链接已过期");
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(), "用户无权限");
return false;
} else {
//分享链接需要用户权限打开
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(), "用户无权限");
return false;
}
} else {
sendResp(ctx, HttpStatus.UNAUTHORIZED.value(), "用户无权限");
return false;
}
} else {
sendResp(ctx, HttpStatus.UNAUTHORIZED.value(), "用户无权限");
return false;
}
}
}
}
//share请求重定向到正常请求
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() {
return url;
}

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

@Override
//设置用于分享的Cookie(Token)参数,访问后台接口使用
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 {
//初始化用于分享用到的Session数据
redisUtil.set(Constants.QRCODE_SHARE_REDIS_KEY, Constants.QRCODE_SHARE_REDIS_VAL);
}

}
  • 由于部分接口访问需要获取用户信息,先通过ApplicationRunner.run初始化分享Cookie(Token)Session数据
  • shouldFilter方法用于判断/share/doBiz请求,是否允许经过UrlShareFilter篡改请求uri和参数,判断逻辑:匹配请求是否符合urlshare.json配置的reqUrls其中的一条uri规则,是的话就需要通过run方法篡改请求。
  • run方法根据shareToken查找此次分享的参数信息,如链接时效性 有效性 授权人并校验,其次篡改RequestUri去除/share前缀和requestParam,添加用于分享用的Cookie(Token)信息
  • 注意UrlShareFilter的优先级应该配置最高优先级
生成二维码

使用hutool工具包的QrCodeUtil类创建二维码并返回给客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码 @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);
throw e;
}
}

总结

生成二维码最好不要将过期时间/授权用户信息直接加密放到requestParam参数传递,因为参数大小的不确定性将会导致二维码非常密集,相机在扫描密集二维码的效果会变得很差很差。

通过shareToken,后端交由UrlShareFilter根据shareToken获取校验过期时间/授权用户;前端可以通过shareToken参数调用接口得到分享页面所需的参数信息。并且二维码链接的长度确定,二维码的扫描性能得到了保证。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%