就是黑马程序员的 Redis 教程里的黑马点评的项目,前面就不讲了,直接开始正题。
# 基于 Session 实现登录流程
分为三个步骤:
- 发送验证码
- 短信验证码登录、注册
- 校验登录状态
# 发送验证码
# 逻辑:
用户在提交手机号后,会校验手机号是否合法,如果不合法,则要求用户重新输入手机号
如果手机号合法,后台此时生成对应的验证码,同时将验证码进行保存,然后再通过短信的方式将验证码发送给用户
# 代码
@Override | |
public Result sendCode(String phone, HttpSession session) { | |
// 校验手机号,不符合就返回错误,符合就生成验证码 | |
if (RegexUtils.isPhoneInvalid(phone)){ | |
return Result.fail("手机号格式错误!"); | |
} | |
String code = RandomUtil.randomNumbers(6); | |
// 保存验证码到 session | |
session.setAttribute("code",code); | |
// 发送验证码 | |
log.debug("发送短信验证码成功,验证码:" + code); | |
return Result.ok(); | |
} |
# 短信验证码登录、注册
# 逻辑:
用户将验证码和手机号进行输入,后台从 session 中拿到当前验证码,然后和用户输入的验证码进行校验,如果不一致,则无法通过校验,如果一致,则后台根据手机号查询用户,如果用户不存在,则为用户创建账号信息,保存到数据库,无论是否存在,都会将用户信息保存到 session 中,方便后续获得当前登录信息
# 代码
@Override | |
public Result login(LoginFormDTO loginForm, HttpSession session) { | |
// 校验手机号和验证码 | |
String phone = loginForm.getPhone(); | |
if (RegexUtils.isPhoneInvalid(phone)){ | |
return Result.fail("手机号格式错误!"); | |
} | |
Object cacheCode = session.getAttribute("code"); | |
String code = loginForm.getCode(); | |
// 不一致,报错 | |
if(cacheCode == null || !cacheCode.toString().equals(code)) { | |
return Result.fail("验证码错误"); | |
} | |
// 一致,根据手机号查用户 | |
User user = query().eq("phone", phone).one(); | |
// 判断是否存在 | |
if (user == null) { | |
// 不存在,创建用户 | |
user = createUserWithPhone(phone); | |
} | |
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class)); | |
return Result.ok(); | |
} |
# 校验登录状态
校验登录状态需要配置拦截器来实现登录拦截功能
# 原理
当用户发起请求时,会访问我们像 tomcat 注册的端口,任何程序想要运行,都需要有一个线程对当前端口号进行监听,tomcat 也不例外,当监听线程知道用户想要和 tomcat 连接连接时,那会由监听线程创建 socket 连接,socket 都是成对出现的,用户通过 socket 像互相传递数据,当 tomcat 端的 socket 接受到数据后,此时监听线程会从 tomcat 的线程池中取出一个线程执行用户请求,在我们的服务部署到 tomcat 后,线程会找到用户想要访问的工程,然后用这个线程转发到工程中的 controller,service,dao 中,并且访问对应的 DB,在用户执行完请求后,再统一返回,再找到 tomcat 端的 socket,再将数据写回到用户端的 socket,完成请求和响应。
每个用户其实对应都是去找 tomcat 线程池中的一个线程来完成工作的, 使用完成后再进行回收,既然每个请求都是独立的,所以在每个用户去访问我们的工程时,我们可以使用 threadlocal 来做到线程隔离,每个线程操作自己的一份数据
# 逻辑
用户在请求时候,会从 cookie 中携带者 JsessionId 到后台,后台通过 JsessionId 从 session 中拿到用户信息,如果没有 session 信息,则进行拦截,如果有 session 信息,则将用户信息保存到 threadLocal 中,并且放行
# 代码
- 首先实现 HandlerInterceptor 接口
public class LoginInterceptor implements HandlerInterceptor { | |
@Override | |
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { | |
//Get session | |
HttpSession session = request.getSession(); | |
//Get user from session | |
Object user = session.getAttribute("user"); | |
//if user exists | |
if (user == null) { | |
//not exists, reject, return 401 | |
response.setStatus(401); | |
return false; | |
} | |
//save user in ThreadLocal | |
UserHolder.saveUser((UserDTO) user); | |
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 { | |
@Override | |
public void addInterceptors(InterceptorRegistry registry) { | |
registry.addInterceptor(new LoginInterceptor()).excludePathPatterns( | |
"/user/code", | |
"/user/login", | |
"/blog/hot", | |
"/shop/**", | |
"/shop-type/**", | |
"/voucher/**" | |
); | |
} | |
} |
# session 共享问题
每个 Tomcat 中都有一份属于自己的 session
, 假设用户第一次访问第一台 Tomcat,并且把自己的信息存放到第一台服务器的 session
中,但是第二次这个用户访问到了第二台 Tomcat,那么在第二台服务器上,肯定没有第一台服务器存放的 session
,所以此时 整个登录拦截功能就会出现问题,我们能如何解决这个问题呢?早期的方案是 session
拷贝,就是说虽然每个 Tomcat 上都有不同的 session
,但是每当任意一台服务器的 session
修改时,都会同步给其他的 Tomcat 服务器的 session
,这样的话,就可以实现 session
的共享了
但是这种方案有两个大问题
-
每台服务器中都有完整的一份
session
数据,服务器压力过大。 -
session
拷贝数据时,可能会出现延迟
所以咱们要基于 Redis 来完成,我们把 session
换成 Redis,Redis 数据本身就是共享的,就可以避免 session
共享的问题了
# Redis 代替 session
的业务流程
# 设计 key
我们可以生成一个随机字符串 token
,来存储。这样既可以满足唯一性也可以满足脱敏性。
# 整体流程
当注册完成后,用户去登录会去校验用户提交的手机号和验证码,是否一致,如果一致,则根据手机号查询用户信息,不存在则新建,最后将用户数据保存到 Redis,并且生成 token
作为 Redis 的 key
,当我们校验用户是否登录时,会去携带着 token
进行访问,从 Redis 中取出 token
对应的 value
,判断是否存在这个数据,如果没有则拦截,如果存在则将其保存到 threadLocal
中,并且放行。
# 代码
直接上代码:
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { | |
@Resource | |
private StringRedisTemplate stringRedisTemplate; | |
@Override | |
public Result sendCode(String phone, HttpSession session) { | |
// 校验手机号,不符合就返回错误,符合就生成验证码 | |
if (RegexUtils.isPhoneInvalid(phone)){ | |
return Result.fail("手机号格式错误!"); | |
} | |
String code = RandomUtil.randomNumbers(6); | |
// 保存验证码到 Redis | |
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES); | |
// 发送验证码 | |
log.debug("发送短信验证码成功,验证码:" + code); | |
return Result.ok(); | |
} | |
@Override | |
public Result login(LoginFormDTO loginForm, HttpSession session) { | |
// 校验手机号和验证码 | |
String phone = loginForm.getPhone(); | |
if (RegexUtils.isPhoneInvalid(phone)){ | |
return Result.fail("手机号格式错误!"); | |
} | |
// 从 redis 获取验证码并校验 | |
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone); | |
String code = loginForm.getCode(); | |
// 不一致,报错 | |
if(cacheCode == null || !cacheCode.equals(code)) { | |
return Result.fail("验证码错误"); | |
} | |
// 一致,根据手机号查用户 | |
User user = query().eq("phone", phone).one(); | |
// 判断是否存在 | |
if (user == null) { | |
// 不存在,创建用户 | |
user = createUserWithPhone(phone); | |
} | |
// 保存用户信息到 redis 中 | |
// 随机生成 token,作为登录令牌 | |
String token = UUID.randomUUID().toString(true); | |
// 将 User 对象转为 HashMap 存储 | |
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())); | |
// 存储 | |
String tokenKey = LOGIN_USER_KEY + token; | |
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap); | |
// 设置 token 有效期 | |
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES); | |
return Result.ok(token); | |
} |
@Configuration | |
public class MvcConfig implements WebMvcConfigurer { | |
@Resource | |
private StringRedisTemplate stringRedisTemplate; | |
@Override | |
public void addInterceptors(InterceptorRegistry registry) { | |
registry.addInterceptor(new LoginInterceptor(stringRedisTemplate)).excludePathPatterns( | |
"/user/code", | |
"/user/login", | |
"/blog/hot", | |
"/shop/**", | |
"/shop-type/**", | |
"/voucher/**" | |
); | |
} | |
} |
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 { | |
//Get token in Header | |
String token = request.getHeader("authorization"); | |
if (StrUtil.isBlank(token)) { | |
//not exists, reject, return 401 | |
response.setStatus(401); | |
return false; | |
} | |
String key = RedisConstants.LOGIN_USER_KEY + token; | |
//Get user from Redis | |
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key); | |
//if user exists | |
if (userMap.isEmpty()) { | |
//not exists, reject, return 401 | |
response.setStatus(401); | |
return false; | |
} | |
//turn Hash to UserDTO | |
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); | |
//save user in ThreadLocal | |
UserHolder.saveUser(userDTO); | |
// refresh token TTL | |
stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES); | |
return true; | |
} | |
@Override | |
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { | |
UserHolder.removeUser(); | |
} | |
} |