본문 바로가기
Backend/Spring Boot

[OAuth2] 03. GitHub 로그인 구현하기(Spring boot) (2) GitHub API로 token, 사용자 정보 불러오기

by 김파치 2023. 10. 23.

시작하기 전에 대략적인 폴더 구조는 다음과 같다.

여기에 config 파일까지 하면 로그인 완성이다!

 

 

먼저 프론트에서 github api를 사용해서 로그인을 한 후, github에서 redirect-url로 보내준 code를 백엔드로 던져준다.

 

그럼 백엔드에서는 code를 사용해서 github api를 사용해서 token과 사용자 정보를 받는다.

 

받은 token을 그대로 사용하면 보안 문제가 있을 것 같아서 jwt token을 자체적으로 만들어서 사용하는 로직을 짰다.

 

 

0. UserDto, UserRepository, JwtService

 

UserDto, UserRepository, JwtService는 아래와 같이 구현했다.

 

OauthService 참고용..!

 

 

 

 

1. UserDto

 

package com.d103.dddev.api.user.repository.dto;

import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;

import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.DynamicInsert;

import com.d103.dddev.api.common.oauth2.Role;
import com.d103.dddev.api.file.repository.dto.ProfileDto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Entity(name = "user")
@Getter
@Setter
@DynamicInsert
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;

	@OneToOne
	@JoinColumn(name = "profile_id")
	private ProfileDto profileDto;

	@Column(name = "github_id")
	private Integer githubId;

	private String nickname;

	@Column(name = "status_msg")
	private String statusMsg;

	@CreationTimestamp
	@JoinColumn(name = "create_time")
	private Date createTime;

	private Boolean valid;

	@Column(name = "refresh_token")
	private String refreshToken;

	@Column(name = "personal_access_token")
	private String personalAccessToken;

	@Enumerated(EnumType.STRING)
	private Role role;

	public void updateRefreshToken(String updateRefreshToken){
		this.refreshToken = updateRefreshToken;
	}

}

 

 

 

2.  UserRepository

 

package com.d103.dddev.api.user.repository;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;

import com.d103.dddev.api.user.repository.dto.UserDto;

public interface UserRepository extends JpaRepository<UserDto, Integer> {
	Optional<UserDto> findByGithubId(Integer githubId);
	Optional<UserDto> findByRefreshToken(String refreshToken);
	Optional<UserDto> findByIdNotAndNickname(Integer id, String nickname);	// id != not and nickname = nickname

}

 

 

 

3. JwtService

 

해당 코드는 아래 블로그를 참고하였다..!

 

 

Spring Security + JWT를 이용한 자체 Login & OAuth2 Login(구글, 네이버, 카카오) API 구현 (3) - JWT 관련 클래

이전에 JWT 정의를 살펴봤다면, 이번에는 JWT 관련 클래스를 직접 생성하여 구현해보려고 합니다! 들어가기 전 JWT 패키지 구조는 다음과 같습니다. JWT 서비스를 생성하기 위해 다음과 같은 오픈

ksh-coding.tistory.com

 

package com.d103.dddev.api.common.oauth2.utils;

import java.util.Date;
import java.util.Optional;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.d103.dddev.api.user.repository.UserRepository;
import com.d103.dddev.api.user.repository.dto.UserDto;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Service
@RequiredArgsConstructor
@Getter
@Slf4j
public class JwtService {
	@Value("${jwt.secretKey}")
	private String secretKey;

	@Value("${jwt.access.expiration}")
	private Long accessTokenExpirationPeriod;

	@Value("${jwt.refresh.expiration}")
	private Long refreshTokenExpirationPeriod;

	@Value("${jwt.access.header}")
	private String accessHeader;

	@Value("${jwt.refresh.header}")
	private String refreshHeader;

	/**
	 * JWT의 Subject와 Claim으로 email 사용 -> 클레임의 name을 "email"으로 설정
	 * JWT의 헤더에 들어오는 값 : 'Authorization(Key) = Bearer {토큰} (Value)' 형식
	 */
	private static final String ACCESS_TOKEN_SUBJECT = "AccessToken";
	private static final String REFRESH_TOKEN_SUBJECT = "RefreshToken";
	private static final String GITHUB_CLAIM = "githubId";
	private static final String BEARER = "Bearer ";

	private final UserRepository userRepository;

