从零开始的抽奖系统创作(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/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.管理员登录(两种⽅式)

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

  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));
        //使用同一个接口完成登录  将接收参数改为公用参数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;
    }
相关推荐
linux-hzh15 分钟前
第二章 Java语言基础
java·开发语言
qq_2142258721 分钟前
深入解析 Java GC 调优:减少 Minor GC 频率,优化系统吞吐
java·jvm·其他·性能优化
jiuweiC38 分钟前
docker使用
java·docker·eureka
往日情怀酿做酒 V176392963842 分钟前
linux基础操作10------(特殊符号,正则表达式,三剑客)
linux·运维·服务器
TE-茶叶蛋43 分钟前
Web Workers 使用指南
开发语言·前端·javascript
五月茶1 小时前
JUC高并发编程
java·开发语言·jvm
惜.己1 小时前
Linux Shell编程(四)
linux·运维·服务器
_龙小鱼_1 小时前
Vue Router动态路由与导航守卫实战
前端·javascript·vue.js·html5
代码搬运媛1 小时前
Webpack 分包策略详解及实现
前端·webpack·node.js
Dontla1 小时前
微服务中API网关作用(统一入口、路由转发、协议转换、认证授权、请求聚合、负载均衡、熔断限流、监控日志)
java·微服务·负载均衡