
我们接着进行抽奖系统的完善。
前面我们完成了
1.结构初始化(统一结果返回之类的,还有包的分类)
2.加密(基于Hutool进行的对称与非对称加密)
3.用户注册
接下来我们先完善一下结构(统一异常处理)
1.统一异常处理
很简单,@RestControllerAdvice+@ExceptionHandler即可
@RestControllerAdvice//可以捕获全局抛出的异常
@ResponseBody
public class GlobalExceptionHandler {
private final static Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
//捕获Service层的异常
@ExceptionHandler(value = ServiceException.class)
public CommonResult<?> serviceException(ServiceException e) {
logger.error("serviceException :", e);
//返回数据
return CommonResult.error(GlobalErrorCodeConstant.INTERNAL_SERVICE_ERROR);
}
//捕获Controller层的异常
@ExceptionHandler(value = ControllerException.class)
public CommonResult<?> controllerException(ControllerException e) {
logger.error("controllerException :", e);
//返回数据
return CommonResult.error(GlobalErrorCodeConstant.INTERNAL_SERVICE_ERROR);
}
//捕获全局的异常
@ExceptionHandler(value = Exception.class)
public CommonResult<?> exception(Exception e) {
logger.error("exception :", e);
//返回数据
return CommonResult.error(GlobalErrorCodeConstant.INTERNAL_SERVICE_ERROR);
}
}
这里使用@ExceptionHandler(value = 类名)的方式捕获异常。
其他没什么要特别注意的,补充GlobalErrorCodeConstant的异常种类
2.登录模块
这里提供短信验证码登录的方式,因此我们要了解一下阿里短信服务
但是现在个人申请不到了,所以我们直接使用虚拟的验证码吧。
附一个申请成功后植入的代码:
1.阿里短信代码模块:
依赖:
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>2.0.24</version>
</dependency>
短信服务工具类:
@Component
public class SMSUtil {
private static final Logger logger = LoggerFactory.getLogger(SMSUtil.class);
@Value(value = "${sms.sign-name}")
private String signName;
@Value(value = "${sms.access-key-id}")
private String accessKeyId;
@Value(value = "${sms.access-key-secret}")
private String accessKeySecret;
/**
* 发送短信
*
* @param templateCode 模板号
* @param phoneNumbers 手机号
* @param templateParam 模板参数 {"key":"value"}
*/
public void sendMessage(String templateCode, String phoneNumbers, String templateParam) {
try {
Client client = createClient();
com.aliyun.dysmsapi20170525.models.SendSmsRequest sendSmsRequest = new SendSmsRequest()
.setSignName(signName)
.setTemplateCode(templateCode)
.setPhoneNumbers(phoneNumbers)
.setTemplateParam(templateParam);
RuntimeOptions runtime = new RuntimeOptions();
SendSmsResponse response = client.sendSmsWithOptions(sendSmsRequest, runtime);
if (null != response.getBody()
&& null != response.getBody().getMessage()
&& "OK".equals(response.getBody().getMessage())) {
logger.info("向{}发送信息成功,templateCode={}", phoneNumbers, templateCode);
return;
}
logger.error("向{}发送信息失败,templateCode={},失败原因:{}",
phoneNumbers, templateCode, response.getBody().getMessage());
} catch (TeaException error) {
logger.error("向{}发送信息失败,templateCode={}", phoneNumbers, templateCode, error);
} catch (Exception _error) {
TeaException error = new TeaException(_error.getMessage(), _error);
logger.error("向{}发送信息失败,templateCode={}", phoneNumbers, templateCode, error);
}
}
/**
* 使用AK&SK初始化账号Client
* @return Client
*/
private Client createClient() throws Exception {
// 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。
// 建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378657.html。
Config config = new Config()
.setAccessKeyId(accessKeyId)
.setAccessKeySecret(accessKeySecret);
// Endpoint 请参考 https://api.aliyun.com/product/Dysmsapi
config.endpoint = "dysmsapi.aliyuncs.com";
return new Client(config);
}
}
配置:
### 短信 ##
## 短信 ##
sms.access-key-id="填写⾃⼰申请的"
sms.access-key-secret="填写⾃⼰申请的"
sms.sign-name="填写⾃⼰申请的"
代码使用:
使用时传入3个参数:
templateCode(模板号):SMS_465324787
phoneNumbers(手机号):传入你申请的
templateParam(模版参数):发送验证码的格式设置为{"key":"value"}
使用时传入一个map并将其序列化
2.验证码模块
利用Hutool工具:(这工具真好用都不用手写了)
//生成随机验证码
public class CaptchaUtil {
/**
* 生成随机验证码
*
* @param length 几位
* @return
*/
public static String getCaptcha(int length) {
// 自定义纯数字的验证码(随机4位数字,可重复)
RandomGenerator randomGenerator = new RandomGenerator("0123456789", length);
LineCaptcha lineCaptcha = cn.hutool.captcha.CaptchaUtil.createLineCaptcha(200, 100);
lineCaptcha.setGenerator(randomGenerator);
// 重新生成code
lineCaptcha.createCode();
return lineCaptcha.getCode();
}
}
3.Controller
基于手机号生成验证码并发送验证码 最后使用Redis缓存验证码用于校验
/**
* 发送验证码
* @param phoneNumber
* @return
*/
@RequestMapping("/verification-code/send")
public CommonResult<Boolean> verificationCode (String phoneNumber) {
//日志打印
logger.info("verificationCode phoneNumber:{}", phoneNumber);
verificationCodeService.sendVerificationCode(phoneNumber);
return CommonResult.success(Boolean.TRUE);
}
4.Service
/**
* 发送验证码
* @param phoneNumber
* @return
*/
public String sendVerificationCode(String phoneNumber);
/**
* 获取验证码
* @param phoneNumber
* @return
*/
public String getVerificationCode(String phoneNumber);
@Override
public String sendVerificationCode(String phoneNumber) {
//校验手机号
if(!RegexUtil.checkMobile(phoneNumber)) {
throw new ServiceException(ServiceErrorCodeConstant.PHONE_NUMBER_ERROR);
}
//生成随机验证码 利用hutool生成
String code = CaptchaUtil.getCaptcha(4);
//发送验证码
Map<String, String> map = new HashMap<>();
map.put("code", code);
smsUtil.sendMessage(PHONE_NUMBER_TEMPLATE_CODE, phoneNumber, JacksonUtil.writeValueAsString(map));
//缓存验证码
redisUtil.set(PHONE_NUMBER_PRE +phoneNumber, code, PHONE_NUMBER_TIMEOUT);
return redisUtil.get(PHONE_NUMBER_PRE +phoneNumber);
}
@Override
public String getVerificationCode(String phoneNumber) {
//校验手机号
if(!RegexUtil.checkMobile(phoneNumber)) {
throw new ServiceException(ServiceErrorCodeConstant.PHONE_NUMBER_ERROR);
}
return redisUtil.get(PHONE_NUMBER_PRE +phoneNumber);
}
Redis使用简单介绍:
配置:
在Linux服务器上通过隧道开放Redis的6379端口号

