= 로그인

SDL은 Knox EpTray, ID/PW, ADFS 로그인을 지원한다.

== EpTray
Knox EpTray 로그인은 Chrome, Edge 등 멀티 브라우저에서 가능하다.
사용자가 Knox에 로그인을 한 후 시스템에 접속을 하게 되면 EpTray값을 이용해 시스템 로그인을 한다.

NOTE: Knox EpTray SSO 를 위한 연계 신청이 필요하다. (스테이지/운영) +
자세한 내용 안내 및 문의는 Knox Support (매뉴얼 > KnoxPortal NewEpTray 연계 가이드) 를 참조한다.

=== EpTray 적용
연계신청 과정에서 생성되는 *rsaprivkey8.pem* 파일을 +
config.properties의 login.sso.knox-tray-private-key-path 경로에 넣어준다.

CAUTION: *rsaprivkey8.pem* 파일이 없는 경우 아래와 같은 에러가 발생하므로 주의한다.

image::newEpTrayUtil.png[Can not load new EpTray Private Key.png]

=== LoginPage.vue

UI 에서 사용자 정보가 없을 경우 LoginPage.vue 페이지로 이동하게 되고 인증 절차를 시작한다.

EpTray 연계를 위한 메서드

[source,javascript]
----
async loginByNewEpTray() {
  let loginResult = false;
  SDLUtil.showLoadingBar(true);
  await this.socConnect().then((data) => {
    this.newEpTrayKey = data;
    this.websocket.close();
  }).catch((err) => {
    SDLUtil.alert(err);
  });
  SDLUtil.showLoadingBar(false);

  if (this.newEpTrayKey) {
    if (this.newEpTrayKey.result === 'success') {
      const { userInfo, key } = this.newEpTrayKey;
      loginResult = await this.loginNewEpTray({ userInfo, key });
    } else {
      SDLUtil.alert(`sdl.epTray.error.${this.newEpTrayKey.errorCode}`);
    }
  }

  if (!loginResult) {
    if (this.$route.query.token) {
      loginService.setToken(this.$route.query.token);
      document.location = `${SDLUtil.WEB_CONTEXT_PATH}/`;
    }
  }
  this.afterLoginProcess(loginResult);
},
----

[source,javascript]
----
socConnect() {
  return new Promise(((resolve, reject) => {
    const server = new WebSocket('wss://localhost:29283'); // <1>
    this.websocket = server;
    server.onopen = () => {
      server.send('{"rqtype":"getknoxsso","token":"","data":"KCC60TRAY0072"}'); // <2>
      server.onmessage = (event) => {
        const socketData = JSON.parse(event.data);
        if (socketData.rpcode === 'RETURN_SUCCESS') {
          resolve(JSON.parse(socketData.data));
        } else {
          this.websocket.close();
          let err;
          if (socketData.rpcode === 'EMPTY_BOX') err = 'sdl.epTray.rpCode.EMPTY_BOX';
          else err = `sdl.epTray.rpCode.${socketData.detail}`;
          reject(err);
        }
      };
    };
    server.onerror = () => {
      const err = 'sdl.epTray.rpCode.CONNECTION_FAILED';
      reject(err);
    };
  }));
},
----
<1> WebSocket HOST 정보
<2> 발급받은 연계용 system ID

=== LoginController.java

LoginController 에서는 EpTray값의 정보를 이용해 사용자를 로그인 한다.
이때 시스템 사용자가 아닐 경우, 등록 화면으로 이동하고, 정상적인 시스템 사용자일 경우
Token을 발급한다.

GET /noauth/login/new-eptray

[source,java]
----
@PostMapping("/noauth/login/new-eptray")
@Operation(value = "", description = "knox new eptray 로그인")
public JwtResponse loginByNewEptray(HttpServletRequest request, HttpServletResponse response, @RequestParam String encodeUserInfo, @RequestParam String encodeAesKey ) throws AccountException {
    loginInterceptorExecutor.applyPreLogin(request, response);
    User user = authenticationService.loginByNewEpTray(encodeUserInfo, encodeAesKey);
    user.setRecentLoginIp(webUtil.getClientIp(request));
    JwtResponse jwtResponse = new JwtResponse(jwtUtil.generateToken(user), user.getUserId());
    user.setJwt(jwtResponse.getJwtToken().split("\\.")[2]);
    loginInterceptorExecutor.applyPostLogin(request, response, user);
    return jwtResponse;
}
----

== ADFS 로그인

NOTE: ADFS 통합인증 사이트 (https://adsso.sec.samsung.net) 에서 신청 후 사용해야 한다. +
자세한 내용은 해당 사이트를 참조하거나 전자통합인증3 (nextsso3@samsung.com) 으로 문의 한다.

SDL에서는 ADFS 통합인증 사이트의 가이드를 참조하여 AD 로그인 샘플을 제공하고 있으며, 각 프로젝트 환경에 맞게 수정하여 사용한다.

=== 참고할 샘플 파일
* LoginPage.vue
* AdLoginController.java
* onelogin.saml.properties

== 로그인/아웃 전,후 처리

로그인 전과 후, 로그아웃 전과 후 비즈니스 로직이 필요한 경우 LoginInterceptor를 상속받아 구현한다.

LoginInterceptor 클래스는 아래와 같은 Interface를 제공한다.
[source, java]
----
 /**
 * 로그인 전
 */
default void preLogin(HttpServletRequest request, HttpServletResponse response) {
}

/**
 * 로그인 후
 */
default void postLogin(HttpServletRequest request, HttpServletResponse response, User user) {
}

/**
 * 로그아웃 전
 */
default void preLogout(HttpServletRequest request, HttpServletResponse response, User user) {
}

/**
 * 로그아웃 후
 */
default void postLogout(HttpServletRequest request, HttpServletResponse response, User user) {
}
----

LoginInterceptor를 구현한 클래스는 LoginInterceptorExecutor에 의해 실행되며 Spring Bean으로 등록하고 SpringWebCofig에 아래와 같이 LoginInterceptorExecutor에 등록한다.

[source, java]
----
@Bean
public LoginInterceptorExecutor loginInterceptorExecutor() {
    List<LoginInterceptor> loginInterceptorList = new ArrayList<>();
    loginInterceptorList.add(userUpdateInterceptor());
    loginInterceptorList.add(loginOutLogInterceptor());
    return new LoginInterceptorExecutor(loginInterceptorList);
}
----

아래는 로그인 후 사용자 정보를 update 하는 UserUpdateInterceptor의 일부분이다.

[source, java]
----
@Override
    public void postLogin(HttpServletRequest request, HttpServletResponse response, User user) {

        String recentLoginIp = user.getRecentLoginIp();
        Date recentLoginDatetime = user.getRecentLoginDatetime();
        user.setLastLoginDate(recentLoginDatetime);
        user.setLastLoginIp(recentLoginIp);
        user.setRecentLoginDatetime(new Date());
        user.setRecentLoginIp(webUtil.getClientIp(request));
        user.setLastActivityTime(System.currentTimeMillis());
        userService.updateUser(user);
    }
----
