[Spring Boot] 구글 Oauth2 인증 및 구글 소셜 로그인 기능 구현

2023. 9. 20. 15:41기술 창고/Spring

728x90
SMALL

이전에는 구글 로그인을 구현하기 위해 선행되어야할 구글 쪽 Oauth 클라이언트를 생성하는 과정을 정리해보았습니다.

이번에는 만든 구글 클라이언트를 이용하여 본격적으로 구글 소셜 로그인을 구현해보도록 하겠습니다.

 

우선 저는 구글 로그인을 적용시키기 이전에 Spring Security 와 JWT 를 사용함으로서 기본적인 사용자들에 대한 관리를 수행하고 있다는 전제를 깔아두도록 하겠습니다.

즉, JWT, Spring Security에 대한 설정은 각자 해놓은 뒤에 거기에 맞게끔 본 내용을 수정시켜가며 적용해야 합니다.

대표적으로 JWTTokenProvider 와 같은 클래스 파일들이 그 예입니다.

 

혹시 모르니 제가 세팅한 JWT, Spring Security 관련 설정 파일들도 올리겠습니다.

 

# JWT, Spring Security 관련 파일

더보기

SecurityConfig 

package music.is.my.life.musicismylife.configuration;

import lombok.RequiredArgsConstructor;

import music.is.my.life.musicismylife.jwt.JwtAuthenticationFilter;
import music.is.my.life.musicismylife.jwt.JwtTokenProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig{

    private final JwtTokenProvider jwtTokenProvider;
    private final CorsConfig corsConfig;

    @Bean
    public SecurityFilterChain getSecurityFilterChain(HttpSecurity http,
                                                      HandlerMappingIntrospector introspector) throws Exception {
        MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector);

        http.csrf(csrfConfigurer ->
                csrfConfigurer.ignoringRequestMatchers(mvcMatcherBuilder.pattern("/miml/**")));

        http.authorizeHttpRequests(auth ->
                auth
                        .requestMatchers(mvcMatcherBuilder.pattern("/miml/member/register"),
                                mvcMatcherBuilder.pattern("/miml/member/login"),
                                mvcMatcherBuilder.pattern("/miml/music/listup/**"),
                                mvcMatcherBuilder.pattern("/miml/music/info/**"),
                                mvcMatcherBuilder.pattern("/miml/music/search/**"),
                                mvcMatcherBuilder.pattern("/miml/auth/login/**"),
                                mvcMatcherBuilder.pattern("/oauth/**"),
                                mvcMatcherBuilder.pattern("/oauth/**"),
                                mvcMatcherBuilder.pattern("/miml/music/chart"),
                                mvcMatcherBuilder.pattern("/v3/api-docs/**"),
                                mvcMatcherBuilder.pattern("/swagger-ui/**"),
                                mvcMatcherBuilder.pattern("/")).permitAll()
                        .anyRequest().authenticated()
        );

        /**
        http.oauth2Login(iod ->
                iod.userInfoEndpoint(id -> id.userService(oauth2UserService))
        );
         **/

        http.httpBasic(withDefaults());
        http.addFilter(corsConfig.corsFilter());
//        http.cors(cors -> cors.disable()); // cors 비활성화
        http.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }


}

Token

package music.is.my.life.musicismylife.jwt.domain;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import music.is.my.life.musicismylife.share.TimeStamped;

@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Entity
public class Token extends TimeStamped {

    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    private Long tokenId;

    // 토큰 권한 타입 (Bearer)
    @Column(nullable = false)
    private String grantType;

    // 액세스 토큰 (api 접근 허용 토큰)
    @Column(nullable = false)
    private String accessToken;

    // 리프레시 토큰 (액세스 토큰과 동일하며, 액세스 토큰 만료 시 재발급 및 토큰 정보 조회 시 활용)
    @Column(nullable = false)
    private String refreshToken;

    // 토큰이 발급된 계정의 id
    @Column
    private Long memberId;
}

TokenRepository

package music.is.my.life.musicismylife.jwt.repository;


import music.is.my.life.musicismylife.jwt.domain.Token;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface TokenRepository extends JpaRepository<Token, Long> {
}

TokenRequestDto

package music.is.my.life.musicismylife.jwt.request;

import lombok.Builder;
import lombok.Getter;

import java.util.Date;

@Getter
@Builder
public class TokenRequestDto {
    private String grantType; // 권한 타입 (Bearer)
    private String accessToken; // 액세스 토큰
    private String refreshToken; // 리프레시 토큰
    private Date accessTokenExpiresIn; // 만료 기간
}

JwtAuthenticationFilter

package music.is.my.life.musicismylife.jwt;


import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.GenericFilterBean;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {

    private final JwtTokenProvider jwtTokenProvider;

    // api 혹은 기능 실행 시 허용된 토큰인지 선행적으로 filter 실행
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        // 1. Request Header 에서 JWT 토큰 추출
        String token = resolveToken((HttpServletRequest) request);

        // 2. validateToken 으로 토큰 유효성 검사
        if (token != null && jwtTokenProvider.validateToken(token)) {
            // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext 에 저장
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
    }

    // Request Header 에서 토큰 정보 추출
    private String resolveToken(HttpServletRequest request) {
        // ServletRequest에 담겨있는 Authoriation 이라는 헤더를 가져옴.
        String bearerToken = request.getHeader("Authorization");

        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer")) {
            return bearerToken.substring(7);
        }

        return null;
    }
}

JwtTokenProvider

package music.is.my.life.musicismylife.jwt;

import com.querydsl.jpa.impl.JPAQueryFactory;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import music.is.my.life.musicismylife.jwt.request.TokenRequestDto;
import music.is.my.life.musicismylife.member.domain.Member;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.stream.Collectors;

import static music.is.my.life.musicismylife.member.domain.QMember.member;

@Slf4j
@Component
public class JwtTokenProvider {

    private final Key key;
    private final JPAQueryFactory jpaQueryFactory;

    public JwtTokenProvider(@Value("${jwt.secret}") String secretKey, JPAQueryFactory queryFactory) {
        byte[] keyBytes = Decoders.BASE64.decode(secretKey);
        this.key = Keys.hmacShaKeyFor(keyBytes);
        this.jpaQueryFactory = queryFactory;
    }

