인텔리제이 아이디어(IntelliJ IDEA) 커뮤니티 버전, Spring JPA 및 롬복(Lombok) 사용을 전제로 합니다.
- 스프링 부트 버전: 2.3.1
- Gradle 버전: 6.4.1
출처: 스프링 부트와 AWS로 혼자 구현하는 웹 서비스
이 방법은 JSTL, Thymeleaf, Mustache 등 서버 사이드 템플릿 엔진을 사용하는 로그인 방법입니다. SPA에서 사용할 수 있는 소셜 로그인 연동 방법은 아래 글을 참고하세요,
순서
build.gradle
에 디펜던시 추가application-oauth.properties
작성 +.gitignore
등록Role
enum 클래스 작성 – 사용자 권한 관리User
클래스 작성 – JPA Entity 클래스OAuthAttributes
클래스 작성 – 구글 로그인 이후 가져온 사용자의 이메일, 이름, 프로필 사진 주소 를 저장하는 DTOCustomOAuth2UserService
클래스 작성 –OAuthAttributes
을 기반으로 가입 및 정보수정, 세션 저장 등 기능 수행SecurityConfig
클래스 작성 – 스프링 시큐리티 설정SessionUser
클래스 작성 – User 엔티티 클래스에서 직렬화가 필요한 경우 별도로 사용IndexController
작성, Thymeleaf 뷰 페이지 작성
스프링 부트 및 기타 기능들을 이용해 구글 로그인 연동을 하는 방법입니다. 그리고 마지막에 부수 결과로 네이버 연동도 해보겠습니다.
깃허브 주소: https://github.com/ayaysir/awsboard (깃허브 내용은 나중에 변경될 수 있습니다.)
프로젝트 구조
먼저 구글로부터 클라이언트 아이디와 비밀번호를 받아야 합니다.
1) build.gradle에 디펜던시 추가
// security + oauth2 implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
2) application-oauth.properties 작성 + .gitignore 등록
위치는 application.properties가 있는 위치와 동일한 곳에 작성합니다. application-oauth.properties 작성 후, application.properties 파일에 등록해야 합니다.
그리고 비밀번호가 있으므로 Git을 사용한다면 이것이 커밋되지 않도록 .gitignore 파일에 추가해야 합니다.
application-oauth.properties
spring.security.oauth2.client.registration.google.client-id=[클라이언트 아이디] spring.security.oauth2.client.registration.google.client-secret=[클라이언트 비밀번호] spring.security.oauth2.client.registration.google.scope=profile,email
application.properties
#application-oauth.properties 로딩 spring.profiles.include=oauth
프로퍼티 파일명을 application-XXX.properties 이런 식으로 지으면, 위에 처럼 spring.profiles.include=XXX
로 로딩할 수 있습니다.
3) Role enum 클래스 작성
사용자의 권한을 enum
클래스로 만들어 관리합니다.
package com.example.awsboard.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; }
4) User 클래스 작성
엔티티(@Entity
) 클래스는 JPA를 통해 SQL을 사용하지 않고도 자바 코드 내에서 테이블을 생성할 수 있습니다.
package com.example.awsboard.domain.user; import com.example.awsboard.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; @Enumerated(EnumType.STRING) @Column(nullable = false) private Role 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(); } }
2021-10-17 추가: UserRepository.java
작성 (findByEmail
은 나중에 사용됩니다.)
package com.example.awsboard.domain.user; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByEmail(String email); }
참고: BaseTimeEntity
클래스 (JpaAuditing – 글 작성 시점 자동 추가)
package com.example.awsboard.domain; import lombok.Getter; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import javax.persistence.EntityListeners; import javax.persistence.MappedSuperclass; import java.time.LocalDateTime; @Getter @MappedSuperclass @EntityListeners(AuditingEntityListener.class) public class BaseTimeEntity { @CreatedDate private LocalDateTime createdDate; @LastModifiedDate private LocalDateTime modifiedDate; }
메인 애플리케이션에서 @EnableJpaAuditing
어노테이션을 추가해 활성화합니다.
package com.example.awsboard; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @EnableJpaAuditing @SpringBootApplication public class AwsboardApplication { public static void main(String[] args) { SpringApplication.run(AwsboardApplication.class, args); } }
5) OAuthAttributes 클래스 작성
구글 로그인 이후 가져온 사용자의 이메일, 이름, 프로필 사진 주소를 저장하는 DTO
package com.example.awsboard.config.auth; import com.example.awsboard.domain.user.Role; import com.example.awsboard.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, name, email, 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; } 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(); } public User toEntity() { return User.builder() .name(name) .email(email) .picture(picture) .role(Role.GUEST) .build(); } }
6) CustomOAuth2UserService 클래스 작성
OAuthAttributes
을 기반으로 가입 및 정보수정, 세션 저장 등 기능 수행합니다.
package com.example.awsboard.config.auth; import com.example.awsboard.config.auth.dto.SessionUser; import com.example.awsboard.domain.user.User; import com.example.awsboard.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 로그인 진행 시 키가 되는 필드값 String userNameAttributeName = userRequest.getClientRegistration() .getProviderDetails() .getUserInfoEndpoint() .getUserNameAttributeName(); // OAuthAttributes: attribute를 담을 클래스 (개발자가 생성) OAuthAttributes attributes = OAuthAttributes .of(registrationId, userNameAttributeName, oAuth2User.getAttributes()); User user = saveOrUpdate(attributes); // SessioUser: 세션에 사용자 정보를 저장하기 위한 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()); return userRepository.save(user); } }
7) SecurityConfig 클래스 작성 – 스프링 시큐리티 설정
.oauth2Login().userInfoEndpoint().userService(customOAuth2UserService);
package com.example.awsboard.config.auth; import com.example.awsboard.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/**", "/h2-console/**").permitAll() .antMatchers("/api/v1/**").hasRole(Role.USER.name()) .anyRequest().authenticated() .and() .logout().logoutSuccessUrl("/") .and() .oauth2Login().userInfoEndpoint().userService(customOAuth2UserService); } }
8) SessionUser 클래스 작성
User
엔티티 클래스에서 직렬화가 필요한 경우 별도로 사용하기 위한 클래스를 작성합니다.
package com.example.awsboard.config.auth.dto; import com.example.awsboard.domain.user.User; import lombok.Getter; import java.io.Serializable; /** * 세션에 저장하려면 직렬화를 해야 하는데 * User 엔티티는 추후 변경사항이 있을 수 있기 때문에 * 직렬화를 하기 위한 별도의 SessionUser 클래스 생성 */ @Getter public class SessionUser implements Serializable { private String name, email, picture; public SessionUser(User user) { this.name = user.getName(); this.email = user.getEmail(); this.picture = user.getPicture(); } }
9) IndexController 작성, Thymeleaf 뷰 페이지 작성
일부 내용은 생략합니다.
package com.example.awsboard.web; import com.example.awsboard.config.auth.LoginUser; import com.example.awsboard.config.auth.dto.SessionUser; import com.example.awsboard.service.posts.PostsService; import com.example.awsboard.web.dto.PostsResponseDTO; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import javax.servlet.http.HttpSession; @RequiredArgsConstructor @Controller public class IndexController { private final PostsService postsService; private final HttpSession httpSession; @GetMapping("/") public String index(Model model, @LoginUser SessionUser user) { // ............. // 사용자 정보: 위의 @LoginUser 어노테이션으로 대체 // SessionUser user = (SessionUser) httpSession.getAttribute("user"); if(user != null) { model.addAttribute("userName", user.getName()); model.addAttribute("userImg", user.getPicture()); } return "index"; } // .................. }
<div class="row"> <div class="col-md-10"> <div th:if="${not #strings.isEmpty(userName)}"> <img style="width:45px; height:45px" src="/image/unnamed.png" th:src="${userImg}" class="rounded-circle img-thumbnail img-responsive"> <span id="login-user" th:text="${userName}">사용자</span> 님, 안녕하세요. <a href="/logout" class="btn btn-sm btn-info active" role="button">Logout</a> </div> <div th:if="${#strings.isEmpty(userName)}"> <!-- 스프링 시큐리티에서 기본 제공하는 URL - 별도 컨트롤러 작성 필요 없음 --> <a href="/oauth2/authorization/google" class="btn btn-sm btn-success active" role="button">Google Login</a> </div> </div> <div th:if="${not #strings.isEmpty(userName)}" class="col-md-2"> <a href="/posts/save" role="button" class="btn btn-primary float-right">글 등록</a> </div> </div>
결과
참고: 네이버 연동하기
위의 작업 내용에 다음 부분을 추가하여 네이버도 연동할 수 있습니다.
네이버 아이디로 로그인하기 클라이언트 및 아이디 획득방법
1) application-oauth.properties 에 다음 내용 추가
## 네이버 ## # registration spring.security.oauth2.client.registration.naver.client-id=[클라이언트 아이디] spring.security.oauth2.client.registration.naver.client-secret=[클라이언트 비밀번호] spring.security.oauth2.client.registration.naver.redirect-uri={baseUrl}/{action}/oauth2/code/{registrationId} spring.security.oauth2.client.registration.naver.authorization_grant_type=authorization_code # 스코프는 변경될 수 있음 spring.security.oauth2.client.registration.naver.scope=name,email,profile_image spring.security.oauth2.client.registration.naver.client-name=Naver # provider spring.security.oauth2.client.provider.naver.authorization_uri=https://nid.naver.com/oauth2.0/authorize spring.security.oauth2.client.provider.naver.token_uri=https://nid.naver.com/oauth2.0/token spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me spring.security.oauth2.client.provider.naver.user_name_attribute=response
2) OAuthAttributes.java에 다음 내용 추가
package com.example.awsboard.config.auth; ......... @Getter public class OAuthAttributes { ......... public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map<String, Object> attributes) { // google, naver switch (registrationId) { case "naver": return ofNaver("id", attributes); } return ofGoogle(userNameAttributeName, attributes); } .......... public static OAuthAttributes ofNaver(String userNameAttributeName, Map<String, Object> attributes) { Map<String, Object> response = (Map<String, Object>) attributes.get("response"); return OAuthAttributes.builder() .name((String) response.get("name")) .email((String) response.get("email")) .picture((String) response.get("profile_image")) .attributes(response) .nameAttributeKey(userNameAttributeName) .build(); } ............... }
3) index.html 뷰 페이지에 구글 로그인 버튼 옆에 다음 추가
<a href="/oauth2/authorization/naver" class="btn btn-sm btn-secondary active" role="button">Naver Login</a>
6개의 댓글
ssung · 2021년 10월 15일 4:31 오후
안녕하세요 저도 이 책으로 공부중인데 도저히 원인을 모르겠는 에러에 막혀서 진행을 못하고있어서 여쭤봅니다 ㅜㅜ
저도 지금 이 부분을 작성하고 실행시키는데
Execution failed for task ‘:Application.main()’.
> Process ‘command ‘C:/Program Files/Java/jdk1.8.0_202/bin/java.exe” finished with non-zero exit value 1
이런 에러가 계속 나오면서 서버 실행자체가 안되더라고여… 이걸로 몇일째 시달리고있는데 혹시 무슨문제인지 알수있을까요..
SecurityConfig 클래스에서 @EnableWebSecurity를 주석치면 실행이되는데 어노테이션을 동작시키면 똑같은 에러가 반복됩니다..
(테스트코드도 비슷한에러가 나오면서 안되더라고여)
혹시 아신다면 도움 부탁드립니다 ㅜㅜ
yoonbumtae (BGSMM) · 2021년 10월 15일 9:08 오후
저도 잘 모르겠습니다 비슷한 문제에 대한 링크를 첨부하니 확인 부탁드립니다
https://githubmemory.com/repo/binchoo/spring-boot-210523/issues/17
코린이 · 2021년 10월 17일 9:41 오후
구글로그인은되는데 save 쪽에서 insert가 안되는데 이유가있을까요ㅠ
yoonbumtae (BGSMM) · 2021년 10월 17일 10:09 오후
save가 안되면 JPA 저장소 문제인 것 같습니다. 혹시 UserRepersitory.java 파일에 아래 메소드를 추가하셨나요?
Optional《User》 findByEmail(String email);
전체 코드: https://github.com/ayaysir/awsboard/blob/master/src/main/java/com/example/awsboard/domain/user/UserRepository.java
코린이 · 2021년 10월 20일 6:45 오후
네 findemail 은 실행이되는데 save가 실행조차안되고있어요 근데막상 컨트롤러같은곳에서 save 실행하면 insert갸되는데 왜이런지모르겠네용ㅜ
yoonbumtae (BGSMM) · 2021년 10월 20일 10:51 오후
save는 JPA에서 기본적으로 제공하는 메소드라 거기서 일어나는 에러는 잘 모르겠네요.. JPA랑 데이터베이스 연결이 잘 안된것일수도 있을것같습니다.