当前位置: 首页 > 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;}

相关文章:

  • AUTOSAR图解==>AUTOSAR_SRS_SecureOnboardCommunication
  • 精益数据分析(71/126):从移情到黏性——创业阶段的关键跨越与数据驱动策略
  • 建站系统哪个好?
  • 从头实现react native expo本地生成APK
  • srs-7.0 支持obs推webrtc流
  • 破解误区:WebView 调试常见认知误区与 WebDebugX 实践指南
  • 纯前端实现 导入/导出/模板下载功能
  • 前端无感登录刷新
  • 汉得集星獭1.8.0正式发布,高效集成再赋能!
  • 苍穹外卖04 新增菜品菜品分页查询删除菜品修改菜品
  • Vue3.0教程005:watch监视ref定义的【基本类型】数据和【对象类型】数据
  • 数据结构与算法分析实验14 实现基本排序算法
  • 5.2.4 wpf中MultiBinding的使用方法
  • 保密行业工作沟通安全:吱吱软件的“四重防泄露”设计
  • Python虚拟环境再PyCharm中自由切换使用方法
  • Git 和 GitHub 学习指南本地 Git 配置、基础命令、GitHub 上传流程、企业开发中 Git 的使用流程、以及如何将代码部署到生产服务器
  • Linux 的 TCP 网络编程 -- 回显服务器,翻译服务器
  • C#处理印尼地区的数字分隔符方法
  • leetcode 162. Find Peak Element
  • 人工智能的“歧视”:“她数据”在算法运行中隐形
  • 全国首例闭环脊髓神经接口手术在浙江完成,截瘫患者实现自主行走
  • 上海普陀:原则同意将工业河更名为同济湾河
  • 专家:新冠病毒流行高峰无明显季节性特征,与人群抗体水平有关
  • 又有明星分析师晋升管理层:“白金分析师”武超则已任中信建投证券党委委员
  • 印尼总统20年来首次访泰:建立战略伙伴关系,加强打击网络诈骗等合作
  • 俄美元首通话超2小时,普京称愿与乌方共同起草和平备忘录