	/**
	 * AccessToken 생성 메소드
	 */
	public String createAccessToken(Integer githubId) {
		Date now = new Date();
		return JWT.create()    // JWT 토큰을 생성하는 빌더 반환
			.withSubject(ACCESS_TOKEN_SUBJECT) // JWT의 subject 지정 -> accessToken
			.withExpiresAt(new Date(now.getTime() + accessTokenExpirationPeriod)) // 토큰 만료 시간 설정
			//클레임으로 uid 사용.
			//추가적으로 식별자나, 이름 등의 정보를 더 추가하셔도 됩니다.
			//추가하실 경우 .withClaim(클래임 이름, 클래임 값) 으로 설정해주시면 됩니다
			.withClaim(GITHUB_CLAIM, githubId)    // 깃허브 아이디 클레임
			.sign(Algorithm.HMAC512(secretKey)); // HMAC512 알고리즘 사용, application-jwt.yml에서 지정한 secret 키로 암호화
	}

	/**
	 * RefreshToken 생성
	 * RefreshToken은 Claim에 email도 넣지 않으므로 withClaim() X
	 */
	public String createRefreshToken() {
		Date now = new Date();
		return JWT.create()
			.withSubject(REFRESH_TOKEN_SUBJECT)
			.withExpiresAt(new Date(now.getTime() + refreshTokenExpirationPeriod))
			.sign(Algorithm.HMAC512(secretKey));
	}

	/**
	 * AccessToken 헤더에 실어서 보내기
	 */
	public void sendAccessToken(HttpServletResponse response, String accessToken) {
		response.setStatus(HttpServletResponse.SC_OK);

		response.setHeader(accessHeader, accessToken);
		log.info("재발급된 Access Token : {}", accessToken);
	}

	/**
	 * AccessToken + RefreshToken 헤더에 실어서 보내기
	 */
	public void sendAccessAndRefreshToken(HttpServletResponse response, String accessToken, String refreshToken) {
		response.setStatus(HttpServletResponse.SC_OK);

		setAccessTokenHeader(response, accessToken);
		setRefreshTokenHeader(response, refreshToken);
		log.info("sendAccessAndRefreshToken :: Access Token, Refresh Token 헤더 설정 완료");
	}

	/**
	 * 헤더에서 RefreshToken 추출
	 * 토큰 형식 : Bearer XXX에서 Bearer를 제외하고 순수 토큰만 가져오기 위해서
	 * 헤더를 가져온 후 "Bearer"를 삭제(""로 replace)
	 */
	public Optional<String> extractRefreshToken(HttpServletRequest request) {
		return Optional.ofNullable(request.getHeader(refreshHeader))
			.filter(refreshToken -> refreshToken.startsWith(BEARER))
			.map(refreshToken -> refreshToken.replace(BEARER, ""));
	}

	/**
	 * 헤더에서 AccessToken 추출
	 * 토큰 형식 : Bearer XXX에서 Bearer를 제외하고 순수 토큰만 가져오기 위해서
	 * 헤더를 가져온 후 "Bearer"를 삭제(""로 replace)
	 */
	public Optional<String> extractAccessToken(HttpServletRequest request) {
		log.info("extractAccessToken");
		//System.out.println(request.getHeader(accessHeader));
		return Optional.ofNullable(request.getHeader(accessHeader))
			.filter(accessToken -> accessToken.startsWith(BEARER))
			.map(accessToken -> accessToken.replace(BEARER, ""));
	}

	/**
	 * Bearer __________형식으로 되어있는 accessToken에서 Bearer를 제거하는 함수
	 * */
	public Optional<String> extractAccessHeaderToToken(String Authorization) {
		if (Authorization.startsWith(BEARER)) {
			return Optional.ofNullable(Authorization)
				.map(token -> token.replace(BEARER, ""));
		} else {
			return Optional.of(Authorization);
		}
		// return Optional.ofNullable(accessToken)
		// 	.filter(token -> token.startsWith(BEARER))
		// 	.map(token -> token.replace(BEARER, ""));
	}

