流程图
发送短信验证码
代码逻辑很简单
service层实现
@Override
public Result sendCode(String phone, HttpSession session) {
// 1. 校验手机号
if(RegexUtils.isPhoneInvalid(phone)){
// 2. 如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 3. 生成验证码
String code = RandomUtil.randomNumbers(6);
// 4. 保存验证码到session
session.setAttribute("code",code);
// 5. TODO 发送验证码-调用第三方平台发送
log.debug("发送验证码成功,验证码为:{}",code);
// 返回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 * from tb_user where phone = ?
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) {
// 创建用户
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
// 2. 保存用户
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中的用户
User user = (User)session.getAttribute("user");
// 3. 判断用户是否存在
if(user == null){
// 4. 不存在,拦截
response.setStatus(401);
return false;
}
//5. 存在,保存用户信息到ThreadLocal
UserHolder.saveUser(user);
// 6. 放行
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 org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@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"
);
}
}
集群的Session共享问题
解决方案就是让session共享, 那可以共享吗?早期可以session拷贝,但可行吗?
有问题的:
第一:拷贝有延迟, 会有数据不一致的问题
第二:互相拷来拷去,相同的数据,浪费内存空间
所以,该方案pass
那么替代session方案应该满足:
- 数据共享
- 内存存储
- key、value结构
那答案就是redis
基于Redis实现共享session登录
我们保存的用户信息的方式选择hash的方式
那登录校验怎么做?
因为用token作为key存储用户信息,现在用户得使用token作为用户的登录凭证,但是tomcat不会把token写到浏览器,所以,我只能手动把token返回客户端,客户端把token保存下来,每次请求携带token。就变成了这样
前端会把token保存到sessionStorage中,以后每次请求都带上。
于是,我们的关于保存到session的代码都要做响应的修改
发送验证码的逻辑
@Override
public Result sendCode(String phone, HttpSession session) {
// 1. 校验手机号
if(RegexUtils.isPhoneInvalid(phone)){
// 2. 如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 3. 生成验证码
String code = RandomUtil.randomNumbers(6);
// 4. 保存验证码到session
// session.setAttribute("code",code);
// 保存到redis
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
// 5. TODO 发送验证码-调用第三方平台发送
log.debug("发送验证码成功,验证码为:{}",code);
// 返回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. 从session获取校验验证码
// Object cacheCode = session.getAttribute("code");
// 2. 从redis获取校验验证码
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
if(cacheCode == null || !cacheCode.equals(code)){
// 3. 不一致,直接报错
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. 保存用户信息到session
// session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
// 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, new HashMap<>(), CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
// 7.3 存储到redis
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
// 设置token有效期
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8. 返回token
return Result.ok(token);
}
拦截器
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();
// 1. 获取请求头中的token
String token = request.getHeader("authorization");
if(StrUtil.isBlank(token)){
// 4. 不存在,拦截
response.setStatus(401);
return false;
}
// 2. 基于token获取redis中的用户
String key = RedisConstants.LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
// 2. 获取session中的用户
// UserDTO user = (UserDTO)session.getAttribute("user");
// 3. 判断用户是否存在
if(userMap.isEmpty()){
// 4. 不存在,拦截
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();
}
}
这里拦截器是我们自定义的,我们不能使用AutoWried让Spring帮助我们导入StringRedisTemplate ,因为这个类都不归Spring管,自然Spring也无法帮我们导入,我们只能使用构造器的方式导入,那在我们的MvcConfig这个配置类中,用到LoginInterceptor的时候,传进去就可以了,于是MvcConfig要修改为这样
package com.hmdp.config;
import com.hmdp.utils.LoginInterceptor;
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/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
);
}
}
解决登录状态刷新的问题
在加一个拦截器,第一个拦截器负责拦截所有的请求,如果存在就刷新token,并且存入到ThreadLocal中,如果不存在也放行,
第二个拦截器只负责拦截和放行,直接从ThreadLocal中取
/**
* 第一个拦截器负责拦截所有的请求,如果存在就刷新token,并且存入到ThreadLocal中,如果不存在也放行,
*/
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");
if(StrUtil.isBlank(token)){
// 为空,也是直接放行,给后面的拦截器判断
return true;
}
// 2. 基于token获取redis中的用户
String key = RedisConstants.LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
// 2. 获取session中的用户
// UserDTO user = (UserDTO)session.getAttribute("user");
// 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, 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();
}
}
/**
* 第二个拦截器只负责拦截和放行
*/
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;
}
}
于是乎,我们的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(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
).order(1);
// 刷新拦截器
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0); // order越小先执行
}
}