【挑战项目】 --- 微服务编程测评系统(在线OJ系统)(二)
三十二、Swagger介绍&使用
官网:https://swagger.io/
什么是swagger
Swagger是一个接口文档生成工具,它可以帮助开发者自动生成接口文档。当项目的接口发生变更时,Swagger可以实时更新文档,确保文档的准确性和时效性。Swagger还内置了测试功能,开发者可以直接在文档中测试接口,无需编写额外的测试代码。
引入swagger
- 在oj-common下创建oj-common-swagger子module
- 导入依赖(放在这个模块的pom文件就可以了)
-
<dependency><groupId>org.springdoc</groupId><artifactId>springdoc-openapi-starter-webmvc-ui</artifactId><version>2.2.0</version> </dependency>
- 录入配置
-
@Configuration public class SwaggerConfig {@Beanpublic OpenAPI openAPI() {return new OpenAPI().info(new Info().title("在线oj系统").description("在线oj系统接⼝⽂档").version("v1"));} }
具体服务引⼊:
由于system模块要用到这个swagger组件,
因此将刚刚封装的swagger的这个组件也引入到要用的微服务中也就是引入到
system的pom文件中。
<dependency><groupId>com.qyy</groupId><artifactId>oj-common-swagger</artifactId><version>${oj-common-swagger.version}</version></dependency>
swagger基本注解
@Tag
- 介绍:用于给接口分组,用途类似于为接口文档添加标签。
- 用于:方法、类、接口。
- 常用属性:
name
:分组的名称@RestController @RequestMapping("/sysUser") @Tag(name = "管理员接口") public class SysUserController extends BaseController {
@Operation
- 介绍:用于描述接口的操作。
- 用于:方法。
- 常用属性:
summary
:操作的摘要信息。description
:操作的详细描述。 @Operation(summary = "管理员登录", description = "根据账号密码进行管理员登录")//controller层如果介绍的是body 参数 需要使用@RequestBody注解public R<String> login(@RequestBody LoginDTO loginDTO) {return sysUserService.login(loginDTO.getUserAccount(), loginDTO.getPassword());}
@Parameters
- 介绍:用于指定
@Parameter
注解对象数组,描述操作的输入参数。- 用于:方法。
@Parameters(value = {@Parameter(name = "userId", in = ParameterIn.PATH, description = "用户ID")})public R<Void> delete(@PathVariable Long userId) {return null;}
@Parameter
- 介绍:用于描述输入参数。
- 用于:方法。
- 常用属性:
name
:参数的名称。in
:参数的位置,可以是path
、query
、header
、cookie
中的一种。description
:参数的描述。@Parameters(value = {@Parameter(name = "userId", in = ParameterIn.PATH, description = "用户ID")})public R<Void> delete(@PathVariable Long userId) {return null;}
@ApiResponse
- 介绍:用于描述操作的响应结果。
- 用于:方法。
- 常用属性:
- responseCode:响应的状态码。
- description:响应的描述。
@ApiResponse(responseCode = "1000", description = "操作成功")@ApiResponse(responseCode = "2000", description = "服务繁忙请稍后重试")@ApiResponse(responseCode = "3102", description = "用户不存在")@ApiResponse(responseCode = "3103", description = "用户名或密码错误")//controller层如果介绍的是body 参数 需要使用@RequestBody注解public R<String> login(@RequestBody LoginDTO loginDTO) {return sysUserService.login(loginDTO.getUserAccount(), loginDTO.getPassword());}
@Schema
- 介绍:用于描述数据模型的属性。
- 用于:方法、类、接口。
- 常用属性:
- description:响应的描述。
@Getter @Setter public class SysUserSaveDTO {@Schema(description = "用户账号")private String userAccount;@Schema(description = "用户密码")private String password; }
为了让SwaggerConfig生效(外部bean让Spring能扫描到)
在oj-common-swagger模块下的 resources 下创建
META-INF.spring包
再创建org.springframework.boot.autoconfigure.AutoConfiguration.imports⽂件
在里面写上路径
com.qyy.swagger.SwaggerConfig;
生成当前接口文档的地址
服务器运行之后,在浏览器输入地址:例如我的地址就是
http://localhost:1208/swagger-ui/index.html
三十三、 管理员登录-接口测试01
如何用swagger进行测试呢
我们使用login接口进行测试
测试内容如下:
1.登录成功
2.账号和密码错误
controller层
代码如下,传入两个参数
loginDTO.getUserAccount(), loginDTO.getPassword()
用户名和密码
DTO就代表是前端传给服务器的数据
@PostMapping("/login") //安全@Operation(summary = "管理员登录", description = "根据账号密码进行管理员登录")@ApiResponse(responseCode = "1000", description = "操作成功")@ApiResponse(responseCode = "2000", description = "服务繁忙请稍后重试")@ApiResponse(responseCode = "3102", description = "用户不存在")@ApiResponse(responseCode = "3103", description = "用户名或密码错误")//controller层如果介绍的是body 参数 需要使用@RequestBody注解public R<String> login(@RequestBody LoginDTO loginDTO) {return sysUserService.login(loginDTO.getUserAccount(), loginDTO.getPassword());}
Service层
ISysUserService类
这是接口
R<String> login(String userAccount, String password);
SysUserServiceImpl类
这是接口的实现
@Override//维护性、性能、安全public R<String> login(String userAccount, String password) {
// try {
// FileInputStream inputStream = new FileInputStream("a.txt");
// } catch (FileNotFoundException e) {
// throw new RuntimeException(e);
// }
// int a = 100 / 0;//通过账号去数据库中查询,对应的用户信息LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();//select password from tb_sys_user where user_account = #{userAccount}SysUser sysUser = sysUserMapper.selectOne(queryWrapper.select(SysUser::getUserId, SysUser::getPassword, SysUser::getNickName).eq(SysUser::getUserAccount, userAccount));
// R loginResult = new R();if (sysUser == null) {
// loginResult.setCode(ResultCode.FAILED_USER_NOT_EXISTS.getCode());
// loginResult.setMsg(ResultCode.FAILED_USER_NOT_EXISTS.getMsg());
// return loginResult;return R.fail(ResultCode.FAILED_USER_NOT_EXISTS);}if (BCryptUtils.matchesPassword(password, sysUser.getPassword())) {
// loginResult.setCode(ResultCode.SUCCESS.getCode());
// loginResult.setMsg(ResultCode.SUCCESS.getMsg());
// return loginResult;// jwttoken = 生产jwttoken的方法return R.ok(tokenService.createToken(sysUser.getUserId(),secret, UserIdentity.ADMIN.getValue(), sysUser.getNickName(), null));}
// loginResult.setCode(ResultCode.FAILED_LOGIN.getCode());
// loginResult.setMsg(ResultCode.FAILED_LOGIN.getMsg());
// return loginResult;return R.fail(ResultCode.FAILED_LOGIN);}
点击try it out 就可以传入用户名和密码
如图返回结果是用户名和密码错误
这是因为数据库中我们存储的密码并不是加密后的。因此密码对应不上
三十四、Apifox介绍&使用
Apifox = Postman + Swagger + Mock + JMeter
Apifox 简介
Apifox 是一款集 API 文档管理、调试、Mock、自动化测试于一体的开发工具,旨在为开发者、测试人员和前端/后端工程师提供一站式 API 开发解决方案。它结合了类似 Postman 的 API 调试功能、Swagger 的文档生成功能以及 Mock 数据服务,极大地提升了团队协作效率和 API 开发体验。
添加接口
设置环境
设置值
读取变量
发送数据(测试成功)
设置响应数据模型
在响应中引入数据模型
隐藏字段
解除关联
单个解除关联:就可以对响应数据进行修改,但不能修改名称
整体解除关联:那么就都可以修改了
Apifox 的核心功能
1. API 文档管理
- Apifox 支持基于 OpenAPI(原 Swagger)标准的 API 文档定义。
- 提供直观的可视化界面,方便开发者快速编写和维护 API 文档。
- 自动生成 API 请求示例代码(如 cURL、JavaScript、Python 等),便于集成到不同开发环境。
- 支持团队协作,多人可以同时编辑和查看文档。
2. API 调试
- 类似于 Postman 的功能,允许用户直接在 Apifox 中发送 HTTP 请求并查看响应结果。
- 支持多种请求方法(GET、POST、PUT、DELETE 等)和复杂的请求参数设置。
- 内置环境变量管理功能,方便切换不同的开发、测试和生产环境。
- 自动保存历史记录,便于回溯和复用请求。
3. Mock 数据
- 根据 API 文档自动生成 Mock 数据,无需手动配置。
- 支持自定义 Mock 规则,满足复杂的业务需求。
- Mock 服务支持动态生成数据,例如随机字符串、时间戳、枚举值等。
- 无需后端开发完成即可进行前端开发,提升开发效率。
4. 自动化测试
- 提供强大的 API 自动化测试功能,支持断言、参数提取、全局变量等功能。
- 测试用例可按顺序执行,支持批量运行。
- 测试结果以清晰的报告形式展示,便于分析和优化。
- 支持 CI/CD 集成,将自动化测试嵌入到持续集成流程中。
5. 团队协作
- 支持多人在线协作,团队成员可以共享 API 文档、测试用例和环境配置。
- 提供权限管理功能,确保不同角色的用户只能访问其权限范围内的资源。
- 版本控制功能,支持 API 文档的历史版本管理。
6. 插件与扩展
- Apifox 提供丰富的插件生态,用户可以根据需求扩展功能。
- 支持与其他工具(如 Git、Jenkins 等)集成,进一步增强开发和测试能力。
Apifox 的优势
-
一体化工具
Apifox 将 API 文档管理、调试、Mock 和测试功能整合到一个平台中,避免了在多个工具之间切换的麻烦。 -
易用性
提供直观的图形化界面,即使是初学者也能快速上手。 -
高效开发
Mock 数据和自动化测试功能显著缩短了前后端联调的时间,提高了开发效率。 -
团队协作友好
强大的团队协作功能使得跨部门沟通更加顺畅,减少了信息不对称的问题。 -
跨平台支持
Apifox 支持 Windows、macOS 和 Linux,覆盖主流操作系统。
适用场景
-
前后端分离项目
在前后端分离的开发模式下,Apifox 可以帮助前端开发者通过 Mock 数据快速启动开发,而无需等待后端接口完成。 -
API 文档维护
对于需要长期维护的 API 项目,Apifox 提供了一个集中化的平台来管理所有 API 文档。 -
接口测试与自动化
测试人员可以利用 Apifox 的自动化测试功能对 API 进行回归测试,确保接口的稳定性和可靠性。 -
微服务架构
在微服务架构中,Apifox 可以帮助团队统一管理多个服务的 API 接口,并提供高效的调试和测试能力。
Apifox vs 其他工具
总结
Apifox 是一款功能强大且易于使用的 API 工具,特别适合需要高效协作和全流程 API 管理的团队。无论是开发、测试还是文档维护,Apifox 都能够为用户提供极大的便利。如果你正在寻找一款能够替代 Postman 和 Swagger 的工具,Apifox 无疑是一个值得尝试的选择。
三十五、管理员登录-代码优化-加密算法介绍
管理员登录:还要考虑可维护性、性能、安全
这里重点讨论
安全问题
数据库中我们如果存储密码的明文,那么黑客如果拿到数据库,就可以随意登录管理员页面,或者即使是内部人员看到也是不可以的。
简介
加密算法是一种将明文转换为密文的过程,以保护数据的安全性和机密性。
作用
- 安全性:存储明文密码是非常不安全的。如果数据库被非法访问或泄露,攻击者可以直接获取所有用户的密码。
- 减少内部风险:即使企业内有不诚实的员工或管理员,他们也无法轻易获取其他管理员的密码,因为密码是加密存储的。
- 提升用户信任度。密码加密有助于提升用户对网站或应用程序的信任感,使其更愿意使用加密保护的网站。
常见的加密算法:
- 可逆算法:一种可以将加密后的密文还原为原始明文的算法。
- 对称算法:对称加密(也叫私钥加密)指加密和解密使用相同密钥的加密算法。它要求发送方和接收方在安全通信之前,商定一个密钥。对称算法的安全性依赖于密钥,泄漏密钥就意味着任何人都可以对他们发送或接收的消息解密,所以密钥的保密性对通信的安全性至关重要。
- 非对称算法:非对称加密是指需要两个密钥来进行加密和解密,这两个秘钥分别是公钥(public key)和私钥(private key),如果用公钥对数据进行加密,只有用对应的私钥才能解密。
- 不可逆算法:一种无法将加密后的密文还原为原始明文的算法。
- 单向散列(hash)加密:是指把任意长的输入串变化成固定长的输出串,并且由输出串难以得到输入串的加密方法。广泛应用于对敏感数据加密,比如用户密码,请求参数,文件加密等。
BCrypt
Bcrypt是一种哈希加密算法,被广泛应用于存储密码和进行身份验证。并且Bcrypt算法包含一个重要特性即每次生成的哈希值都不同,这是由于Bcrypt算法在计算时会先生成一个随机的盐值与用户密码一起参与计算最终得到一个加密后的字符串。由于生成的盐值是随机的,所以即使每次使用相同的密码得到结果也是不同的。这样可以有效的防止攻击者使用一些手段破解用户密码。
package com.qyy.system.utils;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;/*** 加密算法工具类*/
public class BCryptUtils {/*** 生成加密后密文** @param password 密码* @return 加密字符串*/public static String encryptPassword(String password) {BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();return passwordEncoder.encode(password);// 加密}/*** 判断密码是否相同** @param rawPassword 真实密码* @param encodedPassword 加密后密文* @return 结果*/public static boolean matchesPassword(String rawPassword, String encodedPassword) {BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();return passwordEncoder.matches(rawPassword, encodedPassword);// 密码匹配}public static void main(String[] args) {System.out.println(encryptPassword("123"));System.out.println(matchesPassword("123", "$2a$10$Nm0bAesQKuPt5CWijLVbmOb5FXxRuoFUbHNwupVp.8DqbYQjf8iUW"));}}
}
三十六、管理员登录-代码优化-加密算法使用
在SysUserServiceImpl类中
修改login方法,借助BCryptUtils.matchesPassword()方法,来判断
用户输入密码是否正确
@Override//维护性、性能、安全public R<String> login(String userAccount, String password) {//通过账号去数据库中查询,对应的用户信息LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();//select password from tb_sys_user where user_account = #{userAccount}SysUser sysUser = sysUserMapper.selectOne(queryWrapper.select(SysUser::getUserId, SysUser::getPassword, SysUser::getNickName).eq(SysUser::getUserAccount, userAccount));return R.fail(ResultCode.FAILED_USER_NOT_EXISTS);}if (BCryptUtils.matchesPassword(password, sysUser.getPassword())) {return R.ok();}return R.fail(ResultCode.FAILED_LOGIN);}
测试成功
封装返回结果
封装返回方法
package com.qyy.common.core.domain;import com.qyy.common.core.enums.ResultCode;
import lombok.Getter;
import lombok.Setter;//接口文档
@Getter
@Setter
public class R<T> {private int code; //定义一些固定的code,前后端商量好的 0 1 请求成功 常量 2 3 枚举private String msg; //? 通常是code的辅助说明 一个code 对应一个msgprivate T data; //请求某个接口返回的数据list SysUser 泛型//将封装result的封装方法可以写到R里面
// loginResult.setCode(ResultCode.FAILED_USER_NOT_EXISTS.getCode());
// loginResult.setMsg(ResultCode.FAILED_USER_NOT_EXISTS.getMsg());
// return loginResult;public static <T> R<T> ok() {return assembleResult(null, ResultCode.SUCCESS);}public static <T> R<T> ok(T data) {return assembleResult(data, ResultCode.SUCCESS);}public static <T> R<T> fail() {return assembleResult(null, ResultCode.FAILED);}public static <T> R<T> fail(int code, String msg) {return assembleResult(code, msg, null);}/*** 指定错误码** @param resultCode 指定错误码* @param <T>* @return*/public static <T> R<T> fail(ResultCode resultCode) {return assembleResult(null, resultCode);}private static <T> R<T> assembleResult(T data, ResultCode resultCode) {R<T> r = new R<>();r.setCode(resultCode.getCode());r.setData(data);r.setMsg(resultCode.getMsg());return r;}private static <T> R<T> assembleResult(int code, String msg, T data) {R<T> r = new R<>();r.setCode(code);r.setData(data);r.setMsg(msg);return r;}
}
三十七、全局异常处理
思考一下,前面写的代码还有啥问题:
就是我们没有考虑异常出现的时候
例如:
发现代码中有
int a = 100 / 0;
项目依然可以跑起来,但是实际上我们都知道,代码执行到这里可以是会报错的。
那么如果这个错误不明显,不像int a = 100 / 0;
那么我们要找到这个错误是一件很困难的事情
注意:判断密码错误等不属于异常,这只是逻辑错误的判断
如果都加try catch会让代码变得很丑陋,因此我们要使用全局异常处理
异常:
- 编译时异常
- 运行时异常
在oj-common下,创建oj-common-security子工程
创建GlobalExceptionHandler类
@RestControllerAdvice注解
:当抛出异常时,
@RestControllerAdvice
标注的类将被自动调用,并根据异常类型和处理程序的注解来决定如何处理该异常。这使得开发者可以在整个应用程序范围内统一处理异常。
@ExceptionHandler注解
:
@ExceptionHandler
一般与@RestControllerAdvice
配合使用,使用其来捕获和处理不同类型的异常。注意:
1.我们尽量将抛出的异常都使用自定义异常,这样便于在异常处理处进行异常处理,比如统一返回json格式,或者统一进行日志记录等。
2.对于其他微服务来讲,它也是外部bean,因此要加上这个文件,才能将这个bean交给Spring容器去管理
3.为了让用户不看到我们的后台具体出什么问题了,因此就返回一个服务器异常就行了,而我们程序员就看日志的错误就行了
package com.qyy.common.security.handler;import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.qyy.common.core.domain.R;
import com.qyy.common.core.enums.ResultCode;
import com.qyy.common.security.exception.ServiceException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;import java.util.Collection;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;/*** 全局异常处理器*/
@RestControllerAdvice//全局异常处理器
@Slf4j
//我们尽量将抛出的异常都使用自定义异常,这样便于在异常处理处进行异常处理,比如统一返回json格式,或者统一进行日志记录等。
public class GlobalExceptionHandler {/*** 请求方式不支持*/@ExceptionHandler(HttpRequestMethodNotSupportedException.class)public R<?> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,HttpServletRequest request) {String requestURI = request.getRequestURI();log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod());return R.fail(ResultCode.ERROR);}//拦截业务异常@