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

Authorization 헤더를 응답하도록 변경 #981

Merged
merged 19 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
c4a885a
refactor: AuthorizationHeader 를 함께 사용하도록 변경
zangsu Dec 18, 2024
1766805
test: 테스트 코드에서 재사용성을 위한 리팩토링
zangsu Dec 18, 2024
b61fdf2
test: 통합 테스트 관점으로 테스트 수정
zangsu Dec 18, 2024
a24bdcc
test: Auth 관련 통합 테스트 작성
zangsu Dec 19, 2024
6e8bd43
test: 잘못된 비밀번호 부분에 잘못된 값을 사용하도록 변경
zangsu Dec 19, 2024
8569298
refactor: 맞춤법에 맞도록 예외 메시지 수정
zangsu Dec 21, 2024
d77eee0
refactor: zeusStyle 로 포매팅
zangsu Dec 21, 2024
320e655
test: 성공 시나리오의 설명 추가
zangsu Dec 21, 2024
b968ca2
refactor: 중복 동작 제거
zangsu Dec 21, 2024
f27c763
test: 인수테스트 이름 변경
zangsu Dec 21, 2024
0a46b27
refactor: MockTest 에서 사용하는 ObjectMapper 를 @Autowired 로 변경
zangsu Dec 23, 2024
fae61a7
refactor: 통합테스트를 위한 부모 클래스의 이름을 더 명확하게 변경
zangsu Dec 23, 2024
a278e16
refactor: 테스트 포트를 RANDOM_PORT 로 변경
zangsu Dec 23, 2024
873c9d1
test: 브라우저 테스트 구현
zangsu Dec 27, 2024
9eb729a
test: 로그인 시나리오 수정
zangsu Dec 27, 2024
41f5e1a
test: 인수 테스트 전체 작성
zangsu Dec 27, 2024
794a2e9
test: 인수 테스트 삭제
zangsu Jan 3, 2025
38b5f72
refactor: 개행을 없애기 위해 스트림 내부 변수 명에 단축어 사용
zangsu Jan 5, 2025
c917034
refactor: 통일성을 위해 스트림 내부 변수에 단축어 사용
zangsu Jan 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
package codezap.auth.configuration;

import codezap.auth.dto.Credential;
import codezap.auth.manager.CredentialManager;
import codezap.auth.provider.CredentialProvider;
import codezap.member.domain.Member;
import jakarta.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Objects;
import lombok.RequiredArgsConstructor;

import jakarta.servlet.http.HttpServletRequest;

