AWS Back Day 89 ~ 90. OAuth2 + JWT 구현

이강용·2023년 5월 11일
0

Spring Boot

목록 보기
20/20

구글 클라우드 플랫폼

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.6.6</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.study</groupId>
	<artifactId>oauth2</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>oauth2</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencies>
		<!-- [AUTH]-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-oauth2-client</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<!-- [DB]-->
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.2.2</version>
		</dependency>
		<dependency>
		    <groupId>mysql</groupId>
		    <artifactId>mysql-connector-java</artifactId>
		</dependency>
		
		
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<!-- [ LOMBOK ]-->
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
		<!-- [ JWT ]-->
		<dependency>
		    <groupId>io.jsonwebtoken</groupId>
		    <artifactId>jjwt-api</artifactId>
		    <version>0.11.5</version>
		</dependency>
		<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
		<dependency>
		    <groupId>io.jsonwebtoken</groupId>
		    <artifactId>jjwt-impl</artifactId>
		    <version>0.11.5</version>
		    <scope>runtime</scope>
		</dependency>
		<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-jackson -->
		<dependency>
		    <groupId>io.jsonwebtoken</groupId>
		    <artifactId>jjwt-jackson</artifactId>
		    <version>0.11.5</version>
		    <scope>runtime</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

application.yml


spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/book_management
    username: root
    password: root
    
mybatis:
  mapper-locations:
    - /mappers/*.xml    
jwt:
  secret:  uAdzVUhnjML7pCLQLDapBdNacinrqdRjbaqLD7sMUfe0ILk8KKqk5Xb0WncSuIre   

Maven Project Update

SecurityConfig.java

코드를 입력하세요

WebMvcConfig.java

코드를 입력하세요

OAuth2 Service 구현

AuthService

코드를 입력하세요

application.yml (추가)

코드를 입력하세요

React front 구현

npx create-react-app oauth2-front

npm i react-router-dom
npm i axios
npm i react-query
npm i recoil
npm i react-icons

index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { QueryClient, QueryClientProvider } from 'react-query';
import { RecoilRoot } from 'recoil';
import { BrowserRouter } from 'react-router-dom';

const queryClient = new QueryClient();

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <RecoilRoot>
    <QueryClientProvider client={queryClient}>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </QueryClientProvider>
  </RecoilRoot>
  
);


reportWebVitals();

App.js

import  React from 'react';
import { Routes, Route } from "react-router-dom";
import NotFound from './pages/NotFound/NotFound';


function App() {
  return (
    <>
      <Routes>
        <Route path='/' />
        <Route path='/auth/login' />
        <Route path='/auth/register' />
        <Route path='/auth/oauth2/register' />
        <Route path='/*' element={<NotFound />} />
      </Routes>
    </>
  );
}

export default App;

src > components, pages, store 폴더 생성

NotFound > NotFound.js

import React from 'react';

const NotFound = () => {
    return (
        <div>
            <h1>페이지를 찾을 수 없습니다.</h1>
        </div>
    );
};

export default NotFound;

Login > Login.js

import React from 'react';
import { FcGoogle } from 'react-icons/fc';
import { useNavigate } from 'react-router-dom';



const Login = () => {


    const navigate = useNavigate();

    const googleAuthClickHandle = () => {
        window.location.href="http://localhost:8080/oauth2/authorization/google";
    }

    return (
        <div>
            <input type="text" placeholder='email' />
            <input type="password" placeholder='password' />
            <button>로그인</button>
            <button onClick={googleAuthClickHandle}><FcGoogle /></button>
        </div>
    );
};

export default Login;

Back - Config 추가

SecurityConfig.java (추가)

package com.study.oauth2.config;

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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;

import com.study.oauth2.service.AuthService;

import lombok.RequiredArgsConstructor;


@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	private final AuthService authService;
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.httpBasic().disable();
		http.formLogin().disable();
		http.cors();
		http.csrf().disable();
		http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
		http.authorizeRequests()
			.antMatchers("/auth/**")
			.permitAll()
			.anyRequest()
			.authenticated()
			.and()
			.oauth2Login()
			.loginPage("http://localhost:3000/auth/login")
			.userInfoEndpoint()
			.userService(authService);
	}

}

Google Cloud 수정

구현한 로그인 페이지 (구글 접속 버튼)

구글 접속이 정상적으로 이루어졌을 경우

OAuth2Attribute 구현

security > OAuth2Attribute.java

package com.study.oauth2.security;

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

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;

@ToString
@Builder(access = AccessLevel.PRIVATE) //option (NoArg에 private 걸어두는 방법)
@Getter
public class OAuth2Attribute {
    private Map<String, Object> attributes;
    private String email;
    private String name;

   public static OAuth2Attribute of(String provider, Map<String, Object> attributes) {
        switch (provider) {
            case "google":
                return ofGoogle(attributes);
            case "kakao":
                return ofKakao(attributes);
            case "naver":
                return ofNaver( attributes);
            default:
                throw new RuntimeException();
        }
    }

    private static OAuth2Attribute ofGoogle(Map<String, Object> attributes) {
        return OAuth2Attribute.builder()
                .name((String) attributes.get("name"))
                .email((String) attributes.get("email"))
                .attributes(attributes)
                .build();
    }

    private static OAuth2Attribute ofKakao(Map<String, Object> attributes) {
        Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
        Map<String, Object> kakaoProfile = (Map<String, Object>) kakaoAccount.get("profile");

        return OAuth2Attribute.builder()
                .name((String) kakaoProfile.get("nickname"))
                .email((String) kakaoAccount.get("email"))
                .attributes(kakaoAccount)
                .build();
    }

    private static OAuth2Attribute ofNaver( Map<String, Object> attributes) {
        Map<String, Object> response = (Map<String, Object>) attributes.get("response");

        return OAuth2Attribute.builder()
                .name((String) response.get("name"))
                .email((String) response.get("email"))
                .attributes(response)
                .build();
    }

   public Map<String, Object> convertToMap() {
        Map<String, Object> map = new HashMap<>();
        map.put("name", name);
        map.put("email", email);

        return map;
    }
}

AuthService (추가)

코드를 입력하세요

변경 전

ArrayList<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));

변경 후

return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")), Attributes, "email");

SecurityConfig(추가)

코드를 입력하세요

OAuth2 Success Handler 구현

security > OAuth2SuccessHandler.java

코드를 입력하세요

security > jwt > JwtTokenProvider

package com.study.oauth2.security.jwt;

import java.security.Key;
import java.util.Date;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Component;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;


@Component

public class JwtTokenProvider {
	private final Key key;
	
	public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) {
		key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey));
	}
	
	//jwt Token 생성 (회원가입 전용 토큰)
	public String generateOAuth2RegisterToken (Authentication authentication) {
		
		// 만료기간
		Date tokenExpiresDate = new Date(new Date().getTime() + (1000*60*10));
		
		OAuth2User oAuth2User = (OAuth2User)authentication.getPrincipal();
		
		String email = oAuth2User.getAttribute("email");
		
		
	
		System.out.println(email);
		
		 
		
		return  Jwts.builder()
				.setSubject("OAuth2Register")
				.claim("email", email)
				.setExpiration(tokenExpiresDate)
				.signWith(key,SignatureAlgorithm.HS256)
				.compact();
	}
	
	public Boolean validateToken(String token) {
		try {
			Jwts.parserBuilder()
				.setSigningKey(key)
				.build()
				.parseClaimsJws(token);
			return true;
		}catch (Exception e) {
			
		}
		return false;
	}
}

전에 했던 파일에서 복사해오기

front

Register > OAuth2Register.js

import axios from 'axios';
import React, { useState } from 'react';
import { useMutation } from 'react-query';
import { useSearchParams } from 'react-router-dom';

const OAuth2Register = () => {
    const [passwords, setPasswords] = useState({ password: "", checkPassword: ""});
    const [ searchParams, setSearchParams] = useSearchParams();
    const oauth2Register = useMutation(async (registerData) => {

        const option = {
            headers:{
                registerToken: `Bearer ${registerToken}`
            }
        }

        return await axios.post("http://localhost:8080/auth/oauth2/register",registerData,option);
    });

    const registerToken = searchParams.get("registerToken");
    const email = searchParams.get("email");
    const name = searchParams.get("name");

    const passwordInputChangeHandle = (e) => {
        const { name, value} = e.target;
        setPasswords({...passwords,[name]: value});
    }

    const oauth2RegisterSubmitHandle = () => {
        oauth2Register.mutate({
            email,
            name,
            ...passwords

        });
    }

    return (
        <div>
            <input type="text" value={email} disabled = {true} />
            <input type="text" value={name} disabled= {true} />
            <input type="password" name='password' placeholder="비밀번호" onChange={passwordInputChangeHandle} />
            <input type="password" name='checkPassword' placeholder="비밀번호확인" onChange={passwordInputChangeHandle} />
            <button onClick={oauth2RegisterSubmitHandle}>가입하기</button>
        </div>
    );
};

export default OAuth2Register;

back

controller > AuthController

package com.study.oauth2.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.study.oauth2.dto.auth.OAuth2RegisterReqDto;
import com.study.oauth2.security.jwt.JwtTokenProvider;

import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
	
	private final JwtTokenProvider jwtTokenProvider;
	
	@PostMapping("/oauth2/register")
	public ResponseEntity<?> oauth2Register(
			@RequestHeader(value="registerToken") String registerToken,
			@RequestBody OAuth2RegisterReqDto oAuth2RegisterReqDto) {
		
		boolean validated = jwtTokenProvider.validateToken(jwtTokenProvider.getToken(registerToken));
		
		if(!validated) {
			//토큰이 유효하지 않음
			return ResponseEntity.badRequest().body("회원가입 요청 시간이 초과하였습니다.");
		}
		
		System.out.println(oAuth2RegisterReqDto);
		return ResponseEntity.ok(null);
	}
	
}

dto > auth > OAuth2RegisterReqDto

package com.study.oauth2.dto.auth;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import com.study.oauth2.entity.User;

import lombok.Data;

@Data
public class OAuth2RegisterReqDto {
	private String email;
	private String name;
	private String password;
	private String checkPassword;
	private String provider;
	
	public User toEntity() {
		return User.builder()
				.email(email)
				.name(name)
				.password(new BCryptPasswordEncoder().encode(password))
				.provider(provider)
				.build();
	}
}

네이버 개발자 센터
카카오 디벨로퍼

네이버 개발자 센터

  • 애플리케이션 등록 (API 이용신청)

공식 documents - 로그인 개발 가이드

Back

application.yml


spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/book_management
    username: root
    password: root
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: <your id>
            client-secret: <your secret>
            scope:
            - email
            - profile
          kakao:
            client-id: <your id>
            client-secret: <your secret>
            redirect-uri: http://localhost:8080/login/oauth2/code/kakao
            authorization-grant-type: authorization_code
            client-authentication-method: POST
            client-name: Kakao
            scope:
              - profile
              - account_email
          naver:
            client-id: <your id>
            client-secret: <your secret>
            redirect-uri: http://localhost:8080/login/oauth2/code/naver
            authorization-grant-type: authorization_code
            scope:
              - name
              - email
        provider:
          kakao:
            authorization-uri: https://kauth.kakao.com/oauth/authorize
            token-uri: https://kauth.kakao.com/oauth/token
            user-info-uri: https://kapi.kakao.com/v2/user/me
            user-name-attribute: id
          naver:
            authorization-uri: https://nid.naver.com/oauth2.0/authorize
            token-uri: https://nid.naver.com/oauth2.0/token
            user-info-uri: https://openapi.naver.com/v1/nid/me
            user-name-attribute: response
            
mybatis:
  mapper-locations:
    - /mappers/*.xml    
jwt:
  secret:  uAdzVUhnjML7pCLQLDapBdNacinrqdRjbaqLD7sMUfe0ILk8KKqk5Xb0WncSuIre   

front

Login.js(추가)

import React from 'react';
import { FcGoogle } from 'react-icons/fc';
import { useNavigate } from 'react-router-dom';
import { SiNaver } from 'react-icons/si';


const Login = () => {


    const navigate = useNavigate();

    const googleAuthClickHandle = () => {
        window.location.href="http://localhost:8080/oauth2/authorization/google";
    }

    const naverAuthClickHandle = () => {
        window.location.href="http://localhost:8080/oauth2/authorization/naver";
    }

    return (
        <div>
            <input type="text" placeholder='email' />
            <input type="password" placeholder='password' />
            <button>로그인</button>
            <button onClick={googleAuthClickHandle}><FcGoogle /></button>
            <button onClick={naverAuthClickHandle}><SiNaver/></button>
        </div>
    );
};

export default Login;

back

AuthService

/*
 * 확정 로직 아래에서 확인
 */

