Retrofit2로 JWT 인증하기

노력을 즐겼던 사람·2020년 8월 8일
2

사오정 앱개발

목록 보기
6/11
post-thumbnail

Retrofit은 OkHttp 기반으로 동작한다. 따라서 OkHttp를 제외한 다른 HTTP 모듈들에 비해 빠르다. 사용하기도 편리하다. 그래서 통신 모듈을 Retrofit으로 결정했다. 자세한 내용은 벤치 마크를 참고하자

Gradle dependencies

build.gradle(Module: app)에 아래 의존성 추가 (2020.08.08 기준)


// dependencies for retrofit
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:okhttp:4.8.1'

// dependencies for lombok
compileOnly 'org.projectlombok:lombok:1.18.12'
annotationProcessor 'org.projectlombok:lombok:1.18.12'

AndroidManifest.xml

통신을 위해 manifest 밑에 다음 코드 추가

<uses-permission android:name="android.permission.INTERNET"/>

작성할 클래스

  • ServiceGenerator
  • StoreService Interface
  • AuthenticationInterceptor
  • StoreDto

StoreService Interface

StoreService Interface는 우리가 요청을 보낼 URL들을 정의해놓는다. 코드를 보면 바로 이해할 수 있다.

import java.util.List;

import retrofit2.Call;
import retrofit2.http.GET;

public interface StoreService {
    @GET("/api/store")
    Call<List<StoreDto>> getStoreListOrderByGrade();
}

항상 Call<> 안에 서버의 응답에 해당하는 Dto를 넣어야한다. 그리고 GET, POST, PATCH, PUT, DELETE 처럼 서버에 등록된 라우터에 맞는 HTTP METHOD와 URL을 작성하면 된다. getStoreListOrderByGrade() 의 응답 JSON은 다음과 같다.

[
    {
        "store_number": 8,
        "store_name": ".S.",
        "vote_grade_average": 4.8,
        "vote_grade_count": 0,
        "store_id": 2,
        "starred": 0
    },
    ...
    ...
    ...
    {
        "store_number": 8,
        "store_name": "나",
        "vote_grade_average": 0,
        "vote_grade_count": 0,
        "store_id": 5,
        "starred": 0
    }
]

동일한 column들의 반복이므로 List<StoreDto> 를 통해 응답을 받고자 한다. 그러면 다음으로 StoreDto 를 정의해보자

StoreDto

Dto는 Data Tranfer Object의 약자이다. 말 그대로 데이터를 옮겨주는 객체이다. 그래서 아무런 기능이 없다. 그리고 등록만 해놓으면 Retrofit에 등록해놓은 GsonFactory 가 응답 받은 JSON을 Dto로 변환해준다. 개꿀! 코드를 살펴보자

import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class StoreDto {
    @SerializedName("store_number")
    @Expose
    private Integer storeNumber;
    
    @SerializedName("store_name")
    @Expose
    private String storeName;
    
    @SerializedName("vote_grade_average")
    @Expose
    private float voteGradeAverage;
    
    @SerializedName("vote_grade_count")
    @Expose
    private Integer voteGradeCount;
    
    @SerializedName("store_id")
    @Expose
    private Integer storeId;
    
    @SerializedName("starred")
    @Expose
    private Integer starred;
}

getter()setter() 가 꼭 필요하다. 이런 코드를 작성하기가 너무 귀찮기 때문에 찾아보니 http://www.jsonschema2pojo.org/ Json을 입력하면 자바 코드로 변환해주는 녀석이 있었다. 우리 프로젝트의 경우에는 다음과 같은 설정으로 변환을 진행했다.

  • Target language: Java
  • Source type: JSON
  • Annotation style: Gson
  • Include getters and setters uncheck (Lombok 사용시에만)
  • 나머지는 건드리지 않음

주의사항!!
소수형을 Integer로 생성해주더라 직접 float로 바꿔주자

Service Generator

우리가 위에서 작성한 Service Interface를 실행해주는 객체를 생성해주는 녀석이다. 이 녀석을 통해 통신원을 생성해보자 일단 코드를 보자

import android.text.TextUtils;

import okhttp3.OkHttpClient;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class ServiceGenerator {

    public static final String BASE_URL = "YOUR_URL";

    private static OkHttpClient.Builder httpClient = new OkHttpClient.Builder();

    private static Retrofit.Builder builder =
            new Retrofit.Builder()
                    .baseUrl(API_BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create());

    private static Retrofit retrofit = builder.build();

    public static <S> S createService(Class<S> serviceClass) {
        return createService(serviceClass, null);
    }

    public static <S> S createService(
            Class<S> serviceClass, final String authToken) {
        if (!TextUtils.isEmpty(authToken)) {
            AuthenticationInterceptor interceptor =
                    new AuthenticationInterceptor("Bearer " + authToken);

            if (!httpClient.interceptors().contains(interceptor)) {
                httpClient.addInterceptor(interceptor);

                builder.client(httpClient.build());
                retrofit = builder.build();
            }
        }

        return retrofit.create(serviceClass);
    }
}

