手机验证码登录

1. SpringBoot 腾讯云短信

1.1 导入依赖

<!--腾讯短信服务-->
        <dependency>
            <groupId>com.tencentcloudapi</groupId>
            <artifactId>tencentcloud-sdk-java</artifactId>
            <version>3.1.423</version><!-- 注:不要使用4.0.X版本,不是最新的 -->
        </dependency>

注意:不要使用4.0.X版本,不是最新的

1.2. 创建短信发送工具类

package com.lym.reggie.utils;

import com.lym.reggie.common.ClientException;
import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.common.profile.ClientProfile;
import com.tencentcloudapi.common.profile.HttpProfile;
import com.tencentcloudapi.sms.v20190711.SmsClient;
import com.tencentcloudapi.sms.v20190711.models.SendSmsRequest;
import com.tencentcloudapi.sms.v20190711.models.SendSmsResponse;

/**
 * 短信发送工具类
 */
public class TengxunSMSUtils {
    public static final String TEMPLATE_ID = "1484545";//自己的短信模版ID
    public static final String VALIDATE_CODE = "866988";

    public static void sendShortMessage(String templateCode, String phoneNum, String param) throws ClientException {
        try {
            /* 必要步骤:
             * 实例化一个认证对象,入参需要传入腾讯云账户密钥对 secretId 和 secretKey
             * 本示例采用从环境变量读取的方式,需要预先在环境变量中设置这两个值
             * 您也可以直接在代码中写入密钥对,但需谨防泄露,不要将代码复制、上传或者分享给他人
             * CAM 密钥查询:https://console.cloud.tencent.com/cam/capi
             */
            Credential cred = new Credential("AKIDLYXKtGOHwMZZNyJQIwLXVEhw0pPjnbAU", "93iBRWMyNXS8bsvjMn0yiEXbQ2OULwIP");
            // 实例化一个 http 选项,可选,无特殊需求时可以跳过
            HttpProfile httpProfile = new HttpProfile();
            /* SDK 默认使用 POST 方法。
             * 如需使用 GET 方法,可以在此处设置,但 GET 方法无法处理较大的请求 */
            httpProfile.setReqMethod("POST");
            /* SDK 有默认的超时时间,非必要请不要进行调整
             * 如有需要请在代码中查阅以获取最新的默认值 */
            httpProfile.setConnTimeout(60);
            /* SDK 会自动指定域名,通常无需指定域名,但访问金融区的服务时必须手动指定域名
             * 例如 SMS 的上海金融区域名为 sms.ap-shanghai-fsi.tencentcloudapi.com */
            httpProfile.setEndpoint("sms.tencentcloudapi.com");
            /* 非必要步骤:
             * 实例化一个客户端配置对象,可以指定超时时间等配置 */
            ClientProfile clientProfile = new ClientProfile();
            /* SDK 默认用 TC3-HMAC-SHA256 进行签名
             * 非必要请不要修改该字段 */
            clientProfile.setSignMethod("HmacSHA256");
            clientProfile.setHttpProfile(httpProfile);
            /* 实例化 SMS 的 client 对象
             * 第二个参数是地域信息,可以直接填写字符串 ap-guangzhou,或者引用预设的常量 */
            SmsClient client = new SmsClient(cred, "ap-guangzhou", clientProfile);
            /* 实例化一个请求对象,根据调用的接口和实际情况,可以进一步设置请求参数
             * 您可以直接查询 SDK 源码确定接口有哪些属性可以设置
             * 属性可能是基本类型,也可能引用了另一个数据结构
             * 推荐使用 IDE 进行开发,可以方便地跳转查阅各个接口和数据结构的文档说明 */
            SendSmsRequest req = new SendSmsRequest();
            /* 填充请求参数,这里 request 对象的成员变量即对应接口的入参
             * 您可以通过官网接口文档或跳转到 request 对象的定义处查看请求参数的定义
             * 基本类型的设置:
             * 帮助链接:
             * 短信控制台:https://console.cloud.tencent.com/smsv2
             * sms helper:https://cloud.tencent.com/document/product/382/3773 */
            /* 短信应用 ID: 在 [短信控制台] 添加应用后生成的实际 SDKAppID,例如1400006666 */
            String SdkAppid = "1400710560";
            req.setSmsSdkAppid(SdkAppid);
            /* 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名,可登录 [短信控制台] 查看签名信息 */
            String sign = "梦梦爱吃啵啵公众号";
            req.setSign(sign);
            /* 国际/港澳台短信 senderid: 国内短信填空,默认未开通,如需开通请联系 [sms helper] */
            String senderid = "";
            req.setSenderId(senderid);
            /* 模板 ID: 必须填写已审核通过的模板 ID,可登录 [短信控制台] 查看模板 ID */
            req.setTemplateID(templateCode);
            /* 下发手机号码,采用 e.164 标准,+[国家或地区码][手机号]
             * 例如+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号*/
            String[] phoneNumber = {"+86" + phoneNum + ""};
            req.setPhoneNumberSet(phoneNumber);
            /* 模板参数: 若无模板参数,则设置为空*/
            String[] templateParams = {param};
            req.setTemplateParamSet(templateParams);
            /* 通过 client 对象调用 SendSms 方法发起请求。注意请求方法名与请求对象是对应的
             * 返回的 res 是一个 SendSmsResponse 类的实例,与请求对象对应 */
            SendSmsResponse res = client.SendSms(req);
            // 输出 JSON 格式的字符串回包
            System.out.println(SendSmsResponse.toJsonString(res));
            // 可以取出单个值,您可以通过官网接口文档或跳转到 response 对象的定义处查看返回字段的定义
            System.out.println(res.getRequestId());


        } catch (TencentCloudSDKException e) {
            e.printStackTrace();
        }
    }