OAuth2SuccessHandler

package com.study.oauth2.security;

import java.io.IOException;
import java.net.URLEncoder;

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

import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import com.study.oauth2.entity.User;
import com.study.oauth2.repository.UserRepository;
import com.study.oauth2.security.jwt.JwtTokenProvider;

import lombok.RequiredArgsConstructor;



@Component
@RequiredArgsConstructor
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
	
	
	private final UserRepository userRepository;
	private final JwtTokenProvider jwtTokenProvider;
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		
		// 로그인이 성공적으로 이루어졌을때 8080 (Back) 으로 온 응답을 3000 (Front)으로 redirect 
		
		OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
		String email = oAuth2User.getAttribute("email");
		String provider = oAuth2User.getAttribute("provider");
		User userEntity = userRepository.findUserByEmail(email);
		
		if(userEntity == null) {
			// 회원가입 실패
			String registerToken = jwtTokenProvider.generateOAuth2RegisterToken(authentication);
			String name = oAuth2User.getAttribute("name");
			response
			.sendRedirect(
					"http://localhost:3000/auth/oauth2/register"
							+"?registerToken=" + registerToken 
							+ "&email=" + email
							+ "&name=" + URLEncoder.encode(name,"UTF-8")
							+ "&provider=" + provider
			);
		}else {
			// 회원가입 성공
			if(StringUtils.hasText(userEntity.getProvider())) {
				// 회원가입이 됐고, provider가 등록된 경우
				if(!userEntity.getProvider().contains(provider)) {
					// 하지만 로그인이된 oauth2 계정의 provider는 등록이 안된 경우
				}
			}else {
				// 회원가입은 정상적으로 됐으나, provider가 null인 경우
				response.sendRedirect("http://localhost:3000/auth/oauth2/merge"
										+ "?provider=" + provider
										+ "&email=" + email);
			}
		}
	}
}

front

pages > OAuth2Merge > OAuth2Merge.js

/*
 * 확정 로직 아래에서 확인
 */

back

AuthController

package com.study.oauth2.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.study.oauth2.dto.auth.OAuth2ProviderMergeReqDto;
import com.study.oauth2.dto.auth.OAuth2RegisterReqDto;
import com.study.oauth2.security.jwt.JwtTokenProvider;
import com.study.oauth2.service.AuthService;

