😅 이전 포스팅에서 구글, 네이버, 카카오의 클라이언트 ID와 비밀번호를 전부 생성했다
SpringBoot에서 구현해보자 (ver 2.7.18)
1. loginForm
- 로그인버튼 만들기
<a href="/oauth2/authorization/google">구글 로그인</a>
<a href="/oauth2/authorization/kakao">카카오 로그인</a>
<a href="/oauth2/authorization/naver">네이버 로그인</a>
2. User.java
- provider, providerId 컬럼 추가
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String username;
private String password;
private String email;
private String role;
@CreationTimestamp
private Timestamp createDate;
private String provider; // OAuth 로그인 유저 구별을 위해서 (google, kakao, naver 등)
private String providerId;
@Builder
public User(String username, String password, String email, String role, Timestamp createDate,
String provider, String providerId) {
this.username = username;
this.password = password;
this.email = email;
this.role = role;
this.createDate = createDate;
this.provider = provider; // 추가
this.providerId = providerId; // 추가
}
}
3. pom.xml
- SpringBoot 프로젝트를 만들 때 OAuth2 Client 라이브러리를 체크하지 않았다면 dependency 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
4. application.yml
- 클라이언트ID와 클라이언트 보안 비밀번호 넣어주기
spring:
# 나머지 코드
security:
oauth2:
client:
registration:
google:
client-id: #클라이언트 ID
client-secret: #클라이언트 보안 비밀번호
scope:
- email
- profile
naver:
client-id: #Client ID
client-secret: #Client Secret
scope:
- name
- email
client-name: Naver
authorization-grant-type: authorization_code
redirect-uri: http://localhost:8080/login/oauth2/code/naver
kakao:
client-id: #REST API 키
client-secret: #코드
redirect-uri: http://localhost:8080/login/oauth2/code/kakao
authorization-grant-type: authorization_code
client-authentication-method: POST
client-name: Kakao
scope:
- profile_nickname
- account_email
provider:
naver:
authorization-uri: https://nid.naver.com/oauth2.0/authorize
token-uri: https://nid.naver.com/oauth2.0/token
user-info-uri: https://openapi.naver.com/v1/nid/me
user-name-attribute: response
kakao:
authorization-uri: https://kauth.kakao.com/oauth/authorize
token-uri: https://kauth.kakao.com/oauth/token
user-info-uri: https://kapi.kakao.com/v2/user/me
user-name-attribute: id
5. SecurityConfig.java
- 이전 포스트에서 만들어둔 SecurityConfig의 formLogin 다음에 추가
.oauth2Login((oauth2Login) -> oauth2Login
.loginPage("/login")
.userInfoEndpoint()
.userService(principalOauth2UserService));
6. PrincipalOauth2UserService.java
- OAuth로그인과 동시에 회원가입진행 (loadUser 메서드 종료시 @Authentication 어노테이션이 만들어진다)
@Service
public class PrincipalOauth2UserService extends DefaultOAuth2UserService{
// 회원가입 시 비밀번호 암호화
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Autowired
private UserRepository userRepository;
// 구글로 부터 받은 userRequest 데이터에 대한 후처리 함수
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
System.out.println("getClientRegistration : "+userRequest.getClientRegistration());
System.out.println("getAccessToken : "+userRequest.getAccessToken().getTokenValue());
OAuth2User oauth2User = super.loadUser(userRequest);
System.out.println("getAttributes : "+oauth2User.getAttributes());
OAuth2UserInfo oAuth2UserInfo = null;
if(userRequest.getClientRegistration().getRegistrationId().equals("google")) {
System.out.println("구글 로그인 요청");
oAuth2UserInfo = new GoogleUserInfo(oauth2User.getAttributes());
}else if(userRequest.getClientRegistration().getRegistrationId().equals("naver")) {
System.out.println("네이버 로그인 요청");
oAuth2UserInfo = new NaverUserInfo((Map)oauth2User.getAttributes().get("response"));
}else if(userRequest.getClientRegistration().getRegistrationId().equals("kakao")) {
System.out.println("카카오 로그인 요청");
System.out.println(oauth2User.getAttributes());
oAuth2UserInfo = new KakaoUserInfo(oauth2User.getAttributes());
}else {
System.out.println("구글,네이버,카카오만 지원합니다");
}
// 회원가입진행(OAuth)
String provider = oAuth2UserInfo.getProvider();
String providerId = oAuth2UserInfo.getProviderId();
String username = provider+"_"+providerId; // google_1035054098404 (중복방지)
String password = bCryptPasswordEncoder.encode("비밀번호");
String email = oAuth2UserInfo.getEmail();
String role = "ROLE_USER";
User userEntity = userRepository.findByUsername(username);
if(userEntity == null) {
System.out.println("OAuth 최초 로그인");
userEntity = User.builder()
.username(username)
.password(password)
.email(email)
.role(role)
.provider(provider)
.providerId(providerId)
.build();
userRepository.save(userEntity);
}else {
System.out.println("OAuth 로그인 한 적 있음");
}
// PrinciparDetails와 OAuth2User타입이 같아서 사용가능
return new PrincipalDetails(userEntity, oauth2User.getAttributes());
}
}
※ 로그인 → 로그인 창 → 로그인 완료 → code리턴 (OAuth - Client라이브러리) → AccessToken요청
→ userRequest정보 → loadUser 함수 호출 → 회원프로필 받기
7. PrincipalDetails.java
- 스프링 시큐리티가 관리하는 세션에 들어갈 수 있는 타입은 Authentication객체 뿐이다
- Authentication객체 안에는 UserDetails(일반로그인)와 OAuth2User(OAuth로그인) 타입이 들어갈 수 있다
세션정보를 찾을 때 일반 로그인(UserDetails 타입), OAuth로그인(OAuth2USer 타입)을 하나로 묶어 편하게 처리하기 위해
이전 포스팅에서 만든 PrincipalDetails에 UserDetails를 implement 해주었는데 OAuth2User도 함께 implement 해주자
[ @AuthenticationPrincipal UserDetails userdetails; @AuthenticationPrincipal OAuth2User oAuth2User; 따로 쓰지않고
→ @AuthenticationPrincipal PrincipalDetails principaldetails; 하나로 쓰기 위해서 ]
@Data
public class PrincipalDetails implements UserDetails, OAuth2User{
private User user;
private Map<String, Object> attributes;
public PrincipalDetails(User user) {
this.user = user;
}// 일반 로그인
public PrincipalDetails(User user, Map<String, Object> attributes) {
this.user = user;
this.attributes = attributes;
}// OAuth 로그인
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collect = new ArrayList<>();
collect.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return user.getRole();
}
});
return collect;
}// 해당 User의 권한을 리턴
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
// ================ OAuth2User ================ //
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public String getName() {
return null; // attributes.get("sub"); 사용하지않아서 null
}
}
8. provider 패키지
- 각 로그인별로 Attribute값이 다르기때문에 각 클래스를 만들어서 관리해준다
- 유지보수하기가 엄청 수월해지며, 새로운 OAuth를 등록하기도 편해진다
a) OAuth2UserInfo.java
public interface OAuth2UserInfo {
String getProviderId();
String getProvider();
String getEmail();
String getName();
}
b) GoogleUserInfo.java
public class GoogleUserInfo implements OAuth2UserInfo{
private Map<String, Object> attributes; // oauth2User.getAttributes()
public GoogleUserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
@Override
public String getProviderId() {
return (String)attributes.get("sub");
}
@Override
public String getProvider() {
return "google";
}
@Override
public String getEmail() {
return (String)attributes.get("email");
}
@Override
public String getName() {
return (String)attributes.get("name");
}
}
c) NaverUserInfo.java
public class NaverUserInfo implements OAuth2UserInfo{
private Map<String, Object> attributes;
// <네이버 구조>
// {id=qCo0hz7H8ooU_yJsJEvmXnbhpF8U2Eml0XumaE61ogc,
// email=아이디2@naver.com, name=성이름}
public NaverUserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
@Override
public String getProviderId() {
return (String)attributes.get("id");
}
@Override
public String getProvider() {
return "naver";
}
@Override
public String getEmail() {
return (String)attributes.get("email");
}
@Override
public String getName() {
return (String)attributes.get("name");
}
}
d) KakaoUserInfo.java
public class KakaoUserInfo implements OAuth2UserInfo{
private Map<String, Object> attributes; // oauth2User.getAttributes()
public KakaoUserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
@Override
public String getProviderId() {
return (String)attributes.get("id").toString();
}
@Override
public String getProvider() {
return "kakao";
}
@Override
public String getEmail() {
Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
return (String) kakaoAccount.get("email");
}// "kakao_account" 객체 안의 "email" 키 값
@Override
public String getName() {
Map<String, Object> properties = (Map<String, Object>) attributes.get("properties");
return (String) properties.get("nickname");
}// "properties" 객체 안의 "nickname" 키 값
// <카카오 구조>
// {
// id=3297747359,
// connected_at=2024-01-22T17:23:41Z,
// properties=
// {
// nickname=닉네임},
// }
// kakao_account=
// {
// profile_nickname_needs_agreement=false,
// profile_image_needs_agreement=true,
// profile={nickname=닉네임},
// has_email=true,
// email_needs_agreement=false,
// is_email_valid=true,
// is_email_verified=true,
// email=아이디@메일.com
// }
// }
}
>> OAuth 로그인 완료
'개발 > SpringBoot' 카테고리의 다른 글
[SpringBoot] OAuth 로그인 - 1 사전 준비 (구글, 네이버, 카카오) (0) | 2024.02.14 |
---|---|
[SpringBoot] SpringSecurity - 2 로그인 실패 (0) | 2024.02.14 |
[SpringBoot] SpringSecurity - 1 로그인 (0) | 2024.02.13 |
[SpringBoot] 썸머노트(summernote) - 3 이미지 S3 저장 (1) | 2024.02.13 |
[SpringBoot] 썸머노트(summernote) - 2 이미지 저장 (1) | 2024.02.13 |