java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java用户短信验证码登录

Java实现用户短信验证码登录功能实例代码

作者:Light._.House

现在不管是各类的网站,还是大小社交app,登录方式是越来越多了,但是大部分还是以短信登录为主,本文主要介绍了java短信验证码登录功能设计与实现,感兴趣的可以了解一下

此处使用阿里提供的API解决方案

同时需要注意的是,此文章在Java项目操作上需要有一定的编程基础,因为不想罗里吧嗦的一大堆,对于分层理解较差和基础编程能力较低的小白不建议

1.前置阿里云操作

1.登录阿里云后,搜索“短信服务”

2.点击后进入如下界面此处点击“免费开通”,

此处若项目必须要求有真实短信发送,则建议购买最便宜的先进行测试即可

3.点击“快速学习和测试”,依次根据提示,申请“资质”,“签名”,“模板”

此处三个都对应个人和企业,申请需要时间

4.模拟测试,在“快速学习和测试”界面的下方,有测试的模板可以使用,测试需要绑定测试手机号、申请自定义测试模板和自定义测试签名

5.调用API发送短信

在此需要注意的是,VS Code和IEDA需要下载对应的插件才能保证在后续自己的项目中能正常调用到短信发送的API接口

  点击SDK实例后能看到完整的调用代码,此时建议使用V2.0,代码包含java(异步)和java

,采用哪种方式都无所谓,只需将代码全部复制即可

2.java项目操作

0.注意事项

在此之前,你需要准备的东西如下

1.短信签名名称  SignName

2.短信模板Code  TemplateCode

3.SDK实例代码

4.对应编译器的插件必须安装完毕

5.对应的ALIBABA_CLOUD_ACCESS_KEY_ID和ALIBABA_CLOUD_ACCESS_KEY_SECRET

ALIBABA_CLOUD_ACCESS_KEY_ID和ALIBABA_CLOUD_ACCESS_KEY_SECRET可以在个人中心看到

 创建对应的AccessKey时,需要保存好对应的ALIBABA_CLOUD_ACCESS_KEY_ID和ALIBABA_CLOUD_ACCESS_KEY_SECRET,这是调用API时传递到阿里的凭证,十分重要!

以上准备完毕后,就可以在java项目中嵌入对应的API实现验证码的发送

1.创建两个接口,一个是获取验证码,一个是携带验证码登录

在此方案下,采用了将验证码存储到Redis中,此处存入Redis后,可设置验证码的过期时间,减少对底层的访问压力,也能实现验证码限时的操作,另外此出也可加入对应的手机号在固定时间内对于获取验证码接口的访问次数限制,避免恶意访问造成服务器压力过大。

Controller

package com.ruoyi.controller;

import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.ruoyi.common.constant.ReturnConstants;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.domain.dto.*;
import com.ruoyi.domain.entity.User;
import com.ruoyi.pojo.vo.CurrentPrincipal;
import com.ruoyi.security.center.RequestLimit;
import com.ruoyi.service.WeChatLoginService;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.result.WxMpOAuth2AccessToken;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.service.IUserService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.page.TableDataInfo;


@Slf4j
@RestController
@RequestMapping("/v1/user")
public class UserController extends BaseController
{
    @Autowired
    private IUserService userService;

    @Autowired
    private WxMpService wxService;

    @Autowired
    private WeChatLoginService weChatLoginService;

    /**
     * - 手机号格式错误
     *   - 为空
     *   - 不符合手机号
     * - 手机号未注册
     * - 用户被禁用
     *   - 在此处直接验证,以减少发送短信的成本
     *   验证码存储到redis中,在五分钟内可以通过验证码登录
     * @param userVerifyDTO
     * @return
     * @throws Exception
     */
    @RequestLimit
    @PostMapping("verify")
    public AjaxResult verify(@RequestBody UserVerifyDTO userVerifyDTO) throws Exception {
        log.debug("处理验证码获取");
        log.debug("验证信息:{}", userVerifyDTO);
        return AjaxResult.success(userService.verify(userVerifyDTO));
    }

    /**
     * 登录请求,匹配redis中的验证码和数据库中的信息
     * @param userLoginDTO
     * @param request
     * @return
     * @throws SocketException
     * @throws UnknownHostException
     */
    @RequestLimit
    @PostMapping("login")
    public Object login(@RequestBody UserLoginDTO userLoginDTO, HttpServletRequest request) throws SocketException, UnknownHostException {
        log.debug("处理登录请求-携带验证码");
        log.debug("登录信息:{}", userLoginDTO);
        return userService.login(userLoginDTO, request);
    }
}

