Spring Security & 구글 OAuth & 테스트코드로 진행하는 계정 권한 관리 - 2

Spring·2017.08.14 12:54


1-4. 로그인 세션 관리


OAuth2를 사용한다고해서 기존과 다른 마법같은 일이 펼쳐지는것은 아닙니다.
OAuth2는 사용자 인증 및 허가된 정보를 가져오는 것외에는 사용하지 않습니다.
인증된 정보를 통해 로그인 세션을 관리하고, 사용자는 저희가 만든 서비스의 세션을 통해 서비스를 이용해야만 합니다.

세션을 사용하는 방법은 크게 3가지가 있습니다.

  1. 톰캣 세션을 사용한다.
  • 일반적으로 별다른 설정을 하지 않으시고 HttpSession을 사용할 경우입니다.
  • 이렇게 될 경우 톰캣(WAS)에 세션이 저장되기 때문에 2대이상의 WAS가 구동되는 환경에서는 톰캣들간의 세션 공유를 위한 추가설정이 필요합니다.
  1. Database를 세션저장소로 사용한다.
  • 여러 WAS들간의 공용 세션을 사용할 수 있는 가장 쉬운 방법입니다.
  • 많은 설정이 필요없지만, 결국 로그인 요청마다 DB IO가 발생하여 성능상 이슈가 발생할 수 있습니다.
  • 보통 로그인이 요청이 많이 없는 백오피스, 사내시스템 용도에서 사용합니다.
  1. Redis, Memcached와 같은 메모리 DB를 세션 저장소로 사용한다.
  • 사용자 서비스에서 가장 많이 사용되는 방식입니다.
  • 실제 서비스로 사용하기 위해서는 Embedded Redis와 같은 방식이 아닌 외부 메모리 서버가 필요합니다.

여기선 2번째 방식인 Database를 세션 저장소로 사용되는 방식을 선택하여 진행할 예정입니다.
(Redis 세션을 사용하는 방법은 이미 Havi님께서 정리해주셨습니다^^; )

최근 Spring에서는 JdbcSession 사용이 1개의 어노테이션만으로 가능합니다.
아주 간단하게 적용가능한데요, 이를 OAuth2와 연계하는 과정은 코드가 조금 필요합니다.

먼저 JdbcSession을 적용하겠습니다.
관련된 의존성을 build.gradle 등록합니다.

compile('org.springframework.session:spring-session')
compile('org.springframework.boot:spring-boot-starter-jdbc')

그리고 세션의 저장을 Database에 하기 때문에 미리 세션 테이블이 생성되어있어야 합니다.
보통 ORM을 사용하면 Entity 클래스를 생성하여 사용하는데, 세션의 경우 Repository, Entity 가 모두 별도로 관리 되기 때문에 저희가 직접 Java 코드를 작성할 필요는 없습니다.
단, 세션 테이블은 미리 생성되어야만 해당 테이블에 저장/조회가 가능하겠죠?
어떤 형태로 생성되는지는 친절하게 SpringSession에서 알려주고 있으니, 이를 그대로 복사하시면 됩니다.
Command+Shift+o (InteliiJ기준으로 전체 파일 탐색)으로 schema를 검색해보시면 아래와 같이 각 DBMS에 맞춰 스키마 쿼리를 확인할 수 있습니다.

세션스키마

여기서는 개발환경 DB인 H2 스키마를 사용하겠습니다.
src/main/resources에 sehema-h2.sql을 생성하시고 아래 코드를 추가하겠습니다.

CREATE TABLE SPRING_SESSION (
	SESSION_ID CHAR(36) NOT NULL,
	CREATION_TIME BIGINT NOT NULL,
	LAST_ACCESS_TIME BIGINT NOT NULL,
	MAX_INACTIVE_INTERVAL INT NOT NULL,
	PRINCIPAL_NAME VARCHAR(100),
	CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
);

CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);

CREATE TABLE SPRING_SESSION_ATTRIBUTES (
	SESSION_ID CHAR(36) NOT NULL,
	ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
	ATTRIBUTE_BYTES LONGVARBINARY NOT NULL,
	CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME),
	CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES SPRING_SESSION(SESSION_ID) ON DELETE CASCADE
);

CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_ID);

그리고 이 스키마를 읽어가도록 application.yml을 수정하겠습니다.

spring:
  datasource:
    data: classpath:schema-h2.sql # Spring Session 테이블 스키마 적용
  jpa:
    show-sql: true # JPA로 생성되는 쿼리 확인
    hibernate:
      ddl-auto: create # 프로젝트 시작시 테이블 생성
  h2:
    console:
      enabled: true
      path: /h2-console # h2 db 웹 클라이언트 접속 url
  devtools:
    livereload:
      enabled: true # 정적파일들의 실시간 갱신


#security:
#  basic:
#    enabled: false # security 기본 인증 옵션 제거
logging:
  level:
    org.hibernate.type: trace  # JPA로 생성되는 쿼리의 파라미터 값 확인

자 이제 스키마가 적용되었으니 JdbcSession 옵션을 활성화시키겠습니다.
config 패키지 아래에 HttpSessionConfig.java를 생성하고 아래의 코드를 추가하겠습니다.

@EnableJdbcHttpSession
public class HttpSessionConfig {
}

