3

在看完后台登陆操作以后对登陆整个流程有了更深的理解,个人认为有必要总结一下。
下面是整个登陆操作的流程图:

我们首先从前台开始:

onSubmit() {
    const username = this.formGroup.get('username').value;
    const password = this.formGroup.get('password').value;
    this.teacherService.login(username, password).subscribe(result => {
      console.log(result);
      if (result) {
        this.teacherService.setIsLogin(true);
      } else {
        console.log('用户名密码错误');
      }
    });
  }

用户在V层输入信息传到C层再调用M层代码

  login(username: string, password: string): Observable<boolean> {
    const url = 'http://localhost:8080/Teacher/login';
    return this.httpClient.post<boolean>(url, {username, password});
  }

  setIsLogin(isLogin: boolean) {
    window.sessionStorage.setItem(this.isLoginCacheKey, this.convertBooleanToString(isLogin));
    this.isLogin.next(isLogin);
  }

如果后台返回信息为true则设置缓存isLogin为1;
如果后台返回信息为false则登陆失败。

前台通过post方法将username,password传给后台。
之后会被前台的拦截器拦截

export class AuthTokenInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const reqClone = req.clone({
      setHeaders: {'auth-token': CacheService.getAuthToken()}
    });
    ...
      return httpEvent;
    }));
  }
}
 private static authToken: string = sessionStorage.getItem('authToken');
 static getAuthToken() {
    if (CacheService.authToken === null) {
      return '';
    }
    return CacheService.authToken;
  }

拦截器会设置request的header中的令牌通过sessionStorage缓存。

sessionStorage 用于临时保存同一窗口(或标签页)的数据,在关闭窗口或标签页之后将会删除这些数据。

之后数据会转发到后台。
首先会经过后台过滤器,检查request的header中的令牌是否有效,如果有效会设置响应的header中的令牌为该令牌,如果无效会通过UUID.randomUUID().toString()方法再生成一个令牌,并且将该令牌加入到缓存,通过装饰器模式将该令牌加入到请求的header中。之后将数据继续转发。

在这之中在将从请求的header获取中用到了装饰器模式

class HttpServletRequestTokenWrapper extends HttpServletRequestWrapper {
        HttpServletRequestTokenWrapper httpServletRequestTokenWrapper;
        String token;
        private HttpServletRequestTokenWrapper(HttpServletRequest request) {
            super(request); //super()可看作调用父类的构造函数
        }

        public HttpServletRequestTokenWrapper(HttpServletRequest request, String token) {
            this(request);
            this.token = token;
        }

        @Override
        public String getHeader(String name) {
            if(TOKEN_KEY.equals(name)) {
                logger.info("设置了token为" + token);
                return this.token;
            }
            return super.getHeader(name);
        }
    }

装饰器模式个人认为就是相当于对HttpServletRequest新写了一个子类,重写了构造函数使得可以将token传入对象中,再重写它的getheader方法,如果是用getheader获取令牌则返回通过构造函数传入的令牌。并且根据测试装饰器的作用范围不只是在过滤器中,而是适用于整个后台项目,即在其他文件方法中调用getheader("auth-token")也可以得到想要的结果。

数据继续转发后会经过后台拦截器,在这里判断用户是否登陆,如果未登录则会进行拦截并返还给前台401错误。

String url = request.getRequestURI();
        String method = request.getMethod();
        System.out.println("请求的地址为" + url + "请求的方法为:" + method);

        if( "OPTIONS".equals(method)) {
            /** 
            请求方法为OPTIONS,浏览器在进行跨域访问时,如果发现请求的方法不是get,那么在请
            求以前则会向该请求地址发送options方法来确认后台允许前台发起的请求方法,当后台返回
            的允许请求方法中包括了 POST方法时,浏览器才会发起请求,否则将放弃请求。
            **/
            return true;
        }

        // 判断请求地址、方法是否与用户登录相同
        if ("/Teacher/login".equals(url) && "POST".equals(method)) {
            System.out.println("请求地址方法匹配到登录地址,不拦截");
            return true;
        }

        // auth-token是否绑定了用户
        String authToken = request.getHeader(TokenFilter.TOKEN_KEY);
        if (this.teacherService.isLogin(authToken)) {
            System.out.println("当前token已绑定登录用户,不拦截");
            return true;
        }

        System.out.println("当前token未绑定登录用户,返回401");
        // 为响应加入提示:用户未登录
        response.setStatus(401);
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                           @Nullable ModelAndView modelAndView) throws Exception {
        logger.info("执行拦截器postHandle");
    }