    // 유저 정보를 가지고 AccessToken, RefreshToken 을 생성하는 메서드
    // public TokenRequestDto generateToken(Authentication authentication) {
    public TokenRequestDto generateToken(Member member) {
        long now = (new Date()).getTime();

        // 토큰 만료 시간 설정
        Date accessTokenExpiresIn = new Date(now + 86400000);

        // access token 설정
        String accessToken = Jwts.builder()
                .setSubject(member.getEmail())
                .claim("auth", "USER")
                .setExpiration(accessTokenExpiresIn)
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();

        // Refresh Token 생성
        String refreshToken = Jwts.builder()
                .setExpiration(new Date(now + 86400000))
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();

        // 생성한 access token과 refresh Token을 기반으로 Dto객체에 담아 반환
        return TokenRequestDto.builder()
                .grantType("Bearer")
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .accessTokenExpiresIn(accessTokenExpiresIn)
                .build();
    }

    // JWT 토큰을 복호화하여 토큰에 들어있는 정보를 꺼내는 메서드
    public Authentication getAuthentication(String accessToken) {
        // 토큰 복호화
        Claims claims = parseClaims(accessToken);

        log.info("클레임 정보 초기값 - {}", claims);

        if (claims.get("auth") == null) {
            throw new RuntimeException("권한 정보가 없는 토큰입니다.");
        }

        log.info("클레임 정보 - {}", claims.get("auth"));

        // 클레임에서 권한 정보 가져오기
        Collection<? extends GrantedAuthority> authorities =
                Arrays.stream(claims.get("auth").toString().split(","))
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());

        // UserDetails 객체를 만들어서 Authentication 리턴
        UserDetails principal = new User(claims.getSubject(), "", authorities);
        return new UsernamePasswordAuthenticationToken(principal, "", authorities);
    }

    // Security Context Holder에 존재하는 인증받은 유저의 정보를 조회
    public Member getMemberFromAuthentication() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        if (authentication == null || AnonymousAuthenticationToken.class.
                isAssignableFrom(authentication.getClass())) {
            return null;
        }

        String emailAccount = ((UserDetails)authentication.getPrincipal()).getUsername();

        log.info("authentication 이메일 아이디 - {}",emailAccount);

        return jpaQueryFactory
                .selectFrom(member)
                .where(member.email.eq(emailAccount))
                .fetchOne();
    }

    // 토큰 정보를 검증하는 메서드
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            log.info("Invalid JWT Token", e);
        } catch (ExpiredJwtException e) {
            log.info("Expired JWT Token", e);
        } catch (UnsupportedJwtException e) {
            log.info("Unsupported JWT Token", e);
        } catch (IllegalArgumentException e) {
            log.info("JWT claims string is empty.", e);
        }
        return false;
    }

    private Claims parseClaims(String accessToken) {
        try {
            return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody();
        } catch (ExpiredJwtException e) {
            return e.getClaims();
        }
    }
}

UserDetailsImpl

package music.is.my.life.musicismylife.member.service;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NoArgsConstructor;
import music.is.my.life.musicismylife.member.domain.Member;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserDetailsImpl implements UserDetails {

    private String email;
    private String password;
    @Builder.Default
    private List<String> roles = new ArrayList<>();

    // 생성자를 통해 전달받은 유저 객체 정보를 기반으로 UserDetails를 적용
    public UserDetailsImpl(Member member){
        List<String> roles = new ArrayList<>();
        roles.add("ROLE_USER");
        roles.add("ROLE_GUEST");

        this.email = member.getEmail();
        this.password = member.getPassword();
        this.roles = roles;
    }


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities(){
        return this.roles.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }

    @Override
    public String getPassword(){
        return password;
    }

    @Override
    public String getUsername(){
        return email;
    }

    @Override
    public boolean isAccountNonExpired(){
        return true;
    }

    @Override
    public boolean isAccountNonLocked(){
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired(){
        return true;
    }

    @Override
    public boolean isEnabled(){
        return true;
    }
}

UserDetailsServiceImpl

package music.is.my.life.musicismylife.member.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import music.is.my.life.musicismylife.exception.member.MemberExceptionInterface;
import music.is.my.life.musicismylife.member.domain.Member;
import music.is.my.life.musicismylife.member.repository.MemberRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {

    private final MemberExceptionInterface memberExceptionInterface;
    private final MemberRepository memberRepository;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {

        if(!memberExceptionInterface.checkEmail(email)) {
            System.out.println("들어온 이메일에 대한 정보가 없습니다.");
            return null;
        }

        Member authMember = memberRepository.findMemberByEmail(email)
                .orElseThrow(() -> new UsernameNotFoundException("Can't find " + email));

        log.info("loadUserByUsername 실행 - {}", authMember.getEmail());

        return new UserDetailsImpl(authMember);
    }

}

 

1. Build.Gradle 설정

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-authorization-server'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-web-services'
    implementation 'org.springframework.boot:spring-boot-starter-validation'

    // QueryDSL 설정
    implementation "com.querydsl:querydsl-jpa:5.0.0:jakarta"
    implementation 'org.jetbrains:annotations:24.0.0'
    annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"

    // jwt
    implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
    implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
    implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

    // JSONObject
    implementation 'com.googlecode.json-simple:json-simple:1.1.1'
    implementation 'org.json:json:20230227'

    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    runtimeOnly 'com.mysql:mysql-connector-j'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
}

제가 설정한 Dependency 들입니다.

여기서 구글 소셜 로그인을 위해 중점적으로 봐야할 부분은 JWT, Spring Security, JSON Object 디펜던시들입니다.

 

앞서 말했듯 JWT, Spring Security 를 통해 유저들에 대한 관리를 진행하고 있기 때문에 해당 디펜던시들을 넣어주었고, JSON Object의 경우에는 데이터들을 요청하거나 반환할 때 json 형태로 파싱하기 위해 넣어주었습니다.