在Linux上输入命令启动Redis:
service redis-server start
可以在idea上下载插件Redis Helper

刷新后在右侧找到插件点击加号添加Redis服务


1.名称(随便,便于标识)
2.本机就行
3.在Linux隧道绑定的端口号
4.Test测试验证,出现绿色即成功连接Redis
Redis使用测试:
@Test
void redisTest() {
stringRedisTemplate.opsForValue().set("key1", "value2");
System.out.println("从redis中获取value : " + stringRedisTemplate.opsForValue().get("key1"));
}
@Test
void redisUtil() {
// redisUtil.set("key2", "value2");
// redisUtil.set("key3", "value3", 20L);
// System.out.println("key2是否存在: " + redisUtil.hasKey("key2"));
// System.out.println("key3是否存在: " + redisUtil.hasKey("key3"));
// redisUtil.delete("key2");
// System.out.println("key2是否存在: " + redisUtil.hasKey("key2"));
System.out.println("key3是否存在: " + redisUtil.hasKey("key3"));
}
1.使用前注入StringRedisTemplate类
2.使用stringRedisTemplate.opsForValue().set("key1", "value2");添加元素
但是,每次使用都要注入StringRedisTemplate有点麻烦了,我们将其封装成一个util工具。
@Configuration
public class RedisUtil {
public static final Logger logger = LoggerFactory.getLogger(RedisUtil.class);
/**
* StringRedisTemplate : 直接用String存储读取(可读)
* RedisTemplate : 先将被存储数据转换为字节数组(不可读) 再存储到Redis中 读取时以字节数组读取
*/
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 设置值
* @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("RedisUtil 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 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 set 错误:({})", key, e);
return null;
}
}
/**
* 删除值
* @param key
* @return
*/
public boolean delete(String... key) {
try{
if(key != null && 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 delete 错误:({})", key, e);
return false;
}
}
/**
* 查看Key是否存在
* @param key
* @return
*/
public boolean hasKey(String key) {
return StringUtils.hasText(key)
? stringRedisTemplate.hasKey(key)
: false;
}
}
该类提供了:
set:两个,一个是传入key:value 。另一个是传入key:value+过期时间
get:通过key获取value
delete:通过key,进行批量删除
hasKey:查看key是否存在
5.JWT令牌验证
前面我们发送了验证码,在客户端我们就要将该验证码输入进行登录,但还有一些用户会采用手机号/邮箱+密码的方式登录。所以我们要开放两个接口用于登录。
登录完后端会返回给前端一个token用于令牌校验,使验证码登录在多台主机下都可以使用。
JWT本身没什么说的,直接上代码:
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;
}
}
提供了三个方法:
1.getJWT:通过传入一个map生成token并返回。
2.parseJWT:将token解析为map数据
3.getUserIdFromToken:从token中获取用户ID(可能有用)
6.管理员登录(两种⽅式)
学习令牌的使⽤之后, 接下来我们通过令牌来完成⽤⼾的登录的流程
登陆⻚⾯把⽤⼾名密码提交给服务器.
服务器端验证⽤⼾名密码是否正确, 如果正确, 服务器⽣成令牌, 下发给客⼾端.
客⼾端把令牌存储起来(⽐如Cookie, local storage等), 后续请求时, 把token发给服务器
服务器对令牌进⾏校验, 如果令牌正确, 进⾏下⼀步操作
7.Controller
数据准备:
接收与返回的数据:
因为两个登录都含有统一数据,所以对公共的进行提取
//共有的字段
@Data
public class UserLoginParam implements Serializable {
/**
* 身份登录信息
* 可填可不填 不填代表都可以登录
*/
private String mandatoryIdentity;
}
UserPasswordLoginParam:
@Data
@EqualsAndHashCode(callSuper = true)
public class UserPasswordLoginParam extends UserLoginParam{
/**
* 手机号或邮箱
*/
@NotBlank(message = "手机号或邮箱不能为空")
private String loginName;
/**
* 密码
*/
@NotBlank(message = "密码不能为空")
private String password;
}
UserMessageLoginParam:
@Data
@EqualsAndHashCode(callSuper = true)
public class UserMessageLoginParam extends UserLoginParam{
/**
* 登录手机号
*/
@NotBlank(message = "手机号不能为空")
private String loginMobile;
/**
* 验证码
*/
@NotBlank(message = "验证码不能为空")
private String code;
}
UserLoginResult:
@Data
public class UserLoginResult implements Serializable {
/**
* 令牌
*/
@NotBlank(message = "令牌不能为空")
private String token;
/**
* 身份信息
*/
@NotBlank(message = "身份信息不能为空")
private String identity;
}
1.密码登录:
/**
* 密码登录
* @param userLoginParam
* @return
*/
@RequestMapping("/password/login")
private CommonResult<UserLoginResult> passwordLogin(
@Validated @RequestBody UserPasswordLoginParam userLoginParam) {
logger.info("passwordLogin userLoginParam:{}", JacksonUtil.writeValueAsString(userLoginParam));
//使用同一个接口完成登录
UserLoginDTO userLoginDTO = userService.login(userLoginParam);
return CommonResult.success(convertToLoginResult(userLoginDTO));
}
在数据库返回的对象使用DTO,然后返回时用convertToLoginResult进行类型转化。
private UserLoginResult convertToLoginResult(UserLoginDTO userLoginDTO) {
//校验
if(userLoginDTO == null) {
throw new ControllerException(ControllerErrorCodeConstant.LOGIN_ERROR);
}
//数据转化
UserLoginResult userLoginResult = new UserLoginResult();
userLoginResult.setToken(userLoginDTO.getToken());
userLoginResult.setIdentity(userLoginDTO.getIdentity().name());
return userLoginResult;
}
2.短信验证码登录
与之类似:
/**
* 短信验证码登录
* @param userLoginParam
* @return
*/
@RequestMapping("/message/login")
private CommonResult<UserLoginResult> messageLogin(
@Validated @RequestBody UserMessageLoginParam userLoginParam) {
logger.info("messageLogin userLoginParam:{}", JacksonUtil.writeValueAsString(userLoginParam));
//使用同一个接口完成登录 将接收参数改为公用参数extends
UserLoginDTO userLoginDTO = userService.login(userLoginParam);
return CommonResult.success(convertToLoginResult(userLoginDTO));
}
8.Sevice
通过Java14特性属性校验并赋值实现一个接口完成两个登录功能
/**
* 用户登录
* 1.手机号/邮箱 + 密码
* 2.手机号 + 验证码
* @param userLoginParam
* @return
*/
@Override
public UserLoginDTO login(UserLoginParam userLoginParam) {
UserLoginDTO userLoginDTO = null;
//类型检查与类型交换 java 14 版本及以上 实现校验两个登录方式
if(userLoginParam instanceof UserPasswordLoginParam loginParam) {
//手机号/邮箱 + 密码
userLoginDTO = loginByPassword(loginParam);
}else if(userLoginParam instanceof UserMessageLoginParam loginParam) {
//手机号 + 验证码
userLoginDTO = loginByShortMessage(loginParam);
}else {
throw new ServiceException(ServiceErrorCodeConstant.LOGIN_INFO_NOT_EXITS);
}
return userLoginDTO;
}
DTO:
@Data
public class UserLoginDTO implements Serializable {
/**
* JWT令牌
*/
private String token;
/**
* 身份信息
*/
private UserIdentityEnum identity;
}
1.通过手机短信登录
1.校验手机号
if(!StringUtils.hasText(loginParam.getLoginMobile())) {
throw new ServiceException(ServiceErrorCodeConstant.PHONE_NUMBER_ERROR);
}
2.通过手机号完成数据库查询
UserDO userDO = userMapper.selectByPhoneNumber(
new Encrypt(loginParam.getLoginMobile()));
3.校验数据库信息及身份(判断是否是管理员)
//校验数据库数据
if(userDO == null) {
throw new ServiceException(ServiceErrorCodeConstant.USER_INFO_IS_EMPTY);
}else if(StringUtils.hasText(loginParam.getMandatoryIdentity())
&& !loginParam.getMandatoryIdentity()
.equalsIgnoreCase(userDO.getIdentity())) {
//身份校验不通过
throw new ServiceException(ServiceErrorCodeConstant.IDENTITY_ERROR);
}
4.获取Redis中的验证码并于数据库中的数据进行校验
String code = verificationCodeService.getVerificationCode(loginParam.getLoginMobile());
if(!code.equals(loginParam.getCode())) {
throw new ServiceException(ServiceErrorCodeConstant.VERIFICATION_CODE_ERROR);
}
5.将数据封装成一个token
//塞入返回值(JWT)
Map<String, Object> claim = new HashMap<>();
claim.put("id", userDO.getId());
claim.put("identity", userDO.getIdentity());
String token = JWTUtil.getJwt(claim);
6.封装成为DTO返回
UserLoginDTO userLoginDTO = new UserLoginDTO();
userLoginDTO.setToken(token);
userLoginDTO.setIdentity(UserIdentityEnum.forName(userDO.getIdentity()));
return userLoginDTO;
2.通过手机号/邮箱+密码登录
1.校验密码
2.判断是手机号还是邮箱
3.通过手机号/邮箱获取数据库信息
4.校验 数据库信息及身份信息
5.生成token
6.包装成DTO返回
/**
* 通过手机号+密码登录
* @param loginParam
* @return
*/
private UserLoginDTO loginByPassword(UserPasswordLoginParam loginParam) {
UserDO userDO = null;
if(!StringUtils.hasText(loginParam.getPassword())) {
throw new ServiceException(ServiceErrorCodeConstant.PASSWORD_EMPTY);
}
//判断是手机号还是邮箱登录
if(RegexUtil.checkMobile(loginParam.getLoginName())) {
//手机号
//根据手机号查询用户表
userDO = userMapper.selectByPhoneNumber(new Encrypt(loginParam.getLoginName()));
}else if(RegexUtil.checkMail(loginParam.getLoginName())) {
//邮箱登录
//根据邮箱查询用户表
userDO = userMapper.selectByEmail(loginParam.getLoginName());
}else {
throw new ServiceException(ServiceErrorCodeConstant.LOGIN_NOT_EXITS);
}
//校验登录信息
if(userDO == null) {
throw new ServiceException(ServiceErrorCodeConstant.USER_INFO_IS_EMPTY);
}else if(StringUtils.hasText(loginParam.getMandatoryIdentity())
&& !loginParam.getMandatoryIdentity()
.equalsIgnoreCase(userDO.getIdentity())) {
//身份校验不通过
throw new ServiceException(ServiceErrorCodeConstant.IDENTITY_ERROR);
}else if(!DigestUtil.sha256Hex(loginParam.getPassword())
.equals(userDO.getPassword())) {
throw new ServiceException(ServiceErrorCodeConstant.PASSWORD_ERROR);
}
//塞入返回值(JWT)
Map<String, Object> claim = new HashMap<>();
claim.put("id", userDO.getId());
claim.put("identity", userDO.getIdentity());
String token = JWTUtil.getJwt(claim);
UserLoginDTO userLoginDTO = new UserLoginDTO();
userLoginDTO.setToken(token);
userLoginDTO.setIdentity(UserIdentityEnum.forName(userDO.getIdentity()));
return userLoginDTO;
}