import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
	
	private final JwtTokenProvider jwtTokenProvider;
	private final AuthService authService;
	
	@PostMapping("/oauth2/register")
	public ResponseEntity<?> oauth2Register(
			@RequestHeader(value="registerToken") String registerToken,
			@RequestBody OAuth2RegisterReqDto oAuth2RegisterReqDto) {
		
		boolean validated = jwtTokenProvider.validateToken(jwtTokenProvider.getToken(registerToken));
		
		if(!validated) {
			//토큰이 유효하지 않음
			return ResponseEntity.badRequest().body("회원가입 요청 시간이 초과하였습니다.");
		}
		
		return ResponseEntity.ok(authService.oAuth2Register(oAuth2RegisterReqDto));
	}
	
	@PutMapping("/oauth2/merge")
	public ResponseEntity<?> providerMerge(@RequestBody OAuth2ProviderMergeReqDto oAuth2ProviderMergeReqDto){
		
		// 기존의 암호와 비교를 해야함 
		// DB에 암호가 들어있음
		if(!authService.checkPassword(oAuth2ProviderMergeReqDto.getEmail(), oAuth2ProviderMergeReqDto.getPassword())) {
			return ResponseEntity.badRequest().body("비밀번호가 일치하지 않습니다.");
		}
		
		
		return ResponseEntity.ok(authService.oAuth2ProviderMerge(oAuth2ProviderMergeReqDto));
	}
	
}

dto > auth

OAuth2ProviderMergeReqDto

package com.study.oauth2.dto.auth;

import lombok.Data;

@Data
public class OAuth2ProviderMergeReqDto {
	private String email;
	private String password;
	private String provider;
}

AuthService (추가)

package com.study.oauth2.service;

import java.util.Collections;
import java.util.Map;

import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import com.study.oauth2.dto.auth.OAuth2RegisterReqDto;
import com.study.oauth2.entity.Authority;
import com.study.oauth2.entity.User;
import com.study.oauth2.repository.UserRepository;
import com.study.oauth2.security.OAuth2Attribute;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class AuthService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
	
	
	private final UserRepository userRepository;
	 

	@Override
	public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
		
		OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService = new DefaultOAuth2UserService();
		
		OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest);
		
		System.out.println(oAuth2User);
		
		String registrationId = userRequest.getClientRegistration().getRegistrationId(); //Google (문자로 구글, 네이버 카카오를 들고옴)
		
		OAuth2Attribute oAuth2Attribute = OAuth2Attribute.of(registrationId, oAuth2User.getAttributes());
		
		Map<String, Object> Attributes = oAuth2Attribute.convertToMap();
		
//		ArrayList<SimpleGrantedAuthority> authorities = new ArrayList<>();
//		authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
		return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")), Attributes, "email");
	}
	
	public int oAuth2Register(OAuth2RegisterReqDto oAuth2RegisterReqDto) {
		User userEntity = oAuth2RegisterReqDto.toEntity();
		
		userRepository.saveUser(userEntity);
		return userRepository.saveAuthority(
				Authority.builder()
					.userId(userEntity.getUserId())
					.roleId(1)
					.build()
		);
		
	}
	
	// 암호 비교 
	// DI등록하려고 보니.. IOC에 등록이 안돼있음
	public boolean checkPassword(String email, String password) {
		User userEntity = userRepository.findUserByEmail(email);
		BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

		return passwordEncoder.matches(password, userEntity.getPassword());
	}
	
}

SecurityConfig (Bean 등록)

package com.study.oauth2.config;

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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import com.study.oauth2.security.OAuth2SuccessHandler;
import com.study.oauth2.service.AuthService;

import lombok.RequiredArgsConstructor;


@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	private final AuthService authService;
	private final OAuth2SuccessHandler oAuth2SuccessHandler;
	
	@Bean
	public BCryptPasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
	
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.httpBasic().disable();
		http.formLogin().disable();
		http.cors();
		http.csrf().disable();
		http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
		http.authorizeRequests()
			.antMatchers("/auth/**")
			.permitAll()
			.anyRequest()
			.authenticated()
			.and()
			.oauth2Login()
			.loginPage("http://localhost:3000/auth/login")
			.successHandler(oAuth2SuccessHandler)
			.userInfoEndpoint()
			.userService(authService);
			
	}

}

DB provider가 null 일 때

구글 로그인

네이버 로그인

비밀번호가 일치하지 않을 때

AuthService (통합 기능추가)

package com.study.oauth2.service;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import com.study.oauth2.dto.auth.OAuth2ProviderMergeReqDto;
import com.study.oauth2.dto.auth.OAuth2RegisterReqDto;
import com.study.oauth2.entity.Authority;
import com.study.oauth2.entity.User;
import com.study.oauth2.repository.UserRepository;
import com.study.oauth2.security.OAuth2Attribute;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class AuthService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
	
	
	private final UserRepository userRepository;
	 

	@Override
	public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
		
		OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService = new DefaultOAuth2UserService();
		
		OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest);
		
		System.out.println(oAuth2User);
		
		String registrationId = userRequest.getClientRegistration().getRegistrationId(); //Google (문자로 구글, 네이버 카카오를 들고옴)
		
		OAuth2Attribute oAuth2Attribute = OAuth2Attribute.of(registrationId, oAuth2User.getAttributes());
		
		Map<String, Object> Attributes = oAuth2Attribute.convertToMap();
		
//		ArrayList<SimpleGrantedAuthority> authorities = new ArrayList<>();
//		authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
		return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")), Attributes, "email");
	}
	
	public int oAuth2Register(OAuth2RegisterReqDto oAuth2RegisterReqDto) {
		User userEntity = oAuth2RegisterReqDto.toEntity();
		
		userRepository.saveUser(userEntity);
		return userRepository.saveAuthority(
				Authority.builder()
					.userId(userEntity.getUserId())
					.roleId(1)
					.build()
		);
		
	}
	
	// 암호 비교 
	// DI등록하려고 보니.. IOC에 등록이 안돼있음
	public boolean checkPassword(String email, String password) {
		User userEntity = userRepository.findUserByEmail(email);
		BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

		return passwordEncoder.matches(password, userEntity.getPassword());
	}
	
	public int oAuth2ProviderMerge(OAuth2ProviderMergeReqDto oAuth2ProviderMergeReqDto) {
		User userEntity = userRepository.findUserByEmail(oAuth2ProviderMergeReqDto.getEmail());
		
		String provider = oAuth2ProviderMergeReqDto.getProvider();
		
		if(StringUtils.hasText(userEntity.getProvider())) {
			// 문자가 있는경우
			userEntity.setProvider(userEntity.getProvider() + "," + provider); //기존의 로그인 provider, + @
		}else {
			// 문자가 없는경우
			userEntity.setProvider(provider); // provider
		}
		
		return userRepository.updateProvider(userEntity);
		
	}
}

UserRepository (추가)

package com.study.oauth2.repository;

import org.apache.ibatis.annotations.Mapper;

import com.study.oauth2.entity.Authority;
import com.study.oauth2.entity.User;


@Mapper
public interface UserRepository {
	// 이메일 중복확인
	public User findUserByEmail(String email);
	
	// 유저 등록
	public int saveUser (User user);
	public int saveAuthority(Authority authority);
	public int updateProvider(User user);

	
}

UserMapper.xml (추가)

<update id="updateProvider" parameterType="com.study.oauth2.entity.User">
		update user_tb
		set
			provider = #{provider}
		where
			user_id = #{userId}
</update>

onAuthenticationSuccess (추가)

package com.study.oauth2.security;

import java.io.IOException;
import java.net.URLEncoder;

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