	/**
	 * AccessToken에서 githubId 추출
	 * 추출 전에 JWT.require()로 검증기 생성
	 * verify로 AceessToken 검증 후
	 * 유효하다면 getClaim()으로 githubId 추출
	 * 유효하지 않다면 빈 Optional 객체 반환
	 */
	public Optional<Integer> extractGithubId(String Authorization) {
		try {
			String accessToken = extractAccessHeaderToToken(Authorization).get();
			return Optional.ofNullable(JWT.require(Algorithm.HMAC512(secretKey))
				.build()
				.verify(accessToken)
				.getClaim(GITHUB_CLAIM)
				.asInt());
		} catch (Exception e) {
			log.error("extractEmail :: 액세스 토큰이 유효하지 않습니다.");
			e.printStackTrace();
			return Optional.empty();
		}
	}

	public Optional<UserDto> getUser(String Authorization) throws Exception {
		Integer githubId = extractGithubId(Authorization).orElseThrow(() -> new NoSuchFieldException("깃허브 아이디가 없습니다."));
		return userRepository.findByGithubId(githubId);
	}

	/**
	 * AccessToken 헤더 설정
	 */
	public void setAccessTokenHeader(HttpServletResponse response, String accessToken) {
		response.setHeader(accessHeader, accessToken);
	}

	/**
	 * RefreshToken 헤더 설정
	 */
	public void setRefreshTokenHeader(HttpServletResponse response, String refreshToken) {
		response.setHeader(refreshHeader, refreshToken);
	}

	/**
	 * RefreshToken DB 저장(업데이트)
	 */
	public void updateRefreshToken(Integer id, String refreshToken) {
		userRepository.findById(id)
			.ifPresentOrElse(
				user -> {
					user.updateRefreshToken(refreshToken);
					userRepository.saveAndFlush(user);
				},
				() -> new Exception("updateRefreshToken :: 일치하는 회원이 없습니다.")
			);
	}

	public boolean isTokenValid(String token) {
		try {
			JWT.require(Algorithm.HMAC512(secretKey)).build().verify(token);
			return true;
		} catch (Exception e) {
			log.error("유효하지 않은 토큰입니다. {}", e.getMessage());
			return false;
		}
	}

}

 

 

 

 

1. 프론트에서 받은 code로 github에서 토큰 받기

 

1. Oauth2Service.java

 

application.properties에 선언한 변수들을 불러오고, api 호출에 사용할 변수를 선언한다.

 

 

 

login() 전체 코드

 

public Map<String, String> login(String code) throws Exception {
    log.info("service - login :: github api login 진입");
    // github에서 access, refresh token 받아오기
    Map<String, String> response = githubToken(code);
    String githubAccessToken = response.get("access_token");
    String githubRefreshToken = response.get("refresh_token");

    System.out.println(githubAccessToken);

    // 사용자 정보 받아오기
    Map<String, Object> userInfo = getUserInfo(githubAccessToken);
    //System.out.println(userInfo);
    String name = (String)userInfo.get("login");
    Integer githubId = (Integer)userInfo.get("id");

    // jwt로 자체 access, refresh token 만들기
    String accessToken = BEARER + jwtService.createAccessToken(githubId);
    String refreshToken = BEARER + jwtService.createRefreshToken();

    UserDto userDto = getUser(githubId).orElseGet(() -> saveUser(userInfo));

    // refresh token db 저장
    updateRefreshToken(userDto, refreshToken);

    // access, refresh token, 이름
    Map<String, String> map = new HashMap<>();
    map.put("Authorization", accessToken);
    map.put("Authorization-refresh", refreshToken);
    map.put("name", name);
    map.put("role", String.valueOf(userDto.getRole()));

    log.info("login :: github api login 성공");

    return map;
}

 

 

github에서 access, refresh token을 받아오는 githubToken() 메소드를 호출한다.

 

받아온 access, refresh 토큰을 각각 githubAccessToken, githubRefreshToken에 저장한다.

 

사실 refresh token은 사용하지는 않지만 일단 변수로 빼놓았다!

 

// github에서 access, refresh token 받아오기
Map<String, String> response = githubToken(code);
String githubAccessToken = response.get("access_token");
String githubRefreshToken = response.get("refresh_token");

 

 

githubAccessToken을 사용해서 사용자 정보를 받아온다.

 

받아온 이름과 githubId를 변수에 저장한다.

 

// 사용자 정보 받아오기
Map<String, Object> userInfo = getUserInfo(githubAccessToken);
//System.out.println(userInfo);
String name = (String)userInfo.get("login");
Integer githubId = (Integer)userInfo.get("id");

 

 

githubId를 사용해서 jwt로 자체 access, refresh token을 만든다!

 