2.编写service的实现,

ServiceImpl

package com.ruoyi.service.impl;

import java.net.SocketException;
import java.net.UnknownHostException;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import cn.hutool.core.bean.BeanUtil;
import com.alibaba.fastjson2.JSON;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.ruoyi.common.constant.ReturnConstants;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.ip.AddressUtils;
import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.common.utils.uuid.UUID;
import com.ruoyi.constans.JwtConstans;
import com.ruoyi.domain.bo.UserInsertBo;
import com.ruoyi.domain.dto.*;
import com.ruoyi.domain.entity.Addr;
import com.ruoyi.domain.entity.Logininfor;
import com.ruoyi.domain.entity.Loginlogs;
import com.ruoyi.domain.entity.User;
import com.ruoyi.domain.param.UserLoginInfoVO;
import com.ruoyi.domain.vo.UserLoginResultVO;
import com.ruoyi.mapper.LoginlogsMapper;
import com.ruoyi.pojo.vo.CurrentPrincipal;
import com.ruoyi.pojo.vo.PageData;
import com.ruoyi.pojo.vo.UserCachePO;
import com.ruoyi.service.IUserService;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.utils.*;
import eu.bitwalker.useragentutils.UserAgent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.Cacheable;
import com.ruoyi.common.core.redis.RedisCache;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;
import com.ruoyi.mapper.UserMapper;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

import static com.ruoyi.common.utils.PageUtils.startPage;


@Slf4j
@Service
public class UserServiceImpl implements IUserService, JwtConstans
{

    @Value("${token.secret}")
    private String secretKey;

    @Value("${token.expireTime}")
    private Integer expireTime;

    @Value("${wkzr.redis.test}")
    private String secret;

    /**
     * 验证码过期时间
     */
    @Value("${wkzr.redis.verificationExpirationTime}")
    private Integer verificationExpirationTime;


    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private LoginlogsMapper loginlogsMapper;

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RedisCache redisCache;

    //设置初始密码属性
    @Value("${userPassword.initPassword}")
    private String initPassword;

    /**
     * - 手机号格式错误
     *   - 为空
     *   - 不符合手机号
     * - 手机号未注册
     * - 用户被禁用
     *   - 在此处直接验证,以减少发送短信的成本
     *   验证码存储到redis中,在五分钟内可以通过验证码登录
     * @param userVerifyDTO
     * @return
     * @throws Exception
     */
    @Override
    public String verify(UserVerifyDTO userVerifyDTO) throws Exception {
        String phoneNumber = userVerifyDTO.getPhoneNumber();

        // 格式错误
        if (phoneNumber.length() != 11) {
            throw new AccessDeniedException(ReturnConstants.PHONENUMBER_FORMAT_ERROR);
        }

        // 不能为空
        if (StringUtils.isEmpty(phoneNumber)) {
            throw new AccessDeniedException(ReturnConstants.PHONENUMBER_NOT_EMPTY);
        }

        User user = userMapper.selectUserByPhone(phoneNumber);
        // 用户不存在
        if (user == null) {
            throw new AccessDeniedException(ReturnConstants.ACCOUNT_NOT_EXIST);
        }

        // 未启用
        if (user.getEnabled() != null && user.getEnabled() == 0) {
            throw new AccessDeniedException(ReturnConstants.USER_IS_UNENABLED);
        }

        // 生成随机验证码
//        String verificationCode = SendCodeUtils.generateVerificationCode();
        String verificationCode = "000000";
        System.out.println("验证码:" + verificationCode);

        // 发送验证码
        log.info("发送验证码!");
        SendCodeUtils.verify(phoneNumber, verificationCode);

        // 存储验证码到Redis,设置有效期为5分钟
        String rediskey = secret + phoneNumber;
        redisTemplate.opsForValue().set(rediskey, verificationCode, verificationExpirationTime, TimeUnit.MINUTES); // 单位为分钟
        return ReturnConstants.CAPTCHA_SEND_SUCCESS;
    }

