Redis实现短信登录
获取验证码控制层
/**
* 发送手机验证码
*/
@PostMapping("/code")
public Result sendCode(@RequestParam("phone") String phone) {
// TODO 发送短信验证码并保存验证码
return userService.sendCode(phone);
}
获取验证码服务层
Result sendCode(String phone);
@Override
public Result sendCode(String phone) {
//1.校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
//2.如果不符合,返回错误信息
return Result.fail("手机号格式错误");
}
//3.符合,生成验证码
String code = RandomUtil.randomNumbers(6);
// //4.保存验证码到session,用于用户登录时,将输入的验证码与服务端存入的验证码比对
// session.setAttribute("code",code);
//4.保存验证码到redis
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
//5.发送验证码 真正上线项目使用阿里云的发送验证码
log.debug("发送短信验证码成功,验证码:{}",code);
//返回
return Result.ok();
}
登录控制层
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm){
// TODO 实现登录功能
return userService.login(loginForm);
}
登录服务层
Result login(LoginFormDTO loginForm);
@Override
public Result login(LoginFormDTO loginForm) {
//校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误");
}
// //校验验证码
// Object cacheCode = session.getAttribute("code");
// 从redis获取验证码并校验
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
if (cacheCode == null || !cacheCode.equals(loginForm.getCode())) {
//不一致,报错
return Result.fail("验证码错误");
}
//一致,根据手机号查询用户
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("phone", phone);
User user = userMapper.selectOne(queryWrapper);
//判断用户是否存在
if(user == null){
//不存在,创建新用户并保存 调用该类下面的方法
user = createUserWithPhone(phone);
}
// //保存用户信息到session
// session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
//7保存用户信息到redis中
//7.1 随机生成token,作为登录令牌
String token = UUID.randomUUID().toString(true);
//7.2 将User对象转为HashMap存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
CopyOptions.create() //转换为map时,将userDTO中的Long转为String,否则会出现Long转String错误
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName,fieldVlaue) ->fieldVlaue.toString()));
//7.3 存储
stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY + token,userMap);
//7.4设置token有效期,putAll不能同时设置有效期
stringRedisTemplate.expire(LOGIN_USER_KEY + token,LOGIN_USER_TTL,TimeUnit.MINUTES);
//返回token
return Result.ok(token) ;
}
private User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
userMapper.insert(user);
return user;
}
手机号、邮箱、密码、验证码校验工具类
public abstract class RegexPatterns {
/**
* 手机号正则
*/
public static final String PHONE_REGEX = "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$";
/**
* 邮箱正则
*/
public static final String EMAIL_REGEX = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$";
/**
* 密码正则。4~32位的字母、数字、下划线
*/
public static final String PASSWORD_REGEX = "^\\w{4,32}$";
/**
* 验证码正则, 6位数字或字母
*/
public static final String VERIFY_CODE_REGEX = "^[a-zA-Z\\d]{6}$";
}
public class RegexUtils {
/**
* 是否是无效手机格式
* @param phone 要校验的手机号
* @return true:符合,false:不符合
*/
public static boolean isPhoneInvalid(String phone){
return mismatch(phone, RegexPatterns.PHONE_REGEX);
}
/**
* 是否是无效邮箱格式
* @param email 要校验的邮箱
* @return true:符合,false:不符合
*/
public static boolean isEmailInvalid(String email){
return mismatch(email, RegexPatterns.EMAIL_REGEX);
}
/**
* 是否是无效验证码格式
* @param code 要校验的验证码
* @return true:符合,false:不符合
*/
public static boolean isCodeInvalid(String code){
return mismatch(code, RegexPatterns.VERIFY_CODE_REGEX);
}
// 校验是否不符合正则格式
private static boolean mismatch(String str, String regex){
if (StrUtil.isBlank(str)) {
return true;
}
return !str.matches(regex);
}
}
RedisConstants工具类
public static final String LOGIN_CODE_KEY = "login:code:";
public static final Long LOGIN_CODE_TTL = 2L;
public static final String LOGIN_USER_KEY = "login:token:";
public static final Long LOGIN_USER_TTL = 36000L;
public static final String USER_NICK_NAME_PREFIX = "user_";
User与UserDTO实体类
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 手机号码
*/
private String phone;
/**
* 密码,加密存储
*/
private String password;
/**
* 昵称,默认是随机字符
*/
private String nickName;
/**
* 用户头像
*/
private String icon = "";
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
@Data
public class UserDTO {
private Long id;
private String nickName;
private String icon;
}
解决状态登录刷新的问题
配置拦截器
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns("/user/code","/user/login","/blog/hot","/shop/**","/voucher/**","/upload/**","/shop-type/**").order(1);
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate))
.addPathPatterns("/**").order(0);//order值越小,优先级越高或者按默认顺序执行
}
}
登录拦截器
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断是否需要拦截(ThreadLocal中是否有用户)
if(UserHolder.getUser() == null){
//没有,需要拦截,设置状态码
response.setStatus(401);
//拦截
return false;
}
//有用户,放行
return true;
}
}
刷新token拦截器
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// //获取session
// HttpSession session = request.getSession();
//获取请求头中的token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
return true;
}
// //获取session中用户
// Object user = session.getAttribute("user");
//基于TOKEN获取redis中的用户
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);
//判断用户是否存在
if(userMap.isEmpty()) {
return true;
}
//将Map转回为对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//存在,保存用户信息到ThreadLocal
UserHolder.saveUser(userDTO);
//刷新token的有效期
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
//放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
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();
}
}