【Redis_2】短信登录
一、基于Session实现登录
RegexUtils
:是定义的关于一些格式的正则表达式的工具箱
package com.hmdp.utils;
import cn.hutool.core.util.StrUtil;
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);
}
}
Result
:返回的结果类
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
private Boolean success;
private String errorMsg;
private Object data;
private Long total;
public static Result ok(){
return new Result(true, null, null, null);
}
public static Result ok(Object data){
return new Result(true, null, data, null);
}
public static Result ok(List<?> data, Long total){
return new Result(true, null, data, total);
}
public static Result fail(String errorMsg){
return new Result(false, errorMsg, null, null);
}
}
(一)发送短信验证码
userContorller
:
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private IUserService userService;
@Resource
private IUserInfoService userInfoService;
/**
* 发送手机验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
// 发送短信验证码并保存验证码
return userService.sendCode(phone, session);
}
}
userServiceImpl
:
@Override
public Result sendCode(String phone, HttpSession session) {
//1、校验手机号
if(RegexUtils.isPhoneInvalid(phone)){
//2、如果不符合,返回错误信息
return Result.fail("手机号格式错误");
}
//3、符合,生成验证码(随机生成器hutool)
String code = RandomUtil.randomNumbers(6);
//4、保存验证码到session
session.setAttribute("code" ,code);
//5、发送验证码
log.info("短信验证码发送成功,验证码:{}",code);
//6、返回ok
return Result.ok();
}
(二)短信验证码登录、注册
import lombok.Data;
@Data
public class LoginFormDTO {
private String phone;
private String code;
private String password;
}
import lombok.Data;
//用作在控制台输出时显示少量信息
//避免显示敏感信息
@Data
public class UserDTO {
private Long id;
private String nickName;
private String icon;
}
userController
中:
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// TODO 实现登录功能
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、一致,根据手机号查询用户 select * form tb_user where phone =
User user = query().eq("phone", phone).one();
//5、判断用户是否存在
if(user == null){
//不存在,就创建一个新用户并保存
user = createUserWithPhone(phone);
}
//保存用户在session中
session.setAttribute("user", BeanUtil.copyProperties(user,UserDTO.class));
return Result.ok();
}
private User createUserWithPhone(String phone) {
//创建用户
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomNumbers(10));
//保存用户
save(user);
return user;
}
(三)校验登录状态
拦截器
:
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static net.sf.jsqlparser.util.validation.metadata.NamedObject.user;
public class LoginInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
@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);
//放行
return true;
}
}
配置拦截器
:
package com.hmdp.config;
import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
/*
* 配置拦截器
* */
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
//添加拦截器
public void addInterceptors(InterceptorRegistry registry) {
//登录拦截器
registry.addInterceptor(new LoginInterceptor())//拦截部分
.excludePathPatterns(
"/shop/**",
"/shop-type/**",
"/upload/**",
"/voucher/**",
"/blog/hot",
"/user/code",
"/user/login",
"/voucher/seckill"
);
}
}
二、基于Redis实现共享Session登录
集群的Session共享问题:多台Tomcat不共享session存储空间,切换tomcat时会导致数据丢失的问题。
常量类:
package com.hmdp.utils;
public class 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 = 30L;
public static final Long CACHE_NULL_TTL = 2L;
public static final Long CACHE_SHOP_TTL = 30L;
public static final String CACHE_SHOP_KEY = "cache:shop:";
public static final String LOCK_SHOP_KEY = "lock:shop:";
public static final Long LOCK_SHOP_TTL = 10L;
public static final String SECKILL_STOCK_KEY = "seckill:stock:";
public static final String BLOG_LIKED_KEY = "blog:liked:";
public static final String FEED_KEY = "feed:";
public static final String SHOP_GEO_KEY = "shop:geo:";
public static final String USER_SIGN_KEY = "sign:";
}
userServiceImpl
:
package com.hmdp.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import com.hmdp.mapper.UserMapper;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RegexUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.hmdp.utils.RedisConstants.*;
import static com.hmdp.utils.SystemConstants.USER_NICK_NAME_PREFIX;
/**
* <p>
* 服务实现类
* </p>
*
*/
@Slf4j
@Service
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.保存验证码到 redis
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
// 5.发送验证码
log.debug("发送短信验证码成功,验证码:{}", code);
// 返回ok
return Result.ok();
}
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1.校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 3.从redis获取验证码并校验
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.equals(code)) {
// 不一致,报错
return Result.fail("验证码错误");
}
// 4.一致,根据手机号查询用户 select * from tb_user where phone = ?
User user = query().eq("phone", phone).one();
// 5.判断用户是否存在
if (user == null) {
// 6.不存在,创建新用户并保存
user = createUserWithPhone(phone);
}
// 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()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
// 7.3.存储
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
// 7.4.设置token有效期
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8.返回token
return Result.ok(token);
}
/*
@Override
public Result sendCode(String phone, HttpSession session) {
//1、校验手机号
if(RegexUtils.isPhoneInvalid(phone)){
//2、如果不符合,返回错误信息
return Result.fail("手机号格式错误");
}
//3、符合,生成验证码(随机生成器hutool)
String code = RandomUtil.randomNumbers(6);
//4、保存验证码到session
session.setAttribute("code" ,code);
//5、发送验证码
log.info("短信验证码发送成功,验证码:{}",code);
//6、返回ok
return Result.ok();
}
*/
/*@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、一致,根据手机号查询用户 select * form tb_user where phone =
User user = query().eq("phone", phone).one();
//5、判断用户是否存在
if(user == null){
//不存在,就创建一个新用户并保存
user = createUserWithPhone(phone);
}
//保存用户在session中
session.setAttribute("user", BeanUtil.copyProperties(user,UserDTO.class));
return Result.ok();
}*/
private User createUserWithPhone(String phone) {
//创建用户
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomNumbers(10));
//保存用户
save(user);
return user;
}
}
拦截器
:
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static net.sf.jsqlparser.util.validation.metadata.NamedObject.user;
public class LoginInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
/* public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}*/
@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);
//放行
return true;*/
//----------------------------------------------------------redis-------------------------------------------------------------
//1 获取请求头中的token
String token = request.getHeader("authorization");
if(StrUtil.isBlank(token)){
//不存在,拦截,返回401状态码
response.setStatus(401);
return false;
}
//2 基于TOKEN获取redis中的用户
String key = RedisConstants.LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
//3 判断用户是否存在
if(userMap.isEmpty()){
//4 不存在,拦截,返回401状态码
response.setStatus(401);
return false;
}
//5 将查询到的hash数据转化为UserDTO
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//6 存在,保存用户信息到Threadlocal
UserHolder.saveUser(userDTO);
//7 刷新token 有效期
stringRedisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
//8 放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//移除用户
UserHolder.removeUser();
}
}
调用拦截器
:
package com.hmdp.config;
import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
/*
* 配置拦截器
* */
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
//添加拦截器
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
.excludePathPatterns(
"/shop/**",
"/shop-type/**",
"/upload/**",
"/voucher/**",
"/blog/hot",
"/user/code",
"/user/login"
);
}
}