OAuth란, OAuth원리, 프론트 토큰 저장 방식
안녕하세요.
저는 이번에 프로젝트를 진행하면서 프론트를 맡았는데요.
OAuth를 이용하여 로그인을 구현하였습니다.
그래서 제가 공부한 것들을 정리할겸 블로그에 적어 놓습니다.
목차는
1. OAuth란?
2. OAuth원리
3. JWT토큰 프론트에 저장하기
1. OAuth란?
OAuth(Open Authorization)는 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는, 접근 위임을 위한 개방형 표준입니다. 이 매커니즘은 여러 기업들에 의해 사용되는데, 이를테면 카카오, 구글, 깃허브가 있으며 사용자들이 타사 애플리케이션이나 웹사이트의 계정에 관한 정보를 공유할 수 있게 허용합니다.
2. OAuth과정
※블로그에서는 카카오부분만 작성했고, 또한 개인키가 있어서 코드를 수정했습니다.
1. 로그인 버튼 누름
웹서버에서 사용자가 로그인 버튼을 누릅니다. OAuth는 여기서부터 시작됩니다.
카카오로 로그인 하기 버튼을 누르면 시작 되는데요 코드로 확인해볼까요?
{/* 2.인가코드 요청 */}
<Button href={KAKAO_AUTH_URI} className='login_button' variant="contained" style={{ backgroundColor: '#fee500' }} size='large'>
<img src={kakao} alt='kakao' />
<div className='logosb'>Login with Kakao</div>
</Button>
2. 인가코드 요청
버튼을 누르면 KAKAO_AUTH_URI로 이동하여 코드를 요청합니다.
3. 인가코드를 받음
인가코드를 받은 프론트는 REST_API의 POST로 백으로 인가코드를 보냅니다.
4. 백으로 인가코드 전달
//4.인가코드 전달
const res = await axios.post(`/api/oauth/token?code=${code}&provider=${provider}`);
5. 인가코드로 엑세스 토큰 요청
6. 액세스 토큰 발급
@Autowired
UserRepository userRepository;
//5 액세트 토큰 요청
public OauthToken getAccessToken(String code, String provider) {
RestTemplate rt = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
String grantType;
String clientId;
String clientSecret;
String redirectUri;
if (provider.equals("kakao")) {
grantType = "authorization_code";
clientId = "개인마다다름";
clientSecret = "개인마다다름";
redirectUri = "http://localhost:3000/oauth/callback/kakao";
} else {
throw new IllegalArgumentException("Invalid Provider: " + provider);
}
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", grantType);
params.add("client_id", clientId);
params.add("redirect_uri", redirectUri);
params.add("code", code);
params.add("client_secret", clientSecret);
HttpEntity<MultiValueMap<String, String>> tokenRequest = new HttpEntity<>(params, headers);
ResponseEntity<String> tokenResponse = rt.exchange(
getProviderTokenUrl(provider),
HttpMethod.POST,
tokenRequest,
String.class
);
ObjectMapper objectMapper = new ObjectMapper();
OauthToken oauthToken = null;
//6. 액세스 토큰 발급
oauthToken = objectMapper.readValue(tokenResponse.getBody(), OauthToken.class);
return oauthToken;
}
7. 토큰으로 회원 정보 요청
8. 회원 정보반환
9. 회원정보 저장
10. JWT 발행
public String SaveUserAndGetToken(String token, String provider){
//7. 토큰으로 회원 정보 요청
if (provider.equals("kakao")) {
KakaoProfile profile = findKakaoProfile(token);
//회원 정보 조회 by Email
//User user = userRepository.findByEmail(profile.getKakao_account().getEmail());
User user = userRepository.findByEmailAndProvider(profile.getKakao_account().getEmail(),provider);
//8. 회원 정보반환
//9. 회원정보 저장
if (user == null) {
user = User.builder()
.id(profile.getId())
.profileImg(profile.getKakao_account().getProfile().getProfile_image_url())
.nickname(profile.getKakao_account().getProfile().getNickname())
.email(profile.getKakao_account().getEmail())
.provider(provider)
.userRole("ROLE_USER").build();
userRepository.save(user);
} else {
log.info("기존 회원 -> 회원 가입 건너 뜀");
}
//10. JWT 발행
return createToken(user);
11. JWT전달
public ResponseEntity getLogin(String code,String provider){
//5 액세스 토큰 발급
OauthToken oauthToken = oAuthService.getAccessToken(code, provider);
//8 액세스 토큰으로 회원 정보 저장, JWT생성
String jwtToken = oAuthService.SaveUserAndGetToken(oauthToken.getAccess_token(), provider);
//JWT 선언
HttpHeaders headers = new HttpHeaders();
headers.add(JwtProperties.HEADER_STRING, JwtProperties.TOKEN_PREFIX + jwtToken);
//11.프론트로 헤더에 담아 JWT전달
return ResponseEntity.ok().headers(headers).body("\"success\"");
}
12. 프론트 JWT 저장
const token = res.headers.authorization;
//11. 프론트 JWT토큰 저장
sessionStorage.setItem('token',token)
13. JWT로 정보요청
try {
axios.get('/api/me',{
headers: {
Authorization: token,
},
}).then((response) => {//api의 응답을 제대로 받은경우
console.log(response);
console.log(response.data);
setIsLogined((prev) => {
return {
//로그인 상태
state : true,
img: response.data.profileImg,
name: response.data.nickname,
email: response.data.email,
info: "",
token: String(token)
};
});
});
} catch (e) {
console.error(e);
}
14. 백에서 응답 - 이 때 백에선 정보를 카카오에서 가져오는 것이 아닌 미리 저장딘 Mysql DB에서 가져옵니다.
@GetMapping("/me")
public ResponseEntity<Object> getCurrentUser(HttpServletRequest request) throws Exception { //(1)
return userService2.getCurrentUser(request);
}
15. 로그인 후 페이지로 리다이렉트
- 홈페이지로 리다이렉트 합니다. 정상적으로 로그인하면 토큰이 생성되었으므로 AfterLogin화면으로 넘어갑니다.
export default function Home(props) {
const [isLogined,setIsLogined] = useRecoilState(loginState);
const cookie = sessionStorage.getItem('token')
return (
<>
{cookie?<AfterLogin/>:<BeforeLogin scrollRef={props.scrollRef}/>}
</>
)
}
3. 프론트에 JWT 저장하기
사실 이 부분을 쓰기 위해 OAuth의 과정을 설명했습니다.
멘토님께 피드백 받을 때 이 부분을 지적 받았거든요.
프론트에 토큰을 저장할 수 있는 저장소를 알아봅시다.
Local Storage | Session Storage | Cookies | |
저장 방식 | key:value | key:value | key:value |
저장 위치 | 로컬 | 로컬 | 로컬 |
새로고침 | 유지 | 유지 | 유지 |
탭 종료시 | 유지 | 삭제 | 유지 |
보안 | XSS 위험 | XSS 위험 | XSS, CSRF |
서버 부하 | O | O | X |
저장공간 | 무제한 | 무제한 | 4KB |
맨 처음에는 쿠키에 저장했었습니다. 이렇게 말이죠.
React에서 쿠키에 정보를 저장하는 방법은 간단합니다.
일단 React-cookie를 다운받습니다.
npm i react-cookie
그다음 사용할 파일 위에 import합니다.
import {setCookie,getCookie,removeCookie} from '../components/login/cookie'
쿠키 설정 코드입니다.
const expirationTime = new Date();
expirationTime.setTime(expirationTime.getTime() + 30 * 60 * 1000); //30분
setCookie('survey',surveyList,{
//path제한 전체는 "/"
path:"/",
sameSite: "strict",
//만료시간
expires: expirationTime
});
쿠키는 다양한 기능을 설정할 수 있습니다.
먼저 path입니다. path를 추가로 설정하면 특정 주소에서만 쿠키를 사용할 수 있습니다.
저는 문서 저장에 쿠키를 사용했는데요.
모든 곳에 사용 되어야 하기에 "/"로 했습니다.
samesite는 3가지 중 선택할 수 있습니다.
None |
SameSite 가 탄생하기 전 쿠키와 동작하는 방식이 같습니다. None으로 설정된 쿠키의 경우 크로스 사이트 요청의 경우에도 항상 전송됩니다. 즉, 서드 파티 쿠키도 전송됩니다. 따라서, 보안적으로도 SameSite 적용을 하지 않은 쿠키와 마찬가지로 문제가 있는 방식입니다. |
Strict | 가장 보수적인 정책입니다. Strict로 설정된 쿠키는 크로스 사이트 요청에는 항상 전송되지 않습니다. 즉, 서드 파티 쿠키는 전송되지 않고, 퍼스트 파티 쿠키만 전송됩니다. |
Lax | Strict에 비해 상대적으로 느슨한 정책입니다. Lax로 설정된 경우, 대체로 서드 파티 쿠키는 전송되지 않지만, 몇 가지 예외적인 요청에는 전송됩니다. |
만료시간은 30분으로 설정했습니다.
설정하는 식은 ChatGPT에게 물어보면 친절하게 알려줍니다.
*추가할 수 있는 옵션
HTTP secure : 브라우저에서 쿠키의 값을 접근할 수 없도록 막습니다.
secure : HTTPS 통신 외에서는 쿠키를 전달하지 않습니다.
사용예시
//쿠키 생성
const expirationTime = new Date();
expirationTime.setTime(expirationTime.getTime() + 30 * 60 * 1000);
setCookie('survey',surveyList,{
path:"/",
sameSite: "strict",
expires: expirationTime,
HttpOnly : true,
secure: true
});
//쿠키 가져오기
const surveyCookie = getCookie("survey");
//쿠키 삭제
removeCookie('survey')
만약 이 설정을 미리 알았다면 JWT토큰도 쿠키를 써도 됐었을 것 같네요.
그래서 프로젝트에선 JWT토큰은 세션 스토리지에 저장합니다. 이렇게 말이죠
코드는 좀 더 간단합니다.
//세션스토리지에 저장
sessionStorage.setItem('token',token)
//가져오기
const cookie = sessionStorage.getItem('token')
//삭제
sessionStorage.removeItem('token')
//local스토리지에 저장
localStorage.setItem('token',token)
//가져오기
const cookie = localStorage.getItem('token')
//삭제
localStorage.removeItem('token')
참고한 블로그
'IT' 카테고리의 다른 글
Kafka로 CDC구현하기(3/3) - Kafka Sink Connector 생성해서 Postgres 연결하기 (0) | 2023.06.26 |
---|---|
Kafka로 CDC구현하기(2/3) - Kafka Sink Connector 생성해서 Mysql 연결하기 (0) | 2023.06.20 |
Kafka로 CDC구현하기(1/3) - Kafka Source Connector 생성하기 (0) | 2023.06.19 |