인가코드를 통해 토큰을 발급받을 때 401에러가 납니다

문의 시, 사용하시는 개발환경과 디벨로퍼스 앱ID를 알려주세요.


인가코드를 통해 토큰값을 받아오려고 할 때 에러가 납니다.

org.springframework.web.client.HttpClientErrorException$Unauthorized: 401 Unauthorized: [no body]\n\tat org.springframework.web.client.HttpClientErrorException.create(HttpClientErrorException.java:106)\n\tat org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:183)\n\tat org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:137)\n\tat org.springframework.web.client.ResponseErrorHandler.handleError(ResponseErrorHandler.java:63)\n\tat org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:932)\n\tat org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:881)\n\tat org.springframework.web.client.RestTemplate.execute(RestTemplate.java:781)\n\tat org.springframework.web.client.RestTemplate.postForObject(RestTemplate.java:498)\n\tat main.sulsul.oauth.domain.kakao.KakaoApiClient.requestAccessToken(KakaoApiClient.java:61)\n\tat main.sulsul.oauth.domain.oauth.RequestOAuthInfoService.request(RequestOAuthInfoService.java:22)\n\tat main.sulsul.oauth.application.OAuthLoginService.getAuthTokens(OAuthLoginService.java:140)\n\tat main.sulsul.oauth.application.OAuthLoginService.login(OAuthLoginService.java:39)\n\tat main.sulsul.oauth.ui.AuthController.loginKakao(AuthController.java:23)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)\n\tat java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:568)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:262)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:190)\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:917)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:829)\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089)\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979)\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)\n\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914)\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590)\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:205)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\n\tat org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:108)\n\tat org.springframework.security.web.ObservationFilterChainDecorator$FilterObservation$SimpleFilterObservation.lambda$wrap$1(ObservationFilterChainDecorator.java:479)\n\tat org.springframework.security.web.ObservationFilterChainDecorator.lambda$wrapUnsecured$1(ObservationFilterChainDecorator.java:90)\n\tat org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:219)\n\tat org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191)\n\tat org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113)\n\tat org.springframework.web.servlet.handler.HandlerMappingIntrospector.lambda$createCacheFilter$3(HandlerMappingIntrospector.java:195)\n\tat org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113)\n\tat org.springframework.web.filter.CompositeFilter.doFilter(CompositeFilter.java:74)\n\tat org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration$CompositeFilterChainProxy.doFilter(WebMvcSecurityConfiguration.java:225)\n\tat org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:352)\n\tat org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:268)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\n\tat org.springframework.web.filter.ServerHttpObservationFilter.doFilterInternal(ServerHttpObservationFilter.java:109)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:340)\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:391)\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:896)\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1744)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\n\tat java.base/java.lang.Thread.run(Thread.java:840)\n

이러한 에러가 나고,

앱ID는 1057917

이것입니다.

로직은

String url = authUrl + "/oauth/token";

        HttpHeaders httpHeaders = new HttpHeaders();
//        httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        httpHeaders.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

        httpHeaders.add("Accept","application/json");

        MultiValueMap<String, String> body = params.makeBody();
        body.add("grant_type", GRANT_TYPE);
        body.add("client_id", clientId);
        body.add("client_secret", clientSecret);
        body.add("redirect_uri", "http://localhost:8080/login/oauth2/code/kakao"); //로컬

이렇게 되어있는데,
다른 분들 401 noBody 에러는 httpHeaders.add(“Content-type”, “application/x-www-form-urlencoded;charset=utf-8”);
이걸 추가했더니 된다는데 저는 안되가지고 어떤 문제점이 있는것인지 확인부탁드립니다!

 @Override
    public String requestAccessToken(OAuthLoginParams params) {
        String url = authUrl + "/oauth/token";

        HttpHeaders httpHeaders = new HttpHeaders();
//        httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        httpHeaders.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

        httpHeaders.add("Accept","application/json");

        MultiValueMap<String, String> body = params.makeBody();
        body.add("grant_type", GRANT_TYPE);
        body.add("client_id", clientId);
        body.add("client_secret", clientSecret);
        body.add("redirect_uri", "http://localhost:8080/login/oauth2/code/kakao"); //로컬
//        body.add("redirect_uri", "http://ec2-52-78-29-203.ap-northeast-2.compute.amazonaws.com:9090/login/oauth2/code/kakao");
//
        HttpEntity<?> request = new HttpEntity<>(body, httpHeaders);

        KakaoTokens response = restTemplate.postForObject(url, request, KakaoTokens.class);

        assert response != null;
        return response.getAccessToken();
    }

메소드 전체 남깁니다.

안녕하세요

인가코드와 접근토큰 발급에 사용된 앱 키가 서로 다릅니다.(다른 앱의 앱 키)
사용하신 앱 키 확인 부탁드립니다.

로컬에서는 되는건 확인했는데, 9090포트를 사용하는 ec2 검증 서버에서는 또 401에러가 나거든요, 확인한번 해주실 수 있으실까요?
이번에도 앱 키가 달른것인가요?

현재 정상 호출 하는 것으로 보이는데요
여전히 오류가 발생하실까요?

안녕하세요 당시에 알려주신 덕분에 잘 되었습니다! 감사합니다!

그런데 이제 제가 앱 개발자랑 소통하면서 개발을 하고 있는데 앱개발자가 sdk 로 보내는거라 인가코드를 보낼 수가 없다고 하는데,
그래서 자기는 일단 accessToken만 보내줄 수 있다고 하네요. 그렇다면, 해당 accessToken으로 보내보고 있기는 했는데,

이쪽으로 앱단에서 준 액세스 토큰 값을 헤더에 담아서 https://kapi.kakao.com/v2/user/me 여기에 보내고 있는데