승인 받은 토큰 값이나 정보들을 개발 서버에서 인식할 수 있도록 json 형식의 데이터로 파싱하기 위한 용도라고 생각해주면 될 것 같습니다.

 

 

2. properties 파일 설정

applicaion.properties

#Oauth
spring.profiles.include=oauth

 

 

application-oauth.properties

spring.security.oauth2.client.registration.google.client-id={생성한 구글 클라이언트 ID}
spring.security.oauth2.client.registration.google.client-secret={생성한 구글 클라이언트 비밀번호}
spring.security.oauth2.client.registration.google.provider=google
spring.security.oauth2.client.registration.google.scope=profile,email
spring.security.oauth2.client.registration.google.redirect-uri=http://localhost:8080/oauth/google

# 구글 쪽에 요청하여 Authorization Code 발급받을 주소
google.authorize.code.url=https://accounts.google.com/o/oauth2/auth?client_id={생성한 구글 클라이언트 ID}&redirect_uri={구글 클라이언트에 설정한 리디렉션 URI 주소}&response_type={발급받을 인가 Authorization 코드 명 (code로 고정)}&scope={사용할 구글 서비스}

application-oauth.properties 파일에 생성한 구글 클라이언트 정보를 토대로 설정 값들을 넣어줍니다.

 

(1) client-id

생성한 구글 클라이언트 id

 

(2) client-secret

생성한 구글 클라이언트의 비밀 코드 (시크릿 키)

 

(3) provider

서비스 제공자는 당연히 구글이므로  google

 

(4) scope

구글 서비스 중 사용할 서비스들 (profile, email)

profile : 계정 정보

email : 구글 계정 이메일 정보

 

(5) google.authorize.code.url

서비스 사용 요청을 보내 Authorization code를 발급받을 구글 측 주소

 

각각 본인이 구글 클라이언트를 생성하면서 설정했던 값들을 넣어주면 됩니다. 

scope 부분에 자신이 이용할 서비스를 지정해줄 수 있는데 저는 당연히 구글 계정으로 로그인하면서 해당 계정의 정보들을 이용할 것이므로 email과 profile 서비스를 지정해주었습니다.

 

application.properties 에 profile 기능을 사용하여 oauth 를 설정해주었는데,

이것은 설정한 application-oauth.properties 파일의 내용을 불러와서 적용시켜주는 설정입니다.

 

3. SecurityConfig 추가 설정

SecuriityConfig 

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig{

    private final JwtTokenProvider jwtTokenProvider;
    private final CorsConfig corsConfig;

    @Bean
    public SecurityFilterChain getSecurityFilterChain(HttpSecurity http,
                                                      HandlerMappingIntrospector introspector) throws Exception {
        MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector);

        http.csrf(csrfConfigurer ->
                csrfConfigurer.ignoringRequestMatchers(mvcMatcherBuilder.pattern("/miml/**")));

        http.authorizeHttpRequests(auth ->
                auth
                        .requestMatchers(mvcMatcherBuilder.pattern("/miml/member/register"),
                                mvcMatcherBuilder.pattern("/miml/member/login"),
                                mvcMatcherBuilder.pattern("/miml/music/listup/**"),
                                mvcMatcherBuilder.pattern("/miml/music/info/**"),
                                mvcMatcherBuilder.pattern("/miml/music/search/**"),
                                mvcMatcherBuilder.pattern("/miml/auth/login/**"),
                                mvcMatcherBuilder.pattern("/oauth/**"), // 리디렉션 되는 구글 로그인 전용 api 경로 접근 허용
                                mvcMatcherBuilder.pattern("/miml/music/chart"),
                                mvcMatcherBuilder.pattern("/v3/api-docs/**"),
                                mvcMatcherBuilder.pattern("/swagger-ui/**"),
                                mvcMatcherBuilder.pattern("/")).permitAll()
                        .anyRequest().authenticated()
        );

        http.httpBasic(withDefaults());
        http.addFilter(corsConfig.corsFilter());
        http.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }


}

그리고 Spring Security를 넣어주었기 때문에 만들어준 SecurityConfig 파일에 구글 리디렉션 api 주소 경로를 접근 허용해주었습니다.

Spring Security를 적용하게 되면 기본적으로 접근 허용을 해주지 않는 이상 모든 api 경로가 막히기 때문에 추가적으로 api 접근 허용 설정을 해주어야 합니다.

 

- mvcMatcherBuilder.pattern("/oauth/**") : oauth 경로를 가진 모든 api에 대한 접근을 허용하는 코드

 

 

4. Controller 리디렉션 api 생성

OauthController

@Slf4j
@RequiredArgsConstructor
@RestController
public class OauthController {

    private final OauthService oauthService;

    // 구글 로그인 및 회원가입
    @GetMapping("/oauth/google")
    public ResponseEntity<ResponseBody> getGoogleAccessToken(@RequestParam("code") String authorizeCode, HttpServletResponse response) throws Exception {
        log.info("인가받은 코드 : {}", authorizeCode);

        return oauthService.getGoogleAccessToken(authorizeCode, response);
    }

}

구글 클라이언트에 설정해줬던 대로 리디렉션되어 호출될 api 를 만들어줍니다.

리디렉션 되면서 인가받은 Authorization Code 정보가 고정적으로 code라는 변수명으로 이 api를 통해 전달될 것이기 때문에 @RequestParam("code") 를 요청 파라미터로 받아줍니다.

 

이 code 파라미터를 log로 찍어보면 Authorization Code가 받아와진 것을 확인할 수 있습니다.

 

 

5. OauthService 비즈니스 로직 구현

OauthService

@Slf4j
@RequiredArgsConstructor
@Service
public class OauthService {

    private final PasswordEncoder passwordEncoder;
    private final MemberRepository memberRepository;
    private final JwtTokenProvider jwtTokenProvider;
    private final QueryData queryData;
    private final JPAQueryFactory jpaQueryFactory;
    private final TokenRepository tokenRepository;

    // 발급받은 client id
    @Value("${spring.security.oauth2.client.registration.google.client-id}")
    private String clientId;

    // 발급받은 client secret
    @Value("${spring.security.oauth2.client.registration.google.client-secret}")
    private String clientSecret;

