인텔리제이 아이디어(IntelliJ IDEA) 커뮤니티 버전, Spring JPA 및 롬복(Lombok) 사용을 전제로 합니다.
- 스프링 부트 버전: 2.3.1
- Gradle 버전: 6.4.1
출처: 스프링 부트와 AWS로 혼자 구현하는 웹 서비스
이 방법은 JSTL, Thymeleaf, Mustache 등 서버 사이드 템플릿 엔진을 사용하는 로그인 방법입니다. SPA에서 사용할 수 있는 소셜 로그인 연동 방법은 아래 글을 참고하세요,
순서
build.gradle에 디펜던시 추가application-oauth.properties작성 +.gitignore등록Roleenum 클래스 작성 – 사용자 권한 관리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();
}
}

User 엔티티 클래스를 통해 테이블을 만들었습니다.
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>
결과

로그인 전 화면

Google Login 버튼을 클릭하면 위와 같은 화면이 나옵니다.

로그인이 정상적으로 되었습니다.
참고: 네이버 연동하기
위의 작업 내용에 다음 부분을 추가하여 네이버도 연동할 수 있습니다.
네이버 아이디로 로그인하기 클라이언트 및 아이디 획득방법
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랑 데이터베이스 연결이 잘 안된것일수도 있을것같습니다.