[모각코 / 230822] spring security OAuth 로그인 구현하기
안녕하세요 모각코 6일차입니다.
오늘은 구글 로그인을 진행해보았습니다.
구글 클라우드 플랫폼에 들어갑니다.
https://console.cloud.google.com/apis
Google 클라우드 플랫폼
로그인 Google 클라우드 플랫폼으로 이동
accounts.google.com
새 프로젝트를 생성해줍니다.
저는 springboot-oauth-google로 만들었습니다.
그리고 OAuth 동의 화면에 들어가서 외부로 열어주고 URI를 http://localhost:8080/login/oauth2/code/google 를 입력합니다. 구글 로그인을 진행한 후 코드를 받게 되는 url 입니다.
그리고 사용자 인증 정보에서 사용자 인증 정보를 만들어줍니다.
만들어진 clientId를 applicaion.yml 파일에 추가해줍니다.
spring:
security:
oauth2:
client:
registration:
google:
client-id: 클라이언트 id
client-secret: 클라이언트 pwd
scope:
- email
- profile
이제 구글 로그인을 위한 준비를 마쳤습니다.
파일 구조는 다음과 같습니다.
loginForm.html 파일에 구글 로그인 주소를 입력합니다.
/oauth2/authorization/google 은 구글 로그인을 진행하는 정해져있는 url로 오타를 주의해야합니다.
<a href="/oauth2/authorization/google">구글 로그인</a>
SecurityConfig.java 파일에 이를 추가해줍니다.
OAuth2를 진행할 때는
1. 코드 받기 (인증)
2. 엑세스 토큰 받기 (권한)
3. 사용자 프로필 정보를 가져온다
4-1. 그 정보를 토대로 회원가입 자동으로 진행
4-2. 이메일, 전화번호, 이름, 아이디 필요 시 추가적인 회원가입 필요
과정이 있는데 구글 로그인의 경우에는 로그인 완료 후에 엑세스 토큰과 사용자 프로필 정보를 바로 받게 됩니다.
그래서 그 후처리 과정을 principalOauth2UserService를 통해 진행하려 합니다.
.and()
.oauth2Login()
.loginPage("/loginForm") // 구글 로그인이 완료된 후 엑세스 토큰 + 사용자 프로필 정보 받음 코드X
.userInfoEndpoint()
.userService(principalOauth2UserService);
security 안에 session 에는 authentication 타입이 들어갈 수 있습니다.
그 안에서 일반 로그인 사용자의 경우 UserDetails, OAuth 사용자의 경우 OAuth2User 를 갖게 되는데 이 두 개를 편하게 관리하기 위해 PrincipalDetails에 UserDetails와 OAuth2User를 둘 다 상속받습니다.
public class PrincipalDetails implements UserDetails, OAuth2User {}
PrincipalOauth2UserService.java 파일입니다.
@Service
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Autowired
private UserRepository userRepository;
// 구글로부터 받은 userRequest data 에 대해 후처리되는 함수
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
String provider = userRequest.getClientRegistration().getClientId();
String providerId = oAuth2User.getAttribute("sub");
String username = provider + "_" + providerId;
String email = oAuth2User.getAttribute("email");
String password = bCryptPasswordEncoder.encode("겟인데어");
String role = "ROLE_USER";
User user = userRepository.findByUsername(username);
if (user == null) {
userRepository.save(User.builder()
.username(username)
.email(email)
.password(password)
.role(role)
.provider(provider)
.providerId(providerId)
.build());
} else {
System.out.println("구글 로그인을 한 적이 있습니다");
}
return new PrincipalDetails(user, oAuth2User.getAttributes());
}
}
이제 일반 유저나 OAuth 유저 모두 PrincipalDetails로 정보를 받게 됩니다.
@GetMapping("/user")
public @ResponseBody String user(@AuthenticationPrincipal PrincipalDetails principalDetails) {
return "user";
}
그런데 여기서 빈 순환 참조의 문제가 발생하게 됩니다.
***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| securityConfig (field private com.example.security.config.oauth.PrincipalOauth2UserService com.example.security.config.SecurityConfig.principalOauth2UserService)
↑ ↓
| principalOauth2UserService (field private org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder com.example.security.config.oauth.PrincipalOauth2UserService.bCryptPasswordEncoder)
└─────┘
Action:
Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
Process finished with exit code 0
spring은 application context bean 생성 과정에서 생길 수 있는 문제점을 컴파일 과정에서 오류로 감지하기 때문에 @Lazy 어노테이션(지연 로딩)을 통해 해결했습니다. 이는 좋은 방법은 아니고 추후에 프로젝트에서는 좋은 설계로 진행하여 이러한 문제가 나오지 않도록 해야합니다.
@Autowired
private @Lazy BCryptPasswordEncoder bCryptPasswordEncoder;