当前位置: 首页 > news >正文

从零开始的抽奖系统创作(2)

我们接着进行抽奖系统的完善。

前面我们完成了

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/Dysmsapiconfig.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);// 重新生成codelineCaptcha.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);
    @Overridepublic 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);}@Overridepublic 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使用测试:

    @Testvoid redisTest() {stringRedisTemplate.opsForValue().set("key1", "value2");System.out.println("从redis中获取value : " + stringRedisTemplate.opsForValue().get("key1"));}@Testvoid 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中 读取时以字节数组读取*/@Autowiredprivate 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 {//解析tokenclaims = 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.管理员登录(两种⽅式)

学习令牌的使⽤之后, 接下来我们通过令牌来完成⽤⼾的登录的流程

1. 登陆⻚⾯把⽤⼾名密码提交给服务器.

2. 服务器端验证⽤⼾名密码是否正确, 如果正确, 服务器⽣成令牌, 下发给客⼾端.

3. 客⼾端把令牌存储起来(⽐如Cookie, local storage等), 后续请求时, 把token发给服务器

4. 服务器对令牌进⾏校验, 如果令牌正确, 进⾏下⼀步操作

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));//使用同一个接口完成登录  将接收参数改为公用参数extendsUserLoginDTO userLoginDTO = userService.login(userLoginParam);return CommonResult.success(convertToLoginResult(userLoginDTO));}

8.Sevice

通过Java14特性属性校验并赋值实现一个接口完成两个登录功能

    /*** 用户登录*   1.手机号/邮箱 + 密码*   2.手机号 + 验证码* @param userLoginParam* @return*/@Overridepublic 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;}

http://www.xdnf.cn/news/543421.html

相关文章:

  • vue3/vue2大屏适配
  • 基于统计检验与多模型对心脏病数据的分析与预测
  • Oracle 11g post PSU Oct18 设置ssl连接(使用wallets)
  • 企业级网络安全护盾:剖析高防IP原理与防护策略
  • 编程学习论坛测试报告
  • 隐形安全感
  • Linux Bash 中 $? 的详细用法
  • 【算法】定长滑动窗口5.20
  • 畅游Diffusion数字人(30):情绪化数字人视频生成
  • MVDR源码(可直接运行)
  • HarmonyOS NEXT~鸿蒙系统与mPaaS三方框架集成指南
  • 单端传输通道也会有奇偶模现象喔
  • PIL库的图像增强函数
  • 从ISO17025合规到信创适配 解密质检lims系统实验室的 AI 质检全链路实践
  • 【C++】C++的拷贝构造函数介绍使用
  • 【RK3588嵌入式图形编程】-Cairo-形状与填充
  • C++从入门到实战(十六)String(中)String的常用接口(构造接口,析构接口,迭代器,遍历修改,容量管理与数据访问)
  • 零基础设计模式——创建型模式 - 单例模式
  • .NET 10 - 尝试一下Minimal Api的Validation新特性
  • 开源一个记账软件,支持docker一键部署
  • APPtrace 智能参数系统:重构 App 用户增长与运营逻辑
  • C++中String类
  • 《经济日报》深度聚焦|珈和科技携手万果博览荟共筑智慧农业新示范高地 全链赋能蒲江茶果产业数字化转型升级
  • 榕壹云上门家政系统:基于Spring Boot+MySQL+UniApp的全能解决方案
  • 深度剖析ZooKeeper
  • 基于大模型与人工智能体的机械臂对话式交互系统RobotAgent
  • 阿里云CDN刷新预热--刷新URL
  • 【AI 大模型】盘古大模型简介 ( 创建空间 | 体验模型 | 部署模型 )
  • 【华为OD-B卷-打印文件 100分(python、java、c++、js、c)】
  • 面试算法刷题3(核心+acm)