Skip to content

Commit

Permalink
Authorization 헤더를 응답하도록 변경 (#981)
Browse files Browse the repository at this point in the history
  • Loading branch information
zangsu authored Jan 6, 2025
1 parent 5e30751 commit 6e875d2
Show file tree
Hide file tree
Showing 12 changed files with 83 additions and 44 deletions.
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;
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(cm -> cm.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(cm -> cm.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));
}
}
40 changes: 26 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;
private final CredentialProvider credentialProvider;
private final AuthService authService;

Expand All @@ -32,20 +40,24 @@ public ResponseEntity<LoginResponse> login(
) {
LoginMember loginMember = authService.login(request);
Credential credential = credentialProvider.createCredential(loginMember);
credentialManager.setCredential(httpServletResponse, credential);
credentialManagers.forEach(cm -> cm.setCredential(httpServletResponse, credential));
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, "인증 정보가 없습니다. 다시 로그인해 주세요.");
}
return ResponseEntity.ok().build();
}

@PostMapping("/logout")
public ResponseEntity<Void> logout(HttpServletResponse httpServletResponse) {
credentialManager.removeCredential(httpServletResponse);
credentialManagers.forEach(cm -> cm.removeCredential(httpServletResponse));
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,7 @@ 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(cm -> cm.setCredential(mockResponse, credential));
request.setCookies(mockResponse.getCookies());
}
}
Expand Down

0 comments on commit 6e875d2

Please sign in to comment.