Spring security jwt Oauth2 로그인 - 구현(2)

2025. 3. 17. 14:28Springboot/security

전체코드 : https://github.com/gks930620/spring_securty_all

 

구현 자체는  JWT access token,refresh token에 

Oauth2 몇몇 기능을 추가한 거밖에 없다. 

그래서 여기는 전체코드를 전부 설명하는것이 아니라 추가된 내용만 작성하는걸로 하고

전체코드는 github를 참고하도록 하자. 

 

 

Custom-oauth2/login/kakao 컨트롤러

이 떄 이 url은 permitALL 되어야한다.

 

 

Oauth2LoginController

@Controller
@RequestMapping("/custom-oauth2/login")
@RequiredArgsConstructor
public class Oauth2LoginController {


    private final InMemoryAuthorizationRequestRepository authorizationRequestRepository;


    @Value("${spring.security.oauth2.client.registration.kakao.client-id}")
    private String kakaoClientId;

    @Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}")
    private String kakaoRedirectUri;

    @RequestMapping("/kakao")
    public ResponseEntity<?> kakaoOauth2Login(HttpServletRequest request, HttpServletResponse response){
        String state=""+UUID.randomUUID();

        // ✅ OAuth2AuthorizationRequest 직접 생성 == 로그인사용자가 로그인페이지 요청한사용자인지 확인하는 용.  클라이언트 구별!!
        OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
            .authorizationUri("https://kauth.kakao.com/oauth/authorize")
            .clientId(kakaoClientId)
            .redirectUri(kakaoRedirectUri)
            .state(state)
            .attributes(attrs -> attrs.put("registration_id", "kakao"))
            .build();

        authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
        //기본은 session에 저장해서 구별하는건데 여기서는 session없으니까 그냥 map에다..  서버여러대면 DB 등

        // ✅ Authorization URL 생성
        String authorizationUrl = "https://kauth.kakao.com/oauth/authorize"
            + "?client_id=" + kakaoClientId
            + "&redirect_uri=" + kakaoRedirectUri
            + "&response_type=code"
            + "&state=" + state;

        return ResponseEntity.ok(Map.of("authorizationUrl", authorizationUrl)); //client에는 카카오 restAPI문서에서 요구하는 형태로!
    }
}

 

 

InMemoryAuthorizationRequestRepository

@Component
public class InMemoryAuthorizationRequestRepository implements
    AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
    
    private final Map<String, OAuth2AuthorizationRequest> authorizationRequests = new ConcurrentHashMap<>();

    @Override
    public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
        String state = request.getParameter("state");
        if (state == null) {
            return null;
        }
        return authorizationRequests.get(state);
    }

    @Override
    public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {
        if (authorizationRequest == null) {
            return;
        }
        
        String state = authorizationRequest.getState();
        authorizationRequests.put(state, authorizationRequest);

        System.out.println("✅ OAuth2AuthorizationRequest 저장: " + state);

        // 5분 후 자동 삭제 (보안 상 Authorization Request를 계속 들고 있을 필요 없음)
        new Thread(() -> {
            try {
                TimeUnit.MINUTES.sleep(5);
                authorizationRequests.remove(state);
                System.out.println("🗑️ OAuth2AuthorizationRequest 만료 삭제: " + state);
            } catch (InterruptedException ignored) {}
        }).start();
    }

    @Override
    public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request,HttpServletResponse response) {
        String state = request.getParameter("state");
        if (state == null) {
            return null;
        }
        System.out.println("🚀 OAuth2AuthorizationRequest 조회 후 삭제: " + state);
        return authorizationRequests.remove(state);
    }
}

 

 

 

 

로그인과정 

 

우리가 설정할건  카카오로 인가코드 보내기 전에 Oauth2AuthorizationRequest를 검사하는 것이다.

이는 SeucirtyConfig의    authorizationRequestRepository를 위에서 만든 

InMemoryAuthorzationRequestRepository로 지정해주면 된다. 

http.oauth2Login(oauth2 -> oauth2
    .authorizationEndpoint(authEndpoint -> authEndpoint
        .authorizationRequestRepository(authorizationRequestRepository)) // ✅ 직접 구현한 저장소 적용
    .userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService))
    .successHandler(oAuth2LoginSuccessHandler) // ✅ 로그인 성공 시 JWT 발급
    .failureHandler((request, response, exception) -> {
        exception.printStackTrace();
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    })  // ✅ 실패 시 로그 찍기
);

 

CustomOauth2UserDetailsService와 

CustomUserAccount 는  기존 웹 세션방식과 똑같다.   추가해주도록 하자. 

https://brilliantdevelop.tistory.com/221 참고.

 

 

이후 로그인성공을 했다면 클라이언트에게  JWT 방식에 맞게  access token, refresh token을 보내주면 된다.  

successHandler는 위의 config에  successHandler 등록하면 된다.

 

Oauth2LoginSuccessHandler

@Component
@RequiredArgsConstructor
public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private final JwtUtil jwtUtil;
    private  final RefreshRepository refreshRepository;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        CustomUserAccount customUserAccount = (CustomUserAccount) authentication.getPrincipal();


        String accessToken = jwtUtil.createAccessToken(customUserAccount.getUsername());
        String refreshToken=jwtUtil.createRefreshToken(customUserAccount.getUsername());


        // ✅ 새 Refresh Token 저장 (기존 Token 삭제 X)
        RefreshEntity refreshEntity = new RefreshEntity();
        refreshEntity.setUserEntity(customUserAccount.getUserEntity());  // 이전 필터에서 사용자정보저장할 떄  userEntity는  entity상태로 저장됨.
        refreshEntity.setToken(refreshToken);
        refreshRepository.save(refreshEntity);


        // 토큰을 응답에 포함
        response.setContentType("application/json");
//        response.getWriter().write("{\"access_token\": \"" + accessToken + "\"}");
        response.getWriter().write(new ObjectMapper().writeValueAsString(
            Map.of("access_token", accessToken, "refresh_token", refreshToken)
        ));


    }
}

 

 

 

 

이후 로그인과정은  클라이언트가 acccess token, refresh token을 활용하면 된다. 

이는 https://brilliantdevelop.tistory.com/226와 똑같다.

 

 

 

실행결과

 

로그인페이지로 요청 후 authorizationURL을 받는다.  

 

 

 

authorizationURL로 요청하면 다음과 같은 화면이 나온다.  

(지금 우리 서버는 백엔드서버지만 딱히 로그인페이지에서 id,pw를 입력할  클라이언트를 안 만들어서

브라우저로 진행한다.)

 

 

 

 

 

redirect-uri를 처리한 서버로그. 인가코드가 포함되어있다.

 

브라우저는 로그인 후 자동으로 redirect-uri로  요청한다.  

로그를 보면 /login/oauth2/code/kakao?code= 가 있다. 인가코드가 포함된 요청이다.

(브라우저가 아닐 때는 redirect-uri로 클라이언트가 직접 백엔드서버에 요청하면 된다.)

어쨋든 redirect-uri로 요청했기 때문에 우리 백엔드서버는 access token과 refresh token을 준다.

 

 

 

 

 

이후 이 access token으로  /my/info에 요청하면 내 정보를 보게된다.

 

 

나머지 토큰만료, refresh 토큰등은  일반회원과 똑같이 처리된다.
(카카오로 로그인을 했다면 일반회원처럼 DB에 저장되어있기떄문에)