概述
授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。
这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。
需求背景
虽然,我们项目客户大部分为pc端操作,当客户要把手机拍中照片,要上传至系统中,如果APP没有提供专门的接口,客户需要先把照片传到电脑中,然后通过网页上传,这样比较麻烦,于是,产品便提出需要开发扫码上传附件的功能。
流程图
注册服务
上传附件
功能设计
要实现扫码上传附件的功能,
前端功能
- 请求扫描上传附件的申请接口,返回上传地址,生产二维码
- 首先用户扫描二维码后,跳转到网页,点击上传吊起手机选择相册或者拍照的功能,然后选择上传附件,最后再提交
后端功能
- 业务发起申请,后端接受到请求后,注册当前申请的信息,然后获取到一个上传地址
- 用户提交功能,需要根据请求类型,找到相应业务实现类,将之前申请业务的id和当前附件id都交给实现类
从这个几个流程,可以看出几个要点,授权机制、业务处理
授权机制
二维码生成
关于授权机制,我模仿了Oauth2 的授权码的模式。首先由业务发送申请,后端根据请求的生成一个临时的UUID,以UUID为key,请求中的类型、业务的id,保存至Redis缓存,时效性大概30分钟左右,将上传附件的地址和UUID合并,返回给接口,后面业务又提出一个静态二维码的问题,即保存在文件中的二维码,于是又增加一个注册静态二维码的接口,这里只是把生成的UUID,保存到库
源码
/**
上传附处理类这个类即接受参数,也做为返回参数,严格来说,需要分类两个类,一个是参数类,一个结果类
*/
@Data
public class FileByQRCodeDTO implements Serializable {
/**
* 记录的id
*/
private String recordId;
/**
* 类型
*/
private String type;
/**
* 文件的id
*/
private String files;
/**
* 用户的token
*/
private String token;
/**
* 上传限制
*/
private Integer maxCount;
/**
* 临时的用户凭证
*/
private String temporaryUuid;
}
/**
* ITjFileInfoService:: getUploadFileQRCodeRedirectUrl
* <p>TO:获取二维码跳转的地址
* <p>HISTORY: 2021/1/18 liuha : Created.
* @param fileByQRCodeDTO 请求参数
* @return String 上传的地址
*/
public String getUploadFileQRCodeRedirectUrl(FileByQRCodeDTO fileByQRCodeDTO) {
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
String callbackUrl = "";
try {
callbackUrl = URLEncoder.encode(uploadFileQRCode,"utf8");
callbackUrl=String.format(uploadFileQRCode,uuid);
//保存至redis
redisUtil.set(uuid,fileByQRCodeDTO,3600);
} catch (UnsupportedEncodingException e) {
throw new JeecgBootException("生成上传文件的地址失败");
}
return callbackUrl;
}
/**
* ITjFileInfoService::
* <p>TO:获取静态二维码的上传地址
* <p>HISTORY: 2021/1/30 liuha : Created.
*
* @param fileByQRCodeDTO 请求参数
* @return String 静态二维码的地址
*/
@Override
public String getStaticQRCodeRedirectUrl(FileByQRCodeDTO fileByQRCodeDTO) {
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
String callbackUrl = "";
try {
//uploadFileQRCode 跳转到上传附件界面的地址 比如 //http://localhost:8081/uploadqrcodefile/
callbackUrl = URLEncoder.encode(uploadFileQRCode,"utf8");
callbackUrl=String.format(uploadFileQRCode,uuid);
//将关联信息存入至关联表中
TjFileQrcodeLink fileQrcodeLink =new TjFileQrcodeLink();
fileQrcodeLink.setRecordId(fileByQRCodeDTO.getRecordId());
fileQrcodeLink.setType(fileByQRCodeDTO.getType());
fileQrcodeLink.setUuid(uuid);
fileQrcodeLinkService.save(fileQrcodeLink);
} catch (UnsupportedEncodingException e) {
throw new JeecgBootException("生成上传文件的地址失败");
}
return callbackUrl;
}
业务处理
这里处理主要是后端如何把上传后的附件id给相应的业务实现类,我在这块这里设计时,采用了策略模式,即,我把处理接口定义好,由业务实现接口,我根据接口定义的type,找到相应的实现类bean对象,把附件id和业务id交给业务处理,这个在我之前写的文章提过类似的实现方式,项目重构:设计模式(策略模式)
源码
//上传接口定义
public interface IQRCodeUpload {
/**
* IQRCodeUpload:: getType
* <p>TO:获取实现类的类型
* <p>HISTORY: 2021/1/18 liuha : Created.
* @return String
*/
String getType();
/**
* IQRCodeUpload:: handleUploadFile
* <p>TO:上传附件的后处理接口
* <p>HISTORY: 2021/1/18 liuha : Created.
* @param recordId 记录id
* @param files 附件的id,以逗号隔开的方式传递
*/
void handleUploadFile(String recordId,String files);
}
//将实现类的在初始化spring完成后,加载到map中
@Component
public class UploadQRCodeFactory implements InitializingBean, ApplicationContextAware {
private ApplicationContext appContext;
public static final
Map<String, IQRCodeUpload> UPLOAD_IMPL_MAP = new HashMap<>(16);
/**
* UploadContextConfig:: getHandler
* <p>TO:通过type获取IQRCodeUpload的实现类
* <p>HISTORY: 2021/1/19 liuha : Created.
*
* @param type 实现类的的类型
* @return IQRCodeUpload 实现类
*/
public IQRCodeUpload getHandler(String type) {
return UPLOAD_IMPL_MAP.get(type);
}
@Override
public void afterPropertiesSet() {
appContext.getBeansOfType(IQRCodeUpload.class)
.values()
.forEach(handler -> UPLOAD_IMPL_MAP.put(handler.getType(), handler));
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
appContext = applicationContext;
}
}
调用业务实现类的接口
/**
* ITjFileInfoService::submitFileByQRCode
* <p>TO:上传二维码上传文件
* <p>HISTORY: 2021/1/19 liuha : Created.
* @param fileByQRCodeDTO 需要上传的文件
*/
public void submitFileByQRCode(FileByQRCodeDTO fileByQRCodeDTO) {
String type = fileByQRCodeDTO.getType();
if(!StringUtils.isEmpty(type)){
//获取业务的实现类
IQRCodeUpload handler = handlerFactory.getHandler(type);
if (handler == null) {
throw new JeecgBootException("未找到提交处理的实现类");
}
handler.handleUploadFile(fileByQRCodeDTO.getRecordId(),fileByQRCodeDTO.getFiles());
}
}
获取授权TOEKN处理
这里主要为了处理上传附件的接口需要token的认证,一开始,考虑到如果把上传附件的接口去掉token的限制,但是会带来一个问题,如果接口被泄露,会被恶意利用上传附件,所以还是要考虑处理授权的问题,如果是动态的二维码其实比较好处理,就是在缓存中保存当时申请的用户的token,请求时,返回给前端即可,但是静态的二维码,缓存中保存当时申请的用户的token的方式,则失去了时效性。
于是对框架做了部分的调整
1.首先通过uuid授权时候,先查询动态库是否含有当前uuid,对应的id,不存在的时则去查询静态的注册库,查找到信息后,生成一个临时token
2.在验证token时,当发现前缀是uploadFile,则特殊处理
@ApiOperation(value = "通过uuid获取二维码的信息", notes = "通过uuid获取二维码的信息")
@GetMapping(value = "/getqrcodeinfo")
public Result<?> getQRCodeInfo(@RequestParam(name = "uuid", required = true)
String uuid) {
uuid= StringUtils.lowerCase(uuid);
FileByQRCodeDTO fileByQRCodeDTO= (FileByQRCodeDTO) redisUtil.get(uuid);
//如果为空,查询静态库
if (fileByQRCodeDTO == null) {
LambdaQueryWrapper<TjFileQrcodeLink> query= new LambdaQueryWrapper();
query.eq(TjFileQrcodeLink::getUuid,uuid);
TjFileQrcodeLink fileQrcodeLink = fileQrcodeLinkService.getOne(query);
if (fileQrcodeLink != null) {
fileByQRCodeDTO =new FileByQRCodeDTO();
fileByQRCodeDTO.setRecordId(fileQrcodeLink.getRecordId());
fileByQRCodeDTO.setType(fileQrcodeLink.getType());
//生成一个临时的用户密码
String temporaryUuid = UUID.randomUUID().toString().replaceAll("-", "");
//生成临时token
String token =JwtUtil.sign("uploadFile"+uuid,temporaryUuid);
fileByQRCodeDTO.setToken(token);
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token);
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME*2 / 1000);
fileByQRCodeDTO.setMaxCount(fileQrcodeLink.getMaxCount());
fileByQRCodeDTO.setTemporaryUuid(temporaryUuid);
redisUtil.set(uuid,fileByQRCodeDTO,3600);
}
}
return Result.ok(fileByQRCodeDTO);
}
修改ShiroRealm验证的地方
}else if(username.startsWith("uploadFile")){
String[] uploadFiles = username.split("uploadFile");
FileByQRCodeDTO fileByQRCodeDTO= (FileByQRCodeDTO) redisUtil.get(uploadFiles[1]);
loginUser.setUsername(username);
loginUser.setPassword(fileByQRCodeDTO.getTemporaryUuid());
loginUser.setStatus(1);
}
这块地方使用红色提醒,主要不建议使用这段代码,由于静态二维码是后面提出来的,一开始设计没考虑到二维码的类型不同,请求不同授权接口,所以我对这块代码,很不喜欢,一、代码不规范写法;二、接口功能糅合了两个处理方式,可读性差
前端界面
前端,我采用了有赞的vant-vue框架,贴上核心上传界面的代码,
整个前端的界面交互都在整个页面上,获取授权、上传附件、提交附件
UploadFile.vue
<template>
<div style="height: 100%">
<div v-show="uploadDiv">
<van-grid :column-num="3" :gutter="20">
<van-uploader preview-size="120" :max-count="maxCount" v-model="fileList" multiple :after-read="afterRead" />
</van-grid>
<van-tabbar>
<van-button size="large" type="primary" @click="submitFile()">提交</van-button>
</van-tabbar>
</div>
<div v-show="success" class="success-div">
<div>
<van-icon name="success" size="5em" color="#00aa00" />
<div>提交成功</div>
</div>
</div>
<div v-show="fail" class="success-div">
<div>
<van-icon name="fail" size="5em" color="#ff0000" />
<div>操作失败</div>
</div>
</div>
<van-overlay :show="show" />
</div>
</template>
<script>
import {
setToken
} from '../api/auth.js'
import {
Toast
} from 'vant';
export default {
name: 'app',
components: {
setToken,
},
data() {
return {
fileList: [],
recordId: "",
type: "",
token: "",
show: false,
uploadDiv: true,
success: false,
fail: false,
maxCount: 9
};
},
mounted() { //页面初始化方法
this.getUrlParameter();
},
methods: {
submitFile() {
var fileList = this.fileList;
console.log(fileList);
var files = [];
fileList.forEach(function(item) {
if (item.fileId) {
files.push(item.fileId);
}
})
if (files.length == 0) {
this.$notify('未上传附件');
return;
}
var data = {
recordId: this.recordId,
type: this.type,
files: files.join(",")
};
this.$toast.loading({
message: '提交中...',
forbidClick: true,
});
this.$api.post('/file/tjFileInfo/submitfilebyqrcode', data,
response => {
this.$toast.clear();
if (response.status == 200) {
if (response.data.code == 200) {
this.$toast.success('提交成功');
this.uploadDiv = false
this.success = true
} else {
this.$toast.fail('提交失败');
this.uploadDiv = false
this.fail = true
}
} else {
this.$notify('提交失败');
this.uploadDiv = false
this.fail = true
}
});
},
getUrlParameter() {
var url = window.location.href;
var dz_url = url.split('#')[0];
var cs = dz_url.split('?')[1];
var cs_arr = cs.split('&');
var cs = {};
for (var i = 0; i < cs_arr.length; i++) { //遍历数组,拿到json对象
cs[cs_arr[i].split('=')[0]] = cs_arr[i].split('=')[1]
}
var that = this;
this.$api.get('/file/tjFileInfo/getqrcodeinfo', {
uuid: cs.uuid
},
response => {
if (response.status == 200) {
if (response.data.code == 200) {
if (response.data.result) {
that.recordId = response.data.result.recordId;
that.type = response.data.result.type;
that.token = response.data.result.token;
if (response.data.result.maxCount > 0) {
that.maxCount = response.data.result.maxCount;
}
setToken(that.token);
} else {
this.$notify('连接已失效,请重新获取二维码');
that.show = true;
}
} else {
this.$notify('获取用户信息失败');
that.show = true;
}
} else {
this.$notify('获取用户信息失败');
that.show = true;
}
});
},
uploadFile(file) {
let formData = new FormData()
//上传文件到上传至服务器
formData.append('file', file.file)
file.status = 'uploading';
file.message = '上传中...';
this.$api.post('/sys/commonBase/upload', formData,
response => {
if (response.status == 200) {
if (response.data.code == 0) {
file.status = 'done';
file.fileId = response.data.result;
} else {
file.status = 'failed';
file.message = '上传失败...';
}
} else {
file.status = 'failed';
file.message = '上传失败...';
}
});
},
afterRead(files) {
var that = this;
if (files.length) {
files.forEach(function(file) {
that.uploadFile(file);
})
} else {
that.uploadFile(files);
}
},
},
};
</script>
<style>
.success-div {
height: 100%;
display: flex;
display: -webkit-flex;
align-items: center;
justify-content: center;
}
</style>
总结
扫描上传附件的功能,从开始到整个模块开发完,尤其当前后端都是我一个独立完成时,还是有很大的成就感,整个功能还算比较独立,由于策略模式的加入,也变得更加灵活,耦合性降低,有了小程序开发的经验,所以前端的界面开发上手比较快、后端,模仿了授权码的机制。但是有设计上的不足,尤其静态和动态二维码的获取授权,还是要多练习、多思考。
最后
以上就是可靠糖豆为你收集整理的功能设计:如何实现一个扫码上传附件的功能的全部内容,希望文章能够帮你解决功能设计:如何实现一个扫码上传附件的功能所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复