赞
踩
目录
远程仓库采用码云,创建好仓库,复制仓库的url
在idea中点击,出现git选项,点击ok
之后右击项目,点击remotes
填写url即可集成git
传统的登录认证会采用session进行登录认证,将登录的验证码,用户信息都存放到session中,我们通过session来进行操作数据,这有什么问题呢
每个tomcat服务器中都有一份属于自己的session,假设用户第一次访问第一台tomcat,并且把自己的信息存放到第一台服务器的session中,但是第二次这个用户访问到了第二台tomcat,那么在第二台服务器上,肯定没有第一台服务器存放的session,即session在各个服务器之间是不共通的,所以此时整个登录拦截功能就会出现问题,而redis数据本身就是共享的,就可以避免session共享的问题了
存储验证码时采用String类型,key采用业务代码+手机号
存储user数据时采用hash,key采用业务代码+随机的tonke
hash可以将对象中的每个字段独立存储,可以针对单个字段做CRUD,并且内存占用更少,实际上也可以采用String类型,但hash类型消耗内存较少,故选用String类型
当注册完成后,用户去登录会去校验用户提交的手机号和验证码,是否一致,如果一致,则根据手机号查询用户信息,不存在则新建,最后将用户数据保存到redis,并且生成token作为redis的key,当我们校验用户是否登录时,会去携带着token进行访问,从redis中取出token对应的value,判断是否存在这个数据,如果没有则拦截,如果存在则将其保存到threadLocal中,简化后续业务获取用户信息的代码,后续业务需要登录用户的信息,只要在threadLocal中取即可,无需从redis中取从而增加复杂度,最后放行。
redis业务代码常量(后续继续补充)
- public class RedisConstants {
- //发送验证码业务标识
- public static final String LOGIN_CODE_KEY = "login:code:";
- public static final Long LOGIN_CODE_TTL = 2L;
- //用户登录业务标识
- public static final String LOGIN_USER_KEY = "login:token:";
- public static final Long LOGIN_USER_TTL = 30L;
-
-
- }
采用日志打印形式,将验证码打印在控制台
- public Result sendCode(String phone) {
- //校验手机号
- 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();
- }
我们需要对用户敏感信息进行筛选,只给前端返回必要的用户信息数据,需要封装dto对象,同时在uuid生成token,作为redis取数据的key,最后要设置过期时间,防止reids内存爆炸,设置为30分钟,因为session的过期时间也是30分钟
- public Result login(LoginFormDTO loginForm) {
- 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("验证码错误");
- }
- //根据电话号码从数据库查询用户是否存在
- LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
- queryWrapper.eq(User::getPhone, phone);
- User user = userMapper.selectOne(queryWrapper);
- //不存在则创建
- if (user == null) {
- user = createUserWithPhone(phone);
- }
- String token = UUID.randomUUID().toString(true);
- //只返回不敏感的信息,使用dto进行封装
- UserDTO userDTO = new UserDTO();
- BeanUtils.copyProperties(user, userDTO);
- //将dto对象转化为map结构
- Map<String, String> userMap = new HashMap<>();
- userMap.put("id", userDTO.getId().toString());
- userMap.put("icon", userDTO.getIcon());
- userMap.put("nickName", userDTO.getNickName());
- //业务代码+token形成redis中的key
- String tokenKey = LOGIN_USER_KEY + token;
- stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
- stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
-
- return Result.ok(token);
- }
- @Configuration
- public class MvcConfig implements WebMvcConfigurer {
- @Autowired
- 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"
- );
- }
- }
拦截器需要的stringRedisTemplate对象不能通过@Autowired直接注入
因为LoginInterceptor 对象是我们手动创建的,不受spring管控,不在spring容器中,故不能注入spring容器中的bean,只能通过在配置类中通过构造方法注入stringRedisTemplate对象
- 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)) {
- response.setStatus(401);
- return false;
- }
- String tokenKey = LOGIN_USER_KEY + token;
-
- Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);
- //3.判断用户是否存在
- if (userMap.isEmpty()) {
- //4.不存在,拦截,返回401状态码
- response.setStatus(401);
- return false;
- }
- UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
-
- //5.存在,保存用户信息到Threadlocal
- UserHolder.saveUser(userDTO);
- //刷新登录状态
- stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
- //6.放行
- return true;
- }
- }
那就是登录认证和刷新用户状态绑定在一个拦截器中,而需要登录认证的路径并不是全部路径,如果用户不访问需要登录认证的路径,那就刷新不了用户状态,即30分钟之后登录就会退出,这明显不符合我们的常识
我们可以再加一个拦截器,形成一个拦截器链,第一个拦截器对所有路径进行拦截,而它的功能就是刷新登录状态,登录认证则由第二个拦截器完成
刷新状态拦截器
- 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 key = LOGIN_USER_KEY + token;
- Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
- // 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(key, LOGIN_USER_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;
- }
- }
为了保证拦截器的执行先后顺序,配置类需设置order的大小,设定拦截器执行的先后的顺序,如若不设置,就按照注册顺序的先后来执行
- @Configuration
- public class MvcConfig implements WebMvcConfigurer {
-
- @Resource
- private StringRedisTemplate stringRedisTemplate;
-
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- // 登录拦截器
- registry.addInterceptor(new LoginInterceptor())
- .excludePathPatterns(
- "/shop/**",
- "/voucher/**",
- "/shop-type/**",
- "/upload/**",
- "/blog/hot",
- "/user/code",
- "/user/login"
- ).order(1);
- // token刷新的拦截器
- registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
- }
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。