import org.springframework.core.MethodParameter;
import org.springframework.lang.NonNull;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import codezap.auth.dto.Credential;
import codezap.auth.manager.CredentialManager;
import codezap.auth.provider.CredentialProvider;
import codezap.global.exception.CodeZapException;
import codezap.global.exception.ErrorCode;
import codezap.member.domain.Member;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class AuthArgumentResolver implements HandlerMethodArgumentResolver {

private final CredentialManager credentialManager;
private final List<CredentialManager> credentialManagers;
kyum-q marked this conversation as resolved.
Show resolved Hide resolved
private final CredentialProvider credentialProvider;

@Override
Expand All @@ -35,10 +41,19 @@ public Member resolveArgument(
AuthenticationPrinciple parameterAnnotation = parameter.getParameterAnnotation(AuthenticationPrinciple.class);
boolean supported = Objects.nonNull(parameterAnnotation);
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
if (supported && !parameterAnnotation.required() && !credentialManager.hasCredential(request)) {
if (supported && !parameterAnnotation.required() && !hasCredential(request)) {
return null;
}
CredentialManager credentialManager = credentialManagers.stream()
.filter(eachCredentialManager -> eachCredentialManager.hasCredential(request))
.findFirst()
.orElseThrow(() -> new CodeZapException(ErrorCode.UNAUTHORIZED_USER, "인증 정보가 없습니다. 다시 로그인해 주세요."));
Credential credential = credentialManager.getCredential(request);
return credentialProvider.extractMember(credential);
}

private boolean hasCredential(HttpServletRequest request) {
return credentialManagers.stream()
.anyMatch(credentialManager -> credentialManager.hasCredential(request));
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
package codezap.auth.configuration;

import codezap.auth.provider.CredentialProvider;
import java.util.List;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import codezap.auth.manager.CredentialManager;
import codezap.auth.provider.CredentialProvider;
import lombok.RequiredArgsConstructor;

@Configuration
@RequiredArgsConstructor
public class AuthWebConfiguration implements WebMvcConfigurer {

private final CredentialManager credentialManager;
private final List<CredentialManager> credentialManagers;
private final CredentialProvider credentialProvider;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new AuthArgumentResolver(credentialManager, credentialProvider));
resolvers.add(new AuthArgumentResolver(credentialManagers, credentialProvider));
}
}
44 changes: 30 additions & 14 deletions backend/src/main/java/codezap/auth/controller/AuthController.java
Original file line number Diff line number Diff line change
@@ -1,27 +1,35 @@
package codezap.auth.controller;

import codezap.auth.dto.LoginMember;
import codezap.auth.dto.request.LoginRequest;
import codezap.auth.dto.response.LoginResponse;
import codezap.auth.dto.Credential;
import codezap.auth.manager.CredentialManager;
import codezap.auth.provider.CredentialProvider;
import codezap.auth.service.AuthService;
import java.util.List;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import codezap.auth.configuration.AuthenticationPrinciple;
import codezap.auth.dto.Credential;
import codezap.auth.dto.LoginMember;
import codezap.auth.dto.request.LoginRequest;
import codezap.auth.dto.response.LoginResponse;
import codezap.auth.manager.CredentialManager;
import codezap.auth.provider.CredentialProvider;
import codezap.auth.service.AuthService;
import codezap.global.exception.CodeZapException;
import codezap.global.exception.ErrorCode;
import codezap.member.domain.Member;
import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
public class AuthController implements SpringDocAuthController {

private final CredentialManager credentialManager;
private final List<CredentialManager> credentialManagers;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 리스트는 혹시 페어할 때 이야기 나누었던 것처럼 Adapter로 발전할 예정일까요?
Adapter 같은 일급 컬렉션으로 추출되면 적절한 CredentialManager를 반환하는지 테스트하기 더 좋을 것 같네요👍

Copy link
Contributor Author

@zangsu zangsu Dec 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아마 프론트 코드 변경 이후엔 다시 List<CredentialManager> -> CredentialManager 로 변경될 것 같아요. 그래서 지금 예상은 Adapter 로 변경되지 않을 것 같네용

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오홍... 뭔가 우리 클라이언트의 환경이 무려 4개(웹, 익스텐션, vs 플러그인, intellij 플러그인)이기도 하고 ....!
호환성을 생각한다면 Mapping 객체를 하나 두는 게 더 낫다고 생각하긴 하는데 짱수는 어떤가요...!

+) 다시 생각해보니 객체 이름은 Adapter보다는 Mapping이 더 의미가 맞는 것 같아요 정정할게요!
(스프링 MVC 미션할 때 구현했던 HandlerMapping에서 착안)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하,,, 그렇군요!!
그렇다면 지금처럼 여러개의 CredentialManager가 공존해야 하는 상황이 지속될 수 있겠네용~~
Mapping 객체 하나 더 두는 방향으로 수정해 보겠습니당~~

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 작업을 #1010 이슈로 인가합니다!

private final CredentialProvider credentialProvider;
private final AuthService authService;

Expand All @@ -32,20 +40,28 @@ public ResponseEntity<LoginResponse> login(
) {
LoginMember loginMember = authService.login(loginRequest);
Credential credential = credentialProvider.createCredential(loginMember);
credentialManager.setCredential(httpServletResponse, credential);
credentialManagers.forEach(
credentialManager -> credentialManager.setCredential(httpServletResponse, credential)
);
zeus6768 marked this conversation as resolved.
Show resolved Hide resolved
return ResponseEntity.ok(LoginResponse.from(loginMember));
}

@GetMapping("/login/check")
public ResponseEntity<Void> checkLogin(HttpServletRequest httpServletRequest) {
Credential credential = credentialManager.getCredential(httpServletRequest);
credentialProvider.extractMember(credential);
public ResponseEntity<Void> checkLogin(
@AuthenticationPrinciple Member member,
HttpServletRequest httpServletRequest
) {
if (member == null) {
throw new CodeZapException(ErrorCode.UNAUTHORIZED_USER, "인증 정보가 없습니다. 다시 로그인해 주세요.");
}
Comment on lines +52 to +54
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

엇 이거 서비스 계층에서 처리하지 않는 이유가 있나요???
제가 이전 리뷰 놓쳤다면 말해주세요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Member 객체의 nullable 여부는 controller 메서드의 파라미터에 존재하는 @AuthenticationPrinciple의 속성에 따라 결정됩니다.
그래서 nullable 여부를 바로 볼 수 있는 controller 에서 Member 객체의 null 관련 처리 (분기 처리, 예외 처리)를 하게 되었어요~

return ResponseEntity.ok().build();
}

@PostMapping("/logout")
public ResponseEntity<Void> logout(HttpServletResponse httpServletResponse) {
credentialManager.removeCredential(httpServletResponse);
credentialManagers.forEach(
credentialManager -> credentialManager.removeCredential(httpServletResponse)
);
zeus6768 marked this conversation as resolved.
Show resolved Hide resolved
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import codezap.auth.dto.response.LoginResponse;
import codezap.global.swagger.error.ApiErrorResponse;
import codezap.global.swagger.error.ErrorCase;
import codezap.member.domain.Member;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.headers.Header;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
Expand Down Expand Up @@ -41,7 +42,7 @@ public interface SpringDocAuthController {
@ErrorCase(description = "쿠키 없음", exampleMessage = "쿠키가 없어서 회원 정보를 찾을 수 없습니다. 다시 로그인해주세요."),
@ErrorCase(description = "인증 쿠키 없음", exampleMessage = "인증에 대한 쿠키가 없어서 회원 정보를 찾을 수 없습니다. 다시 로그인해주세요."),
})
ResponseEntity<Void> checkLogin(HttpServletRequest request);
ResponseEntity<Void> checkLogin(Member member, HttpServletRequest request);

@Operation(summary = "로그아웃")
@ApiResponse(responseCode = "204", description = "인증 성공")
Expand Down
4 changes: 2 additions & 2 deletions backend/src/main/java/codezap/auth/dto/LoginMember.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

import codezap.member.domain.Member;

public record LoginMember (
public record LoginMember(
long id,
String name,
String password,
String salt
){
) {
public static LoginMember from(Member member) {
return new LoginMember(member.getId(), member.getName(), member.getPassword(), member.getSalt());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package codezap.auth.manager;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;

import codezap.auth.dto.Credential;
import codezap.global.exception.CodeZapException;
import codezap.global.exception.ErrorCode;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;

@Component
@RequiredArgsConstructor
public class AuthorizationHeaderCredentialManager implements CredentialManager {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
package codezap.auth.manager;

import codezap.auth.dto.Credential;
import java.util.Arrays;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import lombok.RequiredArgsConstructor;
import org.springframework.boot.web.server.Cookie.SameSite;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Component;

import codezap.auth.dto.Credential;
import codezap.global.exception.CodeZapException;
import codezap.global.exception.ErrorCode;
import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package codezap.auth.manager;

import codezap.auth.dto.Credential;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import codezap.auth.dto.Credential;

/**
* Credential 정보를 Http 응답에 설정하기 위한 클래스입니다.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package codezap.auth.provider;

import codezap.auth.dto.LoginMember;
import codezap.auth.dto.Credential;
import codezap.auth.dto.LoginMember;
import codezap.member.domain.Member;

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package codezap.auth.provider.basic;

import codezap.auth.dto.LoginMember;
import codezap.auth.dto.Credential;
import java.nio.charset.StandardCharsets;

import jakarta.servlet.http.HttpServletRequest;

import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;

import codezap.auth.dto.Credential;
import codezap.auth.dto.LoginMember;
import codezap.auth.provider.CredentialProvider;
import codezap.global.exception.CodeZapException;
import codezap.global.exception.ErrorCode;
Expand All @@ -24,7 +24,8 @@ public class BasicAuthCredentialProvider implements CredentialProvider {

@Override
public Credential createCredential(LoginMember loginMember) {
String credentialValue = HttpHeaders.encodeBasicAuth(loginMember.name(), loginMember.password(), StandardCharsets.UTF_8);
String credentialValue = HttpHeaders.encodeBasicAuth(loginMember.name(), loginMember.password(),
StandardCharsets.UTF_8);
return Credential.basic(credentialValue);
}

Expand Down
5 changes: 3 additions & 2 deletions backend/src/main/java/codezap/auth/service/AuthService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package codezap.auth.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import codezap.auth.dto.LoginMember;
import codezap.auth.dto.request.LoginRequest;
import codezap.auth.encryption.PasswordEncryptor;
Expand All @@ -8,8 +11,6 @@
import codezap.member.domain.Member;
import codezap.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

import codezap.auth.dto.LoginMember;
import codezap.auth.dto.Credential;
import codezap.auth.manager.AuthorizationHeaderCredentialManager;
import java.lang.reflect.Method;

import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
Expand All @@ -28,8 +30,10 @@

class AuthArgumentResolverTest {
private final CredentialProvider credentialProvider = new PlainCredentialProvider();
private final CredentialManager credentialManager = new CookieCredentialManager();
private final AuthArgumentResolver authArgumentResolver = new AuthArgumentResolver(credentialManager, credentialProvider);
private final List<CredentialManager> credentialManagers =
List.of(new CookieCredentialManager(), new AuthorizationHeaderCredentialManager());

private final AuthArgumentResolver authArgumentResolver = new AuthArgumentResolver(credentialManagers, credentialProvider);

@Nested
@DisplayName("지원하는 파라미터 테스트")
Expand Down Expand Up @@ -131,7 +135,7 @@ void noCredentialTest() {
//when & then
assertThatThrownBy(() -> resolveArgument(requiredMethod, nativeWebRequest))
.isInstanceOf(CodeZapException.class)
.hasMessage("쿠키가 없어서 회원 정보를 찾을 수 없습니다. 다시 로그인해주세요.");
.hasMessage("인증 정보가 없습니다. 다시 로그인해 주세요.");
}

@Test
Expand Down Expand Up @@ -159,7 +163,8 @@ private Member resolveArgument(Method method, NativeWebRequest webRequest) {
private void setCredentialCookie(MockHttpServletRequest request, Member member) {
MockHttpServletResponse mockResponse = new MockHttpServletResponse();
Credential credential = credentialProvider.createCredential(LoginMember.from(member));
credentialManager.setCredential(mockResponse, credential);
credentialManagers.forEach(
credentialManager -> credentialManager.setCredential(mockResponse, credential));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이것도 cm, 풀네임 둘 중 하나로 통일해주세오~

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

헉 놓쳤다!!

request.setCookies(mockResponse.getCookies());
}
}
Expand Down
Loading