티스토리 뷰
Spring Boot 2.7.0 과 OAuth 2 를 사용하여 구글, 네이버, 카카오 로그인 구현 방법을 알아보겠습니다.
1. OAuth2를 사용하기 위한 의존 설정을 해줍니다.
저는 gradle을 사용하였습니다.
dependencies {
...
// oauth
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
...
}
2. 각 서비스에 들어가서 프로젝트를 등록하고 clientId, clientSecret을 발급받습니다.
먼저 구글을 알아보겠습니다.
다음으로 네이버 개발자 페이지에 접속합니다.
마지막으로 카카오 개발자 페이지에 접속합니다.
3. OAuth2 설정 파일을 작성합니다.
application-oauth.yml 파일을 작성하고 application.yml 파일에 추가해줍니다.
spring:
# Security OAuth
security:
oauth2.client:
registration:
google:
clientId: 'GOOGLE_CLIENT_ID'
clientSecret: 'GOOCLE_CLIENT_SECRET'
scope:
- email
- profile
naver:
clientId: 'NAVER_CLIENT_ID'
clientSecret: 'NAVER_CLIENT_SECRET'
clientAuthenticationMethod: post
authorizationGrantType: authorization_code
redirectUri: "{baseUrl}/{action}/oauth2/code/{registrationId}"
scope:
- nickname
- email
- profile_image
clientName: Naver
kakao:
clientId: 'KAKAO_CLIENT_ID'
clientSecret: 'KAKAO_CLIENT_SECRET'
clientAuthenticationMethod: post
authorizationGrantType: authorization_code
redirectUri: "{baseUrl}/{action}/oauth2/code/{registrationId}"
scope:
- profile_nickname
- profile_image
- account_email
clientName: Kakao
# Provider 설정
provider:
naver:
authorizationUri: https://nid.naver.com/oauth2.0/authorize
tokenUri: https://nid.naver.com/oauth2.0/token
userInfoUri: https://openapi.naver.com/v1/nid/me
userNameAttribute: response
kakao:
authorizationUri: https://kauth.kakao.com/oauth/authorize
tokenUri: https://kauth.kakao.com/oauth/token
userInfoUri: https://kapi.kakao.com/v2/user/me
userNameAttribute: id
spring:
profiles:
active: local
include: oauth
...
# 토큰 관련 secret Key 및 RedirectUri 설정
app:
auth:
tokenSecret: TOKEN_SECRET
tokenExpiry: 1800000
refreshTokenExpiry: 604800000
oauth2:
authorizedRedirectUris:
- http://localhost:3000/oauth/redirect
4. Member 엔티티를 작성합니다.
OAuth2 로그인을 통해 전달된 정보를 DB에 저장하는 용도로 사용하는 엔티티입니다. 각자 프로젝트에 맞게 구성하면 됩니다.
package com.beeallho.undefinedauth.domain;
import com.beeallho.undefinedauth.constant.Role;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Entity
@Getter
@NoArgsConstructor
public class Member {
@Id @GeneratedValue
private Long id;
@Column(nullable = false, length = 100, unique = true)
private String email;
@Column(nullable = false, length = 100)
private String name;
@Column(nullable = false, length = 100, unique = true)
private String nickName;
@Column(length = 11)
private String phoneNumber;
@Column
private String imageUrl;
@Column(length = 1)
private String gender;
@Column(length = 8)
private String birthDate;
@Column(length = 50)
@Enumerated(EnumType.STRING)
private Role role;
@Builder
public Member(String name, String email, String picture, Role role){
this.name = name;
this.email = email;
this.imageUrl = picture;
this.role = role;
}
public Member update(String name, String picture){
this.name = name;
this.imageUrl = picture;
return this;
}
public String getRoleKey(){
return this.role.getCode();
}
}
5. SecurityConfig를 작성합니다.
package com.beeallho.undefinedauth.config;
import com.beeallho.undefinedauth.constant.Role;
import com.beeallho.undefinedauth.service.CustomOAuth2UserService;
import lombok.RequiredArgsConstructor;
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;
@Configuration
@EnableWebSecurity // Spring Security 설정 활성화
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomOAuth2UserService customOAuth2UserService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.headers().frameOptions().disable() // h2-console 화면을 사용하기 위해 해당 옵션 disable
.and()
.authorizeRequests()// URL별 권한 권리
.antMatchers("/","/css/**","/images/**","/js/**","/h2-console/**").permitAll()
.antMatchers("/api/v1/**").hasRole(Role.USER.name()) // /api/v1/** 은 USER권한만 접근 가능
.anyRequest().authenticated() // anyRequest : 설정된 값들 이외 나머지 URL 나타냄, authenticated : 인증된 사용자
.and()
.logout()
.logoutSuccessUrl("/")
.and()
.oauth2Login()
.userInfoEndpoint() // oauth2 로그인 성공 후 가져올 때의 설정들
// 소셜로그인 성공 시 후속 조치를 진행할 UserService 인터페이스 구현체 등록
.userService(customOAuth2UserService); // 리소스 서버에서 사용자 정보를 가져온 상태에서 추가로 진행하고자 하는 기능 명시
}
}
6. CustomOAuth2UserService를 작성합니다.
package com.beeallho.undefinedauth.service;
import com.beeallho.undefinedauth.domain.Member;
import com.beeallho.undefinedauth.repository.MemberRepository;
import com.beeallho.undefinedauth.service.model.OAuthAttributes;
import com.beeallho.undefinedauth.service.model.SessionMember;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 javax.servlet.http.HttpSession;
import java.util.Collections;
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final MemberRepository memberRepository;
private final HttpSession httpSession;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
// OAuth2 서비스 id (구글, 카카오, 네이버)
String registrationId = userRequest.getClientRegistration().getRegistrationId();
// OAuth2 로그인 진행 시 키가 되는 필드 값(PK)
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
// OAuth2UserService
OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());
Member member = saveOrUpdate(attributes);
httpSession.setAttribute("user", new SessionMember(member)); // SessionMember (직렬화된 dto 클래스 사용)
// TODO: JWT 생성
return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority(member.getRoleKey())),
attributes.getAttributes(),
attributes.getNameAttributeKey());
}
// 유저 생성 및 수정 서비스 로직
private Member saveOrUpdate(OAuthAttributes attributes){
Member member = memberRepository.findByEmail(attributes.getEmail())
.map(entity -> entity.update(attributes.getName(), attributes.getPicture()))
.orElse(attributes.toEntity());
return memberRepository.save(member);
}
}
7. OAuthAttribute를 작성합니다.
package com.beeallho.undefinedauth.service.model;
import com.beeallho.undefinedauth.constant.Role;
import com.beeallho.undefinedauth.domain.Member;
import lombok.Builder;
import lombok.Getter;
import java.util.Map;
@Getter
public class OAuthAttributes {
private Map<String, Object> attributes; // OAuth2 반환하는 유저 정보 Map
private String nameAttributeKey;
private String name;
private String email;
private String picture;
@Builder
public OAuthAttributes(Map<String, Object> attributes, String nameAttributeKey, String name, String email, String picture) {
this.attributes = attributes;
this.nameAttributeKey = nameAttributeKey;
this.name = name;
this.email = email;
this.picture = picture;
}
public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map<String, Object> attributes){
// 여기서 네이버와 카카오 등 구분 (ofNaver, ofKakao)
switch (registrationId) {
case "google":
return ofGoogle(userNameAttributeName, attributes);
case "naver":
return ofNaver(userNameAttributeName, attributes);
case "kakao":
return ofKakao(userNameAttributeName, attributes);
}
// TODO: Exception 발생
return null;
}
private static OAuthAttributes ofGoogle(String userNameAttributeName, Map<String, Object> attributes) {
return OAuthAttributes.builder()
.name((String) attributes.get("name"))
.email((String) attributes.get("email"))
.picture((String) attributes.get("picture"))
.attributes(attributes)
.nameAttributeKey(userNameAttributeName)
.build();
}
private static OAuthAttributes ofNaver(String userNameAttributeName, Map<String, Object> attributes) {
// naver는 response에 유저정보가 있다.
Map<String, Object> response = (Map<String, Object>)attributes.get("response");
return OAuthAttributes.builder()
.name((String) response.get("name"))
.email((String) response.get("email"))
.picture((String) response.get("profile_image"))
.attributes(attributes)
.nameAttributeKey(userNameAttributeName)
.build();
}
private static OAuthAttributes ofKakao(String userNameAttributeName, Map<String, Object> attributes) {
// kakao는 kakao_account에 유저정보가 있다. (email)
Map<String, Object> kakaoAccount = (Map<String, Object>)attributes.get("kakao_account");
// kakao_account안에 또 profile이라는 JSON객체가 있다. (nickname, profile_image)
Map<String, Object> kakaoProfile = (Map<String, Object>)kakaoAccount.get("profile");
return OAuthAttributes.builder()
.name((String) kakaoProfile.get("nickname"))
.email((String) kakaoAccount.get("email"))
.picture((String) kakaoProfile.get("profile_image_url"))
.attributes(attributes)
.nameAttributeKey(userNameAttributeName)
.build();
}
public Member toEntity(){
return Member.builder()
.name(name)
.email(email)
.picture(picture)
.role(Role.USER)
.build();
}
}
8. 로그인 확인
저는 Vue를 사용하여 로그인 할 수 있는 프론트 프로젝트를 구성하였습니다. 아래 프로젝트를 실행하고 (서버 port 맞추기) 로그인을 수행하면 DB에 정상적으로 데이터가 저장되는 것을 확인할 수 있습니다.
https://github.com/deepIify/oauth-login-fe
참고
- https://deeplify.dev/back-end/spring/oauth2-social-login
- https://github.com/deepIify/oauth-login-be
- https://velog.io/@swchoi0329/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0%EC%99%80-OAuth-2.0%EC%9C%BC%EB%A1%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84#1-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0%EC%99%80-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-oauth2-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8
'Spring' 카테고리의 다른 글
[Validation] @Valid 사용하기 (1) | 2022.06.28 |
---|---|
[Redis] SpringBoot + Redis 연동하기 (0) | 2022.06.08 |
[JPA] Could not extract ResultSet 에러 (1) | 2021.12.02 |
로그인 기능에 jwt 적용하기 (0) | 2021.07.12 |
Spring Security 로그인 구현하기 (0) | 2021.07.12 |
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- Baekjoon
- map
- 소수
- 에라토스테네스의 체
- DFS
- 순열
- string
- sort
- CodeDeploy
- CodePipeline
- SWIFT
- EC2
- 수학
- BFS
- 프로그래머스
- Algorithm
- Dynamic Programming
- spring
- programmers
- array
- Combination
- ECR
- CodeCommit
- 조합
- permutation
- search
- AWS
- java
- cloudfront
- ionic
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
글 보관함