    // 리다이렉트 uri
    @Value("${spring.security.oauth2.client.registration.google.redirect-uri}")
    private String redirectUri;

    private final String googleTokenUrl = "https://oauth2.googleapis.com/token";


    // 인가받은 authorization code 를 넘겨받아 본격적인 accesstoken을 뽑아오는 service
    public ResponseEntity<ResponseBody> getGoogleAccessToken(String authorizeCode, HttpServletResponse response) throws Exception {

        RestTemplate restTemplate = new RestTemplate();
        HashMap<String, String> params = new HashMap<>();

        params.put("code", authorizeCode);
        params.put("client_id", clientId);
        params.put("client_secret", clientSecret);
        params.put("redirect_uri", redirectUri);
        params.put("grant_type", "authorization_code");

        // 인증받은 code로 요청하여 구글 access token을 포함한 정보들을 Dto 객체에 매핑 시켜 추출
        // 인증받은 authorization code를 보낼 access Token 발급 주소
        ResponseEntity<GoogleOAuthResponseDto> responseEntity = restTemplate.postForEntity(googleTokenUrl, params, GoogleOAuthResponseDto.class);

        // 추출된 정보들 중 access token만 따로 추출
        String accessToken = responseEntity.getBody().getAccess_token();
        // 추출한 access token을 getGoogleInfo 함수에 전달하여 구글에 접속한 구글 계정에 대한 정보들을 추출하여 HashMap으로 각자 저장
        HashMap<String, String> googleUserInfo = getGoogleInfo(accessToken);

        return new ResponseEntity<>(new ResponseBody(StatusCode.OK, googleAccountRegistOrLogin(googleUserInfo, response)), HttpStatus.OK);
    }

    // 구글 쪽에서 인가받은 access token을 받아 해당 토큰을 다시 구글 쪽에 보내 로그인 및 접속한 구글 계정의 정보들을 추출
    private HashMap<String, String> getGoogleInfo(String accessToken) throws Exception {

        //요청하는 클라이언트마다 가진 정보가 다를 수 있기에 HashMap 선언
        HashMap<String, String> googleUserInfo = new HashMap<>();

        // 인가받은 access token과 구글 계정 정보들을 가져올 구글 url
        String reqURL = "https://www.googleapis.com/oauth2/v3/userinfo?alt=json&access_token=" + accessToken;

        // url 정보 호출 및 접속
        URL url = new URL(reqURL);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();

        //요청에 필요한 Header에 포함될 내용
        conn.setRequestProperty("Authorization", "Bearer " + accessToken);

        // url에 무사히 접속하여 정상 반응이 되었을 경우 상태값 저장
        int responseCode = conn.getResponseCode();
        log.info("## ResponseCode : {}", responseCode);

        // url에 접속하여 나온 결과값들을 BufferedReader를 통해 불러옴
        BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));

        String line = "";
        String result = "";

        // url 결과 정보값들을 readline 함수를 통해 한 줄씩 읽어 result에 차곡차곡 쌓음
        while ((line = br.readLine()) != null) {
            result += line;
        }

        // result에 차곡차곡 쌓아놓은 데이터들을 파싱시킬 JSONParser 생성
        JSONParser parser = new JSONParser();
        log.info("## Result = {}", result);

        // result를 파싱시켜 JSON형 데이터로 변환
        JSONObject element = (JSONObject) parser.parse(result);

        // JSON으로 변환된 데이터들 중 필요한 name(닉네임), email, picture 정보들만 추출
        String name = (String) element.get("name");
        String email = (String) element.get("email");
        String picture = (String) element.get("picture");

        // 반환하여 전달할 googleUserInfo HashMap에 저장
        googleUserInfo.put("name", name);
        googleUserInfo.put("email", email);
        googleUserInfo.put("picture", picture);

        log.info("## Login Controller : {}", googleUserInfo);
        log.info("구글 이메일 : {}", googleUserInfo.get("email"));
        log.info("구글 사용자 이름 : {}", googleUserInfo.get("name"));
        log.info("구글 프로필 사진 : {}", googleUserInfo.get("picture"));

        return googleUserInfo;
    }

    // 구글을 통해 최종적으로 뽑아온 name, email, picture에 대한 정보와 Response를 전달받아 최종적으로 회원을 가입시키고 로그인 시키며, Response에 자체 jwt를 발급시킬 함수
    private HashMap<String, String> googleAccountRegistOrLogin(HashMap<String, String> googleUserInfo, HttpServletResponse response) throws Exception {

        // 기존에 존재한 계정이 아닐 경우
        if (jpaQueryFactory
                .selectFrom(member)
                .where(member.email.eq(googleUserInfo.get("email")))
                .fetchOne() == null) {

            // 계정의 권한을 설정
            List<String> roles = new ArrayList<>();
            roles.add("USER");

            // 유저 회원가입
            Member member = Member.builder()
                    .email(googleUserInfo.get("email"))
                    .password(passwordEncoder.encode(googleUserInfo.get("email"))) // 비밀번호를 이메일로 변환하여 인코딩하여 저장
                    .nickName(googleUserInfo.get("name"))
                    .picture(googleUserInfo.get("picture"))
                    .description("")
                    .roles(roles)
                    .build();

            memberRepository.save(member);

            // jwt 토큰 발급
            TokenRequestDto tokenDto = receiveToken(member);

            // response에 토큰을 담는다
            tokenToHeaders(tokenDto, response);

            // 발급된 토큰 정보를 토대로 Token 엔티티에 input
            Token token = Token.builder()
                    .grantType(tokenDto.getGrantType())
                    .accessToken(tokenDto.getAccessToken())
                    .refreshToken(tokenDto.getRefreshToken())
                    .memberId(member.getMemberId())
                    .build();

            // 토큰 저장
            tokenRepository.save(token);

            HashMap<String, String> login_info = new HashMap<>();
            login_info.put("email", member.getEmail());
            login_info.put("nickname", member.getNickName());
            login_info.put("accessToken", token.getAccessToken());
            login_info.put("refreshToken", token.getRefreshToken());

            return login_info;
        }

        // 로그인한 유저
        Member login_member = jpaQueryFactory
                .selectFrom(member)
                .where(member.email.eq(googleUserInfo.get("email")))
                .fetchOne();

        // 재로그인 시 기존에 남아있던 토큰 삭제
        if(queryData.findMemberToken(login_member.getMemberId())){
            queryData.deleteToken(login_member.getMemberId());
        }

        // 로그인을 한 구글계정의 아이디와 비밀번호를 가지고 자체 jwt 토큰 발급 및 Dto에 저장
        TokenRequestDto tokenDto = receiveToken(login_member);

        // response에 토큰을 담는다
        tokenToHeaders(tokenDto, response);

        // 발급된 토큰 정보를 토대로 Token 엔티티에 input
        Token token = Token.builder()
                .grantType(tokenDto.getGrantType())
                .accessToken(tokenDto.getAccessToken())
                .refreshToken(tokenDto.getRefreshToken())
                .memberId(login_member.getMemberId())
                .build();

        // 토큰 저장
        tokenRepository.save(token);

        HashMap<String, String> login_info = new HashMap<>();
        login_info.put("email", login_member.getEmail());
        login_info.put("nickname", login_member.getNickName());
        login_info.put("accessToken", token.getAccessToken());
        login_info.put("refreshToken", token.getRefreshToken());

        return login_info;
    }

    // 토큰을 발급하고 Dto 개체에 저장하는 과정
    public TokenRequestDto receiveToken(Member member){
        return jwtTokenProvider.generateToken(member);
    }

    // 헤더에 토큰 넣기
    public void tokenToHeaders(TokenRequestDto tokenDto, HttpServletResponse response) {
        response.addHeader("Authorization", "Bearer " + tokenDto.getAccessToken());
        response.addHeader("Refresh-Token", tokenDto.getRefreshToken());
        response.addHeader("Access-Token-Expire-Time", tokenDto.getAccessTokenExpiresIn().toString());
    }

}

 

