旗开得胜

This commit is contained in:
chen-xin-zhi 2025-04-28 12:34:22 +08:00
parent be354a98ec
commit 2c4ea1a675
13 changed files with 255 additions and 89 deletions

View File

@ -1,7 +1,11 @@
package com.greenorange.promotion.aop;
import cn.hutool.core.date.DateUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.greenorange.promotion.annotation.RequiresPermission;
import com.greenorange.promotion.common.ErrorCode;
@ -9,9 +13,11 @@ import com.greenorange.promotion.exception.ThrowUtils;
import com.greenorange.promotion.model.entity.UserInfo;
import com.greenorange.promotion.model.enums.UserRoleEnum;
import com.greenorange.promotion.service.user.UserInfoService;
import com.greenorange.promotion.utils.JWTUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.User;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
@ -19,6 +25,7 @@ import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.Date;
import java.util.Objects;
@ -36,6 +43,9 @@ public class PermissionCheck {
private UserInfoService userInfoService;
@Resource
private JWTUtils jwtUtils;
/***
* 执行拦截
@ -50,16 +60,21 @@ public class PermissionCheck {
UserRoleEnum mustUserRoleEnum = UserRoleEnum.getEnumByValues(mustRole);
ThrowUtils.throwIf(mustUserRoleEnum == null, ErrorCode.NO_AUTH_ERROR);
// 获取用户权限
String token = request.getHeader("token");
ThrowUtils.throwIf(StringUtils.isBlank(token), ErrorCode.NOT_LOGIN_ERROR);
String id = null;
try {
id = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException jwtDecodeException) {
log.info("JWT已失效");
}
UserInfo userInfo = userInfoService.getById(id);
ThrowUtils.throwIf(userInfo == null, ErrorCode.OPERATION_ERROR);
String token = request.getHeader("Authorization");
ThrowUtils.throwIf(StringUtils.isBlank(token), ErrorCode.NO_AUTH_ERROR, "JWT为空");
// 解析token
DecodedJWT decodedJWT = jwtUtils.verify(token);
String userAccount = decodedJWT.getClaim("userAccount").asString();
String userPassword = decodedJWT.getClaim("userPassword").asString();
// 打印token的过期时间
Date expiresAt = decodedJWT.getExpiresAt();
String formatExpiresAt = DateUtil.format(expiresAt, "yyyy-MM-dd HH:mm:ss");
log.info("Token过期时间为:" + formatExpiresAt);
LambdaQueryWrapper<UserInfo> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(UserInfo::getUserAccount, userAccount).eq(UserInfo::getUserPassword, userPassword);
UserInfo userInfo = userInfoService.getOne(lambdaQueryWrapper);
ThrowUtils.throwIf(userInfo == null, ErrorCode.OPERATION_ERROR, "用户不存在");
// 获取用户权限的枚举类
String userRole = userInfo.getUserRole();
UserRoleEnum userRoleEnum = UserRoleEnum.getEnumByValues(userRole);

View File

@ -0,0 +1,29 @@
package com.greenorange.promotion.config;
import com.google.common.net.HttpHeaders;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.customizers.GlobalOperationCustomizer;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import java.util.List;
/**
* @author: cxz
* @description 为每个接口添加鉴权
*/
@Slf4j
@Component
public class Knife4jOperationCustomizer implements GlobalOperationCustomizer {
@Override
public Operation customize(Operation operation, HandlerMethod handlerMethod) {
List<SecurityRequirement> security = operation.getSecurity();
if (security == null) {
security = List.of(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION));
operation.setSecurity(security);
}
return operation;
}
}

View File

