스프링 시큐리티를 이용해서 애플리케이션의 로그인처리를 하는 예제는 많이 있다. 거기에 OTP(One Time Password) 를 함께 사용하여 보안을 조금 더 강화해보도록 하겠다. 보통 TOTP(Time based One-time Password) 라고도 하는데 사용자에게 할당된 비밀키SecretKey와 OTP 킷에서 시간을 기준으로 생성하는 검증코드(Verification Code) 를 조합하여 인증가능한지 여부를 확인하는 것이다. 이때 사용자에게 할당되는 비밀키는 일회성으로 요청에 따라 새롭게 발급하는 식으로 운영이 된다. 

보통은 issuer + username + secretKey 를 이용하여 등록가능한 QR코드로 애플리케이션에 등록하는 형식을 취하게 된다.

QR리더를 이용해서 담겨있는 내용을 읽어보면 다음과 같다:

otpauth://totp/{issuer}:{username}?secret={secretKey}&issuer={issuer}

의 형식을 취한다. 이때 생성되는 secretKey는 일회성이다. 만약 외부에 노출될 경우에는 새롭게 secretKey를 발급받고 기존에 등록하 OTP 정보를 삭제하고 다시 등록해줘야 한다.

사용자에게 비밀번호를 변경하라(?)는 부담을 주지 않아도 된다. OTP 용 비밀키만 재발급하면 된다. 응?

2단계 인증과 관련한 내용은 구글의 2단계인증 과정에 대한 설명을 살펴보시면 더욱 좋을 듯 하다.

그럼 예제를 살펴보도록 하자.

spring-security-2step-verification 프로젝트

h2databse 는 기본구성이 메모리로 실행된다.

application.yml 설정

spring:
  datasource:
    initialize: true # data.sql 을 이용한 DB 초기화 작업
  mail:
    default-encoding: UTF-8
    username: #${username}
    password: #${password}
    host: smtp.gmail.com
    port: 587
    protocol: smtp
    properties:
      mail.smtp.starttls.enable: true
      mail.smtp.auth: true
  h2: # jdbc ur: jdbc:h2:mem:testdb
    console: # http://localhost:8080/h2-console
      enabled: true

OTP SecretKey 발급

UserServiceImpl
@Override
	public User insert(User user) {
		GoogleAuthenticatorKey key = googleAuthenticator.createCredentials();
		sendTOTPRegistrationMail(user, key);
		user.setOtpSecretKey(key.getKey());

		encodePassword(user);
		return repository.save(user);
	}

	private void sendTOTPRegistrationMail(User user, GoogleAuthenticatorKey key) {
		String qrCodeUrl = GoogleAuthenticatorQRGenerator.getOtpAuthURL(ISSUER, user.getUsername(), key);
		Map<String, Object> attributes = new HashMap<>();
		attributes.put("qrCodeUrl", qrCodeUrl);

		MailMessage mailMessage = MailMessage.builder()
				.templateName(MailMessage.OTP_REGISTRATION)
				.to(new String[]{user.getUsername()})
				.subject("Honeymon TOTP Registration mail")
				.attributes(attributes)
				.build();
		mailService.send(mailMessage);
	}

위의 코드를 보면 알겠지만 사용자 엔티티를 생성하는 순간에 인증키를 생성하여 secretKey를 사용자 엔티티에 할당하고, 그와 동시에 등록된 사용자 계정으로 메일을 발송한다.

사용자 계정생성 -> OTP 비밀키 생성 및 할당 -> OTP 등록 QR코드를 내장한 메일 발송 -> 사용자: OTP 디바이스에 등록 -> 로그인시 사용자명/비밀번호/OTP 검증코드

로그인때 입력하는 검증코드는 일반적으로 6자리를 사용하며 일정시간동안만 유효성을 가진다.

그 결과는 다음과 같으며,

이 QR코드를 Google OTP 앱에서 읽어들이면 다음과 같이 추가된다.


이제 OTP 코드는 일정간격으로 갱신된다. 


갱신되는 타이밍이 맞지 않으면 인증실패가 발생한다.

정상적으로 로그인하면 다음과 같은 화면을 볼 수 있다.


정리

