Tany
백문이불어일Tany
Tany
전체 방문자
오늘
어제
  • 분류 전체보기 (197)
    • JAVA TPC (1)
    • JAVA (10)
    • CS (3)
    • SPRING (5)
    • DFS BFS (12)
    • SQL (7)
    • 알고리즘 정리 (76)
    • Git, Github (3)
    • 학습 계획 (36)
    • 코드스쿼드 학습일지 (19)
    • Servlet (5)
    • VPC (2)
    • AWS (4)
    • JPA (5)
    • 취미생활 (2)
    • 프로젝트 기록 (7)
      • Issue Tracker 삽질 기록 (5)
      • 당근마켓 API 서버 기록 (2)
      • 나만의 블로그 제작 기록 (0)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 완전탐색
  • MVC
  • 재귀
  • 백준
  • EC2
  • 인프런
  • 문자열
  • Stack
  • dfs
  • sql
  • 면접을 위한 CS 전공지식 노트
  • 해시
  • 주간 학습 계획
  • 정렬
  • 자료구조
  • 파이썬
  • hash
  • BFS
  • JPA
  • JSP
  • 이코테
  • github
  • 프로그래머스
  • GIT
  • java
  • 이분탐색
  • MySQL
  • AWS
  • 자바
  • 알고리즘

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Tany

백문이불어일Tany

[Spring] GitHub OAuth2.0 구현하기 (웹 버전)
SPRING

[Spring] GitHub OAuth2.0 구현하기 (웹 버전)

2022. 5. 23. 23:54

출처) 구글 이미지


소셜 로그인 서비스를 구현하기 위해 GitHub API에서 제공하는 OAuth2.0 을 스프링 시큐리티 없이 구현해보려고 한다. ☺️

기록 시작 ~

 

신규 OAuth API 등록하기

가장 먼저 할 일은, GitHub 에 접속하고 Setting -> Developer settings -> oAuth App -> new Oauth app 버튼을 클릭해서

나의 OAuth App 을 등록해야 한다.

버튼을 클릭하면 어려가지 정보를 작성핼 수 있는데, 필수 칸은 무조건 적어줘야 한다.

1. Application name (필수)

- 원하는 어플리케이션 이름을 적어주면 된다.

2. Homepage URL (필수)

- 개발하는 웹 페이지의 홈페이지 주소를 적어준다.

3. Application description (생략 가능)

- 설명을 간략하게 적어준다.

4. Authorization callback URL (필수)

- https://http:github.com/login/oauth/authorize 를 거쳐서 임시 코드(Authorization Code) 를 받아내는 주소를 적어준다.

API 등록 후

필수 정보를 모두 기입하고 등록에 성공했다면 가장 중요한 정보인 Client ID, Client secrets 를 얻을 수 있다.

(원래 Client secrets 정보는 아무한테도 보여주며 안된다 ㅎ.ㅎ ☺️)

Spring Boot 로 구현하기

기록을 해서 다시 봐야하니까 그 때도 빠르게 이해할 수 있도록 최대한 쉽게 구현해봤다.

MVC 패턴을 사용해서 Controller / Service / Repository 등 클래스, 인터페이스를 생성해서 진행했다.

 

1. application.yml 파일 설정

객체에서 @Value 어노테이션을 사용해 편리하게 값을 사용하기 위해 미리 정의해주는 값.

client-id, client-secret 는 OAuth API 를 만들고 받은 자신의 값을 적어주면 된다. 나머지 값들은 GitHub OAuth 에서 정의해준

값들이니 요거 그대로 적어도 무방하다 !

spring:
    ... 중략
(spring 과 같은 Index)
oauth2:
  user:
    github:
      client-id: ~
      client-secret: ~
      login-url: https://github.com/login/oauth/authorize
      redirect-url: http://localhost:8080/login/oauth
      token-url: https://github.com/login/oauth/access_token
      user-url: https://api.github.com/user

 

2. 발급받은 AccessToken 정보를 저장할 DTO 클래스 생성

공식문서에 따르길, Json 타입으로 AccessToken 을 받으면 요런 형태로 응답을 받을 수 있다.