import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import com.study.oauth2.entity.User;
import com.study.oauth2.repository.UserRepository;
import com.study.oauth2.security.jwt.JwtTokenProvider;

import lombok.RequiredArgsConstructor;



@Component
@RequiredArgsConstructor
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
	
	
	private final UserRepository userRepository;
	private final JwtTokenProvider jwtTokenProvider;
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		
		// 로그인이 성공적으로 이루어졌을때 8080 (Back) 으로 온 응답을 3000 (Front)으로 redirect 
		
		OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
		String email = oAuth2User.getAttribute("email");
		String provider = oAuth2User.getAttribute("provider");
		User userEntity = userRepository.findUserByEmail(email);
		
		if(userEntity == null) {
			// 회원가입 실패
			String registerToken = jwtTokenProvider.generateOAuth2RegisterToken(authentication);
			String name = oAuth2User.getAttribute("name");
			response
			.sendRedirect(
					"http://localhost:3000/auth/oauth2/register"
							+"?registerToken=" + registerToken 
							+ "&email=" + email
							+ "&name=" + URLEncoder.encode(name,"UTF-8")
							+ "&provider=" + provider
			);
		}else {
			// 회원가입 성공
			if(StringUtils.hasText(userEntity.getProvider())) {
				// 회원가입이 됐고, provider가 등록된 경우
				if(!userEntity.getProvider().contains(provider)) {
					// 하지만 로그인이된 oauth2 계정의 provider는 등록이 안된 경우
					response.sendRedirect("http://localhost:3000/auth/oauth2/merge"
							+ "?provider=" + provider
							+ "&email=" + email);
				}
			}else {
				// 회원가입은 정상적으로 됐으나, provider가 null인 경우
				response.sendRedirect("http://localhost:3000/auth/oauth2/merge"
										+ "?provider=" + provider
										+ "&email=" + email);
			}
		}
	}
}

``

front

OAuth2Merge.js (추가)

import axios from 'axios';
import React, { useState } from 'react';
import { useMutation } from 'react-query';
import { useSearchParams } from 'react-router-dom';


const OAuth2Merge = () => {
    const providerMerge = useMutation(async (mergeData) => {
        try{
            const response = await axios.put("http://localhost:8080/auth/oauth2/merge", mergeData);
            return response;
        }catch(error){
            /**
             * 비밀번호 틀렸을 때, 토큰이 만료됐을 때 
             */

            setErrorMsg(error.response.data);
            return error;
        }
    },{
        onSuccess: (response) => {
            if(response.status ===200){
                alert("계정 통합 완료!");
                window.location.replace("/auth/login");
            }
        }
    });
    const [password, setPassword] = useState();
    const [errorMsg, setErrorMsg] = useState("");
    const[ searchParams, setSearchParams ] = useSearchParams();
    const email = searchParams.get("email");
    const provider = searchParams.get("provider");

    const passwordChangeHandle = (e) =>{
        setPassword(e.target.value);
    }

    const providerMergeSubmitHandle = () =>{
        providerMerge.mutate({
            email,
            password,
            provider
        })
    }

    return (
        <div>
            <h1>{email}계정을 {provider} 계정과 통합하는 것에 동의 하십니까?</h1>
            <input type="password" onChange={passwordChangeHandle} placeholder='기존 계정의 비밀번호를 입력하세요'  />
            <p>{errorMsg}</p>
            <button onClick={providerMergeSubmitHandle}>동의</button>
            <button>취소</button>
        </div>
    );
};

export default OAuth2Merge;

back

JwtTokenProvider (추가)

package com.study.oauth2.security.jwt;

import java.security.Key;
import java.util.Date;

import javax.management.RuntimeErrorException;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import com.study.oauth2.security.PrincipalUser;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;


@Component

public class JwtTokenProvider {
	private final Key key;
	
	public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) {
		key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey));
	}
	
	
	public String generateAccessToken(Authentication authentication) {
		
		String email = null;
		
		if(authentication.getClass() == UserDetails.class) {
			// PrincipalUser
			PrincipalUser principalUser = (PrincipalUser) authentication.getPrincipal(); //downcasting
			email = principalUser.getEmail();
		}else {
			// OAuth2User
			OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
			email = oAuth2User.getAttribute("email");
		}
		
		
		// 권한 설정
		
		if(authentication.getAuthorities() == null) {
			throw new RuntimeException("등록된 권한이 없습니다.");
		}
		
		StringBuilder roles = new StringBuilder();
		authentication.getAuthorities().forEach(authority -> {
			roles.append(authority.getAuthority() + ",");
		});
		
		roles.delete(roles.length() - 1, roles.length()); //권한 마지막 쉼표 제거
		
		
		Date tokenExpiresDate = new Date(new Date().getTime() + (1000 * 60 * 60 *24));
		
		return Jwts.builder()
				.setSubject("AccessToken")
				.claim("email", authentication)
				.claim("auth", roles)
				.setExpiration(tokenExpiresDate)
				.signWith(key,SignatureAlgorithm.HS256)
				.compact();
	}
	
	
	
	// jwt Token 생성 (회원가입 전용 토큰)
	public String generateOAuth2RegisterToken (Authentication authentication) {
		
		// 만료기간
		Date tokenExpiresDate = new Date(new Date().getTime() + (1000*60*10));
		
		OAuth2User oAuth2User = (OAuth2User)authentication.getPrincipal();
		
		String email = oAuth2User.getAttribute("email");
		 
		
		return  Jwts.builder()
				.setSubject("OAuth2Register")
				.claim("email", email)
				.setExpiration(tokenExpiresDate)
				.signWith(key,SignatureAlgorithm.HS256)
				.compact();
	}
	
	public Boolean validateToken(String token) {
		try {
			Jwts.parserBuilder()
				.setSigningKey(key)
				.build()
				.parseClaimsJws(token);
			return true;
		}catch (Exception e) {
			
		}
		return false;
	}
	
	public String getToken(String jwtToken) {
		String type = "Bearer ";
		if(StringUtils.hasText(jwtToken) && jwtToken.startsWith(type)) {
			return jwtToken.substring(type.length());
		}
		return null;
	}
	
	
}

onAuthenticationSuccess

package com.study.oauth2.security;

import java.io.IOException;
import java.net.URLEncoder;

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

import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import com.study.oauth2.entity.User;
import com.study.oauth2.repository.UserRepository;
import com.study.oauth2.security.jwt.JwtTokenProvider;

import lombok.RequiredArgsConstructor;



@Component
@RequiredArgsConstructor
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
	
	
	private final UserRepository userRepository;
	private final JwtTokenProvider jwtTokenProvider;
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		
		// 로그인이 성공적으로 이루어졌을때 8080 (Back) 으로 온 응답을 3000 (Front)으로 redirect 
		
		OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
		String email = oAuth2User.getAttribute("email");
		String provider = oAuth2User.getAttribute("provider");
		User userEntity = userRepository.findUserByEmail(email);
		
		if(userEntity == null) {
			// 회원가입 실패
			String registerToken = jwtTokenProvider.generateOAuth2RegisterToken(authentication);
			String name = oAuth2User.getAttribute("name");
			response
			.sendRedirect(
					"http://localhost:3000/auth/oauth2/register"
							+"?registerToken=" + registerToken 
							+ "&email=" + email
							+ "&name=" + URLEncoder.encode(name,"UTF-8")
							+ "&provider=" + provider
			);
		}else {
			// 회원가입 성공
			if(StringUtils.hasText(userEntity.getProvider())) {
				if(!userEntity.getProvider().contains(provider)) {
					// 하지만 로그인이된 oauth2 계정의 provider는 등록이 안된 경우
					response.sendRedirect("http://localhost:3000/auth/oauth2/merge"
							+ "?provider=" + provider
							+ "&email=" + email);
				}
				// 회원가입이 됐고, provider가 등록된 경우
				response.sendRedirect("http://localhost:3000/auth/oauth2/login"
						+ "?accessToken=" + jwtTokenProvider.generateAccessToken(authentication));
						
				
			}else {
				// 회원가입은 정상적으로 됐으나, provider가 null인 경우
				response.sendRedirect("http://localhost:3000/auth/oauth2/merge"
										+ "?provider=" + provider
										+ "&email=" + email);
			}
		}
	}
}

