基于 Session 实现
发送验证码
/**
* 发送手机验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
// 发送短信验证码并保存验证码
return userService.sendCode(phone, 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, phone);
// 返回成功信息
return Result.ok();
}
- 发送验证码之前校验手机号是否符合规则
- 不符合校验规则就返回错误信息
- 符合校验规则就生成验证码并保存到session中
RandomUtil.randomNumbers(6)
使用hutool工具生成6位随机验证码- 最后发送验证码并返回成功提示
登录校验验证码
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// 实现登录功能
return userService.login(loginForm, session);
}
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 校验手机号
if(RegexUtils.isPhoneInvalid(loginForm.getPhone())){
// 不符合 返回错误信息
return Result.fail("手机号格式不正确");
}
// 校验验证码
Object cacheCode = session.getAttribute("code");
if(cacheCode == null || !cacheCode.toString().equals(loginForm.getCode())){
// 验证码不一致
return Result.fail("验证码不正确");
}
// 验证码一致 根据手机号查询用户信息
User user = lambdaQuery().eq(User::getPhone, loginForm.getPhone()).one();
// 判断用户是否存在
if(user == null){
// 不存在 创建新用户并保存
User registerUser = new User();
registerUser.setPhone(loginForm.getPhone());
registerUser.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(8));
save(registerUser);
session.setAttribute("user", registerUser);
}
if(user != null){
session.setAttribute("user", user);
}
return Result.ok();
}
- 校验手机号是否符合规则
- 用户输入的验证码与session中保存的验证码进行比对
- 不一致则返回
验证码错误
的提示信息 - 验证码一致则拿用户输入的手机号进行查库看用户是否存在
- 存在则将查到的用户信息存入session中, 以备后续使用
- 不存在则用输入的手机号自动注册一个新用户存库并将信息保存session
登录拦截器
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取session
HttpSession session = request.getSession();
// 获取session中的用户
Object user = session.getAttribute("user");
// 判断用户是否存在
if(user == null){
// 不存在 401拦截
response.setStatus(401);
return false;
}
// 存在 保存用户到ThreadLocal
UserHolder.saveUser((UserDTO) user);
// 放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户
UserHolder.removeUser();
}
}
- 从请求中拿到session, 再从session中拿到user信息
- 如果user信息为空则返回401响应码并拦截登录请求
- 如果存在则将用户信息存入线程
ThreadLocal
线程中, 以备后续使用 - 最后放行
注册拦截器
@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"
);
}
}
基于 Redis 实现
发送验证码
/**
* 发送手机验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
// 发送短信验证码并保存验证码
return userService.sendCode(phone, session);
@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, phone);
// 返回成功信息
return Result.ok();
}
- 对比上面的代码就是将保存验证码到session变成保存到redis中
登录校验
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// 实现登录功能
return userService.login(loginForm, session);
}
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
String code = loginForm.getCode();
// 校验手机号
if(RegexUtils.isPhoneInvalid(phone)){
// 不符合 返回错误信息
return Result.fail("手机号格式不正确");
}
// 从redis获取验证码并校验
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
if(cacheCode == null || !cacheCode.equals(code)){
// 验证码不一致
return Result.fail("验证码不正确");
}
// 验证码一致 根据手机号查询用户信息
User user = lambdaQuery().eq(User::getPhone, phone).one();
// 判断用户是否存在
if(user == null){
// 不存在 创建新用户并保存
user = createUser(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();
}
private User createUser(String phone) {
User registerUser = new User();
registerUser.setPhone(phone);
registerUser.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
save(registerUser);
return registerUser;
}
- 从redis中拿到验证码进行比对
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone)
- 用
UUID
生成登录令牌token
,String token = UUID.randomUUID().toString(true)
- 将User对象转为HashMap存储
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()))
- 第一个参数是要转换的对象,即
userDTO
。 - 第二个参数是目标
Map
对象,这里传入了一个新的空HashMap
。 CopyOptions.create()
创建了一个复制选项对象,用于配置复制行为。setIgnoreNullValue(true)
表示忽略源对象中值为null
的属性。setFieldValueEditor()
设置一个字段值编辑器,使用一个简单的编辑器,将字段值转换为字符串。
- 在token前加固定标识, 便于理解,
String tokenKey = LOGIN_USER_KEY + token
- 将用户信息存入redis,
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap)
- 设置令牌过期时间
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES)
登录拦截器
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 {
// 获取请求头中的token
String token = request.getHeader("authorization");
if(StrUtil.isBlank(token)){
// 不存在 401拦截
response.setStatus(401);
return false;
}
String key = RedisConstants.LOGIN_USER_KEY + token;
// 基于token获取redis中的用户
Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
.entries(key);
// 判断用户是否存在
if(userMap.isEmpty()){
// 不存在 401拦截
response.setStatus(401);
return false;
}
// 将查询到的Hash数据转换为UserDTO对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 存在 保存用户到ThreadLocal
UserHolder.saveUser(userDTO);
// 刷新token有效期
stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
// 放行
return true;
}
- 在自定义的拦截器里想使用
StringRedisTemplate
模板类需要先定义然后用构造函数调用
private StringRedisTemplate stringRedisTemplate;
public LoginInterceptor(StringRedisTemplate stringRedisTemplate){
this.stringRedisTemplate = stringRedisTemplate;
}
- 从请求头里拿到token
String token = request.getHeader("authorization")
- 从redis拿到token对应的user用户信息,
entries
拿到全部属性
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key)
- 将拿到的
userMap
转换成userDTO
, 再存到线程ThreadLocal
中,fillBeanWithMap
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false)
- 刷新token的有效期
stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES)
- 思路: request -> token -> userMap -> userDTO -> ThreadLocal -> token.expire
登录拦截器优化
- 第一个拦截器拦截所有路径, 从请求中拿到token, 用token拿到redis中的用户信息存入线程中
- 第二个拦截器拦截需要登录的路径, 判断线程中是否存在用户信息, 不存在则判定为该用户未登录, 拦截!
原来的登录拦截器改名为token刷新拦截器
即可, 添加一个登录拦截器就行哈哈哈~
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 判断是否需要拦截 ( ThreadLocal中是否存在用户信息 )
if(UserHolder.getUser() == null){
// 不存在 401拦截
response.setStatus(401);
return false;
}
// 存在 放行
return true;
}
}
在MvcConfig
里注册一下这两个拦截器, 给个执行顺序order
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// token 刷新拦截器
registry.addInterceptor(new RefreshTokenlnterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
// 登录拦截器
registry.addInterceptor(new LoginInterceptor())
// 放行请求
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
).order(1);
}