Redis实现短信登录的企业实例分析

一、导入黑马点评项目

黑马点评项目主要包括以下功能:

1. 导入SQL

需要项目资料的私信我

Redis实现短信登录,蜜芽商城Enterprise级别企业案例剖析

其中的表有:

  • tb_user:用户表

  • tb_user_info:用户详情表

  • tb_shop:商户信息表

  • tb_shop_type:商户类型表

  • tb_blog:用户日记表(达人探店日记)

  • tb_follow:用户关注表

  • tb_voucher:优惠券表

  • tb_voucher_order:优惠券的订单表

注意:Mysql的版本采用5.7及以上版本

2. 前后端分离

3. 导入后端项目

3.1 将后端项目导入到 Idea 中

3.2 注意:修改application.yaml文件中的mysql、redis地址信息 将mysql、redis地址信息修改为自己的信息

3.3 启动项目 启动项目后,在浏览器访问:http://localhost:8081/shop-type/list ,如果可以看到数据则证明运行没有问题

4. 导入前端项目

4.1 导入nginx文件夹 将nginx文件夹复制到任意目录,要确保该目录不包含中文、特殊字符和空格,例如:

4.2 运行前端项目 在nginx所在目录下打开一个CMD窗口,输入命令启动nginx:

start nginx.exe

打开chrome浏览器,在空白页面点击鼠标右键,选择检查,即可打开开发者工具:

然后访问: http://127.0.0.1:8080 ,即可看到页面:

二、基于Session实现登录流程

  • 后端将生成的验证码和用户信息保存到session中,并将sessionId返回给前端保存到cookie中

  • 用户登录时,会携带cookie向后端发起请求,后端进行校验时,从cookie中获取sessionId,通过sessionId可以从session中获取用户信息并保存到ThreadLocal中

  • 后续每个线程都有一份ThreadLocal中的用户副本信息,不同线程拿到用户信息后可以实现不同的操作,从而起到线程隔离作用

1. 发送短信验证码

主要代码:

@Slf4j
@RestController
@RequestMapping("
/user"
)
public class UserController {

@Resource
private IUserService userService;


/**
* 发送手机验证码
*/
@PostMapping("
code"
)
public Result sendCode(@RequestParam("
phone"
) String phone, HttpSession session) {
// 发送短信验证码并保存验证码
return userService.sendCode(phone, session);

}
} @Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<
UserMapper, User>
implements IUserService {

@Override
public Result sendCode(String phone, HttpSession session) {
// 1.使用工具类校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("
手机号格式错误!"
);

}
// 3.符合,生成验证码
String code = RandomUtil.randomNumbers(6);


// 4.保存验证码到 session
session.setAttribute("
code"
,code);


// 5.模拟发送验证码
log.debug("
发送短信验证码成功,验证码:{}"
, code);

// 返回ok
return Result.ok();

}
} 2. 短信验证码登录、注册

主要代码:

UserController

/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("
/login"
)
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// 实现登录功能
return userService.login(loginForm, session);

}

UserServiceImpl

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1.校验手机号
String phone = loginForm.getPhone();

if (RegexUtils.isPhoneInvalid(phone)) {
// 如果不符合,返回错误信息
return Result.fail("
手机号格式错误!"
);

}

// 2.校验验证码
Object cacheCode = session.getAttribute("
code"
);

String code = loginForm.getCode();

if (cacheCode == null || !cacheCode.toString().equals(code)) {
// 3.验证码不一致,则报错
return Result.fail("
验证码错误"
);

}

// 4.验证码一致,根据手机号查询用户
User user = query().eq("
phone"
, phone).one();


// 5.判断用户是否存在
if (user == null) {
// 6.用户不存在,则创建用户并保存
user = createUserWithPhone(phone);

}

// 7.保存用户信息到session中,UserDTO只包含简单的用户信息,
// 而不是完整的User,这样可以隐藏用户的敏感信息(例如:密码等),还能减少内存使用
session.setAttribute("
user"
, BeanUtil.copyProperties(user, UserDTO.class));