Front

Login > OAuth2Login.js

import React from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';

const OAuth2Login = () => {
 
    const navigate = useNavigate();
    const [ searchParams, setSearchParams] = useSearchParams();
    const accessToken = searchParams.get("accessToken");
    

    // !! 해야 정상적으로 형변환이 이루어짐
    if(!!accessToken){
        localStorage.setItem("accessToken", accessToken);
        window.location.replace("/");
    }


    return (
        <></>
    );
};

export default OAuth2Login;

store > atoms

AuthAtoms.js

import { atom } from "recoil";


// 전역상태 
export const authenticationState = atom({
    key:"authenticationState",
    default : false
});

Back

AuthController (추가)

@GetMapping("/authenticated")
	
	public ResponseEntity<?> authenticated(@RequestHeader(value = "Authorization") String accessToken) {
		return ResponseEntity.ok(jwtTokenProvider.validateToken(jwtTokenProvider.getToken(accessToken)));
	}

Front

components > auth

AuthRoute.js

import axios from 'axios';
import React from 'react';
import { useQuery } from 'react-query';
import { Navigate, useNavigate } from 'react-router-dom';
import { useRecoilState } from 'recoil';
import { authenticationState } from '../../store/atoms/AuthAtoms';

const AuthRoute = ({ path, element}) => {

    const navigate = useNavigate();
    const [ authState, setAuthState ] = useRecoilState(authenticationState);

    const authenticated = useQuery(["authenticated"], async () => {
        const option = {
            headers: {
                "Authorization": `Bearer ${localStorage.getItem("accessToken")}` 
            }
        }
        return await axios.get("http://localhost:8080/auth/authenticated", option);
    },{
        onSuccess: (response) => {
            if(response.status === 200){
                if(response.data){
                    setAuthState(true);
                    /**
                     *  window.location.replace("/");
                     *  replace 하는 순간 제랜더링 되면서 상태가 날라가는 현상 발생
                     */
                }
            }
        }
    })


    const permitAllPaths = ["/","/notice"];
    const authenticatedPaths = ["/mypage","/user"];
    const authPath ="/auth"

    if(authenticated.isLoading) {
        return <></>
    }

    if(authState && path.startsWith(authPath)) {
        navigate("/");
    }

    if(!authState && authenticatedPaths.filter(authenticatedPath =>  path.startsWith(authenticatedPath)).length > 0){
        navigate("/auth/login");
    }
    return element;
};

export default AuthRoute;

App.js 수정

import  React from 'react';
import { Routes, Route } from "react-router-dom";
import NotFound from './pages/NotFound/NotFound';
import Login from './pages/Login/Login';
import OAuth2Register from './pages/Register/OAuth2Register';
import OAuth2Merge from './pages/OAuth2Merge/OAuth2Merge';
import Index from './pages/Index/Index';
import OAuth2Login from './pages/Login/OAuth2Login';
import AuthRoute from './components/auth/AuthRoute';


function App() {
  return (
    <>
      <Routes>
        <Route path='/' element={<AuthRoute path={"/"} element={<Index />}/>}/>
        <Route path='/mypage' element={<AuthRoute path={"/mypage"} element={<Index />}/>}/>
        <Route path='/auth/login' element={<AuthRoute path={"/auth/login"} element={<Login />}/>} />
        <Route path='/auth/register' />
        <Route path='/auth/oauth2/login' element={<AuthRoute path={"auth/oauth2/login"} element={<OAuth2Login />}/>} />
        <Route path='/auth/oauth2/register' element={<AuthRoute path={"/auth/oauth2/register"} element={<OAuth2Register />}/>} />
        <Route path='/auth/oauth2/merge' element={<AuthRoute path={"/auth/oauth2/merge"} element={<OAuth2Merge />}/>} />
        <Route path='/*' element={<NotFound />} />
      </Routes>
    </>
  );
}

export default App;

AccessToken이 있을 때

AccessToken이 없을 때

http://localhost:3000/mypage 접속을 누르면
http://localhost:3000/auth/login로 이동 (AccessToken이 없기때문)


최종 Back Code

config

SecurityConfig

package com.study.oauth2.config;

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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import com.study.oauth2.security.OAuth2SuccessHandler;
import com.study.oauth2.service.AuthService;

import lombok.RequiredArgsConstructor;


@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	private final AuthService authService;
	private final OAuth2SuccessHandler oAuth2SuccessHandler;
	
	@Bean
	public BCryptPasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
	
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.httpBasic().disable();
		http.formLogin().disable();
		http.cors();
		http.csrf().disable();
		http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
		http.authorizeRequests()
			.antMatchers("/auth/**")
			.permitAll()
			.anyRequest()
			.authenticated()
			.and()
			.oauth2Login()
			.loginPage("http://localhost:3000/auth/login")
			.successHandler(oAuth2SuccessHandler)
			.userInfoEndpoint()
			.userService(authService);
			
	}

}

WebMvcConfig

package com.study.oauth2.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
	
	@Override
	public void addCorsMappings(CorsRegistry registry) {
		registry.addMapping("/**")
				.allowedOrigins("*")
				.allowedMethods("*");
				
	}
}

controller

AuthController

package com.study.oauth2.controller;

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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.study.oauth2.dto.auth.OAuth2ProviderMergeReqDto;
import com.study.oauth2.dto.auth.OAuth2RegisterReqDto;
import com.study.oauth2.security.jwt.JwtTokenProvider;
import com.study.oauth2.service.AuthService;

import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
	
	private final JwtTokenProvider jwtTokenProvider;
	private final AuthService authService;
	
	@PostMapping("/oauth2/register")
	public ResponseEntity<?> oauth2Register(
			@RequestHeader(value="registerToken") String registerToken,
			@RequestBody OAuth2RegisterReqDto oAuth2RegisterReqDto) {
		
		boolean validated = jwtTokenProvider.validateToken(jwtTokenProvider.getToken(registerToken));
		
		if(!validated) {
			//토큰이 유효하지 않음
			return ResponseEntity.badRequest().body("회원가입 요청 시간이 초과하였습니다.");
		}
		
		return ResponseEntity.ok(authService.oAuth2Register(oAuth2RegisterReqDto));
	}
	
	@PutMapping("/oauth2/merge")
	public ResponseEntity<?> providerMerge(@RequestBody OAuth2ProviderMergeReqDto oAuth2ProviderMergeReqDto){
		
		// 기존의 암호와 비교를 해야함 
		// DB에 암호가 들어있음
		if(!authService.checkPassword(oAuth2ProviderMergeReqDto.getEmail(), oAuth2ProviderMergeReqDto.getPassword())) {
			return ResponseEntity.badRequest().body("비밀번호가 일치하지 않습니다.");
		}
		
		
		return ResponseEntity.ok(authService.oAuth2ProviderMerge(oAuth2ProviderMergeReqDto));
	}
	
	
	@GetMapping("/authenticated")
	
	public ResponseEntity<?> authenticated(@RequestHeader(value = "Authorization") String accessToken) {
		return ResponseEntity.ok(jwtTokenProvider.validateToken(jwtTokenProvider.getToken(accessToken)));
	}
}

