# milkbox **Repository Path**: guo-yongchen/milkbox ## Basic Information - **Project Name**: milkbox - **Description**: 牛奶盒工具箱,自己做的网站,目前想要坚持维护。 - **Primary Language**: Java - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: https://www.milkbox.top - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-10-23 - **Last Updated**: 2023-05-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: Java, 个人网站 ## README # 注意 由于`application.yml`文件中有些包含密码等涉密内容,若果自己使用请自己去各个技术申请id和密码等。需要在程序包的外面自己加一个配置文件 # session域中的键含义 | 键 | 值类型 | 销毁时期 | 含义 | | :------------------------ | :----- | :--------- | :----------------------------------------------------------- | | `pictureVerificationCode` | String | 验证后销毁 | 图片验证码 | | `emailVerificationCode` | String | 验证后销毁 | 邮箱验证码 | | `userEmail` | String | 30分钟 | 用户邮箱,**防止用户在获取完邮箱验证码之后再次修改了邮箱然后点登录** | # 用户角色 | 角色名 | 角色主要目的 | | | -------- | ------------------ | ---- | | 管理员 | 管理用户,发布公告 | | | 游客 | 基本使用 | | | 普通用户 | | | | 维修用户 | | | 管理员>维修用户>普通用户>游客 # 接口后缀的含义 | 后缀名 | 含义 | | -------- | -------------------- | | `.admin` | 管理员才能进行的操作 | # token的设计 ![image-20221011161426818](image/image-20221011161426818.png) 最后一次活跃时间是由`/users/getUser`接口更新的,`serUserToRedis`也会更新活跃时间 # 公告系统 公告以JSON形式存在与redis中,优先从redis中查询 管理员可以手动更新缓存,也可以添加一条公告,且只有管理员才能进入公告管理页面 # 接口 ## UserController ### `/users/emailLogin`邮箱登录或注册 请求方式:`POST` **session必须**,token被写入响应对象 请求体参数 ```json { "code": "邮箱验证码", "email": "邮箱地址" } ``` ### `/users/getUser`获取session用户信息 请求方式:`GET` **token必须** 被拦截器校验`LoginInterceptor` ### `/users/logoutCuttent`当前设备退出登录 请求方式:`GET` **token必须** 被拦截器校验`LoginInterceptor` ### `/users/logoutAll`所有设备退出登录 请求方式:`GET` **token必须** 被拦截器校验`LoginInterceptor` ### `/users/resetUserAvatar`重置用户头像 请求方式:`GET` **token必须** 被拦截器校验`LoginInterceptor` ### `/users/uploadAvatar`上传用户头像 请求方式:`POST` **token必须** 被拦截器校验`LoginInterceptor` 头像上传后的文件名:`avatar_用户唯一标识_当前时间毫秒值.原始文件扩展名` 头像上传的位置不确定,要在配置文件中进行修改。新的头像上传后,旧的头像会被移动到文件夹`abandon`中做备份,如果用户的头像是默认头像,则不进行移动。 ### `/users/changeNickname`修改用户昵称 请求方式:`PUT` **token必须** 被拦截器校验`LoginInterceptor` 请求体参数: ```json { "userNickname": "昵称" } ``` ## PictureVerificationCodeController ### `/pictureVC/picture`生成图片验证码 请求方式:`GET` **session必须** 返回一张图片 ## EmailVerificationCodeController ### `/emailVC/send`发送邮件 请求方式:`POST` **session必须** 请求体参数 ```json { "code": "图片验证码", "email": "邮箱地址" } ``` ## VariableNamerController变量名 ### `/vnc/getAll`所有变量名 将中文翻译为英文并获取这个英文的所有变量名方式 请求方式:`GET` 参数:`source=原文` ## NoticeController公告 ### `/notices/getAll`获取所有公告 获取所有公告并根据时间新到旧排序 请求方式:`GET` ### `/notices/updateRedis.admin`立即更新缓存 立即更新公告缓存 **token必须且必须是管理员** 被拦截器校验:`LoginInterceptor`和`AdminInterceptor` ### `/noitces/add.admin`添加公告 添加一条公告并更新缓存 **token必须且必须是管理员** 被拦截器校验:`LoginInterceptor`和`AdminInterceptor` 请求体参数 ```json { "title": "标题", "content": "内容" } ``` ## PostController帖子 ### `/posts/add`上传帖子 请求方式`post` 被拦截器校验:`LoginInterceptor` 请求参数(文件类型的请求) ```java @CookieValue("token") String token, @RequestParam(required = false) MultipartFile[] fileList, @RequestParam(required = false) String content ``` ### `/posts/page`分页查询帖子 请求方式`get` 需要传输token,但是不做强制要求 请求参数 ``` ?pageNum=第几页&pageSize=每一页多少条?isCollectionLimit=null表示不添加此条件,1表示查询收藏的记录,0表示查询为收藏的记录 ``` 返回的包括帖子的信息以及发帖着的简略信息,还有有关分页的所有数据 ### `/posts/getOneAll`获帖子的详细信息 获取某一个帖子的详细信息 请求方式`get` 需要传输token,但是不做强制要求 路径参数 `localhost:88/posts/getOneAll/{postId}` ## PostCommentController帖子的评论 ### `/postComments/add`添加评论 请求方式`post` 被拦截器校验:`LoginInterceptor` 请求体 ```json { postId: "18", content: "评论的内容" } ``` ## PostLikeController帖子点赞 ### `/postLikes/likeOrCancel.login`点赞或取消点赞 请求方式`get` 被拦截器校验:`LoginInterceptor` url参数`?postId=被赞的帖子的id` ## PostCollectionController帖子收藏 ### `/postCollections/collection.login`收藏 请求方式`post` 被拦截器校验:`LoginInterceptor` 请求体 ```json { "id": 被收藏的帖子id } ``` ### `/postCollections/cancelCollection.login`取消收藏 请求方式`delete` 被拦截器校验:`LoginInterceptor` url参数`?postId=被取消收藏的帖子id` # 用户头像存放地址规则 数据库表字段`user_avatar`存放的是用户头像的文件名,用户头像的地址存放在虚拟字段`user_avatar_url`中,由代码将地址拼接起来。 用户的头像存放在一个单独的tomcat服务器中,使用磁盘路径存放,使用网络路径读取 ```yml # 用户头像地址 # 使用的是网络地址,而非磁盘地址 # 在正式上线的时候要把localhost改为对应的服务器主机名或ip地址 # 这些资源是运行在一个单独的tomcat服务器上的 # 数据库中只存储图片的文件名,前端获取图片url的时候,将common-address与数据库中的文件名拼接起来传递 user-avatar: # 用户头像存放的地址 # 如果使用https的话注意在这里修改 common-address: "http://localhost/milkboxResources/pictures/avatars/" # 用户的默认头像位置 default-name: "default.png" # 头像存放的磁盘路径 # 正式上线的时候要改为linux的地址"/XXX/tomcat9ResourcesServer/webapps/milkboxResources/pictures/avatars" disk-address: "G:/my_tomcat/tomcat9ResourcesServer/webapps/milkboxResources/pictures/avatars" ``` # 拦截器链 ![image-20220923171101441](image/image-20220923171101441.png) # 图片类型 图片的类型并不是由扩展名决定的,而是由文件内容中的开头决定的,所以直接改文件扩展名同样可以打开图片,但并不是真正的修改了文件的类型 > JPEG (jpg),文件头:`FF D8 FF E0 00 10 4A 46 49 46` > PNG (png),文件头:`89 50 4E 47 0D 0A 1A 0A` > GIF (gif),文件头:`47 49 46` 在java中`MultipartFile`类用来获取前端传递的文件。在以前判断文件的类型是使用`getOriginalFilename()`方法获取文件名,然后使用`split(".")`获取文件的扩展名,但是这样并不适用于图片,因为图片的类型不是扩展名决定的。有时候上传了一张没有扩展名的图片,系统依然可以正常使用,并不会显示图片错误。这样如果用户把原本图片类型为png的图片改扩展名为jpg然后上传给服务器,服务器就会误以为这是jpg图片,**不便于服务器处理**。如果用户将带有病毒的文件修改扩展名的png,就可以骗过服务器从而保存病毒文件,这样就会**对服务器构成威胁**。关键的是*百度上全是这种判断扩展名的方式*。 于是我找到了新的方式。使用`getContentType()`获取文件的**MIME**类型,通过对MIME类型进行判断,看是不是图片文件。图片的MIME类型是以`image`开头中间用`/`分割后面是图片的类型。通过`split("/")[0]`判断是否为图片。 | **image/gif** | gif | GIF 图像格式 | | ----------------- | --------- | ----------------------------------------- | | **image/jpeg** | jpg, jpeg | JPG(JPEG) 图像格式 | | **image/jp2** | jpg2 | JPG2 图像格式 | | **image/png** | png | PNG 图像格式 | | **image/tiff** | tif, tiff | TIF(TIFF) 图像格式 | | **image/bmp** | bmp | BMP 图像格式(位图格式) | | **image/svg+xml** | svg, svgz | SVG 图像格式 | | **image/webp** | webp | WebP 图像格式 | | **image/x-icon** | ico | ico 图像格式,通常用于浏览器 Favicon 图标 | 举例如下: ```java public Boolean saveUserAvatar(String token, MultipartFile avatar) { // ...... String contentType = avatar.getContentType(); log.debug("头像文件类型:" + contentType); // 判断是否为image类型,这里不会发生空指针异常,因为在controller中空值是传不进来的会被异常捕获 if (!"image".equals(contentType.split("/")[0])) { log.error("头像文件类型不支持"); return false; } // 文件扩展名 String avatarFileExtension = "." + contentType.split("/")[1]; // 生成新的文件名 String newAvatarFileName = "avatar_" + user.getUserId() + "_" + new Date().getTime() + avatarFileExtension; // ...... } ``` # 安装部署 ## redis 安装目录:`/usr/local/redis` 启动方式:在`/usr/local/redis`目录下运行`./startupRedisServer.sh`启动服务端,运行`./startupRedisClent.sh`启动客户端 ![image-20220926152034757](image/image-20220926152034757.png) > 阿里云ecs > > 配置文件位置`/etc/redis.conf` > > ```bash > 启动 > service redis start > 重启 > service redis restart > 停止 > service redis stop > 登录 > redis-cli > ``` > > redis默认开机自启 当前redis不支持远程访问 redis的密码为`????` redis开机不自启