2단계 인증은 비밀번호와 단말기(스마트폰, OTP 킷)가 있어야 로그인을 할 수 있다. 서버쪽에서 계정정보가 유출된다면, 바로 OTP용 비밀키를 초기화하고 사용자에게 재등록하도록 안내를 하면 계정이 도용되는 사례를 쉽게 막을 수 있는 장점을 제공한다.

2단계 인증을…​ 제대로 활용할 수 있다면 꽤 쓸만할 듯 하다.

참고문헌


  1. 나그네 2018.06.29 13:15

    그런데 가상화폐 거래소는 아이디 패스워드 입력 후 페이지 이동 후 otp번호 입력하던데 이렇게 따로따로 전송하는건 어떻게 구현하는 걸까요?

    • 잘?

      로그인하고 인증하는 절차는 시스템에 따라 얼마든지 구현이 가능하죠.

      1차적으로 기본인증(사용자명+비밀번호)을 마쳤다면 1차 인증마쳤다는 정보를 세션이나 헤더 등에 저장시켜놓고 2차 인증(OTP)하는 페이지로 리다이렉트 시켜서 최종구현하면 되니까....

  2. 나그네 2018.06.30 11:04

    그렇군요 근데 spring security를 잘 모르니깐 커스터마이징하기가 쉽지는않네요ㅋㅋ 일단 연구해봐야겟네용

타임리프의 확장은 쉽다. 방언(다이얼렉트,Dialect)를 생성하고 템플릿엔진에 추가하면 된다.

Dialect

타임리프 다이얼렉트는 템플릿에서 사용할 수 있는 기능이다.

다음과 같이 동작하는 다이얼렉트를 작성해보자.

<p hello:sayto="Jake">Hi ya!</p>

모든 다이얼렉트 는 IDialect 를 구현해야한다. 이를 용이하게 할 수 있도록AbstractDialect 을 이용한다. IProcessorDialect 는 실제로 Dialect 가

HelloDialect
public class HelloDialect extends AbstractDialect implements IProcessorDialect {
    public static final String PREFIX = "hello";
    public static final int PRECEDENCE = 501;
 
    public TimezoneDateDialect() {
        super("hello-dialect");
    }
 
    @Override
    public String getPrefix() {
        return PREFIX;
    }
 
    @Override
    public int getDialectProcessorPrecedence() {
        return PRECEDENCE;
    }
 
    @Override
    public Set<IProcessor> getProcessors(String dialectPrefix) {
        Set<IProcessor> iProcessors = Sets.newHashSet()
        iProcessors.add(new SayToProcessor(dialectPrefix)); (1)
        return iProcessors;
    }
}
뭔가~ 다른 것들을 추가할 수 있을 것 같지 않은가??

그럼 이제 이 다이얼렉트가 호출하여 실체 로직처리를 수행할 프로세스를 작성해보자.

SayToProcessor
public class SayToProcessor extends AbstractStandardExpressionAttributeTagProcessor {
 
    public static final String ATTR_NAME = "sayTo";
    public static final int PRECEDENCE = 1501;
 
    public SayToProcessor(String dialectPrefix) {
        super(TemplateMode.HTML, dialectPrefix, ATTR_NAMEPRECEDENCEtrue);
    }
 
    @Override
    protected void doProcess(ITemplateContext contextIProcessableElementTag tagAttributeName attributeNameString attributeValueObject expressionResultIElementTagStructureHandler structureHandler) {
        //TODO 비즈니스로직처리!! 
        structureHandler.setBody("Hello, " + expressionResult, false);
    }
}

작성한 다이얼렉트 를 템플릿엔진에 추가하자.

@Bean
public SpringTemplateEngine templateEngine() {
    SpringTemplateEngine engine = new SpringTemplateEngine();
    engine.setTemplateResolver(templateResolver());
    engine.setMessageSource(messageSource);
    engine.addDialect(new HelloDialect());
    return engine;
}

을 구현하고 나면!! 끝이 난다. 실제로 화면을 불러와 보면

<p>HelloJake!</p>

으로 출력되는 것을 볼 수 있을 것이다.


최근 ISOMORPHIC 이란 단어가 주변에서 많이 회자되기 시작했다.


