1. 개요

    1. 수동 RestTemplate : 프로바이더별 엔드포인트 직접 호출하고 토큰관리,갱신 로직을 직접 만듦 → 반복,버그,보안 취약점 가능성
    2. Spring Security OAuth2 Client
      1. 표준 프로토콜 지원, 인증 플로우 토큰관리(만료 갱신) 지우너, 확장 포인트 제공 → 유지보수성 높음
  2. 전체 아키텍쳐 제안(권장)

    1. 클라이언트 → OAuth2 Authorization Code (소셜) 로그인
    2. Spring Security가 콜백 받아 OAuth2AuthorizedClient 생성(서드파티 액세스 토큰 포함)
    3. 로그인 성공 시 서버에서 자체 JWT(엣세스 토큰, 리프레시 토큰) 발급하여 클라이언트에 전달
    4. 리프레시 토큰은 DB(또는 Redis)에 저장 - 서버가 리프레쉬 토큰을 관리
    5. 서버는 필요시 OAuth2AuthorizedClientService 또는 OAuth2AuthorizedClientManager로 서드파티 토큰을 자동 갱신해 사용
    6. 클라이언트는 서버발행 JWT를 사용해 API 호출(서버는 서드파티 토큰은 내부에서 관리)
  3. 서버는 OAuth2AuthorizedClientService / OAuth2AuthorizedClientManager로 서드파티 토큰 관리

    1. 서드파티 토큰 = 카카오 네이버에서 발급한 Access Token/Refresh Token

    2. 이건 원래 카카오 API,네이버 API 같은 외부 자원서버에 접근하기 위해 필요한 거야.

    3. Spring Security가 OAuth2AuthorizedClientService에 저장해두고 만룍되면 OAuth2AuthorizedClientManager가 알아서 Refresh Token으로 갱신해 줌.

    4. 즉, 서버가 대신 들고 다니는 카카오 / 네이버 열쇄

    5. 클라이언트에는 직접 노출하지 않고, 서버에서만 안전하게 쓴다.

    6. 예시-

      OAuth2AuthorizedClient client = clientService.loadAuthorizedClient("kakao", "user123");
      String kakaoAccessToken = client.getAccessToken().getTokenValue();
      //서버가 직접 카카오 API호출에 사용
      
  4. 클라이언트는 서버발행 JWT를 사용해 API 호출

    1. 서버발행 JWT = 우리 서버가 자체적으로 만들어준 Access Token
    2. 이건 우리 서버의 API 인증/인가 용도로만 쓰임
    3. 클라이언트(브라우저/앱)는 소셜 토큰 대신 서버 JWT를 들고 다니면서 API호출
    4. 서버는 이 JWT를 검증해서 “이 요청자가 누구인지” 확인하고, 필요하면 내부적으로 서드파티 토큰을 꺼내서 카카오 / 네이버 API를 대신 호출해줌
  5. 필수 의존성 application.yml예시 (provider 설정)

    1. 카카오 / 네이버는 각자 엔드포인트가 표준과 조금 다를 수 있으니 authorization-uri, token-uri, user-info=uri를 실제 문서대로 채워야 함

    2. 예시

      spring:
      	security:
      		oauth2:
      			client:
      				registration:
      					kakao:
      						client-id: ${KAKAO_CLIENT_ID}
      						client-secret: ${KAKAO_CLIENT_SECRET}
      						redirect-uri: "{baseUrl}/oauth2/callback/{registrationId}"
      						authorization-grant-type: authorization_code
      						scope:
      							- profile
      							- account_email
      					naver:
      						client-id: ${NAVER_CLIENT_ID}
      						client-secret: ${NAVER_CLIENT_SECRET}
      						client-name: Naver
      						redirect-uri: "{baseUrl}/oauth2/callback/{registrationId}"
      						authorization-grant-type: authorization_code
      						scope:
      							- name
      							- email
      				provider:
      					kakao:
      						authorization-uri: <https://kauth.kakao.com/oauth/authorize>
      						token-uri: <https://nid.naver.com/oauth2.0/token>
      						user-info-uri: <https://openapi.naver.com/v1/nid/me>
      						user-name-attribute: response
      
      1. 실제 프로바이더 문서를 확인해 user-name-attribute와 user-info-uri의 응답 구조에 맞게 매핑해야 함
  6. Security 설정

    1. 핵심

      1. oauth2Login() 설정과 로그인 성공 시 처리 (커스텀 성공 핸들러)로 JWT 발급
      @Configuration
      @EnableWebSecurity
      public class SecurityConfig{
      	@Bean
      	public securityFilterChain securityFilterChain(HttpSecurity http,
      																									CustomOAuth2UserService oAuth2UserService, OAuth2LoginSuccessHandler successHandler) throws Exception{
      		http
      			.csrf().disable()
      			.authorizeHttpRequests(auth -> auth
      				.requestMatchers("/api/public/**", "/oauth2/**").permitAll()
      				.anyRequest().authenticated())
      			.oauth2Login(oauth -> oauth
      				.userInfoEndpoint(userInfo -> userInfo.userService(oAuth2UserService))
      				.successHandler(successHandler));
      					return http.build();
      	}
      }
      
      //코드설명 
      // HttpSecurity 빌더 시작점. 이 체이닝으로 Security 설정을 구성하고, 마지막에 http.build()로 SecurityFilterChain을 만든다.
      2. .csrf().diasble()
      	1. CSRF 보호를 비활성화함
      	2. 왜 흔히 쓰나 : REST API(특히 JWT로 stateless 인증을 하거나, 모바일/ SPA에서 토큰으로 인증)를 제공할 때는 CSRF 토큰을 사용하지 않기 때문에 자주 끈다.
      	3. 주의 쿠키 기반 세션 인증인 경우 CSRF를 꺼버리면 위험. 브라우저를 통해 로그인을 제공하고 쿠키를 사용한다면 기본적으로 CSRF를 켜 두는 편이 안전하다.
      	4. 실무팁 : SPA + 쿠키 방식이면 CookieCsfTokenRepository 같은 안전한 설정을 고려하자.
      3. .authorizeHttpRequests(auth -> auth ...)
      	1.요청별 접근 규칙을 선언하는 블록(누가 어떤 경로에 접근 가능한지)
      	2. 람다 내부에 여러 requestMathcers / mvcMathchers / antMatchers 등을 순서대로 작성 해서 허용 / 차단 룰을 만든다.
      4. .requestMatchers("/api/public/**", "/oauth2/**").permitAll()
      5. .anyRequest().authenticated()
      	1. 위에서 따로 허용한 경로를 제외한 모든 요청은 인증된 사용자만 접근 가능하다는 뜻
      	즉, 인증되지 않은 사용자가 나머지 API를 호출하면 401 / 302로 처리된다.
      	2. 즉, 인증되지 않은 사용자가 나머지 API를 호출하면 401 / 302로 처리된다.
      6. .oauth2Login(oauth -> oauth ...)
      	1. OAuth2 로그인 관련 설정 블록
      	2. 이 블록 안에서 userInfoEndpoint(프로바이더에서 사용자 정보 가져오는 부분)과 successHandler을 커스터마이즈한다.
      	//7. .userInfoEndpoint(userInfo -> userInfo.userService(oAuth2UserService)) userInfoEndpoint()
      		//1. userInfoEndpoint() 는 액세스 토큰으로 프로바이더의 UserInfo 엔드포인트를 호출해 사용자 속성을 얻는 부분을 제어.
      		//2. userService(oAuth2UserService)로 DefualtOAuth2UserService 를 대체하는 커스텀 서비스를 등록하면, provider별 attribute 매핑(카카오 / 네이버 구조 차이 처리), 로컬 사용자 생성 / 조회, 추가 검증 등을 할 수 있다.
      		3. 실행 시점: 토큰 교환이 끝나고, 스프링이 user-info를 조회할 때 이 서비스가 호출된다. 이 서비스에서 예외를 던지면 인증 실패 처리 됨
      	8. .successHandler(successHandler)
      		1. 로그인 성공 후 동작을 담당하는 핸들러를 지정.
      		2. 기본 동작은 세션에 인증 저장하고 이전 요청으로 리다이렉트하거나 기본 타깃 URL로 보낸다. 하지만 SPA나 모바일 API에서는 redirect보다 JSON으로 JWT를 내려주거나 쿠키를 설정하는 식으로 다르게 처리해야 하므로 커스텀 핸들러를 쓴다.
      		3. successHandler는 onAuthenticationSuccess(HttpServletRequest, HttpservletResponse, Authentication)을 구현해서 토큰 발급, DB저장, 리다이렉트. 응답 바디 작성 등을 할 수 있다.
      		4. 실행 순서: userService가 OAuthUser를 만들어 Authentication이 생성된 뒤 호출됨.   
      
  7. 커스텀 OAuth2UserService - 프로바이더 응답을 앱 유저로 매핑

    1. 소셜 프로필을 로컬 user 엔티티로 매핑하고 DB에 저장(최초 로그인 시)

    2. 예시

      @Service
      public class CustomOAuth2UserService extends DefaultOAuth2UserService{
      	@Override
      	public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
      			// 프로바이더에서 사용자 정보를 가져온다.
      			OAuth2User oAuth2User = super.loadUser(userRequest);
      			// 어떤 소셜인지 식별(kakao, naver, google 등)
      			String regustrationId = userRequest.getClientRegistration().getRegistrationId();
      			//프로바이더가 반환한 attribute들을 맵으로 추출
      			Map<String, Object> attributes = oAuth2User.getAttributes();
      			
      			// provider별로 attributes 구조가 다르니 파싱 / 정규화(팩토리/어댑터)
      			SocialUserInfo info = SocialUserInfoFactory.getSocialUserInfo(registrationId, attribute);
      			
      			// 로컬 User 엔티티 생성 조회 업데이트 
      			// 예: User user = userRepository.findByEmail(info.getEmail()).orElseCreate(...);
      			
      			// Soring Security가 사용할 OAuth2User 구현체 반환 (권한, 속성, nameAttributeKey)
      			return new DefaultOAuth2User(
      			Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"))
                  attributes,
      "id") //OAuth2User#getName()가 attributes.get("id")를 사용하게 됨
      			}
      
      }
      
  8. 로그인 성공 시 JWT 발급

    1. 소셜 로그인 성공 후 서버가 자체 JWT(액세스 / 리프레시) 발급하여 클라이언트에 전달

    2. 예시

      @Component
      public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler{
      	private final JwtProvider jwtProvider;
      	private final RefreshTokenService refreshTokenService; //DB 저장
      	
      	@Override
      	public void onAuthenticationSuccess(HttpServletRequest request,
      																			HttpServletResponse response,
      																			AUthentication authentication) throws IOException {
      																				OAuth2User oAuth2User = (oAuth2User) authentication.getPrincipal();
      																				String email =extractEmail(oAuth2User);
      																				
      																				String accessToken = jwtProvider.createAccessToken(email);
      																				String refreshToken = jwtProvider.createRefreshToken(email)'
      																				
      																				refreshTokenService.save(email, refreshToken);
      																				
      																				// 응답: 예시로 JSON body 전송
      																				Map<String, String> tokens =Map.of(
      																					"accessToken", accessToken,
      																					"refreshToken", refreshToken
      														
      																				);
      																				response.setContentType(MediaType.APPLICATION_JSON_VALUE);
      																				new objectMapper().writeValue(response.getWriter(), tokens);
      																			}
      }
      
  9. OAuth2AuthorizedClientService / Manager 활용 (서드파티 토큰 갱신)

    1. 서드파티 API(카카오에 사용자 정보 추가 조회)를 서버에서 호출할 때 OAuth2AUthorizedClientService로 저장된 OAuth2AuthorizedClient에서 토큰을 꺼내 사용하고, 만료 시 자동 갱신하게 할 수 있음

    2. 예시

      @Autowired
      private OAuth2AuthorizedClientService authorizedClientService;
      
      public String callKakaoApi(String principalName){
      	OAuth2AuthorizedClient client = authorizedClientService.loadAuthorizedClient("kakao", principalName);
      	String accessToken = client.getAccessToken().getTokenValue();
      //RestTemplate 또는 WebClient로 카카오 API 호출(Authorization: Bearer ...)
      
      } 
      
      1. 보다 자동화하려면 OAuth2AuthorizedClientManager를 사용해 토큰 갱신을 처리하게 한다.
  10. JWT 기반 세션 대체 전략 (권장)

    1. 액세스 토큰
      1. 짧은 만료 - 클라이언트가 요청마다 전송

      2. 리프레시 토큰: 서버에 저장(데이터베이스 / Redis) → 탈취 위험 줄임

      3. 리프레시 시 서버에서만 갱신하고 새 액세스 토큰 발급

      4. 장점

        1. 서버가 리프레시 토큰을 통제 → 토큰 무효화(logout)가 쉬움
      5. 예시

        @RestController
        @RequestMapping("/auth")
        public class AUthController{
        
        	private final JwtProvider jwtProvider;
        	private final RefreshTokenService refreshTokenService;
        	
        	@PostMapping("/refresh")
        	public ResponseEntity<?> refresh(@RequestBody Map<String, String> body){
        	
        	String refreshToken = body.get("refreshToken");
        	if(!jwtProvider.validateToken){
        		return ResponseEntity.status(HttpStatus.unauthorized).build();
        	}
        	
        	String email = jwtProvider.getSubject(refreshToken);
        	of(!refreshTokenService.exists(email, refreshToken)){
        	
        		return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();}
        	String newAccessToken = jwtProvider.createAccessToken(email);
        	String newRefreshToken = jwtProvider.createRefreshToken(email);
        	refreshTokenService.replace(email, refreshToken, newRefreshToken);
        		return ResponseEntity.ok(Map.of("accessToken", newAccessToken, "refreshToken", newRefreshToken));	
        	
        	
        	}
        
        }
        
  11. OAuth2.0 의 과정과 용어 설명

    1. OAuth 구성 요소
      1. Resource Owner
        1. 웹 서비스를 이용하려는 유저 , 자원(개인정보)를 소유하는 자, 사용자 Resource는 개인정보라고 생각하면 됨
      2. Client
        1. 자사 또는 개인이 만든 애플리케이션 서버
        2. 클라이언트라는 이름은 client 가 Resource server에게 필요한 자원을 요청하고 응답하는 관계여서 그렇다.
      3. Authorization Server
        1. 권한을 부여해주는 서버다.
        2. 사용자는 이 서버로 ID, PW를 넘겨 Authorization Code를 발급 받을 수 있다.
        3. Client는 이 서버로 Authorization code을 넘겨 Token을 발급받을 수 있다.
      4. Resource Server
        1. 사용자의 개인정보를 가지고 있는 애플리케이션 화사 서버
        2. Client는 Token을 이 서버로 넘겨 개인정보를 응답받을 수 있다.
      5. Access Token
        1. 자원에 대한 접근 권한을Resource Owner가 인가하였음을 나타내는 자격증명
      6. Refresh Token
        1. 사용자의 개인정보를 가지고 있는 애플리케이션(Google, Facebook, Kakao 등) 회사 서버
        2. Client는 Token을 이 서버로 넘겨 개인정보를 응답 받을 수 있다.
    2. 과정
      1. 애플리케이션 등록
      2. Client ID, Client Secret, Authorized redirect URL를 등록 페이지에서 IDE에서 등록
    3. Resource owner 승인 과정
      1. 사용자는 서비스를 이용하기 위해 로그인 페이지에 접근한다.
      2. 서비스는 사용자에서 로그인 페이지를 제공하게 된다. 그리고 버튼을 만든다.
      3. 만들어진 버튼을 누르면, 특정한 url이 페이스북 서버쪽으로 보내지게 됩니다.
      4. 클라이언트로부터 보낸 서비스 정보와, 리소스 로그인 서버에 등록된 서비스 정보를 비교한다.
      5. 확인이 완료되면 , Resource Server로 부터 전용 로그인 페이지로 이동하여 사용자에게 보여준다.
      6. ID/PW를 적어서 로그인을 하게되면, client가 사용하려는 기능에 대해 Resource Owner의 동의를 요청한다.
      7. Resource Owner가 Allow 버튼을 누르면 Resource Owner가 권한 을 위임했다는 승인이 Resource Server에 전달된다.
      8. 하지만 Owner가 Client에게 권한 승인을 했더라고 아직 서버가 허락하지 않았다. 따라서 Resource Server도 Client에게 권한 승인을 하기위해 Authorization code 를 Redirect URL를 돛해 사용자에게 응답하고 사용자는 그대로 Client에게 다시 보낸다.
      9. 이제 Client가 Resource Server에게 직접 url (클라이언트 아이디, 비번, 인증코드 …등)을 보낸다.
      10. 그럼 Resource Server는 Client가 전달한 정보들을 비교해서 일치한다면, Access Token을 발급한다. 그리고 이제 필요없어진 Authorization code는 지운다.
      11. 드렇게 토큰을 받은 Client는 사용자에게 최종적으로 로그인이 완료되었다고 응답한다.
      12. 이제 client는 Resource server의 api를 요청해 Resource Owner의 ID 혹은 프로필 정보를 사용할 수 있다.
      13. Access Token이 기간이 만효되어 401에러가 나면, Refresh Token을 통해 Access Token을 재발급 한다.
  12. 궁금했던 부분 SecurityContextHolder.getContext().setAuthentication(authentication)이 궁금 했습니다.