이전에 구글 로그인 인증정보를 발급 받았으니 프로젝트에 적용해보겠다.
package com.chanmi.book.springboot.domain.user;
import com.chanmi.book.springboot.domain.BaseTimeEntity;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Getter
@NoArgsConstructor
@Entity
public class User extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String email;
@Column
private String picture;
//JPA로 데이터베이스를 저장할 때 Enum 값을 어떤 형태로 저장할지 결정
//기본적으로 int로 저장되지만 이러면 나중에 데이터베이스로 확인할 때 그 값이 무슨 코드를 의미하는지 알 수 없다.
//그래서 문자열로 저장될 수 있도록 선언
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;
@Builder
public User(String name, String email, String picture, Role role){
this.name = name;
this.email = email;
this.picture = picture;
this.role = role;
}
public User update(String name, String picture){
this.name = name;
this.picture = picture;
return this;
}
public String getRoleKey(){
return this.role.getKey();
}
}
package com.chanmi.book.springboot.domain.user;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum Role {
GUEST("ROLE_GUEST", "손님"),
USER("ROLE_USER", "일반 사용자");
private final String key;
private final String title;
}
스프링 시큐리티에서는 권한 코드에 항상 ROLE_이 앞에 있어야만 한다.
package com.chanmi.book.springboot.domain.user;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
//소셜 로그인으로 반환되는 값 중 email을 통해 이미 생성된 사용자인지
//처음 가입하는 사용자인지 판단 위한 메소드
Optional<User> findByEmail(String email);
}
User 엔티티 관련 코드 모두 작성 완료!
빌드 완료했고, 이제 OAuth 라이브러리를 이용한 소셜 로그인 설정 코드 작성한다.
먼저 프로젝트 맨 위에(domain과 같은 위치) config.auth 패키지 생성
시큐리티 관련 클래스는 모두 이곳에..
그리고 config.auth.dto도 생성
- User 클래스를 사용하지 않고 SessionUser를 만들어서 사용한 이유는?
User 클래스를 세션에 저장하려고 하면 User 클래스를 직렬화 구현하지 않았다고 에러가 뜬다.
- 그럼 User 클래스를 직렬화하면 되잖아?
User 클래스는 엔티티이기 때문에 언제 다른 엔티티와 관계까 형성될지 모른다.
예를 들어 @OneToMany, @ManyToMany 등 자식 엔티티를 갖고 있다면 직렬화 대상에 자식까지 포함되니 성능 이슈, 부수 효과가 발생할 확률이 높다.
==> 그래서 직렬화 기능을 가진 세션 Dto를 하나 추가로 만드는 게 운영 및 유지보수 때 좋다.
package com.chanmi.book.springboot.config.auth.dto;
import com.chanmi.book.springboot.domain.user.Role;
import com.chanmi.book.springboot.domain.user.User;
import lombok.Builder;
import lombok.Getter;
import java.util.Map;
@Getter
public class OAuthAttributes {
private Map<String, Object> attributes;
private String nameAttributeKey;
private String name;
private String email;
private String picture;
@Builder
public OAuthAttributes(Map<String, Object> attributes, String nameAttributeKey, String name, String email, String picture){
this.attributes = attributes;
this.nameAttributeKey = nameAttributeKey;
this.name = name;
this.email = email;
this.picture = picture;
}
//OAuth2User에서 반환하는 사용자 정보는 Map이기 때문에 값 하나하나를 변환해야만 한다.
public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map<String, Object> attributes){
return ofGoogle(userNameAttributeName, attributes);
}
public static OAuthAttributes ofGoogle(String userNameAttributeName, Map<String, Object> attributes){
return OAuthAttributes.builder()
.name((String) attributes.get("name"))
.email((String) attributes.get("email"))
.picture((String) attributes.get("picture"))
.attributes(attributes)
.nameAttributeKey(userNameAttributeName)
.build();
}
//User 엔티티를 생성한다.
//OAuthAttributes에서 엔티티를 생성하는 시점은 처음 가입할 때
//가입할 때 기본 권한을 GUEST로 주기 위해 ROLE 빌더 값에는 Role.GUEST 사용
public User toEntity(){
return User.builder()
.name(name)
.email(email)
.picture(picture)
.role(Role.GUEST)
.build();
}
}
package com.chanmi.book.springboot.config.auth;
import com.chanmi.book.springboot.config.auth.dto.OAuthAttributes;
import com.chanmi.book.springboot.config.auth.dto.SessionUser;
import com.chanmi.book.springboot.domain.user.User;
import com.chanmi.book.springboot.domain.user.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpSession;
import java.util.Collections;
@RequiredArgsConstructor
@Service
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final UserRepository userRepository;
private final HttpSession httpSession;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException{
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
//현재 로그인 진행 중인 서비스를 구분하는 코드
//구글만 연동했을 때는 구글만 사용하므로 불필요한 값이지만, 이후 네이버 로그인 연동이나 다른 연동 더하면 구글인지 네이버인지 구분
String registrationId = userRequest.getClientRegistration().getRegistrationId();
//OAuth2 로그인 진행 시 키가 되는 필드 값, Primary Key와 같은 의미
//구글의 경우 기본적으로 코드 지원하지만, 네이버/카카오 등은 기본 지원 안함(구글 기본코드 : "sub")
//이후 네이버 로그인과 구글 로그인 동시 지원할 때 사용
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
.getUserInfoEndpoint().getUserNameAttributeName();
//OAuth2UserService를 통해 가져온 OAuth2User의 attribute를 담을 클래스
//이후 네이버 등 다른 소셜 로그인도 이 클래스 사용
OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());
User user = saveOrUpdate(attributes);
//SessionUser는 세션에 사용자 정보를 저장하기 위한 Dto
httpSession.setAttribute("user", new SessionUser(user));
return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority(user.getRoleKey())),
attributes.getAttributes(),
attributes.getNameAttributeKey());
}
//구글 사용자 정보가 업데이트 되었을 때 대비해 구현
//사용자의 이름, 프로필 사진이 변경되면 User 엔티티에도 반영
private User saveOrUpdate(OAuthAttributes attributes){
User user = userRepository.findByEmail(attributes.getEmail())
.map(entity -> entity.update(attributes.getName(), attributes.getPicture()))
.orElse(attributes.toEntity());
return userRepository.save(user);
}
}
package com.chanmi.book.springboot.config.auth;
import com.chanmi.book.springboot.domain.user.Role;
import lombok.RequiredArgsConstructor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomOAuth2UserService customOauth2UserService;
@Override
protected void configure(HttpSecurity http) throws Exception{
http
.csrf().disable()
.headers().frameOptions().disable()
.and()
.authorizeRequests()
.antMatchers("/", "/css/**", "/images/**", "/js/**", "h2-console/**").permitAll()
.antMatchers("/api/v1/**").hasRole(Role.USER.name())
.anyRequest().authenticated()
.and()
.logout()
.logoutSuccessUrl("/")
.and()
.oauth2Login()
.userInfoEndpoint()
.userService(customOauth2UserService);
}
}
로그인 테스트(스프링 시큐리티) 만들었으니 테스트 해보기 위해 로그인 버튼을 만들어서 로그인 할 수 있도록 한다.
구글 로그인 버튼 잘 보이고
버튼 누르니 구글 로그인 뜬다.
아오 가리기 귀찮, 디비에 잘 뜨고
근데 자꾸 as user로 떠서 나중에 찾아봐야 할듯..
전에 로그인 할 때 GUEST 권한으로 하도록 만들었기 때문에 글을 작성할 수 없다.(403 : 권한 거부 뜰 것임)
권한을 변경하겠다.
일단 디비에서 일케 변경하면 글을 등록할 수 있게 된다.
다음엔 조금씩 기능을 개선하겠다..
안녕하세요!! 글 정말 잘보고 있습니다. 깔끔하고 자세하게 설명해 주셔서 열심히 따라해보고 있는데, 질문이 있습니다. 혹시 이거는 세션을 이용해서 만드는 것일까요?