目录

一、登录功能
有两种登录,一种密码+电话号码,另一种验证码+电话号码。
现在短信服务是不支持个人使用了,我只在代码中模拟实现,验证码输出到控制台,如果服务支持的话,直接仿照阿里云的短信服务给出的在系统中引入的方式引入即可,实现自己的工具类,在下面输出控制台的时候转换成调用工具类即可。
时序图:
1.1 短信验证码获取
1.1.1 参数要求
| 参数名 | 描述 | 类型 | 默认值 | 条件 |
|---|---|---|---|---|
| phoneNumber | 用户电话号码 | String | 必须 |
1.1.2 接口规范
[请求] /verification-code/send?phoneNumber=13199999999 GET
[响应]
{
"code": 200,
"data": true,
"msg": ""
}
1.1.3 controller层
com/yj/lottery_system/controller/UserController.java 下实现:
- 直接调用服务端即可
@RequestMapping("/verification-code/send")
public CommonResult<Boolean> sendVerificationCode(String phoneNumber) {
//日志打印
log.info("sendVerificationCode phoneNumber: {}", phoneNumber);
//调用service服务
verificationCodeService.sendVerificationCode(phoneNumber);
return CommonResult.success(true);
}
1.1.3.1 测试
测试:
Postman传参
后端拿到验证码:
1.1.3 service 层
1.1.3.1 IVerificationCodeService 接口类
com/yj/lottery_system/service包下定义 IVerificationCodeService 接口类:
- 包含发送验证码方法
- 获取验证码方法
package com.yj.lottery_system.service;
public interface IVerificationCodeService {
/**
* 发送验证码
* @param phoneNumber
*/
void sendVerificationCode(String phoneNumber);
/**
* 从缓存中获取验证码
* @param phoneNumber
* @return
*/
String getVerificationCode(String phoneNumber);
}
1.1.3.2 VerificationCodeServiceImpl 实现接口
com/yj/lottery_system/service/impl 包下实现 VerificationCodeServiceImpl 类:
- 发送随机验证码的方法:
- 调用 RegexUtil 工具类 检验一下手机号
- 调用 CaptchaUtil 工具类(实现了Hutool 的随机验证码生成的工具类) 生成四位的随机验证码
- 调用 RedisUtil 工具类(实现使用Redis的工具类,在后面Redis配置里实现的)缓存60s的生命周期的验证码,为区分不同功能存入的键值对,我们要加上前缀。
- 获取验证码方法:
- RegexUtil 验证一下手机号,RedisUtil 获取即可。
package com.yj.lottery_system.service.impl;
import com.yj.lottery_system.common.errorcode.ServiceErrorCodeConstants;
import com.yj.lottery_system.common.exception.ServiceException;
import com.yj.lottery_system.common.utils.CaptchaUtil;
import com.yj.lottery_system.common.utils.RedisUtil;
import com.yj.lottery_system.common.utils.RegexUtil;
import com.yj.lottery_system.service.IVerificationCodeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class VerificationCodeServiceImpl implements IVerificationCodeService {
//为区分业务,key加上前缀 VERIFICATION_CODE_
private static final String VERIFICATION_CODE_PREFIX = "VERIFICATION_CODE_";
//过期时间
private static final Long VERIFICATION_CODE_TIMEOUT = 60L;
@Autowired
private RedisUtil redisUtil;
@Override
public void sendVerificationCode(String phoneNumber) {
//校验手机号
if(!RegexUtil.checkMobile(phoneNumber)) {
throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);
}
//生成随机验证码
String code = CaptchaUtil.getCaptcha(4);
//发送验证码, 本来使用阿里云短信服务的,但是现在不给个人使用了,直接在控制台输出一下
System.out.println(phoneNumber + "的验证码是: " + code);
//缓存验证码
redisUtil.set(VERIFICATION_CODE_PREFIX + phoneNumber,code,VERIFICATION_CODE_TIMEOUT);
}
@Override
public String getVerificationCode(String phoneNumber) {
//校验手机号
if(!RegexUtil.checkMobile(phoneNumber)) {
throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);
}
//获取验证码
return redisUtil.get(VERIFICATION_CODE_PREFIX + phoneNumber);
}
}
1.1.3.3 CaptchaUtil 生成随机验证码工具类
com.yj.lottery_system.common.utils 包下,将Hutool官网给的方法复制进来即可
package com.yj.lottery_system.common.utils;
import cn.hutool.captcha.LineCaptcha;
import cn.hutool.captcha.generator.RandomGenerator;
public class CaptchaUtil {
/**
* 生成随机验证码
* @param length 验证码长度
* @return 验证码
*/
public static String getCaptcha(int length) {
RandomGenerator randomGenerator = new RandomGenerator("0123456789", length);
LineCaptcha lineCaptcha =
cn.hutool.captcha.CaptchaUtil.createLineCaptcha(200, 100);
lineCaptcha.setGenerator(randomGenerator);
// 重新⽣成code
lineCaptcha.createCode();
return lineCaptcha.getCode();
}
}
1.1.3.4 测试

