본문 바로가기
Web/SpringBoot

5. Spring security와 OAuth2.0으로 로그인 기능 구현

by SeleniumBindingProtein 2022. 3. 3.
728x90
반응형

1. Spring security & Spring security Oauth 2.0 클라이언트

  • Spring security : Authentication과 Authorization 기능을 가진 프레임워크
  • 스프링 부트 1.5 vs 스프링 부트 2.0 
    • 스프링 부트 1.5에서의 OAuth2 연동 방법이 2.0에서 크게 달라졌지만 인터넷 자료를 보면 설정 방법에 크게 차이가 없음을 확인할 수 있음 => 이유는 spring-security-oauth2-autoconfigure 라이브러리 때문임
    •  spring-security-oauth2-autoconfigure 라이브러리를 사용할 경우, 기존 부트 1.5에서 쓰던 설정을 2.0에서도 사용 가능함
    • 스프링 부트 2 방식 자료를 찾고 싶은 경우
      • spring-security-oauth2-autoconfigure 라이브러리 사용여부, application.properties 혹은 application.yml 정보에 대한 설정 차이를 확인해봐야 함
    • 스프링 부트 1.5 방식에선 url 주소를 모두 명시해야 했지만, 2.0 방식에선 client 인증 정보만 입력하면 됨
      • 1.5버젼에서 직접 입력했던 값들은 2.0버전으로 오면서 모두 enum으로 대체되었음
      • CommonOAuth2Provider라는 enum이 새롭게 추가되어 구글, 깃허브, 페이스북, 옥타의 기본 설정값이 모두 여기서 제공함

2. 구글 서비스 등록

  • 구글 서비스에 신규 서비스 생성
    • 발급된 인증 정보를 통해서 로그인 기능과 소셜 서비스 기능 사용 가능
    • 구글 클라우드 플랫폼 주소(http://console.cloud.google.com)
      • 애플리케이션 이름 : 구글 로그인 시, 사용자에게 노출될 애플리케이션 이름
      • 지원 이메일 : 사용자 동의 화면에서 노출될 이메일 주소
      • 승인된 리디렉션 URI
        • 서비스에서 파라미터로 인증정보를 주었을 때, 인증이 성공하면 구글에서 리다이렉트 할 URL
        • 스프링 부트 2 버젼의 시큐리티에서는 기본적으로 {도메인}/login/oauth2/code/{소셜서비스코드}로 리다이렉트 URL을 지원하고 있음
        • 사용자가 별도로 리다이렉트 URL을 지원하는 Controller를 만들 필요가 없고, 시큐리티에서 이미 구현해 놓은 상태임
        • 현재는 개발 단계이므로, http://localhost:8080/login/oauth2/code/google로만 등록함
        • AWS 서버에 배포하게 되면 localhost 외에 추가로 주소를 추가해야하며, 이건 이후 단계에서 진행하겠음
      • 클라이언트ID와 클라이언트 보안비밀 코드를 프로젝트에서 설정

012345678

 

  • 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
      • 기본값이 openid,profile,email이기 때문에, 많은 예제에서는 이 scope를 별도로 등록하지 않고 있음
      • 강제로 profile,email을 등록한 이유는 openid라는 scope가 있으면 Open Id Provider로 인식하기 때문임
      • 이렇게 되면, OpenId Provider인 서비스와 그렇지 않은 서비스로 나눠서 각각 OAuth2Service를 만들어야 함..
      • 하나의 OAuth2Service로 사용하기 위해 일부러 openid scope를 빼고 등록함 
  • src/main/resources/application.properties에 추가
    • spring.profiles.include=oauth
  • .gitignore 등록
    • 보완을 위해 
    • application-oauth.properties 등록

3. 구글 로그인 연동

  • 사용자 정보를 담당할 도메인인 User클래스
package com.jojoldu.book.springboot.user;

import com.jojoldu.book.springboot.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)	//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
package com.jojoldu.book.springboot.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;
}
  • User의 CRUD를 책임질 UserRepository
package com.jojoldu.book.springboot.user;

