feat(新增): 刷新token

This commit is contained in:
Bunny 2024-09-26 23:09:23 +08:00
parent c8e14fca20
commit 15176c0b5a
12 changed files with 139 additions and 10 deletions

View File

@ -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<String> {
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)

View File

@ -53,6 +53,11 @@
<artifactId>swagger-annotations</artifactId>
<version>1.6.14</version>
</dependency>
<!-- 表单验证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test-junit5</artifactId>

View File

@ -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
}

View File

@ -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"
}
}

View File

@ -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

View File

@ -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<String>? = null,

View File

@ -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,
)

View File

@ -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<String> {
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<RefreshTokenVo> {
val vo: RefreshTokenVo = userService.refreshToken(dto)
return Result.success(vo)
}
}

View File

@ -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)

View File

@ -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<AdminUser> {
* @param email 邮箱
*/
void sendLoginEmail(@NotNull String email);
/**
* 刷新用户token
*
* @param dto 请求token
* @return 刷新token返回内容
*/
@NotNull
RefreshTokenVo refreshToken(@NotNull RefreshTokenDto dto);
}

View File

@ -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<UserMapper?, AdminUser?>(), UserService {
@Autowired
private lateinit var userFactory: UserFactory
@Autowired
private lateinit var redisTemplate: RedisTemplate<Any, Any>
@ -57,5 +66,28 @@ internal class UserServiceImpl : ServiceImpl<UserMapper?, AdminUser?>(), 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
}
}

View File

@ -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)
}
}