diff --git a/common/common-service/src/main/kotlin/cn/bunny/common/service/exception/GlobalExceptionHandler.kt b/common/common-service/src/main/kotlin/cn/bunny/common/service/exception/GlobalExceptionHandler.kt index 258c903..17cba0f 100644 --- a/common/common-service/src/main/kotlin/cn/bunny/common/service/exception/GlobalExceptionHandler.kt +++ b/common/common-service/src/main/kotlin/cn/bunny/common/service/exception/GlobalExceptionHandler.kt @@ -5,6 +5,8 @@ import cn.bunny.dao.pojo.result.Result import cn.bunny.dao.pojo.result.ResultCodeEnum import lombok.extern.slf4j.Slf4j import org.apache.logging.log4j.LogManager +import org.springframework.validation.FieldError +import org.springframework.web.bind.MethodArgumentNotValidException import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.ResponseBody import org.springframework.web.bind.annotation.RestControllerAdvice @@ -12,6 +14,7 @@ import java.io.FileNotFoundException import java.nio.file.AccessDeniedException import java.sql.SQLIntegrityConstraintViolationException import java.util.regex.Pattern +import java.util.stream.Collectors @RestControllerAdvice @@ -29,6 +32,13 @@ class GlobalExceptionHandler { return Result.error(null, code, exception.message) } + // 表单验证字段 + @ExceptionHandler(MethodArgumentNotValidException::class) + fun handleValidationExceptions(ex: MethodArgumentNotValidException): Result { + val errorMessage = ex.bindingResult.fieldErrors.stream().map { obj: FieldError -> obj.defaultMessage }.collect(Collectors.joining(", ")) + return Result.error(null, 201, errorMessage) + } + // 运行时异常信息 @ExceptionHandler(RuntimeException::class) @ResponseBody @@ -47,7 +57,7 @@ class GlobalExceptionHandler { // 错误消息 val message = exception.message ?: "" - + // 匹配到内容 val patternString = "Request method '(\\w+)' is not supported" val matcher = Pattern.compile(patternString).matcher(message) diff --git a/dao/pom.xml b/dao/pom.xml index 0ca6db5..2c96383 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -53,6 +53,11 @@ swagger-annotations 1.6.14 + + + org.springframework.boot + spring-boot-starter-validation + org.jetbrains.kotlin kotlin-test-junit5 diff --git a/dao/src/main/kotlin/cn/bunny/dao/dto/system/RefreshTokenDto.kt b/dao/src/main/kotlin/cn/bunny/dao/dto/system/RefreshTokenDto.kt new file mode 100644 index 0000000..89af03d --- /dev/null +++ b/dao/src/main/kotlin/cn/bunny/dao/dto/system/RefreshTokenDto.kt @@ -0,0 +1,20 @@ +package cn.bunny.dao.dto.system + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank +import lombok.AllArgsConstructor +import lombok.Data +import lombok.NoArgsConstructor + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Schema(name = "RefreshTokenDto对象", title = "登录成功返回内容", description = "登录成功返回内容") +class RefreshTokenDto { + @Schema(name = "refreshToken", title = "请求刷新token") + @NotBlank(message = "请求刷新token不能为空") + var refreshToken: String? = null + + @Schema(name = "readMeDay", title = "记住我天数") + var readMeDay: Long = 1 +} \ No newline at end of file diff --git a/dao/src/main/kotlin/cn/bunny/dao/pojo/constant/LocalDateTimeConstant.kt b/dao/src/main/kotlin/cn/bunny/dao/pojo/constant/LocalDateTimeConstant.kt index c351641..5a555dc 100644 --- a/dao/src/main/kotlin/cn/bunny/dao/pojo/constant/LocalDateTimeConstant.kt +++ b/dao/src/main/kotlin/cn/bunny/dao/pojo/constant/LocalDateTimeConstant.kt @@ -7,5 +7,6 @@ class LocalDateTimeConstant { companion object { const val YYYY_MM_DD: String = "yyyy-MM-dd" const val YYYY_MM_DD_HH_MM_SS: String = "yyyy-MM-dd HH:mm:ss" + const val YYYY_MM_DD_HH_MM_SS_SLASH: String = "yyyy/MM/dd HH:mm:ss" } } diff --git a/dao/src/main/kotlin/cn/bunny/dao/pojo/result/ResultCodeEnum.kt b/dao/src/main/kotlin/cn/bunny/dao/pojo/result/ResultCodeEnum.kt index cb9e712..8e95e84 100644 --- a/dao/src/main/kotlin/cn/bunny/dao/pojo/result/ResultCodeEnum.kt +++ b/dao/src/main/kotlin/cn/bunny/dao/pojo/result/ResultCodeEnum.kt @@ -10,7 +10,7 @@ enum class ResultCodeEnum(val code: Int, val message: String) { // 成功操作 200 SUCCESS(200, "操作成功"), SUCCESS_LOGOUT(200, "退出成功"), - EMAIL_CODE_REFRESH(200, "邮箱验证码已刷新"), + EMAIL_CODE_SEND_SUCCESS(200, "邮箱验证码已发送"), // 验证错误 201 USERNAME_OR_PASSWORD_NOT_EMPTY(201, "用户名或密码不能为空"), @@ -18,7 +18,6 @@ enum class ResultCodeEnum(val code: Int, val message: String) { EMAIL_CODE_EMPTY(201, "邮箱验证码过期或不存在"), EMAIL_CODE_NOT_MATCHING(201, "邮箱验证码不匹配"), LOGIN_ERROR(201, "账号或密码错误"), - LOGIN_ERROR_USERNAME_PASSWORD_NOT_EMPTY(201, "登录信息不能为空"), SEND_MAIL_CODE_ERROR(201, "邮件发送失败"), // 数据相关 206 @@ -31,11 +30,9 @@ enum class ResultCodeEnum(val code: Int, val message: String) { // 身份过期 208 LOGIN_AUTH(208, "请先登陆"), AUTHENTICATION_EXPIRED(208, "身份验证过期"), - SESSION_EXPIRATION(208, "会话过期"), // 封禁 209 FAIL_NO_ACCESS_DENIED_USER_LOCKED(209, "账户已封禁"), - THE_SAME_USER_HAS_LOGGED_IN(209, "相同用户已登录"), // 提示错误 URL_ENCODE_ERROR(216, "URL编码失败"), @@ -45,7 +42,6 @@ enum class ResultCodeEnum(val code: Int, val message: String) { // 无权访问 403 FAIL_REQUEST_NOT_AUTH(403, "用户未认证"), FAIL_NO_ACCESS_DENIED(403, "无权访问"), - FAIL_NO_ACCESS_DENIED_USER_OFFLINE(403, "用户强制下线"), LOGGED_IN_FROM_ANOTHER_DEVICE(403, "没有权限访问"), // 系统错误 500 diff --git a/dao/src/main/kotlin/cn/bunny/dao/vo/user/LoginVo.kt b/dao/src/main/kotlin/cn/bunny/dao/vo/user/LoginVo.kt index 16d4ee4..41c59e3 100644 --- a/dao/src/main/kotlin/cn/bunny/dao/vo/user/LoginVo.kt +++ b/dao/src/main/kotlin/cn/bunny/dao/vo/user/LoginVo.kt @@ -54,7 +54,7 @@ data class LoginVo( var refreshToken: String? = null, @Schema(name = "expires", title = "过期时间") - var expires: Long? = null, + var expires: String? = null, @Schema(name = "roleList", title = "角色列表") var roleList: List? = null, diff --git a/dao/src/main/kotlin/cn/bunny/dao/vo/user/RefreshTokenVo.kt b/dao/src/main/kotlin/cn/bunny/dao/vo/user/RefreshTokenVo.kt new file mode 100644 index 0000000..40f9565 --- /dev/null +++ b/dao/src/main/kotlin/cn/bunny/dao/vo/user/RefreshTokenVo.kt @@ -0,0 +1,21 @@ +package cn.bunny.dao.vo.user + +import io.swagger.v3.oas.annotations.media.Schema +import lombok.AllArgsConstructor +import lombok.Builder +import lombok.NoArgsConstructor + +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Schema(name = "RefreshTokenVo 对象", title = "刷新token返回内容", description = "刷新token返回内容") +data class RefreshTokenVo( + @Schema(name = "accessToken", title = "访问令牌") + var accessToken: String? = null, + + @Schema(name = "refreshToken", title = "刷新token") + var refreshToken: String? = null, + + @Schema(name = "expires", title = "过期时间") + var expires: String? = null, +) \ No newline at end of file diff --git a/services/src/main/kotlin/cn/bunny/services/controller/UserController.kt b/services/src/main/kotlin/cn/bunny/services/controller/UserController.kt index db70267..38fb033 100644 --- a/services/src/main/kotlin/cn/bunny/services/controller/UserController.kt +++ b/services/src/main/kotlin/cn/bunny/services/controller/UserController.kt @@ -1,11 +1,16 @@ package cn.bunny.services.controller +import cn.bunny.dao.dto.system.RefreshTokenDto import cn.bunny.dao.pojo.result.Result +import cn.bunny.dao.pojo.result.ResultCodeEnum +import cn.bunny.dao.vo.user.RefreshTokenVo import cn.bunny.services.service.UserService import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid import org.springframework.beans.factory.annotation.Autowired import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @@ -22,7 +27,6 @@ import org.springframework.web.bind.annotation.RestController @RequestMapping("/admin/user") @Tag(name = "系统用户", description = "系统用户相关接口") class UserController { - @Autowired private lateinit var userService: UserService @@ -30,6 +34,13 @@ class UserController { @PostMapping("noAuth/sendLoginEmail") fun sendLoginEmail(email: String): Result { userService.sendLoginEmail(email) - return Result.success() + return Result.success(ResultCodeEnum.EMAIL_CODE_SEND_SUCCESS) + } + + @Operation(summary = "刷新token", description = "刷新用户token") + @PostMapping("noAuth/refreshToken") + fun refreshToken(@Valid @RequestBody dto: RefreshTokenDto): Result { + val vo: RefreshTokenVo = userService.refreshToken(dto) + return Result.success(vo) } } diff --git a/services/src/main/kotlin/cn/bunny/services/factory/UserFactory.kt b/services/src/main/kotlin/cn/bunny/services/factory/UserFactory.kt index d4fb4bf..245e423 100644 --- a/services/src/main/kotlin/cn/bunny/services/factory/UserFactory.kt +++ b/services/src/main/kotlin/cn/bunny/services/factory/UserFactory.kt @@ -5,6 +5,7 @@ import cn.bunny.common.service.utils.ip.IpUtil import cn.bunny.dao.entity.system.AdminUser import cn.bunny.dao.entity.system.Power import cn.bunny.dao.entity.system.Role +import cn.bunny.dao.pojo.constant.LocalDateTimeConstant import cn.bunny.dao.pojo.constant.RedisUserConstant.Companion.getAdminLoginInfoPrefix import cn.bunny.dao.pojo.constant.RedisUserConstant.Companion.getAdminUserEmailCodePrefix import cn.bunny.dao.vo.user.LoginVo @@ -15,6 +16,8 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.data.redis.core.RedisTemplate import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter import java.util.concurrent.TimeUnit @Component @@ -41,6 +44,12 @@ class UserFactory { updateUser.lastLoginIp = IpUtil.getCurrentUserIpAddress().remoteAddr updateUser.lastLoginIpAddress = IpUtil.getCurrentUserIpAddress().ipRegion + // 计算过期时间,并格式化返回 + val localDateTime: LocalDateTime = LocalDateTime.now() + val plusDay = localDateTime.plusDays(readMeDay) + val dateTimeFormatter = DateTimeFormatter.ofPattern(LocalDateTimeConstant.YYYY_MM_DD_HH_MM_SS_SLASH) + val expires: String = plusDay.format(dateTimeFormatter) + // 构建返回对象 val loginVo = LoginVo() BeanUtils.copyProperties(user, loginVo) @@ -51,7 +60,7 @@ class UserFactory { loginVo.roleList = roleMapper.selectListByUserId(userId).map { role: Role -> role.roleCode!! } loginVo.powerList = powerMapper.selectListByUserId(userId).map { power: Power -> power.powerCode!! } loginVo.updateUser = userId - loginVo.expires = readMeDay + loginVo.expires = expires // 将信息保存在Redis中 redisTemplate.opsForValue().set(getAdminLoginInfoPrefix(email), loginVo, readMeDay, TimeUnit.DAYS) diff --git a/services/src/main/kotlin/cn/bunny/services/service/UserService.java b/services/src/main/kotlin/cn/bunny/services/service/UserService.java index e7faad0..06e47c1 100644 --- a/services/src/main/kotlin/cn/bunny/services/service/UserService.java +++ b/services/src/main/kotlin/cn/bunny/services/service/UserService.java @@ -1,6 +1,8 @@ package cn.bunny.services.service; +import cn.bunny.dao.dto.system.RefreshTokenDto; import cn.bunny.dao.entity.system.AdminUser; +import cn.bunny.dao.vo.user.RefreshTokenVo; import com.baomidou.mybatisplus.extension.service.IService; import org.jetbrains.annotations.NotNull; @@ -20,4 +22,13 @@ public interface UserService extends IService { * @param email 邮箱 */ void sendLoginEmail(@NotNull String email); + + /** + * 刷新用户token + * + * @param dto 请求token + * @return 刷新token返回内容 + */ + @NotNull + RefreshTokenVo refreshToken(@NotNull RefreshTokenDto dto); } diff --git a/services/src/main/kotlin/cn/bunny/services/service/impl/UserServiceImpl.kt b/services/src/main/kotlin/cn/bunny/services/service/impl/UserServiceImpl.kt index e13c97d..c9c8047 100644 --- a/services/src/main/kotlin/cn/bunny/services/service/impl/UserServiceImpl.kt +++ b/services/src/main/kotlin/cn/bunny/services/service/impl/UserServiceImpl.kt @@ -1,12 +1,17 @@ package cn.bunny.services.service.impl import cn.bunny.common.service.exception.BunnyException +import cn.bunny.common.service.utils.JwtHelper +import cn.bunny.dao.dto.system.RefreshTokenDto import cn.bunny.dao.entity.system.AdminUser import cn.bunny.dao.entity.system.EmailUsers import cn.bunny.dao.pojo.constant.RedisUserConstant import cn.bunny.dao.pojo.email.EmailSendInit import cn.bunny.dao.pojo.result.ResultCodeEnum +import cn.bunny.dao.vo.user.LoginVo +import cn.bunny.dao.vo.user.RefreshTokenVo import cn.bunny.services.factory.EmailFactory +import cn.bunny.services.factory.UserFactory import cn.bunny.services.mapper.EmailUsersMapper import cn.bunny.services.mapper.UserMapper import cn.bunny.services.service.UserService @@ -27,6 +32,10 @@ import org.springframework.transaction.annotation.Transactional @Service @Transactional internal class UserServiceImpl : ServiceImpl(), UserService { + + @Autowired + private lateinit var userFactory: UserFactory + @Autowired private lateinit var redisTemplate: RedisTemplate @@ -57,5 +66,28 @@ internal class UserServiceImpl : ServiceImpl(), UserSer // 将验证码存入Redis中 redisTemplate.opsForValue().set(RedisUserConstant.getAdminUserEmailCodePrefix(email), emailCode) } + + /** + * 刷新用户token + * @param dto 请求token + * @return 刷新token返回内容 + */ + override fun refreshToken(dto: RefreshTokenDto): RefreshTokenVo { + // 解析token内容 + val userId = JwtHelper.getUserId(dto.refreshToken) + val adminUser: AdminUser? = getOne(KtQueryWrapper(AdminUser()).eq(AdminUser::id, userId)) + + // 判断用户是否禁用 + when { + adminUser?.status == 1.toByte() -> throw BunnyException(ResultCodeEnum.FAIL_NO_ACCESS_DENIED_USER_LOCKED) + } + + // 构建返回内容 + val buildUserVo: LoginVo = userFactory.buildUserVo(adminUser!!, dto.readMeDay) + val refreshTokenVo = RefreshTokenVo() + BeanUtils.copyProperties(buildUserVo, refreshTokenVo) + + return refreshTokenVo + } } diff --git a/services/src/test/kotlin/cn/bunny/services/service/impl/UserServiceImplTest.kt b/services/src/test/kotlin/cn/bunny/services/service/impl/UserServiceImplTest.kt index ab74e7d..b389195 100644 --- a/services/src/test/kotlin/cn/bunny/services/service/impl/UserServiceImplTest.kt +++ b/services/src/test/kotlin/cn/bunny/services/service/impl/UserServiceImplTest.kt @@ -1,12 +1,15 @@ package cn.bunny.services.service.impl import cn.bunny.dao.entity.system.AdminUser +import cn.bunny.dao.pojo.constant.LocalDateTimeConstant import cn.bunny.services.mapper.UserMapper import org.junit.Test import org.junit.runner.RunWith import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.context.junit4.SpringRunner +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter @SpringBootTest @RunWith(SpringRunner::class) @@ -21,4 +24,14 @@ class UserServiceImplTest { adminUser.lastLoginIpAddress = "内网IP" userMapper.updateById(adminUser) } + + @Test + fun testTime() { + val localDateTime: LocalDateTime = LocalDateTime.now() + val plusDay = localDateTime.plusDays(7) + val dateTimeFormatter = DateTimeFormatter.ofPattern(LocalDateTimeConstant.YYYY_MM_DD_HH_MM_SS_SLASH) + val format = plusDay.format(dateTimeFormatter) + + println(format) + } } \ No newline at end of file