Express 영역에서 HTTP Only Cookie 값을 가지고 오는 서비스를 구현하였습니다.

  1. Express 영역

    1. import 영역

      import '@angular/localize/init';
      import { APP_BASE_HREF } from '@angular/common';
      import * as bodyParser from 'body-parser';
      import * as express from 'express';
      import jwt from 'jsonwebtoken';
      import { environment } from '@/environments/environment'
      
    2. Express 애플리케이션을 생성

      export function app() {
        const server = express();
        // ...
      }
      
    3. Middleware 설정

      server.use(cookieParser());
      server.use(bodyParser.text());
      server.use(bodyParser.urlencoded({ extended: true }));
      server.use((req, res, next) => {
        res.set('Cache-Control', 'no-store');
        next();
      });
      
    4. 사용자 로그인 관련하여 로직 처리

      /**
         * 로그인 처리
         */
        server.get('/get-userinfo', async (req, res) => {
          const headers = Object.keys(req.headers).reduce((acc, cur) => {
            if (excludeKey.indexOf(cur) > -1) return acc;
            return {...acc, [cur]: req.headers[cur]};
          }, {});
      
          const YBSSTK = req.cookies?._YBSSTK || null;
      
          try {
            const decoded = jwt.verify(YBSSTK, environment.ENV_ACCOUNT_JWT_KEY);
            /** 토큰 검증 정상일 경우 시간 체크 */
            const expried = decoded.exp;
            /** 현재시간 Unix 타임 변환 */
            const now = Math.floor(Date.now() / 1000);
            /** 시간 계산 - 분 */
            const timeDiff = Math.floor((expried - now) / 60);
      
            /** 토큰 시간 30분 초과 */
            if (timeDiff > 30) {
              const userInfoOptions = {
                method: 'POST',
                baseURL: ota_url,
                headers: {
                  ...headers,
                  userToken: YBSSTK,
                },
                url: '/user/get-info',
                data: JSON.stringify({
                  language: 'KO',
                  condition: {stationTypeCode: 'STN01'},
                }),
              };
      
              const signinUserRes = await axios(userInfoOptions);
              res.status(signinUserRes.status).json(signinUserRes.data);
              res.end();
      
              return;
            } else {
              if (YBSSTK.indexOf(environment.adminAuthCookie) > -1) {
                res.clearCookie('_YBSSTK');
                return;
              }
            }
      
            const options = {
              method: 'PUT',
              baseURL: account_url,
              headers: {...headers, userToken: YBSSTK},
              url: '/api/signin/token', // 로그인 토큰 유효성 체크 API
            };
      
            /** 세션 토큰 업데이트 */
            const signinTokenRes = await axios(options);
      
            if (signinTokenRes.data.code !== '0000') throw new Error('TokenUpdateFail');
      
            const {_YBSSTK} = cookie.parse(signinTokenRes.headers['set-cookie'].at(0));
      
            res.setHeader(
              'Set-Cookie',
              cookie.serialize('_YBSSTK', _YBSSTK, {
                httpOnly: true,
                path: '/',
                domain: req.headers.host.match(/\\..+/)[0],
                secure: true,
                sameSite: 'Lax',
              })
            );
      
            const userInfoOptions = {
              method: 'POST',
              baseURL: ota_url,
              headers: {...headers, userToken: YBSSTK},
              url: '/user/get-info', // 로그인 토큰 유효성 체크 API
              data: JSON.stringify({
                language: 'KO',
                condition: {stationTypeCode: 'STN01'},
              }),
            };
      
            const userInfoRes = await axios(userInfoOptions);
      
            res.status(signinTokenRes.status).json(userInfoRes.data);
            res.end();
          } catch (error) {
            const json = {
              code: '9999',
              succeedYn: false,
              message: error.message,
              body: {
                description: `${error.name}: ${error.message}`,
              },
            };
            switch (error.name) {
              case 'TokenUpdateFail':
                json.message = '토큰 업데이트 실패';
                break;
              case 'UserinquiryFail':
                json.message = '사용자 정보 조회 실패';
                break;
              default:
            }
            res.status(200);
            res.json(json);
            res.end();
          }
        });
      
    5. 서버 설정 및 시작

      function run() {
        const port = process.env.PORT || environment.serverPort;
        const server = app();
      
        server.listen(port, () => {
          console.log(`Node Express server listening on <http://localhost>:${port}`);
        });
      }
      
      run();
      
  2. RestCall 공통 서비스 영역

    import {HttpClient, HttpErrorResponse, HttpHeaders, HttpParams} from '@angular/common/http';
    import {Injectable} from '@angular/core';
    import {Observable, throwError} from 'rxjs';
    import {catchError, timeout} from 'rxjs/operators';
    
    import {environment} from '@/environments/environment';
    import {RequestParam} from '@/app/common-source/models/common/condition.model';
    
    @Injectable({
      providedIn: 'root',
    })
    export class RestCallService {
      private baseUrl: string;
      private nethruUrl: string;
      private httpOptions: any;
      private accountUrl: string;
    
      constructor(private readonly http: HttpClient) {
        this.initialize();
      }
    
      private initialize() {
        this.baseUrl = environment.API_URL;
        this.nethruUrl = environment.NETHRU_URL;
        this.accountUrl = environment.ACCOUNT_URL;
        this.httpOptions = {
          headers: new HttpHeaders({
            'Content-Type': 'application/json', // You can set other headers here
          }),
          withCredentials: true,
        };
      }
    
      /**
       * handleError
       * console에 api 에러 로그 남기기
       */
      private handleError(error: HttpErrorResponse) {
        if (error.error instanceof ErrorEvent) {
          console.error('통신 에러:', error.error.message);
        } else {
          console.error(`서버 리턴 코드 ${error.status}, ` + `에러 내용: ${error.error}`);
        }
        return throwError('서버 통신 에러 발생하였습니다. 잠시 후 다시 시도하세요');
      }
    
      public httpGet(path: string, data: RequestParam, isInnerAPI?: boolean): Observable<any> {
        return this.http
          .get(isInnerAPI === undefined ? path : this.baseUrl + path, {...this.httpOptions, param: data})
          .pipe(timeout(Number(environment.timeOut)), catchError(this.handleError));
      }
    
      public httpPost(path: string, data?: RequestParam, opt?: any, timeOut?: number): Observable<any> {
        const url: string = this.baseUrl + path;
        console.log('httpPost', url);
        const body: RequestParam = _.cloneDeep(data) || undefined;
        if (body) {
          body.condition = _.omit(body.condition, ['_gac', '_ga']);
        }
        const options: any = {...this.httpOptions, ...opt};
    
        return this.http.post(url, body, options).pipe(timeout(Number(timeOut || environment.timeOut)), catchError(this.handleError));
      }
    
      public httpPut(path: string, data: RequestParam, opt?: any): Observable<any> {
        const url: string = this.baseUrl + path;
        const body: RequestParam = _.cloneDeep(data) || undefined;
        if (body) {
          body.condition = _.omit(body.condition, ['_gac', '_ga']);
        }
        const options: any = {...this.httpOptions, ...opt};
    
        return this.http.put(url, body, options).pipe(timeout(Number(environment.timeOut)), catchError(this.handleError));
      }
    
      public httpDelete(path: string, data: RequestParam): Observable<any> {
        const url: string = this.baseUrl + path + qs.stringify(data);
        return this.http.delete(url).pipe(timeout(Number(environment.timeOut)), catchError(this.handleError));
      }
    }
    
  3. API URL 영역

    import {Injectable} from '@angular/core';
    import {Observable} from 'rxjs';
    
    import {RestCallService} from '../rest-call/rest-call.service';
    
    import {RequestParam} from '@/app/common-source/models/common/condition.model';
    
    @Injectable({
      providedIn: 'root',
    })
    export class ApiUserService {
      constructor(private readonly restCallS: RestCallService) {}
    
      public GET_SSR_USERINFO(body?: RequestParam): Observable<any> {
        return this.restCallS.httpGet('/get-userinfo', body);
      }
    }
    
  4. Service 영역

    ...
    
    public getSSRUserInfo(): Observable<boolean> {
        return this.apiUserS.GET_SSR_USERINFO().pipe(
          map((res: any) => {
            if (res && res.succeedYn) {
              console.log('tokenGetter api : ', res);
              this.updateUserInfo(res);
              return true;
            } else {
              this.clearToken();
              return false;
            }
          }),
          catchError((err: any) => {
            console.log('Get SSRLoginToken Failed', err.errorMessage);
            return of(false); // Emitting false value in case of error
          })
        );
      }