Kakao OAuth2 + JWT + Redis๋ฅผ ํตํ ์ธ์ฆ ๊ณผ์ ๊ตฌํ (1) - ์นด์นด์ค ๋ก๊ทธ์ธ
by rlaehddnd0422DND ์ฌ์ด๋ ํ๋ก์ ํธ์์ ์ธ์ฆ์ผ๋ก ์นด์นด์ค ๋ก๊ทธ์ธ์ ์ฑํํ์ฌ ์งํํ๊ฒ ๋์์ต๋๋ค.
Spring Security์ JWT, OAuth2 Client, Redis๋ฅผ ์ฌ์ฉํ์ฌ ์นด์นด์ค ๋ก๊ทธ์ธ์ ํ๋ก์ ํธ์ ์ด๋ป๊ฒ ์ ์ฉํ๋์ง ํ ๋ฒ ์ ๋ฆฌํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
๋จผ์ ์นด์นด์ค ๋ก๊ทธ์ธ ๊ณผ์ ์ ์ง์ด๋ด ์๋ค.
Step 1. ์ธ๊ฐ ์ฝ๋ ๋ฐ๊ธฐ
1. ์นด์นด์ค ์ธ์ฆ ์๋ฒ๋ก /oauth/authorize URI๋ก GET ์์ฒญ์ ๋ณด๋ ๋๋ค.
2. ์นด์นด์ค ์ธ์ฆ ์๋ฒ์์ ํด๋ผ์ด์ธํธ์๊ฒ ์นด์นด์ค ๋ก๊ทธ์ธ ํ์ด์ง๋ฅผ ํตํด ๋ก๊ทธ์ธ์ ์์ฒญํฉ๋๋ค.
3. ์ฌ์ฉ์๋ ์นด์นด์ค๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ
4. ์นด์นด์ค๊ณ์ ์ด ์ ํจํ ๊ฒฝ์ฐ ์นด์นด์ค ์ธ์ฆ ์๋ฒ์์ ํด๋ผ์ด์ธํธ์๊ฒ ๋์ ํ๋ฉด์ ํตํด ์ฌ์ฉ์ ์ ๋ณด ์์ง ๋์๋ฅผ ์์ฒญํฉ๋๋ค.
5. ํด๋ผ์ด์ธํธ๊ฐ ๋์ํ ํญ๋ชฉ์ ์นด์นด์ค ์ธ์ฆ ์๋ฒ์๊ฒ ์์ฒญํฉ๋๋ค.
6. ์นด์นด์ค ์ธ์ฆ ์๋ฒ์์๋ 302 Redirect URI๋ก ์๋น์ค ์๋ฒ์๊ฒ ์ธ๊ฐ ์ฝ๋๋ฅผ ์ ๋ฌํฉ๋๋ค.
* ์ฌ๊ธฐ์ Redirect URI๋ Kakao Developers - ๋ด ์ ํ๋ฆฌ์ผ์ด์ - ์นด์นด์ค ๋ก๊ทธ์ธ์์ ์ถ๊ฐํ URI๋ก ์ค์ ๋ฉ๋๋ค.
Step 2. ํ ํฐ ๋ฐ๊ธฐ
์ด์ ์๋น์ค ์๋ฒ๋ก ๋ฐ์ ์ธ๊ฐ ์ฝ๋๋ฅผ ๊ฐ์ง๊ณ ์ฌ์ฉ์ ์ ๋ณด์ ๋ํ ํ ํฐ์ ๋ฐ๊ธ๋ฐ์ ์ ์์ต๋๋ค.
1. ์๋น์ค ์๋ฒ๊ฐ Redirect URI๋ก ์ ๋ฌ๋ฐ์ ์ธ๊ฐ ์ฝ๋๋ก ํ ํฐ ๋ฐ๊ธฐ๋ฅผ ์์ฒญํฉ๋๋ค.
2. ์นด์นด์ค ์ธ์ฆ ์๋ฒ๊ฐ ํ ํฐ์ ๋ฐ๊ธํด์ ์๋น์ค ์๋ฒ์ ์ ๋ฌํฉ๋๋ค.
* ์ฌ๊ธฐ์ ํท๊ฐ๋ฆด ์ ์๋๋ฐ, ์ด ํ ํฐ์ ์๋น์ค ์๋ฒ์์ ์ฌ์ฉํ ์ก์ธ์ค ํ ํฐ์ด ์๋๋ผ๋ ์ ์ ์ฃผ์ํฉ์๋ค.
Step 3. ์ฌ์ฉ์ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ
์นด์นด์ค ์๋ฒ๊ฐ ํด์ฃผ์ด์ผ ํ ์ผ์ ๋ชจ๋ ๋๋ฌ์ต๋๋ค. ์ด ๋ถ๋ถ์ ์๋น์ค ์๋ฒ์ ์ง์ ๊ตฌํํด์ผ ํ๋ ๋ถ๋ถ์ผ๋ก Step2 ์์ ํ ํฐ์ ํตํด ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ฐ์์ผ๋ ์ด ์ ๋ณด๋ฅผ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ ์ง ์๋ฒ์ ๊ตฌํํ๋ฉด ๋๊ฒ ์ต๋๋ค.
์๋น์ค ์๋ฒ์ ๊ตฌํํ ์นด์นด์ค ๋ก๊ทธ์ธ ๋ก์ง ์ฝ๋๋ฅผ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
0. ์ค์ ์ ๋ณด
application.yml
- Redirect URI๋ ์นด์นด์ค ๋๋ฒจ๋กํผ์ค์ ์ค์ ํ URI์ ๋์ผํ๊ฒ ์ง์ ํด์ค๋๋ค.
- Client ID, Client Secret์ ์นด์นด์ค ๋๋ฒจ๋กํผ์ค์์ ๋ฐ๊ธ๋ฐ์ ๊ฐ์ผ๋ก ์ง์ ํด์ค๋๋ค.
- authorization-grant-type์ ์ธ๊ฐ ์ฝ๋ ๋ฐฉ์์ ์ฌ์ฉํ๋ฏ๋ก authorization_code ์ง์ ํด์ฃผ๊ณ , client-authentication-method๋ ๋ฐ๋์ cllient_secret_post๋ก ์ง์ ํด์ฃผ์ด์ผ ํฉ๋๋ค.
gradle
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-security'
- ์ธ์ฆ ํํฐ๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด Security๋ฅผ, ์นด์นด์ค ๋ก๊ทธ์ธ์ ์ฌ์ฉํ๊ธฐ ์ํด OAuth2๋ฅผ ์ถ๊ฐํฉ๋๋ค.
1. KakaoDetails
KakaoMemberDetails
@RequiredArgsConstructor
public class KakaoMemberDetails implements OAuth2User {
private final String email;
private final List<? extends GrantedAuthority> authorities;
private final Map<String, Object> attributes;
@Override
public String getName() {
return email;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
}
- Step 3์ ๋ก๊ทธ์ธ ์ฑ๊ณต ํ์ฒ๋ฆฌ ์๋น์ค๋ฅผ ๊ตฌํํ๊ธฐ์ ์์ ํ์ฒ๋ฆฌ ์๋น์ค์์ ํ์ํ ์ ๋ณด๋ค์ ๋จผ์ ๊ตฌํํ๊ฒ ์ต๋๋ค.
- ์ด ํด๋์ค๋ ์ธ์ฆ ๊ฐ์ฒด์ธ Authentication ๊ฐ์ฒด์์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ด๊ธฐ ์ํ ํด๋์ค๋ก, ์ธ์คํด์ค๋ ์นด์นด์ค ๊ณ์ ์ email๊ณผ ์ฌ์ฉ์์ ๊ถํ์ธ authority๊ฐ ์ปฌ๋ ์ ํํ๋ก ํ๋๋ก ๊ฐ์ง๋๋ฐ์. gradle์ ์ถ๊ฐํ OAuth2 Client๊ฐ ์ ๊ณตํ๋ OAuth2User ์ธํฐํ์ด์ค์ ๊ตฌํ์ฒด๋ก ์์ฑํด์ผ Authenticaiton ๊ฐ์ฒด ์์ ๋ด์ ์ ์๊ธฐ ๋๋ฌธ์ OAuth2User๋ฅผ ๊ตฌํํ์ฌ getter๋ฅผ ์ค๋ฒ๋ผ์ด๋ฉ ํ์์ต๋๋ค.
KakaoUserInfo
public class KakaoUserInfo {
public static final String KAKAO_ACCOUNT = "kakao_account";
public static final String EMAIL = "email";
private Map<String, Object> attributes;
public KakaoUserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
public String getEmail() {
ObjectMapper objectMapper = new ObjectMapper();
TypeReference<Map<String, Object>> typeReferencer = new TypeReference<Map<String, Object>>() {
};
Object kakaoAccount = attributes.get(KAKAO_ACCOUNT);
Map<String, Object> account = objectMapper.convertValue(kakaoAccount, typeReferencer);
return (String) account.get(EMAIL);
}
}
๋ก๊ทธ์ธ์ ์ฑ๊ณตํ๊ฒ ๋๋ฉด ํ์ฒ๋ฆฌ ์๋น์ค์์ ๋ฐ์ ์ฌ์ฉ์ ์ ๋ณด๋ค์ KakaoMemberDetails๋ก ์บ์คํ ํ๊ธฐ์ํ ์ ๋ณด๋ค์ ๋๋ค. ์ฐ์ ํ์ฒ๋ฆฌ ์๋น์ค ์ฝ๋๋ฅผ ๋จผ์ ๋ณด๋๊ฒ ์ดํด๊ฐ ๋ ๋น ๋ฅผ ์๋ ์๊ฒ ๋ค์. ํ์ฒ๋ฆฌ ์๋น์ค ์ฝ๋๋ฅผ ๋จผ์ ๋ณด๊ฒ ์ต๋๋ค.
KakaoMemberDetailsService - ์นด์นด์ค ๋ก๊ทธ์ธ ์ฑ๊ณต ํ์ฒ๋ฆฌ ์๋น์ค ๊ฐ์ฒด
Step2์์ ํ ํฐ์ ํตํด ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ฐ๊ฒ ๋๋ฉด ์ด๋ ๊ฒ DefaultOAuth2UserService์ OAuth2UserRequest ๊ฐ์ฒด์ ์ฌ์ฉ์ ์ ๋ณด๊ฐ ๋ด๊ธฐ๊ฒ ๋ฉ๋๋ค!
public class DefaultOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
...
private Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter = new OAuth2UserRequestEntityConverter();
...
}
ํ์ฒ๋ฆฌ ์๋น์ค๋ ์ฌ์ฉ์๊ฐ ๋น์ฆ๋์ค ๋ก์ง์ ๋ง๊ฒ ์ปค์คํ ํด์ ๊ตฌํํด์ผ ํฉ๋๋ค. ๋๋ฌธ์ ์ด DefaultOAuth2UserService๋ฅผ ์์๋ฐ์ ์๊ตฌ์ฌํญ์ ๋ง๊ฒ ์ปค์คํฐ๋ง์ด์ง ํด๋ณด๊ฒ ์ต๋๋ค.
KakaoMemberDetailsService
@Service
@RequiredArgsConstructor
public class KakaoMemberDetailsService extends DefaultOAuth2UserService {
private static final String PREFIX = "๋ฏ์ ";
private final MemberRepository memberRepository;
@Transactional
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
KakaoUserInfo kakaoUserInfo = new KakaoUserInfo(oAuth2User.getAttributes());
Member member = memberRepository.findByEmail(kakaoUserInfo.getEmail())
.orElseGet(() ->
memberRepository.save(
Member.builder()
.email(kakaoUserInfo.getEmail())
.role(Role.USER)
.nickName(PREFIX)
.gender(Gender.NONE)
.updateAgeCount(0)
.updateGenderCount(0)
.build()
)
);
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(member.getRole().name());
return new KakaoMemberDetails(String.valueOf(member.getEmail()),
Collections.singletonList(authority),
oAuth2User.getAttributes());
}
}
DefaultOAuth2UserService์ loadUser ๋ฉ์๋๋ ๋ก๊ทธ์ธ ์ฑ๊ณต ํ ๋์ํ๋ ๋ฉ์๋๋ก ์ค์ ์ ์ผ๋ก ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ ์ง ์์ฑํด์ผ ํ๋ ๊ตฌํ๋ถ์ ๋๋ค.
OAuth2User oAuth2User = super.loadUser(userRequest);
1. ๋จผ์ request๋ก๋ถํฐ ์ฌ์ฉ์ ์ ๋ณด๊ฐ ๋ด๊ธด ๊ฐ์ฒด๋ฅผ ๊บผ๋ด์ต๋๋ค.
KakaoUserInfo kakaoUserInfo = new KakaoUserInfo(oAuth2User.getAttributes());
2. ์ฌ์ฉ์ ์ ๋ณด๊ฐ ๋ด๊ธด ๊ฐ์ฒด์์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊บผ๋ด์ KakaoUserInfo์ ๋ด์ต๋๋ค.
์ ๊ตณ์ด KakaoUserInfo๊ฐ ํ์ํ ๊น?
- ์ฌ์ฉ์ ์ ๋ณด๊ฐ ๋ด๊ธด OAuth2User ๊ฐ์ฒด์ attributes๋ฅผ ์ถ๋ ฅํด๋ณด๋ฉด, ๋ก๊ทธ์ธ ์ ์ ์ ๋ณด๊ฐ ์๋์ ๊ฐ์ด Map<String, Object> ํํ๋ก ์ ์ฅ๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค.
- ๊ทธ ์ค์์๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํ๊ธฐ ์ํด ํ์ํ ์ด๋ฉ์ผ ์ ๋ณด๋ Map<String(key:kakao_account), Map<String,String>> ํํ๋ก ์ ์ฅ๋์ด ์๊ธฐ ๋๋ฌธ์ ์ด ์ ๋ณด๋ค์ ํธํ๊ฒ ํ์ฑํด์ค๊ธฐ ์ํด KakaoUserInfo ํด๋์ค๋ฅผ ๋ง๋ค์ด ์ฃผ์์ต๋๋ค.
oauth2User.getAttributes() = {
setPrivacyInfo=true,
id=3292176436,
connected_at=2024-01-21T09:45:17Z,
properties={nickname=๊น๋์
},
kakao_account = {profile_nickname_needs_agreement=false, profile={nickname=๊น๋์
}, has_email=true, email_needs_agreement=false, is_email_valid=true, is_email_verified=true, email=kdo0422@nate.com}
}
KakaoUserInfo๋ฅผ ๋ค์ ์ดํด๋ด ์๋ค.
public class KakaoUserInfo {
public static final String KAKAO_ACCOUNT = "kakao_account";
public static final String EMAIL = "email";
private Map<String, Object> attributes;
public KakaoUserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
public String getEmail() {
ObjectMapper objectMapper = new ObjectMapper();
TypeReference<Map<String, Object>> typeReferencer = new TypeReference<Map<String, Object>>() {
};
Object kakaoAccount = attributes.get(KAKAO_ACCOUNT);
Map<String, Object> account = objectMapper.convertValue(kakaoAccount, typeReferencer);
return (String) account.get(EMAIL);
}
}
KakaoUserInfo๋ฅผ ์๋์ฒ๋ผ oauth2User.getAttributes()๋ก ์์ฑํ๋ฉด,
KakaoUserInfo kakaoUserInfo = new KakaoUserInfo(oAuth2User.getAttributes());
์ด์ kakaoUserInfo.getEmail()์ ํตํด
1. Map<String, Map<String,String>> attributes์ ๋ด๊ธด kakao_account key์ value๋ฅผ ๊ฐ์ ธ์์
2. kakao_account key์ value์ธ Map<String, String>์์ email์ ๊ฐ์ ธ์ค๋๋ก ํจ์ผ๋ก์จ, ์ค์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ insertํ ํ์ ์ ๋ณด ์ค ํ๋์ธ email์ ํธํ๊ฒ ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค.
๋ณธ๋ก ์ผ๋ก ๋์์ ๋ง์ ํ์ฒ๋ฆฌ ๋ฉ์๋์ธ loadUser()๋ก ๋์์ค๊ฒ ์ต๋๋ค.
KakaoUserInfo kakaoUserInfo = new KakaoUserInfo(oAuth2User.getAttributes());
Member member = memberRepository.findByEmail(kakaoUserInfo.getEmail())
.orElseGet(() ->
memberRepository.save(
Member.builder()
.email(kakaoUserInfo.getEmail())
.role(Role.USER)
.nickName(PREFIX)
.gender(Gender.NONE)
.updateAgeCount(0)
.updateGenderCount(0)
.build()
)
);
- ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ํตํด kakaoUserInfo๋ฅผ ์์ฑํด์ฃผ๊ณ , kakaoUserInfo๋ก ํ์ ๋ฆฌํฌ์งํ ๋ฆฌ์ ํด๋น ํ์์ด ์กด์ฌํ๋ ์ง ์กฐํํ์ฌ ์์ผ๋ฉด ์์ฑ ํ ๋ฆฌํดํ๊ณ ์์ผ๋ฉด ๋ฆฌํดํ๋ ๋ฐฉ์์ผ๋ก ๊ตฌํํ์์ต๋๋ค.
- ์ฌ๊ธฐ์ ๋ฆฌํฌ์งํ ๋ฆฌ์ ํ์์ด ์๋ ๊ฒฝ์ฐ์๋ email ํ๋๋ง ์นด์นด์ค ๋ก๊ทธ์ธ ๊ณ์ ์ผ๋ก ๋ฑ๋กํด์ฃผ๊ณ , ๋๋จธ์ง ํ๋๋ค์ ์๊ตฌ์ฌํญ์ ๋ง๊ฒ ๊ธฐ๋ณธ์ ์ธ ์ ๋ณด๋ค์ Builder๋ก ์์ฑํ์ฌ ๋ฆฌํฌ์งํ ๋ฆฌ์ ์ ์ฅํด์ฃผ์์ต๋๋ค.
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(member.getRole().name());
3. OAuth2User ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ KakaoMemberDetails์ ํ์์ ๊ถํ ์ ๋ณด๋ฅผ ๋ด๊ธฐ ์ํด ์ ์ฅ๋ ํ์์ผ๋ก๋ถํฐ ๊ถํ ์ ๋ณด๋ฅผ ๊บผ๋ด์ค๊ณ , SimpleGrantedAuthority ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด ์ฃผ์์ต๋๋ค.
return new KakaoMemberDetails(String.valueOf(member.getEmail()),
Collections.singletonList(authority),
oAuth2User.getAttributes());
4. KakaoMemberDetails๋ ๊ถํ ์ ๋ณด๋ฅผ Collection ํํ์ ํ๋๋ก ๊ฐ๊ธฐ ๋๋ฌธ์ 3์์ ๋ง๋ ๊ถํ์ ํํ์ ๋ง๊ฒ ๋ณ๊ฒฝํ์ฌ KakaoMemberDetails๋ฅผ ์์ฑ ํ ๋ฆฌํดํฉ๋๋ค.
์ด๋์ ๋ฆฌํด๋ ๊น?
- ์ฌ๊ธฐ์ ๋ฆฌํด๋ UserDetails ๊ฐ์ฒด๋ ์ฌ์ฉ์ ์ธ์ฆ ์ ๋ณด๋ฅผ ๋ํ๋ด๊ธฐ ์ํด Authentication ๊ฐ์ฒด์ ๋ด๊ฒจ์ง๊ณ , ์ด Authentication ๊ฐ์ฒด๋ ์ฌ์ฉ์์ ์ธ์ฆ ์ํ๋ฅผ ๋ํ๋ด๋ฉฐ, SecurityContext์ ์ ์ฅ๋๋ค๊ณ ๋ณด์๋ฉด ๋๊ฒ ์ต๋๋ค.
๋ง์ง๋ง์ผ๋ก ๊ตฌํํ ํ์ฒ๋ฆฌ ์๋น์ค ํด๋์ค๋ฅผ SecurityConfig์ ๋ค์๊ณผ ๊ฐ์ด ๋ฑ๋กํด์ฃผ๋ฉด ๋ !
.oauth2Login(oAuth2Login -> {
oAuth2Login.userInfoEndpoint(userInfoEndpointConfig ->
userInfoEndpointConfig.userService(kakaoMemberDetailsService));
});
์ด๋ ๊ฒ ์นด์นด์ค ๋ก๊ทธ์ธ์ ๊ตฌํ์ด ๋๋ฌ์ต๋๋ค. ์์ง JWT ์ ์ฉ์ด ๋จ์๋๋ฐ์. ๋ค์ ํฌ์คํ ์ผ๋ก ์ด์ด๊ฐ๊ฒ ์ต๋๋ค.
'๐ Backend > Spring Security' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
๋ธ๋ก๊ทธ์ ์ ๋ณด
Study Repository
rlaehddnd0422