출처: http://www.flashandmath.com/mathlets/discrete/graphtheory/graph4.html


왜 Isomorphic 일까?

Isomorphic은 그리스어 '동등equals’를 의미하는 'isos’와 '형태shape’를 의미하는 'morphic’이 합쳐진 합성어다. Isomorphism은 서로 다른 컨텍스트를 가진 동일한 개체를 설명한다. 개발쪽에서 이야기하는 문맥에서는 서버server와 클라이언트client를 말한다.
개발쪽에서 말하는 아이소몰픽(Isomorphic, [àisəmɔ́ːrfik]) 은 동일한 소스코드를 가지고 서버쪽과 클라이언트쪽에서 작성하는 형태를 지칭한다고 볼 수 있다. 이 용어는 주로 수학에서 사용되었지만 최근들어 웹 프로그래밍 패턴에서 프론트엔드와 백엔드의 코드를 공유하는 곳에서 사용되고 있다.

Isomorhpic의 강점

클라이언트와 서버에서 실행되는 동일한 프레임워크

SEO에 친화적

구글봇이 크롤링을 하면서 자바스크립트를 실행하게 되었다. 이를 통해서 템플릿을 내포하고 있는 자바스크립트로 작성된 애플리케이션이 실행될 수 있게 되었다. 이는 '동적인 처리도 가능해진다’는 의미를 가지게 될 것이다.

속도

브라우저에서 직접 HTML을 렌더링 하여 속도가 빨라질 것이다. 이를 통해 보다 나은 사용자경험을 유도할 수 있다.
기존에는 서버에서 HTML을 렌더링하여 브라우저에게 전달했던 방식과 달리, 자바스크립트를 브라우저에 전달하고 브라우저에 의해서 템플릿 렌더링이 수행되어 처리되기 때문에 서버의 부하를 감소시키고 성능이 향상되고 있는 클라이언트(브라우저)에서 처리하면서 빠르게 속도가 처리될 것이다.

쉬운 코드 유지보수

동일한 코드로 작성되었기 때문에 점진적으로 코드를 개선할 수 있게 된 것이다. 흐음.


http://isomorphic.net/javascript 에서 말하는 것과 같이 서버사이드와 클라이언트 사이드에서 동작하는 동일한 코드로 작성된 애플리케이션을 지칭한다고 보면 될 것 같다. 최근에 인기를 끌고 있는 리액트(react,https://facebook.github.io/react/) 의 경우가 이에 해당한다고 보면 되겠다.

리액트를 품을 수 있는 미티오(meteor, https://www.meteor.com/)를 배우면서 아이소모픽이 가지는 강점에 대한 관심을 가지게 되었다.

Isomorphic을 구현하는데 있어 적합한 언어는 역시 최근 각광을 받고 있는 '자바스크립트javascript’가 아닐까 싶다.
자바스크립트는 정말 사용하기 쉽지만, 깊이 쓰려면 어려운 녀석이라고 생각하는데…

우쨌든!!

알고 넘어가자. Isomorhic.

스프링에서는 스프링 4.x에 들어서면서 groovy template engine, 최근 출시된 스프링 4.2에서는 script template 을 지원하고 있다. 최근 흐름은 javascript 쪽으로 넘어간 것은 분명해보인다.

참고


  1. 2016.09.05 17:30

    비밀댓글입니다

@Repository 애노테이션을 사용한 곳에 예외변환 AOP를 적용해서 JPA 예외를 스프링프레임워크가 추상화한 예외로 변환하도록 처리

/**
 * 스프링 JPA 예외변환기 적용 <code>@Repository</code> 사용한 곳에 예외변환 AOP를 적용
 *
 * @return
 */
@Bean
public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
    return new PersistenceExceptionTranslationPostProcessor();
}


JPARepository(org.springframework.data.jpa.repository.JpaRepository) 인터페이스를 구현해도 적용...

스프링부트 소개

0. 스프링부트SpringBoot란?

Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can "just run". We take an opinionated view of the Spring platform and third-party libraries so you can get started with minimum fuss. Most Spring Boot applications need very little Spring configuration.

  • 스프링부트는 단독실행되는, 실행하기만 하면 되는 상용화 가능한 수준의 스프링 기반 애플리케이션을, 쉽게 만들어낼 수 있다.
  • 최소한의 설정으로 스프링 플랫폼과 서드파티 라이브러리들을 사용할 수 있도록 하고 있다.