이 Service 단계에서 구글에게서 인가받은 토큰 값들과 사용 서비스들을 이용해 프로젝트에 맞게끔 기존에 회원정보가 존재하면 바로 JWT 토큰을 발급시키고 로그인, 존재하지 않는다면 회원가입 후 JWT 토큰 발급 및 로그인을 수행하도록 비즈니스 로직이 구성됩니다.

하나씩 살펴보겠습니다.

 

(1) 필요한 의존성 주입

- PasswordEncoder : 회원가입 시 회원의 비밀번호를 인코딩하기 위한 PasswordEncoder

- MemberRepository : 구글 로그인 후 회원가입 및 로그인 시 Member 객체를 조회하거나 등록시키기 위한 용도의 MemberReository

- JwtTokenProvider : 구글 로그인 후 정상적으로 프로젝트에 맞는 회원 정보로 로그인 시 발급될 JWT 토큰을 생성시켜주기 위한 JwtTokenProvider

- QueryData : 회원을 가입시키거나 로그인 시킬 때 존재하는 회원의 정보를 조회하거나, 토큰의 정보 존재 유무 확인, 남아있는 토큰 정보를 삭제 하기 위한 QueryDSL 사용 용도의 QueryData 

- JpaQueryFactory : QueryDSL을 사용하면서 직접적으로 데이터들을 생성하거나 조회, 삭제시키기위한 JpaQueryFactory

- TokenRepository : 직접적으로 토큰 정보를 조회하거나 생성, 삭제 시키기 위한 TokenRepository

 

 

(2) 구글 클라이언트 정보 이용

- clientId : oauth.properties 에서 설정해준 생성 클라이언트의 ID

- clientSecret : oauth.properties 에서 설정해준 생성 클라이언트의 tlzmflt zl

- redireceUri : oauth.properties 에서 설정해준 리다이렉트 uri

- googleTokenUrl : 발급받은 Authorization Code를 가지고 본격적으로 서비스를 사용할 수 있도록 하는 access token 을 발급받기위한 구글 쪽 요청 주소

 

 

(3) getGoogleAccessToken 함수

// 인가받은 authorization code 를 넘겨받아 본격적인 accesstoken을 뽑아오는 service
public ResponseEntity<ResponseBody> getGoogleAccessToken(String authorizeCode, HttpServletResponse response) throws Exception {

    RestTemplate restTemplate = new RestTemplate();
    HashMap<String, String> params = new HashMap<>();

    params.put("code", authorizeCode);
    params.put("client_id", clientId);
    params.put("client_secret", clientSecret);
    params.put("redirect_uri", redirectUri);
    params.put("grant_type", "authorization_code");

    // 인증받은 code로 요청하여 구글 access token을 포함한 정보들을 Dto 객체에 매핑 시켜 추출
    // 인증받은 authorization code를 보낼 access Token 발급 주소
    ResponseEntity<GoogleOAuthResponseDto> responseEntity = restTemplate.postForEntity(googleTokenUrl, params, GoogleOAuthResponseDto.class);

    // 추출된 정보들 중 access token만 따로 추출
    String accessToken = responseEntity.getBody().getAccess_token();
    // 추출한 access token을 getGoogleInfo 함수에 전달하여 구글에 접속한 구글 계정에 대한 정보들을 추출하여 HashMap으로 각자 저장
    HashMap<String, String> googleUserInfo = getGoogleInfo(accessToken);

    return new ResponseEntity<>(new ResponseBody(StatusCode.OK, googleAccountRegistOrLogin(googleUserInfo, response)), HttpStatus.OK);
}

 

getGoogleAccessToken 함수는 인가받은 authorization code 를 넘겨받아 본격적인 accesstoken을 뽑아오는 service 함수입니다.

 

ㄴ 1. 요청 인자 값 

- authorizeCode : 구글 측에서 인가받은  Authorization Code

- HttpServletResponse : 구글 로그인 성공 후 프로젝트에 맞게끔 JWT 토큰을 발급하기 위해 받은 HttpServletResponse

 

 

ㄴ 2. HashMap 생성

클라이언트 정보와 인가 코드 Authorization Code 정보들을 하나로 묶어 관리하기 위한 HashMap을 생성하여 정보들을 넎어줍니다.

