Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[REFACTOR] 로그인 정보를 쿠키 -> HTTP Authorization 헤더 로 변경한다. #908

Open
zangsu opened this issue Nov 13, 2024 · 5 comments
Assignees
Labels
refactor 요구사항이 바뀌지 않은 변경사항

Comments

@zangsu
Copy link
Contributor

zangsu commented Nov 13, 2024

📌 어떤 기능을 리팩터링 하나요?

쿠키의 동작은 브라우저 정책에 의존한다.
항상 동일하게 동작할 수 있도록 HTTP Authorization 헤더로 변경한다.

@zangsu zangsu added the refactor 요구사항이 바뀌지 않은 변경사항 label Nov 13, 2024
@zangsu zangsu added this to the 7차 스프린트 💭 milestone Nov 13, 2024
@zangsu zangsu self-assigned this Nov 13, 2024
@zangsu
Copy link
Contributor Author

zangsu commented Nov 14, 2024

진행 방식

기존의 Cookie 방식을 유지한 채로 Authorization 헤더를 확인하는 로직을 추가한다.
로그인을 유지할 때는 Cookie와 Authorization 헤더를 모두 내려준다.

프론트엔드, 익스텐션 코드를 Authorization 방식으로 변경한다.

백엔드 코드에서 Cookie 방식을 제거한다.

@zangsu
Copy link
Contributor Author

zangsu commented Dec 5, 2024

Cookie와 Authorization에 같은 credential 값을 사용할 수 없는 문제

위 때문에 똑같은 <type> <credential>이라는 값을 서로 다른 두 곳에 사용할 수 없음

+) Base64 인코딩이 모든 상황에서 필요한 것이 아닙니다.

  • 쿠키: 인코딩 필요 ⭕
  • Authorization/Basic: 인코딩 필요 ⭕
  • Authorization/Baerer: 인코딩 필요 ❌
  • ...

하지만, 현재 credential 값을 설정하는 CredentialManager와 credential을 생성하는 CredentialProvider가 별도로 동작해서 쿠키를 사용할 때 BasicAuthCredentialProvider를 사용하고, Authorization 헤더를 사용할 땐 BearerAuthCredentialProvider를 사용할 수 가 없습니다.

이에 CredentialManagerCredentialProvider 의존성을 가지도록 변경하였습니다.

@zangsu
Copy link
Contributor Author

zangsu commented Dec 18, 2024

고민 사항

인증 값의 상수화

Cookie / 헤더의 값을 수동으로 다음 요청에 넣어주기 위해선 쿠키 / 헤더의 이름이 필요하다.
현재 동일한 이 값을 중복해서 사용하고 있는데, 이 부분을 상수화 하여 여러군데에서 사용하는 것이 좋아보인다.

다만, 이 값이 캡슐화 해야 하는 값인지에 의견이 분분한 상황이다.

@zeus6768
Copy link
Contributor

zeus6768 commented Dec 19, 2024

1. 전략 패턴 기반 설계

a. 공통 인터페이스 정의

  • 모든 인증 방식을 처리할 서비스가 구현해야 할 인터페이스를 정의합니다.
public interface LoginService {
    boolean supports(String authorizationType); // 특정 인증 타입 지원 여부
    String login(String authorizationHeader);  // 로그인 처리 로직
}

b. 각 인증 방식에 맞는 구현체 작성

Basic 인증 서비스:

import org.springframework.stereotype.Service;
import java.util.Base64;

@Service
public class BasicLoginService implements LoginService {

    @Override
    public boolean supports(String authorizationType) {
        return authorizationType.equalsIgnoreCase("Basic");
    }

    @Override
    public String login(String authorizationHeader) {
        String base64Credentials = authorizationHeader.substring("Basic ".length());
        String credentials = new String(Base64.getDecoder().decode(base64Credentials));
        String[] values = credentials.split(":", 2);
        String username = values[0];
        String password = values[1];

        if ("user".equals(username) && "password".equals(password)) {
            return "Basic authentication successful for user: " + username;
        }
        return "Invalid Basic authentication credentials";
    }
}

Bearer 인증 서비스:

import org.springframework.stereotype.Service;