import com.jojoldu.book.springboot.user.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User,Long> {
    Optional<User> findByEmail(String email);
}
  • 스프링 시큐리티 설정
    • build.gradle
      • 스프링 시큐리티 관련 의존성 추가
        • 소셜 로그인 등 클라이언트 입장에서 소셜 기능 구현시 필요한 의존성임
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
  • OAuth 라이브러리를 이용한 소셜 로그인 설정 코드 작성
    • 패키지 
      • 클래스 SecurityConfig
package com.jojoldu.book.springboot.config.auth;


import com.jojoldu.book.springboot.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()	//h2-console 화면을 사용하기 위해 해당 옵션들을 disable 시킴
                .and()
                .authorizeRequests()	//URL별 권한 관리를 설정하는 옵션의 시작점이고, authorizeRequests가 선언되어야만 antMatchers 옵션을 사용할 수 있음
                .antMatchers("/", "/css/**", "/images/**","/js/**","/h2-console/**")	//권한 관리 대상을 지정하는 옵션임. URL,HTTP 메소드별로 관리가 가능함. "/"등 지정된 URL들은 permitAll() 옵션을 통해 전체 열람 권한을 주었고, "/api/v1/**"주소를 가진 API는 USER 권한을 가진 사람만 가능하도록 했음	
                .permitAll().antMatchers("/api/v1/**").hasRole(Role.USER.name())
                .anyRequest().authenticated()	//anyRequest : 설정된 값들 이외 나머지 URL들을 나타냄
                .and()							//authenticated()를 추가하여 나머지 URL들은 모두 인증된 사용자들에게만 허용함. 인증된 사용자(로그인한 사용자)
                .logout().logoutSuccessUrl("/")	//로그아웃 기능에 대한 여러 설정의 진입점이고, 로그아웃 성공 시 / 주소로 이동함
                .and().oauth2Login()	//OAuth2 로그인 기능에 대한 여러 설정의 진입점
                .userInfoEndpoint()		//OAuth2 로그인 성공 이후 사용자 정보를 가져올 때의 설정들을 담당함
                .userService(customOAuth2UserService);	//소셜 로그인 성공 시 후속 조치를 진행할 UserService 인터페이스의 구현체를 등록함. 
                										//리소스 서버에서 사용자 정보를 가져온 상태에서 추가로 진행하고자 하는 기능을 명시할 수 있음
    }
}
  • 아래에 있는 클래스는 구글 로그인 이후 가져온 사용자의 정보들을 기반으로 가입 및 정보수정, 세션 저장등의 기능을 지원함
    • registrationId
      • 현재 로그인 진행 중인 서비스를 구분하는 코드
      • 구글만 사용하는 불필요한 값이지만, 네이버 로그인 연동시 구분을 위해 사용됨
    • userNameAttributeName
      • OAuth2 로그인 진행 시, 키가 되는 필드값(Primary Key)
      • 구글의 경우, 기본적으로 코드를 지원하지만, 네이버, 카카오 등은 지원하지 않음
    • OAuthAtrributes
      • OAuth2UserService를 통해 가져온 OAuth2User의 attribute를 담을 클래스이며, 여러 소셜 로그인도 이 클래스를 사용
    • SessionUser
      • 세션에 사용자 정보를 저장하기 위한 Dto 클래스
      • 왜 User 클래스를 쓰지 않고 새로 만들어서 쓰는지 ?
        • 세션에 저장히기 위해 User 클래스를 세션에 저장하려고 하니, User 클래스에 직렬화를 구현하지 않았기 때문에 에러가 발생함.
        • User 클래스가 엔티티이기 때문에 언제 다른 엔티티와 관계가 형성될지 모름
        • 그렇기 때문에 직렬화 하기 보다는 직렬화 기능을 가진 세션 Dto를 하나 추가로 만드는 것이 유지보수 및 운영에 더 도움이 됨
package com.jojoldu.book.springboot.config.auth;


import com.jojoldu.book.springboot.config.auth.dto.OAuthAttributes;
import com.jojoldu.book.springboot.config.auth.dto.SessionUser;
import com.jojoldu.book.springboot.user.UserRepository;
import com.jojoldu.book.springboot.user.User;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import lombok.RequiredArgsConstructor;
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();
        String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint()
                .getUserNameAttributeName();

        OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());

        User user = saveOrUpdate(attributes);
												
        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);
    }

}
  • OAuthAttribute를 Dto로 생각하고 config.auth.dto 패키지에 생성
    • of() 
      • OAuth2User에서 반한되는 사용자 정보는 Map이기 때문에 값 하나하나를 변환해야만 함
    • toEntity()
      • User 엔티티를 생성함
      • OAuthAttributes에서 엔티티를 생성하는 시점은 처음 가입할 때임
      • 가입할 때의 기본권한을 GUEST로 주기 위해서 role 빌더값에는 Role.GUEST를 사용함
      • OAuthAttributes 클래스 생성이 끝났으면 같은 패키지에 SessionUser 클래스를 생성함
