일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- appspec
- 외부키
- 검색
- 예약
- 참조키
- 적용우선순위
- ㅔㄴ션
- 포트
- querydsl
- 테스트메소드
- AuthenticationEntryPoint
- 서브쿼리
- foreignkey
- ubuntu
- appspec.yml
- 컨테이너실행
- 네이티브쿼리
- WeNews
- 2 > /dev/null
- 메세지수정
- 추후정리
- subquery
- 테스트
- 커밋메세지수정
- 메소드명
- docker명령어
- application.yml
- MySQL
- Query
- EC2
- Today
- Total
제뉴어리의 모든것
Spring boot에 Google Login 연동하기 본문
* 인텔리제이 아이디어(IntelliJ IDEA) (커뮤니티 가능) , JPA 및 롬복(Lombok) 사용을 전제로 합니다.
해당 게시물은 스프링 부트(Spring Boot): 구글 로그인 연동 (스프링 부트 스타터의 oauth2-client) 이용 + 네이버 아이디로 로그인 - BGSMM (yoonbumtae.com) 를 참고하여 본인이 직접 개발한 내용을 토대로 작성되었습니다
- 스프링 부트 버전 : 2.4.2
- 메이븐 버전 : 2.4.2
- 인텔리제이 버전 : Intellij Ultimate 2020.3.1
- 마리아 db 버전 : mariadb-10.5.8-winx64
- 자바 버전 : Amazon Corretto jdk11.0.10_9
이 방법은 JSTL, Thymeleaf, Mustache 등 서버 사이드 템플릿 엔진을 사용하는 로그인 방법입니다.
> 순서
- pom.xml에 디펜던시 추가
- application-oauth.properties 작성 + .gitignore 등록
- Role enum 클래스 작성 – 사용자 권한 관리
- User 클래스 작성 – JPA Entity 클래스
- OAuthAttributes 클래스 작성 – 구글 로그인 이후 가져온 사용자의 이메일, 이름, 프로필 사진 주소 를 저장하는 DTO
- CustomOAuth2UserService 클래스 작성 – OAuthAttributes을 기반으로 가입 및 정보수정, 세션 저장 등 기능 수행
- SecurityConfig 클래스 작성 – 스프링 시큐리티 설정
- SessionUser 클래스 작성 – User 엔티티 클래스에서 직렬화가 필요한 경우 별도로 사용
- UserRepository 클래스 생성
- IndexController 작성, Thymeleaf 뷰 페이지 작성
> 프로젝트 구조
> 사전 작업 >구글로부터 연동할 프로젝트(클라이언트)의 아이디와 비밀번호 받아오기
참조 : 구글 클라우드 플랫폼에 앱 클라이언트 생성하기 (tistory.com)
1. pom.xml에 dependency 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
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
3) Role enum 클래스 작성
사용자의 권한을 enum 클래스로 만들어 관리합니다.
package com.portfolio.mymall.domain;
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을 사용하지 않고도 자바 코드 내에서 테이블을 생성할 수 있습니다.
(application.properties에 spring.jpa.hibernate.ddl-auto=update 가 되 있어야한다.)
package com.portfolio.mymall.domain;
import com.portfolio.mymall.domain.Base_Entity;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Getter
@NoArgsConstructor
@Entity
public class User extends Base_Entity {
@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();
}
}
참고: BaseTimeEntity 클래스 (JpaAuditing – 글 작성 시점, 수정 시점 자동 추가)
User 클래스가 상속 받는 Base_Entity의 소스
package com.portfolio.mymall.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.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@MappedSuperclass //클래스가 만들어지지 않는 기초 클래스라는 어노테이션
@Getter
@EntityListeners(value = {AuditingEntityListener.class}) //Entity의 변화를 감지하는 리스너
abstract class Base_Entity {
@CreatedDate //만들어질때 입력 되는 필드임을 정의하는 어노테이션
@Column(name = "reg_date", updatable = false)
private LocalDateTime regDate;
@LastModifiedDate //마지막 수정때 입력 되는 필드임을 정의하는 어노테이션
@Column(name = "mod_date")
private LocalDateTime modDate;
}
메인 애플리케이션에서 @EnableJpaAuditing 어노테이션을 추가해 활성화합니다.
package com.portfolio.mymall;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@EnableJpaAuditing //Base_Entity에 있는 날짜 자동 입력 활성화
@SpringBootApplication
public class MymallApplication {
public static void main(String[] args) {
SpringApplication.run(MymallApplication.class, args);
}
}
5) OAuthAttributes 클래스 작성
구글 로그인 이후 가져온 사용자의 이메일, 이름, 프로필 사진 주소를 저장하는 DTO
package com.portfolio.mymall.dto;
import com.portfolio.mymall.domain.Role;
import com.portfolio.mymall.domain.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.portfolio.mymall.service;
import com.portfolio.mymall.config.auth.SessionUser;
import com.portfolio.mymall.domain.User;
import com.portfolio.mymall.dto.OAuthAttributes;
import com.portfolio.mymall.repository.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 org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpSession;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@Transactional
@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();
//테스트
System.out.println("==================== "+registrationId +" ==================");
// oauth2 로그인 진행 시 키가 되는 필드값
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUserNameAttributeName();
//테스트
System.out.println("================ "+userNameAttributeName+" =================");
// 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) {
// List<User> user = userRepository.findByEmail(attributes.getEmail())
// .map(entity -> entity.update(attributes.getName(), attributes.getPicture())).collect(Collectors.toList());
// return userRepository.save(user.get(0));
List<User> result = userRepository.findByEmail(attributes.getEmail())
.map(entity -> entity.update(attributes.getName(), attributes.getPicture())).collect(Collectors.toList());
User user;
if(result.isEmpty())
{
user = attributes.toEntity();
}
else
{
user = result.get(0);
}
return userRepository.save(user);
}
}
7) SecurityConfig 클래스 작성 – 스프링 시큐리티 설정
package com.portfolio.mymall.config;
import com.portfolio.mymall.domain.Role;
import com.portfolio.mymall.service.CustomOAuth2UserService;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomOAuth2UserService customOAuth2UserService;
private final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.headers().frameOptions().disable()
.and()
.authorizeRequests()
.antMatchers("/", "/Savory-gh-pages/**").permitAll()
//.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/")
.and()
.logout().logoutSuccessUrl("/")
.and()
.oauth2Login().userInfoEndpoint().userService(customOAuth2UserService);
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
logger.info("build Auth global.......");
auth.inMemoryAuthentication()
.withUser("user")
.password("{noop}1111")
.roles(Role.USER.name());
}
}
> configure 함수 내용
csrf().disable() : basic auth를 사용하기 위해 csrf 보호 기능 disable
.authorizeRequests() : 요청에 대한 권한을 지정할 수 있다. (설정 시작점)
.antMatchers("/", "/Savory-gh-pages/**").permitAll() : "/", "/Savory-gh-pages/**" 으로 들어오는 요청은 모두 허용해준다 ("/" 은 index페이지를 호출하는 url, "/Savory-gh-pages/**" 은 /resource/static 에 있는 css 파일들이 정의 되어있는 디렉토리이므로 무조건 허용)
.anyRequest().authenticated() : 본 소스에서는 주석처리 됬지만, 어떠한 요청이든 인증 받고 가능하게 하는 설정.
본 사이트의 어떤 기능을 접근하려 해도 일단 로그인을 요청함.
.formLogin() : form 태그 기반의 로그인을 지원하겠다는 설정입니다. (개발자가 직접 login 폼을 만들 필요 없음)
.loginPage("/login") : "/login" 경로의 로그인 페이지를 연결
defaultSuccessUrl("/") : 로그인 성공 후 리다이렉트할 주소
.logout() : logout에 대한 설정
logoutSccessUrl() : 로그아웃 성공 후 리다이렉트할 주소
invalidateHttpSession() : 로그아웃 이후 세션 전체 삭제 여부
8) SessionUser 클래스 작성
User 엔티티 클래스에서 직렬화가 필요한 경우 별도로 사용하기 위한 클래스를 작성합니다.
package com.portfolio.mymall.config.auth;
import com.portfolio.mymall.domain.User;
import lombok.Getter;
import java.io.Serializable;
@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. UserRepository 생성
User 엔티티의 Select와 Save를 담당할 Repository를 임의로 생성한다
package com.portfolio.mymall.repository;
import com.portfolio.mymall.domain.User;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.stream.Stream;
public class UserRepository {
private final EntityManager entityManager;
public UserRepository(EntityManager entityManager) {
this.entityManager = entityManager;
}
public Stream<User> findByEmail(String email)
{
List<User> result = entityManager.createQuery("select U from User U where U.email = :email", User.class)
.setParameter("email", email)
.getResultList();
if(result.isEmpty())
{
//예외처리
}
return result.stream();
}
public User save(User user) {
entityManager.persist(user);
return user;
}
}
10) IndexController 작성, Thymeleaf 뷰 페이지 작성
- View 페이지 (index.html)
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:replace="~{layout/basic :: setContent(~{this::content})}"> <!-- fragment로 만들어서 본 페이지로 보냄 -->
<th:block th:fragment="content">
<h1>Welcome Page</h1>
<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>
</th:block>
</th:block>
- Index 컨트롤러
package com.portfolio.mymall.controller;
import com.portfolio.mymall.config.auth.SessionUser;
import com.portfolio.mymall.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.web.servlet.oauth2.login.OAuth2LoginSecurityMarker;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import javax.servlet.http.HttpSession;
@RequiredArgsConstructor
@Controller
public class HomeController {
Logger logger = LoggerFactory.getLogger(HomeController.class);
// private final PostsService postsService;
private final HttpSession httpSession;
@GetMapping("/")
public String index(Model model){
SessionUser user = (SessionUser) httpSession.getAttribute("user");
logger.debug("index");
if(user != null) {
model.addAttribute("userName", user.getName());
model.addAttribute("userImg", user.getPicture());
}
return "index";
}
}
결과
Google Login 버튼이 생성되고 클릭하면 아래와 같이 로그인 창이 생성 된다. (구글 클라우드 플랫폼 대시보드에 등록했던 계정으로 로그인할것)
로그인 을 하면 아래와 같이 로그인이 정상적으로 완료된다.
그리고 로그인을 하면 서버의 DB에 있는 User 테이블에 해당 유저의 정보를 Google 로부터 받아서 저장한다.
그리고 기존에 본 사이트에 Google로 로그인 한 적이 있는 유저고 이름등과 같은 정보가 변경 되었다면 update 된다.
참조 : 15. 스프링 시큐리티와 OAuth 2.0으로 로그인 기능 구현하기 (2) (tistory.com)
'기타...' 카테고리의 다른 글
html에서 controller로 값을 넘길 때 매개변수에 매칭되는 기준.. (0) | 2021.02.27 |
---|---|
html, script, thymeleaf 주석 방법 (0) | 2021.02.27 |
구글 클라우드 플랫폼에 앱 클라이언트 생성하기 (0) | 2021.02.18 |
git 원격 저장소의 내용을 내 로컬로 가져오기 (0) | 2021.02.15 |
git revert시 주의 사항 (0) | 2021.02.15 |