@Service
public class BearerLoginService implements LoginService {

    @Override
    public boolean supports(String authorizationType) {
        return authorizationType.equalsIgnoreCase("Bearer");
    }

    @Override
    public String login(String authorizationHeader) {
        String token = authorizationHeader.substring("Bearer ".length());
        if ("valid-token".equals(token)) {
            return "Bearer authentication successful for token: " + token;
        }
        return "Invalid Bearer token";
    }
}

c. 서비스 레지스트리 작성

  • 각 인증 서비스를 관리하는 레지스트리를 작성합니다.
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class LoginServiceRegistry {

    private final List<LoginService> loginServices;

    public LoginServiceRegistry(List<LoginService> loginServices) {
        this.loginServices = loginServices;
    }

    public LoginService getService(String authorizationType) {
        return loginServices.stream()
                .filter(service -> service.supports(authorizationType))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("Unsupported Authorization type: " + authorizationType));
    }
}

d. 컨트롤러 리팩토링

  • 레지스트리를 통해 적합한 서비스 구현체를 가져와 호출합니다.
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/auth")
public class LoginController {

    private final LoginServiceRegistry loginServiceRegistry;

    public LoginController(LoginServiceRegistry loginServiceRegistry) {
        this.loginServiceRegistry = loginServiceRegistry;
    }

    @PostMapping("/login")
    public String login(@RequestHeader("Authorization") String authorizationHeader) {
        String authorizationType = authorizationHeader.split(" ")[0];
        LoginService loginService = loginServiceRegistry.getService(authorizationType);
        return loginService.login(authorizationHeader);
    }
}

2. 확장 시의 이점

  • 새로운 인증 방식이 추가되면 새로운 LoginService 구현체만 작성하면 됩니다.
  • 컨트롤러의 로직(필드 및 조건문)은 변경되지 않으므로 **OCP(Open-Closed Principle)**를 만족합니다.
  • 서비스 레지스트리는 DI를 통해 자동으로 관리되므로 인증 방식이 늘어나도 코드가 깔끔하게 유지됩니다.

3. 추가 인증 방식 예제

Custom 인증 서비스

import org.springframework.stereotype.Service;

@Service
public class CustomLoginService implements LoginService {

    @Override
    public boolean supports(String authorizationType) {
        return authorizationType.equalsIgnoreCase("Custom");
    }

    @Override
    public String login(String authorizationHeader) {
        String customToken = authorizationHeader.substring("Custom ".length());
        if ("valid-custom-token".equals(customToken)) {
            return "Custom authentication successful for token: " + customToken;
        }
        return "Invalid Custom token";
    }
}

이 새로운 인증 서비스를 추가해도 컨트롤러나 레지스트리를 변경할 필요가 없습니다.


4. 결과

요청 예시

  • Basic 인증:

    curl -X POST http://localhost:8080/api/auth/login \
    -H "Authorization: Basic dXNlcjpwYXNzd29yZA=="  # user:password (Base64 인코딩)
  • Bearer 인증:

    curl -X POST http://localhost:8080/api/auth/login \
    -H "Authorization: Bearer valid-token"
  • Custom 인증:

    curl -X POST http://localhost:8080/api/auth/login \
    -H "Authorization: Custom valid-custom-token"

5. 요약

  1. 전략 패턴을 활용하여 각 인증 방식을 독립적으로 구현.
  2. 서비스 레지스트리로 인증 서비스를 관리하고 컨트롤러의 복잡성을 제거.
  3. 새로운 인증 방식 추가 시, 기존 코드 수정 없이 구현체만 추가하여 확장 가능.

이 설계는 유지보수성, 확장성, 코드 가독성 측면에서 훌륭한 접근 방식입니다.

@zangsu
Copy link
Contributor Author

zangsu commented Dec 19, 2024

제우스의 제안에 대한 변경 여부는 인증 타입에 대한 확장성을 고려해야 하는 OAuth 도입 작업에서 고민하기로 결정 및 전달 하였습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
refactor 요구사항이 바뀌지 않은 변경사항
Projects
Status: Todo
Development

No branches or pull requests

3 participants