package com.jojoldu.book.springboot.config.auth.dto;

import com.jojoldu.book.springboot.user.Role;
import com.jojoldu.book.springboot.user.User;
import lombok.Builder;
import lombok.Getter;

import java.util.Map;

@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;
    }

    public static OAuthAttributes of(String registrationId, String userNameAttributeName,
                                     Map<String, Object> attributes){
        if ("naver".equals(registrationId)){
            return ofNaver("id", 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();
    }

    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();
    }

    public User toEntity(){
        return User.builder()
                .name(name)
                .email(email)
                .picture(picture)
                .role(Role.GUEST)
                .build();
    }
}
  • SessionUser 클래스 추가
    • 인증된 사용자 정보만 필요함
package com.jojoldu.book.springboot.config.auth.dto;

import com.jojoldu.book.springboot.user.User;
import lombok.Getter;

import java.io.Serializable;

@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에 로그인 버튼과 로그인 성공 시 사용자 이름을 보여주는 코드 추가
    • {{#userName}}
      • 머스테치는 다른 언어와 같은 if문을 제공하지 않음
      • true/false 여부만 판단할 뿐임
      • 머스테치에서는 항상 최종값을 넘겨줘야 함
      • userName이 있다면 userName을 노출시키도록 구성했음
    • 개발자가 별도로 URL에 해당하는 컨트롤러를 만들 필요가 있음 
    • {{^userName}}
      • 머스테치에서 해당 값이 존재하지 않는 경우에 사용함
      • userName 없다면 로그인 버튼을 노출시키도록 구성했음
    • a href ="/oauth2/authorizationgoogle"
      • 스프링 시큐리티에서 기본적으로 제공하는 로그인 URL
      • 로그아웃 URL과 마찬가지로 개발자가 별도의 컨트롤러를 생성할 필요가 없음
<!DOCTYPE HTML>
<html>
<head>
    <title>스프링부트 웹 서비스</title>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
</head>
    <body>
    {{>layout/header}}
        <h1>스프링부트로 시작하는 웹서비스</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" role="button">Google Login</a>

                        <a href="/oauth2/authorization/naver" class="btn btn-secondary" role="button">Naver Login</a>
                    {{/userName}}
                </div>
            </div>
            <br>
<!--           목록 출력 영역-->
            <table class="table table-horizontal table-bordered">
                <thead class="thread-strong">
                <tr>
                    <th>게시글 번호</th>
                    <th>제목</th>
                    <th>작성자</th>
                    <th>최종수정일</th>
                </tr>
                </thead>
                <tbody id="tbody">
                {{#posts}} <!-- posts라는 List를 순회함 (for문과 비슷함) -->
                    <tr>
                        <td>{{id}}</td>  <!-- List에서 뽑아낸 객체의 필드를 사용함함-->
                        <td><a href="/posts/update/{{id}}">{{title}}</a></td>
                        <td>{{author}}</td>
                        <td>{{modifiedDate}}</td>
                    </tr>
                {{/posts}}
                </tbody>
            </table>
        </div>
    {{>layout/footer}}
    </body>
</html>

 

 

  • index.mustache에서 userName을 사용할 수 있게 IndxController에서 userName을 model로 저장하는  코드 추가
    • (SessionUser)httpSession.getAttribute("user")
      • 로그인 성공시 httpSession.getAttribute("user")에서 값을 가져올 수 있음
    • if(user!=null)
      • 세션에 저장된 값이 있을 때만 model에 userName으로 등록함
      • 세션에 저장된 값이 없으면 model엔 아무런 값이 없는 상태이니 로그인 버튼이 보이게 됨
package com.jojoldu.book.springboot.web;

import com.jojoldu.book.springboot.config.auth.LoginUser;
import com.jojoldu.book.springboot.config.auth.dto.SessionUser;
import com.jojoldu.book.springboot.service.PostsService;
import com.jojoldu.book.springboot.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){
        model.addAttribute("posts", postsService.findAllDesc());

        if(user!=null){
            model.addAttribute("userName", user.getName());
        }
        return "index";
    }

    @GetMapping("/posts/save")
    public String postsSave(){
        return "posts-save";
    }

    @GetMapping("/posts/update/{id}")
    public String postsUpdate(@PathVariable Long id, Model model){
        PostsResponseDto dto = postsService.findById(id);
        model.addAttribute("post", dto);

        return "posts-update";
    }


}

[결과 출력]

01234

 

4. 어노테이션 기반으로 개선

  • 같은 코드가 반복되면, 수정이 반영되지 않거나, 유지 보수성이 떨어질 수 밖에 없음
    • 이를 개선하기 위해, 어노테이션 기반을 사용함
  • config/auth/LoginUser.interface
package com.jojoldu.book.springboot.config.auth;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)	//생성될 수 있는 위치를 지정함. PARAMETER로 지정했으니, 메소드의 파리미터로 선언된 객체에서만 사용할 수 있음
@Retention(RetentionPolicy.RUNTIME) 
public @interface LoginUser {	//어노테이션 클래스로 지정함. LoginUser라는 이름을 가진 어노테이션이 생성되었음

}
  • LoginUserArgumentResolver라는 HandlerMethodArgumentResolver 인터페이스를 구현한 클래스
    • HandlerMethodArgumentResolver는 한가지 기능을 지원하는데, 조건에 맞는 경우 메소드가 있다면 구현체가 지정한 값으로 해당 메소드의 파리미터로 넘길 수 있음
package com.jojoldu.book.springboot.config.auth;

import com.jojoldu.book.springboot.config.auth.dto.SessionUser;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import javax.servlet.http.HttpSession;

@RequiredArgsConstructor
@Component
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {
    private final HttpSession httpSession;

    @Override		//컨트롤러 메서드의 특정 파라미터를 지원하는지 판단함. 파라미터에 @LoginUser 어노테이션이 붙어있고, 파라미터 클래스 타입이 SessionUser.class인 경우 true로 반홤함
    public boolean supportsParameter(MethodParameter parameter){
        boolean isLoginUserAnnotation = parameter.getParameterAnnotation(LoginUser.class) != null;
        boolean isUserClass = SessionUser.class.
                    equals(parameter.getParameterType());
        return isLoginUserAnnotation && isUserClass;
    }

    @Override		//파라미터에 전달할 객체를 생성하고, 세션에서 객체를 가져옴
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception{
        return httpSession.getAttribute("user");
    }
}
  • LoginUserArgumentResolver가 스프링에서 인식될 수 있도록 WebMvcConfigureer에 추가
    • HandlerMethodArgumentResolver는 항상 WebMvcConfigurer의 addArgumentResolvers()를 통해 추가해야함
package com.jojoldu.book.springboot.config;

import com.jojoldu.book.springboot.config.auth.LoginUserArgumentResolver;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {
    private final LoginUserArgumentResolver loginUserArgumentResolver;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers){
        argumentResolvers.add(loginUserArgumentResolver);
    }
}
  • IndexController 코드에서 반복되는 부분들을 모두 @LoginUser로 개선함