그리고 githubId를 사용해서 db에서 해당 githubId와 일치하는 사용자가 있으면 dto를 불러오고 없으면 db에 저장하고 dto를 반환한다!

 

그리고 refresh token을 db에 저장한다.

 

// jwt로 자체 access, refresh token 만들기
String accessToken = BEARER + jwtService.createAccessToken(githubId);
String refreshToken = BEARER + jwtService.createRefreshToken();

UserDto userDto = getUser(githubId).orElseGet(() -> saveUser(userInfo));

// refresh token db 저장
updateRefreshToken(userDto, refreshToken);

 

 

그리고 생성한 access token, refresh token, name, role을 map에 저장하고 반환한다.

 

자세한 부분은 추후 설명한댜!

 

// access, refresh token, 이름
Map<String, String> map = new HashMap<>();
map.put("Authorization", accessToken);
map.put("Authorization-refresh", refreshToken);
map.put("name", name);
map.put("role", String.valueOf(userDto.getRole()));

log.info("login :: github api login 성공");

return map;

 

 

 

githubToken 전체 코드 : code를 사용해서 github api를 호출해 access, refresh token을 받아오는 함수

 

 

api docs는 아래 링크의 3번 문항을 참고하면 된다! (찾는데 한참 걸렸다아...ㅜㅜ)

 

Generating a user access token for a GitHub App - GitHub Docs

You can generate a user access token for your GitHub App in order to attribute app activity to a user.

docs.github.com

 

public Map<String, String> githubToken(String code) throws Exception {
    log.info("service - gethubToken :: github에서 token 받아오기 진입");
    RestTemplate restTemplate = new RestTemplate();

    // header 만들기
    HttpHeaders headers = new HttpHeaders();
    headers.add("Content-Type", "application/x-www-form-urlencoded");

    // body 만들기
    MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
    params.add("client_id", CLIENT_ID);
    params.add("client_secret", CLIENT_SECRET);
    params.add("code", code);
    params.add("basicAuth", false);

    // header랑 body 합치기
    HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(params, headers);

    // post 요청
    ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
        ACCESS_TOKEN_REQUEST_URL,
        HttpMethod.POST,
        entity,
        new ParameterizedTypeReference<Map<String, Object>>() {
        }
    );

    Map<String, Object> responseBody = response.getBody();

    Map<String, String> tokens = new HashMap<>();
    tokens.put("access_token", (String)responseBody.get("access_token"));
    tokens.put("refresh_token", (String)responseBody.get("refresh_token"));

    return tokens;
}

 

 

RestTemplate 객체를 생성한다.

 

HttpHeaders 객체를 생성 후, content type을 아래와 같이 설정한다.

 

RestTemplate restTemplate = new RestTemplate();

// header 만들기
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "application/x-www-form-urlencoded");

 

body로 보낼 MultiValueMap 객체를 생성한다.

 

body에는 client_id, client_secret, code를 추가하고 basicAuth를 false로 설정한다.

 

client_id, client_secret은 사전설정에서 github app에서 받은 것을 말한다. 

 

code는 프론트에서 받아온 것!

// body 만들기
MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
params.add("client_id", CLIENT_ID);
params.add("client_secret", CLIENT_SECRET);
params.add("code", code);
params.add("basicAuth", false);

 

 

HttpEntity에 header랑 body를 합친 후 post요청을 보낸다.

 

// header랑 body 합치기
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(params, headers);

// post 요청
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
    ACCESS_TOKEN_REQUEST_URL,
    HttpMethod.POST,
    entity,
    new ParameterizedTypeReference<Map<String, Object>>() {
    }
);

 

 

response는 다음과 같다!

{
	access_token=ghu_{},
    expires_in=28800, 
    refresh_token=ghr_{}, 
    refresh_token_expires_in=15811200, 
    token_type=bearer, 
    scope=
}

 

response에서 access token과 refresh token을 가져와서 tokens라는 map에 넣고 return 한다.

 

Map<String, Object> responseBody = response.getBody();

Map<String, String> tokens = new HashMap<>();
tokens.put("access_token", (String)responseBody.get("access_token"));
tokens.put("refresh_token", (String)responseBody.get("refresh_token"));

return tokens;

 

 

 

2. 받아온 access token으로 사용자 정보 받아오기

 

 

getUserInfo 전체 코드

 

 