@ -1,31 +1,39 @@
package com.greenorange.promotion.controller.user;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.google.common.net.HttpHeaders;
import com.greenorange.promotion.annotation.RequiresPermission;
import com.greenorange.promotion.common.BaseResponse;
import com.greenorange.promotion.common.ErrorCode;
import com.greenorange.promotion.common.ResultUtils;
import com.greenorange.promotion.constant.UserConstant;
import com.greenorange.promotion.exception.BusinessException;
import com.greenorange.promotion.exception.ThrowUtils;
import com.greenorange.promotion.model.dto.CommonBatchRequest;
import com.greenorange.promotion.model.dto.CommonRequest;
import com.greenorange.promotion.model.dto.user.UserInfoAddRequest;
import com.greenorange.promotion.model.dto.user.UserInfoLoginRequest;
import com.greenorange.promotion.model.dto.user.UserInfoQueryRequest;
import com.greenorange.promotion.model.dto.user.UserInfoUpdateRequest;
import com.greenorange.promotion.model.entity.UserInfo;
import com.greenorange.promotion.model.vo.user.UserInfoVO;
import com.greenorange.promotion.service.common.CommonService;
import com.greenorange.promotion.service.user.UserInfoService;
import com.greenorange.promotion.utils.JWTUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
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;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 用户表 控制器
@ -40,25 +48,55 @@ public class UserInfoController {
@Resource
private UserInfoService userInfoService;
@Resource
private CommonService commonService;
@Resource
private RedisTemplate<String, String> redisTemplate;
@Resource
private JWTUtils jwtUtils;
/**
* web端管理员登录
* @param userInfoLoginRequest 用户登录请求体
* @return 是否登录成功
*/
@PostMapping("login")
@Operation(summary = "web端管理员登录", description = "参数用户登录请求体权限管理员boss, admin)方法名userInfoLogin")
public BaseResponse<String> userInfoLogin(@RequestBody UserInfoLoginRequest userInfoLoginRequest, HttpServletRequest request) {
String userAccount = userInfoLoginRequest.getUserAccount();
String userPassword = userInfoLoginRequest.getUserPassword();
ThrowUtils.throwIf(StringUtils.isAnyBlank(userAccount, userPassword), ErrorCode.PARAMS_ERROR);
String token = userInfoService.userInfoLogin(userAccount, userPassword, request);
return ResultUtils.success(token);
}
/**
* web端管理员退出登录(用户退出时将 token 加入 Redis 黑名单)
* @return 是否退出登录成功
*/
@PostMapping("logout")
@Operation(summary = "web端管理员退出登录", description = "参数JWT权限管理员boss, admin)方法名userInfoLogout")
@RequiresPermission(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Boolean> userInfoLogout(@RequestHeader("Authorization") String token) {
// 获取 token 的过期时间
DecodedJWT decodedJWT = jwtUtils.verify(token);
long expirationTime = decodedJWT.getExpiresAt().getTime() - System.currentTimeMillis();
// token 存入 Redis 黑名单并设置过期时间与 token 一致
redisTemplate.opsForValue().set(token, token, expirationTime, TimeUnit.MILLISECONDS);
return ResultUtils.success(true);
}
// /**
// * web端管理员登录
// * @param userInfoAddRequest 用户表添加请求体
// * @return 是否添加成功
// */
// @PostMapping("add")
// @Operation(summary = "web端管理员添加用户表", description = "参数用户表添加请求体权限管理员boss, admin)方法名addUserInfo")
// @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
// public BaseResponse<Boolean> addUserInfo(@RequestBody UserInfoAddRequest userInfoAddRequest) {
// ThrowUtils.throwIf(userInfoAddRequest == null, ErrorCode.PARAMS_ERROR);
// UserInfo userInfo = commonService.copyProperties(userInfoAddRequest, UserInfo.class);
// userInfoService.save(userInfo);
// return ResultUtils.success(true);
// }
@ -76,6 +114,7 @@ public class UserInfoController {
return ResultUtils.success(true);
}
/**
* web端管理员更新用户表
* @param userInfoUpdateRequest 用户表更新请求体
@ -135,6 +174,7 @@ public class UserInfoController {
*/
@PostMapping("queryById")
@Operation(summary = "web端管理员根据id查询用户表", description = "参数用户表查询请求体权限管理员boss, admin),方法名:queryUserInfoById")
@RequiresPermission(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<UserInfoVO> queryUserInfoById(@RequestBody CommonRequest commonRequest) {
ThrowUtils.throwIf(commonRequest == null || commonRequest.getId() <= 0, ErrorCode.PARAMS_ERROR);
Long id = commonRequest.getId();

View File

@ -6,6 +6,7 @@ import com.greenorange.promotion.common.ResultUtils;
import io.swagger.v3.oas.annotations.Hidden;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@ -23,14 +24,14 @@ public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public BaseResponse<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
log.error("MethodArgumentNotValidException", e);
return ResultUtils.error(ErrorCode.PARAMS_ERROR, "参数验证失败");
return ResultUtils.error(ErrorCode.PARAMS_ERROR, e.getMessage());
}
// 处理消息体解析失败的异常
@ExceptionHandler(HttpMessageNotReadableException.class)
public BaseResponse<?> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
log.error("HttpMessageNotReadableException", e);
return ResultUtils.error(ErrorCode.PARAMS_ERROR, "请求参数不正确");
return ResultUtils.error(ErrorCode.PARAMS_ERROR, e.getMessage());
}

View File

@ -3,19 +3,23 @@ package com.greenorange.promotion.model.dto.user;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serial;
import java.io.Serializable;
/**
* 用户添加请求体
* 用户添加请求体
*/
@Data
@Schema(description = "用户表添加请求体", requiredProperties = {"name", "categoryId", "price", "image", "period", "isShelves"})
@Schema(description = "用户表添加请求体", requiredProperties = {"nickName", "userAvatar", "phoneNumber",
"userAccount", "userPassword", "invitationCode", "userRole", "parentUserId", "superUserList"})
public class UserInfoAddRequest implements Serializable {
/**
* 用户昵称
*/
@NotBlank(message = "参数不能为空")
@Schema(description = "用户昵称", example = "${field.example}")
private String nickName;
@ -28,13 +32,22 @@ public class UserInfoAddRequest implements Serializable {
/**
* 手机号
*/
@NotBlank(message = "参数不能为空")
@Schema(description = "手机号", example = "${field.example}")
private String phoneNumber;
/**
* 密码建议加密存储
* 账号
*/
@Schema(description = "密码(建议加密存储)", example = "${field.example}")
@NotBlank(message = "参数不能为空")
@Schema(description = "账号", example = "${field.example}")
private String userAccount;
/**
* 密码
*/
@NotBlank(message = "参数不能为空")
@Schema(description = "密码", example = "${field.example}")
private String userPassword;
/**
@ -46,6 +59,7 @@ public class UserInfoAddRequest implements Serializable {
/**
* 用户角色
*/
@NotBlank(message = "参数不能为空")
@Schema(description = "用户角色", example = "${field.example}")
private String userRole;

View File

@ -0,0 +1,27 @@
package com.greenorange.promotion.model.dto.user;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 用户登录请求体
*/
@Data
@Schema(description = "用户登录请求体", requiredProperties = {"userAccount", "userPassword"})
public class UserInfoLoginRequest implements Serializable {
/**
* 账号
*/
@Schema(description = "账号", example = "${field.example}")
private String userAccount;
/**
* 密码
*/
@Schema(description = "密码", example = "${field.example}")
private String userPassword;
}

View File

@ -7,7 +7,7 @@ import java.io.Serializable;
import com.greenorange.promotion.common.PageRequest;
/**
* 用户查询请求体继承自分页请求 PageRequest
* 用户查询请求体继承自分页请求 PageRequest
*/
@Data
@Schema(description = "用户表查询请求体", requiredProperties = {"current", "pageSize"})
@ -19,54 +19,12 @@ public class UserInfoQueryRequest extends PageRequest implements Serializable {
@Schema(description = "用户表 ID", example = "1")
private Long id;
/**
* 用户昵称
*/
@Schema(description = "用户昵称", example = "${field.example}")
private String nickName;
/**
* 用户头像URL
*/
@Schema(description = "用户头像URL", example = "${field.example}")
private String userAvatar;
/**
* 手机号
*/
@Schema(description = "手机号", example = "${field.example}")
private String phoneNumber;
/**
* 密码建议加密存储
*/
@Schema(description = "密码(建议加密存储)", example = "${field.example}")
private String userPassword;
/**
* 邀请码
*/
@Schema(description = "邀请码", example = "${field.example}")
private String invitationCode;
/**
* 用户角色
*/
@Schema(description = "用户角色", example = "${field.example}")
private String userRole;
/**
* 上级用户id
*/
@Schema(description = "上级用户id", example = "${field.example}")
private Long parentUserId;
/**
* 上级用户列表1,2,3
*/
@Schema(description = "上级用户列表1,2,3", example = "${field.example}")
private String superUserList;
@Serial
private static final long serialVersionUID = 1L;

View File

@ -7,10 +7,11 @@ import java.io.Serial;
import java.io.Serializable;
/**
* 用户更新请求体
* 用户更新请求体
*/
@Data
@Schema(description = "用户表更新请求体", requiredProperties = {"id", "name", "categoryId", "price", "image", "period", "isShelves"})
@Schema(description = "用户表更新请求体", requiredProperties = {"id", "nickName", "userAvatar", "phoneNumber",
"userAccount", "userPassword", "invitationCode", "userRole", "parentUserId", "superUserList"})
public class UserInfoUpdateRequest implements Serializable {
/**
@ -38,9 +39,15 @@ public class UserInfoUpdateRequest implements Serializable {
private String phoneNumber;
/**
* 密码建议加密存储
* 账号
*/
@Schema(description = "密码(建议加密存储)", example = "${field.example}")
@Schema(description = "账号", example = "${field.example}")
private String userAccount;
/**
* 密码
*/
@Schema(description = "密码", example = "${field.example}")
private String userPassword;
/**

View File

@ -37,7 +37,12 @@ public class UserInfo implements Serializable {
private String phoneNumber;
/**
* 密码建议加密存储
* 账号
*/
private String userAccount;
/**
* 密码
*/
private String userPassword;

View File

@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.greenorange.promotion.model.dto.user.UserInfoQueryRequest;
import com.greenorange.promotion.model.entity.UserInfo;
import com.baomidou.mybatisplus.extension.service.IService;
import jakarta.servlet.http.HttpServletRequest;
/**
* @author 35880
@ -17,4 +18,10 @@ public interface UserInfoService extends IService<UserInfo> {
* 获取查询条件
*/
QueryWrapper<UserInfo> getQueryWrapper(UserInfoQueryRequest userInfoQueryRequest);
/**
* web端用户登录
*/
String userInfoLogin(String userAccount, String userPassword, HttpServletRequest request);
}

View File

@ -1,13 +1,27 @@
package com.greenorange.promotion.service.user.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.greenorange.promotion.annotation.RequiresPermission;
import com.greenorange.promotion.common.ErrorCode;
import com.greenorange.promotion.constant.CommonConstant;
import com.greenorange.promotion.exception.ThrowUtils;
import com.greenorange.promotion.mapper.UserInfoMapper;
import com.greenorange.promotion.model.dto.user.UserInfoQueryRequest;
import com.greenorange.promotion.model.entity.UserInfo;
import com.greenorange.promotion.service.user.UserInfoService;
import com.greenorange.promotion.utils.JWTUtils;
import com.greenorange.promotion.utils.SqlUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* @author 35880
* @description 针对表user_info(用户基本信息表)的数据库操作Service实现
@ -17,12 +31,41 @@ import org.springframework.stereotype.Service;
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo>
implements UserInfoService{
@Resource
private JWTUtils jwtUtils;
/**
* 获取查询条件
*/
@Override
public QueryWrapper<UserInfo> getQueryWrapper(UserInfoQueryRequest userInfoQueryRequest) {
return null;
Long id = userInfoQueryRequest.getId();
String phoneNumber = userInfoQueryRequest.getPhoneNumber();
String sortField = userInfoQueryRequest.getSortField();
String sortOrder = userInfoQueryRequest.getSortOrder();
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(id != null, "id", id);
queryWrapper.eq(StringUtils.isNotBlank(phoneNumber), "phoneNumber", phoneNumber);
queryWrapper.orderBy(SqlUtils.validSortField(sortField), sortOrder.equals(CommonConstant.SORT_ORDER_ASC), sortField);
return queryWrapper;
}
/**
* web端用户登录
*/
@Override
public String userInfoLogin(String userAccount, String userPassword, HttpServletRequest request) {
ThrowUtils.throwIf(userAccount.length() < 6 || userPassword.length() < 6, ErrorCode.PARAMS_ERROR);
LambdaQueryWrapper<UserInfo> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(UserInfo::getUserAccount, userAccount).eq(UserInfo::getUserPassword, userPassword);
UserInfo userInfo = this.getOne(lambdaQueryWrapper);
ThrowUtils.throwIf(userInfo == null, ErrorCode.OPERATION_ERROR, "用户不存在");
Map<String, String> payload = new HashMap<>();
payload.put("userAccount", userAccount);
payload.put("userPassword", userPassword);
return jwtUtils.generateToken(payload);
}
}

View File

@ -4,18 +4,32 @@ import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.greenorange.promotion.common.ErrorCode;
import com.greenorange.promotion.exception.ThrowUtils;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Calendar;
import java.util.Map;
@Component
public class JWTUtils {
/**
* 生成token header.payload.signature
*/
private static final String SECRET = "qingcheng";
public static String getToken(Map<String, String> map) {
@Resource
private RedisTemplate<String, String> redisTemplate;
/**
* 生成JWT token
*/
public String generateToken(Map<String, String> map) {
Calendar instance = Calendar.getInstance();
// 默认7天过期
@ -26,14 +40,19 @@ public class JWTUtils {
// payload
map.forEach(builder::withClaim);
return builder.withExpiresAt(instance.getTime()) //指定令牌过期时间
.sign(Algorithm.HMAC256(SECRET));
}
/**
* 验证token 合法性
* 解析JWT token
*/
public static DecodedJWT verify(String token) {
public DecodedJWT verify(String token) {
// 检查 token 是否在黑名单中
String tokenFromBlacklist = redisTemplate.opsForValue().get(token);
ThrowUtils.throwIf(tokenFromBlacklist != null, ErrorCode.NO_AUTH_ERROR, "JWT 已被注销");
return JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
}
}

View File

@ -8,6 +8,7 @@ import com.auth0.jwt.interfaces.JWTVerifier;
import org.junit.jupiter.api.Test;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
public class JwtDemo {