이번 포스팅엣서는 SpringBoot로 Zoom API를 사용해서 회의(Meeting)을 생성하는 코드를 알아보겠습니다.
제가 열심히 삽질한 내용이니 더 좋은 방법이 있으시면 공유나 따끔한 한마디 부탁드리겠습니다.
이 글은 Zoom App MarketPlace에서 OAuth app 을 생성한 이후 포스팅을 봐주시면 감사하겠습니다.
Zoom App MarketPlace - 앱 생성 URL
OAuth for user authorized apps - 권한 API 문서
위 사이트는 ZOOM Developers의 OAuth 관련 문서입니다.
여기서 제가 자유롭게 해석해서 제 맛대로 사용할 수 있습니다.
아래 글은 제 멋대로 사용한 코드임으로 무단으로 사용하셔도 좋습니다 ^^
Zoom's OAuth 2.0 endpoints are:
https://zoom.us/oauth/authorize for user authorizationhttps://zoom.us/oauth/devicecode for device code authorizationhttps://zoom.us/oauth/token for access tokenshttps://zoom.us/oauth_device or https://zm.me/in for device code verification해당 요청으로 권한, 엑세스 토큰을 얻을 수 있다고 합니다. 저는 웹사이트를 구축하기 때문에 device에 대한 endpoint는 무시해도 됩니다.
결국 제가 원하는 회의 생성, 삭제, 조회 등 다양한 api를 사용하기 위한
access token 발급 절차를 알려주는 부분입니다.
간단하게 이야기하면, https://zoom.us/oauth/authorize 로 권한을 얻고,
권한을 얻은 상태에서 https://zoom.us/oauth/token 로 token을 얻을 수 있습니다.
결국 2번의 api 조회 끝에, 원하는 api까지 도달 할 수 있는 준비가 완료된것이죠.
| Query Parameter | Description |
|---|---|
| response_type | Access response type being requested. The supported authorization workflow requires the value code. |
| redirect_uri | URI to handle successful user authorization. Must match with Development or Production Redirect URI in your OAuth app settings. |
| client_id | OAuth application's Development or Production Client ID. |
| state | (Optional) An opaque value that you can use to maintain state between the request and callback. The authorization server appends the state value to the redirect URI. This is also useful to prevent https://datatracker.ietf.org/doc/html/rfc6749#section-10.12. |
| code_challenge | (Optional, but required if using PKCE A challenge derived from the code verifier sent in the authorization request to verify against the code_verifier later. |
| code_challenge_method | (Optional) A method that was used to derive the code challenge. Defaults to "plain" if not present in the request. Code verifier transformation method is "S256" or "plain". |
해당 엔드포인트로 위의 parameter들과 함께 전송을 하면 됩니다. 뭔말인지 모르겠다구요 ?
예시를 들어보겠습니다.
springboot 일 경우 : redirect_uri의 주소에 queryString 으로 code 값이 옵니다.
redirect_uri = http://localhost:8080/_new/support/reservation/zoomApi
- 해당 uri는 자신의 프로젝트에 따라 다르게 설정하셔도 됩니다 !!
@RequestMapping(value="_new/support/reservation/zoomApi" , method = {RequestMethod.GET, RequestMethod.POST})
public String googleAsync(HttpServletRequest req, @RequestParam String code) {
System.out.println("code: "+ code);
}
이렇게 구성하면 됩니다.
https://example.com/?code=obBEe8ewaL_KdyNjniT4KPd8ffDWt9fGB
이렇게 해당 code값이 옵니다.
POSTRequest headers
| Key | Value description |
|---|---|
| Authorization | The string Basic with your Client ID and Client Secret separated with colon (:), https://www.base64encode.org/. For example, Client_ID:Client_Secret Base64-encoded is Q2xpZW50X0lEOkNsaWVudF9TZWNyZXQ=. |
| Content-Type | application/x-www-form-urlencoded |
Request body
Send the appropriate request body parameters:
| Key | Value description |
|---|---|
| code | The authorization code supplied to the callback by Zoom. User authorization only, not device. |
| grant_type | One of the following:- authorization_code for user authorization- urn:ietf:params:oauth:grant-type:device_code for device authorization |
| redirect_uri | Your application's redirect URI. User authorization only, not device. |
| code_verifier | _Optional, but required if using PKCE. A cryptographically random string used to correlate the authorization request to the token request. User authorization only. |
| device_code | The device code supplied to the callback by Zoom. Device authorization only. |
이쯤 되면 슬슬 해석이 되기 시작합니다. 해당 endpoint로 Request 요청을 보내야하는데
header
Authorization 에 {client_id}:{client_secret} 값을 Base64로 인코딩한 값을 넣어줍니다.
Content-Type 에 application/x-www-form-urlencoded 넣어줍니다.
body
code : Step1에서 얻었던 code 값
grant_type 에 authorization_code
redirect_uri: [REDIRECT URI]
code_verifier: [CODE VERIFIER]
이렇게 넣어줍니다.
SpringBoot 코드
String zoomUrl = "https://zoom.us/oauth/token";
//통신을 위한 okhttp 사용 maven 추가 필요a
OkHttpClient client = new OkHttpClient();
ObjectMapper mapper = new ObjectMapper();
FormBody formBody = new FormBody.Builder()
.add("code", code) // 1단계에서 받은 code 값
.add("redirect_uri", "http://localhost:8080/_new/support/reservation/zoomApi") //등록 된 uri
.add("grant_type", "authorization_code") // 문서에 명시 된 grant_type
.add("code_verifier", EncodeUtil.encode(code)) // code를 SHA-256 방식으로 암호화하여 전달
.build();
Request zoomRequest = new Request.Builder()
.url(zoomUrl) // 호출 url
.addHeader("Content-Type", "application/x-www-form-urlencoded") // 공식 문서에 명시 된 type
.addHeader("Authorization","Basic " + secretKey) // Client_ID:Client_Secret 을 Base64-encoded 한 값
.post(formBody)
.build();
Response zoomResponse = client.newCall(zoomRequest).execute();
String zoomText = zoomResponse.body().string();
mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
Map<String, String> list = mapper.readValue(zoomText, new TypeReference<>() {});
ZoomToken zoomToken = new ZoomToken();
zoomToken.setId(0L);
zoomToken.setAccessToken(list.get("access_token"));
zoomToken.setRefreshToken(list.get("refresh_token"));
이렇게 Response 데이터로 받아오면
{
"access_token": "eyJhbGciOiJIUzUxMiIsInYiOiIyLjAiLCJraWQiOiI8S0lEPiJ9.eyJ2ZXIiOiI2IiwiY2xpZW50SWQiOiI8Q2xpZW50X0lEPiIsImNvZGUiOiI8Q29kZT4iLCJpc3MiOiJ1cm46em9vbTpjb25uZWN0OmNsaWVudGlkOjxDbGllbnRfSUQ-IiwiYXV0aGVudGljYXRpb25JZCI6IjxBdXRoZW50aWNhdGlvbl9JRD4iLCJ1c2VySWQiOiI8VXNlcl9JRD4iLCJncm91cE51bWJlciI6MCwiYXVkIjoiaHR0cHM6Ly9vYXV0aC56b29tLnVzIiwiYWNjb3VudElkIjoiPEFjY291bnRfSUQ-IiwibmJmIjoxNTgwMTQ2OTkzLCJleHAiOjE1ODAxNTA1OTMsInRva2VuVHlwZSI6ImFjY2Vzc190b2tlbiIsImlhdCI6MTU4MDE0Njk5MywianRpIjoiPEpUST4iLCJ0b2xlcmFuY2VJZCI6MjV9.F9o_w7_lde4Jlmk_yspIlDc-6QGmVrCbe_6El-xrZehnMx7qyoZPUzyuNAKUKcHfbdZa6Q4QBSvpd6eIFXvjHw",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzUxMiIsInYiOiIyLjAiLCJraWQiOiI8S0lEPiJ9.eyJ2ZXIiOiI2IiwiY2xpZW50SWQiOiI8Q2xpZW50X0lEPiIsImNvZGUiOiI8Q29kZT4iLCJpc3MiOiJ1cm46em9vbTpjb25uZWN0OmNsaWVudGlkOjxDbGllbnRfSUQ-IiwiYXV0aGVudGljYXRpb25JZCI6IjxBdXRoZW50aWNhdGlvbl9JRD4iLCJ1c2VySWQiOiI8VXNlcl9JRD4iLCJncm91cE51bWJlciI6MCwiYXVkIjoiaHR0cHM6Ly9vYXV0aC56b29tLnVzIiwiYWNjb3VudElkIjoiPEFjY291bnRfSUQ-IiwibmJmIjoxNTgwMTQ2OTkzLCJleHAiOjIwNTMxODY5OTMsInRva2VuVHlwZSI6InJlZnJlc2hfdG9rZW4iLCJpYXQiOjE1ODAxNDY5OTMsImp0aSI6IjxKVEk-IiwidG9sZXJhbmNlSWQiOjI1fQ.Xcn_1i_tE6n-wy6_-3JZArIEbiP4AS3paSD0hzb0OZwvYSf-iebQBr0Nucupe57HUDB5NfR9VuyvQ3b74qZAfA",
"expires_in": 3599,
"scope": "user:read:admin"
}
이렇게 access_token과 관련된 데이터들이 옵니다.
그런데 여기서 주목 !!!!!!
refresh_token과 expires_in 은 왜 있는지 궁금하지 않습니까? 바로바로
access_token의 유효시간은 60분
맞습니다.. 60분이 지나면 access_code를 사용할 수 없습니다. 그래서 refresh_token이 있는데요. refresh_token을 사용하는 방법도 문서에 있습니다 !
POSTRequest headers
| Key | Value description |
|---|---|
| Authorization | The string Basic with your Client ID and Client Secret separated with colon (:), https://www.base64encode.org/. For example, Client_ID:Client_Secret Base64-encoded is Q2xpZW50X0lEOkNsaWVudF9TZWNyZXQ=. |
| Content-Type | application/x-www-form-urlencoded |
Request body
| Key | Value description |
|---|---|
| grant_type | refresh_token |
| refresh_token | Your refresh token. |
이제 혹시 어떻게 사용해야하는지 이제 보이시나요 ?
이해하셨다면 당신은,, 대단한 사람,,
해당 요청이 성공적으로 마쳤다면 아래의 데이터가 옵니다.
public String refreshToken() throws IOException {
String zoomUrl = "https://zoom.us/oauth/token";
//통신을 위한 okhttp 사용 maven 추가 필요
OkHttpClient client = new OkHttpClient();
ObjectMapper mapper = new ObjectMapper();
FormBody formBody = new FormBody.Builder()
.add("grant_type", "refresh_token") // 문서에 명시 된 grant_type
.add("refresh_token", zoomTokenRepository.findById(0L).get().getRefreshToken()) //
.build();
Request zoomRequest = new Request.Builder()
.url(zoomUrl) // 호출 url
.addHeader("Content-Type", "application/x-www-form-urlencoded") // 공식 문서에 명시 된 type
.addHeader("Authorization","Basic " + secretKey) // Client_ID:Client_Secret 을 Base64-encoded 한 값
.post(formBody)
.build();
Response zoomResponse = client.newCall(zoomRequest).execute();
String zoomText = zoomResponse.body().string();
mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
Map<String, String> list = mapper.readValue(zoomText, new TypeReference<>() {});
String accessToken = list.get("access_token");
String refreshToken = list.get("refresh_token");
}
{
"access_token": "eyJhbGciOiJIUzUxMiIsInYiOiIyLjAiLCJraWQiOiI8S0lEPiJ9.eyJ2ZXIiOiI2IiwiY2xpZW50SWQiOiI8Q2xpZW50X0lEPiIsImNvZGUiOiI8Q29kZT4iLCJpc3MiOiJ1cm46em9vbTpjb25uZWN0OmNsaWVudGlkOjxDbGllbnRfSUQ-IiwiYXV0aGVudGljYXRpb25JZCI6IjxBdXRoZW50aWNhdGlvbl9JRD4iLCJ1c2VySWQiOiI8VXNlcl9JRD4iLCJncm91cE51bWJlciI6MCwiYXVkIjoiaHR0cHM6Ly9vYXV0aC56b29tLnVzIiwiYWNjb3VudElkIjoiPEFjY291bnRfSUQ-IiwibmJmIjoxNTgwMTQ3Mzk0LCJleHAiOjE1ODAxNTA5OTQsInRva2VuVHlwZSI6ImFjY2Vzc190b2tlbiIsImlhdCI6MTU4MDE0NzM5NCwianRpIjoiPEpUST4iLCJ0b2xlcmFuY2VJZCI6MjZ9.5c58p0PflZJdlz4Y7PgMIVCrQpHDnbM565iCKlrtajZ5HHmy00P5FCcoMwHb9LxjsUgbJ7653EfdeX5NEm6RoA",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzUxMiIsInYiOiIyLjAiLCJraWQiOiI8S0lEPiJ9.eyJ2ZXIiOiI2IiwiY2xpZW50SWQiOiI8Q2xpZW50X0lEPiIsImNvZGUiOiI8Q29kZT4iLCJpc3MiOiJ1cm46em9vbTpjb25uZWN0OmNsaWVudGlkOjxDbGllbnRfSUQ-IiwiYXV0aGVudGljYXRpb25JZCI6IjxBdXRoZW50aWNhdGlvbl9JRD4iLCJ1c2VySWQiOiI8VXNlcl9JRD4iLCJncm91cE51bWJlciI6MCwiYXVkIjoiaHR0cHM6Ly9vYXV0aC56b29tLnVzIiwiYWNjb3VudElkIjoiPEFjY291bnRfSUQ-IiwibmJmIjoxNTgwMTQ3Mzk0LCJleHAiOjIwNTMxODczOTQsInRva2VuVHlwZSI6InJlZnJlc2hfdG9rZW4iLCJpYXQiOjE1ODAxNDczOTQsImp0aSI6IjxKVEk-IiwidG9sZXJhbmNlSWQiOjI2fQ.DwuqOzywRrQO2a6yp0K_6V-hR_i_mOB62flkr0_NfFdYsSqahIRRGk1GlUTQnFzHd896XDKf_FnSSvoJg_tzuQ",
"expires_in": 3599,
"scope": "user:read"
}
제가 refresh_token 이렇게 테스트 해봤는데 access_token을 비교 해봤습니다.
엑세스 코드가 수정되서 날아옵니다.
access_token을 받아오는데까지 (ㅠㅠ) 알아봤습니다. 그럼 이제 회의를 생성해야겠죠 ?
이제 Zoom을 생성하는 API를 사용해 보겠습니다.
PATH PARAMETERS
| userId | The user's user ID or email address. For user-level apps, pass the me value. |
|---|
REQUEST BODY : application/json
Meeting object
{
"agenda": "My Meeting",
"default_password": false,
"duration": 60,
"password": "123456",
"pre_schedule": false,
"recurrence": {
"end_date_time": "2022-04-02T15:59:00Z",
"end_times": 7,
"monthly_day": 1,
"monthly_week": 1,
"monthly_week_day": 1,
"repeat_interval": 1,
"type": 1,
"weekly_days": "1"
},
"schedule_for": "jchill@example.com",
"settings": {
"additional_data_center_regions": [
"TY"
],
"allow_multiple_devices": true,
"alternative_hosts": "jchill@example.com;thill@example.com",
"alternative_hosts_email_notification": true,
"approval_type": 2,
"approved_or_denied_countries_or_regions": {
"approved_list": [
"CX"
],
"denied_list": [
"CA"
],
"enable": true,
"method": "approve"
},
"audio": "telephony",
"audio_conference_info": "test",
"authentication_domains": "example.com",
"authentication_exception": [
{
"email": "jchill@example.com",
"name": "Jill Chill"
}
],
"authentication_option": "signIn_D8cJuqWVQ623CI4Q8yQK0Q",
"auto_recording": "cloud",
"breakout_room": {
"enable": true,
"rooms": [
{
"name": "room1",
"participants": [
"jchill@example.com"
]
}
]
},
"calendar_type": 1,
"close_registration": false,
"contact_email": "jchill@example.com",
"contact_name": "Jill Chill",
"email_notification": true,
"encryption_type": "enhanced_encryption",
"focus_mode": true,
"global_dial_in_countries": [
"US"
],
"host_video": true,
"jbh_time": 0,
"join_before_host": false,
"language_interpretation": {
"enable": true,
"interpreters": [
{
"email": "interpreter@example.com",
"languages": "US,FR"
}
]
},
"sign_language_interpretation": {
"enable": true,
"interpreters": [
{
"email": "interpreter@example.com",
"sign_language": "American"
}
]
},
"meeting_authentication": true,
"meeting_invitees": [
{
"email": "jchill@example.com"
}
],
"mute_upon_entry": false,
"participant_video": false,
"private_meeting": false,
"registrants_confirmation_email": true,
"registrants_email_notification": true,
"registration_type": 1,
"show_share_button": true,
"use_pmi": false,
"waiting_room": false,
"watermark": false,
"host_save_video_order": true,
"alternative_host_update_polls": true,
"internal_meeting": false,
"continuous_meeting_chat": {
"enable": true,
"auto_add_invited_external_users": true
},
"participant_focused_meeting": false,
"push_change_to_calendar": false
},
"start_time": "2022-03-25T07:32:55Z",
"template_id": "Dv4YdINdTk+Z5RToadh5ug==",
"timezone": "America/Los_Angeles",
"topic": "My Meeting",
"tracking_fields": [
{
"field": "field1",
"value": "value1"
}
],
"type": 2
}Request Body를 통해 데이터를 보낼 수 있는데 굉장히 많은 데이터를 보낼 수 있습니다. 제목부터 시간, 비밀번호, 진행시간, 시작시간, 날짜, 다양한 Settings 등을 요청값으로 보낼 수 있습니다. 위는 예시 JSON 코드입니다.
이렇게 API 요청을 보내게 되면, 회의가 생성되고 다양한 Response 값들이 전달됩니다.
{
"assistant_id": "kFFvsJc-Q1OSxaJQLvaa_A",
"host_email": "jchill@example.com",
"id": 92674392836,
"registration_url": "https://example.com/meeting/register/7ksAkRCoEpt1Jm0wa-E6lICLur9e7Lde5oW6",
"agenda": "My Meeting",
"created_at": "2022-03-25T07:29:29Z",
"duration": 60,
"h323_password": "123456",
"join_url": "https://example.com/j/11111",
"chat_join_url": "https://example.com/launch/jc/11111",
"occurrences": [
{
"duration": 60,
"occurrence_id": "1648194360000",
"start_time": "2022-03-25T07:46:00Z",
"status": "available"
}
],
"password": "123456",
"pmi": "97891943927",
"pre_schedule": false,
"recurrence": {
"end_date_time": "2022-04-02T15:59:00Z",
"end_times": 7,
"monthly_day": 1,
"monthly_week": 1,
"monthly_week_day": 1,
"repeat_interval": 1,
"type": 1,
"weekly_days": "1"
},
"settings": {
"allow_multiple_devices": true,
"alternative_hosts": "jchill@example.com;thill@example.com",
"alternative_hosts_email_notification": true,
"alternative_host_update_polls": true,
"approval_type": 0,
"approved_or_denied_countries_or_regions": {
"approved_list": [
"CX"
],
"denied_list": [
"CA"
],
"enable": true,
"method": "approve"
},
"audio": "telephony",
"audio_conference_info": "test",
"authentication_domains": "example.com",
"authentication_exception": [
{
"email": "jchill@example.com",
"name": "Jill Chill",
"join_url": "https://example.com/s/11111"
}
],
"authentication_name": "Sign in to Zoom",
"authentication_option": "signIn_D8cJuqWVQ623CI4Q8yQK0Q",
"auto_recording": "cloud",
"breakout_room": {
"enable": true,
"rooms": [
{
"name": "room1",
"participants": [
"jchill@example.com"
]
}
]
},
"calendar_type": 1,
"close_registration": false,
"contact_email": "jchill@example.com",
"contact_name": "Jill Chill",
"custom_keys": [
{
"key": "key1",
"value": "value1"
}
],
"email_notification": true,
"encryption_type": "enhanced_encryption",
"focus_mode": true,
"global_dial_in_countries": [
"US"
],
"global_dial_in_numbers": [
{
"city": "New York",
"country": "US",
"country_name": "US",
"number": "+1 1000200200",
"type": "toll"
}
],
"host_video": true,
"jbh_time": 0,
"join_before_host": true,
"language_interpretation": {
"enable": true,
"interpreters": [
{
"email": "interpreter@example.com",
"languages": "US,FR"
}
]
},
"sign_language_interpretation": {
"enable": true,
"interpreters": [
{
"email": "interpreter@example.com",
"sign_language": "American"
}
]
},
"meeting_authentication": true,
"mute_upon_entry": false,
"participant_video": false,
"private_meeting": false,
"registrants_confirmation_email": true,
"registrants_email_notification": true,
"registration_type": 1,
"show_share_button": true,
"use_pmi": false,
"waiting_room": false,
"watermark": false,
"host_save_video_order": true,
"internal_meeting": false,
"continuous_meeting_chat": {
"enable": true,
"auto_add_invited_external_users": true
},
"participant_focused_meeting": false,
"push_change_to_calendar": false
},
"start_time": "2022-03-25T07:29:29Z",
"start_url": "https://example.com/s/11111",
"timezone": "America/Los_Angeles",
"topic": "My Meeting",
"tracking_fields": [
{
"field": "field1",
"value": "value1",
"visible": true
}
],
"type": 2
}public ZoomMeetingObjectEntity createMeeting(ZoomMeetingObjectDTO zoomMeetingObjectDTO) throws IOException {
// refresh 검
isExpired();
System.out.println("Request to create a Zoom meeting");
// replace zoomUserId with your user ID
String apiUrl = "https://api.zoom.us/v2/users/" + "jongminshin373@gmail.com" + "/meetings";
// replace with your password or method
// replace email with your email
zoomMeetingObjectDTO.setHost_email("jongminshin373@gmail.com");
// Optional Settings for host and participant related options
ZoomMeetingSettingsDTO settingsDTO = new ZoomMeetingSettingsDTO();
settingsDTO.setJoin_before_host(false);
settingsDTO.setParticipant_video(true);
settingsDTO.setHost_video(false);
settingsDTO.setAuto_recording("cloud");
settingsDTO.setMute_upon_entry(true);
zoomMeetingObjectDTO.setSettings(settingsDTO);
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + zoomTokenRepository.findById(0L).get().getAccessToken());
headers.add("content-type", "application/json");
HttpEntity<ZoomMeetingObjectDTO> httpEntity = new HttpEntity<ZoomMeetingObjectDTO>(zoomMeetingObjectDTO, headers);
ResponseEntity<ZoomMeetingObjectDTO> zEntity = restTemplate.exchange(apiUrl, HttpMethod.POST, httpEntity, ZoomMeetingObjectDTO.class);
if(zEntity.getStatusCodeValue() == 201) {
System.out.println("Zooom meeeting response {}" + zEntity);
return zoomMeetingRepository.save(zEntity.getBody().toEntity());
} else {
System.out.println("Error while creating zoom meeting {}" + zEntity.getStatusCode());
}
zoomMeetingObjectDTO.setSettings(null);
return null;
}
그럼 해당 회의 Dto를 생성해서 원하는 데이터를 담고 데이터베이스에 저장하면 ~~~ 끝이납니다.
이렇게 하면, Access_Token을 발급받을 때 굉장히 복잡하게 됩니다.
매번 API 요청을 보내야하고, 계속해서 Code 발급받고,
access_token을 발급받아야하는데요 그래서 제가 자동화 코드를 짰습니다 !!
access_token, refresh_token, updatedAt 데이터 세개를 DB에 저장하고,
updatedAt 이 1시간이 지났으면, refresh_token으로 토큰을 재생성하고
다시 DB에 넣어두고 사용하는 방식입니다.
비효율적인거 같으면 말해주세요 ! 좋은 방법있으면 수정하겠습니다.
유효성 확인 메서드 : isExpired()
public void isExpired() throws IOException {
// 현재 시간 가져오기
LocalDateTime now = LocalDateTime.now();
// updatedAt과 현재 시간의 차이 계산 (단위: 분)
long minutesSinceUpdate = ChronoUnit.MINUTES.between(zoomTokenRepository.findById(0L).get().getUpdatedAt(), now);
// 만료 시간을 60분으로 설정
long expirationTimeInMinutes = 60;
// 현재 시간과 updatedAt의 차이가 expirationTimeInMinutes 이상인 경우 만료되었다고 판단
// updatedAt 시간이 현재 시간으로부터 60분 이상 경과했다면 true를 반환하며, 그렇지 않으면 false를 반환합니다.
boolean checkExpired = minutesSinceUpdate >= expirationTimeInMinutes;
// 토큰 재발급
if(checkExpired) {
refreshToken();
}
}
해당부분이 핵심이긴 하지만, yml, Base64 코드,수많은 Dto, 다양한 파일들을 생성해야 위의 메서드를 사용할 수 있습니다. 여러분들이 필요하시면 제가 사용한 java 코드를 공유하도록 하겠습니다. Dto도 위의 Request, Response 보고 GPT 보고 만들어줘 하면 10초만에 만들어 줄껍니다 하하 !! 여러분은 저처럼 삽질하지 마시고, 똑똑하게 코딩하세요
잘 읽었습니다.