HashMap<String, String> params = new HashMap<>();

params.put("code", authorizeCode);
params.put("client_id", clientId);
params.put("client_secret", clientSecret);
params.put("redirect_uri", redirectUri);
params.put("grant_type", "authorization_code");

 

 

ㄴ 3, RestTemplate 생성

RestTemplate을 통해 요청 정보값들을 전달해 ResponseEntity 형식으로 반환값을 받도록 합니다.

RestTemplate restTemplate = new RestTemplate();

// 인증받은 code로 요청하여 구글 access token을 포함한 정보들을 Dto 객체에 매핑 시켜 추출
// 인증받은 authorization code를 보낼 access Token 발급 주소
ResponseEntity<GoogleOAuthResponseDto> responseEntity = restTemplate.postForEntity(googleTokenUrl, params, GoogleOAuthResponseDto.class);

RestTemaplate 에서 지원하는 postForEntity 함수를 통해서 요청을 보내 토큰을 받아올 수 있습니다.

 

위에서 설정한 access token을 발급받기 위해 요청하는 구글 측 주소, access token을 발급받기 위해 같이 요청을 보낼 HashMap으로 담은 요청 정보 값, 요청이 정상적으로 반영되면 반환될 때 파싱될 GoogleOAuthResponseDto.class 객체를 postForEntity 함수를 통해 요청합니다.

 

정상적으로 요청이 성공적으로 완료될 경우 구글 측에서 정상적으로 반환받은 정보들이 GoogleOAuthResponseDto 객체에 파싱되어 정보가 저장되어 반환되게 됩니다.

 

<GoogleOAuthResponseDto>

package music.is.my.life.musicismylife.oauth.response;

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

@AllArgsConstructor
@NoArgsConstructor
@Getter
public class GoogleOAuthResponseDto { // JSON 파싱 객체
    private String access_token; // 구글 액세스 토큰
    private String expires_in; // 구글 액세스 토큰 만료 시간
    private String scope; // 구글 api 권한 범위 (profile, email)
    private String token_type; // 구글 토큰 타입 (authorization code)
    private String id_token; // 구글 계정 정보들이 암호환된 jwt 토큰
}

 

 

 

ㄴ 4. 발급받은 access token 확인 및 구글 계정 정보 추출

// 추출된 정보들 중 access token만 따로 추출
String accessToken = responseEntity.getBody().getAccess_token();
// 추출한 access token을 getGoogleInfo 함수에 전달하여 구글에 접속한 구글 계정에 대한 정보들을 추출하여 HashMap으로 각자 저장
HashMap<String, String> googleUserInfo = getGoogleInfo(accessToken);

RestTemplate을 통해 정상적으로 ResponseEntity 형식의 json 데이터로 반환된 데이터들 중 access token 부분 만을 추출합니다.

기타 다른 데이터 정보들은 굳이 사용하지 않기 때문에 필요하지 않아 access  token 만을 추출하였습니다.

그 전에 반환받은 ResponseEntity의 body 부분을 확인해보면,

 

이와 같은 형식으로 데이터들이 추출된 것을 확인할 수 있습니다.

여기서 access_token 부분 만을 추출한 것입니다.

 

이제 추출한 access token 정보를 getGoogleInfo 함수로 전달해줍니다.

전달하고 정상적으로 해당 함수의 로직이 수행되면 HashMap 형태로 반환될 것입니다.

 

 

(4) getGoogleInfo 함수로 구글 계정 정보 추출 

// 구글 쪽에서 인가받은 access token을 받아 해당 토큰을 다시 구글 쪽에 보내 로그인 및 접속한 구글 계정의 정보들을 추출
private HashMap<String, String> getGoogleInfo(String accessToken) throws Exception {

    //요청하는 클라이언트마다 가진 정보가 다를 수 있기에 HashMap 선언
    HashMap<String, String> googleUserInfo = new HashMap<>();

    // 인가받은 access token과 구글 계정 정보들을 가져올 구글 url
    String reqURL = "https://www.googleapis.com/oauth2/v3/userinfo?alt=json&access_token=" + accessToken;

    // url 정보 호출 및 접속
    URL url = new URL(reqURL);
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();

    //요청에 필요한 Header에 포함될 내용
    conn.setRequestProperty("Authorization", "Bearer " + accessToken);

    // url에 무사히 접속하여 정상 반응이 되었을 경우 상태값 저장
    int responseCode = conn.getResponseCode();
    log.info("## ResponseCode : {}", responseCode);

    // url에 접속하여 나온 결과값들을 BufferedReader를 통해 불러옴
    BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));

    String line = "";
    String result = "";

    // url 결과 정보값들을 readline 함수를 통해 한 줄씩 읽어 result에 차곡차곡 쌓음
    while ((line = br.readLine()) != null) {
        result += line;
    }

    // result에 차곡차곡 쌓아놓은 데이터들을 파싱시킬 JSONParser 생성
    JSONParser parser = new JSONParser();
    log.info("## Result = {}", result);

    // result를 파싱시켜 JSON형 데이터로 변환
    JSONObject element = (JSONObject) parser.parse(result);

    // JSON으로 변환된 데이터들 중 필요한 name(닉네임), email, picture 정보들만 추출
    String name = (String) element.get("name");
    String email = (String) element.get("email");
    String picture = (String) element.get("picture");

    // 반환하여 전달할 googleUserInfo HashMap에 저장
    googleUserInfo.put("name", name);
    googleUserInfo.put("email", email);
    googleUserInfo.put("picture", picture);

    log.info("## Login Controller : {}", googleUserInfo);
    log.info("구글 이메일 : {}", googleUserInfo.get("email"));
    log.info("구글 사용자 이름 : {}", googleUserInfo.get("name"));
    log.info("구글 프로필 사진 : {}", googleUserInfo.get("picture"));

    return googleUserInfo;
}

getGoogleInfo 함수는 구글 쪽에서 인가받은 access token을 받아 해당 토큰을 다시 구글 쪽에 보내 로그인 및 접속한 구글 계정의 정보들을 추출합니다.

 