dto > auth

OAuth2ProviderMergeReqDto

package com.study.oauth2.dto.auth;

import lombok.Data;

@Data
public class OAuth2ProviderMergeReqDto {
	private String email;
	private String password;
	private String provider;
}

OAuth2RegisterReqDto

package com.study.oauth2.dto.auth;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import com.study.oauth2.entity.User;

import lombok.Data;

@Data
public class OAuth2RegisterReqDto {
	private String email;
	private String name;
	private String password;
	private String checkPassword;
	private String provider;
	
	public User toEntity() {
		return User.builder()
				.email(email)
				.name(name)
				.password(new BCryptPasswordEncoder().encode(password))
				.provider(provider)
				.build();
	}
}

entity

Authority

package com.study.oauth2.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@Builder
@NoArgsConstructor
@Data
public class Authority {
	private int authorityId;
	private int userId;
	private int roleId;
	
	
	private Role role;
	
}

Role

package com.study.oauth2.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor


public class Role {
	private int roleId;
	private String roleName;
}

User

package com.study.oauth2.entity;

import java.util.ArrayList;
import java.util.List;

import com.study.oauth2.security.PrincipalUser;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class User {
	private int userId;
	private String email;
	private String password;
	private String name;
	private String provider;
	
	private List<Authority> authorities;
	
	
	public PrincipalUser toPrincipal() {
		
		List<String> roles = new ArrayList<>();
		
		authorities.forEach(authority ->{
			roles.add(authority.getRole().getRoleName());
		});
		
		return PrincipalUser.builder()
				.userId(userId)
				.email(email)
				.password(password)
				.authorities(authorities)
				.build();
	}
}

repository

UserRepository

package com.study.oauth2.repository;

import org.apache.ibatis.annotations.Mapper;

import com.study.oauth2.entity.Authority;
import com.study.oauth2.entity.User;


@Mapper
public interface UserRepository {
	// 이메일 중복확인
	public User findUserByEmail(String email);
	
	// 유저 등록
	public int saveUser (User user);
	public int saveAuthority(Authority authority);
	public int updateProvider(User user);

	
}

security

OAuth2Attribute

package com.study.oauth2.security;

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

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;

@ToString
@Builder(access = AccessLevel.PRIVATE) //option (NoArg에 private 걸어두는 방법)
@Getter
public class OAuth2Attribute {
    private Map<String, Object> attributes;
    private String email;
    private String name;
    private String provider;

   public static OAuth2Attribute of(String provider, Map<String, Object> attributes) {
        switch (provider) {
            case "google":
                return ofGoogle(provider,attributes);
            case "kakao":
                return ofKakao(provider, attributes);
            case "naver":
                return ofNaver(provider, attributes);
            default:
                throw new RuntimeException();
        }
    }

    private static OAuth2Attribute ofGoogle(String provider, Map<String, Object> attributes) {
        return OAuth2Attribute.builder()
                .name((String) attributes.get("name"))
                .email((String) attributes.get("email"))
                .provider(provider)
                .attributes(attributes)
                .build();
    }

    private static OAuth2Attribute ofKakao(String provider, Map<String, Object> attributes) {
        Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
        Map<String, Object> kakaoProfile = (Map<String, Object>) kakaoAccount.get("profile");

        return OAuth2Attribute.builder()
                .name((String) kakaoProfile.get("nickname"))
                .email((String) kakaoAccount.get("email"))
                .provider(provider)
                .attributes(kakaoAccount)
                .build();
    }

    private static OAuth2Attribute ofNaver(String provider, Map<String, Object> attributes) {
        Map<String, Object> response = (Map<String, Object>) attributes.get("response");

        return OAuth2Attribute.builder()
                .name((String) response.get("name"))
                .email((String) response.get("email"))
                .provider(provider)
                .attributes(response)
                .build();
    }

   public Map<String, Object> convertToMap() {
        Map<String, Object> map = new HashMap<>();
        map.put("name", name);
        map.put("email", email);
        map.put("provider", provider);

        return map;
    }
}

OAuth2SuccessHandler

package com.study.oauth2.security;

import java.io.IOException;
import java.net.URLEncoder;

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

import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import com.study.oauth2.entity.User;
import com.study.oauth2.repository.UserRepository;
import com.study.oauth2.security.jwt.JwtTokenProvider;

import lombok.RequiredArgsConstructor;



@Component
@RequiredArgsConstructor
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
	
	
	private final UserRepository userRepository;
	private final JwtTokenProvider jwtTokenProvider;
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		
		// 로그인이 성공적으로 이루어졌을때 8080 (Back) 으로 온 응답을 3000 (Front)으로 redirect 
		
		OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
		String email = oAuth2User.getAttribute("email");
		String provider = oAuth2User.getAttribute("provider");
		User userEntity = userRepository.findUserByEmail(email);
		
		if(userEntity == null) {
			// 회원가입 실패
			String registerToken = jwtTokenProvider.generateOAuth2RegisterToken(authentication);
			String name = oAuth2User.getAttribute("name");
			response
			.sendRedirect(
					"http://localhost:3000/auth/oauth2/register"
							+"?registerToken=" + registerToken 
							+ "&email=" + email
							+ "&name=" + URLEncoder.encode(name,"UTF-8")
							+ "&provider=" + provider
			);
		}else {
			// 회원가입 성공
			if(StringUtils.hasText(userEntity.getProvider())) {
				if(!userEntity.getProvider().contains(provider)) {
					// 하지만 로그인이된 oauth2 계정의 provider는 등록이 안된 경우
					response.sendRedirect("http://localhost:3000/auth/oauth2/merge"
							+ "?provider=" + provider
							+ "&email=" + email);
					return;
				}
				// 회원가입이 됐고, provider가 등록된 경우
				response.sendRedirect("http://localhost:3000/auth/oauth2/login"
						+ "?accessToken=" + jwtTokenProvider.generateAccessToken(authentication));
						
				
			}else {
				// 회원가입은 정상적으로 됐으나, provider가 null인 경우
				response.sendRedirect("http://localhost:3000/auth/oauth2/merge"
										+ "?provider=" + provider
										+ "&email=" + email);
			}
		}
	}
}

PrincipalUser

package com.study.oauth2.security;

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

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import com.study.oauth2.entity.Authority;

import lombok.Builder;
import lombok.Getter;

@Builder
@Getter
public class PrincipalUser implements UserDetails {
	
	
	private static final long serialVersionUID = 3893676052625302075L;
	
	
	private int userId;
	private String email;
	private String password;

	private List<Authority> authorities;
	

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		
		List<SimpleGrantedAuthority> authorities= new ArrayList<>();
		
		this.authorities.forEach(authority -> {
			authorities.add(new SimpleGrantedAuthority(authority.getRole().getRoleName()));
		});
		
		return authorities;
	}

	@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;
	}

}

jwt

JwtTokenProvider

package com.study.oauth2.security.jwt;

import java.security.Key;
import java.util.Date;

import javax.management.RuntimeErrorException;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import com.study.oauth2.security.PrincipalUser;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;


@Component

public class JwtTokenProvider {
	private final Key key;
	
