2024.01.07 - [Spring Boot] - [Spring Boot] 로그인 기능 구현 (0) - 공통 기능, 코드 구현
1. OAuth 란 ?
- 사용자가 비밀번호를 사용하지 않고 소셜 서비스 (구글, 카카오톡 등) 의 접근 권한을 현재의 다른 애플리케이션에 안전하게 위임할 수 있도록 하는 개방형 표준 프로토콜
- 쉽게 말하면 사이트에 회원가입하지 않고 구글, 카카오톡 등으로 로그인 하는 기능
- 액세스 토큰 (Access Token) 사용
- 사용자의 로그인 정보를 직접 주고받지 않고 서비스 간의 인증, 인가 기능이 가능함
2. OAuth 사용 로그인 구현
- 기존의 스프링 시큐리티 로그인 구현 ( 2024.01.07 - [Spring Boot] - [Spring Boot] 로그인 기능 구현 (3) - 스프링 시큐리티 로그인 ) 을 재사용함
2-1. 구글 API 콘솔 프로젝트 생성
- 구글 API 콘솔 ( https://console.cloud.google.com/apis/dashboard?project=oauth-login-411109 ) 접속
- 프로젝트 선택 > 새 프로젝트
- 프로젝트 이름 : google-login > 만들기
2-2. 동의 화면 구성
- 사용자 인증 정보 > 동의 화면 구성
- User Type : 외부 > 만들기
- 앱 이름 : OAuth2.0 구글 로그인 예제
- 사용자 지원 이메일 선택
- 개발자 연락처 정보 > 이메일 주소 : 본인 이메일 입력
- 다른 것 추가하지 않고 '저장 후 계속'
2-3. 사용자 인증 정보 (OAuth 클라이언트) 생성
- 사용자 인증 정보 만들기 > OAuth 클라이언트 ID
- 애플리케이션 유형 : 웹 애플리케이션
- 이름 : OAuth2.0 Client
- 승인된 리디렉션 URI : URI 추가 > http://localhost:8080/login/oauth2/code/google
- 만들기 > 클라이언트 ID, 클라이언트 보안 비밀번호 확인 가능함
2-4. 의존성 추가
- build.gradle
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
2-5. application.yml
# application.yml에 아래 설정 추가
spring:
security:
oauth2:
client:
registration:
google:
client-id: [위에서 확인한 클라이언트 ID]
client-secret: [위에서 확인한 클라이언트 비밀번호]
scope:
- email
- profile
2-6. Member
- provider와 providerId 컬럼 추가
@Entity
@Builder
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="member_id")
private Long id;
private String loginId;
private String password;
private String name;
@Enumerated(EnumType.STRING)
private MemberRole role;
// provider : google이 들어감
private String provider;
// providerId : 구굴 로그인 한 유저의 고유 ID가 들어감
private String providerId;
}
2-7. 기존의 CustomUserDetails 이름을 CustomSecurityUserDetails 로 변경
- OAuth 로그인 뿐만 아니라 Form 로그인도 가능하게 하기 위함
2-8. CustomOauth2UserDetails 추가
- UserDetails와 OAuth2User를 상속받음
- 메서드 오버라이드
public class CustomOauth2UserDetails implements UserDetails, OAuth2User {
private final Member member;
private Map<String, Object> attributes;
public CustomUserDetails(Member member, Map<String, Object> attributes) {
this.member = member;
this.attributes = attributes;
}
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public String getName() {
return null;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return member.getRole().name();
}
});
return collection;
}
@Override
public String getPassword() {
return member.getPassword();
}
@Override
public String getUsername() {
return member.getLoginId();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
- attributes : 구글에서 받아온 정보들
- getAttributes(), getName() 을 제외하고는 기존의 코드와 동일
2-9. OAuth2UserInfo 인터페이스 추가
- 구글 로그인 뿐만 아니라 나중에 카카오, 네이버 등의 소셜 서비스 로그인도 진행할 것
- 따라서 인터페이스를 추가해서 통일성 부여
public interface OAuth2UserInfo {
String getProvider();
String getProviderId();
String getEmail();
String getName();
}
2-10. GoogleUserDetails 추가
- 소셜 서비스마다 데이터를 추출하는 방식이 다르기 때문에 구글만의 클래스 추가함
@AllArgsConstructor
public class GoogleUserDetails implements OAuth2UserInfo{
private Map<String, Object> attributes;
@Override
public String getProvider() {
return "google";
}
@Override
public String getProviderId() {
return (String) attributes.get("sub");
}
@Override
public String getEmail() {
return (String) attributes.get("email");
}
@Override
public String getName() {
return (String) attributes.get("name");
}
}
2-11. CustomOauth2UserService 추가
- Form 로그인 방식은 기존의 CustomUserDetailsService 를 통해서 진행됨
- DefaultOAuth2UserService를 상속
- 첫 로그인이면 자동으로 회원가입 진행
@Service
@RequiredArgsConstructor
@Slf4j
public class CustomOauth2UserService extends DefaultOAuth2UserService {
private final MemberRepository memberRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
log.info("getAttributes : {}",oAuth2User.getAttributes());
String provider = userRequest.getClientRegistration().getRegistrationId();
OAuth2UserInfo oAuth2UserInfo = null;
// 뒤에 진행할 다른 소셜 서비스 로그인을 위해 구분 => 구글
if(provider.equals("google")){
log.info("구글 로그인");
oAuth2UserInfo = new GoogleUserDetails(oAuth2User.getAttributes());
}
String providerId = oAuth2UserInfo.getProviderId();
String email = oAuth2UserInfo.getEmail();
String loginId = provider + "_" + providerId;
String name = oAuth2UserInfo.getName();
Member findMember = memberRepository.findByLoginId(loginId);
Member member;
if (findMember == null) {
member = Member.builder()
.loginId(loginId)
.name(name)
.provider(provider)
.providerId(providerId)
.role(MemberRole.USER)
.build();
memberRepository.save(member);
} else{
member = findMember;
}
return new CustomOauth2UserDetails(member, oAuth2User.getAttributes());
}
}
- loginId : "provider"_"providerId" 형태로 저장
- provider : google
- providerId : google에서의 사용자의 ID
log.info() 로 찍어본 attributes
더보기
getAttributes :
{sub=106308847269354937091,
name=안창민/학생/전자공학,
given_name=/학생/전자공학,
family_name=안창민,
picture=https://lh3.googleusercontent.com/a/ACg8ocKd0s7wDGcdVsGE9p1CITOaoY4JvHwNCYZVSeTt-D3Z=s96-c,
email=chm2006@inha.edu,
email_verified=true, locale=ko, hd=inha.edu}
이러한 attributes의 구조를 바탕으로 GoogleUserDetails에서 값을 추출함
2-12. SecurityConfig 수정
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
// 접근 권한 설정
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/oauth-login/admin").hasRole(MemberRole.ADMIN.name())
.requestMatchers("/oauth-login/info").authenticated()
.anyRequest().permitAll()
);
// 폼 로그인 방식 설정
http
.formLogin((auth) -> auth.loginPage("/oauth-login/login")
.loginProcessingUrl("/oauth-login/loginProc")
.usernameParameter("loginId")
.passwordParameter("password")
.defaultSuccessUrl("/oauth-login")
.failureUrl("/oauth-login")
.permitAll());
// OAuth 2.0 로그인 방식 설정
http
.oauth2Login((auth) -> auth.loginPage("/oauth-login/login")
.defaultSuccessUrl("/oauth-login")
.failureUrl("/oauth-login/login")
.permitAll());
http
.logout((auth) -> auth
.logoutUrl("/oauth-login/logout"));
http
.csrf((auth) -> auth.disable());
return http.build();
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
2-13. login.html 수정
- a href 태그 추가하여 구글 로그인 가능하게 수정
<a href="/oauth2/authorization/google">구글 로그인</a>
3. 실행 결과
3-1. 로그인 화면
- 구글 로그인 항목이 추가됨
3-2. 구글 로그인 클릭 시
- 구글 로그인 가능 확인
3-3. 구글 로그인 이후 마이페이지
- ID : "provider(google)"_"providerId" 형태
3-4. Form 로그인 사용자와 OAuth 2.0 로그인 사용자의 정보를 MySQL에서 확인
- Form 로그인 사용자는 provider와 providerId가 비어있음
- OAuth 2.0 로그인 사용자는 password가 저장되지 않음
'Spring Boot' 카테고리의 다른 글
[Spring Boot] 로그인 기능 구현 (7) - 네이버 로그인 (OAuth 2.0) (1) | 2024.01.14 |
---|---|
[Spring Boot] 로그인 기능 구현 (6) - 카카오 로그인 (OAuth 2.0) (0) | 2024.01.14 |
[Spring Boot] 로그인 기능 구현 (4) - 스프링 시큐리티 사용 JWT 로그인 (1) | 2024.01.13 |
[Spring Boot] 스프링으로 엑셀 파일 읽기 (1) | 2024.01.08 |
[Spring Boot] 로그인 기능 구현 (3) - 스프링 시큐리티 로그인 (4) | 2024.01.07 |