一、概述
目前微信小程序或网站的登录方式大部分采取了微信扫码或短信验证码等方式,为什么短信验证码登录方式会受到互联网公司的青睐,因为其确实有许多好处:
- 方便快捷:用户无需记忆复杂的用户名和密码,只需通过短信验证码就可以快速登录。
- 安全性高:短信验证码通常是由运营商或第三方服务提供商发送的,具有较高的安全性。
- 防止恶意攻击:通过短信验证码登录可以有效防止恶意攻击,例如密码被盗、网站挂马等情况。
- 方便记忆:短信验证码可以使用户更方便地记忆密码,不用担心密码被遗忘。
- 兼容性好:短信验证码适用于各种类型的网站和应用程序,可以在不同的设备上使用。
综上所述,短信验证码登录是一种安全、方便、快捷的登录方式,有助于提高用户的使用体验。
本文主要讲解的是如何使用Redis实现短信验证码登录的过程。
如图:
二、登录流程
当用户注册完成后,用户去登录会去校验用户提交的手机号和验证码,是否一致,如果一致,则根据手机号查询用户信息,不存在则新建,最后将用户数据保存到redis,并且生成token作为redis的key,当我们校验用户是否登录时,会去携带着token进行访问,从redis中取出token对应的value,判断是否存在这个数据,如果没有则拦截,如果存在则将其保存到threadLocal中,并且放行。
流程图:
三、具体实现
1、前端页面
1.1 html部分
<div id="app">
<div class="login-container">
<div class="header">
<div class="header-back-btn" @click="goBack" ><i class="el-icon-arrow-left"></i></div>
<div class="header-title">手机号码快捷登录 </div>
</div>
<div class="content">
<div class="login-form">
<div style="display: flex; justify-content: space-between">
<el-input style="width: 60%" placeholder="请输入手机号" v-model="form.phone" >
</el-input>
<el-button style="width: 38%" @click="sendCode" type="success" :disabled="disabled">{{codeBtnMsg}}</el-button>
</div>
<div style="height: 5px"></div>
<el-input placeholder="请输入验证码" v-model="form.code">
</el-input>
<div style="text-align: center; color: #8c939d;margin: 5px 0">未注册的手机号码验证后自动创建账户</div>
<el-button @click="login" style="width: 100%; background-color:#f63; color: #fff;">登录</el-button>
<div style="text-align: right; color:#333333; margin: 5px 0"><a href="/login2.html">密码登录</a></div>
</div>
</div>
</div>
</div>
1.2 JS部分
发送验证码
sendCode(){
if (!this.form.phone) {
this.$message.error("手机号不能为空");
return;
}
// 发送验证码
axios.post("/user/code?phone="+this.form.phone)
.then(() => {})
.catch(err => {
console.log(err);
this.$message.error(err)
});
// 禁用按钮
this.disabled = true;
// 按钮倒计时
let i = 60;
this.codeBtnMsg = (i--) + '秒后可重发'
let taskId = setInterval(() => this.codeBtnMsg = (i--) + '秒后可重发', 1000);
setTimeout(() => {
this.disabled = false;
clearInterval(taskId);
this.codeBtnMsg = "发送验证码";
}, 59000)
}
提交登录
login(){
if(!this.radio){
this.$message.error("请先确认阅读用户协议!");
return
}
if(!this.form.phone || !this.form.code){
this.$message.error("手机号和验证码不能为空!");
return
}
axios.post("/user/login", this.form)
.then(({data}) => {
debugger
console.log("data=======" , data)
if(data){
// 保存用户信息到session
sessionStorage.setItem("token", data);
}
// 跳转到首页
location.href = "/index.html"
})
.catch(err => {
debugger
console.log("err=======" , err)
this.$message.error(err)
})
},
1.3 axios拦截器
let commonURL = "/api";
// 设置后台服务地址
axios.defaults.baseURL = commonURL;
axios.defaults.timeout = 2000;
// request拦截器,将用户token放入头中
let token = sessionStorage.getItem("token");
axios.interceptors.request.use(
config => {
if(token) config.headers['authorization'] = token
return config
},
error => {
return Promise.reject(error)
}
)
axios.interceptors.response.use(function (response) {
// 判断执行结果
if (!response.data.success) {
return Promise.reject(response.data.errorMsg)
}
return response.data;
}, function (error) {
// 一般是服务端异常或者网络异常
if(error.response.status == 401){
// 未登录,跳转
setTimeout(() => {
location.href = "/login.html"
}, 200);
return Promise.reject("请先登录");
}
return Promise.reject("服务器异常");
});
axios.defaults.paramsSerializer = function(params) {
let p = "";
Object.keys(params).forEach(k => {
if(params[k]){
p = p + "&" + k + "=" + params[k]
}
})
return p;
}
2、SpringBoot后端
2.1 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
2.2 属性配置
spring:
redis:
host: 127.0.0.1
port: 6379
password:
database: 3
lettuce:
pool:
max-active: 10
max-idle: 10
min-idle: 1
time-between-eviction-runs: 10s
2.3 发送验证码实现
/**
* 发送手机验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
return userService.sendCode(phone, session);
}
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public Result sendCode(String phone, HttpSession session) {
//1、验证是否合法
if(RegexUtils.isPhoneInvalid(phone)){
//2、如果不符合,返回错误信息
return Result.fail("手机不合法,格式错误!");
}
//3、符合,生成验证码
String randomNumbers = RandomUtil.randomNumbers(6);
//4、保证验证码到session
session.setAttribute(phone, randomNumbers);
redisTemplate.opsForValue().set(RedisKey.SMS_PRE + phone, randomNumbers, 2, TimeUnit.MINUTES);
//5、发送验证码
log.debug("发送验证码成功!验证码是:{}" , randomNumbers);
//6、返回ok
return Result.ok("发送成功!");
}
2.4 登录实现
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
return userService.login(loginForm, session);
}
- 1、验证是否合法
- 2、如果不符合,返回错误信息
- 3、验证验证码是否一致
- 4、根据手机号查询用户
- 5、不存在创建用户
- 6、生成token
- 7 、保存到Redis
- 8、设置token有效期
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1、验证是否合法
String phone = loginForm.getPhone();
if(RegexUtils.isPhoneInvalid(phone)){
//2、如果不符合,返回错误信息
return Result.fail("手机不合法,格式错误!");
}
// String smsCode = (String) session.getAttribute("smsCode");
String smsCode = redisTemplate.opsForValue().get(RedisKey.SMS_PRE + loginForm.getPhone());
//3、验证验证码是否一致
if(MyStrUtil.isEmpty(smsCode) || !smsCode.equals(loginForm.getCode())){
//4、如果不符合,返回错误信息
return Result.fail("验证码错误!");
}
//4、根据手机号查询用户
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>();
wrapper.eq(User::getPhone, phone);
User user = this.getOne(wrapper);
//5、创建用户
if(user == null){
user = new User();
user.setPhone(phone);
user.setPassword("123456");
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
user.setNickName(RedisKey.NICK_PRE + RandomUtil.randomString(5));
this.save(user);
}
//6、保存到session
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
//7 生成token
String token = UUID.randomUUID().toString(true);
// redisTemplate.opsForValue().set(RedisKey.LOGIN_USER, JSON.toJSONString(userDTO));
// session.setAttribute(RedisKey.LOGIN_USER, userDTO);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())
);
redisTemplate.opsForHash().putAll(RedisKey.TOKEN_PRE + token, userMap);
//token 1小时过期
redisTemplate.expire(RedisKey.TOKEN_PRE + token, 1, TimeUnit.HOURS);
return Result.ok(token);
}
2.5 获取登录用户信息
@GetMapping("/me")
public Result me(){
UserDTO user = UserHolder.getUser();
return Result.ok(user);
}
public class UserHolder {
private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
public static void saveUser(UserDTO user){
tl.set(user);
}
public static UserDTO getUser(){
return tl.get();
}
public static void removeUser(){
tl.remove();
}
}
2.6 登录用户信息拦截器
- 1 获取请求头中的token
- 2 根据token获取session中的用户
- 3 判断用户是否存在
- 4 如果存在,存入ThreadLocal
- 5 刷新token的有效期
public class LoginInterceptor implements HandlerInterceptor {
// @Autowired
private StringRedisTemplate redisTemplate;
public LoginInterceptor(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1 获取请求头中的token
String token = request.getHeader("authorization");
if(MyStrUtil.isEmpty(token)){
response.setStatus(401);
return false;
}
String key = RedisKey.TOKEN_PRE + token;
//2 根据token获取session中的用户
Map<Object, Object> userMap = redisTemplate.opsForHash().entries(key);
//3 判断用户是否存在
if(userMap == null || userMap.isEmpty()){
//5 不存在,拦截
response.setStatus(401);
return false;
}
UserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//4 如果存在,存入ThreadLocal
UserHolder.saveUser(user);
//7 刷新token的有效期
redisTemplate.expire(key, 1, TimeUnit.HOURS);
//8 放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
2.7 注册拦截器
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor(redisTemplate))
.addPathPatterns("/**")
//放行的路径
.excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/upload/**",
"/voucher/**"
);
}
}
四、总结
Redis代替session的业务流程主要有以下优点:
- 高度可配置性:Redis可以通过配置参数来灵活地控制数据的存储方式、过期时间、过期后的自动清除等,这使得Redis可以很容易地替换掉传统的session实现。
- 数据持久化:Redis支持将数据持久化到磁盘,这意味着即使在服务器关闭的情况下,数据也不会丢失。这对于需要保证数据安全性的应用场景非常重要。
- 支持高并发:Redis可以支持高并发请求,因为它的内存数据结构比较紧凑,并且支持多个读写操作同时进行,这使得Redis适合于高并发的应用场景。
- 支持分布式:Redis可以实现分布式部署,这使得多个Redis实例可以部署在不同的机器上,从而实现分布式应用。
- 方便的操作语法:Redis提供了简单直观的命令操作语法,这使得Redis的使用比较简单,并且不需要学习复杂的Session技术。
五、源码下载
gitee.com/charlinchenlin/koo-erp