스프링 기반의 애플리케이션을 개발하기 쉽도록 기본설정되어 있는 설정을 기반으로 해서 빠르게 개발할 수 있도록 해주는 개발플랫폼이랄까?

0.1. 스프링부트 기능

  • Create stand-alone Spring applications

    단독실행가능한 스프링애플리케이션을 생성한다.,

  • Embed Tomcat, Jetty or Undertow directly (no need to deploy WAR files)

    내장형 톰캣, 제티 혹은 언더토우를 내장(WAR 파일로 배포할 경우에는 필요없음)

  • Provide opinionated 'starter' component to simplify your build configuration

    기본설정되어 있는 'starter' 컴포넌트들을 쉽게 추가

  • Automatically configure Spring whenever possible

    가능한 자동설정되어 있음

  • Provide production-ready features such as metrics, health checks and externalized configuration

    상용화에 필요한 통계, 상태 점검 및 외부설정을 제공

  • Absolutely no code generation and no requirement for XML configuration

    설정을 위한 XML 코드를 생성하거나 요구하지 않음


1. 스프링부트 시작하기

1.1. 스프링부트 프로젝트 생성하기

  • 주의사항

    • 네트워크가 연결되어 있어야 한다.

      그렇다면, 네트워크가 연결되지 않은 인트라넷 환경에서는 어떻게 해야할까? = 넥서스Nexus 에 스프링부트 설정을 해야겠지요? 사실, 안해봐서 모르겠음. @_@);;

    • Maven 혹은 Gradle 플러그인이 IDE에 설치되어 있어야 한다.

1.1.1. http://start.spring.io/ 에서 생성하기

1.1.1.1. 프로젝트 메타데이터를 등록

  • Maven 보다는 Gradle
    • Maven 예제가 많은 편이지만, Maven의 골, 페이즈만으로는 프로젝트의 필요한 기능을 모두 지원하지 못할 수도 있음
    • Gradle은 Groovy DSL로 구성되어 있어서 그루비를 익혀야하지만, 지원되는 기능을 익히고 나면 훨씬 강력해짐
  • 배포형태에 따라서 war 또는 jar
    • 기본적으로는 단독실행가능지만, 프로젝트 환경에 따라 배포할 수도 있으니 war도 가능

1.1.1.2. [Generate Project] 버튼클릭

  • 'artifact' 이름으로 된 zip 파일 다운로드

1.1.1.3. IDE에서 Import Project

1.1.2. STS에서 생성하기

1.1.2.1. [File]-[New]-[Spring Starter project] 선택

1.1.2.2. 사용하려는 스프링 starter 선택

  • 최초에는 필요한 라이브러리들을 다운로드 받는데 상단한 시간이 소요된다.

1.2. 스프링부트 프로젝트 실행

  • [Run as] - [Spring Boot App] 선택
  • [gradle] - [Tasks quick launcher] 을 이용해서 실행
    • 프로젝트 지정된 JDK 버전과 IDE 실행 JDK 버전이 다르면...
  • 프로젝트 생성 위치에서 $ gradle bootRun 명령 실행

2. 스프링부트 구성 살펴보기

2.1. 최초 생성된 스프링부트 프로젝트 살펴보기

2.1.1. jar 프로젝트

  • build.gradle
//코드 생략
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'
apply plugin: 'io.spring.dependency-management'
 
jar {
    baseName = 'demo'
    version = '0.0.1-SNAPSHOT'
}
//코드생략
dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    testCompile("org.springframework.boot:spring-boot-starter-test")
}
  • {artifact-name}Application.java
  • IDE에서 Java Application project로 인식
  • build 태스크 실행시 demo.jar 생성
  • 실행
$ java -jar demo.jar

2.1.2. war 프로젝트

  • build.gradle
//코드생략
apply plugin: 'java'
apply plugin: 'eclipse-wtp'
apply plugin: 'idea'
apply plugin: 'spring-boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'war'
 