JdbcSession 적용하는 코드는 이게 끝입니다.
@EnableJdbcHttpSession하나로 적용된 것입니다.
이렇게 적용하신후, Application.java를 다시 실행시켜보시면 로그로 잘 적용된 것을 확인할 수 있습니다.

세선적용

자 근데 여기서 세션의 기능을 확인할 수는 없습니다.
OAuth 로그인 성공시 세션을 저장하는 기능을 추가하지 않았기 때문입니다.
해당 기능을 추가해보겠습니다.
SuccessHandler를 생성하여, OAuth2 로그인시 필요한 처리를 SuccessHandler에서 처리하도록 하겠습니다.
GoogleAuthenticationSuccessHandler.java를 생성하여 아래와 같이 코드를 작성하겠습니다.

@Component
public class GoogleAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private HttpSession httpSession;
    private ObjectMapper objectMapper;

    public GoogleAuthenticationSuccessHandler(HttpSession httpSession, ObjectMapper objectMapper) {
        this.httpSession = httpSession;
        this.objectMapper = objectMapper;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        httpSession.setAttribute(SessionConstants.LOGIN_USER, getGoogleUser(authentication)); // 간단한 구글계정 정보를 세션에 저장
        response.sendRedirect("/me");
    }

    private GoogleUser getGoogleUser(Authentication authentication) { // OAuth 인증정보를 통해 GoogleUser 인스턴스 생성
        OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) authentication;
        return objectMapper.convertValue(oAuth2Authentication.getUserAuthentication().getDetails(), GoogleUser.class);
    }
}

AuthenticationSuccessHandler를 구현하도록 하여, onAuthenticationSuccess 를 오버라이딩 합니다.
이때, 구글 계정 정보는 Authentication authentication를 통해 얻을 수 있습니다.
단, Map으로 전부 되어있기 때문에 ObjectMapper를 통해 GoogleUser클래스의 인스턴스로 반환되도록 하였습니다.
성공적으로 세션에 로그인 정보가 저장되면 /me 로 리다이렉트 됩니다.

여기서 기존에 작성하지 않은 코드들을 사용하는게 보이실텐데요

  1. ObjectMapper DI
  2. 리다이렉트 되는 /me 처리
  3. GoogleUser.class

이 부분들을 하나씩 추가하겠습니다.
먼저 ObjectMapper Bean을 등록하겠습니다.
AppConfig.java를 하나 생성하겠습니다.

import com.fasterxml.jackson.databind.ObjectMapper;

@Configuration
public class AppConfig {

    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper();
    }
}

SpringBoot에선 기본적으로 jackson을 지원하고 있어 별도로 의존성은 필요없습니다.
바로 Bean으로 등록하였습니다.

두번째로는 MainController를 작성해서 리다이렉트 처리를 하겠습니다.

@RestController
public class MainController {

    private HttpSession httpSession;

    public MainController(HttpSession httpSession) {
        this.httpSession = httpSession;
    }

    @GetMapping("/me")
    public Map<String, Object> me(){
        Map<String, Object> response = new LinkedHashMap<>();
        response.put("profile", httpSession.getAttribute(SessionConstants.LOGIN_USER));
        return response;
    }
}

/me요청시 세션에 저장된 정보를 그대로 Map에 담아 JSON으로 리턴하도록 하였습니다.
마지막으로 GoogleUser.java를 생성하겠습니다.

@NoArgsConstructor
@Getter
@Setter
@JsonIgnoreProperties(ignoreUnknown = true)
public class GoogleUser implements Serializable {

    @JsonProperty("email")
    private String email;
    @JsonProperty("name")
    private String name;
    @JsonProperty("picture")
    private String picture;

}

롬복 코드가 있지만, 이해하시는데 큰 어려움은 없으실것 같습니다. ObjectMapper가 필드를 명확히 인식하기 위해 @JsonProperty로 필드명을 지정하고, 멤버변수로 지정되지 않은 필드는 무시하도록 옵션(@JsonIgnoreProperties(ignoreUnknown = true))을 추가하였습니다.

자 그럼 모든 설정이 완료되었습니다.
실제 테스트를 진행해보겠습니다.
브라우저를 열어 localhost:8080/me로 이동하겠습니다.

me403

요렇게 403 에러가 발생합니다.
정상적으로 인증필터가 적용된걸 확인했습니다.
바로 localhost:8080/login 으로 이동해보겠습니다.

me200

짠! 로그인이 성공하고 로그인한 유저의 정보를 보여주는 /me로 자동 리다이렉트 되는것까지 확인되었습니다.

그리고 세션 DB로 사용중인 H2 DB의 웹 콘솔로 접속해보겠습니다.
브라우저를 열어 localhost:8080/h2-console로 접속하셔서 아래와 같이 select 쿼리를 실행해보시면!

h2console

방금 로그인한 사용자의 세션값이 저장된 것을 확인할 수 있습니다.

여기까지가! OAuth2 + Session을 적용한 기본 설정이였습니다.
이제부터 본격적으로 처음에 논의 되었던 비지니스 문제를 다뤄보겠습니다.

감사합니다^_^

참고 2.

spring session jdbc 가이드





네이버 페이로 후원 (응원) 할 수 있어요!

(모바일에서만 지원되요 ㅠ)







IntelliJ & 안드로이드 스튜디오의 기본기를 배우고 싶다면 아래 영상을 참고해보세요!

Posted by 창천향로 창천향로

태그