目录
[1 . 导入项目 :](#1 . 导入项目 :)
[2 . 基于Session实现短信验证登录](#2 . 基于Session实现短信验证登录)
[2 . 1 原理 :](#2 . 1 原理 :)
[2 . 2 发送短信验证码 :](#2 . 2 发送短信验证码 :)
[2 . 3 短信验证码登录和验证功能 :](#2 . 3 短信验证码登录和验证功能 :)
[2 . 4 登录验证功能](#2 . 4 登录验证功能)
[2 . 5 隐藏用户敏感信息](#2 . 5 隐藏用户敏感信息)
[2 . 6 session共享问题](#2 . 6 session共享问题)
[2 . 7 Redis 代替 session](#2 . 7 Redis 代替 session)
[2 . 8 基于Redis实现短信登录](#2 . 8 基于Redis实现短信登录)
[发送短信验证码 :](#发送短信验证码 :)
[用户登录 :](#用户登录 :)
[LoginInterceptor :](#LoginInterceptor :)
[报错 :](#报错 :)
[测试 :](#测试 :)
[2 . 9 登录拦截器的优化](#2 . 9 登录拦截器的优化)
1 . 导入项目 :
先导入sql文件 :
导入后端项目 :
注意要修改一些地方 :
1 . mysql配置,要改成自己的 :
如果用的是8.x版本,需要在pom文件中修改依赖 :
2 . 修改redis的url,为自己虚拟机redis开放端口 :
3 . 直接启动项目之后,访问http://localhost:8081/shop-type/list
4 . 将前端搭建好之后,访问8080,用手机模式打开 :
2 . 基于Session实现短信验证登录
2 . 1 原理 :
发送验证码:
用户在提交手机号后,会校验手机号是否合法,如果不合法,则要求用户重新输入手机号
如果手机号合法,后台此时生成对应的验证码,同时将验证码进行保存,然后再通过短信的方式将验证码发送给用户
短信验证码登录、注册:
用户将验证码和手机号进行输入,后台从session中拿到当前验证码,然后和用户输入的验证码进行校验,如果不一致,则无法通过校验,如果一致,则后台根据手机号查询用户,如果用户不存在,则为用户创建账号信息,保存到数据库,无论是否存在,都会将用户信息保存到session中,方便后续获得当前登录信息
校验登录状态:
用户在请求时候,会从cookie中携带者JsessionId到后台,后台通过JsessionId从session中拿到用户信息,如果没有session信息,则进行拦截,如果有session信息,则将用户信息保存到threadLocal中,并且放行 ;
2 . 2 发送短信验证码 :
@Override
public Result sendCode(String phone, HttpSession session) {
// 1 . 检验啊手机号
if(RegexUtils.isPhoneInvalid(phone)){
// 2 . 如果不符合 , 报错
return Result.fail("手机号格式错误!") ;
}
// 3 . 符合 , 生成验证码
String code = RandomUtil.randomNumbers(6) ; //生成长度为6位的随机验证码
// 4 . 保存验证码到 session
session.setAttribute("code",code);
// 5 . 发送验证码
log.debug("验证码方程成功,验证码 : {}",code);
return Result.ok();
}
关于如何校验,参考 : java实现手机号,密码,游邮箱 , 验证码的正则匹配工具类-CSDN博客
这里的发送验证码功能没有实现,只是做了个假的,日后有时间再完成 ;
启动项目,在前端点击发送验证码 :
2 . 3 短信验证码登录和验证功能 :
/**
* 用户登录
* @param loginForm
* @param session
* @return
*/
@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 . 1 不一致,直接报错返回
return Result.fail("验证码错误") ;
}
// 4 . 一致, 根据手机号查询用户
User user = query().eq("phone",phone).one() ;
// 5 . 判断用户是否存在
if(user == null){
// 6 . 为空,表示之前未创建 , 则创建
user = createUserWithPhone(phone) ;
}
// 7 . 保存用户信息到session中
session.setAttribute("user",user);
return Result.ok() ;
}
private User createUserWithPhone(String phone) {
// 1. 新建用户
User user = new User() ;
user.setPhone(phone) ;
user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10)) ;
// 2 . 保存用户
save(user) ;
return user ;
}
2 . 4 登录验证功能
先定义一个拦截器 :
/**
* 拦截器
*/
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 ;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
这里因为之前登录的时候,在session中存了user信息,如果这里查不到,那就对其进行拦截,查到了,就放行 ;
然后让定义的拦截器生效 (定义一个配置类) :
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"upload/**",
"/shop/**",
"/shop-type/**",
"voucher/**"
);
}
}
其中设置了一些放行的端口(也就是不需要登录也能够访问得到的端口) ;
然后再实现一下"me"接口 :
这里直接获取ThreadLocal中之前设置的user对象即可 ;
这里ThreadLocal来设置user和获取user定义成了一个工具类 :
package com.hmdp.utils;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
public class UserHolder {
private static final ThreadLocal<User> tl = new ThreadLocal<>();
public static void saveUser(User user){
tl.set(user);
}
public static User getUser(){
return tl.get();
}
public static void removeUser(){
tl.remove();
}
}
到时候直接调用即可 ;
2 . 5 隐藏用户敏感信息
用UserDto(只包括id,nickName,icon三个)来隐藏用户的敏感信息(如password,phone等),也可以减少内存的压力 ;
这里直接在存入session的时候,就转换为UserDTO :
然后在LoginInterceptor中存入ThreadLocal的时候将user转换为UserDTo,
那么对应的UserHolder中也要改 :
然后修改报错的地方,将User修改成UserDTO ;
然后重新登录测试 :
2 . 6 session共享问题
用redis来解决session的内存不共享的问题 ;
2 . 7 Redis 代替 session
在登录发验证码的时候用手机号作为key,验证码作为value ;
在保存用户的时候 :
用hash结构来保存用户信息 , 用一个随机的token作为key ;
在用session做登录校验的时候,tomcat会将session的id写到浏览器的cookie中,然后每一次的请求都会带着cookie,也就带着session_id , 然后就能够通过session_id找到session,然后找到用户 ;
在用redis代替token的时候,我们只能够手动的将token传给前端(客户端),然后客户端每一次请求都会携带token,然后我们可以基于token获取用户数据 ;
2 . 8 基于Redis实现短信登录
UserServiceImpl
发送短信验证码 :
先注入StringRedisTemplate对象 :
修改保存逻辑,将验证码保存到redis中以key= phone,value:code的形式,并且设置过期时间 :
这里对于"login:code:"和2可以设置一个常量类保存起来代码更加规范 :
完整代码 :
@Resource
private StringRedisTemplate stringRedisTemplate ; // 注入
/**
* 发送手机验证码
* @param phone
* @param session
* @return
*/
@Override
public Result sendCode(String phone, HttpSession session) {
// 1 . 检验啊手机号
if(RegexUtils.isPhoneInvalid(phone)){
// 2 . 如果不符合 , 报错
return Result.fail("手机号格式错误!") ;
}
// 3 . 符合 , 生成验证码
String code = RandomUtil.randomNumbers(6) ; //生成长度为6位的随机验证码
// 4 . 保存验证码到 redis , 并设置两分钟的有效期(减少内存压力,防止一直点)
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);// 前面加一个login:code:标识,进行业务区分
// session.setAttribute("code",code);
// 5 . 发送验证码
log.debug("验证码方程成功,验证码 : {}",code);
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 . 校验验证码
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone) ;// 本地code
String code = loginForm.getCode(); // 前端传来的code
if(cacheCode == null || !cacheCode.toString().equals(code)){
// 3 . 1 不一致,直接报错返回
return Result.fail("验证码错误") ;
}
// 4 . 一致, 根据手机号查询用户
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对象转换为hash存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class) ;
Map<String,Object> userMap = BeanUtil.beanToMap(userDTO);
// 7 . 3 存储
String tokenKey = LOGIN_USER_KEY + token ;
stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
// 7 . 4 设置token有效期 (30分钟)
stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);//这样是在登录那一刻的30分钟后就过期了,然后可以在拦截器哪里设置每一次访问就更新有效期
// 8 . 返回token
return Result.ok(token) ;
}
- 这里校验验证码,直接从redos中获取 ;
- 保存用户到redis中 , 使用hash存储,用随机的token作为key(前面加一个标识前缀),用相应的user转换成map对象作为value ;
- 最后还要设置token的有效期,这里只能够设置在登录之后有效期为30分钟,但是实际应该为每次访问的时候,都能够有30分钟的有效期,那么这个将在拦截器中设置 ;
LoginInterceptor :
完整代码 :
package com.hmdp.utils;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import lombok.val;
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;
/**
* 拦截器
*/
public class LoginInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate ;
// 这里不能够使用Resource 和 AutoWired 等来进行注入,只能够使用构造函数来进行依赖注入
// 因为 LoginInterceptor 是我们自己手动new出来的 , 不是由spring创建的 ;
// 这里可以在MvcConfig中来注入 stringRedisTemplate 对象
public LoginInterceptor(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 . 获取请求头中的token
String token = request.getHeader("authorization") ;
if(StrUtil.isBlank(token)){
// 4 . 不存在,拦截,返回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 , 30, TimeUnit.MINUTES) ;
// 8 . 放行
return true ;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
这里主要进行修改的就是需要基于token从redis中获取数据 , 然后还要在每次拦截的时候,对token的有效期进行修改 ;
对于导入StringRedisTemplate方法参考 : 关于在拦截器中注入依赖对象-CSDN博客
报错 :
运行起来之后,登录一下啊,能够发现报错 :
能够发现,大概是 : 出现类型转换错误 :
测试 :
能够看到请求头中携带了token,然后redis中也存入了响应的token ;
这样改造就完成了 ;
2 . 9 登录拦截器的优化
在上面方案中,他确实可以使用对应路径的拦截,同时刷新登录token令牌的存活时间,但是现在这个拦截器他只是拦截需要被拦截的路径,假设当前用户访问了一些不需要拦截的路径,那么这个拦截器就不会生效,所以此时令牌刷新的动作实际上就不会执行,所以这个方案他是存在问题的
首先加一个token刷新拦截器类 :
package com.hmdp.utils;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import lombok.val;
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;
/**
* 拦截器
*/
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate ;
// 这里不能够使用Resource 和 AutoWired 等来进行注入,只能够使用构造函数来进行依赖注入
// 因为 LoginInterceptor 是我们自己手动new出来的 , 不是由spring创建的 ;
// 这里可以在MvcConfig中来注入 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 . 获取请求头中的token
String token = request.getHeader("authorization") ;
if(StrUtil.isBlank(token)){
return true ;
}
// 2 . 基于token获取redis中的用户
String key = RedisConstants.LOGIN_USER_KEY + token ;
Map<Object,Object> userMap = stringRedisTemplate.opsForHash().entries(key) ;
// 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 , 30, TimeUnit.MINUTES) ;
// 8 . 放行
return true ;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
然后LoginInterceptor中就只需要执行拦截功能了 :
然后在MvcConfig中进行配置 :
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())
.excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"upload/**",
"/shop/**",
"/shop-type/**",
"voucher/**"
).order(1);
// token刷新的拦截器
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
}
}
测试 : 在主界面刷新一下,然后看到redis中的时间重置了 :