war {
    baseName = 'demowar'
    version = '0.0.1-SNAPSHOT'
}
//코드생략
configurations {
    providedRuntime
}
 
dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    providedRuntime("org.springframework.boot:spring-boot-starter-tomcat")
    testCompile("org.springframework.boot:spring-boot-starter-test")
}
  • public class ServletInitializer extends SpringBootServletInitializer 클래스가 있음
  • IDE에서 웹프로젝트로 인식
  • build 태스크 실행시 demowar.war 생성
  • 실행

2.1.3. Excutable JAR

2.2. @SpringBootApplication

  • 코드
/**
 * Indicates a {@link Configuration configuration} class that declares one or more
 * {@link Bean @Bean} methods and also triggers {@link EnableAutoConfiguration
 * auto-configuration} and {@link ComponentScan component scanning}. This is a convenience
 * annotation that is equivalent to declaring {@code @Configuration},
 * {@code @EnableAutoConfiguration} and {@code @ComponentScan}.
 * 
 * @author Phillip Webb
 * @since 1.2.0
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Configuration
@EnableAutoConfiguration
@ComponentScan
public @interface SpringBootApplication {
 
    /**
     * Exclude specific auto-configuration classes such that they will never be applied.
     * @return the classes to exclude
     */
    Class<?>[] exclude() default {};
 
}
  • @SpringBootApplication 애노테이션이 선언된 애플리케이션 클래스는 패키지 루트에 위치하는 것이 좋다.
    • 물론 @ComponentScan에 별도의 패키지를 지정할 수 있지만 굳이~ 그렇게 하지 맙시다.
    • Locating the main application class 의 기본적인 구조를 따르는 걸 권장하고 싶음
      com
      +- example
           +- myproject
               +- Application.java
               |
               +- domain
               |   +- Customer.java
               |   +- CustomerRepository.java
               |
               +- service
               |   +- CustomerService.java
               |
               +- web
                   +- CustomerController.java

3. 스프링부트 설정

3.1. 스프링부트 자동설정AutoConfig

3.1.1. autoconfigure 패키지 확인

3.1.2. @Conditional, @ConditionalOnBean, @ConditionalOnMissingBean, @ConditionalOnClass 조건에 따라 스프링 빈Bean 으로 등록

  • @ConditionalOnBean
  • @ConditionalOnMissingBean
  • @ConditionalOnClass

3.2. 외부설정 하기

  • Common Application properties
  • 스프링부트의 프로퍼티스 확인순서에 따라서 외부요인들을 읽어오게 됨
  • Environment

    3.2.1. application.properties

  • PropertySource

    3.2.2. application.yml

  • YamlPropertySourceLoader

3.2.3. 프로파일즈 활용하기

  • 로컬, 개발, 테스트, 운영 설정을 각각 관리 및 적용
  • Profiles - springframework
  • 환경별 설정요소를 한곳에서 집중하여 관리할 수 있음

3.2.3.1. 기존 방식

  • 설정디렉토리를 분리
    • config/local
    • config/development
    • config/test
    • config/production
  • 빌드시 프로파일을 지정하여 지정한 설정디렉토리의 설정파일을 적용하는 형식을 취함

3.2.3.2. application.properties

  • application-{profiles}.properties
    • application-local.properties
    • application-development.properties
    • application-test.properties
    • application-production.properties

3.2.3.3. application.yaml

  • application.yaml
# 공통설정부분 지정가능
---
spring:
    profiles: local
---
spring:
  profiles: development
---
spring:
    profiles: test
---
spring:
  profiles: production

4. 스프링부트 확장하기

4.1. starter POMs 추가하기

  • Starer POMs
  • 추가시 spring.provides 에 연관된 스프링 라이브러리들이 정의되어 있다.
  • 관련 의존성 라이브러리에 대해서는 starter 프로젝트 내부에 있는 pom.xml 에 정의되어 있음

4.2. 의존성 라이브러리 추가하기

4.2.1. 의존성 버전


● 개인적 목표

  • JDK 8 이상 사용
  • Gradle 사용
  • SpringBoot 적용
    • 스프링 프레임워크 4.0 이상 적용
    • Spring Data JPA 사용


+ Recent posts