文章目录
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)登录
处理逻辑:
- 校验⽤户输⼊的短信验证码是否正确,如果验证码错误则登录失败
- 如果验证码正确,则判断当前⽤户是否为会员,如果不是会员则⾃动完成会员注册
- 登陆成功,则删除Redis缓存的验证码
- 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("验证码输入错误!");
}