Skip to content

Pochak 회원가입 & 로그인 로직 정리

Hagoeun edited this page Aug 15, 2023 · 1 revision

Written by Goeun Ha

전체적인 흐름

login

사진 출처: https://kimsy8979.tistory.com/2

  • 프론트: 인가 코드 넘기기

  • 백엔드: 인가 코드로 Access Token 발급 후 Access Token으로 유저 정보(email, socialId, name) 가져옴

    → socialId의 경우 소셜 회원가입을 한 회원인지 알기 위함

  • 소셜 회원가입 완료 후 프로필 생성까지 완료하면 JWT 발급, DB에 유저 정보 저장

  • access token 만료 시 fresh token을 이용하여 재발급

    → fresh token 만료 시 재로그인


Google 회원가입 & 로그인

/**
 * GOOGLE 소셜 로그인 기능
 * 프로트가 넘겨줄 인가코드
 * https://localhost:8080/login/oauth2/code/google?code=코드정보
 */
@ResponseBody
@GetMapping("/login/oauth2/code/google")
public BaseResponse<?> googleOAuthRequest(@RequestParam String code) throws BaseException {
    return new BaseResponse<>(googleOAuthService.login(code));
}

  • 인가코드로 access token 받아오기
/**
 * Get Access Token
 */
public String getAccessToken(String code) throws BaseException {
    GoogleTokenResponse googleTokenResponse = webClient.post()
            .uri(GOOGLE_BASE_URL, uriBuilder -> uriBuilder
                    .queryParam("grant_type", "authorization_code")
                    .queryParam("client_id", GOOGLE_CLIENT_ID)
                    .queryParam("client_secret",  GOOGLE_CLIENT_SECRET)
                    .queryParam("redirect_uri", GOOGLE_REDIRECT_URI)
                    .queryParam("code", code)
                    .build())
            .retrieve()
            .onStatus(HttpStatus::is4xxClientError, response -> Mono.error(new RuntimeException("Social Access Token is unauthorized")))
            .onStatus(HttpStatus::is5xxServerError, response -> Mono.error(new RuntimeException("Internal Server Error")))
            .bodyToMono(GoogleTokenResponse.class)
            .flux()
            .toStream()
            .findFirst()
            .orElseThrow(() -> new BaseException(INVALID_ACCESS_TOKEN));

    log.info(googleTokenResponse.getAccessToken());
    return googleTokenResponse.getAccessToken();
}

  • access token으로 유저 정보 받아오기
/**
 * Get User Info Using Access Token
 */
public GoogleUserResponse getUserInfo(String accessToken) throws BaseException {
    GoogleUserResponse googleUserResponse = webClient.get()
            .uri(GOOGLE_USER_BASE_URL, uriBuilder -> uriBuilder.queryParam("access_token", accessToken).build())
            .retrieve()
            .onStatus(HttpStatus::is4xxClientError, response -> Mono.error(new RuntimeException("Social Access Token is unauthorized")))
            .onStatus(HttpStatus::is5xxServerError, response -> Mono.error(new RuntimeException("Internal Server Error")))
            .bodyToMono(GoogleUserResponse.class)
            .flux()
            .toStream()
            .findFirst()
            .orElseThrow(() -> new BaseException(INVALID_USER_INFO));

    return googleUserResponse;
}

  • 이미 회원가입을 한 유저라면 앱의 access token과 refresh token을 발행함
  • 회원가입을 해야 하는 유저라면 isNewMember를 true로 하여 유저 정보를 넘김
public OAuthResponse login(String code) throws BaseException {
    String accessToken = getAccessToken(code);
    GoogleUserResponse userResponse = getUserInfo(accessToken);

    User user = userRepository.findUserWithSocialId(userResponse.getId()).orElse(null);

    if (user == null) {
        return OAuthResponse.builder()
						    .isNewMember(true)
                .socialType("google")
                .id(userResponse.getId())
                .name(userResponse.getName())
                .email(userResponse.getEmail())
                .build();
    }

    String appRefreshToken = jwtService.createRefreshToken();
    String appAccessToken = jwtService.createAccessToken(user.getHandle());

    user.updateRefreshToken(appRefreshToken);
    userRepository.saveUser(user);
    return OAuthResponse.builder()
           .isNewMember(false)
           .accessToken(appAccessToken)
           .refreshToken(appRefreshToken)
           .build();
    }

  • 추가 정보(이름, 별명, 프로필 이미지, 한 줄 소개) 입력 후 실제 유저 정보가 저장되는 코드
  • access token과 refresh token 발급도 이루어짐