이건 아무리 찾아봐도.... 공식문서를 못찾아서... 헣.... chatGPT와 다른 블로그들의 도움을 받았당 히히

 

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

header : { "Authorization" : "Bearer" + {github access token} }

 

으로 요청하면 해당 사용자의 사용자 정보를 받아올 수 있다!

 

public Map<String, Object> getUserInfo(String githubAccessToken) throws Exception {
    log.info("servie - getUserInfo :: github api로 사용자 정보 받아오기");
    RestTemplate restTemplate = new RestTemplate();

    String userInfoUrl = API_URL + "/user";

    // header
    HttpHeaders headers = new HttpHeaders();
    String userInfoAccessToken = USER_INFO_REQUEST_TOKEN + githubAccessToken;
    headers.add("Authorization", userInfoAccessToken);

    // HttpEntity
    HttpEntity<String> entity = new HttpEntity<>(headers);

    ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
        userInfoUrl,
        HttpMethod.GET,
        entity,
        new ParameterizedTypeReference<Map<String, Object>>() {
        }
    );

    return response.getBody();
}

 

이전과 똑같이 RestTemplate을 선언한다.

 

userInfoUrl에 사용할 url을 넣고

 

headers에 access token을 넣어준댜

 

RestTemplate restTemplate = new RestTemplate();

String userInfoUrl = API_URL + "/user";

// header
HttpHeaders headers = new HttpHeaders();
String userInfoAccessToken = USER_INFO_REQUEST_TOKEN + githubAccessToken;
headers.add("Authorization", userInfoAccessToken);

 

header를 entity에 넣고 GET 요청을 보낸다.

 

// HttpEntity
HttpEntity<String> entity = new HttpEntity<>(headers);

ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
    userInfoUrl,
    HttpMethod.GET,
    entity,
    new ParameterizedTypeReference<Map<String, Object>>() {
    }
);

return response.getBody();

 

 

 

 

response.getBody()를 출력해보면 아래와 같이 나온다!

 

login이 github에 설정한 닉네임? 같은 느낌이고 id는 github에서 사용자별로 제공하는 id인 것 같다.

 

나는 우리 프로젝트에서 login을 닉네임으로 사용하고 id를 JwtService.java에서  jwt token에 넣어서 사용자 구분용도로 사용하였다.

 

 

 

{
	login={사용자 이름},
	id={github에서 제공하는 id}, 
    node_id=__________, 
    avatar_url=https://avatars.githubusercontent.com/u/{id}?v=4, 
    gravatar_id=, 
    url=https://api.github.com/users/{login}, 
    html_url=https://github.com/{login}, 
    followers_url=https://api.github.com/users/{login}/followers, 
    following_url=https://api.github.com/users/{login}/following{/other_user}, 
    gists_url=https://api.github.com/users/{login}/gists{/gist_id}, 
    starred_url=https://api.github.com/users/{login}/starred{/owner}{/repo}, 
    subscriptions_url=https://api.github.com/users/{login}/subscriptions, 
    organizations_url=https://api.github.com/users/{login}/orgs, 
    repos_url=https://api.github.com/users/{login}/repos, 
    events_url=https://api.github.com/users/{login}/events{/privacy}, 
    received_events_url=https://api.github.com/users/{login}/received_events, 
    type=User, 
    site_admin=false, 
    name=null, 
    company=_____, 
    blog=, 
    location=null, 
    email=null, 
    hireable=null, 
    bio=null, 
    twitter_username=null, 
    public_repos=7, 
    public_gists=0, 
    followers=0, 
    following=0, 
    created_at=2017-11-24T01:04:27Z, 
    updated_at=2023-10-17T08:17:56Z
}

 

 

참고로 https://api.github.com/users/{login} 이걸로 요청보내면 해당 사용자의 사용자 정보를 간단하게 받아올 수 있다!

 

 

 

 

getUser() : githubId로 db에서 UserDto를 불러오는 메소드이다.

 

public Optional<UserDto> getUser(Integer githubId) throws Exception {
    log.info("getUser :: DB에서 사용자 정보 가져오기");
    return userRepository.findByGithubId(githubId);
}

 

 

 

 

saveUser() : getUserInfo에서 받아온 사용자 정보를 사용해서 DB에 저장하는 메소드이다.

 

