项目介绍
导入黑马点评项目
项目架构
基于Session实现登录
基本流程
实现发送短信验证码功能
controller层中
/**
* 发送手机验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
// 发送短信验证码并保存验证码
return userService.sendCode(phone,session);
}
Service层中
真的发送的话要接入阿里云或腾讯云的短信发送功能,这里假装发送成功。
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public Result sendCode(String phone, HttpSession session) {
//1.校验手机号
if(RegexUtils.isPhoneInvalid(phone)){
//2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
//3.符合,生成验证码
String code = RandomUtil.randomNumbers(6);
//4.保存验证码
session.setAttribute("code",code);
//5.发送验证码
log.debug("发送短信验证码成功,验证码:{}",code);
return Result.ok();
}
}
再次点击发送就可以看见如下:
实现验证码登录和注册功能
Controller层中
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// 实现登录功能
return userService.login(loginForm,session);
}
Service层中
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1.验证手机号
String phone=loginForm.getPhone();
if(RegexUtils.isPhoneInvalid(phone)){
//1.1不符合,返回报错
return Result.fail("手机号格式错误!");
}
//2.校验验证码
Object cacheCode = session.getAttribute("code");
String code = loginForm.getCode();
if(cacheCode==null||!cacheCode.toString().equals(code)){
//3.不一致,报错
return Result.fail("验证码错误");
}
//4.一致,根据手机号查询用户 select * from tb_user where phone=?
User user = query().eq("phone", phone).one();
//5.判断用户是否存在
if(user==null){
//6.不存在,创建新用户并保存
user= createUserWithPhone(phone);
}
//7.存在,保存用户信息到session中
session.setAttribute("user",user);
return Result.ok();
}
private User createUserWithPhone(String phone) {
//1.创建用户
User user=new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
//2.保存
save(user);
return user;
}
这里的session之所以能够获取到发送验证码请求时存入的验证码是因为这是在同一个会话当中,session会被返回给前端,下一次请求也会携带同一个session过来。所以才有之前保存的信息在里面。
实现登录校验拦截器
接口多了之后就要拦截器进行统一校验,然后为了传递用户信息给后序的业务,需要将用户信息存入Threadlocal里面的.Threadlocal是每个请求线程的一个独立保存空间。
在拦截器的最后一个方法中,清空thread local的信息,第一可以做到退出登录,第二可以防止内存泄露
新建一个拦截器
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.获取session
HttpSession session = request.getSession();
//2.获取session中的用户
Object user = session.getAttribute("user");
//3.判断用户是否存在
if(user==null){
//4.不存在,拦截
response.setStatus(401);
return false;
}
//5.存在,保存用户信息到ThreadLocal
UserHolder.saveUser((UserDTO) user);
//6.放行
return true;
}
@Override
public void afterCompletion(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(
"/shop/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code" ,
"/user/login");
}
}
登录校验接口
@GetMapping("/me")
public Result me(){
// 获取当前登录的用户并返回
UserDTO user= UserHolder.getUser();
return Result.ok(user);
}
隐藏用户敏感信息
在前面代码里返回给前端的是用户完整信息,这是不合理的,所以改成如下。
在session存的时候就只存部分信息。然后上面登录校验逻辑因为黑马资料的问题已经是隐藏之后的了的,所以只改这里一个地方即可。
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
....
....
//7.存在,保存用户信息到session中
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
return Result.ok();
}
集群的session共享问题
使用Redis解决Session共享问题。
基于Redis实现共享session登录
业务流程
基于redis实现短信登录
发送短信验证码部分的代码修改:
生成验证码之后将验证码存入Redis
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result sendCode(String phone, HttpSession session) {
//1.校验手机号
...
//3.符合,生成验证码
...
//4.保存验证码到Redis
stringRedisTemplate.opsForValue().set("login:code:"+phone,code,2, TimeUnit.MINUTES);
//5.发送验证码
...
}
短信验证码登录,注册功能部分的代码修改:
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1.验证手机号
...
// 2.从redis获取 校验验证码
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone);
...
//4.一致,根据手机号查询用户 select * from tb_user where phone=?
...
//5.判断用户是否存在
...
//7.存在,保存用户信息到Redis中
//7.1.随机生成token,作为登录令牌
String token = UUID.randomUUID().toString(true);
//7.2将User对象转为Hash存储
UserDTO userDTO=BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO);
//7.3存储
String tokenKey="login::token:"+token;
stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
//7.4设置token有效期
stringRedisTemplate.expire(tokenKey,30,TimeUnit.MINUTES);
//8.返回token
return Result.ok(token);
}
登录拦截器代码修改
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 {
//1.获取请求头中的token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
//4.不存在,拦截
response.setStatus(401);
return false;
}
//2.基于token获取redis中的用户
String tokenKey=RedisConstants.LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
.entries(tokenKey);
//3.判断用户是否存在
if(userMap.isEmpty()){
//4.不存在,拦截
response.setStatus(401);
return false;
}
//5.将查询到的Hash数据转为UserDTO对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//6.存在,保存用户信息到ThreadLocal
UserHolder.saveUser( userDTO);
//7.刷新token有效期
stringRedisTemplate.expire(tokenKey,RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
//8.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//移除用户
...
}
}
因为这里不能用注解注入stringRedisTemplate,要在MVC配置器哪里进行注入
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
.excludePathPatterns(
"/shop/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code" ,
"/user/login");
}
}
出现Long转String的报错
在登录逻辑里面将对象转为Map时做多点工作
//7.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()));
可以看见对应东西
解决登录状态刷新问题
登录状态的刷新只会在访问需要拦截的请求时才刷新,如果是不需要拦截的请求就不会刷新.
为了拦截所有请求,这里新加一个拦截器
新增刷新拦截器:
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 tokenKey=RedisConstants.LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
.entries(tokenKey);
//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(tokenKey,RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
//8.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//移除用户
UserHolder.removeUser();
}
}
修改原本的登录拦截器
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;
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//移除用户
UserHolder.removeUser();
}
}
MVC配置类当中
需要添加order保证刷新拦截器先执行。
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//登录拦截器
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/shop/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code" ,
"/user/login").order(1);
//刷新拦截器
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate))
.addPathPatterns("/**").order(0);
}
}