// 8.返回ok
return Result.ok();

}

private User createUserWithPhone(String phone) {
// 1.创建用户
User user = new User();

user.setPhone(phone);

// 随机设置昵称 user_mrkuw05lok
user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));

// 2.保存用户
save(user);

return user;

} 3. 登录验证功能

用户请求登录时,会携带cookie,cookie中包含JSEESIONID

为了避免用户请求每个controller时,每次都去校验用户信息,所以可以加拦截器

拦截器只需在用户请求访问时,校验一次后将用户信息保存到ThreadLocal中,供后续线程使用

主要代码:

在工具类中编写ThreadLocal

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();

}
}

在工具类中编写登录拦截器

public class LoginInterceptor implements HandlerInterceptor {

/**
* 前置拦截
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.获取session
HttpSession session = request.getSession();

// 2.获取session中的用户
Object user = session.getAttribute("
user"
);

// 3.判断用户是否存在
if(user == null){
// 4.不存在,拦截,返回401状态码
response.setStatus(401);

return false;

}
// 5.存在,保存用户信息到ThreadLocal
UserHolder.saveUser((User)user);

// 6.放行
return true;

}

/**
* 后置拦截器
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
// 请求结束后移除用户,防止ThreadLocal造成内存泄漏
UserHolder.removeUser();

}
}

在配置类中添加拦截器配置类

@Configuration
public class MvcConfig implements WebMvcConfigurer {

/**
* 添加拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 登录拦截器
registry.addInterceptor(new LoginInterceptor())
// 排除不需要拦截的路径
.excludePathPatterns(
"
/shop/**"
,
"
/voucher/**"
,
"
/shop-type/**"
,
"
/upload/**"
,
"
/blog/hot"
,
"
/user/code"
,
"
/user/login"

);

}
}

UserController

@GetMapping("
/me"
)
public Result me(){
// 获取当前登录的用户并返回
UserDTO user = UserHolder.getUser();

return Result.ok(user);

} 三、集群的session共享问题

四、基于Redis实现共享session的登录功能1. 选择合适的数据结构存入Redis
  • 手机号作为key,String类型的验证码作为value

  • 用户登录时正好会提交手机号,方便通过Redis进行校验验证码

token作为key,Hash类型的用户信息作为value

后端校验成功后,会返回token给前端,前端会将token保存到sessionStorage中(这是浏览器的存储方式),以后前端每次请求都会携带token,方便后端通过Redis校验用户信息

前端代码:将后端返回的token保存到sessionStorage中

前端每次请求时,都会通过拦截器将token设置到请求头中,赋值给变量authorization,后端通过authorization获取前端携带的token进行校验

2. 发送短信验证码

修改之前代码,将验证码存入Redis

@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<
UserMapper, User>
implements IUserService {

@Resource
private StringRedisTemplate stringRedisTemplate;


@Override
public Result sendCode(String phone, HttpSession session) {
// 1.使用工具类校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("
手机号格式错误!"
);

}
// 3.符合,生成验证码
String code = RandomUtil.randomNumbers(6);


// 4.保存验证码到 session
// session.setAttribute("
code"
,code);

// 4.保存验证码到 redis
// "
login:code:"
是业务前缀,以"
login:code:"
+ 手机号为key,过期时间2分钟
stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);


// 5.模拟发送验证码
log.debug("
发送短信验证码成功,验证码:{}"
, code);

// 返回ok
return Result.ok();

}
} 3. 短信验证码登录、注册
  • 修改之前代码,从Redis获取验证码并校验

  • 随机生成token,保存用户信息到redis中,返回token

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1.校验手机号
String phone = loginForm.getPhone();

if (RegexUtils.isPhoneInvalid(phone)) {
// 如果不符合,返回错误信息
return Result.fail("
手机号格式错误!"
);

}

// // 2.校验验证码
// Object cacheCode = session.getAttribute("
code"
);

// String code = loginForm.getCode();

// if (cacheCode == null || !cacheCode.toString().equals(code)) {
// // 3.验证码不一致,则报错
// return Result.fail("
验证码错误"
);

// }

// 2.从Redis获取验证码并校验
String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);

String code = loginForm.getCode();

if (cacheCode == null || !cacheCode.equals(code)) {
// 3.验证码不一致,则报错
return Result.fail("
验证码错误"
);

}

// 4.验证码一致,根据手机号查询用户
User user = query().eq("
phone"
, phone).one();


// 5.判断用户是否存在
if (user == null) {
// 6.用户不存在,则创建用户并保存
user = createUserWithPhone(phone);

}

// // 7.保存用户信息到session中,UserDTO只包含简单的用户信息,而不是完整的User,这样可以隐藏用户的敏感信息(例如:密码等),还能减少内存使用
// session.setAttribute("
user"
, BeanUtil.copyProperties(user, UserDTO.class));


// 7.保存用户信息到redis中
// 7.1随机生成token,作为登录令牌
// 使用hutool工具中的UUID,true表示不带“-”符号的UUID
String token = UUID.randomUUID().toString(true);


// 7.2将User对象转为Hash类型进行存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);

// 由于使用的是stringRedisTemplate,所以存入的value中的值必须都是String类型的
// 但是UserDTO中的id是Long类型的,所以进行对象属性拷贝时,需要自定义实现转换规则
Map<
String, Object>
userMap = BeanUtil.beanToMap(userDTO, new HashMap<
>
(),CopyOptions.create().setIgnoreNullValue(true).setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) ->
fieldValue.toString()));


// 7.3存入redis, "
login:token:"
是业务前缀,以 "
login:token:"
+ token作为key
stringRedisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY + token, userMap);

// 7.4设置token有效期,有效期为30分钟
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);


// 8.返回token
return Result.ok(token);

}

private User createUserWithPhone(String phone) {
// 1.创建用户
User user = new User();

user.setPhone(phone);

// 随机设置昵称 user_mrkuw05lok
user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));

// 2.保存用户
save(user);

return user;

} 4. 解决token刷新问题
  • token刷新问题是指,用户长时间不进行界面操作时,到了过期时间,token自动失效;但是,用户一旦进行操作,就需要给token续期,即更新token过期时间

  • 为了解决token刷新问题,需要加2个拦截器

  • 第一个拦截器可以拦截所有请求,只要用户有请求就刷新token,并保存用户信息到ThreadLocal中

  • 第二个拦截器只对登录请求进行拦截,从ThreadLocal中获取用户信息进行校验

刷新token的拦截器代码:

public class RefreshTokenInterceptor implements HandlerInterceptor {

// 因为LoginInterceptor不是通过Spring进行管理的Bean,所以不能再LoginInterceptor中进行注入StringRedisTemplate
// 可以通过构造方法传入StringRedisTemplate
private StringRedisTemplate stringRedisTemplate;


public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;

}

/**
* 前置拦截
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// // 1.获取session
// HttpSession session = request.getSession();

// // 2.获取session中的用户
// Object user = session.getAttribute("
user"
);

// // 3.判断用户是否存在
// if(user == null){
// // 4.不存在,拦截,返回401状态码
// response.setStatus(401);

// return false;

// }
// // 5.存在,保存用户信息到ThreadLocal
// UserHolder.saveUser((UserDTO)user);

// // 6.放行
// return true;


// 1.获取请求头中的token
String token = request.getHeader("
authorization"
);

if (StrUtil.isBlank(token)) {
// 不存在,则拦截,返回401状态码
response.setStatus(401);

return false;

}

// 2.通过token获取redis中的用户
Map<
Object, Object>
userMap = stringRedisTemplate.opsForHash()
.entries(RedisConstants.LOGIN_USER_KEY + token);


// 3.判断用户是否存在
if (userMap.isEmpty()) {
// 4.用户不存在,则拦截,返回401状态码
response.setStatus(401);

return false;

}

// 5.将redis中Hash类型数据转换成UserDTO对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);


// 6.用户存在,保存用户信息到ThreadLocal
UserHolder.saveUser(userDTO);


// 7.刷新token有效期
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);


// 8.放行
return true;

}

/**
* 后置拦截器
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
// 请求结束后移除用户,防止ThreadLocal造成内存泄漏
UserHolder.removeUser();

}
}

登录拦截器的代码:

public class LoginInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.判断是否需要拦截(ThreadLocal中是否有用户)
if (UserHolder.getUser() == null) {
// 没有,需要拦截,设置状态码
response.setStatus(401);

// 拦截
return false;

}
// 有用户,则放行
return true;

}

} @Configuration
public class MvcConfig implements WebMvcConfigurer {

@Resource
private StringRedisTemplate stringRedisTemplate;


/**
* 添加拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 登录拦截器
registry.addInterceptor(new LoginInterceptor())
// 排除不需要拦截的路径
.excludePathPatterns(
"
/shop/**"
,
"
/voucher/**"
,
"
/shop-type/**"
,
"
/upload/**"
,
"
/blog/hot"
,
"
/user/code"
,
"
/user/login"

).order(1);


// token刷新的拦截器,order越小,执行优先级越高,所以token刷新的拦截器先执行
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("
/**"
)
.excludePathPatterns(
// RefreshTokenInterceptor拦截器也需要放行"
/user/code"
,"
/user/login"
,不然token过期后再重新登录就会一直被拦截
"
/user/code"
,
"
/user/login"
)
.order(0);

}
}

蜜芽商城一直不断地追求更好的用户使用体验,早在2015年初,就已经推出了短信登录功能,该功能不仅方便了用户注册和登录操作,还大大提高了用户的使用体验。但面对日益增长的用户量和并发量,如何确保快速、稳定地响应用户请求呢?
一、背景
作为全球领先的婴童母婴品牌电商,蜜芽商城拥有数百万注册用户和大量的访问量。登录是用户访问蜜芽商城的基本需求,传统的帐号、密码登录方式,容易存在帐号泄露、密码忘记等问题。而且,在安全性上,传统方式的用户密码等信息容易被恶意破解,导致重大信息泄露事件的发生。
二、方案
为此,蜜芽商城企业针对以上问题,采用Redis实现短信登录方案,进一步短信快速登录方案的流程,提高用户登录的易用性和安全性。
三、Redis介绍
Redis是一个开源的数据结构服务器,它可以存储键空间中映射到不同类型的值,支持多种数据结构,如字符串、散列表、列表、集合、有序集合等等。除此之外,Redis还提供发布与订阅、Lua脚本、事务等特性,支持分布式、持久化和很高的灵活性。
四、Redis实现短信登录技术细节
蜜芽商城企业使用Redis实现短信登录的核心技术细节包括:
1. 发送短信验证码
用户输入手机号码后,然后点击发送验证码,服务器通过Redis的setex命令设置该手机号作为key,验证码作为value,并设置有效期60秒。短信验证码的发送方面采用大厂提供的第三方服务,完成短信的发送。
2. 验证码验证
用户输入收到的验证码后,服务器会对Redis中的key-value进行匹配,如果验证码和手机号匹配成功,则返回用户登录成功。
五、方案成效
采用Redis实现短信登录解决了用户常见的账号泄露问题,用户只需要输入自己的手机号和短信验证码,就可以登录平台,登录过程更加便捷、快速。此外,上述方案对用户信息的安全性也有了更好的保障。
六、未来展望
随着电商行业的不断发展,蜜芽商城企业将不断完善、优化、升级Redis实现短信登录的方案,探索出更加符合用户需求的登录方式,构筑更加高效、安全的大数据处理平台。
七、总结
本次案例分析提到了Redis实现短信登录的方案,这种方案能够帮助企业充分发挥Redis的功能优势,同时为用户提供便捷、快速的登录方式。可以说,Redis实现短信登录逐渐成为新兴企业实现精准营销、数据分析等重要模块的技术手段之一。