따라서 AccessToken 을 저장하는 인스턴스의 변수도 해당 Json 데이터에 맞춰서 생성하면 된다.

Json 파싱을 위해 생성자에 @JsonCreator, 생성자 매개변수에 @JsonProperty 어노테이션을 추가한다. (없으면 파싱 에러 🥲)

{
    "access_token":"~",
      "token_type":"bearer"
}
@Getter
public class AccessToken {

    private final String accessToken;
    private fianl String tokenType;

    @JsonCreator
    public AccessToken(
        @JsonProperty("access_token") String accessToken,
        @JsonProperty("token_type") String tokenType) {

    this.accessToken = accessToken;
    this.tokenType = tokenType;
    }
}

 

3. 사용자 수락 까지 LoginController

LoginContorller 와 LoginService 의 의존관계를 생성자를 통해 설정한다.

최초 사용자가 /login 으로 요청을 보내면, 서버에서 ClientID, RedirectUrl 등 정보를 OAuth 쪽으로 보내서 임시 Code 를 받아올 수

있다. 이 작업을 LoginService 의 getCode() 에서 진행하도록 하자.

@RestController
public class LoginController {

    private final LoginService loginService;

    public LoginController(LoginService loginService) {
        this.loginService = loginService;
    }

    @GetMapping("/login")
    public RedirectView getCode(RedirectAttributes redirectAttributes) {
        return loginService.requestCode(redirectAttributes)
    }
}

 

4. 사용자 수락 까지 LoginService

application.yml 파일에 미리 정의한 값들을 @Value 어노테이션을 사용해 가져올 수 있다. (생성자를 통해 인스턴스 변수 셋팅)

지금 단계에서는 clientId, redirect_uri (필수 값), state (권장 값) 을 GET https://github.com/login/oauth/authorize (loginUrl)

경로로 보내면 된다. state 값은 필수 값은 아니지만, 추측할 수 없는 임의의 랜덤 값으로, 요청 위조 공격으로부터 보호하는데 사용할 수

있는 값이기 때문에 나는 넣어줬다.

requestCode() 메서드가 종료되면서 loginUrl 경로로 리다이렉트 !

 

String clientId - GitHub 에서 받은 내 클라이언트 ID

String redirectUrl - 사용자의 승인 후, 전송되는 URL

String state - 임의의 랜덤 값 (여기서는 UUID 를 생성해서 셋팅하고 사용한다.)

@Service
public class LoginService {

    private String clientId;
    private String redirectUrl;
    private String loginUrl;
    private String state;

    public LoginService(
        @Value("${oauth2.user.github.client-id}") String clientId,
        @Value("${oauth2.user.github.redirect-url}") String redirectUrl,
        @Value("${oauth2.user.github.login-url}") String loginUrl) {

        this.clientId = clientId;
        this.redirectUrl = redirectUrl;
        this.loginUrl = loginUrl;
        this.state = UUID.randomUUID().toString();
    }

    public RedirectView requestCode(RedirectAttributes redirectAttributes) {
        redirectAttributes.addAttribute("client_id", clientId);
            redirectAttributes.addAttribute("redirect_url", redirectUrl);
            redirectAttributes.addAttribute("state", state);

        return new RedirectView(loginUrl);
    }
}

 

5. 사용자의 수락 이후 토큰 발매 까지 LoginContorller

loginUrl 로 리다이렉트 하고 드디어 임시 Code 를 받을 수 있는데, 나는 위 과정에서 String state 값 또한 보냈기 때문에 같이 받게 된다.

쿼리 스트링 형태로 임시 Code 등의 정보가 오게되는데, API 설정 시 Callback URL 뒤에 붙어져서 보내지게 된다.

