短信登录
基于session的短信登录
-
发送验证码
-
校验手机号码合法性(正则表达式)
-
保存验证码在session中
-
session.setAttribute("code",code)
-
-
-
登录,验证码校验 ,注册
-
获取验证码session,一致则根据电话号码查询数据库,无,则新建用户
-
已登录用户信息,存在用户session中
-
session.setAttribute("user" , BeanUtil.copyProperties(user,UserDTO.class)) // user复制到UserDTO中
-
-
-
登录状态校验
- 用户请求时,从cookie中携带JsessionId到后台,通过JsessionId从session中获取用户信息
- 没有session信息,则拦截
- 有session信息,则将用户信息保存在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();
}
}
- 定义:
- ThreadLocal 是 Java 中的一个类,用于实现线程的局部变量。
- 作用:
- ThreadLocal 提供了一种在多线程环境下实现线程私有数据的机制。
- 每个线程都可以独立地访问和修改自己的局部变量,互不干扰。
- 原理:
- 每个 ThreadLocal 对象维护一个独立的变量副本,存储在各个线程的 ThreadLocalMap 中。
- 线程通过 ThreadLocal 对象的 get() 方法获取自己的局部变量值,通过 set() 方法设置局部变量值。
- 内存泄漏:
- ThreadLocal 使用弱引用来引用 ThreadLocal 对象,但是值仍然使用强引用。
- 如果没有及时清理 ThreadLocal 变量,即调用 remove() 方法,可能导致内存泄漏。
- 内存泄漏发生在 ThreadLocalMap 中,当 ThreadLocal 对象被回收时,对应的值仍然存在于 ThreadLocalMap 中。
发送验证码
@Override
public Result sendCode(String phone, HttpSession session) {
// 1.校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 3.符合,生成验证码
String code = RandomUtil.randomNumbers(6); //生成验证码,这里使用的是hutool工具类的随机数生成方法,参数表示生成6位数验证码
// 4.保存验证码到 session中
session.setAttribute("code",code);
// 5.发送验证码
log.debug("发送短信验证码成功,验证码:{}", code);
// 返回ok
return Result.ok();
}
String code = RandomUtil.randomNumbers(6); //生成验证码,这里使用的是hutool工具类的随机数生成方法,参数表示生成6位数验证码
登录
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1.校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 3.校验验证码
Object cacheCode = session.getAttribute("code");
String code = loginForm.getCode();
if(cacheCode == null || !cacheCode.toString().equals(code)){
//3.不一致,报错
return Result.fail("验证码错误");
}
//一致,根据手机号查询用户
User user = query().eq("phone", phone).one();
//5.判断用户是否存在
if(user == null){
//不存在,则创建
user = createUserWithPhone(phone);
}
//7.保存用户信息到session中
session.setAttribute("user",user);
return Result.ok();
}
//创建新用户
private User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX+RandomUtil.randomNumbers(10)); // 设置常量user_
save(user)
return user;
}
登录验证功能,登录拦截器
public class LoginInterceptor implements HandlerInterceptor {
@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;
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//移除
UserHolder.removeUser();
}
自定义拦截器
- 实现HandlerInterceptor 接口
- 重新 preHandle(在请求处理之前) postHandle(请求处理之后的操作) afterCompletion(请求完成后的操作)
- 拦截器注册到Spring Boot应用中
- @Configuration
- 实现WebMvcConfigurer接口
- register注册
- excludePathPatterns
- oder优先级
让拦截器生效
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//登录拦截器
registry.addInterceptor(new LoginInterceptor()) // 进行拦截器的注册
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
)
}
}
隐藏用户敏感信息
在UserHolder处:将user对象换成UserDTO
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();
}
}
在登录方法处修改
//7.保存用户信息到session中
session.setAttribute("user", BeanUtils.copyProperties(user,UserDTO.class));
BeanUtils.copyProperties(user,UserDTO.class)); // hutool 工具包 工具类 uesr – 》UserDTO.class
在登录拦截器处:
//5.存在,保存用户信息到Threadlocal
UserHolder.saveUser((UserDTO) user);
session共享问题
基于Redis实现登录流程
整体访问流程
key值:生成token作为redis的key
1、key要具有唯一性
2、key要方便携带
发送验证码
//发送验证码
@Override
public Result sendCode(String phone, HttpSession session) {
//1.校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误");
}
//2.生成验证码
String code = RandomUtil.randomNumbers(6);
//3.保存验证码道session中
// session.setAttribute("code",code);
//3.以手机号为KEY保存验证码到Redis中 key=login:code:13168320031 value =
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
//4.发送验证码
log.debug("发送短信验证码成功,验证码:{} ",code);
return Result.ok();
}
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone,code,2, TimeUnit.MINUTES); // 设置k v 获取时间
登录实现
@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); //true 是无符号
// 7.2.将User对象转为HashMap存储 将 UserDTO 对象中的属性名和属性值转换为 Map 中的键和值。
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); //putAll()方法用于将一个 HashMap中的键值对批量存储到Redis的Hash结构中。
// 7.4.设置token有效期
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8.返回token
return Result.ok(token);
}
将用户信息存储到 Redis 中:
- 生成随机的登录令牌(token)作为键。
- 将 User 对象转换为 UserDTO 对象,然后将 UserDTO 转换为 HashMap 存储用户信息。
- 使用 Redis 的 Hash 数据结构将用户信息存储到 Redis 中,以 token 作为键。
- expire设置登录令牌的有效期。
登录拦截器优化
RefreshTokenInterceptor
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 {
// 1.获取请求头中的token
String token = request.getHeader("authorization"); // 在登录成功的时候,返回了token值
if (StrUtil.isBlank(token)) {
return true;
}
// 2.基于TOKEN获取redis中的用户
String key = LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key); // entries() 方法用于获取指定键的所有字段和值。
// 3.判断用户是否存在
if (userMap.isEmpty()) {
return true;
}
// 5.将查询到的hash数据转为UserDTO
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 6.存在,保存用户信息到 ThreadLocal
UserHolder.saveUser(userDTO);
// 7.刷新token有效期
stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户
UserHolder.removeUser();
}
}
string token = request.getHeader("authorization")
返回的是 token 是因为在 HTTP 请求的头部中,常用的一种认证方式是将身份凭证(如 token)放置在 Authorization 头部中传递。这是一种标准的 HTTP 头部字段,用于指示请求的身份验证凭证。
LoginInterceptor
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;
}
}
MvcConfig
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@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刷新拦截器
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
}
}
order(0),1 2 3 4 5 是执行顺序