目录
session实现登录流程?编辑
session实现登录的缺点——集群的session共享问题
解决方案——基于Redis实现共享session登录
项目结构
代码及其说明
config下MvcConfig 配置两个拦截器,第一个拦截一切路径,让token刷新的拦截器,第二个拦截需要登录的路径
第一个拦截器的代码
第二个拦截器的代码
接下来主要是发送短信验证码并保存验证码,然后就是实现登录功能,这里用到了msql和redis两种数据库
UserController
UserService
重要的是这里实现类
1.sendcode
2.login
展示
总结
session实现登录流程
session实现登录的缺点——集群的session共享问题
多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题。
解决方案——基于Redis实现共享session登录
项目结构
代码及其说明
config下MvcConfig 配置两个拦截器,第一个拦截一切路径,让token刷新的拦截器,第二个拦截需要登录的路径
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(/*stringRedisTemplate*/)) .excludePathPatterns( "/shop/**", "/voucher/**", "/shop-type/**", "/upload/**", "/blog/hot", "/user/code", "/user/login" ).order(1); //拦截所有 registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0); } }
第一个拦截器的代码
获取请求头中的token,如果token为空,放行,基于TOKEN获取redis中的用户,然后判断用户是否存在,如果用户不存在,放行,再将查询到的hash数据转为UserDTO,保存用户信息到 ThreadLocal,刷新token有效期,放行
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 boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("authorization"); if(StrUtil.isBlank(token)){ return true; } String key=RedisConstants.LOGIN_USER_KEY+token; Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key); if(userMap.isEmpty()){ return true; } UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); UserHolder.saveUser(userDTO); 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(); } }
第二个拦截器的代码
判断是否需要拦截(ThreadLocal中是否有用户)没有,需要拦截,设置状态码,有用户,则放行
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 org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.servlet.HandlerInterceptor; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.sql.Struct; import java.util.Map; import java.util.concurrent.TimeUnit; 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 { /* HttpSession session = request.getSession(); Object user = session.getAttribute("user"); if(user==null){ response.setStatus(401); return false; } UserHolder.saveUser((UserDTO) user); return true;*/ /* String token = request.getHeader("authorization"); if(StrUtil.isBlank(token)){ response.setStatus(401); return false; } String key=RedisConstants.LOGIN_USER_KEY+token; Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key); if(userMap.isEmpty()){ response.setStatus(401); return false; } UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); UserHolder.saveUser(userDTO); stringRedisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);*/ if(UserHolder.getUser()==null){ response.setStatus(401); return false; } return true; } /* @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserHolder.removeUser(); }*/ }
接下来主要是发送短信验证码并保存验证码,然后就是实现登录功能,这里用到了msql和redis两种数据库
UserController
package com.hmdp.controller; import com.hmdp.dto.LoginFormDTO; import com.hmdp.dto.Result; import com.hmdp.dto.UserDTO; import com.hmdp.entity.User; import com.hmdp.entity.UserInfo; import com.hmdp.service.IUserInfoService; import com.hmdp.service.IUserService; import com.hmdp.utils.UserHolder; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.servlet.http.HttpSession; @Slf4j @RestController @RequestMapping("/user") public class UserController { @Resource private IUserService userService; @Resource private IUserInfoService userInfoService; /** * 发送手机验证码 */ @PostMapping("code") public Result sendCode(@RequestParam("phone") String phone, HttpSession session) { // TODO 发送短信验证码并保存验证码 return userService.sendCode(phone,session); } /** * 登录功能 * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码 */ @PostMapping("/login") public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){ // TODO 实现登录功能 return userService.login(loginForm,session); } /** * 登出功能 * @return 无 */ @PostMapping("/logout") public Result logout(){ // TODO 实现登出功能 return Result.fail("功能未完成"); } @GetMapping("/me") public Result me(){ // TODO 获取当前登录的用户并返回 UserDTO user= UserHolder.getUser(); return Result.ok(user); } @GetMapping("/info/{id}") public Result info(@PathVariable("id") Long userId){ // 查询详情 UserInfo info = userInfoService.getById(userId); if (info == null) { // 没有详情,应该是第一次查看详情 return Result.ok(); } info.setCreateTime(null); info.setUpdateTime(null); // 返回 return Result.ok(info); } }
UserService
package com.hmdp.service; import com.baomidou.mybatisplus.extension.service.IService; import com.hmdp.dto.LoginFormDTO; import com.hmdp.dto.Result; import com.hmdp.entity.User; import javax.servlet.http.HttpSession; public interface IUserService extends IService<User> { Result sendCode(String phone, HttpSession session); Result login(LoginFormDTO loginForm, HttpSession session); }
重要的是这里实现类
1.sendcode
校验手机号的合法性,如果不符合,返回错误信息,如果符合,生成验证码
然后是这行代码
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
这段代码的目的是在Redis中为一个特定的电话号码设置一个验证码,并为其设置一个特定的生存时间。
StringRedisTemplate: 这是Spring Data Redis的一个组件,它提供了方便的Redis字符串操作,例如设置和获取字符串值。
opsForValue(): 这个方法返回一个操作字符串值的操作对象。在Redis中,数据通常存储为字符串,所以这个方法用于获取操作字符串值的接口。
set(...): 这是上面提到的操作对象的一个方法,用于设置Redis中的值。
LOGIN_CODE_KEY + phone: 这是要设置的键。
code: 这是要设置的值。
LOGIN_CODE_TTL: 表示键的生存时间(Time To Live)。当这个时间过去后,Redis会自动删除这个键和它的值。
TimeUnit.MINUTES: 这指定了生存时间的单位。在这里,它是分钟。
2.login
校验手机号如果不符合,返回错误信息
下面这行的意思是然后从redis获取验证码并校验
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
不一致,报错,一致,根据手机号查询用户 select * from tb_user where phone = ?
下面代码用的是MP,的意思是从数据库中查询具有给定电话号码的第一个用户,并将结果存储在
User user = query().eq("phone", phone).one();
然后,判断用户是否存在,不存在,创建新用户并保存,这里也用了MP
user = createUserWithPhone(phone);
然后保存用户信息到 redis中,随机生成token,作为登录令牌
下面的代码意思是将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);
最后返回token
package com.hmdp.service.impl; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.copier.CopyOptions; import cn.hutool.core.lang.UUID; import cn.hutool.core.util.RandomUtil; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.hmdp.dto.LoginFormDTO; import com.hmdp.dto.Result; import com.hmdp.dto.UserDTO; import com.hmdp.entity.User; import com.hmdp.mapper.UserMapper; import com.hmdp.service.IUserService; import com.hmdp.utils.RedisConstants; import com.hmdp.utils.RegexPatterns; import com.hmdp.utils.RegexUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import javax.annotation.Resource; import javax.servlet.http.HttpSession; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import static com.hmdp.utils.RedisConstants.*; import static com.hmdp.utils.SystemConstants.USER_NICK_NAME_PREFIX; import static java.util.concurrent.TimeUnit.MINUTES; @Slf4j @Service 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); //session.setAttribute("code",code); 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("手机号格式错误"); } // Object cacheCode =session.getAttribute("code"); 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); } // session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class)); String token = UUID.randomUUID().toString(true); 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); stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES); return Result.ok(token); } private User createUserWithPhone(String phone) { User user = new User(); user.setPhone(phone); user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10)); save(user); return user; } }
展示
输入手机号,获取验证码登录
验证码成功了
输入
登录成功!!
总结
想要完整代码的私信我吧!
人生第一次发博客!
单纯为了记录!