스프링 부트, 확장!

준비중..

스프링 부트, 확장!

댓글 기능 및 소셜 로그인!

30 구글 로그인 연동하기

# 구글 로그인 연동하기 ## 미션 --- 구글 로그인을 위한 DTO/서비스/리파지터리/엔티티를 완성하고, 이와 관련된 "스프링 시큐리티" 설정을 작성하시오. ## 개념 --- #### ⭐️ 소셜 로그인 인증 과정 😎"사용자"가 로그인 요청을 한다. 👩‍💻"개발자"는 해당 요청을 🏢"구글"로 보낸다. 구글은 소셜 로그인을 제공하고, 성공 시 사용자 정보를 "개발자"에게 넘긴다. "개발자"는 넘어온 정보를 DB에 저장하고, 이를 기반으로 사용자를 인증해야 한다. 이를 위해 필요한 컴포넌트(DTO, Entity, Repository, Service 등)를 만들어야 한다. 직접 만들면 힘들다. "OAuth" 라이브러리를 활용하면 그나마 쉽다..! #### ⭐️ 사용자 인증과 권한 부여 사용자는 일반적으로 GUEST, USER, ADMIN 등으로 나뉜다. 이를 위한 프레임워크가 🔐"스프링 시큐리티"다. 시큐리티를 사용하면 사용자 별 권한을 손쉽게 부여할 수 있다. ## 튜토리얼 --- #### ⭐️ JPA 관련 1) 엔티티 만들기: "entity/User" ``` ... @Getter @NoArgsConstructor @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String email; @Column(nullable = false) private String name; @Column private String picture; // 원래 Enum 값이 DB로 들어가면 숫자로 저장 됨. // 근데 숫자만 보면 무슨 의미인지 파악하기 힘듦. // 때문에, String 타입으로 저장하기로 설정! @Enumerated(EnumType.STRING) // 구글링: "자바 Enum이란?" & "@Enumerated 애노테이션" @Column(nullable = false) private Role role; @Builder public User(String email, String name, String picture, Role role) { this.email = email; this.name = name; 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(); } } ``` 2) Enum 만들기: 사용자의 권한 설정을 위한 Enum! enums 패키지를 만들고 거기에 "enums/Role" 생성. ![클라우드스터딩-스프링부트-시큐리티-Role-enum](https://i.imgur.com/28qhAXA.png) ``` ... @Getter public enum Role { // 주의: "Enum"(O), "class"(X) GUEST("ROLE_GUEST", "손님"), USER("ROLE_USER", "일반 사용자"); private final String key; private final String title; // 아래 생성자는 "@RequiredArgsConstructor"로 생략 가능! // 참조: https://www.daleseo.com/lombok-popular-annotations/ Role(String key, String title) { this.key = key; this.title = title; } } ``` 3) 리파지터리 생성: "repository/UserRepository" ``` ... public interface UserRepository extends JpaRepository<User, Long> { // 중복 이메일 검증을 위함! Optional<User> findByEmail(String email); // } ``` #### ⭐️ OAuth2 관련 4) 라이브러리 추가: "build.gradle"에 ouath2 관련 라이브러리를 등록. ``` ... dependencies { // oauth2 라이브러리 등록 implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' ... } ... ``` 5) 시큐리티 설정 클래스 생성: "config.auth/SecurityConfig" ![클라우드스터딩-스프링부트-시큐리티-설정-클래스](https://i.imgur.com/uS38OOJ.png) ``` ... @RequiredArgsConstructor @EnableWebSecurity // 스프링 시큐리티 설정 활성화 해줌! public class SecurityConfig extends WebSecurityConfigurerAdapter { private final CustomOAuth2UserService customOAuth2UserService; @Override protected void configure(HttpSecurity http) throws Exception { http // h2-console 사용을 위해! 구글링 "csrf 란?" .csrf().disable() .headers().frameOptions().disable() .and() // URL에 따른 설정 시작! .authorizeRequests() .antMatchers( // 아래 패턴의 URL은, "/", "/articles", "/init", "/css/**", "/images/**", "/js/**", "/h2-console/**" ).permitAll() // 누구나 접근하게 한다! .antMatchers( // 아래 패턴의 URL은, "/api/**" ).hasRole(Role.USER.name()) // "USER"인 경우만 사용 가능! hasRole("USER")와 같음! 참조: "자바 enum name()" .anyRequest().authenticated() // 나머지 URL 요청은 무조건 인증 해야 함! .and() .logout() .logoutSuccessUrl("/") // 로그아웃 후, 리다이렉트 될 URL .and() .oauth2Login() // OAuth2 로그인 관련 설정 .userInfoEndpoint() // 로그인 성공 시, 사용자 정보 관련 설정 .userService(customOAuth2UserService); // 로그인 성공 후, 사용자를 다룰 서비스를 등록! } } ``` 6) 서비스 생성: "config.auth/CustomOAuth2UserService" ``` ... @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 delegate = new DefaultOAuth2UserService(); OAuth2User oAuth2User = delegate.loadUser(userRequest); // 서비스 구분용 id (구글/네이버/페북/카카오 등..) String registrationId = userRequest .getClientRegistration().getRegistrationId(); // 로그인 시, PK가 되는 필드 값 (구글: sub, 페북, String userNameAttributeName = userRequest .getClientRegistration().getProviderDetails() .getUserInfoEndpoint().getUserNameAttributeName(); // 소셜 로그인 된 유저 정보, 이를 객체화! OAuthAttributes attributes = OAuthAttributes. of(registrationId, userNameAttributeName, oAuth2User.getAttributes()); // 사용자 정보 업데이트! User user = saveOrUpdate(attributes); // 세션에 사용자 정보를 등록! // 이를 위한 DTO가 SessionUser // 왜 User 엔티티를 사용하지 않고 DTO를 만들까? httpSession.setAttribute("user", new SessionUser(user)); return new DefaultOAuth2User( Collections.singleton(new SimpleGrantedAuthority(user.getRoleKey())), attributes.getAttributes(), attributes.getNameAttributeKey() ); } private User saveOrUpdate(OAuthAttributes attributes) { User user = userRepository.findByEmail(attributes.getEmail()) .map( // 해당 이메일의 사용자를 찾으면, 이름과 사진을 업데이트! entity -> entity.update(attributes.getName(), attributes.getPicture()) ) // 해당 사용자가 없으면, 신규 생성! .orElse(attributes.toEntity()); // DB에 저장 후, 해당 값 반환! return userRepository.save(user); } } ``` 7) DTO 생성(1): "config.auth/OAuthAttributes" ``` ... @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; } // OAuth로 전달 받은 값을, 재 규격화 함! public static OAuthAttributes of(String registrationId, // 서비스 id String userNameAttributeName, // 대표 필드 Map<String, Object> attributes) { // 속성 값들 return _ofGoogle(userNameAttributeName, attributes); } // 구글 정보를 재규격화 private 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(); } // 신규 유저의 경우 수행! public User toEntity() { return User.builder() .name(name) .email(email) .picture(picture) .role(Role.GUEST) .build(); } } ``` 8) DTO 생성(2): "config.auth/SessionUser" ``` ... @Getter public class SessionUser implements Serializable { private String name; private String email; private String picture; public SessionUser(User user) { this.name = user.getName(); this.email = user.getEmail(); this.picture = user.getPicture(); } } ``` ## 훈련하기 --- - 소셜 로그인 과정을 그림으로 그려 설명하시오. - 스프링 시큐리티 설정 파일을 분석하시오. ## 면접 질문 --- - 소셜 로그인을 사용하지 않고, 직접 회원 관리를 구현하는 경우를 그림으로 그린다면?