在完成用户模块时,需要用户上传头像,文件上传功能毕竟属于开发过程中重要的模块,也是出现漏洞较多的点,所以写起来十分小心,并记录下。
在用户注册的时候,平台会自动生成一张图片作为用户的头像,后续用户可以自己上传自己喜欢的头像。
0x01 用户头像随机生成
平台头像仿Github
初始的头像,回生成随机的头像,具体代码如下,网上应该也有源代码
1 | # -*- coding: UTF-8 -*- |
2 | __author__ = 'Joynice' |
3 | import random, math |
4 | import numpy as np |
5 | import cv2 |
6 | |
7 | class GithubAvatarGenerator: |
8 | ''' |
9 | Github default avatar is a 420*420 image contains 5*5 block vertex. |
10 | Each block is a 70*70 square. |
11 | The width of the frame around block vertex is 35px. |
12 | This is an example avatar. |
13 | https://raw.githubusercontent.com/josephzxy/pic/master/example_github_avatar.png |
14 | This class aims at generating a github-avatar-like avatars. |
15 | Usage: |
16 | - Initialize this class |
17 | - Call get_randowm_avatar() |
18 | ''' |
19 | |
20 | avatar_width = 420 # the length of line of the avatar |
21 | block_vertex_dimension = 5 # the dimension of block vertex |
22 | block_width = 70 # the length of line of block |
23 | background_color = [230, 230, 230] # the brackground color |
24 | frame_width = 35 # the width of frame surrounding block vertex |
25 | # some color that might be approiprate for the color of block. |
26 | color_pool_rgb = ( |
27 | (170, 205, 102), |
28 | (159, 255, 84), |
29 | (209, 206, 0), |
30 | (255, 255, 0), |
31 | (47, 107, 85), |
32 | (47, 255, 173), |
33 | (0, 173, 205), |
34 | (8, 101, 139), |
35 | (180, 180, 238), |
36 | (106, 106, 255), |
37 | (155, 211, 255), |
38 | (204, 50, 153), |
39 | (101, 119, 139) |
40 | ) |
41 | |
42 | def _get_avatar_vertex(self): |
43 | ''' |
44 | Generate a vertex of which each value is a boolean value. |
45 | This 5*5 vertex denotes the strcture of 5*5 block vertex in github avatar |
46 | ''' |
47 | # get 5*5 2d array full of False |
48 | avatar_vertex = np.empty((self.block_vertex_dimension, self.block_vertex_dimension), dtype=np.bool) |
49 | |
50 | for row in avatar_vertex: |
51 | for i in range(math.ceil(self.block_vertex_dimension / 2)): |
52 | row[i] = True if random.randint(0, 1) == 1 else False |
53 | # copy left half to right half |
54 | for row in avatar_vertex: |
55 | for i in range(math.floor(self.block_vertex_dimension / 2)): |
56 | row[self.block_vertex_dimension - 1 - i] = row[i] |
57 | |
58 | return avatar_vertex |
59 | |
60 | def _get_avatar_data(self): |
61 | ''' |
62 | Generate a 3d array contains color info in each pixel in the avatar |
63 | ''' |
64 | # fill the whole img with the background |
65 | avatar_data = np.zeros((self.avatar_width, self.avatar_width, 3), dtype=np.uint8) |
66 | avatar_data[:][:] = self.background_color |
67 | |
68 | rand_color_index = random.randint(0, len(self.color_pool_rgb)) |
69 | rand_color = self.color_pool_rgb[rand_color_index] |
70 | |
71 | avatar_vertex = self._get_avatar_vertex() |
72 | |
73 | # add blocks according to avatar vertex |
74 | for i in range(len(avatar_vertex)): |
75 | for j in range(len(avatar_vertex[i])): |
76 | is_True = avatar_vertex[i][j] |
77 | if is_True: |
78 | up_left_point = (self.frame_width + i * self.block_width, self.frame_width + j * self.block_width) |
79 | for k in range(self.block_width): |
80 | for l in range(self.block_width): |
81 | lvl1 = k + up_left_point[0] |
82 | lvl2 = l + up_left_point[1] |
83 | avatar_data[lvl1][lvl2] = rand_color |
84 | else: |
85 | continue |
86 | |
87 | return avatar_data |
88 | |
89 | def get_random_avatar(self): |
90 | img = self._get_avatar_data() |
91 | cv2.imshow('My pic', img) |
92 | cv2.waitKey() |
93 | |
94 | def save_avatar(self, filepath): |
95 | img = self._get_avatar_data() |
96 | cv2.imwrite(filepath, img) |
97 | |
98 | if __name__ == '__main__': |
99 | gen = GithubAvatarGenerator() #初始化类 |
100 | gen.save_avatar(filepath='../static/cms/img/user/111.png') #保存路径 |
0x02 平台头像绑定用户
有头像了,下面就要在用户注册的时候,生成随机头像绑定用户,这里在数据库中有个avatar_path
字段用来保存头像存储路径,路径要存相对路径或者使用os.path.dirname(os.path.abspath(__file__))
获取项目路径,然后进行路径拼接。头像命名的话,我这里使用用户邮箱进行命名。
1 |
|
2 |
|
3 |
|
4 | def create_cms_user(username, password, email): |
5 | avatar = GithubAvatarGenerator() #chu初始化类 |
6 | path = '../static/cms/img/user/'+ email +'.png' |
7 | avatar.save_avatar(filepath='./static/cms/img/user/'+ email +'.png') #头像保存本地 |
8 | user = User(username=username, password=password, email=email, avatar_path=path) #创建用户 |
9 | db.session.add(user) #添加 |
10 | db.session.commit() #提交 |
11 | print('用户添加成功') |
0x03 用户修改头像
用户修改头像的话,有几个注意点:
- 用户头像图片类型(png、jpg等常规图片类型检查)
- 用户上传头像大小限制
- 用户上传完图片后,重命名保存(防止图片马)
前端图片上传的话使用过两个插件,fileinput和Layui中的图片上传功能,其基本实现都差不多,fileinput
这个插件好久没用了,这个插件基于bootstrap
,需要导入相关依赖,所有这里用Layui
做演示,支持国产,但不得不说“坑”有点多。
实现如图的功能,具体弹窗、表单就不说了,主要记录图片上传功能,这个功能主要是给每个题目上传一个背景图,同时具有修改背景图的功能。非常类似用户修改头像。
- html
html代码中用到Layui
的form
,代码如下:
1 | <form class="layui-form" action="" lay-filter="example2" id="upload_img" style="display: none"> |
2 | <div class="layui-form-item"> |
3 | <label class="layui-form-label">题目ID</label> |
4 | <div class="layui-input-inline"> |
5 | <input type="number" name="id" lay-verify="required" autocomplete="off" placeholder="请输入题目ID" class="layui-input" disabled> |
6 | </div> |
7 | </div> |
8 | <div class="layui-form-item"> |
9 | <label class="layui-form-label">上传图片</label> |
10 | <div class="layui-input-inline uploadHeadImage"> |
11 | <div class="layui-upload-drag" id="headImg"> |
12 | <i class="layui-icon"></i> |
13 | <p>点击上传图片,或将图片拖拽到此处</p> |
14 | </div> |
15 | </div> |
16 | <div class="layui-input-inline"> |
17 | <div class="layui-upload-list"> |
18 | <img class="layui-upload-img headImage" id="demo1" width="200" height="150"> |
19 | <p id="demoText"></p> |
20 | </div> |
21 | </div> |
22 | </div> |
23 | </form> |
html中在第二个layui-form-item
设置图片上传,同时form样式设置display:none
配合Layui
的弹出层使用。
- js
js代码中实现文件上传,Layui
图片上传插件自带文件格式判断、大小限制以及预览功能。
1 | var uploadInst = upload.render({ |
2 | elem: '#headImg' //绑定元素 |
3 | , url: 'xxxx' //上传接口 |
4 | , method: 'post' //方法 |
5 | , headers: {'X-CSRF-TOKEN': token} //设置csrf-token |
6 | , size: 1024 * 10 //前端限制文件大小,这里为10m |
7 | , accept: 'images' //接受文件类型,这里为图片 |
8 | , data: { |
9 | 'id': data.id, |
10 | } //同时传递题目ID |
11 | , before: function (obj) { |
12 | //预读本地文件示例,不支持ie8 |
13 | obj.preview(function (index, file, result) { |
14 | $('#demo1').attr('src', result); //图片链接(base64) |
15 | }); |
16 | } |
17 | , done: function (res) { |
18 | //上传完毕回调 |
19 | if (res.code === 0) { |
20 | table.reload('LAY-app-content-list'); //表格重载 |
21 | layer.msg(res.message); |
22 | layer.closeAll('page'); //关闭弹窗 |
23 | } else { |
24 | return layer.msg(res.message); |
25 | } |
26 | } |
27 | , error: function () { |
28 | //请求异常回调 |
29 | var demoText = $('#demoText'); |
30 | demoText.html('<span style="color: #FF5722;">上传失败</span> <a class="layui-btn layui-btn-mini demo-reload">重试</a>'); |
31 | demoText.find('.demo-reload').on('click', function () { |
32 | uploadInst.upload(); |
33 | }); |
34 | } |
35 | }); |
虽然使用这个插件非常方便,但是实现这个功能还是遇到很多的坑,这里记录我解决的一个坑
上传第一个图片正常,但是上传第二个文件的时候无反应
原因: 在上传完,第一个图片后,上传html代码中会绑定一些元素,如果页面没有刷新的话,在进行下面的上传,会出现无反应的现象。
解决办法:在弹窗层销毁后,将元素重置。
1 | end: function () { |
2 | $('.uploadHeadImage').empty().append(" <div class=\"layui-upload-drag\" id=\"headImg\">\n" + |
3 | " <i class=\"layui-icon\"></i>\n" + |
4 | " <p>点击上传图片,或将图片拖拽到此处</p>\n" + |
5 | " </div>"); |
6 | } |
- 后端
后端的话,没有添加文件大小检测,其他都实现了
1 | def post(self): |
2 | file = request.files['file'] #获取文件 |
3 | id = request.form.get('id') #获取id |
4 | if not id: |
5 | return field.params_error(message='参数缺失') |
6 | if file and allowed_file(file.filename, ALLOWED_EXTENSIONS=config.ALLOWED_PIC_EXTENSIONS): #文件类型判断 |
7 | filename = secure_filename(file.filename) |
8 | new_name = tools.rename(filename) #重命名 |
9 | new_path = os.path.join(config.UPLOAD_PIC_PATH, tools.rename(filename)) |
10 | file.save(new_path) #保存文件 |
11 | vul = Vuls.query.get(id) |
12 | if not vul: |
13 | return field.params_error(message='题目不存在') |
14 | old_path = os.path.join(config.UPLOAD_PIC_PATH, vul.img) |
15 | if vul.img: |
16 | if os.path.exists(old_path): |
17 | os.remove(old_path) #删除上一个头像 |
18 | vul.img = new_name |
19 | db.session.commit() |
20 | return field.layui_success(message='上传成功') |
21 | return field.params_error(message='文件类型错误') |
此片文章记录了Flask实现文件上传的全过程,以供以后学习查看。