티스토리 뷰
1. 구글 계정 만들기
먼저 OAuth 동의 화면에서 이름 짓고 밑에 3개 잘 등록되었는지만 확인하고 생성
제일 상단처럼 프로젝트 이름을 짓고 왼쪽 햄버거 버튼으로 사용자 인증 정보로 넘어오면 오른쪽 네모칸 처럼 클릭
승인된 리디렉션 URI
- 서비스에서 파라미터로 인증 정보를 주었을 때 인증이 성공하면 구글에서 리다이렉트할 URL입니다.
- 스프링 부트 security 2.0 부터는 기본적으로 {도메인}/login/oauth2/code/{소셜서비스코드}로 리다이렉트 URL을 지원하고 있습니다.
- 사용자가 별도로 리다이렉트할 URL을 지원하는 컨트롤러를 만들 필요가 없습니다. 시큐리티에서 구현됨
- 현재는 개발 상태이므로 위와 같이 적었고 포트는 알아서 맞춰서 사용하세요(기본 8080)
만들기 버튼으로 생성된 OAuth 2.0 클라이언트 ID 을 클릭하면 클라이언트 ID와 클라이언트 보안 비밀키가 확인이 된다.
이제 인텔리제이로 가서 설정을 하자.
src/main/resources 하위에 application-oauth.properties 파일을 생성합니다. 그리고 아래와 같이 작성.
spring.security.oauth2.client.registration.google.client-id = 클라이언트 ID
spring.security.oauth2.client.registration.google.client-secret = 클라이언트 보안 비밀
spring.security.oauth2.client.registration.google.scope = profile, email
강제로 profile과 email을 등록한 이유는 openid라는 scope가 있으면 Opne id Provider로 인식하기 때문입니다. 이렇게 되면 OpenId Provider인 서비스(구글)와 그렇지 않은 서비스(네이버/카카오)로 나눠서 각각 OAuth2Service를 만들어야 하기 때문입니다. 무슨 소린지 모르겠죠? 일단 진행합시다 저도 어려워요 ㅋㅋ
스프링 부트에서는 properties의 이름을 application-xxx.properties로 만들면 xxx라는 이름의 profile이 생성되어 이를 통해 관리할 수 있습니다. 즉 profile=xxx 라는 식으로 호출하면 해당 프로퍼티의 설정들을 가져올 수 있습니다. 그렇기에 같은 위치에 application.properties을 만들어 아래와 같이 옵션을 추가합시다.
spring.profiles.include = oauth
아그리고 여기까지 진행 했을 경우 application-oauth 파일은 절대 깃허브에 올라가면 안되니 ignore파일에 꼭 추가하시고 혹시라도 실수 해서 올라갔다면 지우고 다시 만들거나 하셔야 합니다. 보안키는 절대 털리면 안됩니다.
(혹시 .gitignore에 추가했는데도 커밋 목록에 노출되면 https://jojoldu.tistory.com/307 을 참고하세요)
사용자의 정보를 담당할 도메인인 User 클래스 생성
@Getter
@NoArgsConstructor
@Entity
public class User {
@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) //JPA로 데이터베이스로 저장할 때 Enum 값을 어떤 형태로 저장할지 결정
@Column(nullable = false) //기본적으로 int로 된 숫자가 저장됨, 숫자로하면 의미를 알 수 없어 문자열로 수정
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() ;
}
}
각 사용자의 권한을 관리할 Enum 클래스 Role을 생성
@Getter
@RequiredArgsConstructor
public enum Role {
GUEST("ROLE_GUEST", "손님"),
USER("ROLE_USER", "일반 사용자");
private final String key;
private final String title;
//스프링 시큐리티에서는 권한 코드에 항상 ROLE_ 가 앞에 있어야만 합니다.
}
마지막으로 User의 CRUD를 책임질 UserRepository 생성
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface User2Repository extends JpaRepository<User, Long> {
//소셜 로그인으로 반환되는 값 중 email을 통해 이미 생성된 사용자인지 처음 가입하는 사용자인지 판단하기 위함
Optional<User> findByEmail(String email);
}
의존성 추가하기
//소셜로그인 등 클라이언트에서 필요한 의존성
compile('org.springframework.boot:spring-boot-starter-oauth2-client')
SecurityConfig 클래스 생성
@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/**", "/profile").permitAll()
.antMatchers("/api/v1/**").hasRole(Role.USER.name())
.anyRequest().authenticated()
.and()
.logout()
.logoutSuccessUrl("/")
.and()
.oauth2Login()
.userInfoEndpoint()
.userService(customOAuth2UserService);
}
}
@EnableWebSecurity
-Spring Security 설정들을 활성화 시켜줍니다.
csrf().disable().headers().frameOptions().disable()
-h2-console 화면을 사용하기 위해 해당 옵션들을 disable 합니다.
authorizeRequests
-URL별 권한 관리를 설정하는 옵션의 시작점입니다. 이게 선언되야만 antMatchers 옵션 사용가능
antMatchers
-권한 관리 대상을 지정, URL, HTTP 메소드별로 관리가 가능
-"/"등 지정된 URL은 permitAll()로 전체 열람 가능하게 했고, /api/v1** 주소들은 USER 권한이 있어야 가능
logout().logoutSuccessUrl("/")
-로그아웃 성공시 이동할 주소이며 이는 컨트롤러에서 만들어서 사용해야함
oauth2Login
-OAuth2 로그인 기능에 대한 여러 설정의 진입점이며 소셜 로그인 성공 시 후속 조치를 진행할 UserService 인터페이스의 구현체를 등록하는 곳이 userService() 입니다. 소셜 서비스들에서 사용자 정보를 가져온 상태에서 추가로 진행하고 싶은 기능을 명시합니다.
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);
//현재 로그인 진행중인 서비스를 구분하는 코드, 지금은 구글만 사용하는 불필요한 기능이지만 이후 네이버 로그인 연동시 필요
String registrationId = userRequest.getClientRegistration().getRegistrationId() ;
//OAuth2 로그인 진행 시 키가되는 필드값을 이야기 합니다. PK와 같은 의미, 구글만 기본 코드 sub를 지원
String userNameAttributedName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName() ;
//OAuth2UserService를 통해 가져온 OAuth2User의 attribute를 담을 클래스
OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributedName, oAuth2User.getAttributes()) ;
User user = saveOrUpdate(attributes) ;
//SessionUser : 세션에 사용자 정보를 담기위한 Dto, User 클래스보다 좋음
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) ;
}
}
saveOrUpdate 메서드는 우리 테이블에 없는 정보면 insert을 하고 이미 있는 유저면 update을 한다. update하는 이유는 사용자의 정보가 바뀔 경우가 존재할까봐. (회원사진이나 이름을 바꿀 경우)
OAuthAttributes 클래스 생성
package com.bahngFamily.jihoon.springboot.config.auth.dto;
import com.bahngFamily.jihoon.springboot.domain.user.Role;
import com.bahngFamily.jihoon.springboot.domain.user.User;
import java.util.Map;
import lombok.Builder;
import lombok.Getter;
@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 ;
}
//of() : OAuth2User에서 반환하는 사용자 정보는 Map이기 때문에 값 하나하나를 변환해야만 합니다.
public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map<String, Object> attributes) {
if("naver".equals(registrationId)) {
return ofNaver("id", 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() ;
}
private 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() ;
}
//toEntity() : User엔티티 생성, 엔티티를 생성하는 시점은 처음 가입때 입니다.
//가입할 때 기본권한을 GUEST로 주기 위해 role 빌더값에는 Role.GUEST를 사용함
//OAuthAttribute클래스 생성이 끝났으면 같은 패키지에 SessionUser 클래스를 생성
public User toEntity() {
return User.builder()
.name(name)
.email(email)
.picture(picture)
.role(Role.GUEST)
.build();
}
}
SessionUser 클래스 생성
package com.bahngFamily.jihoon.springboot.config.auth.dto;
import com.bahngFamily.jihoon.springboot.domain.user.User;
import java.io.Serializable;
import lombok.Getter;
@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() ;
}
}
로그인 테스트
index.mustache에 로그인 버튼과 로그인 성공 시 사용자 이름을 보여주는 코드를 작성합니다.
{{>layout/header}}
<h1>스프링 부트로 시작하는 웹 서비스 Ver.2</h1>
<div class = "col-md-12">
<div class = "row">
<div class = "col-md-6">
<a href="/posts/save" role="button" class = "btn btn-primary">글 등록</a>
{{#userName}}
Logged in as: <span id="user">{{userName}}</span>
<a href="/logout" class="btn btn-info active" role="button">Logout</a>
{{/userName}}
{{^userName}}
<a href="/oauth2/authorization/google" class="btn btn-success active" role="button">Google Login</a>
<a href="/oauth2/authorization/naver" class="btn btn-secondary active" role="button">Naver Login</a>
<!-- <a href="/sighup" class="btn btn-info" role="button">회원가입</a>-->
{{/userName}}
</div>
</div>
<br>
<!-- 목록 출력 영역 -->
<table class="table table-horizontal table-bordered">
<thead class="thead-strong">
<tr>
<th>게시글 번호</th>
<th>제목</th>
<th>작성자</th>
<th>최종수정일</th>
</tr>
</thead>
<tbody id="tbody">
{{#posts}}
<tr>
<td>{{id}}</td>
<td><a href="/posts/update/{{id}}">{{title}}</a></td>
<td>{{author}}</td>
<td>{{modifiedDate}}</td>
</tr>
{{/posts}}
</tbody>
</table>
</div>
{{>layout/footer}}
a href = "/logout"
-스프링 시큐리티에서 기본적으로 제공하는 로그아웃 URL입니다.
-즉, 개발자가 별도로 저 URL에 해당하는 컨트롤러를 만들 필요가 없습니다.
a href="/oauth2/authorization/google
-스프링 시큐리티에서 기본적으로 제공하는 로그인 URL입니다.
-로그아웃과 마찬가지로 따로 컨트롤러를 만들 필요가 없습니다.
Controller
@RequiredArgsConstructor
@Controller
public class IndexController {
private final PostsService postsService;
private final HttpSession httpSession;
@GetMapping("/")
public String index(Model model, @LoginUser SessionUser user){
model.addAttribute("posts", postsService.findAllDesc());
SessionUser user = (SessionUser) httpSession.getAttribute("user");
if(user != null)
model.addAttribute("userName", user.getName());
return "index";
}
}
스프링 시큐리티에서 간편 로그인과 관련하여 제공해주는게 아주 많기 때문에 설정만 잘하고 필요한 클래스만 상속 잘 받아서 오버라이딩만 하면 잘 진행된다. 이에 대해 자세히 알고 싶다면 아래의 사이트를 참고하면 좋을 것 같다.
www.baeldung.com/spring-security-5-oauth2-login
출처 : 스프링 부트와 AWS로 혼자 구현하는 웹 서비스
'Back-end > Spring' 카테고리의 다른 글
SpringBoot에서 Swagger 설정하기(3.0 버전) (0) | 2022.07.30 |
---|---|
JWT 구현하기 (0) | 2020.08.25 |
Jwt 세션방식을 토큰 방식으로.. (0) | 2020.08.22 |
SpringBoot에서 JPA 설정 및 데이터 저장해보기 (0) | 2020.08.09 |
SpringBoot 기본구조와 단위 테스트 작성 (0) | 2020.08.08 |