    /**
     * 从redis中获取验证码
     *  匹配
     * - 未通过
     * - 不存在或过期
     * - 存在且通过
     * @param userLoginDTO
     * @param request
     * @return
     * @throws SocketException
     * @throws UnknownHostException
     */
    @Override
    public Object login(UserLoginDTO userLoginDTO, HttpServletRequest request) throws SocketException, UnknownHostException {
        log.info("request:{}", request);
        String phoneNumber = userLoginDTO.getPhoneNumber();
        String remoteAddr = IpUtils.getIpAddr();// ip地址
        String macaddr = GetMacAddr.getLocalMac(remoteAddr);//mac地址

        //获取浏览器
        UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
        String browser = userAgent.getBrowser().getName();
        //获取操作系统
        String os = userAgent.getOperatingSystem().getName();
        //获取操作地点
        String location = AddressUtils.getRealAddressByIP(remoteAddr);

        log.info("remoteAddr:{}", remoteAddr);
        log.info("userAgent:{}", userAgent);

        // 从Redis中获取存储的验证码
        String redisKey = secret + phoneNumber;
        String storedCode = redisTemplate.opsForValue().get(redisKey);

        if (storedCode == null) {
            return AjaxResult.forbidden(ReturnConstants.CAPTCHA_NOT_EXIT);
        }

        if (!userLoginDTO.getVerificationCode().equals(storedCode)) {
            return AjaxResult.forbidden(ReturnConstants.CAPTCHA_IS_ERROR);
        }

        User user = userMapper.selectUserByPhone(phoneNumber);
        log.info("user信息:{}", user);
        if (user == null) {
            return AjaxResult.forbidden(ReturnConstants.USER_NOT_EXIST);
        }

        if (user.getEnabled() == 0){
            return AjaxResult.forbidden(ReturnConstants.USER_IS_UNENABLED);
        }

        // 获取用户信息
        Integer userId = user.getUserId();
        String userAccount = user.getUserAccount();
        String userName = user.getUsername();

        Logininfor logininfor = Logininfor.builder()
                .userAccount(userAccount)
                .userName(userName)
                .ipaddr(remoteAddr)
                .macAddr(macaddr)
                .browser(browser)
                .os(os)
                .loginLocation(location)
                .loginTime(new Date())
                .phoneNumber(user.getPhoneNumber())
                .enable(1)
                .wxBind(user.getWxBind())
                .status(1)
                .build();
        loginlogsMapper.insertTrinvLoginlogs(logininfor);

        // 生成token
        // JWT
        Map<String, Object> claims = new HashMap<>();
        String uuid = UUID.randomUUID().toString();
        claims.put(CLAIM_USER_ID, userId);
        claims.put(CLAIM_UUID, uuid);
        claims.put(CLAIM_PHONE_NUMBER, phoneNumber);
        claims.put(CLAIM_USER_ACCOUNT, userAccount);
        claims.put(CLAIM_USER_NAME, userName);
        claims.put(CLAIM_USER_AGENT, userAgent); // mac
        claims.put(CLAIM_REMOTE_ADDR, remoteAddr); // ip
        claims.put(CLAIM_OS, os); // os操作系统
        claims.put(CLAIM_MAC, macaddr); // mac地址
        claims.put(CLAIM_BROWSER, browser); // 浏览器名称
        String jwt = JwtUtils.createJWT(claims, secretKey);
        log.info("生成用户的JWT数据:{}", jwt);

        UserLoginInfoVO userLoginInfoVO = userMapper.getLoginInfoByUsername(userName);
        log.info("userLoginInfoVO:{}", userLoginInfoVO);

        List<GrantedAuthority> authorities = new ArrayList<>();
        // 获取角色关键字 用于后续权限判断
        List<String> rolekeys = userLoginInfoVO.getRolekeys();
        for (String rolekey : rolekeys) {
            authorities.add(new SimpleGrantedAuthority(rolekey));
        }
        String authoritiesJsonString = JSON.toJSONString(authorities);

        UserCachePO userCachePO = new UserCachePO();
        userCachePO.setEnable(userLoginInfoVO.getEnable());
        userCachePO.setAuthoritiesJsonString(authoritiesJsonString);
        userCachePO.setToken(jwt);

        // 转换hash数据类型,存入redis
        String jwtRedisKey = "JWT_Token:" + uuid;// 键
        HashOperations<String, Object, Object> opsForHash = redisTemplate.opsForHash();
        Map<String, Object> userLoginInfoMap = BeanUtil.beanToMap(userCachePO);
        opsForHash.putAll(jwtRedisKey, userLoginInfoMap);
        redisTemplate.expire(jwtRedisKey, 86400, TimeUnit.MINUTES);// 过期时间
        log.info("向缓存中存入用户状态数据:{}", userCachePO);

        // 返回登录结果VO
        UserLoginResultVO userLoginResultVO = new UserLoginResultVO()
                .setUserId(userId)
                .setUsername(userName)
                .setToken(jwt)
                .setAuthorities(rolekeys);
        return AjaxResult.success(userLoginResultVO);
    }
}