1.2 管理员登录
- 登陆⻚⾯把⽤⼾名密码提交给服务器.
- 服务器端验证⽤⼾名密码是否正确, 如果正确, 服务器⽣成令牌, 下发给客⼾端.
- 客⼾端把令牌存储起来(⽐如Cookie, local storage等), 后续请求时, 把token发给服务器
- 服务器对令牌进⾏校验, 如果令牌正确, 进⾏下⼀步操作

时序图;
1.2.1 参数要求
密码登录:
| 参数名 | 描述 | 类型 | 默认值 | 条件 |
|---|---|---|---|---|
| loginName | 用户电话号码或者使用邮箱 | String | 必须 | |
| password | 用户密码 | String | 必须 | |
| mandatoryIdentity | 强制用户身份标识为管理员 | String | 非必须 |
短信验证码登录:
| 参数名 | 描述 | 类型 | 默认值 | 条件 |
|---|---|---|---|---|
| loginMobile | 用户电话号码 | String | 必须 | |
| verificationCode | 验证码 | String | 必须 | |
| mandatoryIdentity | 强制用户身份标识为管理员 | String | 非必须 |
1.2.2 接口规范
密码登录
[请求] /password/login POST
{
"loginName":"13199999999",
"password":"123456",
"mandatoryIdentity":"ADMIN"
}
[响应]
{
"code": 200,
"data": {
"token":
"eyJhbGciOiJIUzI1NiJ9.eyJpZGVudGl0eSI6Ik5PUk1BTCIsInVzZXJJZCI6MjEsImlhdCI6MTcxN
jI2MjI5OCwiZXhwIjoxNzE2MjY0MDk4fQ.QfiZmZcfzd5ls_t8lg7bsTF7kA0daK-psjUt1QRj9d4",
"identity": "ADMIN"
},
"msg": ""
}
验证码登录
[请求] /message/login POST
{
"loginMobile":"13199999999",
"verificationCode":"0475",
"mandatoryIdentity":"ADMIN"
}
[响应]
{
"code": 200,
"data": {
"token":
"eyJhbGciOiJIUzI1NiJ9.eyJpZGVudGl0eSI6Ik5PUk1BTCIsInVzZXJJZCI6MjEsImlhdCI6MTcxN
jI2MjUyMywiZXhwIjoxNzE2MjY0MzIzfQ.XEuwO8AvNcqstbOrkI9kWaMhbN-HN2DfnUYGhJthA3I",
"identity": "ADMIN"
},
"msg": ""
}
1.2.3 controller层
1.2.3.1 密码登录
com/yj/lottery_system/controller/UserController.java 实现方法
密码登录:
- 打印日志
- UserLoginResult 密码登录controller的返回值
- UserPasswordLoginParam 密码登录controller的参数
- 调用service的服务
- convertToUserLoginResult 方法转换service层与controller层的返回值转换
- 返回成功
/**
* 密码登录
* @param param
* @return
*/
@RequestMapping("/password/login")
public CommonResult<UserLoginResult> userPasswordLogin(@Validated @RequestBody UserPasswordLoginParam param) {
//日志打印
log.info("userPasswordLogin userPasswordLoginParam: {}", JacksonUtil.writeValueAsString(param));
//调用service服务
UserLoginDTO userLoginDTO = userService.login(param);
return CommonResult.success(convertToUserLoginResult(userLoginDTO));
}
1.2.3.2 短信验证码登录
com/yj/lottery_system/controller/UserController.java 实现方法
短信验证码登录:
- 打印日志
- UserLoginResult 密码登录controller的返回值
- ShortMessageLoginParam 密码登录controller的参数
- 调用service的服务
- convertToUserLoginResult 方法转换service层与controller层的返回值转换
- 返回成功
/**
* 短信验证码登录
* @param param
* @return
*/
@RequestMapping("/message/login")
public CommonResult<UserLoginResult> shortMessageLogin(@Validated @RequestBody ShortMessageLoginParam param) {
//日志打印
log.info("userPasswordLogin ShortMessageLoginParam: {}", JacksonUtil.writeValueAsString(param));
//调用service服务
UserLoginDTO userLoginDTO = userService.login(param);
return CommonResult.success(convertToUserLoginResult(userLoginDTO));
}
1.2.3.3 convertToUserLoginResult : service 与 controller的返回值的转换
com/yj/lottery_system/controller/UserController.java 实现方法
- 非空校验
- 设置值
- 返回
private UserLoginResult convertToUserLoginResult(UserLoginDTO userLoginDTO) {
if(null == userLoginDTO) {
throw new ControllerException(ControllerErrorCodeConstants.LOGIN_ERROR);
}
UserLoginResult userLoginResult = new UserLoginResult();
userLoginResult.setToken(userLoginDTO.getToken());
userLoginResult.setIdentity(userLoginDTO.getIdentity().name());
return userLoginResult;
}
1.2.3.4 UserLoginResult :controller 返回类
com.yj.lottery_system.controller.result 包下:
- 根据接口规范的响应的data 属性,返回JWT令牌 和 登录人员身份
package com.yj.lottery_system.controller.result;
import lombok.Data;
import java.io.Serializable;
@Data
public class UserLoginResult implements Serializable {
//JWT令牌
private String token;
//登录人员身份
private String identity;
}
1.2.3.5 UserLoginParam :两个登录方法的公共属性
com.yj.lottery_system.controller.param 包下:
- 为了我们在service层只写一个方法,我们将公共属性抽象出来单独成立一个类。
package com.yj.lottery_system.controller.param;
import lombok.Data;
import java.io.Serializable;
@Data
public class UserLoginParam implements Serializable {
/**
* 强制身份登录信息
* @see com.yj.lottery_system.service.enums.UserIdentityEnum#name()
*/
private String mandatoryIdentity;
}
1.2.3.6 UserPasswordLoginParam :密码登录参数类
com.yj.lottery_system.controller.param 包下:
- 我们从接口请求中写出属性,除了父类的属性外,这两个属性都是不为空和不为空值的,使用@NotBlank
package com.yj.lottery_system.controller.param;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class UserPasswordLoginParam extends UserLoginParam{
//用户电话号码或者使用邮箱
@NotBlank(message = "用户电话号码或者邮箱不能为空")
private String loginName;
//用户密码
@NotBlank(message = "密码不能为空")
private String password;
}
1.2.3.7 ShortMessageLoginParam :短信验证码登录参数类
com.yj.lottery_system.controller.param 包下:
- 我们从接口请求中写出属性,除了父类的属性外,这两个属性都是不为空和不为空值的,使用@NotBlank
package com.yj.lottery_system.controller.param;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@Data
@EqualsAndHashCode(callSuper = true)
public class ShortMessageLoginParam extends UserLoginParam {
//电话号码
@NotBlank(message = "电话号码不能为空")
private String loginMobile;
//验证码
@NotBlank(message = "验证码不能为空")
private String verificationCode;
}
1.2.3.8 新增错误码
com.yj.lottery_system.common.errorcode包下ControllerErrorCodeConstants类新增:
ErrorCode LOGIN_ERROR = new ErrorCode(101,"登录失败");
1.2.3.9 测试