ㄴ 1. Access Token을 통한 구글 측 계정 정보 호출

// 인가받은 access token과 구글 계정 정보들을 가져올 구글 url
String reqURL = "https://www.googleapis.com/oauth2/v3/userinfo?alt=json&access_token=" + accessToken;

// url 정보 호출 및 접속
URL url = new URL(reqURL);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();

//요청에 필요한 Header에 포함될 내용
conn.setRequestProperty("Authorization", "Bearer " + accessToken);

// url에 무사히 접속하여 정상 반응이 되었을 경우 상태값 저장
int responseCode = conn.getResponseCode();
log.info("## ResponseCode : {}", responseCode);

// url에 접속하여 나온 결과값들을 BufferedReader를 통해 불러옴
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));

String line = "";
String result = "";

// url 결과 정보값들을 readline 함수를 통해 한 줄씩 읽어 result에 차곡차곡 쌓음
while ((line = br.readLine()) != null) {
    result += line;
}

// result에 차곡차곡 쌓아놓은 데이터들을 파싱시킬 JSONParser 생성
JSONParser parser = new JSONParser();
log.info("## Result = {}", result);

// result를 파싱시켜 JSON형 데이터로 변환
JSONObject element = (JSONObject) parser.parse(result);

// JSON으로 변환된 데이터들 중 필요한 name(닉네임), email, picture 정보들만 추출
String name = (String) element.get("name");
String email = (String) element.get("email");
String picture = (String) element.get("picture");

- reqURL : 인가받은 access token을 전달하여 구글 계정 정보들을 가져올 구글 url

 

우선 전달한 인가받은 access token을 포함한 구글 계정 정보 호출 주소를  reqURL 변수로 만들어줍니다.

reqURL 변수에 저장된 주소를 URL 객체를 통해 브라우저 url 로 만들어준 뒤, HttpURLConnection 을 통해 연결을 수립시켜 줍니다.

 

이때 setRequestProperty 함수로 Authorization이라는 헤더를 만들어 준 뒤, 이곳에도 인가받은 access token 값을 넣어줍니다.

이래야지 인가 받은 헤더 토큰이 존재하기 떄문에 정상적으로 요청이 완료됩니다.

 

또한 .getResponseCode() 함수를 통해 정상적으로 요청이 수행되었는지 확인하기 위한 응답 코드를 뽑아봅니다.

 

해당 url에 요청하여 정상적으로 반환받은 데이터들을  InputStream 으로 받아와서 BufferedReader로 읽습니다.

BufferedReader로 읽은 데이터들을 한 줄씩 읽어 line 변수에 저장하고 line 변수를 다시 result 변수에 축적시켜 저장시킵니다.

 

완전히 축적시킨 데이터를 JSONParser 를 통해 json 형식의 데이터로 파싱합니다.

그 다음 파싱한 데이터 내에 존재하는 name, email, picture 속성 값들을 get 함수로 가져옵니다.

name, email, picture 모두 처음에 scope로 지정한 구글 요청 서비스인 profile, email을 통해 반환받은 데이터들입니다.

 

 

ㄴ 2. HashMap으로 추출 정보 저장 및 반환

//요청하는 클라이언트마다 가진 정보가 다를 수 있기에 HashMap 선언
HashMap<String, String> googleUserInfo = new HashMap<>();

// 반환하여 전달할 googleUserInfo HashMap에 저장
googleUserInfo.put("name", name);
googleUserInfo.put("email", email);
googleUserInfo.put("picture", picture);

log.info("## Login Controller : {}", googleUserInfo);
log.info("구글 이메일 : {}", googleUserInfo.get("email"));
log.info("구글 사용자 이름 : {}", googleUserInfo.get("name"));
log.info("구글 프로필 사진 : {}", googleUserInfo.get("picture"));

return googleUserInfo;

이제 정상적으로 받아온 데이터들을 HashMap에 저장하여 반환시킵니다.

 

 

(5) googleAccountRegistOrLogin() 함수를 통한 본격 로그인 / 회원가입

// 구글을 통해 최종적으로 뽑아온 name, email, picture에 대한 정보와 Response를 전달받아 최종적으로 회원을 가입시키고 로그인 시키며, Response에 자체 jwt를 발급시킬 함수
private HashMap<String, String> googleAccountRegistOrLogin(HashMap<String, String> googleUserInfo, HttpServletResponse response) throws Exception {

    // 기존에 존재한 계정이 아닐 경우
    if (jpaQueryFactory
            .selectFrom(member)
            .where(member.email.eq(googleUserInfo.get("email")))
            .fetchOne() == null) {

        // 계정의 권한을 설정
        List<String> roles = new ArrayList<>();
        roles.add("USER");

        // 유저 회원가입
        Member member = Member.builder()
                .email(googleUserInfo.get("email"))
                .password(passwordEncoder.encode(googleUserInfo.get("email"))) // 비밀번호를 이메일로 변환하여 인코딩하여 저장
                .nickName(googleUserInfo.get("name"))
                .picture(googleUserInfo.get("picture"))
                .description("")
                .roles(roles)
                .build();

        memberRepository.save(member);

        // jwt 토큰 발급
        TokenRequestDto tokenDto = receiveToken(member);

        // response에 토큰을 담는다
        tokenToHeaders(tokenDto, response);

        // 발급된 토큰 정보를 토대로 Token 엔티티에 input
        Token token = Token.builder()
                .grantType(tokenDto.getGrantType())
                .accessToken(tokenDto.getAccessToken())
                .refreshToken(tokenDto.getRefreshToken())
                .memberId(member.getMemberId())
                .build();

        // 토큰 저장
        tokenRepository.save(token);

        HashMap<String, String> login_info = new HashMap<>();
        login_info.put("email", member.getEmail());
        login_info.put("nickname", member.getNickName());
        login_info.put("accessToken", token.getAccessToken());
        login_info.put("refreshToken", token.getRefreshToken());

        return login_info;
    }

    // 로그인한 유저
    Member login_member = jpaQueryFactory
            .selectFrom(member)
            .where(member.email.eq(googleUserInfo.get("email")))
            .fetchOne();

    // 재로그인 시 기존에 남아있던 토큰 삭제
    if(queryData.findMemberToken(login_member.getMemberId())){
        queryData.deleteToken(login_member.getMemberId());
    }

    // 로그인을 한 구글계정의 아이디와 비밀번호를 가지고 자체 jwt 토큰 발급 및 Dto에 저장
    TokenRequestDto tokenDto = receiveToken(login_member);

    // response에 토큰을 담는다
    tokenToHeaders(tokenDto, response);

    // 발급된 토큰 정보를 토대로 Token 엔티티에 input
    Token token = Token.builder()
            .grantType(tokenDto.getGrantType())
            .accessToken(tokenDto.getAccessToken())
            .refreshToken(tokenDto.getRefreshToken())
            .memberId(login_member.getMemberId())
            .build();

    // 토큰 저장
    tokenRepository.save(token);

    HashMap<String, String> login_info = new HashMap<>();
    login_info.put("email", login_member.getEmail());
    login_info.put("nickname", login_member.getNickName());
    login_info.put("accessToken", token.getAccessToken());
    login_info.put("refreshToken", token.getRefreshToken());

    return login_info;
}

 

