一 、基于Session实现登录
1.1流程图
1.2获取验证码
@Override
public Result sendCode(String phone, HttpSession session) {
if(phone == null){
return Result.fail("请输入手机号");
}
//1.校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
//2.如果不符合,返回错误信息
return Result.fail("手机号格式错误");
}
//3.生成验证码
String code = RandomUtil.randomNumbers(6);
//4.保存验证码到session
session.setAttribute("code",code);
//5.发送验证码 模拟发送
log.info(code);
return Result.ok();
}
1.3登录校验
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
if(loginForm.getCode() == null){
return Result.fail("请输入验证码");
}
if(loginForm.getPhone() == null){
return Result.fail("请输入手机号");
}
//1.验证手机号和校验码
if (RegexUtils.isPhoneInvalid(loginForm.getPhone())) {
return Result.fail("手机号格式错误");
}
String cacheCode = (String) session.getAttribute("code");
if (!cacheCode.equals(loginForm.getCode())) {
return Result.fail("验证码错误");
}
//2.查询用户是否存在
User user = query().eq("phone", loginForm.getPhone()).one();
//3.不存在,创建新用户并保存
if (user == null){
user = createUserWithPhone(loginForm.getPhone());
}
//4.存在,保存用户信息到session
session.setAttribute("user",user);
return Result.ok();
}
1.4用户状态处理
登录拦截器
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(javax.servlet.http.HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.获取用户
Object user = request.getSession().getAttribute("user");
//2.判断用户是否存在
if (user == null) {
//3.不存在拦截
response.setStatus(401);
return false;
}
//4.保存用户到UserHolder
UserHolder.saveUser(BeanUtil.copyProperties(user, UserDTO.class));
//5.放行
return true;
}
@Override
public void afterCompletion(javax.servlet.http.HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//移除用户
UserHolder.removeUser();
}
}
配置登录拦截器
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//登录拦截器
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
//配置不会拦截的路径 例如
"/user/login"
).order(1);
}
}
1.5集群的session共享问题
session共享问题:多台Tomcat并不共享session存储空间,当请求到不同tomcat服务器时导致数据丢失的问题
如果session做数据拷贝,则内存消耗太大;
解决方案:Redis
二、基于Redis实现共享session登录
1.1流程图
1.2存储验证码
@Override
public Result sendCode(String phone, HttpSession session) {
if(phone == null){
return Result.fail("请输入手机号");
}
//1.校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
//2.如果不符合,返回错误信息
return Result.fail("手机号格式错误");
}
//3.生成验证码
String code = RandomUtil.randomNumbers(6);
//4.保存验证码到redis
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
//5.发送验证码 模拟发送
log.debug("发送验证码成功,验证码:{}",code);
return Result.ok();
}
1.3登录校验
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
if(loginForm.getCode() == null){
return Result.fail("请输入验证码");
}
if(loginForm.getPhone() == null){
return Result.fail("请输入手机号");
}
//1.验证手机号和校验码
if (RegexUtils.isPhoneInvalid(loginForm.getPhone())) {
return Result.fail("手机号格式错误");
}
String redisCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + loginForm.getPhone());
if (redisCode == null || !redisCode.equals(loginForm.getCode())) {
return Result.fail("验证码错误");
}
//2.查询用户是否存在
User user = query().eq("phone", loginForm.getPhone()).one();
//3.不存在,创建新用户并保存
if (user == null){
user = createUserWithPhone(loginForm.getPhone());
}
//4.存在,保存用户信息到Redis
//4.1随机生成token
String token = UUID.randomUUID().toString(true);
//4.2将User对象转换成Hash存储
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);
//4.3设置token有效期
stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
//5.返回token
return Result.ok(token);
}
1.4拦截器
LoginInterceptor:
public class LoginInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(javax.servlet.http.HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.获取token
String token = request.getHeader("authorization");
if(StrUtil.isBlank(token)){
//不存在拦截
response.setStatus(401);
return false;
}
//2.基于token查询用户是否存在
String key = LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
//3.判断用户是否存在
if (userMap.isEmpty()) {
//4.不存在拦截
response.setStatus(401);
return false;
}
//5.转为UserDTO对象
UserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//6.保存用户到UserHolder
UserHolder.saveUser(BeanUtil.copyProperties(user, UserDTO.class));
//7.刷新token有效期
stringRedisTemplate.expire(key,LOGIN_USER_TTL, TimeUnit.MINUTES);
//8.放行
return true;
}
@Override
public void afterCompletion(javax.servlet.http.HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//移除用户
UserHolder.removeUser();
}
}
MvcConfig
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//登录拦截器
registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
.excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"/upload/**",
"/shop-type/**",
"/voucher/**",
"/shop/**"
)
}
}
1.5解决状态登录刷新问题
在这个方案中,他确实可以使用对应路径的拦截,同时刷新登录token令牌的存活时间,但是现在这个拦截器他只是拦截需要被拦截的路径,假设当前用户访问了一些不需要拦截的路径,那么这个拦截器就不会生效,所以此时令牌刷新的动作实际上就不会执行,所以这个方案他是存在问题的
既然之前的拦截器无法对不需要拦截的路径生效,那么我们可以添加一个拦截器,在第一个拦截器中拦截所有的路径,把第二个拦截器做的事情放入到第一个拦截器中,同时刷新令牌,因为第一个拦截器有了threadLocal的数据,所以此时第二个拦截器只需要判断拦截器中的user对象是否存在即可,完成整体刷新功能。
RefreshTokenInterceptor
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(javax.servlet.http.HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.获取token
String token = request.getHeader("authorization");
if(StrUtil.isBlank(token)){
//不存在拦截
response.setStatus(401);
return false;
}
//2.基于token查询用户是否存在
String key = LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
//3.判断用户是否存在
if (userMap.isEmpty()) {
//4.不存在拦截
response.setStatus(401);
return false;
}
//5.转为UserDTO对象
UserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//6.保存用户到UserHolder
UserHolder.saveUser(BeanUtil.copyProperties(user, UserDTO.class));
//7.刷新token有效期
stringRedisTemplate.expire(key,LOGIN_USER_TTL, TimeUnit.MINUTES);
//8.放行
return true;
}
@Override
public void afterCompletion(javax.servlet.http.HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//移除用户
UserHolder.removeUser();
}
}
LoginInterceptor
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;
}
//2.由用户,放行
return true;
}
}
MvcConfig
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//登录拦截器
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"/upload/**",
"/shop-type/**",
"/voucher/**",
"/shop/**"
).order(1);
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0);
}
}
三、 使用配置文件实现session的存储到Redis(超简单)
1. 引入依赖
redis-starter:可以操作redis(注意:这里的版本引入与 SpringBoot 相同的版本。)
spring-session-redis:可以将session 自动存储到redis中
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.6.4</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.6.3</version>
</dependency>
2.配置redis
spring:
redis:
host: 127.0.0.1
port: 6379
password: xxxxx #没有则不需要配置
database: 1
lettuce:
pool:
max-active: 10
max-idle: 10
min-idle: 1
time-between-eviction-runs: 10s
3.修改spring-session存储配置
spring:
session:
#失效时间
timeout: 86400
store-type: redis
配置完成之后,就可以自动将session存储到数据库中,并且还有原来request的方式
4.使用方式
// 存储用户信息到 session
request.getSession().setAttribute("user", user);
// 在 session 中读取用户信息
request.getSession().getAttribute("user");
超级简单而且还解决了session单机存储信息的问题