1.2.4 service 层
1.2.4.1 login 接口方法
com.yj.lottery_system.service 包下:
/**
* 用户登录
* 密码或验证码
* @param param
* @return
*/
UserLoginDTO login(UserLoginParam param);
1.2.4.2 实现接口
com.yj.lottery_system.service.impl 包下重写login方法
- 判断传来的参数类型,并转换,调用对应的参数类型的处理方法
- 返回
@Override
public UserLoginDTO login(UserLoginParam param) {
UserLoginDTO userLoginDTO = new UserLoginDTO();
//类型检查与转换
if(param instanceof UserPasswordLoginParam loginParam) {
//密码登录
userLoginDTO = loginByUserPassword(loginParam);
} else if(param instanceof ShortMessageLoginParam loginParam) {
//短信验证码登录
userLoginDTO = loginByShortMessage(loginParam);
} else {
throw new ServiceException(ServiceErrorCodeConstants.LOGIN_INFO_NOT_EXISTS);
}
return userLoginDTO;
}
1.2.4.3 loginByUserPassword:密码登录参数处理方法
com.yj.lottery_system.service.impl 包下实现:
- 使用 RegexUtil工具类中的校验方法 判断是手机号登录还是邮箱登录并调用对应dao层的Mapper
- 校验登录信息,校验得到的dao层返回结果
- 校验身份
- 校验密码
- 调用JWTUtil 工具类生成token,构造返回值
/**
* 密码登录流程
* @param loginParam
* @return
*/
private UserLoginDTO loginByUserPassword(UserPasswordLoginParam loginParam) {
UserDO userDO = new UserDO();
//判断手机号登录还是邮箱
if(RegexUtil.checkMobile(loginParam.getLoginName())) {
//手机号登录,调用Mapper
userDO = userMapper.selectPhoneNum(new Encrypt(loginParam.getLoginName()));
}else if(RegexUtil.checkMail(loginParam.getLoginName())) {
//邮箱登录,调用Mapper
userDO = userMapper.selectByByMail(loginParam.getLoginName());
} else {
throw new ServiceException(ServiceErrorCodeConstants.LOGIN_METHOD_NOT_EXISTS);
}
//校验登录信息
if(null == userDO) {
throw new ServiceException(ServiceErrorCodeConstants.USER_INFO_IS_NULL);
} 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())) {
//密码校验错误
throw new ServiceException(ServiceErrorCodeConstants.PASSWORD_ERROR);
}
//塞入返回值(JWT)
Map<String, Object> claim = new HashMap<>();
claim.put("id",userDO.getId());
claim.put("identity",userDO.getIdentity());
String token = JWTUtil.genJwt(claim);
UserLoginDTO userLoginDTO = new UserLoginDTO();
userLoginDTO.setToken(token);
userLoginDTO.setIdentity(UserIdentityEnum.forName(userDO.getIdentity()));
return userLoginDTO;
}
1.2.4.4 loginByShortMessage:验证码参数处理方法
com.yj.lottery_system.service.impl 包下实现:
- 使用 RegexUtil工具类中的正则校验方法 判断手机号格式
- 校验登录信息,校验得到的dao层返回结果
- 校验身份
- 校验验证码
- 调用JWTUtil 工具类生成token,构造返回值
/**
* 短信验证码登录流程
* @param loginParam
* @return
*/
private UserLoginDTO loginByShortMessage(ShortMessageLoginParam loginParam) {
//校验电话号
if(!RegexUtil.checkMobile(loginParam.getLoginMobile())) {
throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);
}
//获取用户数据
UserDO userDO = userMapper.selectPhoneNum(new Encrypt(loginParam.getLoginMobile()));
//校验登录信息
if(null == userDO) {
throw new ServiceException(ServiceErrorCodeConstants.USER_INFO_IS_NULL);
} else if(StringUtils.hasText(loginParam.getMandatoryIdentity())
&& !loginParam.getMandatoryIdentity().equalsIgnoreCase(userDO.getIdentity())) {
//强制身份校验,身份校验不通过
throw new ServiceException(ServiceErrorCodeConstants.IDENTITY_ERROR);
}
//验证码校验错误
String code = verificationCodeService.getVerificationCode(loginParam.getLoginMobile());
if(!loginParam.getVerificationCode().equals(code)) {
throw new ServiceException(ServiceErrorCodeConstants.VERIFICATION_CODE_ERROR);
}
//塞入返回值(JWT)
Map<String, Object> claim = new HashMap<>();
claim.put("id",userDO.getId());
claim.put("identity",userDO.getIdentity());
String token = JWTUtil.genJwt(claim);
UserLoginDTO userLoginDTO = new UserLoginDTO();
userLoginDTO.setToken(token);
userLoginDTO.setIdentity(UserIdentityEnum.forName(userDO.getIdentity()));
return userLoginDTO;
}
1.2.4.5 UserLoginDTO :service层返回类型
com.yj.lottery_system.service.dto 包下:
- 返回JWT令牌和登录人员身份即可
package com.yj.lottery_system.service.dto;
import com.yj.lottery_system.service.enums.UserIdentityEnum;
import lombok.Data;
@Data
public class UserLoginDTO {
//JWT令牌
private String token;
//登录人员身份
private UserIdentityEnum identity;
}
1.2.4.6 新增错误码
com.yj.lottery_system.common.errorcode 包下ServiceErrorCodeConstants 类:
ErrorCode LOGIN_INFO_NOT_EXISTS = new ErrorCode(108,"登录信息不存在");
ErrorCode LOGIN_METHOD_NOT_EXISTS = new ErrorCode(109,"登录方式不存在");
ErrorCode USER_INFO_IS_NULL = new ErrorCode(110,"用户信息为空");
ErrorCode VERIFICATION_CODE_ERROR = new ErrorCode(111,"验证码校验失败");
1.2.5 dao层
com.yj.lottery_system.dao.mapper 包下UserMapper 接口类:
- 实现根据电话号码和邮箱返回数据的两个方法。
@Select("select * from user where phone_number = #{phoneNumber}")
UserDO selectPhoneNum(@Param("phoneNumber") Encrypt phoneNumber);
@Select("select * from user where email = #{email}")
UserDO selectByByMail(@Param("email") String email);
二、Redis 配置使用:
2.1 配置
pom.xml 引入依赖
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置端⼝转发:
配置项:
application.yml:
spring:
data:
redis:
host: localhost
port: 8888 #redis端口号与主机
timeout: 60s # 连接空闲超过N(s秒、ms毫秒)后关闭,0为禁⽤,这⾥配置值和tcp-keepalive值⼀致
lettuce:
pool:
max-active: 8 # 默认使⽤ lettuce 连接池 允许最⼤连接数,默认8(负值表⽰没有限制)
max-idle: 8 # 最⼤空闲连接数,默认8
min-idle: 0 # 最⼩空闲连接数,默认0
max-wait: 5s # 连接⽤完时,新的请求等待时间(s秒、ms毫秒),超过该时间抛出异常 JedisConnectionException,(默认-1,负值表⽰没有限制)
2.2 封装工具类 RedisUtil
com.yj.lottery_system.common.utils 包下:
实现五个方法:get和set都是针对key与value都是String类型的方法(因为电话号码和验证码都是String)。
package com.yj.lottery_system.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.concurrent.TimeUnit;
@Configuration
public class RedisUtil {
//将被存储的数据直接存储,可读
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private final static Logger LOGGER = LoggerFactory.getLogger(RedisUtil.class);
/**
* 针对String设置值
* @param key
* @param value
* @return
*/
public boolean set(String key, String value) {
try {
stringRedisTemplate.opsForValue().set(key,value);
return true;
} catch (Exception e) {
LOGGER.error("Redis Error set({}, {})",key,value,e);
return false;
}
}
/**
* 针对String设置值 有过期时间
* @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("Redis Error set({}, {}, {})",key,value,time,e);
return false;
}
}
/**
* 针对String 获取值
* @param key
* @return
*/
public String get(String key) {
if(!StringUtils.hasText(key)) {
return null;
}
try {
String s = stringRedisTemplate.opsForValue().get(key);
return s;
} catch (Exception e) {
LOGGER.error("Redis Error get({})",key,e);
return null;
}
}
/**
* 删除值
* @param key
* @return
*/
public boolean del(String... key) {
try {
if(null != key && key.length > 0) {
if(1 == key.length) {
stringRedisTemplate.delete(key[0]);
} else {
stringRedisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
}
}
return true;
} catch (Exception e) {
LOGGER.error("Redis Error del({})",key,e);
return false;
}
}
/**
*
* @param key
* @return
*/
public boolean hasKey(String key) {
try{
return StringUtils.hasText(key) ? stringRedisTemplate.hasKey(key) : false;
}catch (Exception e) {
LOGGER.error("Redis Error hasKey({})",key,e);
return false;
}
}
}
2.3 测试