org.springframework.web.client.HttpClientErrorException$Unauthorized: 401 Unauthorized: [no body]

이런 에러가 납니다! 혹시 출근하시면 확인해주시면 감사하겠습니다!
어떤 원인인지 모르겠어서요 ?

지난번에도 감사했고 확인해주시면 감사하겠습니다!

안녕하세요.

앱 개발자가 accessToken 만 전달 가능하신 상황으로 보아 앱에서는 디벨로퍼스에서 제공하는 네이티브 SDK를 사용 하는 것으로 보입니다.

네이티브 SDK 사용 시, 인가코드는 네이티브 레벨에서 사용되어 접근토큰까지 발급되므로 서비스에서는 인가코드를 사용하실 필요 없이 접근토큰을 전달 받아 유효성을 검사하시고 필요하신 로직 처리를 하실 수 있습니다.

401 오류의 경우 자세한 확인을 위해 구현하신 코드를 같이 첨부 부탁드립니다.

기존에는

@PostMapping("/kakao")
    public ResponseEntity<AuthTokens> loginKakao(@RequestBody KakaoLoginParams params) {
        return ResponseEntity.ok(oAuthLoginService.login(params));
    }

컨트롤러를 통해 들어와서

    public AuthTokens login(OAuthLoginParams params) {
        return getAuthTokens(params);
    }
private AuthTokens getAuthTokens(OAuthLoginParams params) {
        OAuthInfoResponse oAuthInfoResponse = requestOAuthInfoService.request(params);
        Long memberId = findOrCreateMember(oAuthInfoResponse);
        return authTokensGenerator.generate(memberId);
    }

이렇게 로직을 타는데
requestOAuthInfoService.request(params); 여기서 params에 인가코드가 있었어서,

    public OAuthInfoResponse request(OAuthLoginParams params) {
        OAuthApiClient client = clients.get(params.oAuthProvider());
        String accessToken = client.requestAccessToken(params);
        return client.requestOauthInfo(accessToken);
    }

client.requestAccessToken(params); 이 메서드를 통해서
accessToken을 받아오게 되었습니다.

해당 accessToken을 통해서

client.requestOauthInfo(accessToken);

이 메서드를 통해 아래와 같이 호출을 했는데요

    @Override
    public OAuthInfoResponse requestOauthInfo(String accessToken) {
        String url = apiUrl + "/v2/user/me";

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        httpHeaders.set("Authorization", "Bearer " + accessToken);

        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("property_keys", "[\"kakao_account.email\", \"kakao_account.profile\"]");

        HttpEntity<?> request = new HttpEntity<>(body, httpHeaders);

        return restTemplate.postForObject(url, request, KakaoInfoResponse.class);
    }

근데 이제 이게 기존 방식이라면,

    public OAuthInfoResponse request(OAuthLoginParams params) {
        OAuthApiClient client = clients.get(params.oAuthProvider());
        String accessToken = client.requestAccessToken(params);
        return client.requestOauthInfo(accessToken);
    }

여기서 params에 기존에 담겨있던 인가코드가 아니라 params에 accessToken가 리퀘스트를 통해 이미 들어와있기 때문에,

    @Override
    public OAuthInfoResponse requestOauthInfo(String accessToken) {
        String url = apiUrl + "/v2/user/me";

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        httpHeaders.set("Authorization", "Bearer " + accessToken);

        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("property_keys", "[\"kakao_account.email\", \"kakao_account.profile\"]");

        HttpEntity<?> request = new HttpEntity<>(body, httpHeaders);

        return restTemplate.postForObject(url, request, KakaoInfoResponse.class);
    }

이 메서드에 넣으면 된다고 판단을 했는데요, 혹시 이런 방식으로 진행하면 안되나요? 아니면 앱단에서 가져온 클라이언트 값이나 시크릿 값이나 그런 설정변수들이 좀 달랐을 가능성이 있을 까요?

우선, 네이티브에서 SDK사용 시 인가코드는 SDK에서 사용하기에 백엔드로 전달할 수 없습니다.
때문에, 접근 토큰을 전달하고자 하시는 것으로 보이는데
토큰을 전달하고자 하는 이유가 어떻게 되시나요?
(ex, 회원가입 또는 서비스측의 기존 계정과 카카오 계정 매핑)

회원가입을 하려고 합니다! 흠 그 후에, 기존에 하던 방식대로 제가 인가코드를 직접해서 액세스토큰을 받아서 저장해둔 다음 기존에 토큰받는 로직을 없애고 저장해둔 액세스 토큰으로 한번 시도를 했더니 되더라구요, 제가 어제 했을 때는 안되긴 했었는데, 혹시 궁금한게 sdk를 통해 백엔드로 넘겨주는 액세스토큰 값과 서버에서 인가코드를 사용하여 얻은 액세스토큰 값이 다른가요? 아니면 그냥 둘이 같은 로직으로 만들어지는 액세스토큰일까요?

@dlxoalsdla 안녕하세요.

액세스토큰은 발급받을 때마다 새로운 값이 전달됩니다.
액세스토큰 발급 시, 사용한 디벨로퍼스 앱키 종류에 따라 만료 시간이 다르나 기본적으로 같은 기능을 하는 토큰이라 생각하시면 됩니다.

저장해둔 토큰은 아마도 만료되었을 것 같네요.

아 그럼 우선 앱에서 sdk를 통해 받은 액세스 토큰이건, 클라에서 인가코드를 넘겨받아서 서버쪽에서 자체적으로 받은 액세스토큰이건 같은 액세스 토큰인게 맞는걸까요??

네, 만료시간만 차이 있고 기능상 같은 액세스 토큰입니다.