    /*//写个主方法测试一下:
    public static void main( String[] args ) throws ClientException {
        String templateID = "1484545";//自己的短信模板ID
        String phoneNumbers = "13653700199";
        //生成随机六位数
        Integer param = ValidateCodeUtils.generateValidateCode(6);
        sendShortMessage(templateID,phoneNumbers,param.toString());
    }*/
}

3.3 创建随机生成验证码工具类

package com.lym.reggie.utils;

import java.util.Random;

/**
 * 随机生成验证码工具类 (提供了两个方法 返回值分别为Integer、String)
 */
public class ValidateCodeUtils {
    /**
     * 随机生成验证码
     * @param length 长度为4位或者6位
     * @return
     */
    public static Integer generateValidateCode(int length){
        Integer code =null;
        if(length == 4){
            code = new Random().nextInt(9999);//生成随机数,最大为9999
            if(code < 1000){
                code = code + 1000;//保证随机数为4位数字
            }
        }else if(length == 6){
            code = new Random().nextInt(999999);//生成随机数,最大为999999
            if(code < 100000){
                code = code + 100000;//保证随机数为6位数字
            }
        }else{
            throw new RuntimeException("只能生成4位或6位数字验证码");
        }
        return code;
    }

    /**
     * 随机生成指定长度字符串验证码
     * @param length 长度
     * @return
     */
    public static String generateValidateCode4String(int length){
        Random rdm = new Random();
        String hash1 = Integer.toHexString(rdm.nextInt());
        String capstr = hash1.substring(0, length);
        return capstr;
    }
}

2. 在1.的基础上 SpringBoot + Session实现用户验证码登录

2.1 拦截器中添加不需要处理的请求路径,并放行已登录的

package com.lym.reggie.filter;

import com.alibaba.fastjson.JSON;
import com.lym.reggie.common.BaseContext;
import com.lym.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.AntPathMatcher;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 检查用户是否已经完成登录
 * filterName过滤器名字
 * urlPatterns拦截的请求,这里是拦截所有的请求
 */
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter{
    //路径匹配器,支持通配符
    public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        // 0 对请求和响应进行强转,我们需要的是带http的
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        //1、获取本次请求的URI
        String requestURI = request.getRequestURI();// /backend/index.html

        log.info("拦截到请求:{}",requestURI);

        // 定义不需要处理的请求路径  登录、登出、页面(只拦截除登录登出外的 Controller的请求)(静态页面我们不需要拦截,因为此时的静态页面是没有数据的)
        String[] urls = new String[]{
                "/employee/login",
                "/employee/logout",
                "/backend/**",
                "/front/**",
                "/user/sendMsg",
                "/user/login"
        };


        //2、判断本次请求是否需要处理
        boolean check = check(urls, requestURI);

        //2.1 如果不需要处理,则直接放行
        if(check){
            log.info("本次请求{}不需要处理",requestURI);
            filterChain.doFilter(request,response);
            return;
        }

        //3. 放行已登录的
        //3.1、判断后台系统员工用户的登录状态,如果已登录,则直接放行 (存到Session中)
        if(request.getSession().getAttribute("employee") != null){
            log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
            /*//线程id
            long threadId = Thread.currentThread().getId();
            log.info("LoginFilter  :ThreadLocal获得的id:{}",threadId);*/
            // 1)用户ID存储到ThreadLocal。为后面在MyMetaObjecthandler中,统一为公共字段赋值
            Long id = (Long) request.getSession().getAttribute("employee");
            // 保存
            BaseContext.setCurrentId(id);

            // 2)放行
            filterChain.doFilter(request,response);
            return;
        }
        //3.2、判断移动端用户的登录状态,如果已登录,则直接放行 (存到Session中)
        if(request.getSession().getAttribute("user") != null){
            log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
            // 1)保存当前登录用户id
            Long userId = (Long) request.getSession().getAttribute("user");
            // 保存
            BaseContext.setCurrentId(userId);
            
            // 2)放行
            filterChain.doFilter(request,response);
            return;
        }

        log.info("用户未登录");
        //4、如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
        return;

    }

    /**
     * 路径匹配,检查本次请求是否需要放行
     * @param urls
     * @param requestURI
     * @return
     */
    public boolean check(String[] urls,String requestURI){
        for (String url : urls) {
            //把浏览器发过来的请求和我们定义的不拦截的url做比较,匹配则放行
            boolean match = PATH_MATCHER.match(url, requestURI);
            if(match){
                return true;
            }
        }
        return false;
    }
}