package com.jojoldu.book.springboot.web;

import com.jojoldu.book.springboot.config.auth.LoginUser;
import com.jojoldu.book.springboot.config.auth.dto.SessionUser;
import com.jojoldu.book.springboot.service.PostsService;
import com.jojoldu.book.springboot.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){
        model.addAttribute("posts", postsService.findAllDesc());

        if(user!=null){
            model.addAttribute("userName", user.getName());
        }
        return "index";
    }

    @GetMapping("/posts/save")
    public String postsSave(){
        return "posts-save";
    }

    @GetMapping("/posts/update/{id}")
    public String postsUpdate(@PathVariable Long id, Model model){
        PostsResponseDto dto = postsService.findById(id);
        model.addAttribute("post", dto);

        return "posts-update";
    }


}

5. 세션 저장소로 데이터베이스 사용

  • 애플리케이션을 재실행하면 로그인이 풀리는데, 그 이유는 세션이 내장 톰캣의 메모리에 저장되기 때문임
  • 기본적으로 세션은 실행되는 WAS의 메모리에서 저장되고 호출되며,
  • 메모리에 저장되다 보니 내장 톰캣처럼 애플리케이션 실행 시 실행되는 구조에선 항상 초기화가 됨
  • 즉, 배포할때 마다 톰캣이 재시작됨
  • 또한, 2대 이상의 서버에서 서비스하고 있다면, 톰캣마다 세션 동기화 설정을 해야만 함
  • 세션 저장소
    • 1) 톰캣 세션을 사용함
      • 이 경우는 기본적으로 선택되는 방식으로, 톰캣에 세션이 저장되기 때문에 2대 이상의 WAS가 구동되는 환경에서는 톰캣들 간의 세션 공유를 위한 추가 설정이  필요함
    • 2) MySQL과 같은 데이터베이스를 세션 저장소로 사용함
      • 여러 WAS간의 공용 세션을 사용할 수 있는 가장 쉬운 방법이고, 많은 설정이 필요 없지만, 결국 로그인 요청마다 DB IO가 발생하여 성능상 이슈가 발생할 수 있음
      • 보통 로그인 요청이 많이 없는 백오피스, 사내 시스템 용도에서 많이 사용함
    • 3) Redis, Memcached와 같은 메모리 DB를 세션 저장소로 사용
      • B2C 서비스에서 가장 많이 사용하는 방식
      • 실제 서비스로 사용하기 위해선 Embedded Redis와 같은 방식이 아닌 외부 메모리 서버가 필요함
  • 두 번째 방식인 데이터베이스를 세션 저장소로 사용하는 방식을 선택하여 진행
    • 설정 간단, 사용자가 많은 서비스가 아님, 비용 절감
    • build.gradle에서 spring-session-jdbc 등록
