【尚庭公寓|项目日记】第六天

摘要:

        本文介绍了短信登录功能的实现方案:1)使用阿里云短信服务测试功能,配置YML文件并创建Client对象;2)编写验证码生成工具类;3)实现短信发送逻辑,通过Redis存储验证码并控制发送频率;4)完成基础登录功能,支持新用户自动注册;5)使用拦截器实现Token验证,通过ThreadLocal存储用户信息。同时指出了当前方案的不足:验证码有效期过长、未删除已用验证码、电话号码安全性等问题,建议增加密码登录方式并优化验证机制。

一,短信登录功能

项目采用阿里云的短信通信服务,目前只使用了该服务的测试功能,只能在测试内的手机号上使用

(1)开通阿里云的短信通信服务,绑定设置的手机号码,设置一个密钥,将密钥和密码放入yml文件:

aliyun:
  sms:
    access-key-id: ----
    access-key-secret: ----
    endpoint: ----

 (2)common通用模块中编写配置类和注册Client对象

   注意:@ConditionalOnProperty(name = "aliyun.sms.endpoint")表示仅在yml存在name对应 value的模块生效

@Data
@ConfigurationProperties(prefix = "aliyun.sms")
public class AliyunSMSProperties {

    private String accessKeyId;

    private String accessKeySecret;

    private String endpoint;

}
@Configuration
@EnableConfigurationProperties(AliyunSMSProperties.class)
@ConditionalOnProperty(name = "aliyun.sms.endpoint")
public class AliyunSMSConfiguration {

    @Autowired
    private AliyunSMSProperties properties;

    @Bean
    public Client createClient(){
        // 创建 Client 对象
        Config config = new Config();
        config.setAccessKeyId(properties.getAccessKeyId());
        config.setAccessKeySecret(properties.getAccessKeySecret());
        config.setEndpoint(properties.getEndpoint());

        try {
            return new Client(config);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

(3)编写发送验证码实现类

import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.atguigu.lease.web.app.service.SmsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class SmsServiceImpl implements SmsService {

    @Autowired
    private Client client;

    @Override
    public void sendCode(String phone, String code) {

        // 创建发送短信请求对象
        SendSmsRequest request = new SendSmsRequest();
        // 设置接收短信的手机号码
        request.setPhoneNumbers(phone);
        // 设置短信签名
        request.setSignName("阿里云短信测试");
        // 设置短信模板
        request.setTemplateCode("SMS_154950909");
        // 设置短信模板参数
        request.setTemplateParam("{\"code\":\"" + code + "\"}");

        try {
            // 发送短信
            client.sendSms(request);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}

(4)编写验证码生成工具类(通用类放在common模块中)

         主要用到StringBuilder拼接类和Random随机生成类

import java.util.Random;

public class CodeUtil {

    public static String getCode(Integer len){

        //字符串拼接类
        StringBuilder builder = new StringBuilder();
        //随机生成类
        Random random = new Random();

        //循环生成随机数,数字长度为len
        for (int i = 0; i < len; i++) {
            //随机生成0-9的数字
            int num = random.nextInt(10);
            //将数字拼接到字符串中
            builder.append(num);
        }
        return builder.toString();
    }
}

(5)实现获取短信验证码接口(过期时间过长有待改进)

        1,电话号码作为key,验证码作为value

        2,设置验证码过期时间

        3,防止短时间重复申请验证码

@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private SmsService smsService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public void getCode(String phone) {
        String code = CodeUtil.getCode(6);
        String key = RedisConstant.APP_LOGIN_PREFIX+phone;

        //这段逻辑为防止短时间重复申请验证码
        //判断redis中是否存在该手机号的验证码
        Boolean hasKey = redisTemplate.hasKey(key);
        //如果已经存在,说明发送过
        if(hasKey){
            //得到剩余时间
            Long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS);
            //总过期时间-ttl得到距发送验证码后过去的时间,如果小于60秒则拒绝
            if(RedisConstant.APP_LOGIN_CODE_TTL_SEC-ttl<RedisConstant.APP_LOGIN_CODE_RESEND_TIME_SEC){
                throw new LeaseException(ResultCodeEnum.APP_SEND_SMS_TOO_OFTEN);
            }
        }

        //发送code
        smsService.sendCode(phone, code);
        //设置过期时间
        redisTemplate.opsForValue().
                set(key, code,RedisConstant.APP_LOGIN_CODE_TTL_SEC, TimeUnit.SECONDS);
    }
}

二,基础登录功能(登录注册一体)

登录逻辑校验:

1,判断手机号和验证码不为空

2,拼接key然后在redis里查询验证码

3,redis查询的验证码不为空,与用户输入的验证码进行比较

4,查询数据表中用户信息

LambdaQueryWrapper<UserInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserInfo::getPhone, loginVo.getPhone());
UserInfo userInfo = userInfoMapper.selectOne(queryWrapper);

5,如果用户信息为空说明是新用户,然后注册用户

细节一:userInfo = new UserInfo();直接将新用户对象赋值给查询为空的UserInfo类对象

细节二:userInfoMapper.insert(userInfo);插入后会自动生成id,然后回显到userInfo里

6,判断用户是否被禁用,若没有返回jwt令牌

三,返回登录用户信息的接口

这个接口比较简单,只有一个LoginUserHolder的记录登录用户信息的类要注意

//ThreadLocal的主要作用是为每个使用它的线程提供一个独立的变量副本,
//使每个线程都可以操作自己的变量,而不会互相干扰
public class LoginUserHolder {
    public static ThreadLocal<LoginUser> threadLocal = new ThreadLocal<>();

    public static void setLoginUser(LoginUser loginUser) {
        threadLocal.set(loginUser);
    }

    public static LoginUser getLoginUser() {
        return threadLocal.get();
    }

    public static void clear() {
        threadLocal.remove();
    }
}

四,拦截器且注册

实现preHandle和after...方法,pre...表示在执行所有请求处理之前的逻辑,通过key得到token,解析token,获取userId和username放入到本地线程中l

@Component
public class AuthenticationInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        //得到请求头
        String token = request.getHeader("access-token");
        //解析token
        Claims claims = JwtUtils.parseToken(token);

        Long userId = claims.get("userId", Long.class);
        String username = claims.get("username", String.class);
        //放入本地线程中
        LoginUserHolder.setLoginUser(new LoginUser(userId, username));

        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        LoginUserHolder.clear();
    }
}
//注册拦截器
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {

    @Autowired
    private AuthenticationInterceptor authenticationInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(this.authenticationInterceptor).
                addPathPatterns("/app/**").
                excludePathPatterns("/app/login/**");
    }
}

五,总结

1,redis里对验证码的过期时间设置过长(10分钟),是否考虑缩短

2,登录成功后验证码是否需要删除

3,直接将电话号码作为username放在jwt中不太安全

4,登录注册一体是否可以分开,前端如何修改

5,添加用户密码登录功能,与手机号登录一起

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值