이번 시간은 Kakao 로그인이다.
🤔 왜 OAUTH2를 진행했나?
OAUTH2를 사용하는 또다른 중요한 이유는 회원에 대한 중요한 개인정보(ex. 계정과 계정에 대한 비밀번호, 기타 개인정보)를 내 DB에 저장하지 않음으로써 혹시라도 노출될 사고를 줄일 수 있다는 점때문에 사용하였다. 결론적으로 사용자와 개발자 모두에게 편의성과 보안성을 제공할 수 있기 때문에 진행하게 되었다.
📌 Spring Security 동작 원리
모든 책임을 백엔드가 맡음
필자는 소셜 로그인을 백엔드로만 진행했다. 보통 백엔드와 프론트엔드를 섞어서 프론트에서 코드를 받거나 토큰을 발급받는 경우가 있는데 카카오와 같은 대형 서비스 개발 포럼 및 보안 규격에서는 책임을 나누어 받는 것을 지양한다. 따라서 모든 책임을 프론트에서 맡거나, 모든 책임을 백엔드에서 맡아야 하는데 필자는 모든 책임을 백엔드에서 맡는 방법으로 구현하였다.
동작 순서를 간단히 보자면
- 클라이언트가 소셜 로그인 버튼을 클릭한다.
- 백엔드로 하이퍼링크 요청이 날라온다.
- 백엔드에서 카카오 로그인 페이지(카카오 인증서 버)로 리다이렉트 시켜준다.
- 클라이언트가 로그인을 시도한다.
- 카카오 인증서버에서 로그인이 성공하면 뒤에 code = ??? 이런식으로 인증 코드를 발급해준다.
- 이 인증 코드를 갖고 설정한 개인정보를 볼 수 있는 엑세스 토큰(카카오에서 발급해주는 엑세스 토큰)을 요청한다.
- 엑세스 토큰을 인증서버에서 발급받는다.
- 이 엑세스 토큰으로 유저에 대한 정보를 요청한다.
- 유저 정보를 리소스 서버에서 발급받는다.
- 유저 정보를 확인하고 우리 서버에 회원가입이 되어있는지 확인하고 안되어 있으면 우리 DB에 정보를 저장하고(중요한 정보 보단 사용자의 이메일이나, 프로필 사진 정도) 우리 서버를 이용할 수 있는 AccessToken, RefreshToken을 발급해주고, 되어 있다면 마찬가지로 Access,Refresh 토큰을 발급해준다.
위 과정을 진행하기 위해 먼저 카카오 로그인을 하기 위해서는 카카오 디벨로퍼에 내 어플리케이션을 등록한다.
애플리케이션 추가하기
애플리케이션을 등록한뒤 비즈앱을 신청해야 한다. 비즈앱을 신청해야 카카오 간편가입을 사용할 수 있다.
비즈앱 신청하기
이렇게 신청 자격을 휙득하고
개인정보 동의학목 심사 신청
개인정보 동의항목 심사 신청을 한 뒤 밑에서 카카오 로그인 후에 얻어야 할 정보들 필자는 닉네임, 프로필 사진, 이메일이 필요해서 다 필수 동의로 진행했다.
카카오 로그인 활성화
그러고 난 뒤 카카오 로그인을 진행할 것이기 때문에 카카오 로그인 활성화를 ON 해준다.
플랫폼 사이트 도메인 등록
그러고 난 뒤 사이트 도메인을 등록한다. 사이트 도메인을 등록함으로써 Redirect URI만 등록할 경우 사전에 등록되지 않은 도메인으로의 리디렉션 요청은 거부할 수 있기 때문에 도메인 등록은 꼭 해주어야 한다.
Redirect URI 설정
Redirct URI를 설정한다. 여기서 Redirect URI는 내가 받을 서버의 주소를 말한다.
백엔드 구현
구현 순서
- application.yml 설정
- SecurityConfig 설정
- CustomOAuthUserService 구현
- OAuth2Response 구현
- KakaoResponse 구현
- CustomOAuth2User 구현
- CustomSuccessHandler구현
그러면 스프링으로 돌아가서 로직을 구현해보자.
1. application.yml 설정
registration - kakao
- Client-name : 설정 파일 내에서 클라이언트를 구분하는 데 사용 (Naver는 client-name 이 naver이다.) 이건 자유롭게 지정 가능 (Kakao를 kak 이런식으로 지정해도 상관 x)
- Client-id : 앱 설정 -> 앱 키 - > Rest API 키
- Client-secret : 제품 설정 -> 카카오 로그인 -> 보안 -> Clinet Secret
- redirect-uri : 인가 코드와 엑세스 토큰을 받을 내 주소
- authorization-grant-type : authorization_code 권한을 수여받을 타입을 지정
- scope : 내가 받고 싶은 권한 (위에서 이메일,닉네임,프로필 이미지를 받고 싶으므로)
provider - kakao
- authorization-uri : 카카오에서 설정한 로그인 폼을 보여줄 즉 authorization_code를 받을 페이지
- token-uri : 카카오에서 설정한 토큰을 받을 페이지
- user-info-uri : 유저 정보를 받을 페이지
- user-name-attribute : 카카오에서는 요청 정보를 id 키에 담아서 리턴한다. 그래서 id 로 설정
구현에 들어가기 앞서 이 정보들이 어떻게 쓰이는지 정리해보겠다.
- 프론트에서 로그인 버튼 클릭
- SpringSecurity가 이 요청을 가로채서 https://kauth.kakao.com/oauth/authorize로 인가 코드 요청을 보낸다. 이때 client-id와 redirect-uri, 응답 유형 등의 정보가 같이 넘어간다.(카카오 에서도 어떤 앱인지 알아야 함으로)
- 사용자는 이메일과 비밀번호를 입력하여 로그인한다.
- 로그인이 성공하면 카카오에서 설정한 redirect-uri와 yml에서 설정한 redirect-uri 즉 내 서버로 요청을 받기 위해 redirect-uri로 인가 코드를 반환받는다.
- SpringSecurity는 인가 코드를 받아서 https://kauth.kakao.com/oauth/token 으로 엑세스 토큰 요청을 자동으로 보낸다. 이때 clinet-id와 client-secret, 엑세스 토큰(인가 코드), redirect-uri등이 포함된다. client-secret이 있는 이유는 내 애플리이션에서 카카오쪽으로 요청을 보내는게 맞는지 확인하기 위해서이다. (애플리케이션의 신원을 확인)
- 엑세스 토큰을 받은 후 SpringSecurity는 https://kapi.kakao.com/v2/user/me 로 받은 엑세스 토큰을 이용하여 사용자 정보를 받아온다.
2. SecurityConfig 설정
@Configuration
@EnableWebSecurity // security 활성화 어노테이션
@RequiredArgsConstructor
public class SecurityConfig {
private final CorsConfigurationSource corsConfigurationSource;
//AuthenticationManager가 인자로 받을 AuthenticationConfiguraion 객체 생성자 주입
private final AuthenticationConfiguration authenticationConfiguration;
private final JwtUtil jwtUtil;
private final TokenRepository tokenRepository;
private final CustomOAuth2UserService customOAuth2UserService;
private final CustomSuccessHandler customSuccessHandler;
//AuthenticationManager Bean 등록
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
http
// CSRF 비활성화 (token을 사용하는 방식이기 때문에 csrf.token이 필요 없음)
.csrf(CsrfConfigurer::disable)
//시큐리티 폼 로그인 방식 사용 x
.formLogin(auth -> auth.disable())
//httpBasic방식 사용 x -> Bearer 방식
.httpBasic(auth -> auth.disable())
//oauth2
.oauth2Login(oauth2 -> oauth2.userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig.userService(customOAuth2UserService)).successHandler(customSuccessHandler))
// corsConfig 사용
.cors(cors -> cors.configurationSource(corsConfigurationSource))
//세션 사용 하지 않음(ALWAYS, IF_REQUIRED, NEVER등)
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth.dispatcherTypeMatchers().permitAll()
.requestMatchers("/login").permitAll()
.requestMatchers("/api/v1/refresh-token").permitAll()
.requestMatchers("/api/v1/users").permitAll()
.requestMatchers("/api/v1/users/email-check").permitAll()
.requestMatchers("/api/v1/users/**").hasAuthority("USER")
.anyRequest().authenticated()) // 허가된 사람만 인가
.addFilterAt(new JWTFilter(jwtUtil),LoginFilter.class)
.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil, tokenRepository), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new CustomLogoutFilter(jwtUtil, tokenRepository), LogoutFilter.class);
return http.build();
}
}
사실 이전 글에서 달라진건 oauth2Login 설정이 들어간 것 뿐이다.
3. OAuthUserService 설정
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
System.out.println(oAuth2User);
String registrationId = userRequest.getClientRegistration().getRegistrationId();
OAuth2Response oAuth2Response = null;
if (registrationId.equals("kakao")) {
oAuth2Response = new KakaoResponse(oAuth2User.getAttributes());
}
else {
return null;
}
//db에 유저가 있는지 판단
Optional<User> byUser = userRepository.findByEmail(oAuth2Response.getEmail());
//회원가입 유저 없으면 (이부분 수정)
if(byUser.isEmpty()){
String username = oAuth2Response.getProvider()+" "+oAuth2Response.getName();
User user = new User(SocialType.KAKAO,oAuth2Response.getEmail(),username,Role.USER,oAuth2Response.getProfileImage());
userRepository.save(user);
return new CustomOAuth2User(user);
}else{
//회원가입 유저 있으면 로그인 진행
User user = byUser.get();
return new CustomOAuth2User(user);
}
}
}
이제 Security와 OAuth로 카카오 요청이 성공적으로 마치고 난다면 CustomOAuth2UserService로 요청이 들어오게 될 것이다. 여기서 이제 내 DB에 회원 정보가 없으면 회원 정보를 넣어주고, 회원 가입된 유저가 있으면 OAuth2Response에 담아서 성공적으로 로그인 해주기 위해 return해준다.
4. OAuth2Response 설정
public interface OAuth2Response {
//제공자 (ex. naver, kakao)
String getProvider();
//제공자에서 발급해주는 아이디(번호)
String getProviderId();
//이메일
String getEmail();
//이름
String getName();
//사진
String getProfileImage();
}
5. KakaoResponse 설정
@RequiredArgsConstructor
public class KakaoResponse implements OAuth2Response{
private final Map<String, Object> attributes;
@Override
public String getProvider() {
return "kakao";
}
@Override
public String getProviderId() {
return attributes.get("id").toString();
}
@Override
public String getEmail() {
Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
return kakaoAccount.get("email").toString();
}
@Override
public String getName() {
Map<String, Object> properties = (Map<String, Object>) attributes.get("properties");
return properties.get("nickname").toString();
}
@Override
public String getProfileImage() {
Map<String, Object> properties = (Map<String, Object>) attributes.get("properties");
return properties.get("profile_image").toString();
}
}
6. CustomOAuth2User 설정
@RequiredArgsConstructor
public class CustomOAuth2User implements OAuth2User {
private final User user;
@Override
public Map<String, Object> getAttributes() {
return null;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return user.getRole().getName();
}
});
return collection;
}
public User getUser(){ return user;}
public Long getUserId(){ return user.getId();}
@Override
public String getName() {
return user.getEmail();
}
public String getNickname(){
return user.getNickname();
}
}
7. CustomSuccessHandler 설정
@Component
@RequiredArgsConstructor
public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JwtUtil jwtUtil;
private final TokenRepository tokenRepository;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//OAuth2User
CustomOAuth2User customUserDetails = (CustomOAuth2User) authentication.getPrincipal();
User user = customUserDetails.getUser();
Long userId = customUserDetails.getUserId();
String username = customUserDetails.getName();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
GrantedAuthority auth = iterator.next();
String role = auth.getAuthority();
//유효기간 30분
String accessToken = jwtUtil.createAccessToken("accessToken", userId, role,30*60*1000L);
//유효기간 30일
String refreshToken = jwtUtil.createRefreshToken("refreshToken", userId, 30*24*60*60*1000L);
Token refreshEntity = Token.toEntity(user, refreshToken, LocalDateTime.now().plusDays(30));
tokenRepository.save(refreshEntity);
response.addCookie(createCookie("accessToken", accessToken));
response.addCookie(createCookie("refreshToken", refreshToken));
response.sendRedirect("http://localhost:3000/");
}
private Cookie createCookie(String key, String value){
Cookie cookie = new Cookie(key, value);
cookie.setMaxAge(24*60*60);
// cookie.setSecure(true);
// cookie.setPath("/");
cookie.setHttpOnly(true);
return cookie;
}
}
카카오 로그인이 성공한다면 CustomSuccessHandler로 와서 토큰을 만들어서 유저에게 반환을 해주는 것으로 마무리 됩니다.
혹시 진행하다가 궁금한게 있으시면 댓글로 남겨주세요! 성심성의껏 답변드리겠습니다.
'프로젝트 > WMS' 카테고리의 다른 글
[CI/CD] EC2+도커+젠킨스+NIGNX 배포하기 (1) (1) | 2024.11.04 |
---|---|
[JPA] List 조회시 N+1문제 Fetch join으로 성능 개선하기 (0) | 2024.08.08 |
[AWS/S3] Spring에서 S3에 데이터 저장하기 (0) | 2024.08.06 |
Spirng Security + JWT + OAUTH2 를 활용한 일반로그인, 소셜로그인(Kakao) (1) (1) | 2024.07.02 |
Entity의 생성일과 수정일을 자동으로 관리하자 ( BaseEntity ) (0) | 2024.06.05 |