三、JWT令牌
JWT官网:https://www.jwt.io/
pom.xml引入依赖:
<!-- JWT 工具 -->
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<!-- or jjwt-gson if Gson is preferred -->
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
com/yj/lottery_system/common/utils 包下实现 JWTUtil 工具包:
package com.yj.lottery_system.common.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtParserBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @author: yibo
*/
public class JWTUtil {
private static final Logger logger = LoggerFactory.getLogger(JWTUtil.class);
/**
* 密钥:Base64编码的密钥
*/
private static final String SECRET = "SDKltwTl3SiWX62dQiSHblEB6O03FG9/vEaivFu6c6g=";
/**
* 生成安全密钥:将一个Base64编码的密钥解码并创建一个HMAC SHA密钥。
*/
private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(
Decoders.BASE64.decode(SECRET));
/**
* 过期时间(单位: 毫秒)
*/
private static final long EXPIRATION = 60*60*1000;
/**
* 生成密钥
*
* @param claim {"id": 12, "name":"张山"}
* @return
*/
public static String genJwt(Map<String, Object> claim){
//签名算法
String jwt = Jwts.builder()
.setClaims(claim) // 自定义内容(载荷)
.setIssuedAt(new Date()) // 设置签发时间
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION)) // 设置过期时间
.signWith(SECRET_KEY) // 签名算法
.compact();
return jwt;
}
/**
* 验证密钥
*/
public static Claims parseJWT(String jwt){
if (!StringUtils.hasLength(jwt)){
return null;
}
// 创建解析器, 设置签名密钥
JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder().setSigningKey(SECRET_KEY);
Claims claims = null;
try {
//解析token
claims = jwtParserBuilder.build().parseClaimsJws(jwt).getBody();
}catch (Exception e){
// 签名验证失败
logger.error("解析令牌错误,jwt:{}", jwt, e);
}
return claims;
}
/**
* 从token中获取用户ID
*/
public static Integer getUserIdFromToken(String jwtToken) {
Claims claims = JWTUtil.parseJWT(jwtToken);
if (claims != null) {
Map<String, Object> userInfo = new HashMap<>(claims);
return (Integer) userInfo.get("userId");
}
return null;
}
}

