Github OAuth ์ธ์ฆ ํ๋ฆ๊ณผ ์ฌ์ ์ค๋น
Github ์ธ์ฆ ํ๋ฆ์ ๋ค์๊ณผ ๊ฐ๋ค.
1.https://github.com/login/oauth/authorize?client_id={๋ฐ๊ธ๋ฐ์ client_id}
๋ก ์ด๋ํ๋ ๋งํฌ๋ฅผ ์ฌ์ฉ์๊ฐ ํด๋ฆญํ๋ค.
2. ์ฌ์ฉ์๋ ์๋์ ๊ฐ์ ํ๋ฉด์ด ๋ํ๋๋ ํ๋ฉด์ผ๋ก ์ด๋ํ๊ณ , Github ์ธ์ฆ์ ํ๋ค.
3. ์ธ์ฆ์ ์ฑ๊ณตํ๋ฉด
{์ค์ ํ ์ฝ๋ฐฑ URL}?code={์ธ์ฆ์ฝ๋}
๋กcode
๊ฐ์ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ ํํ๋ก ๋ณด๋ด์ค๋ค.
4. ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ก ์ ๋ฌ๋code
๋ฅผ ์๋ฒ์์ ๋ฐ๊ณ ,https://github.com/login/oauth/access_token
์ผ๋ก{client_ID, client_Secret, code}
๋ฅผ ๋ด์ POST ์์ฒญ์ ํ๋ค.
5. ๊นํ๋ธ ์ธก์์access_token
์ ์๋ฒ์ธก์ผ๋ก ์๋ตํ๋ค.
6. ๋ฐ์access_token
์https://api.github.com/user
๋ก ๋ด์์ GET ์์ฒญ์ ๋ณด๋ธ๋ค.
7. ๊นํ๋ธ ์ธก์์ ์ธ์ฆํ ์ฌ์ฉ์์ ์ ๋ณด๋ฅผ ์๋ตํ๋ค.
8. ๊ทธ๋ ๊ฒ ๋ฐ์ ์ฌ์ฉ์์ ์ ๋ณด๋ฅผ ์๋ฒ์ธก์์ ํ์ฉํ๋ค.
๊ตฌํํ๊ธฐ์ ์์,
Settings
> Developer settings
> New Github App
์ผ๋ก ์ด๋ํ์ฌ ๊ธฐ๋ฅ ๊ตฌํ์ ์ํ Client_ID
์ Client_Secret
์ ๋ฐ๊ธ๋ฐ์์ผ ํ๋ค.
Homepage Url
์ ์ผ๋จ ๋ก์ปฌ ํ๊ฒฝ์์ ๊ตฌํํด๋ณผ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ localhost:8080
์ผ๋ก ํด๋์๋ค.
๋์ค์ AWS EC2 ๋ก ๋์ฐ๊ฑฐ๋ ํ๋ฉด, EC2 ์ฃผ์์ ํฌํธ๋ฒํธ๋ฅผ ์ ๋ ฅํด๋๋ฉด ๋๋ค.
Callback URL
์ ์ด๋ค ์ฌ์ฉ์๊ฐ ๊นํ๋ธ ๋ก๊ทธ์ธ์ ์ฑ๊ณตํ๋ฉด ๊นํ๋ธ ์ธก์์ Code
๋ฅผ ์ฟผ๋ฆฌํ๋ผ๋ฏธํฐ๋ก ๋ณด๋ด์ฃผ๋๋ฐ, ๊ทธ ํ๋ผ๋ฏธํฐ๋ฅผ ๋ฐ์ ์ฃผ์์ด๋ค.
๋๋ http://localhost:8080/oauth2/redirect
๋ก ์ค์ ํ์๋ค.
http://localhost:8080/oauth2/redirect?code={์ฝ๋~~}
์ด๋ฐ์์ผ๋ก ๋ฆฌ๋ค์ด๋ ํธ ๋ ๊ฒ์ด๋ค.
์ ์์ ์ผ๋ก ๋ฑ๋ก์ ๋ง์น๋ฉด Client ID
์ Client Secret
์ ๋ณด๋ฅผ ์ป์ ์ ์๋ค.
Client Secret
์ ๋ฏผ๊ฐ์ ๋ณด์ด๋ฏ๋ก ๋
ธ์ถ๋์ง ์๋๋ก ์ฃผ์ํ๋ค.
application.yml ์ค์
github:
client-id:
client-secret:
์์ ๊ฐ์ ๋ถ๋ถ์ ์ถ๊ฐํด์ค๋ค.
์ผ๋จ, client-id์ client-secret์ IntellJ ํ๊ฒฝ๋ณ์๋ก ๋ฃ์ด์ฃผ์๋ค.
html ํ์ผ
<a class="circle github"
th:onclick="'window.open(\'https://github.com/login/oauth/authorize?client_id=' + @{{clientId}(clientId=${@environment.getProperty('github.client-id')})} + '\',\'_self\')'">
<i class="fa fa-github fa-fw"></i>
</a>
html ์ฝ๋๋ ์ด๋ค ๋ฐฉ์์ด๋ ์๊ด์๋ค.
๋๋ ์ผ๋จ ๋ฒํผ์ ํด๋ฆญํ๋ฉด https://github.com/login/oauth/authorize?client_id={๋ฐ๊ธ๋ฐ์ client_id}
๋ก ์ด๋ํ๊ฒ๋ ํ๋ค.
ํ์๋ฆฌํ ๋ฌธ๋ฒ์ ํ์ฉํ๋ฉด, ํ๊ฒฝ๋ณ์๋ฅผ ์ป์ด์ ์ฌ์ฉํ ์ ์๊ธฐ ๋๋ฌธ์ ์์ ๊ฐ์ด ๋ง๋ค์๋ค.
๋ง์ฝ ํ์๋ฆฌํ๋ฅผ ์ฌ์ฉํ์ง ์๋๋ค๋ฉด, html a ํ๊ทธ์ href ๋ฐฉ์์ ์ฌ์ฉํ๋ฉด ๋ ๊ฒ์ด๋ค.
๊ตฌํ
๋จผ์ , ์ฌ์ฉ์์ ๊นํ๋ธ์ ์ฌ์ฉ์๋ช ์ ๋ฐ์ DB์ ๊ฐ์ ๋ ์ฌ์ฉ์๊ฐ ์์ผ๋ฉด jwt๋ฅผ ์์ฑํด ๋ธ๋ผ์ฐ์ ์ฟ ํค์ ์ ์ฅํ๊ณ
์์ง ๊ฐ์ ๋ ์ฌ์ฉ์๊ฐ ์๋๋ผ๋ฉด ๊ฐ์ ์ฒ๋ฆฌ ํ, jwt ๋ฅผ ์์ฑํด ๋ธ๋ผ์ฐ์ ์ฟ ํค์ ์ ์ฅํ๋ ๋ฐฉ์์ผ๋ก ๊ตฌํํ๋ค.
๊นํ๋ธ์์ ๋ฐ์์ค๋ ์ฌ์ฉ์๋ช ์ Unique ํ๊ธฐ ๋๋ฌธ์, ์๋ณํ๋ ์ฉ๋๋ก ์ฌ์ฉํ ์ ์๋ค๊ณ ํ๋จํ์๋ค.
JWT ๊ด๋ จ Enum ์ ์
public class CookieConstants {
public static final String JWT_COOKIE_NAME = "jwt";
public static final int JWT_COOKIE_AGE = 60 * 60;
}
jwt ์ ํจ ๊ธฐ๊ฐ์ 60 * 60 ์ด = 1์๊ฐ์ผ๋ก ๋์๋ค.
๊ทธ๋ฆฌ๊ณ ์ฟ ํค ์ด๋ฆ์ผ๋ก "jwt" ๋ก ์ ์ฅ๋๊ฒ๋ ํ ๊ฒ์ด๋ค.
CookieUtil ์ ์
public class CookieUtil {
public static void setCookie(HttpServletResponse response, String cookieName, String cookieValue, int cookieAge) {
ResponseCookie cookie = ResponseCookie.from(cookieName, cookieValue)
.path("/")
.httpOnly(true)
.maxAge(cookieAge)
.build();
response.addHeader("Set-Cookie", cookie.toString());
}
}
HttpServletResponse ๊ฐ์ฒด๋ฅผ ํตํด ์๋ต ์, ์ฟ ํค ์ด๋ฆ๊ณผ ์ฟ ํค๋ก ์ ์ฅ๋ ๊ฐ ๊ทธ๋ฆฌ๊ณ ์ ํจ๊ธฐ๊ฐ์ ์ค์ ํด์ ์ ์ฅ๋๋๋กํ๋ ๋ฉ์๋์ด๋ค.
Controller
@Controller
@RequiredArgsConstructor
public class UserJoinController {
@Value("${github.client-id}")
private String clientId;
@Value("${github.client-secret}")
private String clientSecret;
private final GithubJoinService githubJoinService;
private final UserJoinService userJoinService;
@GetMapping("/oauth2/redirect")
public String githubLogin(@RequestParam String code) {
String accessToken = githubJoinService.getAccessToken(clientId, clientSecret, code);
return "redirect:/githubLogin/success?access_token=" + accessToken;
}
@GetMapping("/githubLogin/success")
public String githubLoginSuccess(HttpServletResponse response, @RequestParam(name = "access_token") String accessToken) {
UserProfile userProfile = githubJoinService.getUserInfo(accessToken);
String jwt = userJoinService.login(userProfile);
CookieUtil.setCookie(response, JWT_COOKIE_NAME, jwt, JWT_COOKIE_AGE);
return "redirect:/";
}
}
๋จผ์ @Value
์ด๋
ธํ
์ด์
์ผ๋ก clientId
์ clientSecret
ํ๊ฒฝ๋ณ์๊ฐ ์ฃผ์
๋ ์ ์๋๋ก ํ๋ค.
๊ทธ๋ฆฌ๊ณ ์ฌ์ ์ค์ ์์, ์ฌ์ฉ์๊ฐ ๊นํ๋ธ ์ธ์ฆ์ ํ๋ฉด /oauth2/redirect?code={์ฝ๋~~}
๋ก ๋ฆฌ๋ค์ด๋ ํธ๋ฅผ ํ๋๋ก ์ค์ ํ๊ธฐ ๋๋ฌธ์
githubLogin(@RequestParam String code)
๋ฉ์๋๋ฅผ ์ ์ํ์๊ณ , ๋ฐ์ code
๋ก accessToken
์ ์ป์ด์ค๋ ๋ฉ์๋๋ GithubJoinService
๋ก์ ๋ฐ๋ก ์ ์ํ ๊ฒ์ด๋ค.
์ ์์ ์ผ๋ก accessToken
์ ๋ฐ์๋ค๋ฉด /githubLogin/success?access_token={๋ฐ์ access Token}
๋ก ๋ฆฌ๋ค์ด๋ ํธ ์ํค๊ฒ๋ ํ์๋ค.
๊ทธ๋ฌ๋ฉด ์๋ ์ ์ํ githubLoginSuccess()
๋ฉ์๋๊ฐ ๋์ํ๋ค.
์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ก์ ์ ๋ฌ๋ฐ์ accessToken
์ ์ด์ฉํด์ ํ์ ์ ๋ณด๋ฅผ ์ป์ด์ค๋ ๋ฉ์๋๋ฅผ ๋ง๋ค๊ณ , jwt๋ฅผ ๋ฐ๊ธ๋ฐ์ ์ฟ ํค์ ์ ์ฅํ๋๋ก ํ๋ฉด ๋ก๊ทธ์ธ ๊ณผ์ ์ด ๋ง๋ฌด๋ฆฌ๋๋ค.
ํ์ ์ ๋ณด๋ก login ํ jwt ์ํ๋ ๋ก์ง์ ์ด ๊ฒ์๊ธ์์๋ ์๋ตํ๊ฒ ๋ค.
GithubJoinService ์ธํฐํ์ด์ค ์ ์
Spring์ผ๋ก ํ ์ ์๋ ์ฌ๋ฌ๊ฐ์ง REST ์์ฒญ ๋ฐฉ๋ฒ์ ์ฌ์ฉํ ๊ฒ์ด๋ฏ๋ก ์ธํฐํ์ด์ค๋ก ์ ์ํ๊ฒ ๋ค.
public interface GithubJoinService {
String getAccessToken(@RequestParam String code);
UserProfile getUserInfo(String accessToken);
}
UserProfile ์ ์
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class UserProfile {
@JsonProperty("login")
private String userName;
@JsonProperty("avatar_url")
private String imageUrl;
private String blog;
@JsonProperty("html_url")
private String githubUrl;
}
์์ ๊ฐ์ด ์ ์ฉํ๋ค.
๋ฐ์ access_token
์ https://api.github.com/user
๋ก ๋ด์์ GET ์์ฒญ์ ๋ณด๋ด๋ฉด ์๋์ ๊ฐ์ด ์๋ตํ๊ธฐ ๋๋ฌธ์
@JsonProperty
๋ฅผ ์ด์ฉํด ํ์ํ ์ ๋ณด๋ง ํ๋๋ง ๋งคํ๋๋๋ก ํ๋ค.
{
"login": ,
"id": ,
"node_id": ,
"avatar_url": ,
"gravatar_id": "",
"url": ,
"html_url":,
"followers_url": ,
"following_url": ,
"gists_url": ,
"starred_url": ,
"subscriptions_url": ,
"organizations_url": ,
"repos_url": ,
"events_url": ,
"received_events_url": ,
"type": ,
"site_admin": ,
"name":,
"company": ,
"blog": ,
"location":,
"email": ,
"hireable": ,
"bio": ,
"twitter_username": ,
"public_repos": ,
"public_gists": ,
"followers": ,
"following": ,
"created_at": ,
"updated_at":
}
TextParsingUtil
public class TextParsingUtil {
public static Map<String, String> parsingFormData(String formData) {
Map<String, String> map = new HashMap<>();
String[] split = formData.split("&");
for (String s : split) {
String[] data = s.split("=");
if (data.length >= 2) {
map.put(data[0], data[1]);
}
}
return map;
}
}
Github์์ ์๋ต์
access_token={๊ฐ}&expires_in={๊ฐ}&refresh_token={๊ฐ}&refresh_token_expires_in={๊ฐ}&scope=&token_type={๊ฐ}
์์ ๊ฐ์ด ํ๊ธฐ ๋๋ฌธ์, ์ ํ์์ Map ์ผ๋ก Key- value ํ์์ผ๋ก ์ ์ฅํ๋ ๋ฉ์๋๋ฅผ ์ ์ํ๋ค.
์ด์ ์ ์ํ GithubJoinService ์ธํฐํ์ด์ค์ ๊ตฌํ์ฒด๋ฅผ ๋ง๋ค๋ฉด์
RestTemplate · WebClient · FaignClient ์ด 3๊ฐ์ง ๋ฐฉ๋ฒ์ ๋ค ์ฌ์ฉํด๋ณด๋๋ก ํ๊ฒ ๋ค.
RestTemplate ๋ฐฉ์
RestTemplate ๋ Spring์์ ์ง์ํ๋ ๋ฐฉ์์ด๊ณ , ์์กด์ฑ์ ๋ฐ๋ก ์ถ๊ฐํ์ง ์์๋ ์ฌ์ฉํ ์ ์๋ค.
REST API ํธ์ถ ์ดํ, ์๋ต์ ๋ฐ์ ๋ ๊น์ง ๊ธฐ๋ค๋ฆฌ๋ ๋๊ธฐ ๋ฐฉ์์ด๋ค.
WebClient ๋ฐฉ์์ด ์ถ๊ฐ๋์ด Deprecated ๋ ์ํ์ด์ง๋ง, ํ๋ฒ ๊ตฌํ์ ํด๋ณด๊ฒ ๋ค.
RestTemplateConfig
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
๋น์ผ๋ก ๊ด๋ฆฌํ๊ธฐ ์ํด ์์ ๊ฐ์ Config ํด๋์ค๋ฅผ ์ ์ํ๋ค.
GithubJoinServiceRestTemplateImpl
@Service
@RequiredArgsConstructor
public class GithubJoinServiceRestTemplateImpl implements GithubJoinService {
private final RestTemplate restTemplate;
@Override
public String getAccessToken(String clientId, String clientSecret, String code) {
HttpEntity<String> response = restTemplate.exchange("https://github.com/login/oauth/access_token",
HttpMethod.POST,
setParam(clientId, clientSecret, code),
String.class);
return TextParsingUtil.parsingFormData(response.getBody()).get("access_token");
}
private HttpEntity<MultiValueMap<String, String>> setParam(String clientId, String clientSecret, String code) {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("client_id", clientId);
params.add("client_secret", clientSecret);
params.add("code", code);
return new HttpEntity<>(params, new HttpHeaders());
}
@Override
public UserProfile getUserInfo(String accessToken) {
UserProfile body = restTemplate.exchange("https://api.github.com/user"
, HttpMethod.GET
, setAccessToken(accessToken)
, UserProfile.class)
.getBody();
System.out.println("restTemplate " + body);
return body;
}
private HttpEntity<MultiValueMap<String, String>> setAccessToken(String accessToken) {
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.add("Authorization", String.format("Bearer %s",accessToken));
return new HttpEntity<>(requestHeaders);
}
}
์ ์ฒด ์ฝ๋๋ ์์ ๊ฐ๋ค.
RestTemplate
๋ ๋น์ผ๋ก ๋ฑ๋ก๋์ด ์์ด ์ฃผ์
๋ ๊ฒ์ด๋ค.
getAccessToken()
@Override
public String getAccessToken(String clientId, String clientSecret, String code) {
HttpEntity<String> response = restTemplate.exchange("https://github.com/login/oauth/access_token",
HttpMethod.POST,
setParam(clientId, clientSecret, code),
String.class);
return TextParsingUtil.parsingFormData(response.getBody()).get("access_token");
}
private HttpEntity<MultiValueMap<String, String>> setParam(String clientId, String clientSecret, String code) {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("client_id", clientId);
params.add("client_secret", clientSecret);
params.add("code", code);
return new HttpEntity<>(params, new HttpHeaders());
}
๋จผ์ restemplate.exchange({url} , {HTTP Method} , {Request Body} , {์๋ต๋ฐ์ ๊ฐ์ฒด ํ์
})
๊ณผ ๊ฐ์ ํ์์ผ๋ก ์์ฒญํ๋ฉด ๋๋ค.
๊ทธ๋ฆฌ๊ณ response.getBody()
ํ๋ฉด ์์์ ์ค๋ช
ํ๋ฏ,
access_token={๊ฐ}&expires_in={๊ฐ}&refresh_token={๊ฐ}&refresh_token_expires_in={๊ฐ}&scope=&token_type={๊ฐ}
์์ ๊ฐ์ ๋ฌธ์์ด๋ก ์๋ตํ๋ฏ๋ก, parsingํด์ ๋ฐํํ๋ฉด ๋๋ค.
getUserInfo()
@Override
public UserProfile getUserInfo(String accessToken) {
return restTemplate.exchange("https://api.github.com/user",
HttpMethod.GET,
setAccessToken(accessToken),
UserProfile.class)
.getBody();
}
private HttpEntity<MultiValueMap<String, String>> setAccessToken(String accessToken) {
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.add("Authorization", String.format("Bearer %s",accessToken));
return new HttpEntity<>(requestHeaders);
}
์ด ๋ถ๋ถ๋ ์ ์ฌํ๊ฒ ๊ตฌํํ๋ฉด ๋๋ค.
๋ค๋ง, GET ์์ฒญ์ ํด์ผํ๊ณ Authorization ํค๋์ accessToken์ ์ถ๊ฐํด์ฃผ์ด์ผ ํ๋ค.
์ฐธ๊ณ : https://docs.github.com/ko/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user
UserProfile์ ์ด์ ์ ์ค๋ช ํ๋ฏ ๊ตฌํํ๋ฉด, ์๋ ๋งคํ๋์ด ๋ฐํ๋๋ค.
WebClient ๋ฐฉ์
WebClient ๋ ๋๊ธฐ / ๋น๋๊ธฐ ๋ฐฉ์ HTTP ์์ฒญ์ด ๊ฐ๋ฅํ, Spring ์์ ๊ถ์ฅํ๋ REST API ์์ฒญ ๋ฐฉ์์ด๋ค.
Dependency ์ถ๊ฐ
implementation 'org.springframework.boot:spring-boot-starter-webflux'
์ ์์กด์ฑ์ ์ถ๊ฐํด์ค๋ค.
WebClientConfig
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient() {
return WebClient.builder().build();
}
}
AccessTokenRequest
@Getter
@Builder
public class AccessTokenRequest {
@JsonProperty("client_id")
private String clientId;
@JsonProperty("client_secret")
private String clientSecret;
private String code;
public static AccessTokenRequest createAccessTokenRequest(String clientId, String clientSecret, String code) {
return AccessTokenRequest.builder()
.clientId(clientId)
.clientSecret(clientSecret)
.code(code)
.build();
}
}
WebClient ๋ฐฉ์์ Request Body ๋ก ์ ๋ฌํ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ฒด๋ก ์ ์ํด๋๊ณ
๋ฉ์๋ ํ๋ผ๋ฏธํฐ์ ๋ฃ์ผ๋ฉด ์ง๋ ฌํ์ํฌ ์ ์๊ธฐ ๋๋ฌธ์ ์์ ๊ฐ์ด ์ ์ํด๋๋ค.
GithubJoinServiceWebClientImpl
@Service
@RequiredArgsConstructor
public class GithubJoinServiceWebClientImpl implements GithubJoinService {
private final WebClient webClient;
@Override
public String getAccessToken(String clientId, String clientSecret, String code) {
String response = webClient.post()
.uri("https://github.com/login/oauth/access_token")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.bodyValue(AccessTokenRequest.createAccessTokenRequest(clientId, clientSecret, code))
.retrieve()
.toEntity(String.class)
.block()
.getBody();
return TextParsingUtil.parsingFormData(response).get("access_token");
}
@Override
public UserProfile getUserInfo(String accessToken) {
return webClient.get()
.uri("https://api.github.com/user")
.header("Authorization", String.format("Bearer %s",accessToken))
.retrieve()
.toEntity(UserProfile.class)
.block()
.getBody();
}
}
์ ์ฒด ์ฝ๋๋ ์์ ๊ฐ๋ค.
getAccessToken()
@Override
public String getAccessToken(String clientId, String clientSecret, String code) {
String response = webClient.post()
.uri("https://github.com/login/oauth/access_token")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.bodyValue(AccessTokenRequest.createAccessTokenRequest(clientId, clientSecret, code))
.retrieve()
.toEntity(String.class)
.block()
.getBody();
return TextParsingUtil.parsingFormData(response).get("access_token");
}
์์ ๊ฐ์ ๋ฐฉ์์ผ๋ก ๊ตฌํํ๋ฉด ๋๊ณ , post ์์ฒญ ์, bodyValue()
๋ฉ์๋์ ์ง๋ ฌํ ์ํฌ ๊ฐ์ฒด๋ฅผ ๋ฃ์ด์ฃผ๋ฉด ๋๋ค.
getUserInfo()
@Override
public UserProfile getUserInfo(String accessToken) {
return webClient.get()
.uri("https://api.github.com/user")
.header("Authorization", String.format("Bearer %s",accessToken))
.retrieve()
.toEntity(UserProfile.class)
.block()
.getBody();
}
get ์์ฒญ์ ์์ ๊ฐ์ด ๊ตฌํํ๋ฉด ๋๊ณ ,
์ ๋ฌํด์ผํ accessToken
์ header("Authorization", "Bearer " + accessToken)
์ผ๋ก ์ ๋ฌํ๋ฉด ๋๋ค.
๐ก ํ์คํ, ์์ฒญ์ ๋ฉ์๋ ์ฒด์ธ ๋ฐฉ์์ผ๋ก ๊ตฌํํ ์ ์์ด, RestTemplate ๋ณด๋ค ์ฝ๋ ๊ฐ๋ ์ฑ์ด ์ข๋ค๋ ์ฅ์ ์ ๋๋ ์ ์์๋ค.
FeignClient ๋ฐฉ์
FeignClient๋ Netflix์์ ๊ฐ๋ฐํ ๋ฐฉ์์ด๋ผ๊ณ ํ๋ค.
ํ์ฌ๋ ์คํ์์ค๋ก ์ ํ๋์์ผ๋ฉฐ SpringCloud ํ๋ ์์ํฌ์ ํ๋ก์ ํธ ์ค ํ๋๋ก ๋ค์ด๊ฐ๋ค๊ณ ํ๋ค.
ํ ์คํธ ์ฝ๋๋ฅผ ์งค ๋ ๊ฐํธํ๊ณ ๊ฐ๋ ์ฑ ๋์ ์ฝ๋๋ฅผ ์งค ์ ์๋ค๊ณ ํ๋ค.
๋จ, ๋๊ธฐ ์์ฒญ๋ง ๊ฐ๋ฅํ๊ธฐ ๋๋ฌธ์ ๋น๋๊ธฐ ๋์์ด ํ์ํ ๊ฒฝ์ฐ๋ ์ฌ์ฉํ ์ ์๋ค
Dependency ์ถ๊ฐ
implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-openfeign', version: '4.0.4'
Feign Client๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด ์ ์์กด์ฑ์ ์ถ๊ฐํด์ค๋ค.
application.yml ์ถ๊ฐ
feign:
client:
access-token:
url: https://github.com
user-profile:
url: https://api.github.com
ํ ์คํธ ์ฝ๋ ์์ฑ ์, url์ ์ธ๋ถ์์ ์ฃผ์ ํ ์ ์๊ฒ ํ๊ฒฝ๋ณ์๋ก ๋ฑ๋กํด์ฃผ๋ฉด ์ข๋ค.
๋ฐ๋ผ์ ์์ ๊ฐ์ ํ๊ฒฝ ๋ณ์๋ฅผ ์ถ๊ฐํด์ค๋ค.
@EnableFeignClients ์ถ๊ฐ
@SpringBootApplication
@EnableFeignClients
public class GrowithApplication {
public static void main(String[] args) {
SpringApplication.run(GrowithApplication.class, args);
}
}
Application ์คํํ๋ ํด๋์ค์ @EnableFeignClients
๋ฅผ ์ถ๊ฐํ๋ค.
AccessTokenFeignClient
@FeignClient(name = "access-token-feign-client", url = "${feign.client.access-token.url}")
public interface AccessTokenFeignClient {
@PostMapping("/login/oauth/access_token")
String getAccessToken(
@RequestParam("client_id") String clientId,
@RequestParam("client_secret") String clientSecret,
@RequestParam("code") String code
);
}
FeignClient ๋ interface ํ์์ผ๋ก ์ ์ํ์ฌ REST API ์์ฒญ์ ํ ์ ์๋ค.
Spring Data JPA ์ JpaRepository๋ฅผ ์์๋ฐ์ ์ฌ์ฉํ๋ interface์ ์ ์ฌํ ๋ฐฉ์์ด๋ผ๊ณ ์ดํดํ๋ฉด ๋๋ค.
accessToken์ ์๋ต๋ฐ๋ ์์ฒญ์ ์ํด ์์ ๊ฐ์ด ์ ์ํ๋ค.
์ ๋ง ๊ฐ๋จํ๊ฒ, ํ๋ผ๋ฏธํฐ์ @RequestParam
์ ํตํด RequestBody์ body ๋ฅผ ์ค์ ํ ์ ์๋ค.
UserProfileFeignClient
@FeignClient(name = "user-profile-feign-client", url = "${feign.client.user-profile.url}")
public interface UserProfileFeignClient {
@GetMapping("/user")
UserProfile getUserInfo(@RequestHeader("Authorization") String accessToken);
}
ํ์ ์ ๋ณด๋ฅผ ๋ฐ์์ค๋ ์์ฒญ์ ์์ ๊ฐ์ด Header ์ Authorization์ ์ถ๊ฐํด์ผํ๊ธฐ ๋๋ฌธ์ @RequestHeader
๋ฅผ ์ฌ์ฉํ๋ฉด ๋๋ค.
Controller ์ฝ๋ ์์
@Controller
@RequiredArgsConstructor
public class UserJoinController {
@Value("${github.client-id}")
private String clientId;
@Value("${github.client-secret}")
private String clientSecret;
private final UserJoinService userJoinService;
private final AccessTokenFeignClient accessTokenFeignClient;
private final UserProfileFeignClient userProfileFeignClient;
@GetMapping("/oauth2/redirect")
public String githubLogin(@RequestParam String code) {
String response = accessTokenFeignClient.getAccessToken(clientId, clientSecret, code);
return "redirect:/githubLogin/success?access_token=" + TextParsingUtil.parsingFormData(response).get("access_token");
}
@GetMapping("/githubLogin/success")
public String githubLoginSuccess(HttpServletResponse response, @RequestParam(name = "access_token") String accessToken) {
UserProfile userProfile = userProfileFeignClient.getUserInfo(String.format("Bearer %s",accessToken));
String jwt = userJoinService.login(userProfile);
CookieUtil.setCookie(response, JWT_COOKIE_NAME, jwt, JWT_COOKIE_AGE);
return "redirect:/";
}
}
accessTokenFeignClient.getAccessToken(clientId, clientSecret, code);
๋ฉ์๋์ ์๋ต ๊ฒฐ๊ณผ๋
access_token={๊ฐ}&expires_in={๊ฐ}&refresh_token={๊ฐ}&refresh_token_expires_in={๊ฐ}&scope=&token_type={๊ฐ}
์ ๊ฐ๊ธฐ ๋๋ฌธ์, Controller ๋ฉ์๋์์ Parsing ์์ ์ ํด์ฃผ๋๋ก ์์ ํ๋ค.
๊ทธ๋ฆฌ๊ณ , userProfileFeignClient.getUserInfo(String.format("Bearer %s",accessToken));
์ด ๋ถ๋ถ ์ญ์, Header
์ค์ ์ Bearer
ํค์๋๊ฐ ๋ถ๋๋ก ์์ ํด์ฃผ๋ฉด ๋๋ค.
Spring์ผ๋ก REST API ์์ฒญ์ ํ ์ ์๋ ๋ํ์ ์ธ ๋ฐฉ๋ฒ๋ค์ ์ฌ๋ฌ๊ฐ๋ฅผ ๋ชจ๋ ์ฌ์ฉํด๋ณด๋
๊ฐ ๋ฐฉ์์ ์ฅ๋จ์ ์ ์ ์ ์์๋ค.
ํ์คํ ๊ฐ์ฅ ์ค๋๋ ๊ธฐ์ ์ธ RestTemplate ๋ฐฉ์์ด, ์์ฒญ์ ๊ตฌ์ฑํ๋ ๋ฐฉ์์ด ๊ฐ์ฅ ๋ถํธํ์์ ์ ์ ์์๋ค.
๋ฐ๋ผ์, ๋น๋๊ธฐ ์์ฒญ์ด ํ์ํ ๊ฒฝ์ฐ์๋ WebClient ๋ฅผ ์ฌ์ฉํ ๊ฒ์ด๊ณ ๋๊ธฐ ์์ฒญ์๋ FeignClient ๋ฅผ ์ฌ์ฉํ๋ฉด ์ข์ ๊ฒ ๊ฐ๋ค๋ ์ธ์ฌ์ดํธ๋ฅผ ์ป์ ์ ์์๋ค.