public UserDto saveUser(Map<String, Object> userInfo) {
    log.info("saveUser :: DB에 사용자 정보 저장");
    String name = (String)userInfo.get("login");
    Integer githubId = (Integer)userInfo.get("id");

    UserDto user = UserDto.builder()
        .nickname(name)
        .githubId(githubId)
        .valid(true)
        .role(Role.GUEST)
        .build();

    return userRepository.save(user);
}

 

 

getUser()와 saveUser()는 login()에서 사용된다.

 

getUser()로 UserDto가 호출되면 기존 사용자이고, 없으면 새로 회원가입한 사용자이기 때문에 saveUser()로 사용자 정보를 저장해준다!

 

 

 

 

 

updateRefreshToken() : 로그인 후 refresh token을 db에 업데이트 해주는 메소드이다!

 

public void updateRefreshToken(UserDto user, String refreshToken) {
    user.updateRefreshToken(refreshToken);
    userRepository.saveAndFlush(user);
}

 

 

 

Oauth2Service.java 전체 코드

 

중간의 unlink는 로그아웃에 필요한 코드이다! 이건 로그아웃 포스팅에서 이야기할 것...!

package com.d103.dddev.api.common.oauth2.service;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import com.d103.dddev.api.common.oauth2.Role;
import com.d103.dddev.api.common.oauth2.utils.JwtService;
import com.d103.dddev.api.user.repository.UserRepository;
import com.d103.dddev.api.user.repository.dto.UserDto;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Service
@RequiredArgsConstructor
@Slf4j
public class Oauth2Service {

	private final JwtService jwtService;
	private final UserRepository userRepository;

	private String ACCESS_TOKEN_REQUEST_URL = "https://github.com/login/oauth/access_token";

	private String API_URL = "https://api.github.com";
	private String USER_INFO_REQUEST_TOKEN = "token ";

	private String BEARER = "Bearer ";

	@Value("${spring.security.oauth2.client.registration.github.client-id}")
	private String CLIENT_ID;

	@Value("${spring.security.oauth2.client.registration.github.client-secret}")
	private String CLIENT_SECRET;

	@Value("${aes.secretKey}")
	private String AES_SECRET_KEY;

	public Map<String, String> login(String code) throws Exception {
		log.info("service - login :: github api login 진입");
		// github에서 access, refresh token 받아오기
		Map<String, String> response = githubToken(code);
		String githubAccessToken = response.get("access_token");
		String githubRefreshToken = response.get("refresh_token");

		System.out.println(githubAccessToken);

		// 사용자 정보 받아오기
		Map<String, Object> userInfo = getUserInfo(githubAccessToken);
		System.out.println(userInfo);
		String name = (String)userInfo.get("login");
		Integer githubId = (Integer)userInfo.get("id");

		// jwt로 자체 access, refresh token 만들기
		String accessToken = BEARER + jwtService.createAccessToken(githubId);
		String refreshToken = BEARER + jwtService.createRefreshToken();

		UserDto userDto = getUser(githubId).orElseGet(() -> saveUser(userInfo));

		// refresh token db 저장
		updateRefreshToken(userDto, refreshToken);

		// access, refresh token, 이름
		Map<String, String> map = new HashMap<>();
		map.put("Authorization", accessToken);
		map.put("Authorization-refresh", refreshToken);
		map.put("name", name);
		map.put("role", String.valueOf(userDto.getRole()));

		log.info("login :: github api login 성공");

		return map;
	}

	public Map<String, String> githubToken(String code) throws Exception {
		log.info("service - gethubToken :: github에서 token 받아오기 진입");
		RestTemplate restTemplate = new RestTemplate();

		// header 만들기
		HttpHeaders headers = new HttpHeaders();
		headers.add("Content-Type", "application/x-www-form-urlencoded");

		// body 만들기
		MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
		params.add("client_id", CLIENT_ID);
		params.add("client_secret", CLIENT_SECRET);
		params.add("code", code);
		params.add("basicAuth", false);

		// header랑 body 합치기
		HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(params, headers);

		// post 요청
		ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
			ACCESS_TOKEN_REQUEST_URL,
			HttpMethod.POST,
			entity,
			new ParameterizedTypeReference<Map<String, Object>>() {
			}
		);

		Map<String, Object> responseBody = response.getBody();

		Map<String, String> tokens = new HashMap<>();
		tokens.put("access_token", (String)responseBody.get("access_token"));
		tokens.put("refresh_token", (String)responseBody.get("refresh_token"));