(내 Callback URL 은 http://localhost:8080/login/oauth 이니까, /login/oauth?code= ~ & state= ~ 이런식으로)

따라서 Callback URL 를 컨트롤러를 하나 만들어주고 @RequestParam 어노테이션을 사용해 code, state 를 사용할 수 있도록 한다.

... 코드 중략

@GetMapping("/login/oauth")
public ResponseEntity<HttpHeaders> getToken(
    @RequestParam String code,
    @Requestparam String state,
    HttpServletRequest request) {

    ... 중략
}

 

6. LoginController 의 getToken() / LoginService 의 getAccessToken() 내부 구현

이제 임시 code 를 사용해 AccessToken 을 발급받아보자.

공식 문서에 따르면, 사용자의 수락 후 POST https://github.com/login/oauth/access_token 로 다시 리다이렉션 하라고 하는데,

이 때 필수 매개변수로 clientID, clientSecret, code, redirectUrl 을 함께 보내주면 AccessToken 의 정보를 얻을 수 있다.

 

1. 필수 매개변수는 Map<String, String> 을 선언해 넣어준다.

2. 미리 만들어놓은 AccessToken DTO 에 Json 타입으로 받은 데이터를 파싱한다.

3. Json 으로 데이터를 받아오기 위해 HttpHeader 를 선언해 Accept 를 Json 으로 설정한다. (내부에 List 를 받는거라 List.of 사용)

4. HttpEntity 를 선언해 필수 매개변수를 담은 headers, bodies 를 담는다.

5. POST https://github.com/login/oauth/access_token 으로 요청을 담아 보낸 뒤, 응답의 body 를 리턴하면 끝.

6. 이제 AccessToken DTO 에 값이 잘 담겨있는 것을 확인할 수 있다.

--- LoginController ---

@GetMapping("/login/oauth")
public ResponseEntity<HttpHeaders> getToken(
    @RequestParam String code,
    @RequestParam String state,
    HttpServeltRequest request) {

	AccessToken accessTokenInfo = loginService.getAccessToken(code, state);   
}


--- LoginService ---

public AccessToken getAccessToken(String code, String state) {
    Map<String, String> bodies = new HashMap<>();
    bodies.put("client_id", clientId);
    bodies.put("client_secret", clientSecret);
    bodies.put("code", code);
    bodies.put("state", state);
    
    HttpHeaders headers = new HttpHeaders();
    headers.setAccept(List.of(MediaType.APPLICATION_JSON);
    
    HttpEntity<Object> request = new HttpEntity<>(bodies, headers);
    ResponseEntity<AccessToken> response = new RestTemplate()
        .postForEntity(tokenUrl, request, AccessToken.class);
        
    return response.getBody();
}

 

7. AccessToken 을 활용해 해당 유저의 GItHub 정보를 불러오기

이제 받아온 토큰을 활용해 GItHub 정보를 가져와보자.

AccessToken DTO 와 비슷하게 DTO 를 하나 더 생성한다.

인스턴스 변수와 파싱하는 변수 이름이 서로 다른데, 이건 GitHub OAuth 에서 보내주는 값이 알아보기 힘들어서 임의로 바꾼 것 ㅎㅎ ☺️..

OAuth 에서는 login 값이 username, id 값이 유저 일련번호, name 값이 별명으로 오기 때문에 좀 알아보기 쉽게 바꿔줬다.

예를 들면, login = juni8453, id = 1253123, name = Jeon(Tany) 이런 식으로 ..

@Getter
public class GitHubUserInfo() {

    private final String username;
    private final String idNumber;
    private final String nickname;
    
    @JsonCreator
    public GitHubInfo(
        @JsonProperty("login") String username,
        @JsonProperty("id") String idNumber,
        @JsonProperty("name") String nickname) {
    
        this.username = username;
        this.idNumber = idNumber;
        this.nicknam = nickname;
    }
}

 

다시 LoginController 의 getToken() 메서드에 Github 유저의 정보를 가져오는 로직을 추가하고, LoginService 에서

getGitHubUserInfo() 메서드를 추가하자.

공식 문서에 따르면,

1. Header 에 Authorization 을 아래 형식으로 추가하고 2번 URL 로 보낸다.

2. GET https://api.github.com/user  

따라서 HttpHeaders 를 선언하고 똑같은 형식으로 나의 AccessToken 을 담아주고 GET 방식으로 Json 데이터를 파싱받아오기 위해

위에서 AccessToken 을 파싱하기 위해 사용한 postForEntity() 가 아닌 exchage() 메서드를 사용해 GitHubUserInfo 에 파싱한다.

(내부 인자로 Httpmethod.GET 을 꼭 넣어줘야 한다 !)

--- LoginController

@GetMapping("/login/oauth")
    public ResponseEntity<HttpHeaders> getToken(
        @RequestParam String code, 
        @RequestParam String state, 
        HttpServletRequest request) {

        AccessToken accessTokenInfo = loginService.getAccessToken(code, state);
        
        GitHubUserInfo gitHubUserInfo = loginService.getGitHubUserInfo(accessTokenInfo);

        ... 중략
    }
    

--- LoginService
public GitHubUserInfo getGitHubUserInfo(AccessToken accessTokenInfo) {
  HttpHeaders headers = new HttpHeaders();
  headers.set("Authorization", "token " + accessTokenInfo.getAccessToken());
  
  HttpEntity<Object> request = new HttpEntity<>(headers);
  ResponseEntity<GitHubUserInfo> response = new RestTemplate()
    .exchage(userUrl, Httpmethod.GET, request, GitHubUserInfo.class);
    
  return response.getBody();
}

여기까지 잘 진행됐으면 AccessToken 과 GitHubUserInfo DTO 내부에 값이 잘 저장됐다. 

이제 진짜 로그인을 위해 Session 을 만들고 값을 셋팅한 뒤 DB 에 유저를 저장해보자.

 

1.  Session 셋팅

HttpSession 객체를 사용해 간단히 Session 을 만들 수 있다.

만든 Session 내부에 GitHubUserInfo DTO 의 값을 원하는 Key 값으로 저장하면 된다.

@GetMapping("/login/oauth")
    public ResponseEntity<HttpHeaders> getToken(
        @RequestParam String code, 
        @RequestParam String state, 
        HttpServletRequest request) {

        AccessToken accessTokenInfo = loginService.getAccessToken(code, state);
        
        GitHubUserInfo gitHubUserInfo = loginService.getGitHubUserInfo(accessTokenInfo);

        HttpSession session = new HttpSession();
        session.setAttribute("userInfo", gitHubUserInfo);
        
        ... 중략
    }

 

2. DB 에 User 저장하기

DB 에 저장하기 전에 User Entity 를 먼저 만들어주자. 나는 Spring Data JPA 를 사용했다.

학습용이니까 간단하게 PK 값인 id, 이름을 담을 수 있게 username 만 필드에 선언하고 생성자 인자로 GitHubUserInfo 객체를

받을 수 있도록 하자.

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
  
  @Id @GeneratedValue
  private Long id;
  
  @Column(nullable = false)
  private String username
  
  public User(GitHubUserInfo gitHubUserInfo) {
    this.username = gitHubUserInfo.getId();
  }
}

 

JpaRepository 인터페이스를 상속받는 UserRepository 인터페이스를 생성해주자. (JPA 을 사용하기 때문에)

나중에 로그인 검증을 위해 사용하는 쿼리 메서드인 findByUsername() 를 미리 만들어놓자.

public interface UserRepository extends JpaRepository<User, Long> {

  Optional<User> findByUsername(String username);
}

 

LoginController 로 돌아와서 인자로 GitHubUserInfo 객체를 가진 loginService.saveMember() 메서드를 호출한다.

LoginService 에서는 의존 주입을 위해 인스턴스 변수, 생성자에 UserRepository 를 추가한다. 

이제 LoginController 에서 saveMember() 메서드가 호출되면서,

1.  인자로 받아온 GitHubUserInfo 객체를 활용해 새로운 User 객체를 생성한다.

2. 리다이렉트를 위해 HttpHeaders 객체를 생성하고, Location 을 홈페이지로 맞춰준다.

3. header 를 ResponseEntity 객체에 넣고 return !

(HttpStatus.SEE_OTHER 은 리다이렉트를 위해 넣어준 값인데, header 에 Location 값이 있는데 굳이 넣어줘야하는지 ..? 정확히

몰라서 좀 더 알아봐야 하는 부분 😵‍💫)

--- LoginController

... 중략

@GetMapping("/login/oauth")
    public ResponseEntity<HttpHeaders> getToken(
        @RequestParam String code, 
        @RequestParam String state, 
        HttpServletRequest request) {

        AccessToken accessTokenInfo = loginService.getAccessToken(code, state);
        
        GitHubUserInfo gitHubUserInfo = loginService.getGitHubUserInfo(accessTokenInfo);

        HttpSession session = new HttpSession();
        session.setAttribute("userInfo", gitHubUserInfo);
        
        return loginService.saveMember(gitHubUserInfo);
    }
    
 
 --- LoginService
 
 private final UserRepository userRepository;
 ... 중략
 
 public LoginService (
   UserRepository userRepository,
   ... 중략) {
 
   this.userRepository = userRepository;
   ... 중략
 }
 
 
 
 public ResponseEntity<HttpHeaders> saveMember(GitHubUserInfo gitHubUserInfo) {
   userRepository.save(new User(gitHubUserInfo));
   
   HttpHeaders headers = new HttpHeaders();
   headers.setLocation(URI.create("http://localhost:8080/"));
   
   return new ResponseEntity<>(headers, HttpStatus.SEE_OTHER);
 }

 

3. Session 검증

무탈히 진행되었다면, 서버에 Session 이, DB 에 새로운 User 가 저장되면서 "localhost:8080/" URL 로 리다이렉트 될 것이다.

이제 해당 Session 을 검증하는 과정을 진행해보자.

먼저 "localhost:8080:/" 을 매핑하는 Controller 를 하나 만들어준다. 검증 부분에서는 두 번의 검증을 실시하게 된다.

1. 첫 번째 검증 - 세션이 존재하는가 ?

2. 두 번째 검증 - 또는 Session 값에서 얻어온 Username 과 일치하는 User 가 존재하는가 ?

 

만약, 무사히 해당 분기문을 통과하게 되면 Home Page 로 이동하고, 그렇지 못하면 다시 로그인 페이지로 이동하게 된다.

이 부분은 GlobalException 처리를 통해 예외처리를 할 수도 있는데, 이 부분이 중요한게 아니라고 판단해서 슈도 코드 처럼 짜봤다.

아마 Interceptor 를 활용할 수도 있을 것 같다 !

@GetMapping("/")
public ResponseEntity<String> home(HttpServletResponse response, HttpSession session) {
  GitHubUserInfo checkSession = (GitHubUserInfo) session.getAttribute("userInfo");
  
  if((checkSession == null) || 
       userRepository.findByUsername(checkSession.getUsername()).isEmpty()) {
         try {
           response.sendRedirect("http://localhost:8080/loginPage");  
         } catch(Exception e) {
           e.printStackTrace();
         }
       }
       
       return ResponseEntity.ok("Home Page 입니다.");
}

저번 프로젝트 때 공부한 GitHub OAuth2.0 를 정리해봤는데, 아직 모자란 부분이 많고 프론트 하고 통신을 해보지 않아서 추가로 알게될

부분이 많은 것 같다. 그래도 일단 쭉 사이클을 돌려서 구현해봤다는 것에 의의를 두기로 ~~ 😃

'SPRING' 카테고리의 다른 글

Custom Exception 활용법  (0) 2023.06.10
[Spring Security] Authentication 인증 처리 과정 알아보기  (0) 2022.12.17
DTO 반환에 대해  (4) 2022.10.13
Spring @Validation 을 통한 검증, 예외처리 도전기  (0) 2022.04.16
    'SPRING' 카테고리의 다른 글
    • Custom Exception 활용법
    • [Spring Security] Authentication 인증 처리 과정 알아보기
    • DTO 반환에 대해
    • Spring @Validation 을 통한 검증, 예외처리 도전기
    Tany
    Tany
    내가 보려고 만드는 백엔드 기록장 Github https://github.com/juni8453

    티스토리툴바