2.2 处理请求

1)发送验证码

	//发送验证码
    @PostMapping("/sendMsg")
    public R<String> sendMsg(@RequestBody User user, HttpSession session){
        //1 发送验证码、2 将生成的验证码保存入Session
        String phone = user.getPhone();
        if (StringUtils.isNotEmpty(phone)){
            String validateCode = ValidateCodeUtils.generateValidateCode(6).toString();
            try {
                //1. 发送验证码
                TengxunSMSUtils.sendShortMessage(TengxunSMSUtils.TEMPLATE_ID,phone,validateCode);
                //2 将生成的验证码保存入Session
                session.setAttribute(phone,validateCode);

                return R.success("发送成功");
            } catch (ClientException e) {
                e.printStackTrace();
                //发送失败
                return R.error(phone+"验证码 发送失败");
            }

            /*//为避免浪费,先不发送验证码 密码统一为111111
            String validateCode = "111111";
            //将验证码存储到sessiom
            session.setAttribute(phone,validateCode);
            return R.success("验证码发送成功!");*/
        }
        return R.error("手机号为空");
    }

2)登录

//登录
    @PostMapping("/login")
    public R<String> login(@RequestBody Map map,HttpSession session){
        //校验验证码是否正确 -- 若正确则登录成功,若为新用户则自动帮助注册 -- session中保存用户登录状态
        String phone = map.get("phone").toString();
        String validateCode = map.get("code").toString();
        String codeInSession = session.getAttribute(phone).toString();

        // 1. 校验验证码是否正确
        if (codeInSession!=null && codeInSession.equals(validateCode)){
            //校验通过 则登录成功,若为新用户则自动帮助注册 -- 向客户端写入Cookie,将会员信息保存到redis
            // 2. 若为新用户则 自动注册
            LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(User::getPhone,phone);
            User user = userService.getOne(queryWrapper);
            if (user==null){
                //自动注册
                user = new User();
                user.setPhone(phone);
                user.setStatus(1);//可设置可不设置,因为数据库帮我们设置了默认值
                userService.save(user);
            }

            // 3.session中保存用户登录状态,这样过滤器就会放行
            session.setAttribute("user",user.getId());
            return R.success("登陆成功!");
        }
        return R.error("验证码输入错误!");
    }

3. 在1.的基础上 SpringBoot + Redis缓存实现用户验证码登录

  • Note:
    但用Redis缓存来实现登录 怎样判断登录状态❓是否需要放行❓

3.1 导入redis所需依赖

<!-- 集成redis依赖  -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

3.2 yml中配置redis连接信息

redis:
    host: 127.0.0.1  # Redis服务器地址
    port: 6379       # Redis服务器连接端口
    password: 123456
    database: 0     # Redis数据库索引(默认为0)
    #timeout: 5000   # 连接超时时间(毫秒)
    #jedis:
     # pool:
      #	max-active: 32 #连接池最大连接数(使用负值表示没有限制)
       # max-idle: 16   #最大能够保持idel状态的对象数
        #min-idle: 8   #最小能够保持idel状态的对象数
        #max-wait: 100000  #连接池最大阻塞等待时间(使用负值表示没有限制)