		return tokens;
	}

	public Map<String, Object> getUserInfo(String githubAccessToken) throws Exception {
		log.info("servie - getUserInfo :: github api로 사용자 정보 받아오기");
		RestTemplate restTemplate = new RestTemplate();

		String userInfoUrl = API_URL + "/user";

		// header
		HttpHeaders headers = new HttpHeaders();
		String userInfoAccessToken = USER_INFO_REQUEST_TOKEN + githubAccessToken;
		headers.add("Authorization", userInfoAccessToken);

		// HttpEntity
		HttpEntity<String> entity = new HttpEntity<>(headers);

		ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
			userInfoUrl,
			HttpMethod.GET,
			entity,
			new ParameterizedTypeReference<Map<String, Object>>() {
			}
		);

		return response.getBody();
	}

	public Boolean unlink(String oauthAccessToken) throws Exception {
		log.info("service - unlink :: github authorization 연결 끊기 진입");
		RestTemplate restTemplate = new RestTemplate();

		String deleteUrl = API_URL + "/applications/" + CLIENT_ID + "/grant";

		HttpHeaders headers = new HttpHeaders();
		headers.setBasicAuth(CLIENT_ID, CLIENT_SECRET);
		headers.setContentType(MediaType.APPLICATION_JSON);

		JSONObject requestBody = new JSONObject();
		requestBody.put("access_token", oauthAccessToken);

		HttpEntity<String> entity = new HttpEntity<>(requestBody.toString(), headers);

		ResponseEntity<Object> response = restTemplate.exchange(
			deleteUrl,
			HttpMethod.DELETE,
			entity,
			Object.class
		);
		return response.getStatusCode().is2xxSuccessful();
	}

	public Optional<UserDto> getUser(Integer githubId) throws Exception {
		log.info("getUser :: DB에서 사용자 정보 가져오기");
		return userRepository.findByGithubId(githubId);
	}

	public UserDto saveUser(Map<String, Object> userInfo) {
		log.info("saveUser :: DB에 사용자 정보 저장");
		String name = (String)userInfo.get("login");
		Integer githubId = (Integer)userInfo.get("id");

		UserDto user = UserDto.builder()
			.nickname(name)
			.githubId(githubId)
			.valid(true)
			.role(Role.GUEST)
			.build();

		return userRepository.save(user);
	}

	public void updateRefreshToken(UserDto user, String refreshToken) {
		user.updateRefreshToken(refreshToken);
		userRepository.saveAndFlush(user);
	}

}

 

 

 

3. Oauth2Controller.java

 

controller에서는 프론트에서 던져준 code를 받아서 프론트에게 jwt 토큰과 id, 그리고 name을 헤더에 넣어서 return해준당!

 

package com.d103.dddev.api.common.oauth2.controller;

import java.util.Map;

import javax.servlet.http.HttpServletResponse;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.d103.dddev.api.common.oauth2.service.Oauth2Service;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@RestController
@RequestMapping("/oauth")
@RequiredArgsConstructor
@Slf4j
@Api(tags = {"GitHub 로그인 api"})
public class Oauth2Controller {

	private final Oauth2Service oauth2Service;

	@GetMapping("/sign-in")
	@ApiOperation(value = "GitHub 로그인 api", notes = "GitHub 로그인 api")
	public ResponseEntity<String> signIn(@ApiParam(value = "redirect 코드", required = true) @RequestParam String code,
		HttpServletResponse response) {
		try {
			log.info("로그인 api 진입 :: {}", code);
			Map<String, String> login = oauth2Service.login(code);

			// access, refresh, 이름 헤더로 보내기
			response.setStatus(HttpServletResponse.SC_OK);
			response.setHeader("Authorization", login.get("Authorization"));
			response.setHeader("Authorization-refresh", login.get("Authorization-refresh"));
			response.setHeader("name", login.get("name"));
			response.setHeader("role", login.get("role"));

			return new ResponseEntity<>("로그인 성공!", HttpStatus.OK);
		} catch (Exception e) {
			log.info("소셜 로그인에 실패했습니다. 에러 메시지 :: {}", e.getMessage());
			return new ResponseEntity<>("로그인 실패ㅜㅜ!", HttpStatus.INTERNAL_SERVER_ERROR);
		}
	}

}

 

 

 

그럼 github api를 사용한 로그인은 끝!!!

 

다음은 로그아웃!!