目录
附加:本项目的抽奖系统测试计划测试计划(抽奖系统)-CSDN博客
人员模块
一、注册
1.1敏感字段加密
用户在注册时,需要填写账号以及密码等敏感信息,不应该以明文的方式出现,服务器存储的信息是已经加密好的
本次项目针对密码使用加盐哈希法(常见)
- 用户注册时,给他随机生成一段字符串,这段字符串就是盐(Salt)
- 把用户注册输入的密码和盐拼接在一起,叫做加盐密码
- 对加盐密码进行哈希,并把结果和盐都储存起来
针对手机号使用Java工具类库 Hutool ,对文件、流、加密解密、转码、正则、线程、XML 等 JDK 方法进行了封装,开箱即用!
引入jar包:
Maven 仓库地址:https://mvnrepository.com/artifact/cn.hutool
HuTool 官网地址:https://hutool.cn/
<!-- hutool java 工具大全 -->
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.25</version>
</dependency>
1.2用户注册时序图:
1.3注册 Controller 层接口设计:
package com.example.lotterysystem.controller;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@RestController
public class UserController {
//在那个对象类中使用就传那个class
private final static Logger logger = LoggerFactory.getLogger(UserController.class);
@Autowired
private UserService userService;
/**
* 用户注册
*
* @param param
* @return
*/
@RequestMapping("/register")
public CommonResult<UserRegisterResult> userRegister(@Validated @RequestBody UserRegisterParam param) {
/**
* 日志打印
* 打印请求参数(param)相关的内容,有利于后续异常出现的排查
* 这里打印一个 info 级别的日志,展示了哪个方法(userRegister),哪个参数类型(UserRegisterParam)
* 以及正文内容,使用 {} 占位符表示,由于 param 是 json 类型,所以先将其转为了 String 类型
*/
logger.info("userRegister UserRegisterParam:{}", JacksonUtil.writeValueAsString(param));
// 调用 Service 层服务进行访问
UserRegisterDTO userRegisterDTO = userService.register(param);
return CommonResult.success(converToUserRegisterResult(userRegisterDTO));
}
}
1.4接口参数UserRegisterParam:
package com.example.lotterysystem.controller.param;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.NonNull;
import org.springframework.stereotype.Controller;
import org.springframework.validation.annotation.Validated;
import java.io.Serializable;
@Data
public class UserRegisterParam implements Serializable {
/**
* 姓名
*/
@NotBlank(message = "姓名不能为空")
private String name;
/**
* 邮箱
*/
@NotBlank(message = "邮箱不能为空")
private String mail;
/**
* 手机号
*/
@NotBlank(message = "手机号不能为空")
private String phoneNumber;
/**
* 密码
*/
//普通用户不能输入密码
private String password;
/**
* 身份信息
**/
@NotBlank(message = "身份信息不能为空")
private String identity;
}
1.5用户类型枚举UserIdentityEnum
package com.example.lotterysystem.service.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 用户身份枚举
*/
@Getter
//@AllArgsConstructor
public enum UserIdentityEnum {
ADMIN("管理员"),
NORMAL("普通用户");
private final String message;
UserIdentityEnum(String message) {
this.message = message;
}
/**
* 根据name获取枚举
* @param name
* @return
*/
public static UserIdentityEnum forName(String name) {
for (UserIdentityEnum userIdentityEnum : UserIdentityEnum.values()) {
if (userIdentityEnum.name().equalsIgnoreCase(name)) {
return userIdentityEnum;
}
}
return null;
}
public String getMessage() {
return message;
}
}
1.6返回结果UserRegisterResult:
@Data
public class UserLoginResult implements Serializable {
/**
* JWT 令牌
*/
private String token;
/**
* 登录人员信息
*/
private String identity;
}
1.7Validation注解
对于controller接口入参字段的验证,可以使用SpringBoot中集成的Validation来完成。例如可以
看到我们在接口入参上加入了@Validated注解,并且param对象中的每个成员都使用
@NotBlank注解来检查参数不能为空。使用需引l入依赖:
<!-- spring-boot 2.3及以上的版本只需要引⼊下⾯的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
1.8Service层接口设计:
为什么进行接口分离设计?接口与实现的分离是Java编程中推崇的一种设计哲学,它有助于创建更加灵活、可维护和可扩展的软件系统
- 抽象与具体实现分离:接口定义了一组操作的契约,而实现则提供了这些操作的具体行为。这种分离允许改变具体实现而不影响使用接口的客户端代码。
- 支持多态性:接口允许通过共同的接口来引用不同的实现,这是多态性的基础,使得代码更加灵活和通用。
- 提高代码的可读性和可理解性:接口提供了清晰的API视图,使得其他开发者能够更容易地理解和使用这些API。
- 安全性:接口可以隐藏实现细节,只暴露必要的操作,这有助于保护系统的内部状态和实现不被外部直接访问
- 遵循开闭原则:软件实体应当对扩展开放,对修改封闭。接口与实现的分离使得在不修改客户端代码的情况下扩展系统的功能。
- 促进面向对象的设计:接口与实现的分离鼓励开发者进行面向对象的设计,考虑如何将系统分解为可重用和可组合的组件。
public interface UserService {
/**
* 用户 注册
* @param request
* @return
*/
UserRegisterDTO register(UserRegisterParam request);
}
1.9注册接口实现UserServiceImpl :
package com.example.lotterysystem.service.impl;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
public class UserServiceImpl implements UserService {
private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);
@Autowired
private UserMapper userMapper;
/**
* 注册用户
*
* @param param
* @return
*/
@Override
public UserRegisterDTO register(UserRegisterParam param) {
//校验注册信息
checkRegisterInfo(param);
//加密私密数据(构造 dao 层请求)
UserDO userDO = new UserDO();
userDO.setUserName(param.getName());
userDO.setEmail(param.getMail());
userDO.setPhoneNumber(new Encrypt(param.getPhoneNumber())); //使用AES 方式加密手机号
userDO.setIdentity(param.getIdentity());
if(StringUtils.hasText(param.getPassword())){
//如果密码不为空,说明是管理员,使用 sha256 加密
userDO.setPassword(DigestUtil.sha256Hex(param.getPassword()));
}
//保存数据
userMapper.insert(userDO);
//构造返回
//mock 数据
UserRegisterDTO userRegisterDTO = new UserRegisterDTO();
userRegisterDTO.setUserId(userDO.getId());
return userRegisterDTO;
}
/**
* 校验注册信息
*
* @param param
*/
public void checkRegisterInfo(UserRegisterParam param) {
// 判空
if (null == param) {
throw new ServiceException(ServiceErrorCodeConstants.REGISTER_INFO_IS_EMPTY);
}
// 校验邮箱格式
if (!RegexUtil.checkMail(param.getMail())) {
throw new ServiceException(ServiceErrorCodeConstants.MAIL_ERROR);
}
// 校验手机号格式
if (!RegexUtil.checkMobile(param.getPhoneNumber())) {
throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);
}
// 校验身份信息
if (null == UserIdentityEnum.forName(param.getIdentity())) {
throw new ServiceException(ServiceErrorCodeConstants.IDENTITY_ERROR);
}
// 校验管理员密码是否填写(必填)
if (param.getIdentity().equalsIgnoreCase(UserIdentityEnum.ADMIN.name()) && !StringUtils.hasText(param.getPassword())) {
throw new ServiceException(ServiceErrorCodeConstants.PASSWORD_IS_EMPTY);
}
// 密码校验,至少 6 位
if (StringUtils.hasText(param.getPassword()) && !RegexUtil.checkPassword(param.getPassword())) {
throw new ServiceException(ServiceErrorCodeConstants.PASSWORD_ERROR);
}
// 校验邮件是否被使用
if (checkMailUsed(param.getMail())) {
throw new ServiceException(ServiceErrorCodeConstants.MAIL_USED);
}
// 校验手机号是否被使用
if (checkPhoneNumberUsed(param.getPhoneNumber())) {
throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_USED);
}
}
/**
* 校验手机号是否被使用
*
* @param phoneNumber
* @return
*/
private boolean checkPhoneNumberUsed(@NotBlank(message = "电话不能为空!") String phoneNumber) {
int count = userMapper.countByPhone(new Encrypt(phoneNumber));
return count > 0;
}
/**
* 校验邮箱是否被使用
*
* @param mail
* @return
*/
private boolean checkMailUsed(@NotBlank(message = "邮箱不能为空!") String mail) {
// 需要去数据库中检查该邮箱是否已被使用
int count = userMapper.countByMail(mail);
return count > 0;
}
/**
* 登录
*
* @param param
* @return
*/
@Override
public UserLoginDTO login(UserLoginParam param) {
UserLoginDTO userLoginDTO ;
// 类型检查与类型转换
// Java 14 版本及以上可用以下一行代码完成上述过程
if(param instanceof UserPasswordLoginParam loginParam){
// 密码登录流程
userLoginDTO = loginByUserPassword(loginParam);
}else {
throw new ServiceException(ServiceErrorCodeConstants.LOGIN_INFO_NOT_EXIST);
}
return userLoginDTO;
}
/**
* 根据用户身份查找用户信息
*
* @param identity 用户身份枚举,用于筛选用户列表如果为null,则不进行筛选
* @return 返回一个UserDTO列表,包含符合身份条件的用户信息
*/
@Override
public List<UserDTO> findUserInfo(UserIdentityEnum identity) {
// 将枚举转换为字符串,以便在数据库查询中使用
String identityString = null == identity ? null : identity.name();
// 查表
List<UserDO> userDOList = userMapper.findUserInfo(identityString);
// 将查询结果转换为DTO列表
List<UserDTO> userDTOList = userDOList.stream()
.map(userDO -> {
UserDTO userDTO = new UserDTO();
userDTO.setUserId(userDO.getId());
userDTO.setUserName(userDO.getUserName());
userDTO.setEmail(userDO.getEmail());
userDTO.setPhoneNumber(userDO.getPhoneNumber().getValue());
userDTO.setIdentity(UserIdentityEnum.forName(userDO.getIdentity()));
return userDTO;
}).collect(Collectors.toList());
return userDTOList;
}
/**
* 密码登录
* @param loginParam
* @return
*/
private UserLoginDTO loginByUserPassword(UserPasswordLoginParam loginParam) {
// 初始化用户对象
UserDO userDO = null;
// 检查登录名是否为有效的邮箱格式
if (RegexUtil.checkMail(loginParam.getLoginName())) {
// 邮箱登录
// 根据邮箱查询用户表
userDO = userMapper.selectByMail(loginParam.getLoginName());
// 检查登录名是否为有效的手机号格式
}else if (RegexUtil.checkMobile(loginParam.getLoginName())) {
// 手机号登录
// 根据手机号查询用户表
userDO = userMapper.selectByPhoneNumber(new Encrypt(loginParam.getLoginName()));
} else {
// 如果登录名既不是有效的邮箱也不是有效的手机号,抛出服务异常,提示登录不存在
throw new ServiceException(ServiceErrorCodeConstants.LOGIN_NOT_EXIST);
}
// 校验登录信息
if (null == userDO) {
// 如果用户信息为空,则抛出异常
throw new ServiceException(ServiceErrorCodeConstants.USER_INFO_IS_EMPTY);
} else if (StringUtils.hasText(loginParam.getMandatoryIdentity())
// 如果登录参数中的身份信息与用户信息中的身份信息不匹配,则抛出异常
&& !loginParam.getMandatoryIdentity().equalsIgnoreCase(userDO.getIdentity())) {
throw new ServiceException(ServiceErrorCodeConstants.IDENTITY_ERROR);
} else if (!DigestUtil.sha256Hex(loginParam.getPassword()).equals(userDO.getPassword())) {
// 如果密码经过SHA-256加密后的结果与数据库中的密码不匹配,则抛出异常
throw new ServiceException(ServiceErrorCodeConstants.PASSWORD_ERROR);
}
// 创建一个映射,用于存储用户登录后的信息
Map<String, Object> claim = new HashMap<>();
claim.put("userId", userDO.getId());
claim.put("Identity", userDO.getIdentity());
// 生成JWT令牌
String token = JwtUtil.genJwtToken(claim);
// 创建用户登录信息对象,并设置相关属性
UserLoginDTO userLoginDTO = new UserLoginDTO();
userLoginDTO.setToken(token);
userLoginDTO.setIdentity(UserIdentityEnum.forName(userDO.getIdentity()));
// 返回用户登录信息对象
return userLoginDTO;
}
}
其中校验用户信息,例如邮箱、电话、密码格式的内容,我们封装成了一个 util 来完成:
校验信息RegexUtil
package com.example.lotterysystem.common.utils;
import org.springframework.util.StringUtils;
import java.util.regex.Pattern;
/**
* 正则工具类
* @author
* @date 2018年5月23日
*/
public class RegexUtil {
//验证邮箱
public static boolean checkMail(String content){
if(!StringUtils.hasText(content)){
return false;
}
/**
* ^ 表示匹配字符串的开始。
* [a-z0-9]+ 表示匹配一个或多个小写字母或数字。
* ([._\\-]*[a-z0-9])* 表示匹配零次或多次下述模式:一个点、下划线、反斜杠或短横线,后面跟着一个或多个小写字母或数字。这部分是可选的,并且可以重复出现。
* @ 字符字面量,表示电子邮件地址中必须包含的"@"符号。
* ([a-z0-9]+[-a-z0-9]*[a-z0-9]+.) 表示匹配一个或多个小写字母或数字,后面可以跟着零个或多个短横线或小写字母和数字,然后是一个小写字母或数字,最后是一个点。这是匹配域名的一部分。
* {1,63} 表示前面的模式重复1到63次,这是对顶级域名长度的限制。
* [a-z0-9]+ 表示匹配一个或多个小写字母或数字,这是顶级域名的开始部分。
* $ 表示匹配字符串的结束。
*/
String regex = "^[a-z0-9]+([._\\\\-]*[a-z0-9])*@([a-z0-9]+[-a-z0-9]*[a-z0-9]+.){1,63}[a-z0-9]+$";
return Pattern.matches(regex, content);
}
/**
* 手机号码以1开头的11位数字
*
* @param content
* @return
*/
public static boolean checkMobile(String content) {
if (!StringUtils.hasText(content)) {
return false;
}
/**
* ^ 表示匹配字符串的开始。
* 1 表示手机号码以数字1开头。
* [3|4|5|6|7|8|9] 表示接下来的数字是3到9之间的任意一个数字。这是中国大陆手机号码的第二位数字,通常用来区分不同的运营商。
* [0-9]{9} 表示后面跟着9个0到9之间的任意数字,这代表手机号码的剩余部分。
* $ 表示匹配字符串的结束。
*/
String regex = "^1[3|4|5|6|7|8|9][0-9]{9}$";
return Pattern.matches(regex, content);
}
/**
* 密码强度正则,6到12位
*
* @param content
* @return
*/
public static boolean checkPassword(String content){
if (!StringUtils.hasText(content)) {
return false;
}
/**
* ^ 表示匹配字符串的开始。
* [0-9A-Za-z] 表示匹配的字符可以是:
* 0-9:任意一个数字(0到9)。
* A-Z:任意一个大写字母(从A到Z)。
* a-z:任意一个小写字母(从a到z)。
* {6,12} 表示前面的字符集合(数字、大写字母和小写字母)可以重复出现6到12次。
* $ 表示匹配字符串的结束。
*/
String regex= "^[0-9A-Za-z]{6,12}$";
return Pattern.matches(regex, content);
}
}
控制层通用异常处理
spring boot中使用 @RestControllerAdvice 注解,完成优雅的全局异常处理类,可以针对所有异常类型先进行通用处理后,再对特定异常类型进行不同的处理操作。
使用@RestControllerAdvice+@ExceptionHandler注解
package com.example.lotterysystem.controller.handler;
import com.example.lotterysystem.common.errorcode.GlobalErrorCodeConstants;
import com.example.lotterysystem.common.exception.ControllerException;
import com.example.lotterysystem.common.exception.ServiceException;
import com.example.lotterysystem.common.pojo.CommonResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理
*/
@RestControllerAdvice // 可以捕获全局异常
public class GlobalExceptionHandler {
private final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(value = ServiceException.class)
public CommonResult<?> serviceException(ServiceException e) {
//打印错误日志
logger.error("serviceException: ", e);
//构造器错误
return CommonResult.error(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode(), e.getMessage());
}
@ExceptionHandler(value = ControllerException.class)
public CommonResult<?> controllerException(ControllerException e) {
//打印错误日志
logger.error("controllerException: ", e);
//构造器错误
return CommonResult.error(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode(), e.getMessage());
}
@ExceptionHandler(value = Exception.class)
public CommonResult<?> exception(Exception e) {
//打印错误日志
logger.error("服务异常: ", e);
//构造器错误
return CommonResult.error(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode(), e.getMessage());
}
}
验证注册接口:
二、登陆
2.1用户登陆时序图:
2.2redis的使用
redis该项目中主要是缓存活动详情信息和中奖记录,提高了数据的访问速度,减轻了数据库的压力。同时,在数据发生变化时,会及时更新缓存,保证了数据的一致性。相对于从磁盘和数据库中访问数据,从内存中进行访问速度会很快;redis可以部署在集群环境中,是将数据存放在内存中;
redis的优点:
- redis将数据放在内存中,可以提高数据访问的速度
- redis是有c语言实现的,距离操作系统比较近
- redis使用的是单线程,可以避免锁竞争
1️⃣导入redis依赖,配置端口转发,使用端口转发的方式,直接把服务器的redis端口映射到本地,这样本地就能访问redis
配置redis相关项:
## redis spring boot 3.x ##
spring.data.redis.host=localhost
spring.data.redis.port=6379
# 连接空闲超过N(s秒、ms毫秒)后关闭,0为禁⽤,这⾥配置值和tcp-keepalive值⼀致
spring.data.redis.timeout=60s
# 默认使⽤ lettuce 连接池
# 允许最⼤连接数,默认8(负值表⽰没有限制)
spring.data.redis.lettuce.pool.max-active=8
# 最⼤空闲连接数,默认8
spring.data.redis.lettuce.pool.max-idle=8
# 最⼩空闲连接数,默认0
spring.data.redis.lettuce.pool.min-idle=0
# 连接⽤完时,新的请求等待时间(s秒、ms毫秒),超过该时间抛出异常JedisConnectionException,(默认-1,负值表⽰没有限制)
spring.data.redis.lettuce.pool.max-wait=5s
对redis中关于stringredistemplate进行工具类封装,后续对于redis的操作通过redisutil工具类来完成:下面代码是对于redis中数据的增加,删除,查找的实现:
package com.example.lotterysystem.common.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
@Configuration
public class RedisUtil {
private static final Logger logger = LoggerFactory.getLogger(RedisUtil.class);
@Autowired
private StringRedisTemplate stringRedisTemplate;
//--------------------String-------------------
/**
* 设置值
* @param key
* @param value
* @return
*/
boolean set(String key, String value){
try {
stringRedisTemplate.opsForValue().set(key,value);
return true;
} catch (Exception e) {
logger.error("RedisUtil error ,set{}, {}",key,value,e );
return false;
}
}
/**
* 设置值(过期时间)
* @param key
* @param value
* @param time
* @return
*/
public boolean set(String key, String value, Long time){
try {
stringRedisTemplate.opsForValue().set(key,value,time, TimeUnit.SECONDS);
return true;
} catch (Exception e) {
logger.error("RedisUtil error ,set{}, {},{}",key,value,time,e );
return false;
}
}
/**
* 获取
* @param key
* @return
*/
public String get(String key){
try {
return StringUtils.hasText(key)?
stringRedisTemplate.opsForValue().get(key) : null;
}catch (Exception e){
logger.error("RedisUtil error ,get{}, {}",key,e );
return null;
}
}
/**
* 删除
* @param key
* @return
*/
public boolean del(String... key){
try{
if(null != key && key.length > 0){
if(key.length > 1){
stringRedisTemplate.delete(key[0]);
}else {
stringRedisTemplate.delete(
(Collection<String>) CollectionUtils.arrayToList(key)
);
}
}
return true;
}catch(Exception e){
logger.error("RedisUtil error ,del{}, {}",key,e );
return false;
}
}
/**
* 判断key是否存在
* @param key
* @return
*/
public boolean hasKey(String key){
try {
return StringUtils.hasText(key) ?
stringRedisTemplate.hasKey(key) : false;
}catch(Exception e){
logger.error("RedisUtil error ,haskey{}, {}",key,e );
return false;
}
}
}
2.3JWT
传统的登陆流程:
- 登陆页面把用户名密码提交给服务器,
- 服务器端验证用户名密码是否正确,并返回校验结果给后端
- 如果密码正确,则在服务器端创建Session.通过Cookie把sessionld返回给浏览器,
问题:集群环境下无法直接使用Session。
解决:令牌
JWT工具类:
package com.example.lotterysystem.common.utils;
import cn.hutool.jwt.JWTUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.JwtParserBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.util.StringUtils;
import javax.crypto.SecretKey;
import javax.crypto.SecretKey;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* 令牌实现
*/
@Slf4j
public class JwtUtil{
private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class);
//密钥
public static String secretStr =
"5QGoH4qLyxEw7vAccxo2KHg26iJztJvJlaT9MKTatqI=";
/**
* 生成安全密钥:将一个Base64编码的密钥解码并创建一个HMAC SHA密钥。
*/
private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(
Decoders.BASE64.decode(secretStr));
//过期时间(单位: 毫秒)
public static long JWT_EXPIRATION = 24*60*60*1000;//⼀天
/**
* 生成JWT令牌
*
* @param claim 包含要添加到JWT中的自定义声明或数据
* @return 返回生成的JWT令牌字符串
*/
public static String genJwtToken(Map<String,Object> claim){
// 开始构建JWT令牌
String token = Jwts.builder()
.setClaims(claim) // 设置JWT中的自定义声明或数据
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis()+JWT_EXPIRATION)) // 设置令牌的过期时间 当前毫秒加上设置的时间
.signWith(SECRET_KEY) // 使用密钥对令牌进行签名
.compact(); // 将令牌压缩为字符串形式
logger.error("生成JWT令牌成功:{}",token);
return token;
}
/**
* 校验token
* claims为 null 表示校验失败
*/
public static Claims parseToken(String token) {
// 判断token是否为空
if (!StringUtils.hasLength(token)) return null;
// 创建解析器, 设置签名密钥
JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder().setSigningKey(SECRET_KEY);
Claims claims = null;
try {
//解析token
claims = jwtParserBuilder.build().parseClaimsJws(token).getBody();
} catch (Exception e) {
logger.error("校验token失败:{}", e);
return null;
}
return claims;
}
/**
* 从token中获取用户ID
* @param token
* @return
*/
public static Integer getIdByToken(String token){
Claims claims = parseToken(token);
if(claims !=null){
Map<String,Object> userInfo = new HashMap<>(claims);
return (Integer) userInfo.get("userId");
}
return null;
}
}
2.4controller层登录接口设计:
/**
* 密码登录
* @param param
* @return
*/
@RequestMapping("/password/login")
public CommonResult<UserLoginResult> userPasswordLogin(@Validated @RequestBody UserPasswordLoginParam param) {
// 打印日志
logger.info("userPasswordLogin UserPasswordLoginParam:{}", JacksonUtil.writeValueAsString(param));
UserLoginDTO userLoginDTO = userService.login(param);
return CommonResult.success(converToUserLoginResult(userLoginDTO));
}
/**
* 将 UserLoginDTO 转换为 UserLoginResult 类型返回
* @param userLoginDTO
* @return
*/
private UserLoginResult converToUserLoginResult (UserLoginDTO userLoginDTO){
if(null == userLoginDTO){
throw new ControllerException(ControllerErrorCodeConstants.LOGIN_ERROR);
}
UserLoginResult result = new UserLoginResult();
result.setToken(userLoginDTO.getToken());
result.setIdentity(userLoginDTO.getIdentity().name());
return result;
}
2.5UserLoginParam用户登陆参数:
@Data
public class UserLoginParam implements Serializable {
/**
* 强制某身份登录,不填不限制身份
* @see UserIdentityEnum#name()
* 上面一句表示从 UserIdentityEnum 传入用户身份
*/
private String mandatoryIdentity;
}
@Data
@EqualsAndHashCode(callSuper = true)
public class UserPasswordLoginParam extends UserLoginParam{
// 手机或邮箱
@NotBlank(message = "手机或邮箱不能为空!")
private String loginName;
// 密码
@NotBlank(message = "密码不能为空!")
private String password;
}
2.6UserLoginResult用户登陆结果:
@Data
public class UserLoginResult implements Serializable {
/**
* JWT 令牌
*/
private String token;
/**
* 登录人员信息
*/
private String identity;
}
2.7Service层登录接口设计:
public interface UserService {
// 登录
UserLoginDTO login(UserLoginParam param);
}
2.8UserLoginDTO
@Data
public class UserLoginDTO {
/**
* JWT令牌
*/
private String token;
/**
* 登录人员信息身份
*/
private UserIdentityEnum identity;
}
2.9用户登陆接口实现UserServiceImpl:
/**
* 登录
*
* @param param
* @return
*/
@Override
public UserLoginDTO login(UserLoginParam param) {
UserLoginDTO userLoginDTO ;
// 类型检查与类型转换
// Java 14 版本及以上可用以下一行代码完成上述过程
if(param instanceof UserPasswordLoginParam loginParam){
// 密码登录流程
userLoginDTO = loginByUserPassword(loginParam);
}else {
throw new ServiceException(ServiceErrorCodeConstants.LOGIN_INFO_NOT_EXIST);
}
return userLoginDTO;
}
/**
* 根据用户身份查找用户信息
*
* @param identity 用户身份枚举,用于筛选用户列表如果为null,则不进行筛选
* @return 返回一个UserDTO列表,包含符合身份条件的用户信息
*/
@Override
public List<UserDTO> findUserInfo(UserIdentityEnum identity) {
// 将枚举转换为字符串,以便在数据库查询中使用
String identityString = null == identity ? null : identity.name();
// 查表
List<UserDO> userDOList = userMapper.findUserInfo(identityString);
// 将查询结果转换为DTO列表
List<UserDTO> userDTOList = userDOList.stream()
.map(userDO -> {
UserDTO userDTO = new UserDTO();
userDTO.setUserId(userDO.getId());
userDTO.setUserName(userDO.getUserName());
userDTO.setEmail(userDO.getEmail());
userDTO.setPhoneNumber(userDO.getPhoneNumber().getValue());
userDTO.setIdentity(UserIdentityEnum.forName(userDO.getIdentity()));
return userDTO;
}).collect(Collectors.toList());
return userDTOList;
}
/**
* 密码登录
* @param loginParam
* @return
*/
private UserLoginDTO loginByUserPassword(UserPasswordLoginParam loginParam) {
// 初始化用户对象
UserDO userDO = null;
// 检查登录名是否为有效的邮箱格式
if (RegexUtil.checkMail(loginParam.getLoginName())) {
// 邮箱登录
// 根据邮箱查询用户表
userDO = userMapper.selectByMail(loginParam.getLoginName());
// 检查登录名是否为有效的手机号格式
}else if (RegexUtil.checkMobile(loginParam.getLoginName())) {
// 手机号登录
// 根据手机号查询用户表
userDO = userMapper.selectByPhoneNumber(new Encrypt(loginParam.getLoginName()));
} else {
// 如果登录名既不是有效的邮箱也不是有效的手机号,抛出服务异常,提示登录不存在
throw new ServiceException(ServiceErrorCodeConstants.LOGIN_NOT_EXIST);
}
// 校验登录信息
if (null == userDO) {
// 如果用户信息为空,则抛出异常
throw new ServiceException(ServiceErrorCodeConstants.USER_INFO_IS_EMPTY);
} else if (StringUtils.hasText(loginParam.getMandatoryIdentity())
// 如果登录参数中的身份信息与用户信息中的身份信息不匹配,则抛出异常
&& !loginParam.getMandatoryIdentity().equalsIgnoreCase(userDO.getIdentity())) {
throw new ServiceException(ServiceErrorCodeConstants.IDENTITY_ERROR);
} else if (!DigestUtil.sha256Hex(loginParam.getPassword()).equals(userDO.getPassword())) {
// 如果密码经过SHA-256加密后的结果与数据库中的密码不匹配,则抛出异常
throw new ServiceException(ServiceErrorCodeConstants.PASSWORD_ERROR);
}
// 创建一个映射,用于存储用户登录后的信息
Map<String, Object> claim = new HashMap<>();
claim.put("userId", userDO.getId());
claim.put("Identity", userDO.getIdentity());
// 生成JWT令牌
String token = JwtUtil.genJwtToken(claim);
// 创建用户登录信息对象,并设置相关属性
UserLoginDTO userLoginDTO = new UserLoginDTO();
userLoginDTO.setToken(token);
userLoginDTO.setIdentity(UserIdentityEnum.forName(userDO.getIdentity()));
// 返回用户登录信息对象
return userLoginDTO;
}
强制登陆
配置拦截器实现强制登陆,当用户访问非登录注册页面时,例如抽奖页面,如果用户当前尚未登陆,我们希望动跳转到登陆页自面。
首次登陆成功,客户端会保存token,每当前端请求时,后端就会验证token,如果不存或者失效则会跳转到登陆界面
拦截器
采用拦截器来完成校验 token 的合法性
这篇博客实现了拦截器:抽奖系统(1)——功能模块-CSDN博客
测试登陆接口: