Skip to content

Commit

Permalink
Feat/#130 구글 소셜 로그인 구현 (#157)
Browse files Browse the repository at this point in the history
* feat: 회원 엔티티 및 테이블 추가

* feat: 회원을 등록하고 조회하는 기능 추가

세부사항
- 회원 엔티티 필드를 객체로 포장한 것으로 변경
- email 객체와 nickName 객체 변수명 수정

* feat: jwt 토큰을 생성하고 검증하는 기능 추가

* feat: 구글 로그인 기능 추가

세부사항
- 구글 access token 받아오는 기능 추가
- 구글 member info 받아오는 기능 추가

* refactor: 어노테이션 수정

세부사항
- MemberService transactional 어노테이션 추가
- GoogleProvider componenet 어노테이션 service 어노테이션으로 수정

* style: 사용하지 않는 import문 제거

* config: 의존성 추가 주석 영어로 수정

* refactor: TokenProvider의 secretKey 재할당 받지 못하게 수정

* refactor: 구글 api로 받은 객체 필드 카멜케이스로 이용할 수 있도록 수정

* refactor: GoogleInfoProvider 전반적인 부분 수정

세부사항
- memberInfo 요청을 보낼 때 accessToken을 url의 param으로 요청하는 것에서 header로 넣어서 요청하는 방식으로 수정
- 예외처리 세분화 (4XX 예외와 5XX 예외 분리)
- accessToken요청 시 요청 param을 객체로 추출
- 메소드 반환의 형태가 ResponseEntity가 아닌 body를 반환

* refactor: parameter 타입을 reference 타입에서 primitive 타입으로 수정

* refactor: 코드 컨벤션에 맞게 수정 및 가독성 향상

세부사항
- @repository 붙이기
- 오타 수정
- 불필요한 공백제거
- final 키워드 붙이기
- 불필요한 @NoArgsConstructor 제거
- 예외 네이밍 수정

* refactor: token 예외 처리 세분화

세부사항
- 만료된 토큰 예외 추가
- 만료된 토큰에 대한 test 추가

* refactor: 코드 통일성 및 오타 수정

* chore: 파일 명 수정 (NickName -> Nickname)

* refactor: 소셜 로그인 흐름 수정

세부사항
- 소셜 로그인 시 nickname을 입력받지 않고 email로 지정한 후 추후에 수정할 수 있도록 수정

* refactor: 코드 가독성 향상 및 오타 수정
  • Loading branch information
seokhwan-an authored Aug 9, 2023
1 parent 4873586 commit f71c43b
Show file tree
Hide file tree
Showing 28 changed files with 1,075 additions and 2 deletions.
4 changes: 4 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// JWT Dependency
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

//swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package shook.shook.auth.jwt.application;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import shook.shook.auth.jwt.exception.TokenException;

@Component
public class TokenProvider {

private final long accessTokenValidTime;
private final long refreshTokenValidTime;
private final Key secretKey;

public TokenProvider(
@Value("${jwt.access-token-valid-time}") final long accessTokenValidTime,
@Value("${jwt.refresh-token-valid-time}") final long refreshTokenValidTime,
@Value("${jwt.secret-code}") final String secretCode
) {
this.accessTokenValidTime = accessTokenValidTime;
this.refreshTokenValidTime = refreshTokenValidTime;
this.secretKey = generateSecretKey(secretCode);
}

private Key generateSecretKey(final String secretCode) {
final String encodedSecretCode = Base64.getEncoder().encodeToString(secretCode.getBytes());
return Keys.hmacShaKeyFor(encodedSecretCode.getBytes());
}

public String createAccessToken(final long memberId) {
return createToken(memberId, accessTokenValidTime);
}

public String createRefreshToken(final long memberId) {
return createToken(memberId, refreshTokenValidTime);
}

private String createToken(final long memberId, final long validTime) {
final Claims claims = Jwts.claims().setSubject("user");
claims.put("memberId", memberId);
Date now = new Date();
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + validTime))
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
}

