ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -1,13 +1,16 @@
 package nextstep.app;
 
+import jakarta.servlet.http.HttpServletRequest;
 import nextstep.app.domain.Member;
 import nextstep.app.domain.MemberRepository;
 import nextstep.security.authentication.AuthenticationException;
 import nextstep.security.authentication.BasicAuthenticationFilter;
 import nextstep.security.authentication.UsernamePasswordAuthenticationFilter;
+import nextstep.security.authorization.AuthorizationFilter;
 import nextstep.security.authorization.CheckAuthenticationFilter;
 import nextstep.security.authorization.SecuredAspect;
 import nextstep.security.authorization.SecuredMethodInterceptor;
+import nextstep.security.authorization.manager.*;
 import nextstep.security.config.DefaultSecurityFilterChain;
 import nextstep.security.config.DelegatingFilterProxy;
 import nextstep.security.config.FilterChainProxy;
@@ -22,6 +25,10 @@
 import java.util.List;
 import java.util.Set;
 
+import static nextstep.security.authorization.matcher.RequestMatcherEntry.createDefaultMatcher;
+import static nextstep.security.authorization.matcher.RequestMatcherEntry.createMvcMatcher;
+import static org.springframework.http.HttpMethod.GET;
+
 @EnableAspectJAutoProxy
 @Configuration
 public class SecurityConfig {
@@ -46,19 +53,20 @@ public FilterChainProxy filterChainProxy(List<SecurityFilterChain> securityFilte
     public SecuredMethodInterceptor securedMethodInterceptor() {
         return new SecuredMethodInterceptor();
     }
-//    @Bean
-//    public SecuredAspect securedAspect() {
-//        return new SecuredAspect();
-//    }
 
     @Bean
     public SecurityFilterChain securityFilterChain() {
+        final AuthorizationManager<HttpServletRequest> authorizationManager = new RequestAuthorizationManager(List.of(
+                createMvcMatcher(GET, "/members", new AuthorityAuthorizationManager<>("ADMIN")),
+                createMvcMatcher(GET, "/members/me", new AuthenticatedAuthorizationManager<>()),
+                createMvcMatcher(GET, "/search", new PermitAllAuthorizationManager<>())
+        ), createDefaultMatcher(new DenyAllAuthorizationManager<>()));
         return new DefaultSecurityFilterChain(
                 List.of(
                         new SecurityContextHolderFilter(),
                         new UsernamePasswordAuthenticationFilter(userDetailsService()),
                         new BasicAuthenticationFilter(userDetailsService()),
-                        new CheckAuthenticationFilter()
+                        new AuthorizationFilter(authorizationManager)
                 )
         );
     }

리뷰: 잘 추가해주셨네요 👍
@@ -0,0 +1,5 @@
+package nextstep.security.authorization.manager;
+
+public interface AuthorizationResult {
+    boolean isGranted();
+}

리뷰: 미션내의 요구사항은 `AuthorizationDecision`이었으나 최근 버전에서의 추상화를 잘 구현해주셨네요
@@ -0,0 +1,20 @@
+package nextstep.security.authorization.manager;
+
+public enum AuthorizationDecision implements AuthorizationResult {
+    GRANTED(true),
+    NOT_GRANTED(false);
+
+    private final boolean granted;
+
+    AuthorizationDecision(final boolean granted) {
+        this.granted = granted;
+    }
+
+    public boolean isGranted() {
+        return this.granted;
+    }
+
+    public static AuthorizationResult from(boolean granted) {
+        return granted ? GRANTED : NOT_GRANTED;
+    }
+}

리뷰: true와 false로 구성되어있어 enum으로 생각해볼 수도 있으나 실제 구현체에서는 `AuthorizationDecision`을 상속받아 구현하는 새로운 구현체들도 있어 enum으로 사용되지는 않습니다.

일반적으로 enum을 구성하는 것은 몇가지 고정값이 있기 때문에 enum으로 표현하지는 않고 정말 static한 정보들로 표기될때에만 사용되어요. 예를들면 `HttpStatus`는 400, 401과 같은 코드가 있기 때문에 각각을 표기하는 것처럼요.

그래서 사실은 `AuthorizationDecision`은 granted가 있다 없다로 생성되는 객체일뿐 enum에 적합하진 않습니다.
@@ -1 +1,17 @@
 # spring-security-authorization
+
+# 🚀 1단계 - AuthorizationManager를 활용
+
+- [x] AuthorizationManager를 활용하여 인가 과정 추상화
+  - [x] SecuredAuthorizationManager 구현
+  - [x] RequestAuthorizationManager 구현
+  - [x] SecuredAuthorizationManager 구현
+
+# 🚀 2단계 - 요청별 권한 검증 정보 분리
+
+- [x] 요청별 권한 검증 정보를 별도의 객체로 분리하여 관리
+  - [x] RequestMatcher 작성
+  - [x] AnyRequestMatcher 구현
+  - [x] MvcRequestMatcher 구현
+  - [x] RequestMatcherEntry 작성
+

리뷰: ```suggestion
  - [x] SecuredAuthorizationManager 구현
  - [x] RequestAuthorizationManager 구현
```
중복되어있네요 :)
@@ -0,0 +1,25 @@
+package nextstep.security.authorization.manager;
+
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.Secured;
+import org.aopalliance.intercept.MethodInvocation;
+
+import java.lang.reflect.Method;
+
+public class SecuredAuthorizationManager implements AuthorizationManager<MethodInvocation> {
+
+    @Override
+    public AuthorizationResult authorize(Authentication authentication, MethodInvocation target) {
+        return AuthorizationDecision.from(hasAuthority(authentication, target.getMethod()));
+    }
+
+    private boolean hasAuthority(Authentication authentication, Method method) {
+        if (!method.isAnnotationPresent(Secured.class)) {
+            return false;
+        }
+        return authentication != null
+                && authentication.isAuthenticated()
+                && authentication.getAuthorities()
+                .contains(method.getAnnotation(Secured.class).value());
+    }
+}

리뷰: `SecuredAuthorizationManager`의 구현 내용과 `AuthorityAuthorizationManager`의 구현 내용을 생각해보면 서로 비슷한 동작이 반복되는 것을 볼 수 있는데요.

`AuthorizationManager`들의 구현체들은 각각 따로 구성되는 것이 아닌 서로를 사용할 수 있도록 연결할 수 있어요. 그래서 `AuthorityAuthorizationManager`에서 authority가 있는지를 체크하는 로직을 현재와 같이 전부 몰아넣고, `SecuredAuthorizationManager`는 `@Secured`에 있는 값을 꺼내 `AuthorityAuthorizationManager`에게 인가여부를 묻는 형태로 구성이 될 수 있습니다.

https://github.com/spring-projects/spring-security/blob/main/core/src/main/java/org/springframework/security/authorization/method/SecuredAuthorizationManager.java
@@ -50,11 +50,11 @@ public List<ChatRoomDetailResponseDTO> getChatRoomList(Member member) {
             .filter(chatRoom -> {
                 Chat chat = getLatestChatByChatRoom(chatRoom);
                 chatRoomLastChatMap.put(chatRoom, chat);
-                if (chat == null) {
+                if (Objects.isNull(chat)) {
                     return false;
                 }
                 LocalDateTime lastDeleteAt = getLastDeleteAtByMember(chatRoom, member);
-                return lastDeleteAt == null || chat.getCreatedAt().isAfter(lastDeleteAt);
+                return Objects.isNull(lastDeleteAt) || chat.getCreatedAt().isAfter(lastDeleteAt);
             })
             .sorted((chatRoomA, chatRoomB) -> {
                 Chat chatA = chatRoomLastChatMap.get(chatRoomA);
@@ -97,12 +97,12 @@ public CountChatRoomsWithNewChatDTO countChatRoomsWithNewChat(Member member) {
             .filter(chatRoom -> {
                 Chat chat = getLatestChatByChatRoom(chatRoom);
 
-                if (chat == null) {
+                if (Objects.isNull(chat)) {
                     return false;
                 }
 
                 LocalDateTime lastDeleteAt = getLastDeleteAtByMember(chatRoom, member);
-                return lastDeleteAt == null || chat.getCreatedAt().isAfter(lastDeleteAt);
+                return Objects.isNull(lastDeleteAt) || chat.getCreatedAt().isAfter(lastDeleteAt);
             }).toList();
 
         long chatRoomsWithNewChatCount = chatRoomList.stream()
@@ -168,20 +168,10 @@ private ChatRoomDetailResponseDTO toChatRoomDetailResponseDTO(ChatRoom chatRoom,
 
         // 상대가 탈퇴한 경우
         if (Objects.isNull(chatRoom.getMemberA()) || Objects.isNull(chatRoom.getMemberB())) {
-            return ChatRoomConverter.toChatRoomDetailResponseDTO(UNKNOWN_SENDER_NICKNAME, chat.getContent(),
-                chatRoom.getId(), null, null, false);
+            return ChatRoomConverter.toChatRoomDetailResponseDTO(UNKNOWN_SENDER_NICKNAME,
+                chat.getContent(), chatRoom.getId());
         }
 
-        return ChatRoomConverter.toChatRoomDetailResponseDTO(
-            member.getNickname().equals(chatRoom.getMemberA().getNickname()) ?
-                chatRoom.getMemberB().getNickname() : chatRoom.getMemberA().getNickname(),
-            chat.getContent(),
-            chatRoom.getId(),
-            member.getNickname().equals(chatRoom.getMemberA().getNickname()) ?
-                chatRoom.getMemberB().getPersona() : chatRoom.getMemberA().getPersona(),
-            member.getNickname().equals(chatRoom.getMemberA().getNickname()) ?
-                chatRoom.getMemberB().getId() : chatRoom.getMemberA().getId(),
-            hasNewChat
-        );
+        return ChatRoomConverter.toChatRoomDetailResponseDTO(member, chat, chatRoom, hasNewChat);
     }
 }
\ No newline at end of file

리뷰: Objects.isNull() 사용하도록 수정했습니다
@@ -55,27 +55,27 @@ private void checkMemberMisMatch(Member member, ChatRoom chatRoom) {
         // ChatRoom의 MemberA가 null(탈퇴 회원)이고, 현재 요청 Member가 ChatRoom의 MemberB와 다른 경우
         if (Objects.isNull(chatRoom.getMemberA())
             && !member.getId().equals(chatRoom.getMemberB().getId())) {
-            throw new GeneralException(ErrorStatus._CHATROOM_MEMBER_MISMATCH);
+            throw new GeneralException(ErrorStatus._CHATROOM_MEMBERB_REQUIRED_WHEN_MEMBERA_NULL);
         }
 
         // ChatRoom의 MemberB가 null(탈퇴 회원)이고, 현재 요청 Member가 ChatRoom의 MemberA와 다른 경우
         if (Objects.isNull(chatRoom.getMemberB())
             && !member.getId().equals(chatRoom.getMemberA().getId())) {
-            throw new GeneralException(ErrorStatus._CHATROOM_MEMBER_MISMATCH);
+            throw new GeneralException(ErrorStatus._CHATROOM_MEMBERA_REQUIRED_WHEN_MEMBERB_NULL);
         }
 
         // ChatRoom의 두 Member가 모두 null(탈퇴 회원)이 아닌 경우, 현재 요청 Member가 MemberA, MemberB 둘다 아닌 경우
         if (!member.getId().equals(chatRoom.getMemberA().getId())
             && !member.getId().equals(chatRoom.getMemberB().getId())) {
-            throw new GeneralException(ErrorStatus._CHATROOM_MEMBER_MISMATCH);
+            throw new GeneralException(ErrorStatus._CHATROOM_INVALID_MEMBER);
         }
     }
 
     private List<Chat> getFilteredChatList(ChatRoom chatRoom, Member member) {
         List<Chat> findChatList = chatRepository.findAllByChatRoom(chatRoom);
         LocalDateTime memberLastDeleteAt = getMemberLastDeleteAt(chatRoom, member);
         return findChatList.stream()
-            .filter(chat -> memberLastDeleteAt == null || chat.getCreatedAt()
+            .filter(chat -> Objects.isNull(memberLastDeleteAt) || chat.getCreatedAt()
                 .isAfter(memberLastDeleteAt))
             .toList();
     }

리뷰: 애러 메시지를 나누었습니다
@@ -50,11 +50,11 @@ public List<ChatRoomDetailResponseDTO> getChatRoomList(Member member) {
             .filter(chatRoom -> {
                 Chat chat = getLatestChatByChatRoom(chatRoom);
                 chatRoomLastChatMap.put(chatRoom, chat);
-                if (chat == null) {
+                if (Objects.isNull(chat)) {
                     return false;
                 }
                 LocalDateTime lastDeleteAt = getLastDeleteAtByMember(chatRoom, member);
-                return lastDeleteAt == null || chat.getCreatedAt().isAfter(lastDeleteAt);
+                return Objects.isNull(lastDeleteAt) || chat.getCreatedAt().isAfter(lastDeleteAt);
             })
             .sorted((chatRoomA, chatRoomB) -> {
                 Chat chatA = chatRoomLastChatMap.get(chatRoomA);
@@ -97,12 +97,12 @@ public CountChatRoomsWithNewChatDTO countChatRoomsWithNewChat(Member member) {
             .filter(chatRoom -> {
                 Chat chat = getLatestChatByChatRoom(chatRoom);
 
-                if (chat == null) {
+                if (Objects.isNull(chat)) {
                     return false;
                 }
 
                 LocalDateTime lastDeleteAt = getLastDeleteAtByMember(chatRoom, member);
-                return lastDeleteAt == null || chat.getCreatedAt().isAfter(lastDeleteAt);
+                return Objects.isNull(lastDeleteAt) || chat.getCreatedAt().isAfter(lastDeleteAt);
             }).toList();
 
         long chatRoomsWithNewChatCount = chatRoomList.stream()
@@ -168,20 +168,10 @@ private ChatRoomDetailResponseDTO toChatRoomDetailResponseDTO(ChatRoom chatRoom,
 
         // 상대가 탈퇴한 경우
         if (Objects.isNull(chatRoom.getMemberA()) || Objects.isNull(chatRoom.getMemberB())) {
-            return ChatRoomConverter.toChatRoomDetailResponseDTO(UNKNOWN_SENDER_NICKNAME, chat.getContent(),
-                chatRoom.getId(), null, null, false);
+            return ChatRoomConverter.toChatRoomDetailResponseDTO(UNKNOWN_SENDER_NICKNAME,
+                chat.getContent(), chatRoom.getId());
         }
 
-        return ChatRoomConverter.toChatRoomDetailResponseDTO(
-            member.getNickname().equals(chatRoom.getMemberA().getNickname()) ?
-                chatRoom.getMemberB().getNickname() : chatRoom.getMemberA().getNickname(),
-            chat.getContent(),
-            chatRoom.getId(),
-            member.getNickname().equals(chatRoom.getMemberA().getNickname()) ?
-                chatRoom.getMemberB().getPersona() : chatRoom.getMemberA().getPersona(),
-            member.getNickname().equals(chatRoom.getMemberA().getNickname()) ?
-                chatRoom.getMemberB().getId() : chatRoom.getMemberA().getId(),
-            hasNewChat
-        );
+        return ChatRoomConverter.toChatRoomDetailResponseDTO(member, chat, chatRoom, hasNewChat);
     }
 }
\ No newline at end of file

리뷰: 상대 사용자가 탈퇴한 경우 컨버터로 넘기는 파라미터를 줄였고, 
상대 사용자가 존재하는 경우 아래 삼항 연산자 로직을 컨버터로 옮기면서 컨버터에서 삼항연산자를 한 번만 사용하도록 수정했습니다.
@@ -0,0 +1,27 @@
+oauth2:
+  client:
+    registration:
+      github:
+        client-id: "{client-id}"
+        client-secret: "{client-secret}"
+        response-type: "code"
+        scope: "user:email"
+        redirect-uri: "http://localhost:8080/login/oauth2/code/github"
+      google:
+        client-id: "{client-id}"
+        client-secret: "{client-secret}"
+        response-type: "code"
+        scope: "email"
+        redirect-uri: "http://localhost:8080/login/oauth2/code/google"
+    provider:
+      github:
+        token-uri: "https://github.com/login/oauth/access_token"
+        user-info-uri: "https://api.github.com/user"
+        authorization-uri: "https://github.com/login/oauth/authorize"
+      google:
+        token-uri: "https://oauth2.googleapis.com/token"
+        user-info-uri: "https://www.googleapis.com/oauth2/v3/userinfo"
+        authorization-uri: "https://accounts.google.com/o/oauth2/v2/auth"
+
+
+

리뷰: `client-secret`는 노출되지 말아야하는 정보가 맞지만 `client-id`는 굳이 막을 필요가 없지 않을까요? client-id는 서버 식별값이라 봐주시면 될 것 같아요.
@@ -0,0 +1,31 @@
+package nextstep.app;
+
+import nextstep.security.oauth2.OAuth2ClientProviderProperties;
+import nextstep.security.oauth2.OAuth2ClientRegistrationProperties;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.Map;
+import java.util.Set;
+
+@ConfigurationProperties(prefix = "oauth2.client")
+public class OAuth2ClientProperties {
+    private final Map<String, OAuth2ClientRegistrationProperties> registration;
+    private final Map<String, OAuth2ClientProviderProperties> provider;
+
+    public OAuth2ClientProperties(Map<String, OAuth2ClientRegistrationProperties> registration, Map<String, OAuth2ClientProviderProperties> provider) {
+        this.registration = registration;
+        this.provider = provider;
+    }
+
+    public OAuth2ClientRegistrationProperties getOauth2Registration(String providerName) {
+        return registration.get(providerName);
+    }
+
+    public OAuth2ClientProviderProperties getOauth2Provider(String providerName) {
+        return provider.get(providerName);
+    }
+
+    public Set<String> getRegistrations() {
+        return registration.keySet();
+    }
+}

리뷰: `Properties`를 로직들에서 가져다 쓰고 있는데요. 사실 해당 객체는 속성이기 때문에 바로 Map의 형태로 꺼내서 사용하지는 않아요.

말그대로 속성이기 때문에 무언가를 저장하고 찾는 Repository의 기능은 아닌거죠. 목적은 단순히 oauth2의 정보를 yml에서 가져온다가 끝이라고 보시면 되어요.
@@ -0,0 +1,49 @@
+package nextstep.security.oauth2;
+
+import nextstep.security.authentication.Authentication;
+
+import java.util.Set;
+
+public class OAuth2AuthenticationToken implements Authentication {
+
+    private final Object principal;
+    private final Object accessToken;
+    private final boolean authenticated;
+    private final Set<String> authorities;
+
+    private OAuth2AuthenticationToken(Object principal, Object accessToken, boolean authenticated, Set<String> authorities) {
+        this.principal = principal;
+        this.accessToken = accessToken;
+        this.authenticated = authenticated;
+        this.authorities = authorities;
+    }
+
+    public static OAuth2AuthenticationToken unauthenticated(String principal, OAuth2AccessToken credentials) {
+        return new OAuth2AuthenticationToken(principal, credentials, false, Set.of());
+    }
+
+
+    public static OAuth2AuthenticationToken authenticated(String principal, String credentials, Set<String> authorities) {
+        return new OAuth2AuthenticationToken(principal, credentials, true, authorities);
+    }
+
+    @Override
+    public Set<String> getAuthorities() {
+        return authorities;
+    }
+
+    @Override
+    public Object getCredentials() {
+        return accessToken;
+    }
+
+    @Override
+    public Object getPrincipal() {
+        return principal;
+    }
+
+    @Override
+    public boolean isAuthenticated() {
+        return authenticated;
+    }
+}

리뷰: OAuth2도 사실 인가의 한 과정이기 때문에 말씀해주신대로 비슷한 형태가 더 들어갈 수 밖에 없기는 합니다 ㅎㅎ
@@ -0,0 +1,49 @@
+package nextstep.security.oauth2;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import nextstep.security.access.PathPatternRequestMatcher;
+import nextstep.security.oauth2.registration.ClientRegistration;
+import nextstep.security.oauth2.registration.ClientRegistrationRepository;
+import org.springframework.http.HttpMethod;
+import org.springframework.web.filter.GenericFilterBean;
+
+import java.io.IOException;
+
+public class OAuth2LoginRedirectFilter extends GenericFilterBean {
+    private static final String REGISTRATION = "registration-id";
+    private static final String OAUTH_REQUEST_URI_PATTERN = "/oauth2/authorization/{" + REGISTRATION + "}";
+
+    private final ClientRegistrationRepository clientRegistrationRepository;
+    private final PathPatternRequestMatcher pathPatternRequestMatcher;
+
+    public OAuth2LoginRedirectFilter(ClientRegistrationRepository clientRegistrationRepository) {
+        this.clientRegistrationRepository = clientRegistrationRepository;
+        this.pathPatternRequestMatcher = new PathPatternRequestMatcher(HttpMethod.GET, OAUTH_REQUEST_URI_PATTERN);
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        HttpServletResponse httpServletResponse = ((HttpServletResponse) response);
+        HttpServletRequest httpServletRequest = ((HttpServletRequest) request);
+
+        if (!pathPatternRequestMatcher.matches(httpServletRequest)) {
+            chain.doFilter(request, response);
+            return;
+        }
+
+        final String registrationId = pathPatternRequestMatcher.getPathVariable(httpServletRequest, REGISTRATION);
+        final ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(registrationId);
+
+        if (clientRegistration == null) {
+            httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
+            return;
+        }
+
+        httpServletResponse.sendRedirect(clientRegistration.getOauth2AuthorizationRedirectURl().toString());
+    }
+}

리뷰: `PathPattern`을 그대로 사용할 수도 있겠지만 지난 미션을 떠올려보면 `RequestMatcher`를 여기서 사용해볼 수 있을 것 같아요.
@@ -0,0 +1,49 @@
+package nextstep.security.oauth2;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import nextstep.security.access.PathPatternRequestMatcher;
+import nextstep.security.oauth2.registration.ClientRegistration;
+import nextstep.security.oauth2.registration.ClientRegistrationRepository;
+import org.springframework.http.HttpMethod;
+import org.springframework.web.filter.GenericFilterBean;
+
+import java.io.IOException;
+
+public class OAuth2LoginRedirectFilter extends GenericFilterBean {
+    private static final String REGISTRATION = "registration-id";
+    private static final String OAUTH_REQUEST_URI_PATTERN = "/oauth2/authorization/{" + REGISTRATION + "}";
+
+    private final ClientRegistrationRepository clientRegistrationRepository;
+    private final PathPatternRequestMatcher pathPatternRequestMatcher;
+
+    public OAuth2LoginRedirectFilter(ClientRegistrationRepository clientRegistrationRepository) {
+        this.clientRegistrationRepository = clientRegistrationRepository;
+        this.pathPatternRequestMatcher = new PathPatternRequestMatcher(HttpMethod.GET, OAUTH_REQUEST_URI_PATTERN);
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        HttpServletResponse httpServletResponse = ((HttpServletResponse) response);
+        HttpServletRequest httpServletRequest = ((HttpServletRequest) request);
+
+        if (!pathPatternRequestMatcher.matches(httpServletRequest)) {
+            chain.doFilter(request, response);
+            return;
+        }
+
+        final String registrationId = pathPatternRequestMatcher.getPathVariable(httpServletRequest, REGISTRATION);
+        final ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(registrationId);
+
+        if (clientRegistration == null) {
+            httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
+            return;
+        }
+
+        httpServletResponse.sendRedirect(clientRegistration.getOauth2AuthorizationRedirectURl().toString());
+    }
+}

리뷰: redirect path를 만드는 로직은 별도의 메소드로 빼보는건 어떨까요?
@@ -0,0 +1,27 @@
+oauth2:
+  client:
+    registration:
+      github:
+        client-id: "{client-id}"
+        client-secret: "{client-secret}"
+        response-type: "code"
+        scope: "user:email"
+        redirect-uri: "http://localhost:8080/login/oauth2/code/github"
+      google:
+        client-id: "{client-id}"
+        client-secret: "{client-secret}"
+        response-type: "code"
+        scope: "email"
+        redirect-uri: "http://localhost:8080/login/oauth2/code/google"
+    provider:
+      github:
+        token-uri: "https://github.com/login/oauth/access_token"
+        user-info-uri: "https://api.github.com/user"
+        authorization-uri: "https://github.com/login/oauth/authorize"
+      google:
+        token-uri: "https://oauth2.googleapis.com/token"
+        user-info-uri: "https://www.googleapis.com/oauth2/v3/userinfo"
+        authorization-uri: "https://accounts.google.com/o/oauth2/v2/auth"
+
+
+

리뷰: 식별값도 노출하고 싶지 않아서 진행했습니다 :)