四、将前面的使用手机验证码改为QQ邮箱
4.1 配置项
pom.xml配置:
<!-- 邮件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
application.yml
spring:
mail: # QQ邮箱短信服务
host: smtp.qq.com
port: 465 # SSL 端口
username: ${MAIL_USER:3082589463@qq.com}
password: ${MAIL_PWD:xljwmdljkkmvdhaf} # 16 位授权码
protocol: smtps
default-encoding: UTF-8
properties:
mail.smtp.ssl.enable: true
mail.smtp.auth: true
mail.debug: false # 开发期可 true 看日志
4.2 工具类
com.yj.lottery_system.common.utils 包下: EmailCodeUtil 类
package com.yj.lottery_system.common.utils;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.Random;
@Slf4j
@Component
@RequiredArgsConstructor
@Data
public class EmailCodeUtil {
private final JavaMailSender mailSender;
@Value("${spring.mail.username}")
private String from;
@Value("${mail.code.length:6}")
private int codeLength;
/**
* 发送「纯数字验证码」到邮箱
* 语义保持与原来 SMSUtil.sendMessage 一致,方便替换
*
* @param templateCode 此处无实际意义,保留参数避免调用方改动
* @param mailAddress 接收验证码的邮箱
* @param templateParam 留空即可(原短信模板参数用不到)
* @return 生成的验证码(调用方可选择缓存)
*/
public String sendMessage(String templateCode, String mailAddress, String templateParam) {
String code = generateCode(codeLength);
try {
SimpleMailMessage msg = new SimpleMailMessage();
msg.setFrom(from);
msg.setTo(mailAddress);
msg.setSubject("验证码");
msg.setText("您的验证码为:" + code + "," + codeLength + "位数字,1分钟内有效,请勿泄露。");
mailSender.send(msg);
log.info("向{}发送验证码成功,code={}", mailAddress, code);
} catch (Exception e) {
log.error("向{}发送验证码失败", mailAddress, e);
throw new RuntimeException("邮件发送失败");
}
return code; // 调用方可自行缓存到 Redis / Session
}
/* 生成 length 位纯数字随机码 */
private String generateCode(int length) {
int bound = (int) Math.pow(10, length);
return String.format("%0" + length + "d", new Random().nextInt(bound));
}
/* 可选:快速发送方法,只传邮箱 */
public String sendCode(String mailAddress) {
return sendMessage(null, mailAddress, null);
}
/**
* 发送「指定验证码」到邮箱(不再重新生成)
*/
public void sendFixedCode(String mailAddress, String code) {
try {
SimpleMailMessage msg = new SimpleMailMessage();
msg.setFrom(from);
msg.setTo(mailAddress);
msg.setSubject("验证码");
msg.setText("您的验证码为:" + code + ",4位数字,请勿泄露。");
mailSender.send(msg);
log.info("向{}发送固定验证码成功,code={}", mailAddress, code);
} catch (Exception e) {
log.error("向 {} 发送验证码异常", mailAddress, e); // 带堆栈
throw new RuntimeException("邮件发送失败: " + e.getMessage(), e);
}
}
}
4.3 代码修改
由于前端我实在不熟悉,在这就不改参数名了,怕接口调用不起来:
4.3.1 VerificationCodeServiceImpl
com.yj.lottery_system.service.impl 包下的VerificationCodeServiceImpl 的 sendVerificationCode 方法改成我们自己的工具类,将错误信息修改一下。
package com.yj.lottery_system.service.impl;
import com.yj.lottery_system.common.errorcode.ServiceErrorCodeConstants;
import com.yj.lottery_system.common.exception.ServiceException;
import com.yj.lottery_system.common.utils.CaptchaUtil;
import com.yj.lottery_system.common.utils.EmailCodeUtil;
import com.yj.lottery_system.common.utils.RedisUtil;
import com.yj.lottery_system.common.utils.RegexUtil;
import com.yj.lottery_system.service.IVerificationCodeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class VerificationCodeServiceImpl implements IVerificationCodeService {
//为区分业务,key加上前缀 VERIFICATION_CODE_
private static final String VERIFICATION_CODE_PREFIX = "VERIFICATION_CODE_";
//过期时间
private static final Long VERIFICATION_CODE_TIMEOUT = 60L;
@Autowired
private RedisUtil redisUtil;
@Autowired
private EmailCodeUtil emailCodeUtil; // 通过构造器注入
@Override
public void sendVerificationCode(String mail) {
// //校验手机号
// if(!RegexUtil.checkMobile(mail)) {
// throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);
// }
// //生成随机验证码
// String code = CaptchaUtil.getCaptcha(4);
// //发送验证码, 本来使用阿里云短信服务的,但是现在不给个人使用了,直接在控制台输出一下
// System.out.println(mail + "的验证码是: " + code);
// //缓存验证码
// redisUtil.set(VERIFICATION_CODE_PREFIX + mail,code,VERIFICATION_CODE_TIMEOUT);
// 1. 邮箱格式校验
if (!RegexUtil.checkMail(mail)) {
throw new ServiceException(ServiceErrorCodeConstants.MAIL_ERROR);
}
// 2. 生成 4 位验证码
String code = CaptchaUtil.getCaptcha(4);
// 3. 发送刚才生成的验证码(不再重新生成)
emailCodeUtil.sendFixedCode(mail, code);
// 4. 缓存
redisUtil.set(VERIFICATION_CODE_PREFIX + mail, code, VERIFICATION_CODE_TIMEOUT);
}
@Override
public String getVerificationCode(String mail) {
//校验邮箱
if(!RegexUtil.checkMail(mail)) {
throw new ServiceException(ServiceErrorCodeConstants.MAIL_ERROR);
}
//获取验证码
return redisUtil.get(VERIFICATION_CODE_PREFIX + mail);
}
}
4.3.2 ShortMessageLoginParam
com.yj.lottery_system.controller.param 包下:ShortMessageLoginParam 类,修改一下中文即可,参数属性名就不改了。
package com.yj.lottery_system.controller.param;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@Data
@EqualsAndHashCode(callSuper = true)
public class ShortMessageLoginParam extends UserLoginParam {
//电话号码
@NotBlank(message = "邮箱不能为空")
private String loginMobile;
//验证码
@NotBlank(message = "验证码不能为空")
private String verificationCode;
}
4.3.3 loginByShortMessage
com/yj/lottery_system/service/impl 包下 UserServiceImpl 类:将电话校验变为邮箱校验,电话获取用户信息变成邮箱获取用户信息。
/**
* 短信验证码登录流程
* @param loginParam
* @return
*/
private UserLoginDTO loginByShortMessage(ShortMessageLoginParam loginParam) {
//校验邮箱
if(!RegexUtil.checkMail(loginParam.getLoginMobile())) {
throw new ServiceException(ServiceErrorCodeConstants.MAIL_ERROR);
}
//获取用户数据,实际是传入的邮箱参数
UserDO userDO = userMapper.selectByByMail(loginParam.getLoginMobile());
//校验登录信息
if(null == userDO) {
throw new ServiceException(ServiceErrorCodeConstants.USER_INFO_IS_NULL);
} else if( StringUtils.hasText(loginParam.getMandatoryIdentity())
&& !loginParam.getMandatoryIdentity().equalsIgnoreCase(userDO.getIdentity())) {
//强制身份校验,身份校验不通过
throw new ServiceException(ServiceErrorCodeConstants.IDENTITY_ERROR);
}
//验证码校验错误
String code = verificationCodeService.getVerificationCode(loginParam.getLoginMobile());
if(!loginParam.getVerificationCode().equals(code)) {
throw new ServiceException(ServiceErrorCodeConstants.VERIFICATION_CODE_ERROR);
}
//塞入返回值(JWT)
Map<String, Object> claim = new HashMap<>();
claim.put("id",userDO.getId());
claim.put("identity",userDO.getIdentity());
String token = JWTUtil.genJwt(claim);
UserLoginDTO userLoginDTO = new UserLoginDTO();
userLoginDTO.setToken(token);
userLoginDTO.setIdentity(UserIdentityEnum.forName(userDO.getIdentity()));
return userLoginDTO;
}
4.3.4 sendVerificationCode 方法
com/yj/lottery_system/controller 包下:UserController类:
//实际传入的是QQ邮箱
@RequestMapping("/verification-code/send")
public CommonResult<Boolean> sendVerificationCode(String phoneNumber) {
//日志打印
log.info("sendVerificationCode mail: {}", phoneNumber);
//调用service服务
verificationCodeService.sendVerificationCode(phoneNumber);
return CommonResult.success(true);
}
五、前端代码
static/blogin.html :
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>管理员登录页面</title>
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/4.5.2/css/bootstrap.min.css">
<link rel="stylesheet" href="./css/base.css">
<<link rel="stylesheet" href="./css/login.css">
</head>
<body class="login-page">
<!-- 登录框区域 -->
<div class="login-box row">
<div class="img-box col-sm-6 col-md-6 col-lg-7 col-xl-7">
<img src="./pic/login-left.png" class="img-fluid" alt srcset>
</div>
<div class="login-container col-sm-6 col-md-6 col-lg-5 col-xl-5">
<div class="tab-box">
<span class="active tab-span" data-form="loginForm">密码登录</span>
<span class="tab-span" data-form="codeForm">验证码登录</span>
</div>
<form id="loginForm">
<div class="form-group">
<label for="phoneNumber">电话号</label>
<input class="form-control" type="text" id="phoneNumber"
name="phoneNumber"
required placeholder="请输入电话号">
</div>
<div class="form-group">
<label for="password">密码</label>
<input class="form-control" type="password"
id="password" name="password"
required placeholder="请输入密码">
</div>
<button type="submit"
class="btn btn-primary btn-block login-btn">登录</button>
<div class="error"></div>
</form>
<!-- 验证码登录框 -->
<form id="codeForm" style="display: none;">
<div class="form-group">
<label for="loginMobile">QQ邮箱号</label>
<input class="form-control" type="text" id="loginMobile"
name="loginMobile"
required placeholder="请输入QQ邮箱号">
</div>
<div class="form-group">
<label for="verificationCode">验证码</label>
<div class="code-box">
<input class="form-control" type="text"
id="verificationCode" name="verificationCode"
required placeholder="请输入验证码">
<div class="btn btn-primary " id="getVerificationCode">获取验证码</div>
</div>
</div>
<button type="submit"
class="btn btn-primary btn-block login-btn">登录</button>
<div class="error"></div>
</form>
<div class="register-link">
还没有账号,去<a href="register.html?admin=true">注册</a>
</div>
</div>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery-validate/1.19.3/jquery.validate.min.js"></script>
<script src="https://cdn.staticfile.org/twitter-bootstrap/4.5.2/js/bootstrap.min.js"></script>
<script>
$("#loginForm").validate({
rules: {
phoneNumber: "required",
password: {
required: true,
minlength: 6
}
},
messages: {
phoneNumber: "请输入您的手机号",
password: {
required: "请输入密码",
minlength: "密码长度至少为6个字符"
}
},
submitHandler: function(form) {
var loginName = $('#phoneNumber').val();
var password = $('#password').val();
// 清除之前的错误消息
$('.error').text('');
// 发送AJAX请求
$.ajax({
url: '/password/login',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({loginName: loginName, password: password, mandatoryIdentity: "ADMIN"}),
success: function(result) {
if (result.code != 200) {
alert("登录失败!" + result.msg);
} else {
localStorage.setItem("user_token", result.data.token);
localStorage.setItem("user_identity", "ADMIN");
location.assign("admin.html");
}
},
});
return false; // 阻止表单的默认提交行为
}
});
// tab切换
$('.tab-box span').click(function(e){
let formId ='#' + e.target.dataset.form
$(this).addClass('active')
$(this).siblings().removeClass('active')
$('form').hide()
console.log(formId)
$(formId).show()
})
// 获取验证码
var timer = null
$('#getVerificationCode').click(function(e){
console.log(e,$(this).text())
var txt = $(this).text()
var num = 60
if(txt.indexOf('获取')!==-1){
$('#getVerificationCode').text(num+'s')
getCode()
// 获取验证码接口
timer&&clearInterval(timer)
timer = setInterval(function(){
if(num>1){
num--
$('#getVerificationCode').text(num+'s')
}else{
timer&&clearInterval(timer)
$('#getVerificationCode').text('重新获取')
}
},1000)
}
})
function getCode(){
$.ajax({
url: '/verification-code/send',
type: 'GET',
data:{ phoneNumber:$('#loginMobile').val() },
success: function(result) {
if (result.code != 200) {
timer&&clearInterval(timer)
$('#getVerificationCode').text('重新获取')
toastr.error('验证码获取失败');
} else {
toastr.success('验证码发送成功')
}
},
});
}
// 验证码登录
$("#codeForm").validate({
rules: {
loginMobile: "required",
verificationCode:"required",
},
messages: {
loginMobile: "请输入您的QQ邮箱号",
verificationCode: "请输入验证码",
},
submitHandler: function(form) {
var loginMobile = $('#loginMobile').val();
var verificationCode = $('#verificationCode').val();
// 清除之前的错误消息
$('.error').text('');
// 发送AJAX请求
$.ajax({
url: '/message/login',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({loginMobile: loginMobile , verificationCode: verificationCode, mandatoryIdentity: "ADMIN"}),
success: function(result) {
if (result.code != 200) {
alert("登录失败!" + result.msg);
} else {
localStorage.setItem("user_token", result.data.token);
localStorage.setItem("user_identity", "ADMIN");
location.assign("admin.html");
}
},
});
return false; // 阻止表单的默认提交行为
}
});
</script>
</body>
</html>
六、登录拦截器
com.yj.lottery_system.common.interceptor 包下:
package com.yj.lottery_system.common.interceptor;
import com.yj.lottery_system.common.utils.JWTUtil;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
/**
* 预处理,业务请求之前调用
*
* @param request
* @param response
* @param handler
* @return
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 获取请求头
String token = request.getHeader("user_token");
log.info("获取token:{}", token);
log.info("获取路径:{}", request.getRequestURI());
// 令牌解析
Claims claims = JWTUtil.parseJWT(token);
if (null == claims) {
log.error("解析JWT令牌失败!");
response.setStatus(401);
return false;
}
log.info("解析JWT令牌成功!放行");
return true;
}
}
配置:
com.yj.lottery_system.common.config 包下:
package com.yj.lottery_system.common.config;
import com.yj.lottery_system.common.interceptor.LoginInterceptor;
import jakarta.annotation.Resource;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Arrays;
import java.util.List;
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Resource
private LoginInterceptor loginInterceptor;
private final List<String> excludes = Arrays.asList(
"/**/*.html",
"/css/**",
"/js/**",
"/pic/**",
"/*.jpg",
"/favicon.ico",
"/**/login",
"/register",
"/verification-code/send",
"/winning-records/show"
);
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(excludes);
}
}
浙公网安备 33010602011771号