3.3 RedisConfig 重新配置Redis序列化

config包下,创建RedisConfig,加上@Configuration 注解,固定的套路

package com.lym.reggie.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lym.reggie.common.CustomException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {
	//配置我们自己的redisTemplate
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();

        //默认的Key序列化器为:JdkSerializationRedisSerializer
        redisTemplate.setKeySerializer(new StringRedisSerializer()); // key序列化   采用String的序列化
        //redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // value序列化

        redisTemplate.setConnectionFactory(connectionFactory);
        return redisTemplate;
    }
}
  • IDEA中: Git -> Add
  • 命令行中:git commit -m “内容备注”

3.4 处理请求

(1)实现验证码的发送

将验证码缓存到Redis中,并设置有效期为5分钟

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
    @Autowired
    private UserService userService;
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    //发送验证码
    @PostMapping("/sendMsg")
    public R<String> sendMsg(@RequestBody User user){
        log.info("user: {}",user);
        // 发送验证码 -- 将验证码保存入Redis
        String phone = user.getPhone();

        //1、连接Redis,查找手机验证码是否存在
        String code = (String)redisTemplate.opsForValue().get(phone+RedisMessageConstant.SENDTYPE_LOGIN);

        //====================================================
        // 1.1如果存在的话,说明在5分钟内已经发送过验证码了,不能再发了
        if (!StringUtils.isEmpty(code)) {
            System.out.println("已存在,还没有过期,不能再次发送");
            return R.error(phone+"验证码已存在,还没有过期");
        }
        //=====================================================

        //1.2 如果不存在的话,那么redis创建键值对生成验证码并存储,设置过期时间
        //发送
        String newCode = ValidateCodeUtils.generateValidateCode(6).toString();
        try {
            TengxunSMSUtils.sendShortMessage(TengxunSMSUtils.TEMPLATE_ID,phone,newCode);

            // 因为有短信轰炸的情况,短信服务对每次发送限制次数,所以有发送不成功的情况,要考虑
            //将验证码缓存到Redis中,并设置有效期为5分钟
            redisTemplate.opsForValue().set(phone,validateCode,5, TimeUnit.MINUTES);
            return R.success("验证码发送成功!");

            System.out.println("发送成功:"+newCode);
            return R.success(phone+":"+newCode+" 发送成功!");
        } catch (ClientException e) {
            e.printStackTrace();
            //验证码发送失败
            return R.error(phone+":"+newCode+" 发送失败");
        }

        /*//为避免浪费,先不发送验证码 密码统一为1111
        String validateCode = "111111";
        //将验证码存储到redis中 设置5分钟的保存时间
        redisTemplate.opsForValue().set(phone,validateCode,5, TimeUnit.MINUTES);
        return R.success(phone+":"+validateCode+" 发送成功!");*/
    }
}

(2)登录

处理逻辑

  1. 校验⽤户输⼊的短信验证码是否正确,如果验证码错误则登录失败
  2. 如果验证码正确,则判断当前⽤户是否为会员,如果不是会员则⾃动完成会员注册
  3. 登陆成功,则删除Redis缓存的验证码
  4. session中保存用户登录状态,这样过滤器就会放行
 	//登录
    @PostMapping("/login")
    public R<String> login(@RequestBody Map map, HttpServletResponse response){
        //校验验证码是否正确 -- 若正确则登录成功,若为新用户则自动帮助注册 -- session中保存用户登录状态

        String phone = map.get("phone").toString();
        String validateCode = map.get("code").toString();
        
        String codeIn = (String) redisTemplate.opsForValue().get(phone);

        // 1. 校验验证码是否正确
        if (codeIn!=null && codeIn.equals(validateCode)){
            //校验通过 则登录成功,若为新用户则自动帮助注册 -- 向客户端写入Cookie,将会员信息保存到redis
            // 2. 若为新用户则 自动注册
            LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(User::getPhone,phone);
            User user = userService.getOne(queryWrapper);
            if (user==null){
                //自动注册
                user = new User();
                user.setPhone(phone);
                user.setStatus(1);//可设置可不设置,因为数据库帮我们设置了默认值
                userService.save(user);
            }
            //登陆成功,则删除Redis缓存的验证码
            redisTemplate.delete(phone);

            // 3.session中保存用户登录状态,这样过滤器就会放行
            session.setAttribute("user",user.getId());
            return R.success("登陆成功!");
        }

        return R.error("验证码输入错误!");
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值