receiveToken 함수

// 토큰을 발급하고 Dto 개체에 저장하는 과정
public TokenRequestDto receiveToken(Member member){
    return jwtTokenProvider.generateToken(member);
}

 

tokenToHeaders 함수

// 헤더에 토큰 넣기
public void tokenToHeaders(TokenRequestDto tokenDto, HttpServletResponse response) {
    response.addHeader("Authorization", "Bearer " + tokenDto.getAccessToken());
    response.addHeader("Refresh-Token", tokenDto.getRefreshToken());
    response.addHeader("Access-Token-Expire-Time", tokenDto.getAccessTokenExpiresIn().toString());
}

 

위의 getGoogleInfo 함수를 통해 전달받은 email, name, picture 정보를 전달받아 로그인 혹은 회원가입 처리를 진행합니다.

 

ㄴ 1. 프로젝트에 가입되지 않은 이메일의 경우

// 기존에 존재한 계정이 아닐 경우
if (jpaQueryFactory
        .selectFrom(member)
        .where(member.email.eq(googleUserInfo.get("email")))
        .fetchOne() == null) {

    // 계정의 권한을 설정
    List<String> roles = new ArrayList<>();
    roles.add("USER");

    // 유저 회원가입
    Member member = Member.builder()
            .email(googleUserInfo.get("email"))
            .password(passwordEncoder.encode(googleUserInfo.get("email"))) // 비밀번호를 이메일로 변환하여 인코딩하여 저장
            .nickName(googleUserInfo.get("name"))
            .picture(googleUserInfo.get("picture"))
            .description("")
            .roles(roles)
            .build();

    memberRepository.save(member);

    // jwt 토큰 발급
    TokenRequestDto tokenDto = receiveToken(member);

    // response에 토큰을 담는다
    tokenToHeaders(tokenDto, response);

    // 발급된 토큰 정보를 토대로 Token 엔티티에 input
    Token token = Token.builder()
            .grantType(tokenDto.getGrantType())
            .accessToken(tokenDto.getAccessToken())
            .refreshToken(tokenDto.getRefreshToken())
            .memberId(member.getMemberId())
            .build();

    // 토큰 저장
    tokenRepository.save(token);

    HashMap<String, String> login_info = new HashMap<>();
    login_info.put("email", member.getEmail());
    login_info.put("nickname", member.getNickName());
    login_info.put("accessToken", token.getAccessToken());
    login_info.put("refreshToken", token.getRefreshToken());

    return login_info;
}

 

QueryDSL을 통해 전달받은 email 정보를 가진 유저의 정보가 존재하지 않는지 확인하고 없을 경우, 회원가입을 시킵니다.

회원가입을 시킨 후, receiveToken 함수를 통해 JWT 토큰을 생성하고 발급받습니다.

또한 tokenToHeaders 함수를 통해 Response Header에 JWT 토큰을 발급합니다.

 

 

 

ㄴ 2. 계정이 존재하는 유저의 경우

// 로그인한 유저
Member login_member = jpaQueryFactory
        .selectFrom(member)
        .where(member.email.eq(googleUserInfo.get("email")))
        .fetchOne();

// 재로그인 시 기존에 남아있던 토큰 삭제
if(queryData.findMemberToken(login_member.getMemberId())){
    queryData.deleteToken(login_member.getMemberId());
}

// 로그인을 한 구글계정의 아이디와 비밀번호를 가지고 자체 jwt 토큰 발급 및 Dto에 저장
TokenRequestDto tokenDto = receiveToken(login_member);

// response에 토큰을 담는다
tokenToHeaders(tokenDto, response);

// 발급된 토큰 정보를 토대로 Token 엔티티에 input
Token token = Token.builder()
        .grantType(tokenDto.getGrantType())
        .accessToken(tokenDto.getAccessToken())
        .refreshToken(tokenDto.getRefreshToken())
        .memberId(login_member.getMemberId())
        .build();

// 토큰 저장
tokenRepository.save(token);

HashMap<String, String> login_info = new HashMap<>();
login_info.put("email", login_member.getEmail());
login_info.put("nickname", login_member.getNickName());
login_info.put("accessToken", token.getAccessToken());
login_info.put("refreshToken", token.getRefreshToken());

return login_info;

 

계정이 존재하는 경우, receiveToken 함수와 tokenToHeaders 함수를 통해 토큰만 생성하고 발급하여 로그인 처리 해줍니다.

 

이제 여기까지 완료가 되었다면 정상적으로 구글 로그인이 수행된 것입니다.

로그인이 완료된 내용을 PostMan 으로 찍어서 확인해보겠습니다.

 

 

정상적으로 로그인된 계정 정보가 추출되는 것을 확인할 수 있습니다.

이로써 구글 로그인이 완료되었습니다.

 

 

!! 주의

JWT, Spring Security 가 적용됨으로써 올린 내용대로 진행하였음에도 정상적으로 수행되지 않을 수도 있습니다.

 

728x90
반응형
LIST