public Claims parseClaims(final String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
} catch (MalformedJwtException e) {
throw new TokenException.NotIssuedTokenException();
} catch (ExpiredJwtException e) {
throw new TokenException.ExpiredTokenException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package shook.shook.auth.jwt.exception;

public class TokenException extends RuntimeException {

public static class NotIssuedTokenException extends TokenException {

public NotIssuedTokenException() {
super();
}
}

public static class ExpiredTokenException extends TokenException {

public ExpiredTokenException() {
super();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package shook.shook.auth.oauth.application;

import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestTemplate;
import shook.shook.auth.oauth.application.dto.GoogleAccessTokenRequest;
import shook.shook.auth.oauth.application.dto.GoogleAccessTokenResponse;
import shook.shook.auth.oauth.application.dto.GoogleMemberInfoResponse;
import shook.shook.auth.oauth.exception.OAuthException;

@RequiredArgsConstructor
@Component
public class GoogleInfoProvider {

private static final String TOKEN_PREFIX = "Bearer ";
private static final String GRANT_TYPE = "authorization_code";
private static final String AUTHORIZATION_HEADER = "Authorization";

@Value("${oauth2.google.access-token-url}")
private String GOOGLE_ACCESS_TOKEN_URL;

@Value("${oauth2.google.member-info-url}")
private String GOOGLE_MEMBER_INFO_URL;

@Value("${oauth2.google.client-id}")
private String GOOGLE_CLIENT_ID;

@Value("${oauth2.google.client-secret}")
private String GOOGLE_CLIENT_SECRET;

@Value("${oauth2.google.redirect-uri}")
private String LOGIN_REDIRECT_URL;

private final RestTemplate restTemplate;

public GoogleMemberInfoResponse getMemberInfo(final String accessToken) {
try {
final HttpHeaders headers = new HttpHeaders();
headers.set(AUTHORIZATION_HEADER, TOKEN_PREFIX + accessToken);
final HttpEntity<Object> request = new HttpEntity<>(headers);

final GoogleMemberInfoResponse responseEntity = restTemplate.exchange(
GOOGLE_MEMBER_INFO_URL,
HttpMethod.GET,
request,
GoogleMemberInfoResponse.class).getBody();

if (!Objects.requireNonNull(responseEntity).isVerifiedEmail()) {
throw new OAuthException.InvalidEmailException();
}

return responseEntity;
} catch (HttpClientErrorException e) {
throw new OAuthException.InvalidAccessTokenException();
} catch (HttpServerErrorException e) {
throw new OAuthException.GoogleServerException();
}
}

public GoogleAccessTokenResponse getAccessToken(final String authorizationCode) {
try {
final GoogleAccessTokenRequest googleAccessTokenRequest = new GoogleAccessTokenRequest(
authorizationCode,
GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET,
LOGIN_REDIRECT_URL,
GRANT_TYPE);
final HttpEntity<GoogleAccessTokenRequest> request = new HttpEntity<>(
googleAccessTokenRequest);

return Objects.requireNonNull(restTemplate.postForEntity(
GOOGLE_ACCESS_TOKEN_URL,
request,
GoogleAccessTokenResponse.class).getBody());

} catch (HttpClientErrorException e) {
throw new OAuthException.InvalidAuthorizationCodeException();
} catch (HttpServerErrorException e) {
throw new OAuthException.GoogleServerException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package shook.shook.auth.oauth.application;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import shook.shook.auth.jwt.application.TokenProvider;
import shook.shook.auth.oauth.application.dto.GoogleAccessTokenResponse;
import shook.shook.auth.oauth.application.dto.GoogleMemberInfoResponse;
import shook.shook.auth.oauth.application.dto.LoginResponse;
import shook.shook.member.application.MemberService;
import shook.shook.member.domain.Email;
import shook.shook.member.domain.Member;

@RequiredArgsConstructor
@Service
public class OAuthService {

private final MemberService memberService;
private final GoogleInfoProvider googleInfoProvider;
private final TokenProvider tokenProvider;

public LoginResponse login(final String accessCode) {
final GoogleAccessTokenResponse accessTokenResponse =
googleInfoProvider.getAccessToken(accessCode);
final GoogleMemberInfoResponse memberInfo = googleInfoProvider
.getMemberInfo(accessTokenResponse.getAccessToken());

final String userEmail = memberInfo.getEmail();
final Member member = memberService.findByEmail(new Email(userEmail))
.orElseGet(() -> memberService.register(userEmail));

final long memberId = member.getId();
final String accessToken = tokenProvider.createAccessToken(memberId);
final String refreshToken = tokenProvider.createRefreshToken(memberId);
return new LoginResponse(accessToken, refreshToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package shook.shook.auth.oauth.application.dto;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
@Getter
public class GoogleAccessTokenRequest {

private String code;
private String clientId;
private String clientSecret;
private String redirectUri;
private String grantType;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package shook.shook.auth.oauth.application.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class GoogleAccessTokenResponse {

@JsonProperty("access_token")
private String accessToken;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package shook.shook.auth.oauth.application.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class GoogleMemberInfoResponse {

private String email;

@JsonProperty("verified_email")
private boolean verifiedEmail;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package shook.shook.auth.oauth.application.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public class LoginResponse {

private String accessToken;
private String refreshToken;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package shook.shook.auth.oauth.config;

import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class OAuthConfig {

@Bean
public RestTemplate getRestTemplate() {
return new RestTemplateBuilder().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package shook.shook.auth.oauth.exception;

public class OAuthException extends RuntimeException {

public static class InvalidEmailException extends OAuthException {

public InvalidEmailException() {
super();
}
}

public static class InvalidAccessTokenException extends OAuthException {

public InvalidAccessTokenException() {
super();
}
}

public static class InvalidAuthorizationCodeException extends OAuthException {

public InvalidAuthorizationCodeException() {
super();
}
}

public static class GoogleServerException extends OAuthException {

public GoogleServerException() {
super();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package shook.shook.auth.oauth.ui;

import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import shook.shook.auth.oauth.application.OAuthService;
import shook.shook.auth.oauth.application.dto.LoginResponse;

@RequiredArgsConstructor
@RestController
public class OauthController {

private final OAuthService oAuthService;

@GetMapping("/login/google")
public ResponseEntity<LoginResponse> googleLogin(
@RequestParam("code") final String accessCode) {
final LoginResponse response = oAuthService.login(accessCode);
return ResponseEntity.ok(response);
}
}
Loading

0 comments on commit f71c43b

Please sign in to comment.