	public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) {
		key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey));
	}
	
	
	public String generateAccessToken(Authentication authentication) {
		
		String email = null;
		
		if(authentication.getClass() == UserDetails.class) {
			// PrincipalUser
			PrincipalUser principalUser = (PrincipalUser) authentication.getPrincipal(); //downcasting
			email = principalUser.getEmail();
		}else {
			// OAuth2User
			OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
			email = oAuth2User.getAttribute("email");
		}
		
		
		// 권한 설정
		
		if(authentication.getAuthorities() == null) {
			throw new RuntimeException("등록된 권한이 없습니다.");
		}
		
		StringBuilder roles = new StringBuilder();
		authentication.getAuthorities().forEach(authority -> {
			roles.append(authority.getAuthority() + ",");
		});
		
		roles.delete(roles.length() - 1, roles.length()); //권한 마지막 쉼표 제거
		
		
		Date tokenExpiresDate = new Date(new Date().getTime() + (1000 * 60 * 60 *24));
		
		return Jwts.builder()
				.setSubject("AccessToken")
				.claim("email", authentication)
				.claim("auth", roles)
				.setExpiration(tokenExpiresDate)
				.signWith(key,SignatureAlgorithm.HS256)
				.compact();
	}
	
	
	
	// jwt Token 생성 (회원가입 전용 토큰)
	public String generateOAuth2RegisterToken (Authentication authentication) {
		
		// 만료기간
		Date tokenExpiresDate = new Date(new Date().getTime() + (1000*60*10));
		
		OAuth2User oAuth2User = (OAuth2User)authentication.getPrincipal();
		
		String email = oAuth2User.getAttribute("email");
		 
		
		return  Jwts.builder()
				.setSubject("OAuth2Register")
				.claim("email", email)
				.setExpiration(tokenExpiresDate)
				.signWith(key,SignatureAlgorithm.HS256)
				.compact();
	}
	
	public Boolean validateToken(String token) {
		try {
			Jwts.parserBuilder()
				.setSigningKey(key)
				.build()
				.parseClaimsJws(token);
			return true;
		}catch (Exception e) {
			
		}
		return false;
	}
	
	public String getToken(String jwtToken) {
		String type = "Bearer ";
		if(StringUtils.hasText(jwtToken) && jwtToken.startsWith(type)) {
			return jwtToken.substring(type.length());
		}
		return null;
	}
	
	
}

service

AuthService

package com.study.oauth2.service;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import com.study.oauth2.dto.auth.OAuth2ProviderMergeReqDto;
import com.study.oauth2.dto.auth.OAuth2RegisterReqDto;
import com.study.oauth2.entity.Authority;
import com.study.oauth2.entity.User;
import com.study.oauth2.repository.UserRepository;
import com.study.oauth2.security.OAuth2Attribute;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class AuthService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
	
	
	private final UserRepository userRepository;
	 

	@Override
	public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
		
		OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService = new DefaultOAuth2UserService();
		
		OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest);
		
		System.out.println(oAuth2User);
		
		String registrationId = userRequest.getClientRegistration().getRegistrationId(); //Google (문자로 구글, 네이버 카카오를 들고옴)
		
		OAuth2Attribute oAuth2Attribute = OAuth2Attribute.of(registrationId, oAuth2User.getAttributes());
		
		Map<String, Object> Attributes = oAuth2Attribute.convertToMap();
		
//		ArrayList<SimpleGrantedAuthority> authorities = new ArrayList<>();
//		authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
		return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")), Attributes, "email");
	}
	
	public int oAuth2Register(OAuth2RegisterReqDto oAuth2RegisterReqDto) {
		User userEntity = oAuth2RegisterReqDto.toEntity();
		
		userRepository.saveUser(userEntity);
		return userRepository.saveAuthority(
				Authority.builder()
					.userId(userEntity.getUserId())
					.roleId(1)
					.build()
		);
		
	}
	
	// 암호 비교 
	// DI등록하려고 보니.. IOC에 등록이 안돼있음
	public boolean checkPassword(String email, String password) {
		User userEntity = userRepository.findUserByEmail(email);
		BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

		return passwordEncoder.matches(password, userEntity.getPassword());
	}
	
	public int oAuth2ProviderMerge(OAuth2ProviderMergeReqDto oAuth2ProviderMergeReqDto) {
		User userEntity = userRepository.findUserByEmail(oAuth2ProviderMergeReqDto.getEmail());
		
		String provider = oAuth2ProviderMergeReqDto.getProvider();
		
		if(StringUtils.hasText(userEntity.getProvider())) {
			// 문자가 있는경우
			userEntity.setProvider(userEntity.getProvider() + "," + provider); //기존의 로그인 provider, + @
		}else {
			// 문자가 없는경우
			userEntity.setProvider(provider); // provider
		}
		
		return userRepository.updateProvider(userEntity);
		
	
	}
	
}

Oauth2Application

package com.study.oauth2;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Oauth2Application {

	public static void main(String[] args) {
		SpringApplication.run(Oauth2Application.class, args);
	}

}

resources

application.yml

/*
 * 위 내용 참고
 */

mappers

UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.study.oauth2.repository.UserRepository">



	<resultMap type="com.study.oauth2.entity.User" id="userMap">
		<id property="userId" column="user_id"/>
		<result property="email" column="email"/>
		<result property="password" column="password"/>
		<result property="name" column="name"/>
		<result property="provider" column="provider"/>
		<collection property="authorities" javaType="list" resultMap="authorityMap" />
	</resultMap>
	<resultMap type="com.study.oauth2.entity.Authority" id="authorityMap">
		<id property="authorityId" column="authority_id"/>
		<result property="userId" column="user_id"/>
		<result property="roleId" column="role_id"/>
		<association property="role" resultMap="RoleMap"></association>
	
	</resultMap>
	<resultMap type="com.study.oauth2.entity.Role" id="RoleMap">
		<id property="roleId" column="role_id"/>
		<result property="roleName" column="role_name"/>
	</resultMap>

	<select id="findUserByEmail" resultMap="userMap">
	
		select
			ut.user_id,
			ut.email,
			ut.password,
			ut.name,
			ut.provider,
		
			at.authority_id,
			at.user_id,
			at.role_id,
			
			rt.role_id,
			rt.role_name
		from
			user_tb ut
			left outer join authority_tb at on(at.user_id = ut.user_id)
			left outer join role_tb rt on(rt.role_id = at.role_id)
		where
			ut.email = #{email}

	</select>
	
	<insert id="saveUser" 
	parameterType="com.study.oauth2.entity.User"
	useGeneratedKeys="true"
	keyProperty="userId">
		insert into user_tb
		values (0, #{email},#{password},#{name},#{provider})
	</insert>
	
	<insert id="saveAuthority" parameterType="com.study.oauth2.entity.Authority">
		insert into authority_tb
		values (0, #{userId}, #{roleId})
	</insert>
	
	
	<update id="updateProvider" parameterType="com.study.oauth2.entity.User">
		update user_tb
		set
			provider = #{provider}
		where
			user_id = #{userId}
	</update>

</mapper>

최종 Front Code

Login.js

import React from 'react';
import { FcGoogle } from 'react-icons/fc';
import { useNavigate } from 'react-router-dom';
import { SiNaver } from 'react-icons/si';


const Login = () => {


    const navigate = useNavigate();

    const googleAuthClickHandle = () => {
        window.location.href="http://localhost:8080/oauth2/authorization/google";
    }

    const naverAuthClickHandle = () => {
        window.location.href="http://localhost:8080/oauth2/authorization/naver";
    }

    return (
        <div>
            <input type="text" placeholder='email' />
            <input type="password" placeholder='password' />
            <button>로그인</button>
            <button onClick={googleAuthClickHandle}><FcGoogle /></button>
            <button onClick={naverAuthClickHandle}><SiNaver/></button>
        </div>
    );
};

export default Login;

OAuth2Login.js

import React from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';

const OAuth2Login = () => {
 
    const navigate = useNavigate();
    const [ searchParams, setSearchParams] = useSearchParams();
    const accessToken = searchParams.get("accessToken");
    

    // !! 해야 정상적으로 형변환이 이루어짐
    if(!!accessToken){
        localStorage.setItem("accessToken", accessToken);
        window.location.replace("/");
    }


    return (
        <></>
    );
};

export default OAuth2Login;

OAuth2Register.js

import axios from 'axios';
import React, { useState } from 'react';
import { useMutation } from 'react-query';
import { useNavigate, useSearchParams } from 'react-router-dom';

const OAuth2Register = () => {

    const navigate = useNavigate();
    const [passwords, setPasswords] = useState({ password: "", checkPassword: ""});
    const oauth2Register = useMutation(async (registerData) => {
        
        const option = {
            headers:{
                registerToken: `Bearer ${registerToken}`
            }
        }
        
        try{
            const response = await axios.post("http://localhost:8080/auth/oauth2/register", registerData, option);
            return response;
        } catch (error) {
            alert("페이지가 만료되었습니다.");
            window.location.replace("/auth/login");
            return error;
        }  
    },{
        onSuccess: (response) => {
            if(response.status ===200){
                alert("회원가입 완료.");
                window.location.replace("/auth/login");
            }
        }
    });

    const [ searchParams, setSearchParams] = useSearchParams();
    
    const registerToken = searchParams.get("registerToken");
    const email = searchParams.get("email");
    const name = searchParams.get("name");
    const provider = searchParams.get("provider");

    const passwordInputChangeHandle = (e) => {
        const { name, value} = e.target;
        setPasswords({...passwords,[name]: value});
    }

    const oauth2RegisterSubmitHandle = () => {
        oauth2Register.mutate({
            email,
            name,
            ...passwords,
            provider
        });
    }

    return (
        <div>
            <input type="text" value={email} disabled = {true} />
            <input type="text" value={name} disabled= {true} />
            <input type="password" name='password' placeholder="비밀번호" onChange={passwordInputChangeHandle} />
            <input type="password" name='checkPassword' placeholder="비밀번호확인" onChange={passwordInputChangeHandle} />
            <button onClick={oauth2RegisterSubmitHandle}>가입하기</button>
        </div>
    );
};

export default OAuth2Register;

OAuth2Merge.js

import axios from 'axios';
import React, { useState } from 'react';
import { useMutation } from 'react-query';
import { useSearchParams } from 'react-router-dom';





const OAuth2Merge = () => {
    const providerMerge = useMutation(async (mergeData) => {
        try{
            const response = await axios.put("http://localhost:8080/auth/oauth2/merge", mergeData);
            return response;
        }catch(error){
            /**
             * 비밀번호 틀렸을 때, 토큰이 만료됐을 때 
             */

            setErrorMsg(error.response.data);
            return error;
        }
    },{
        onSuccess: (response) => {
            if(response.status ===200){
                alert("계정 통합 완료!");
                window.location.replace("/auth/login");
            }
        }
    });
    const [password, setPassword] = useState();
    const [errorMsg, setErrorMsg] = useState("");
    const[ searchParams, setSearchParams ] = useSearchParams();
    const email = searchParams.get("email");
    const provider = searchParams.get("provider");

    const passwordChangeHandle = (e) =>{
        setPassword(e.target.value);
    }

    const providerMergeSubmitHandle = () =>{
        providerMerge.mutate({
            email,
            password,
            provider
        })
    }

    return (
        <div>
            <h1>{email}계정을 {provider} 계정과 통합하는 것에 동의 하십니까?</h1>
            <input type="password" onChange={passwordChangeHandle} placeholder='기존 계정의 비밀번호를 입력하세요'  />
            <p>{errorMsg}</p>
            <button onClick={providerMergeSubmitHandle}>동의</button>
            <button>취소</button>
        </div>
    );
};

export default OAuth2Merge;

AuthAtoms.js

import { atom } from "recoil";


// 전역상태 
export const authenticationState = atom({
    key:"authenticationState",
    default : false
});

AuthRoute.js

import axios from 'axios';
import React from 'react';
import { useQuery } from 'react-query';
import { Navigate, useNavigate } from 'react-router-dom';
import { useRecoilState } from 'recoil';
import { authenticationState } from '../../store/atoms/AuthAtoms';

const AuthRoute = ({ path, element}) => {

    const navigate = useNavigate();
    const [ authState, setAuthState ] = useRecoilState(authenticationState);

    const authenticated = useQuery(["authenticated"], async () => {
        const option = {
            headers: {
                "Authorization": `Bearer ${localStorage.getItem("accessToken")}` 
            }
        }
        return await axios.get("http://localhost:8080/auth/authenticated", option);
    },{
        onSuccess: (response) => {
            if(response.status === 200){
                if(response.data){
                    setAuthState(true);
                    /**
                     *  window.location.replace("/");
                     *  replace 하는 순간 제랜더링 되면서 상태가 날라가는 현상 발생
                     */
                }
            }
        }
    })


    const permitAllPaths = ["/","/notice"];
    const authenticatedPaths = ["/mypage","/user"];
    const authPath ="/auth"

    if(authenticated.isLoading) {
        return <></>
    }

    if(authState && path.startsWith(authPath)) {
        navigate("/");
    }

    if(!authState && authenticatedPaths.filter(authenticatedPath =>  path.startsWith(authenticatedPath)).length > 0){
        navigate("/auth/login");
    }
    return element;
};

export default AuthRoute;

Index.js

import React from 'react';
import { useRecoilState } from 'recoil';
import { authenticationState } from '../../store/atoms/AuthAtoms';

const Index = () => {

    const [ authState, setAuthState ] = useRecoilState(authenticationState);

    return (
        <div>
           {authState ? "인증됨" : "인증안됨"} 
        </div>
    );
};

export default Index;

NotFound.js

import React from 'react';

const NotFound = () => {
    return (
        <div>
            <h1>페이지를 찾을 수 없습니다.</h1>
        </div>
    );
};

export default NotFound;

App.js

import  React from 'react';
import { Routes, Route } from "react-router-dom";
import NotFound from './pages/NotFound/NotFound';
import Login from './pages/Login/Login';
import OAuth2Register from './pages/Register/OAuth2Register';
import OAuth2Merge from './pages/OAuth2Merge/OAuth2Merge';
import Index from './pages/Index/Index';
import OAuth2Login from './pages/Login/OAuth2Login';
import AuthRoute from './components/auth/AuthRoute';


function App() {
  return (
    <>
      <Routes>
        <Route path='/' element={<AuthRoute path={"/"} element={<Index />}/>}/>
        <Route path='/mypage' element={<AuthRoute path={"/mypage"} element={<Index />}/>}/>
        <Route path='/auth/login' element={<AuthRoute path={"/auth/login"} element={<Login />}/>} />
        <Route path='/auth/register' />
        <Route path='/auth/oauth2/login' element={<AuthRoute path={"auth/oauth2/login"} element={<OAuth2Login />}/>} />
        <Route path='/auth/oauth2/register' element={<AuthRoute path={"/auth/oauth2/register"} element={<OAuth2Register />}/>} />
        <Route path='/auth/oauth2/merge' element={<AuthRoute path={"/auth/oauth2/merge"} element={<OAuth2Merge />}/>} />
        <Route path='/*' element={<NotFound />} />
      </Routes>
    </>
  );
}

export default App;
profile
HW + SW = 1

0개의 댓글