์šฐ๊ทœ์ด์ธ์šฐ์œค
Eager To Learn ๐ŸŒŒ
์šฐ๊ทœ์ด์ธ์šฐ์œค
์ „์ฒด ๋ฐฉ๋ฌธ์ž
์˜ค๋Š˜
์–ด์ œ

๋ธ”๋กœ๊ทธ ๋ฉ”๋‰ด

  • ๐Ÿก ํ™ˆ
  • ๐Ÿš€ ๊นƒํ—ˆ๋ธŒ
  • โ›… ํƒœ๊ทธ ํด๋ผ์šฐ๋“œ
  • ๋ถ„๋ฅ˜ ์ „์ฒด๋ณด๊ธฐ (217)
    • ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป PS (170)
      • JAVA (82)
      • MYSQL (1)
      • Docker (2)
      • PYTHON (24)
      • LeetCode 150 (39)
      • Algorithm ๊ธฐ๋ฒ• (1)
      • ๋ฐ”ํ‚น๋… (21)
    • ๋ธ”๋กœ๊ทธ ์ด์‚ฌ (0)
    • Error (1)
    • CS (15)
      • DataBase (2)
      • OS (7)
      • Network (1)
      • Spring (1)
      • ์ž๋ฃŒ๊ตฌ์กฐ (3)
      • Java (1)
    • Learned (7)
      • Spring (7)
    • ๊ฐœ๋ฐœ์„œ์  (15)
      • ๊ฐ€์ƒ ๋ฉด์ ‘ ์‚ฌ๋ก€๋กœ ๋ฐฐ์šฐ๋Š” ๋Œ€๊ทœ๋ชจ ์‹œ์Šคํ…œ ์„ค๊ณ„ ๊ธฐ์ดˆ (1)
      • ์˜ค๋ธŒ์ ํŠธ - ์กฐ์˜ํ˜ธ (7)
      • ์นœ์ ˆํ•œ SQL ํŠœ๋‹ (7)
    • ํšŒ๊ณ  (2)
hELLO ยท Designed By ์ •์ƒ์šฐ.
์šฐ๊ทœ์ด์ธ์šฐ์œค
Learned/Spring

Spring Boot Github ์†Œ์…œ ๋กœ๊ทธ์ธ ๊ตฌํ˜„ํ•˜๊ธฐ [ RestTemplate ยท WebClient ยท FeignClient ๋ฅผ ๋น„๊ตํ•ด๋ณด์ž ]

Learned/Spring

Spring Boot Github ์†Œ์…œ ๋กœ๊ทธ์ธ ๊ตฌํ˜„ํ•˜๊ธฐ [ RestTemplate ยท WebClient ยท FeignClient ๋ฅผ ๋น„๊ตํ•ด๋ณด์ž ]

2023. 8. 21. 18:24

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 ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค๋Š” ์ธ์‚ฌ์ดํŠธ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

  • Github OAuth ์ธ์ฆ ํ๋ฆ„๊ณผ ์‚ฌ์ „ ์ค€๋น„
  • application.yml ์„ค์ •
  • html ํŒŒ์ผ
  • ๊ตฌํ˜„
  • JWT ๊ด€๋ จ Enum ์ •์˜
  • CookieUtil ์ •์˜
  • Controller 
  • GithubJoinService ์ธํ„ฐํŽ˜์ด์Šค ์ •์˜
  • UserProfile ์ •์˜
  • TextParsingUtil
  • RestTemplate ๋ฐฉ์‹
  • RestTemplateConfig
  • GithubJoinServiceRestTemplateImpl
  • WebClient ๋ฐฉ์‹
  • Dependency ์ถ”๊ฐ€
  • WebClientConfig
  • AccessTokenRequest
  • GithubJoinServiceWebClientImpl
  • FeignClient ๋ฐฉ์‹
  • Dependency ์ถ”๊ฐ€
  • application.yml ์ถ”๊ฐ€
  • @EnableFeignClients ์ถ”๊ฐ€
  • AccessTokenFeignClient
  • UserProfileFeignClient
  • Controller ์ฝ”๋“œ ์ˆ˜์ •
'Learned/Spring' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€
  • Spring Boot ์™ธ๋ถ€ API ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์งœ๋ณด์ž! [MockWebServer vs WireMock]
  • Dispatcher Servlet์€ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ• ๊นŒ?
  • Swagger API ์ ์šฉ ์‹œ, Controller ์ฝ”๋“œ๊ฐ€ ๋„ˆ๋ฌด ๋”๋Ÿฌ์›Œ์ง„๋‹ค.. ๋ถ„๋ฆฌํ•ด๋ณด์ž
  • Dto Validation ์˜ˆ์™ธ ์ฒ˜๋ฆฌ์— AOP๋ฅผ ์ ์šฉํ•ด๋ณด์ž!
์šฐ๊ทœ์ด์ธ์šฐ์œค
์šฐ๊ทœ์ด์ธ์šฐ์œค
๊ฐœ๋ฐœ์ž ๊ฟˆ๋‚˜๋ฌด

ํ‹ฐ์Šคํ† ๋ฆฌํˆด๋ฐ”

๋‹จ์ถ•ํ‚ค

๋‚ด ๋ธ”๋กœ๊ทธ

๋‚ด ๋ธ”๋กœ๊ทธ - ๊ด€๋ฆฌ์ž ํ™ˆ ์ „ํ™˜
Q
Q
์ƒˆ ๊ธ€ ์“ฐ๊ธฐ
W
W

๋ธ”๋กœ๊ทธ ๊ฒŒ์‹œ๊ธ€

๊ธ€ ์ˆ˜์ • (๊ถŒํ•œ ์žˆ๋Š” ๊ฒฝ์šฐ)
E
E
๋Œ“๊ธ€ ์˜์—ญ์œผ๋กœ ์ด๋™
C
C

๋ชจ๋“  ์˜์—ญ

์ด ํŽ˜์ด์ง€์˜ URL ๋ณต์‚ฌ
S
S
๋งจ ์œ„๋กœ ์ด๋™
T
T
ํ‹ฐ์Šคํ† ๋ฆฌ ํ™ˆ ์ด๋™
H
H
๋‹จ์ถ•ํ‚ค ์•ˆ๋‚ด
Shift + /
โ‡ง + /

* ๋‹จ์ถ•ํ‚ค๋Š” ํ•œ๊ธ€/์˜๋ฌธ ๋Œ€์†Œ๋ฌธ์ž๋กœ ์ด์šฉ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ํ‹ฐ์Šคํ† ๋ฆฌ ๊ธฐ๋ณธ ๋„๋ฉ”์ธ์—์„œ๋งŒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.