OAuth2 Client 위주로 설명할 것이고, Security와 Redis는 필요한 부분만 설명할 것이다.
OAuth2 Client
에서 다 처리하다보니 중간에 필요한 로직들을 설정하는게 어려울 수 있다. ex) OAuth2 Client는 인가인데, Security는 인증의 단계이므로 중간에 Filter 설정을 해줘야한다.Client
- Client에서도 처리가 가능하나, code는 노출되더라도 accessToken, 유저 정보가 노출되는 것은 민감한 문제다.
RestTemplate
- 무난하게 사용할 수 있으나, JSON 파싱하는 과정에서 코드가 너무 난잡해진다.
OpenFeign
- 일반 Controller와 비슷한 느낌이라 RestTemplate보다 간결
- 하지만 이 역시 카카오, 구글, 네이버 따로 설정해줘야한다.
- 내가 사용하고 있는 Springboot 3 버전에서는 에러가 많이 발생했다.
라이브러리는 무조건 단어의 뜻을 잘 알자! 만드신 분들이 다 신경써서 지은 변수명, 클래스명, 메서드명이다.
Authorization Code Grant
방식이다.# 라이브러리
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.security:spring-security-oauth2-authorization-server:0.4.1'
# yml 설정 => 이 설정을 읽어서 OAuth2 Client 과정이 진행된다!
spring:
security:
oauth2:
client:
registration:
kakao:
clientId: @@@@@@
clientSecret: @@@@@@@
redirectUri: http://localhost:3001/login/oauth2/code/kakao
client-authentication-method: client_secret_post
authorization-grant-type: authorization_code
# scope: profile_nickname, account_email #동의 항목
clientName: Kakao
naver:
clientId: @@@@@
clientSecret: @@@@@
client-authentication-method: client_secret_post
authorizationGrantType: authorization_code
redirectUri: http://localhost:3001/login/oauth2/code/naver
scope:
- nickname
- profile_image
clientName: Naver
google:
clientId: @@@@@
clientSecret: @@@@@
redirectUri: http://localhost:3001/login/oauth2/code/google
scope:
- email
- profile
provider:
kakao:
authUri: https://kauth.kakao.com
authorizationUri: https://kauth.kakao.com/oauth/authorize
tokenUri: https://kauth.kakao.com/oauth/token
userInfoUri: https://kapi.kakao.com/v2/user/me
userNameAttribute: id
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
# 카카오
{
"id":123456789,
"connected_at": "2022-04-11T01:45:28Z",
"kakao_account": {
"profile_nickname_needs_agreement": false,
"profile": {
"nickname": "홍길동"
}
},
"properties":{
"${CUSTOM_PROPERTY_KEY}": "${CUSTOM_PROPERTY_VALUE}",
...
}
}
# 네이버
{
"resultcode": "00",
"message": "success",
"response": {
"email": "openapi@naver.com",
"nickname": "OpenAPI",
"profile_image": "https://ssl.pstatic.net/static/pwe/address/nodata_33x33.gif",
"age": "40-49",
"gender": "F",
"id": "32742776",
"name": "오픈 API",
"birthday": "10-01",
"birthyear": "1900",
"mobile": "010-0000-0000"
}
}
# 구글
{
sub=1030@@@@@00@@0,
name=@@@,
given_name=@@,
family_name=@,
picture=https://lh3.googleusercontent.com/a/AE@@@@TaOLog9sDPN6@@@@C=s96-c, email=@@@@@gmail.com,
email_verified=true,
locale=ko
}
# 유저 정보를 받아오는 인터페이스
public interface OAuth2UserInfo {
String getProviderId();
String getProvider();
String getEmail();
}
# 위와 같이 각 Provider마다 오버라이딩해서 구현해주면 된다.
@AllArgsConstructor
public class NaverUserInfo implements OAuth2UserInfo {
private Map<String, Object> attributes;
// 오버라이딩
}
DefaultOAuth2UserService
참고@Service
@Slf4j
@RequiredArgsConstructor
public class PrincipalOAuth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
# 메서드의 역할
// 1. accessToken으로 서드파티에 요청해서 사용자 정보를 얻어옴
// 2. 해당 사용자가 이미 회원가입 되어있는 사용자인지 확인 후 처리
// 3. UserPrincipal 을 return (세션 방식에서는 여기서 return한 객체가 시큐리티 세션에 저장된다)
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
log.info("getAttributes : {}", oAuth2User.getAttributes());
// 여기서 받아온 사용자 정보를 간단하게 validation해줘도 좋다.
OAuth2UserInfo oAuth2UserInfo = null;
String provider = userRequest.getClientRegistration().getRegistrationId();
LoginType loginType = LoginType.setLoginType(provider);
if (provider.equals(LoginType.KAKAO.getProvider())) {
oAuth2UserInfo = new KakaoUserInfo((Map) oAuth2User.getAttributes());
} else if (provider.equals(LoginType.GOOGLE.getProvider())) {
oAuth2UserInfo = new GoogleUserInfo((Map) oAuth2User.getAttributes());
} else if (provider.equals(LoginType.NAVER.getProvider())) {
oAuth2UserInfo = new NaverUserInfo((Map) oAuth2User.getAttributes().get("response"));
}
User user = 유저를 저장 혹은 불러오는 메서드(oAuth2UserInfo, loginType);
return new PrincipalDetails(user, oAuth2User.getAttributes());
}
@Configuration
@RequiredArgsConstructor
@EnableMethodSecurity
public class SecurityConfig {
private final PrincipalOAuth2UserService principalOAuth2UserService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 대강이나마 이정도로 적어주면 알아서 잘된다.
http.csrf().disable()
.authorizeHttpRequests()
.anyRequest().permitAll()
.and()
.oauth2Login() // loginPage 기본: "/login"
.successHandler(successHandler())
.userInfoEndpoint()
.userService(
principalOAuth2UserService);//구글 로그인이 완료된(구글회원) 뒤의 후처리가 필요함 . Tip.코드x, (엑세스 토큰+사용자 프로필 정보를 받아옴)
return http.build();
}
UserDetailsService
UserDetails
OAuth2User
(UserDetails와 같은 역할)
@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<User> foundUser = userRepository.findByName(username);
return foundUser.map(PrincipalDetails::new).orElse(null);
}
}
// UserDetails도 구현한 이유는 일반로그인을 가능하게하기 위함이다.
@Data
public class PrincipalDetails implements UserDetails, OAuth2User {
private User user;
private Map<String, Object> attributes;
// 일반 로그인
public PrincipalDetails(User user) {
this.user = user;
}
// OAuth 로그인
public PrincipalDetails(User user, Map<String, Object> attributes) {
this.user = user;
this.attributes = attributes;
}
.....
}
username
이다!@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<User> foundUser = userRepository.findByEmail(username);
return foundUser.map(PrincipalDetails::new).orElse(null);
}
}
구글 로그인
네이버 로그인
카카오
- This class supports client_secret_basic, client_secret_post, and none by default.
=> client-authentication-method 값을 POST가 아니라 client_secret_post로 변경하면 해결
네이버
- 로컬환경에서 "@@앱에 로그인할 수 없습니다."
=> 서비스, 리다이렉트 url을 127.0.0.1 -> localHost
구글
- "엑세스 차단됨: 이 앱의 요청이 잘못되었습니다."
=> 설정파일에 redirect url이 있는지?
https://oauth.net/2/
https://developerbee.tistory.com/245 ⇒ OauthUser attributes 객체 및 inner static 활용
https://chb2005.tistory.com/183 ⇒ 매우 좋은 자료인듯(이걸 중점으로 참고함)
https://co-de.tistory.com/29
https://ttl-blog.tistory.com/249
https://kim-jong-hyun.tistory.com/150
https://velog.io/@kyunghwan1207/22년도-하계-모각코-Springboot-Security와-OAuth2.0을-활용한-소셜로그인구글개발-4st08oc3
https://wildeveloperetrain.tistory.com/248
https://velog.io/@jinmin2216/Spring-Security-Spring-Security-Oauth2-JWT-2.-%EC%8B%A4%EC%8A%B5#securityconfig
https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/oauth2/client/OAuth2AuthorizedClient.html
https://lotuus.tistory.com/78
https://ws-pace.tistory.com/102
https://devwithpug.github.io/spring/oauth2-testing-with-mockmvc/
https://gaga-kim.tistory.com/347
https://bcp0109.tistory.com/379
https://king-ja.tistory.com/106
https://velog.io/@ch_kang/Spring-FeignClient로-카카오-로그인-구현
https://velog.io/@ads0070/카카오-로그인-API로-로그인-인증하기
https://velog.io/@park2348190/JWT에서-Session으로의-전환
글이 잘 정리되어 있네요. 감사합니다.