111
社区成员




是许多移动端应用中的常见功能,比如图片上传、文件分享等。在我们的宠物APP中,用户可以上传宠物照片以及自己的头像,也可以自己更换app的背景。这项技术的难点在于网络请求的异步处理、传输数据的准确性和处理文件权限问题。文件的下载,其实有url就可以对我们的服务器中的图片或者视频资源进行展示了,所以url的生成和获取也是十分重要的
JWT(JSON Web Token)用于无状态的用户身份认证,在分布式系统或微服务架构中尤为重要。对于宠物APP的开发,我们从很多地方都需要进行身份认证,从用户登录的信息确认到获取到用户对应的信息和宠物信息,都是需要身份认证。它能有效避免传统会话认证的弊端。学习JWT是为了提高系统安全性,支持扩展。难点在于签名加密、Token过期策略和避免Replay攻击。
这个接口的功能是允许用户上传一个头像文件,并将其保存到云存储中。下面是这个接口的详细分析:
请求部分:
使用了 @PostMapping 注解,这意味着该接口是一个处理 POST 请求的方法,路径是 /avatar/upload
。
接口需要传入Authorization
头部,用于身份验证,同时传入一个文件 data,这是用户上传的头像。
//更新头像
@PostMapping("/avatar/upload")
public Result upload(@RequestHeader(name="Authorization") String token,@RequestParam("data")MultipartFile file) throws IOException {
文件验证:
首先检查上传文件的扩展名是否属于允许的图片类型,如 jpg、jpeg、png、gif 等。如果文件扩展名不符合要求,返回错误信息。
接着,通过 file.getContentType()
获取文件的 MIME
类型,确保文件是图片类型。如果不是图片类型,返回错误信息。
String originalFilename=file.getOriginalFilename();
// 判断文件是否为空
if (originalFilename == null || originalFilename.isEmpty()) {
return Result.error("上传文件为空");
}
// 获取文件扩展名
String extension = FilenameUtils.getExtension(originalFilename).toLowerCase();
// 定义允许的图片扩展名
Set<String> allowedExtensions = Set.of("jpg", "jpeg", "png", "gif", "bmp", "webp");
// 检查文件类型是否合法
if (!allowedExtensions.contains(extension)) {
return Result.error("只允许上传图片格式的文件(jpg, jpeg, png, gif, bmp, webp)");
}
String mimeType = file.getContentType();
if (mimeType == null || !mimeType.startsWith("image")) {
return Result.error("上传文件必须是图片");
}
文件命名和上传:
通过 UUID.randomUUID().toString()
生成一个唯一的文件名,确保每次上传的文件名不重复。
调用 HuaweiObsUtil.uploadFile(filename, file.getInputStream())
上传文件到华为云 OBS(对象存储服务),并获取上传后文件的 URL。
//保证名字唯一
String filename= UUID.randomUUID().toString()+originalFilename.substring(originalFilename.lastIndexOf("."));
String url=HuaweiObsUtil.uploadFile(filename,file.getInputStream());
用户信息更新:
解析Authorization
头部的 JWT token
,获取当前登录用户的用户名。
根据用户名查询数据库,找到对应的用户并更新其头像 URL。
返回:
返回更新后的用户信息(包括新的头像 URL)给前端。
Map<String,Object> map =JwtUtil.parseToken(token);
String username=(String) map.get("username");
User user=userService.findByUserName(username);
user.setAvatar(url);
userService.updateUserInfo(user);
return Result.success(user);
}
整体代码:
//更新头像
@PostMapping("/avatar/upload")
public Result upload(@RequestHeader(name="Authorization") String token,@RequestParam("data")MultipartFile file) throws IOException {
log.info("用户更新头像");
String originalFilename=file.getOriginalFilename();
// 判断文件是否为空
if (originalFilename == null || originalFilename.isEmpty()) {
return Result.error("上传文件为空");
}
// 获取文件扩展名
String extension = FilenameUtils.getExtension(originalFilename).toLowerCase();
// 定义允许的图片扩展名
Set<String> allowedExtensions = Set.of("jpg", "jpeg", "png", "gif", "bmp", "webp");
// 检查文件类型是否合法
if (!allowedExtensions.contains(extension)) {
return Result.error("只允许上传图片格式的文件(jpg, jpeg, png, gif, bmp, webp)");
}
String mimeType = file.getContentType();
if (mimeType == null || !mimeType.startsWith("image")) {
return Result.error("上传文件必须是图片");
}
//保证名字唯一
String filename= UUID.randomUUID().toString()+originalFilename.substring(originalFilename.lastIndexOf("."));
String url=HuaweiObsUtil.uploadFile(filename,file.getInputStream());
//////至此用户图片已经检验上传
Map<String,Object> map =JwtUtil.parseToken(token);
String username=(String) map.get("username");
User user=userService.findByUserName(username);
user.setAvatar(url);
userService.updateUserInfo(user);
return Result.success(user);
}
//更新宠物头像
@PostMapping("/{id}/avatar/upload")
public Result upload(@RequestHeader(name="Authorization") String token,@RequestParam("data") MultipartFile file,@PathVariable int id) throws IOException {
log.info("宠物更新头像");
String originalFilename=file.getOriginalFilename();
// 判断文件是否为空
if (originalFilename == null || originalFilename.isEmpty()) {
return Result.error("上传文件为空");
}
// 获取文件扩展名
String extension = FilenameUtils.getExtension(originalFilename).toLowerCase();
// 定义允许的图片扩展名
Set<String> allowedExtensions = Set.of("jpg", "jpeg", "png", "gif", "bmp", "webp");
// 检查文件类型是否合法
if (!allowedExtensions.contains(extension)) {
return Result.error("只允许上传图片格式的文件(jpg, jpeg, png, gif, bmp, webp)");
}
String mimeType = file.getContentType();
if (mimeType == null || !mimeType.startsWith("image")) {
return Result.error("上传文件必须是图片");
}
//保证名字唯一
String filename= UUID.randomUUID().toString()+originalFilename.substring(originalFilename.lastIndexOf("."));
String url= HuaweiObsUtil.uploadFile(filename,file.getInputStream());
//////至此用户图片已经检验上传
Map<String,Object> map =JwtUtil.parseToken(token);
int userId=(int) map.get("id");
Pet pet =petService.getPetById(id);
pet.setAvatar(url);
petService.updatePet(userId,id,pet);
return Result.success(pet);
}
此接口功能与用户更新头像接口类似,唯一的不同是它针对的是宠物头像。详细流程如下:
请求部分:
使用 @PostMapping("/{id}/avatar/upload")
,id 为宠物的 ID,表示上传特定宠物的头像。
需要传入 Authorization 头部用于验证身份,并上传文件 data。
文件验证与上传:
与用户头像上传接口相同,进行文件类型和 MIME
类型的验证。
为了确保文件名唯一,使用 UUID
来生成文件名,并将文件上传到华为云 OBS。
宠物信息更新:
解析 Authorization token
,获取当前用户的 ID。
根据传入的宠物 ID 查找宠物对象,并更新其头像 URL。
返回:
// 上传文章图片
@PostMapping("/{id}/addPicture")
public Result addPicture(@RequestHeader(name="Authorization") String token, @PathVariable int id,
@RequestParam("data") MultipartFile file) throws IOException {
log.info("上传文章图片: {}", id);
String originalFilename=file.getOriginalFilename();
// 判断文件是否为空
if (originalFilename == null || originalFilename.isEmpty()) {
return Result.error("上传文件为空");
}
// 获取文件扩展名
String extension = FilenameUtils.getExtension(originalFilename).toLowerCase();
// 定义允许的图片扩展名
Set<String> allowedExtensions = Set.of("jpg", "jpeg", "png", "gif", "bmp", "webp");
// 检查文件类型是否合法
if (!allowedExtensions.contains(extension)) {
return Result.error("只允许上传图片格式的文件(jpg, jpeg, png, gif, bmp, webp)");
}
String mimeType = file.getContentType();
if (mimeType == null || !mimeType.startsWith("image")) {
return Result.error("上传文件必须是图片");
}
//保证名字唯一
String filename= UUID.randomUUID().toString()+originalFilename.substring(originalFilename.lastIndexOf("."));
String url=HuaweiObsUtil.uploadFile(filename,file.getInputStream());
//////至此图片已经检验上传
articleService.uploadPicture(id,url);
return Result.success();
}
该接口允许用户上传与文章相关的图片。
请求部分:
使用 @PostMapping("/{id}/addPicture")
,id 为文章的 ID,表示上传与特定文章相关的图片。
请求需要传入 Authorization
头部和图片文件。
文件验证与上传:
与前两个接口类似,验证文件类型和 MIME
类型,并生成唯一的文件名。
上传文件到华为云 OBS 并获取文件 URL。
文章更新:
调用 articleService.uploadPicture(id, url)
,将图片 URL 保存到对应文章的记录中。
返回:
返回成功响应。
这三个接口的设计逻辑大体是相同的
代码部分:
package com.petgeeks.pethub_backend.utils;
import com.obs.services.ObsClient;
import com.obs.services.exception.ObsException;
import com.obs.services.model.PutObjectRequest;
import java.io.File;
import java.io.InputStream;
public class HuaweiObsUtil {
public static String uploadFile(String filename, InputStream in) {
String url="";
// 您可以通过环境变量获取访问密钥AK/SK,也可以使用其他外部引入方式传入。如果使用硬编码可能会存在泄露风险。
// 您可以登录访问管理控制台获取访问密钥AK/SK
String ak ="5M7M6JWJXMI1SC4ZQ3Q7";
String sk ="S0QUy4HJb1Nlu5Zf40oL64ELf891tBMEJx4LpOqD";
// 【可选】如果使用临时AK/SK和SecurityToken访问OBS,同样建议您尽量避免使用硬编码,以降低信息泄露风险。
// 您可以通过环境变量获取访问密钥AK/SK/SecurityToken,也可以使用其他外部引入方式传入。
// String securityToken = System.getenv("SECURITY_TOKEN");
// endpoint填写桶所在的endpoint, 此处以中国-香港为例,其他地区请按实际情况填写。
String endPoint = "https://obs.cn-south-1.myhuaweicloud.com";
// 您可以通过环境变量获取endPoint,也可以使用其他外部引入方式传入。
//String endPoint = System.getenv("ENDPOINT");
// 创建ObsClient实例
// 使用永久AK/SK初始化客户端
ObsClient obsClient = new ObsClient(ak,sk,endPoint);
// 使用临时AK/SK和SecurityToken初始化客户端
// ObsClient obsClient = new ObsClient(ak, sk, securityToken, endPoint);
try {
// 文件上传
// localfile 为待上传的本地文件路径,需要指定到具体的文件名
PutObjectRequest request = new PutObjectRequest();
request.setBucketName("pethub");
request.setObjectKey(filename);
url="https://"+request.getBucketName()+"."+endPoint.substring(endPoint.lastIndexOf("/")+1)+"/"+filename;
request.setInput(in);
obsClient.putObject(request);
System.out.println("putObject successfully");
} catch (ObsException e) {
System.out.println("putObject failed");
// 请求失败,打印http状态码
System.out.println("HTTP Code:" + e.getResponseCode());
// 请求失败,打印服务端错误码
System.out.println("Error Code:" + e.getErrorCode());
// 请求失败,打印详细错误信息
System.out.println("Error Message:" + e.getErrorMessage());
// 请求失败,打印请求id
System.out.println("Request ID:" + e.getErrorRequestId());
System.out.println("Host ID:" + e.getErrorHostId());
e.printStackTrace();
} catch (Exception e) {
System.out.println("putObject failed");
// 其他异常信息打印
e.printStackTrace();
}
return url;
}
}
初始化客户端:利用华为云提供的 ObsClient
初始化与 OBS 的连接。
String url="";
// 您可以通过环境变量获取访问密钥AK/SK,也可以使用其他外部引入方式传入。如果使用硬编码可能会存在泄露风险。
// 您可以登录访问管理控制台获取访问密钥AK/SK
String ak ="5M7M6JWJXMI1SC4ZQ3Q7";
String sk ="S0QUy4HJb1Nlu5Zf40oL64ELf891tBMEJx4LpOqD";
// 【可选】如果使用临时AK/SK和SecurityToken访问OBS,同样建议您尽量避免使用硬编码,以降低信息泄露风险。
// 您可以通过环境变量获取访问密钥AK/SK/SecurityToken,也可以使用其他外部引入方式传入。
// String securityToken = System.getenv("SECURITY_TOKEN");
// endpoint填写桶所在的endpoint, 此处以中国-香港为例,其他地区请按实际情况填写。
String endPoint = "https://obs.cn-south-1.myhuaweicloud.com";
// 您可以通过环境变量获取endPoint,也可以使用其他外部引入方式传入。
//String endPoint = System.getenv("ENDPOINT");
// 创建ObsClient实例
// 使用永久AK/SK初始化客户端
ObsClient obsClient = new ObsClient(ak,sk,endPoint);
// 使用临时AK/SK和SecurityToken初始化客户端
// ObsClient obsClient = new ObsClient(ak, sk, securityToken, endPoint);
文件上传:将文件流上传到指定的 OBS 存储桶中。
try {
// 文件上传
// localfile 为待上传的本地文件路径,需要指定到具体的文件名
PutObjectRequest request = new PutObjectRequest();
request.setBucketName("pethub");
request.setObjectKey(filename);
url="https://"+request.getBucketName()+"."+endPoint.substring(endPoint.lastIndexOf("/")+1)+"/"+filename;
request.setInput(in);
obsClient.putObject(request);
System.out.println("putObject successfully");
}
在这里我们是定义了一个HuaweiObsUtil
的工具类,通过它我们可以使用华为云的OBS对象存储功能,利用桶对象,把用户上传的照片以及文章照片等信息收集起来。
生成访问 URL:返回文件上传后的外部访问链接。
url="https://"+request.getBucketName()+"."+endPoint.substring(endPoint.lastIndexOf("/")+1)+"/"+filename;
request.setInput(in);
obsClient.putObject(request);
System.out.println("putObject successfully");
这样我们可以获得上传文件后的url来确定资源,并且可以将文件对应的url存储到数据库中以便后续用户对它的访问
关于这个知识我这里介绍个b站视频方便大家进行学习参考
身份认证(JWT)
JWT的主要用途:
身份认证:在用户登录后生成Token,Token携带用户信息用于后续认证。
信息传输:前后端在无需保存状态的情况下安全地传递数据。
权限管理:根据Token中角色信息(如admin、user)控制资源访问权限。
跨服务认证:适合微服务架构,减少因分布式环境而带来的状态同步问题。
优点:
无状态,支持分布式系统扩展。
数据自包含,减少数据库查询压力。
安全性强(签名验证防篡改)。
缺点:
数据体积较大,不适合存储敏感数据。
Token一旦泄露可能造成滥用。
核心步骤:
生成Token:用户登录时生成Token,并返回给前端;
Token解析:请求携带Token时,通过中间件解析获取用户信息;
认证与鉴权:根据解析结果进行权限验证。
因为在我们的宠物APP中,我的前后端都有参与开发,我就从我开发的模块对这些部分进行讲解
这是我们写的JwtUtil
类
public class JwtUtil {
private static final String KEY = "petgeeks";
//接收业务数据,生成token并返回
public static String genToken(Map<String, Object> claims) {
return JWT.create()
.withClaim("claims", claims)
.withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 7))// token有效时间为7天
.sign(Algorithm.HMAC256(KEY));
}
//接收token,验证token,并返回业务数据
public static Map<String, Object> parseToken(String token) {
return JWT.require(Algorithm.HMAC256(KEY))
.build()
.verify(token)
.getClaim("claims")
.asMap();
}
}
在这里我们设置了token的有效期限
public static String genToken(Map<String, Object> claims) {
return JWT.create()
.withClaim("claims", claims)
.withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 7))// token有效时间为7天
.sign(Algorithm.HMAC256(KEY));
}
并且不仅我们可以通过业务数据从而生成token,也可以在收到token时对其进行解析
后端springboot的登录接口
@PostMapping("/login")
public Result<String> login(@RequestBody User user) {
log.info("用户登录:"+user.toString());
// 根据用户名查询用户
User loginUser = userService.findByUserName(user.getUsername());
// 判断用户名是否存在
// 用户名不存在
if (loginUser == null) {
return Result.error("用户名错误");
}
// 用户名存在
// 密码正确
if (Md5Util.getMD5String(user.getPassword()).equals(loginUser.getPassword())) {
// 登陆成功
Map<String, Object> claims = new HashMap<>();
claims.put("id", loginUser.getId());
claims.put("username", loginUser.getUsername());
String token = JwtUtil.genToken(claims);
return Result.success(token);
}
// 密码错误
return Result.error("密码错误");
}
较为关键的代码就是
// 密码正确
if (Md5Util.getMD5String(user.getPassword()).equals(loginUser.getPassword())) {
// 登陆成功
Map<String, Object> claims = new HashMap<>();
claims.put("id", loginUser.getId());
claims.put("username", loginUser.getUsername());
String token = JwtUtil.genToken(claims);
return Result.success(token);
}
// 密码错误
return Result.error("密码错误");
}
claims
内容设置JWT中载荷所带的id和username的数据
String token = JwtUtil.genToken(claims);
通过上面提到的JwtUtil
类,在用户登录验证通过后,我们就相当于用自己设置的载荷(id,username)和我们JwtUtil
中生成JWT的方法,为每个用户生成了不同的JWT令牌。这样一来,每个用户的权限,所访问到的内容,都会基于他们登录所生成的令牌
举个例子
// 获取用户信息
@GetMapping("/userInfo")
public Result<User> userInfo(@RequestHeader(name="Authorization") String token) {
Map<String,Object> map =JwtUtil.parseToken(token);
int id= (int)map.get("id");
User user=userService.findByUserId(id);
log.info("获取用户信息:"+user.toString());
return Result.success(user);
}
}
RequestHeader(name="Authorization") String token
,获得token并解析出对应用户的登录名和id,这样以来,我们可以根据这两个重要信息去数据库搜索到对应的用户信息
private var selectedDate: String = "" // 默认值为空字符串
private lateinit var binding: ActivityWriteLogBinding
private lateinit var logAdapter: LogAdapter
private lateinit var etDate: EditText
private var allLogEntries = mutableListOf<LogEntry>() // 存储所有日志条目
val token = TokenManager.getToken(App.INSTANCE) ?: ""
// 创建 Retrofit 实例
val retrofit = Retrofit.Builder()
.baseUrl("http://113.44.68.6:8080/")
.addConverterFactory(GsonConverterFactory.create())
.client(
OkHttpClient.Builder()
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.addHeader("Authorization", token)
.build()
chain.proceed(request)
}
.build())
.build()
获取Token:从 TokenManager 获取当前存储的 JWT,用于后续请求。
自动附加Token:通过 OkHttp 的拦截器为每个网络请求的请求头添加 Authorization 字段,携带 JWT。
身份验证:服务器通过解析 JWT 验证请求来源及权限。
前端的话则是在创建 Retrofit 实例会用到addHeader("Authorization", token)
进行身份认证和获取对应信息
文件上传与下载: 实现了用户头像、宠物头像和文章图片的上传功能。通过文件验证确保上传的安全性和准确性,采用UUID生成唯一文件名,并使用华为云OBS存储文件,同时返回存储URL供前端展示。
华为云OBS集成: 借助华为云OBS SDK实现文件的高效上传和管理,确保文件传输的稳定性与存储的可靠性,为文件存储提供云端支持。
身份认证(JWT): 使用JWT进行用户身份验证,确保请求合法性,并通过解析Token关联用户或宠物的相关数据,提升系统的安全性和扩展性。
这些技术相辅相成,为宠物APP提供了便捷、安全的操作体验和高效的云服务支持。