public OAuthResponse signup(UserInfoRequest userInfoRequest) throws BaseException {
    userRepository.findUserWithSocialId(userInfoRequest.getSocialId())
            .ifPresent(i -> new BaseException(EXIST_USER_ID));

    String refreshToken = jwtService.createRefreshToken();
    String accessToken = jwtService.createAccessToken(userInfoRequest.getHandle());

    User user = User.signupUser()
            .name(userInfoRequest.getName())
            .email(userInfoRequest.getEmail())
            .handle(userInfoRequest.getHandle())
            .message(userInfoRequest.getMessage())
            .socialId(userInfoRequest.getSocialId())
            .profileImage(userInfoRequest.getProfileImage())
            .socialType(SocialType.of(userInfoRequest.getSocialType()))
            .build();

    user.updateRefreshToken(refreshToken);
    userRepository.saveUser(user);
    return OAuthResponse.builder()
            .isNewMember(false)
            .accessToken(accessToken)
            .refreshToken(refreshToken)
            .build();
}

JWT

  • access token과 refresh token으로 구현
  • access token을 30분 refresh token을 한 달로 잡음
  • access token이 만료되면 refresh token을 이용하여 재발급을 할 수 있음
  • access token는 유저의 정보(pochak의 경우 handle)을 가지고 있음
  • 프론트는 access token의 유효 기간이 만료 또는 만료 30초 전일 때 재발행을 요청함
  • access token을 파싱 하여 handle을 얻고 handle을 통해 유저를 조회하여 db에 저장된 refresh token과 header를 통해 전달된 refresh token이 같은지 비교하여 유효성을 검증함
  • refresh token의 유효성이 확인되면 재발급 그렇지 않다면 재로그인

  • access token과 refresh token을 얻음
// OAuth
// TOKEN_PREFIX = "Bearer ";
// HEADER_AUTHORIZATION = "Authorization";
// HEADER_REFRESH_TOKEN = "RefreshToken";

public class JwtHeaderUtil {

    public static String getAccessToken() {
        return getToken(HEADER_AUTHORIZATION);
    }

    public static String getRefreshToken() {
        return getToken(HEADER_REFRESH_TOKEN);
    }

    private static String getToken(String tokenHeader) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        String headerValue = request.getHeader(tokenHeader);
        if (headerValue == null) return null;
        if (StringUtils.hasText(headerValue) && headerValue.startsWith(TOKEN_PREFIX)) {
            return headerValue.substring(TOKEN_PREFIX.length());
        }
        return headerValue;
    }
}

  • refresh token의 유효성 검사를 위해 access token이 만료되어도 파싱 할 것
public String getHandle(String token) throws BaseException {
    try {
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    } catch (ExpiredJwtException e) {
        return e.getClaims().getSubject();
    }
}

  • access token 재발급
public PostTokenResponse reissueAccessToken() throws BaseException {
    String accessToken = JwtHeaderUtil.getAccessToken();
    String refreshToken = JwtHeaderUtil.getRefreshToken();

    if (!validate(refreshToken)) {
        throw new BaseException(INVALID_TOKEN);
    }

    String handle = validateRefreshToken(accessToken, refreshToken);
    String newAccessToken = createAccessToken(handle);

    return PostTokenResponse.builder()
            .accessToken(newAccessToken)
            .build();
}

  • refresh token 유효성 검증
  • access token에서 얻은 handle로 유저 조회해서 refresh token이 동일한지 확인
public String validateRefreshToken(String accessToken, String refreshToken) throws BaseException {
    String handle = getHandle(accessToken);
    User user = userRepository.findUserWithUserHandle(handle);
    if (user.getRefreshToken() != refreshToken) {
        throw new BaseException(INVALID_TOKEN);
    }
    return user.getHandle();
}
Clone this wiki locally