目录
短信登录
基于session实现短信登录

实现发送短信验证码功能

UserServiceImpl.java代码:
添加sendCode方法
java
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@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、发送验证码
log.debug("发送短信验证码成功,验证码:{}",code);
//返回ok
return Result.ok();
}
}


实现短信验证码登录和注册功能

UserServiceImpl.java代码:
添加login和createUserWithPhone方法
java
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1、校验手机号
String phone = loginForm.getPhone();
if(RegexUtils.isPhoneInvalid(phone)){
//2、如果不符合,返回错误信息
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、一致、根据手机号查询用户
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;
}
实现登录校验拦截器

1、创建拦截器loginInterceptor.java
java
package com.hmdp.utils;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class LoginInterceptor implements HandlerInterceptor {
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//移除用户
UserHolder.removeUser();
}
@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;
}
}
2、配置拦截器MvcConfig.java
java
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(
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/upload/**",
"/voucher/**"
);
}
}
3、实现/user/me接口
java
@GetMapping("/me")
public Result me(){
// 获取当前登录的用户并返回
User user = UserHolder.getUser();
return Result.ok(user);
}
登录成功点击我的调用/user/me接口会获取用户信息数据

隐藏用户敏感信息
登录时,不将user的完整信息存入session,而是实现一个UserDto封装信息,将userDto存入
java
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
UserServiceImpl.java/login方法
java
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1、校验手机号
String phone = loginForm.getPhone();
if(RegexUtils.isPhoneInvalid(phone)){
//2、如果不符合,返回错误信息
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、一致、根据手机号查询用户
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));
return Result.ok();
}
将拦截器中取出的user转换为userDto
java
UserHolder.saveUser((UserDTO) user);
将/user/me接口取出的用户信息转换为userDTO
java
@GetMapping("/me")
public Result me(){
// 获取当前登录的用户并返回
UserDTO user = UserHolder.getUser();
return Result.ok(user);
}
重新登录一下,调用/user/me接口得到用户的敏感信息已经隐藏

session共享的问题分析
session共享问题: 多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题。

session的替代方案应该满足
- 数据共享
- 内存存储
- key、value结构
所以我们可以使用redis来解决。
redis代替session的业务流程
短信验证码使用手机作为key,用户信息使用token作为key。
实现登录注册时,需要将生成的token返回给前端,在校验时,拿到前端传递的token。

基于redis实现短信登录
UserServiceImpl.java
java
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@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(可以加上业务前缀:login:code:以作区分),时间有效期2分钟
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("手机号格式错误!");
}
//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、一致、根据手机号查询用户
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();
//7.2 将user对象转为Hash存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
//转成map时,自定义hashMap的值,将其中的参数转为string
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);
}
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;
}
}
LoginInterceptor.java
java
public class LoginInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//移除用户
UserHolder.removeUser();
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1、获取请求头中的token
String token = request.getHeader("authorization");
if(StrUtil.isBlank(token)){
//不存在,拦截并返回 401 状态码
response.setStatus(401);
return false;
}
//2、基于token获取redis中的用户,使用entries取出所有hashMap
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;
}
}
注册拦截器时注入StringRedisTemplate
解决状态登录刷新的问题
所存在的问题: 目前拦截器是拦截需要登录的路径,若用户一直访问不需要登录的页面,则刷新token有效期则不会生效,导致用户会丢失登录态。

解决方案: 在此拦截器前加一个新的拦截器,拦截一切路径,并将刷新token有效期的代码逻辑放在这个拦截器。

新建刷新token拦截器RefreshTokenInterceptor.java
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;
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//移除用户
UserHolder.removeUser();
}
@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中的用户,使用entries取出所有hashMap
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,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
//8、放行
return true;
}
}
修改登录拦截器LoginInterceptor.java
java
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 io.netty.util.internal.StringUtil;
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 {
@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.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(
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/upload/**",
"/voucher/**"
).order(1);
//token刷新的拦截器
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
}
}
原有效时间

刷新首页后的有效时间被重置,说明代码没问题

商户查询缓存
添加商户缓存

shop/{id}获取店铺信息接口中修改
java
/**
* 根据id查询商铺信息
* @param id 商铺id
* @return 商铺详情数据
*/
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
return shopService.queryById(id);
}
ShopServiceImpl.java
java
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
String key = RedisConstants.CACHE_SHOP_KEY + id;
//1、从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2、判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
//3、存在、直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//4、不存在,根据Id查询数据库
Shop shop = getById(id);
//5、不存在,返回错误
if (shop == null) {
return Result.fail("店铺不存在!");
}
//6、存在,写入redis
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
//7、返回
return Result.ok(shop);
}
}