「黑马点评」一、短信登陆及 session 登陆
基于 session 实现短信登陆

image.png|500
对于短信验证码来说,只有生成、发送、校验三步,在 Service 中即 /sendCode
、/login
两步
显然这是利用 B/S 中的 JsessionId 在服务器中存储相关会话信息,即 smsCode 和 user
但是如果多台集群服务下,session 无法共享,所以引出了 Redis 实现短信登陆
image.png|500
基于 Redis 实现短信登陆

image.png|500

image.png|500
实现的 ThreadLocal
存储当前线程的 User,但是为了隐密,用了 UserDTO,做了敏感处理
UserHolder
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();
}
}
实现的拦截器
如图所示,使用了 Redis 对 smsCode 和 sessionToken 进行存储,但是又引出一个问题,token 是有有效期的,难不成每次过期都要重新接一次验证码,用户体验不方便,我们的运营成本也上去了,所以需要接入拦截器

image.png|500
token 刷新拦截器,拦截一切路径用于刷新 sessionToken 有效期(可以考虑用 JWT 无状态),第二个拦截器,用于拦截未登陆用户访问需要登陆的接口
LoginInterceptor
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if(UserHolder.getUser() == null) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
return true;
}
}
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") ;
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();
}
}
MvcConfig
@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);
}
}
Controller
UserController
@PostMapping("code")
public Result<String> sendCode(@RequestParam("phone") String phone, HttpSession session) {
return userService.sendCode(phone, session);
}
@PostMapping("/login")
public Result<String> login(@RequestBody LoginFormDTO loginForm, HttpSession session){
return userService.login(loginForm, session);
}
Service
UserServiceImpl
public class UserServiceImpl implements IUserService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Autowired
private UserMapper userMapper;
@Override
public Result<String> sendCode(String phone, HttpSession session) {
if(RegexUtils.isPhoneInvalid(phone)){
return Result.error("手机格式不符合");
}
String code = RandomUtil.randomNumbers(6);
stringRedisTemplate.opsForValue()
.set(RedisConstants.LOGIN_CODE_KEY + phone
, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
// log.debug("验证码方程成功,验证码 : {}",code);
return Result.success("验证码为 " + code + ", 有效期 2 分钟");
}
@Override
public Result<String> login(LoginFormDTO loginFormDTO, HttpSession session) {
String phone = loginFormDTO.getPhone();
if (RegexUtils.isPhoneInvalid(phone)){
return Result.error("手机格式不符合");
}
String cacheCode = stringRedisTemplate.opsForValue()
.get(RedisConstants.LOGIN_CODE_KEY + phone);
String code = loginFormDTO.getCode();
if(cacheCode == null || !cacheCode.equals(code)) {
return Result.error("验证码错误") ;
}
User user = userMapper.findByPhone(phone);
if(user == null){
user = userMapper.createUserWithPhone(phone) ;
}
String token = UUID.randomUUID().toString();
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class) ;
Map<String,Object> userMap = BeanUtil.beanToMap(userDTO);
String tokenKey = RedisConstants.LOGIN_USER_KEY + token ;
stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
stringRedisTemplate.expire(tokenKey, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
return Result.success(token) ;
}
@Override
public Result<UserDTO> me() {
UserDTO userDTO = UserHolder.getUser();
return Result.success(userDTO);
}
}
Mapper
UserMapper.xml
<select id="findByPhone" resultType="com.hmdp.entity.User" parameterType="java.lang.String">
select id, phone, password, nick_name, icon, create_time, update_time
from tb_user
where phone = #{phone}
</select>
<insert id="createUserWithPhone" parameterType="java.lang.String">
insert into tb_user (phone, create_time, update_time)
values (#{phone}, now(), now())
</insert>