3.发送短信的API

此处代码有两个工具类

        // 生成随机验证码
        String verificationCode = SendCodeUtils.generateVerificationCode();
        System.out.println("验证码:" + verificationCode);

        // 发送验证码
        log.info("发送验证码!");
        SendCodeUtils.verify(phoneNumber, verificationCode);
package com.ruoyi.utils;

import com.aliyun.tea.TeaException;

import java.util.Random;

public class SendCodeUtils {

    public static String generateVerificationCode() {
        // 设置验证码长度为6
        int length = 6;
        // 验证码字符集
        String digits = "0123456789";
        Random random = new Random();
        StringBuilder sb = new StringBuilder();

        // 生成六位数验证码
        for (int i = 0; i < length; i++) {
            int index = random.nextInt(digits.length());
            sb.append(digits.charAt(index));
        }
        return sb.toString();
    }

    /**
     * <b>description</b> :
     * <p>使用AK&amp;SK初始化账号Client</p>
     * @return Client
     *
     * @throws Exception
     */
    public static com.aliyun.dysmsapi20170525.Client createClient() throws Exception {
        // 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。
        // 建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378657.html。
        com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
                // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。
                .setAccessKeyId("ALIBABA_CLOUD_ACCESS_KEY_ID")
                // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
                .setAccessKeySecret("ALIBABA_CLOUD_ACCESS_KEY_SECRET");
        // Endpoint 请参考 https://api.aliyun.com/product/Dysmsapi
        config.endpoint = "dysmsapi.aliyuncs.com";
        return new com.aliyun.dysmsapi20170525.Client(config);
    }

    public static String verify(String phoneNumber, String verificationCode) throws Exception {
//        java.util.List<String> args = java.util.Arrays.asList(args_);
        com.aliyun.dysmsapi20170525.Client client = SendCodeUtils.createClient();
        com.aliyun.dysmsapi20170525.models.SendSmsRequest sendSmsRequest = new com.aliyun.dysmsapi20170525.models.SendSmsRequest()
                .setPhoneNumbers(phoneNumber)
                .setSignName("签名名称")
                .setTemplateCode("模板Code")
                .setTemplateParam("{\"code\":\"" + verificationCode + "\"}");
        try {
            // 复制代码运行请自行打印 API 的返回值
            client.sendSmsWithOptions(sendSmsRequest, new com.aliyun.teautil.models.RuntimeOptions());
            return verificationCode;
        } catch (TeaException error) {
            // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
            // 错误 message
            System.out.println(error.getMessage());
            // 诊断地址
            System.out.println(error.getData().get("Recommend"));
            com.aliyun.teautil.Common.assertAsString(error.message);
            return null;
        } catch (Exception _error) {
            TeaException error = new TeaException(_error.getMessage(), _error);
            // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
            // 错误 message
            System.out.println(error.getMessage());
            // 诊断地址
            System.out.println(error.getData().get("Recommend"));
            com.aliyun.teautil.Common.assertAsString(error.message);
            return null;
        }
    }
}

之前需要的几个关键信息可以在此发挥用处

此处若是公司内部代码可将ALIBABA_CLOUD_ACCESS_KEY_ID和ALIBABA_CLOUD_ACCESS_KEY_SECRET的值直接填入对应位置

但若是代码可能会泄露,则还是建议在对应的环境中部署环境变量,代码运行时获取环境变量自动填入,涉及的Windows和Linux环境下的环境变量设置在后续文章中可找到,此处不多赘述。

// 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。
.setAccessKeyId(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"))
// 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
.setAccessKeySecret(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"));

4.以上操作和数据库都完成后,即可实现短信验证码的发送,登录时要求用户携带验证码,并与Redis中存储的验证码匹配即可通过校验。

到此这篇关于Java实现用户短信验证码登录功能的文章就介绍到这了,更多相关Java用户短信验证码登录内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文