implementation 'org.springframework.session:spring-session-jdbc'
  • 세션 저장소를 데이터베이스로 교체했지만 스프링을 재시작하면 세션이 풀림
    • 그 이유는 H2 기반으로 스프링이 재실행될 때, H2도 재시작 되기 때문임
    • AWS로 배포하면 AWS의 데이터베이스 서비스인 RDS(Relational Database Service)를 사용하게 되니, 이때부턴 세션이 풀리지 않음

6. 네이버 로그인

 

애플리케이션 - NAVER Developers

 

developers.naver.com

01

  • 네이버에서는 스프링 시큐리티를 공식 지원하지 않기 때문에 CommonOAuth2Provider에서 해주던 값들 전부 수동으로 입력함
    • user_name_attribute=response
      • 기준이 되는 user_name의 이름을 네이버에서는 response로 해야 함
      • 이유는 네이버의 회원 조회 시 반환되는 JSON 형태 때문임
#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
  • 스프링 시큐리티 설정 등록
    • 네이버인지 판단하는 코드와 네이버 생성자만 추가해줌 ( 이전 구글 로그인 등록하면서 대부분 코드가 확장성 있게 작성되었음)
package com.jojoldu.book.springboot.config.auth.dto;

import com.jojoldu.book.springboot.user.Role;
import com.jojoldu.book.springboot.user.User;
import lombok.Builder;
import lombok.Getter;

import java.util.Map;

@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;
    }

    public static OAuthAttributes of(String registrationId, String userNameAttributeName,
                                     Map<String, Object> attributes){
        if ("naver".equals(registrationId)){
            return ofNaver("id", 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();
    }

	//네이버
    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();
    }

    public User toEntity(){
        return User.builder()
                .name(name)
                .email(email)
                .picture(picture)
                .role(Role.GUEST)
                .build();
    }
}
  • indx.mustache에 네이버 로그인 버튼을 추가
    • /oauth2/authorization/naver
    • 네이버 로그인 URL은 application-oauth.properties에 등록한 redirect-uri 값에 맞춰 자동으로 등록됨
  {{^userName}}
         <a href="/oauth2/authorization/google" class="btn btn-success" role="button">Google Login</a>

         <a href="/oauth2/authorization/naver" class="btn btn-secondary" role="button">Naver Login</a>
{{/userName}}

[결과 출력]

01

728x90
반응형

댓글