method 들은 public static 으로 정의가 되어있고 멤버 변수들은 private static 으로 정의가 되어있는걸보니 아마도 통신원을 여러개를 무분별하게 생성해서 프로그램의 성능 저하가 발생 하지는 않을 것 같다. 코드의 출처는 여기를 참고하자 굉장히 설명도 잘해놨다.

주의사항!!!

  • JWT를 사용하기 위해서는 TOKEN 문자열에 꼭 "Bearer "를 더해주자.
  • BASE_URL은 https 통신이어야 한다.
  • BASE_URL은 마지막에 꼭 "/"로 끝나야한다.

우리 프로젝트에서 사용하는 인증방식인 JWT는 TOKEN을 기반으로 인증을 진행한다. 따라서 통신을 할 때마다 Authorization 헤더에 TOKEN을 추가해서 전송해야하는데 Retrofit에서는 OkHttp3 Interceptor를 통해 Retrofit Instance를 생성할 때 Retrofit이 기반하고 있는 OkHttp3의 코어에 헤더를 수정하는 것 같다(뇌피셜;; 아마 Retrofit과 OkHttp는 Keras와 Tensorflow의 관계 같다)

Authentication Interceptor

import java.io.IOException;

import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

public class AuthenticationInterceptor implements Interceptor {

    private String authToken;

    public AuthenticationInterceptor(String token) {
        this.authToken = token;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request original = chain.request();

        Request.Builder builder = original.newBuilder()
                .header("Authorization", authToken);

        Request request = builder.build();
        return chain.proceed(request);
    }
} 

이 녀석은 HTTP 통신에 사용하는 Request 객체를 생성해준다. 새로운 생성한 Request 의 헤더에는 TOKEN을 담은 Authorization 옵션이 추가되어 있다.

어쨌든 통신이 필요한 곳에서 ServiceGenerator.createService(ServiceInterface.class, TOKEN) 을 호출하면 되는데 아래에서 호출하는 코드를 살펴보자

FOO Activity

통신이 필요한 액티비의 onCreate 함수에서 호출을하자

protected void onCreate(Bundle savedInstanceState) {
	...
	...
    String TOKEN = getToken(); // access token을 가져오는 함수를 직접 정의하셔야합니다. 
    storeService = ServiceGenerator.createService(StoreService.class, TOKEN);
	...
    	...
    	...
    loadStores();
}

prviate void loadStores() {
	storeService.getStoreListOrderByGrade().enqueue(new Callback<List<StoreDto>>() {
            @Override
            public void onResponse(Call<List<StoreDto>> call,
                                   Response<List<StoreDto>> response) {
                if (response.isSuccessful()) {
                    // response.body()
                    // response.body()에서 넘어오는 데이터로 Adapter에 뿌려주기
                } else {
                    Log.d("REST FAILED MESSAGE", response.message());
                }
            }

            @Override
            public void onFailure(Call<List<StoreDto>> call, Throwable t) {
                Log.d("REST ERROR!", t.getMessage());
            }

}

이런 식으로 작성하면 된다. 기억할 것은 Service Interface 를 생성한 후 (ServiceGenerator 를 통해 생성해야한다) Service Interface 에 정의되어 있는 함수를 호출하고 CallBack 함수를 정의해주면 된다.

참고문서

https://uareuni.tistory.com/30
https://chuumong.github.io/android/2017/01/13/Get-Started-With-Retrofit-2-HTTP-Client
https://jungwoon.github.io/android/2019/07/11/Retrofit/
https://futurestud.io/tutorials/android-basic-authentication-with-retrofit
https://stackoverflow.com/questions/41078866/retrofit2-authorization-global-interceptor-for-access-token

profile
노력하는 자는 즐기는 자를 이길 수 없다 를 알면서도 게으름에 지는 중

2개의 댓글

comment-user-thumbnail
2020년 11월 10일

안녕하세요 덕분에 헤더에 JWT를 넣고 서버에 보내는 방법에 대해 차근차근 이해할 수 있었습니다. 감사합니다. 이것을 읽으면서 궁금한 점이 생겨 이렇게 댓글을 달게 되었습니다. 서버에서 로그인시에 JWT 값을 받아와서 이를 사용자 인증이 필요할 때마다 헤더에 실어 보내는 경우 storeService = ServiceGenerator.createService(StoreService.class, TOKEN); 이 부분을 불러올 때, TOKEN에 에러메세지가 뜨는데 어떻게 해결해야하는건지 궁금합니다! 블로그에서 많이 배우고 있습니다. 감사합니다! (에러:CANNOT RESLOVE SYMBOL "TOKEN")

1개의 답글