【黑马点评】短信登录
基于Session的实现
Session:
Session 是保存在服务端(Tomcat)的一块内存空间,用来存本次用户会话的数据
HTTP 协议是无状态协议 ,多次请求之间相互独立,服务器默认无法识别多次请求来自同一个用户,不知道用户的登录状态。 如果每次请求都去查询数据库校验登录,会消耗大量性能。 用户登录成功后,服务端生成唯一 sessionId,创建对应的 Session 对象保存在服务端内存;再把 sessionID写入 Cookie 返回给浏览器。 浏览器后续每一次请求,自动携带 Cookie 中的 sessionID。服务器读取 sessionID,找到服务端对应的 Session,就可以识别该用户的登录状态。
实现流程

发送短信验证码:
java
//Controller:
/**
* 发送手机验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
return userService.sendCode(phone,session);
}
//service
public Result sendCode(String phone, HttpSession session) {
//1.校验手机号
if(isPhoneInvalid(phone))
{
return Result.fail("手机号错误");
}
//2.生成验证码
String code = RandomUtil.randomNumbers(6);
//3.保存验证码到session
session.setAttribute("code", code);
//4.发送验证码
log.info("验证码:{}",code);
return Result.ok();
}
session可以存放数据,我们将验证码放在session里面,方便后面验证的时候用
短信验证码登录,注册:
java
//Controller
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// TODO 实现登录功能
return userService.login(loginForm,session);
}
//Service
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
//1.校验手机号
if(isPhoneInvalid(phone))
{
return Result.fail("手机号错误");
}
//2.校验验证码
//cachecode是用户输入的验证码
String cachecode = loginForm.getCode();
//code是系统生成的验证码
Object code = session.getAttribute("code");
if(cachecode==null || !cachecode.toString().equals(code))
{
return Result.fail("验证码错误");
}
//3.校验用户是否存在
User user = query().eq("phone", phone).one();
//不存在,创建并保存在session中
if(user==null)
{
user=createUserByPhone(phone);
}
//直接保存在session中
session.setAttribute("user", BeanUtil.copyProperties(user,UserDTO.class));
return Result.ok();
}
private User createUserByPhone(String phone) {
User user=new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomNumbers(7));
return user;
}
这里做了数据筛除,为了保护用户信息,用UserDTO封装用户信息并返回,里面只包含用户名称和ID。
校验用户状态:
java
//拦截器
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) {
response.setStatus(401);
return false;
}
//保存
UserHolder.saveUser((UserDTO) user);
//放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
//Config
@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"
);
}
}
ThreadLocal的工作流程:
UserHolder如下:
java
package com.hmdp.utils;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
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 是一个可以隔绝其他变量的工具,只处理当前的变量。
当我们请求拦截器拦截的方法时,ThreadLocal就会启动。
以请求首页为例:
请求首页→触发拦截器→拦截器流程结束后→执行Controller→Service→Mapper
拦截器内部:
首先会先获取Session,并获取session里面存放的用户
java
HttpSession session = request.getSession();
Object user = session.getAttribute("user");
检查用户是否登录,如果登录则将用户信息存储之后放行,否则拦截
java
if(user == null) {
response.setStatus(401);
return false;
}
//此方法用到了ThreadLocal的set方法
UserHolder.saveUser((UserDTO) user);
//放行
return true;
在响应返回浏览器之后,会销毁信息,防止内存泄漏
java
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
session的共享问题

由于session是每个tomcat服务器独有的,数据不能共享,就会导致当用户第一次在服务器1请求,第二次去服务器二请求的时候找不到登录信息,无法实现数据共享。
上述数据共享问题,redis可以解决。而且redis的数据存储是存在内存之中的,数据的结构也是key,value结构。
基于redis的短信验证
发送验证码:
存储数据结构:String key为手机号,value为验证码


校验登录状态:
存储数据方式:Hash
key: token value:存储数据
代码实现
发送验证码:
只需要修改存储地方就行
java
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result sendCode(String phone, HttpSession session) {
//1.校验手机号
if(isPhoneInvalid(phone))
{
return Result.fail("手机号错误");
}
//2.生成验证码
String code = RandomUtil.randomNumbers(6);
//3.保存验证码到redis
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY +phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
//4.发送验证码
log.info("验证码:{}",code);
return Result.ok();
}
登录校验:
java
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
//1.校验手机号
if(isPhoneInvalid(phone))
{
return Result.fail("手机号错误");
}
//2.校验验证码,从redis里面提取
String cachecode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY +phone);
Object code = loginForm.getCode();
if(cachecode==null || !cachecode.equals(code))
{
return Result.fail("验证码错误");
}
//3.校验用户是否存在
User user = query().eq("phone", phone).one();
//不存在,创建并保存在session中
if(user==null)
{
user=createUserByPhone(phone);
}
//4.存在,保存在redis里
//4.1.将user转成userDTO
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
//4.2.将userDTO里的数据转成Map,方便一次性写入redis
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create().setIgnoreNullValue(true).
setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
//4.3.生成token作为key
String token = UUID.randomUUID().toString();
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL, TimeUnit.MINUTES);
// session.setAttribute("user", BeanUtil.copyProperties(user,UserDTO.class));
return Result.ok(token);
}
private User createUserByPhone(String phone) {
User user=new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomNumbers(7));
return user;
}
拦截器:

拦截器采用两个,主要针对用户在长时间浏览不需要登录验证的网页导致token不刷新而过期的现象
LoginInterceptor:拦截需要登录的网页
java
package com.hmdp.utils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (UserHolder.getUser()==null) {
response.setStatus(401);
return false;
}
return true;
}
}
RefreshInterceptor:拦截所有网页,用于token的刷新
java
package com.hmdp.utils;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;
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");
//2.查看token是否为空,空的说明没登录,不需要刷新token,放行
if (StrUtil.isBlank(token)) {
return true;
}
//获取用户信息
String tokenKey = LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);
//如果用户信息为空,说明token已经过期
if (userMap.isEmpty()) {
return true;
}
//将数据转换为userDTO
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//保存
UserHolder.saveUser(userDTO);
//刷新token有效期
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
MvcConfig:配置拦截器,并配置先后顺序
java
```java
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(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
).order(1);
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0);
}
}
如果喜欢我的内容,觉得我的内容有帮助到你的,可以动动小手点赞收藏,我们下期再见!