经过拦截器之后请求会被转发到后台的C层,匹配到相应的url和方法后执行对应的login操作。如果根据传来的username和password验证失败就会直接返回false,如果验证成功则会把当前请求包含的令牌与当前登陆用户的ID进行绑定。并返回true;

令牌与用户ID绑定:

this.authTokenTeacherIdHashMap.put(this.request.getHeader("auth-token"), teacher.getId());

相应的,如果想要判断用户是否登陆只需要判断当前的token有没有与之绑定的用户id,即:

public boolean isLogin(String authToken) {
        Long teacherId = this.authTokenTeacherIdHashMap.get(authToken);
        return teacherId != null;
    }

如果想要注销就只需要删除令牌与用户id的绑定:

public void logout() {
        String authToken = this.request.getHeader("auth-token");
        logger.info("获取到的auth-token为" + this.request.getHeader("auth-token"));
        this.authTokenTeacherIdHashMap.remove(authToken);
    }

当后台处理完毕返回结果时还要经过后台拦截器的事后过滤,经过事后过滤一个完整的响应就已经完成了,在这之后还需要经过后台过滤器进行事后监控才会将响应传往前台。
响应到达前台后还要经过前台拦截器进行更新当前令牌的操作才能被继续转发。

const reqClone = req.clone({
      setHeaders: {'auth-token': CacheService.getAuthToken()}
    });
 return next.handle(reqClone).pipe(map((httpEvent) => {
      if (httpEvent instanceof HttpResponse) {
        const httpResponse = httpEvent as HttpResponse<any>;
        const authToken = httpResponse.headers.get('auth-token');
        console.log('拦截1'); //执行一次
        CacheService.setAuthToken(authToken);
      }
      console.log('拦截2'); //执行两次
      return httpEvent;
    }));

这也证明了拦截器会对响应作处理,上面是执行一次请求的执行结果。
将响应的令牌存到缓存中即更新当前令牌:

static setAuthToken(token: string) {
    CacheService.authToken = token;
    sessionStorage.setItem('authToken', token);
  }

至此整个登陆流程介绍完毕。

另外:本周还遇到一个小问题-钉钉机器人连接失败。
钉钉机器人建立时是默认连接成功的,在执行请求时会调用以下代码:

      // 由客户端执行(发送)Post请求
      response = httpClient.execute(httpPost);
      // 从响应模型中获取响应实体
      HttpEntity responseEntity = response.getEntity();

      System.out.println("响应状态为:" + response.getStatusLine());
      if (responseEntity != null) {
        if (EntityUtils.toString(responseEntity).substring(11, 12).equals("0")) {
          ding.setConnectionStatus(true);
        } else {
          ding.setConnectionStatus(false);
        }

即如果得到的响应不符合要求就会将连接状态设为false,起初认为是webhook或secret出了问题可是经过多次验证也没有发现问题,更换其他机器人也没有解决。这时我想到在计算机时间错误时访问一些网站会发生类似于下面这样的报错


连接到 www.kancloud.cn 时发生错误。OCSP 回应尚未生效(含有一个未来的日期)

错误代码:SEC_ERROR_OCSP_FUTURE_RESPONSE

所以我猜测钉钉机器人可能也会有这样的机制,随后把时间改过来再次尝试便连接成功。


李明
441 声望19 粉丝