[spring security] enum으로 @Secured 권한 관리하기

Spring Security Annotations


spring security는 @Secured나 @PreAuthorize 같은 어노테이션을 사용하여 메소드에 권한을 줄 수 있다. Secured 어노테이션은 @Secured("ROLE_USER") 와 같이 권한 이름을 파라미터로 사용하는데, 이 권한 이름들을 Enum에 정의하고, 사용해보자.

아래와 같은 권한용 Enum이 있다.

import org.springframework.security.core.GrantedAuthority;

public enum MyAuthority implements GrantedAuthority{

    USER("ROLE_USER", "유저권한"),
    ADMIN("ROLE_ADMIN", "어드민권한");

    private String authority;
    private String description;

    private MyAuthority(String authority, String description){
        this.authority = authority;
        this.description = description;
    }

    @Override
    public String getAuthority() {
        return authority;
    }

    public String getDescription() {
        return description;
    }

}

그리고 나서 @Secured에서 아래와 같이 사용해보자.

@Secured(MyAuthority.USER.getAuthority())

@Secured안에는 String이 들어가고, getAuthority()는 String을 리턴하는데, "The value for annotation attribute Secured.value must be a constant expression" 라는 컴파일 에러가 났다. 어노테이션안에 Enum을 사용한 적은 많은 것 같은데, 왜 에러일까?

Enum 타입 사용이 가능했던 어노테이션들은 실제로 Enum을 받도록 만들어진 어노테이션이였다. @RequestMapping의 method를 예로들면, 타입이 String이 아닌 RequestMethod 라는 Enum을 받도록 되어있다. 만약 @Secured에서 Enum을 사용하려 했다면, String이 아닌 Enum을 받도록 설계되어 있어야 한다.

그렇다면 방법은 전혀 없는 것일까? 차선책으로는 static 레벨에 final로 권한을 선언하는 것이다. Enum 안에 ROLE_USER와 같은 권한들은 객체안에 생성되서 컴파일러가 가변적이라고 판단하는 것 같다. 그래서 코드를 아래와 같이 바꾸었다.

import org.springframework.security.core.GrantedAuthority;

public enum MyAuthority implements GrantedAuthority{

    USER(ROLES.USER, "유저권한"),
    ADMIN(ROLES.ADMIN, "어드민권한");

    public static class ROLES{
        public static final String USER = "ROLE_USER";
        public static final String ADMIN = "ROLE_ADMIN";
    }

    private String authority;
    private String description;

    private MyAuthority(String authority, String description){
        this.authority = authority;
        this.description = description;
    }

    @Override
    public String getAuthority() {
        return authority;
    }

    public String getDescription() {
        return description;
    }
}

@Secured를 사용하는 부분은 아래와 같이 바꾸면 잘 된다.

@Secured(MyAuthority.ROLES.USER)

기존의 목표였던 권한을 Enum에서 관리하면서, Secured 어노테이션에서도 그 권한을 참조 할 수 있게 되었다. String을 파라미터로 받는 어노테이션에 변수를 전달하고 싶을 때는 final로 선언되어 있어야 한다.

[spring] static에서 spring bean 가져오기

먼저 목적은 아래와 같은 코드가 작동하게 하는 것이다.

@Component
public class Foo {
    public void doA(){
        System.out.println("doA");
    }
}

public class Boo {
    @Autowired
    public static Foo foo;
}

public class ApplicationTests {
    @Test
    public void contextLoads() {
        Boo.foo.doA();
    }
}

물론 디자인 상으로는 클래스에 저장되는 static 자원에서 객체를 가리키게 하는 것은 좋지 않지만, 이런 상황에서 그나마 깔끔하게 처리하는 방법은 없을까?

문제점 :

  1. 먼저 @Autowired는 Spring에 의해서 관리되야 주입을 해주는데, Boo는 현재 그렇지 않다.
  2. 아무리 Boo에서는 클래스 레벨에서 Foo를 사용하고 싶다고 하더라도 객체는 클래스 로딩된 후에 생성되기 때문에 Boo 클래스가 로딩될 당시에 Foo 객체는 존재하지 않는다.

해결책 :

  • Boo 도 싱글턴 객체로 스프링에서 관리되게 등록해준다.
  • Boo 객체가 생성될 때 Foo 객체를 가져오고, static 필드에 넘겨준다.

먼저 첫번째 방법은 @PostContruct를 이용해서 static 필드에 값을 넘겨주는 방법이다.

@Component
public class Boo {

    public static Foo foo;

    @Autowired
    private Foo fooInstance;

    @PostConstruct
    private void init() {
        foo = fooInstance;
    }
}

두번째 방법은 생성자에 @Autowired를 사용하는 방법이다.

@Component
public class Boo {

    public static Foo foo;

    @Autowired
    private Boo(Foo foo) {
        this.foo = foo;
    }
}

생성자나 init 같은 함수들은 private으로 하더라도 Spring이 알아서 접근해서 호출해준다. 생성자에 @Autowired는 파라미터로 들어오는 bean을 주입시켜 준다.

참고 : http://stackoverflow.com/questions/17659875/autowired-and-static-method

[spring security + uaa] 로그인용 OAuth 서버 사용하기

목표

로그인 전용 서버를 따로 두고, 여러 어플리케이션에서 로그인을 처리하지 않고, 로그인 전용 서버에 역할을 위임해서 처리하고 싶다. 이 글에서는 OAuth를 사용하여 로그인 전용 서버로 사용했다.





App 이라는 웹 어플리케이션에서 로그인 서버의 인증을 사용하는 케이스의 sequence diagram 이다. 인증 방법은 authorization_code 방법이다.

App에서는 사용자의 정보를 가지고 있지 않고, 로그인 서버에서 사용자 정보를 가져온 후, 세션에 저장한다.

코드 관점에서 보면 App이 Spring Security를 사용한다면, SecurityContext에 외부에서 가져온 데이터를 채워 넣어야 한다.

방법

CloudFoundry의 User Account and Authentication Service(UAA) 에 이러한 서비스를 제공하는 라이브러리가 있다.

<dependency>
    <groupId>org.cloudfoundry.identity</groupId>
    <artifactId>cloudfoundry-identity-common</artifactId>
    <version>1.4.3</version>
</dependency>

이 라이브러리 안에는 ClientAuthenticationFilter가 있는데, UsernamePasswordAuthenticationFilter 대신 사용하면 된다.

샘플용 소스를 하나 만들어서 github에 올렸다. https://github.com/jekalmin/samples 에서 uaa 폴더 안에 프로젝트가 두개 있는데, 아래처럼 둘 다 받아서 서버에 올리면 된다.

여기서는 oauth_consumer가 위에서 설명한 App이고, oauth_provider가 Login Server이다.

서버를 실행하고, 위의 Authentication Sequence를 보면서 테스트해보자.

먼저 http://localhost:8080/oauth_consumer를 호출하면, Authentication Sequence의 1,2,3,4 번에 해당하는 작업을 수행한다. 그래서 아래 이미지와 같이 4번에 해당하는 로그인 페이지에 오게 되었다.

min / min 계정으로 로그인을 해보면 아래 이미지와 같이 권한 승인 페이지로 가게 되는데, 이 부분은 Authentication Sequence의 6번에 해당하는 권한 승인 페이지이다.

승인을 하게 되면, Authentication Sequence의 7번부터 15번까지 실행되고 아래와 같이 http://localhost:8080/oauth_consumer 에 해당하는 페이지가 출력된다.

User Account and Authentication Service(UAA)

위의 예제에서는 App 부분인 oauth_consumer에만 UAA의 라이브러리를 사용했고, 로그인 서버부분인 oauth_provider는 예전에 포스팅했던 http://jekalmin.tistory.com/entry/spring-bootoauth-%EC%84%B8%ED%8C%85-%ED%85%8C%EC%8A%A4%ED%8A%B8 글의 코드와 같다.

oauth_consumer에서 UAA의 ClientAuthenticationFilter가 Authentication Sequence의 핵심인 2,10,12,14 번에 해당하는 부분들을 담당하고 있다.

UAA에서는 클라이언트 사이드 뿐만 아니라 서버사이드도 많은 기능을 제공하는데(revoke token, RemoteTokenStore, etc..), 사실 서버쪽의 기능들이 더 핵심인 것 같다.

p.s.) ClientAuthenticationFilter에서 redirect와 /oauth/token을 요청하는 부분은 https://github.com/dsyer/uaa/blob/feature/bootify/common/src/main/java/org/cloudfoundry/identity/uaa/client/SocialClientUserDetailsSource.java 의 99번째 줄인 restTemplate에 프록시로 걸려있다.

Sample Code

https://github.com/jekalmin/samples

Reference


[spring security] SecurityContext 가져오기

SecurityContext가 생성되는 시점은 시큐리티가 생성한 필터들 중에 두 번째 쯔음 호출되는 SecurityContextPersistenceFilter 필터 안에서 HttpSessionSecurityContextRepository 클래스를 사용하여 생성한다.
그래서 그 필터를 지난 이후에는 SecurityContextHolder에서 가져올 수 있다.

SecurityContext securityContext = SecurityContextHolder.getContext();

하지만 그 필터보다 더 앞에 호출되는 필터에서는 SecurityContext가 생성되기 전이기 때문에 SecurityContextHolder에서 가져올 수 없다.
그래도 결국 SecurityContext도 세션에 저장되기 때문에, 아래와 같이 세션에서 직접 가져올 수 있다.

Object securityContextObject =  session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
if(securityContextObject != null){
    SecurityContext securityContext = (SecurityContext)securityContextObject;
}

Session에 저장되는 키는 “SPRING_SECURITY_CONTEXT”인데, 이 키는 HttpSessionSecurityContextRepository 클래스에 정의 되있다.

[spring boot] application.properties 가능한 설정들

복사 붙여넣기 하지말고 필요한 부분만 가져다가 써야한다.

# ===================================================================
# COMMON SPRING BOOT PROPERTIES
#
# This sample file is provided as a guideline. Do NOT copy it in its
# entirety to your own application.               ^^^
# ===================================================================

# ----------------------------------------
# CORE PROPERTIES
# ----------------------------------------

# SPRING CONFIG (ConfigFileApplicationListener)
spring.config.name= # config file name (default to 'application')
spring.config.location= # location of config file

# PROFILES
spring.profiles.active= # comma list of active profiles

# APPLICATION SETTINGS (SpringApplication)
spring.main.sources=
spring.main.web-environment= # detect by default
spring.main.show-banner=true
spring.main....= # see class for all properties

# LOGGING
logging.path=/var/logs
logging.file=myapp.log
logging.config= # location of config file (default classpath:logback.xml for logback)
logging.level.*= # levels for loggers, e.g. "logging.level.org.springframework=DEBUG" (TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF)

# IDENTITY (ContextIdApplicationContextInitializer)
spring.application.name=
spring.application.index=

# EMBEDDED SERVER CONFIGURATION (ServerProperties)
server.port=8080
server.address= # bind to a specific NIC
server.session-timeout= # session timeout in seconds
server.context-path= # the context path, defaults to '/'
server.servlet-path= # the servlet path, defaults to '/'
server.ssl.client-auth= # want or need
server.ssl.key-alias=
server.ssl.key-password=
server.ssl.key-store=
server.ssl.key-store-password=
server.ssl.key-store-provider=
server.ssl.key-store-type=
server.ssl.protocol=TLS
server.ssl.trust-store=
server.ssl.trust-store-password=
server.ssl.trust-store-provider=
server.ssl.trust-store-type=
server.tomcat.access-log-pattern= # log pattern of the access log
server.tomcat.access-log-enabled=false # is access logging enabled
server.tomcat.internal-proxies=10\.\d{1,3}\.\d{1,3}\.\d{1,3}|\
        192\.168\.\d{1,3}\.\d{1,3}|\
        169\.254\.\d{1,3}\.\d{1,3}|\
        127\.\d{1,3}\.\d{1,3}\.\d{1,3} # regular expression matching trusted IP addresses
server.tomcat.protocol-header=x-forwarded-proto # front end proxy forward header
server.tomcat.port-header= # front end proxy port header
server.tomcat.remote-ip-header=x-forwarded-for
server.tomcat.basedir=/tmp # base dir (usually not needed, defaults to tmp)
server.tomcat.background-processor-delay=30; # in seconds
server.tomcat.max-threads = 0 # number of threads in protocol handler
server.tomcat.uri-encoding = UTF-8 # character encoding to use for URL decoding

# SPRING MVC (HttpMapperProperties)
http.mappers.json-pretty-print=false # pretty print JSON
http.mappers.json-sort-keys=false # sort keys
spring.mvc.locale= # set fixed locale, e.g. en_UK
spring.mvc.date-format= # set fixed date format, e.g. dd/MM/yyyy
spring.mvc.message-codes-resolver-format= # PREFIX_ERROR_CODE / POSTFIX_ERROR_CODE
spring.view.prefix= # MVC view prefix
spring.view.suffix= # ... and suffix
spring.resources.cache-period= # cache timeouts in headers sent to browser
spring.resources.add-mappings=true # if default mappings should be added

# THYMELEAF (ThymeleafAutoConfiguration)
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.content-type=text/html # ;charset=<encoding> is added
spring.thymeleaf.cache=true # set to false for hot refresh

# FREEMARKER (FreeMarkerAutoConfiguration)
spring.freemarker.allowRequestOverride=false
spring.freemarker.cache=true
spring.freemarker.checkTemplateLocation=true
spring.freemarker.charSet=UTF-8
spring.freemarker.contentType=text/html
spring.freemarker.exposeRequestAttributes=false
spring.freemarker.exposeSessionAttributes=false
spring.freemarker.exposeSpringMacroHelpers=false
spring.freemarker.prefix=
spring.freemarker.requestContextAttribute=
spring.freemarker.settings.*=
spring.freemarker.suffix=.ftl
spring.freemarker.templateLoaderPath=classpath:/templates/
spring.freemarker.viewNames= # whitelist of view names that can be resolved

# GROOVY TEMPLATES (GroovyTemplateAutoConfiguration)
spring.groovy.template.cache=true
spring.groovy.template.charSet=UTF-8
spring.groovy.template.configuration.*= # See Groovy's TemplateConfiguration
spring.groovy.template.contentType=text/html
spring.groovy.template.prefix=classpath:/templates/
spring.groovy.template.suffix=.tpl
spring.groovy.template.viewNames= # whitelist of view names that can be resolved

# VELOCITY TEMPLATES (VelocityAutoConfiguration)
spring.velocity.allowRequestOverride=false
spring.velocity.cache=true
spring.velocity.checkTemplateLocation=true
spring.velocity.charSet=UTF-8
spring.velocity.contentType=text/html
spring.velocity.dateToolAttribute=
spring.velocity.exposeRequestAttributes=false
spring.velocity.exposeSessionAttributes=false
spring.velocity.exposeSpringMacroHelpers=false
spring.velocity.numberToolAttribute=
spring.velocity.prefix=
spring.velocity.properties.*=
spring.velocity.requestContextAttribute=
spring.velocity.resourceLoaderPath=classpath:/templates/
spring.velocity.suffix=.vm
spring.velocity.viewNames= # whitelist of view names that can be resolved

# INTERNATIONALIZATION (MessageSourceAutoConfiguration)
spring.messages.basename=messages
spring.messages.cacheSeconds=-1
spring.messages.encoding=UTF-8


# SECURITY (SecurityProperties)
security.user.name=user # login username
security.user.password= # login password
security.user.role=USER # role assigned to the user
security.require-ssl=false # advanced settings ...
security.enable-csrf=false
security.basic.enabled=true
security.basic.realm=Spring
security.basic.path= # /**
security.headers.xss=false
security.headers.cache=false
security.headers.frame=false
security.headers.contentType=false
security.headers.hsts=all # none / domain / all
security.sessions=stateless # always / never / if_required / stateless
security.ignored=false

# DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
spring.datasource.name= # name of the data source
spring.datasource.initialize=true # populate using data.sql
spring.datasource.schema= # a schema (DDL) script resource reference
spring.datasource.data= # a data (DML) script resource reference
spring.datasource.sqlScriptEncoding= # a charset for reading SQL scripts
spring.datasource.platform= # the platform to use in the schema resource (schema-${platform}.sql)
spring.datasource.continueOnError=false # continue even if can't be initialized
spring.datasource.separator=; # statement separator in SQL initialization scripts
spring.datasource.driverClassName= # JDBC Settings...
spring.datasource.url=
spring.datasource.username=
spring.datasource.password=
spring.datasource.max-active=100 # Advanced configuration...
spring.datasource.max-idle=8
spring.datasource.min-idle=8
spring.datasource.initial-size=10
spring.datasource.validation-query=
spring.datasource.test-on-borrow=false
spring.datasource.test-on-return=false
spring.datasource.test-while-idle=
spring.datasource.time-between-eviction-runs-millis=
spring.datasource.min-evictable-idle-time-millis=
spring.datasource.max-wait=

# MONGODB (MongoProperties)
spring.data.mongodb.host= # the db host
spring.data.mongodb.port=27017 # the connection port (defaults to 27107)
spring.data.mongodb.uri=mongodb://localhost/test # connection URL
spring.data.mongo.repositories.enabled=true # if spring data repository support is enabled

# JPA (JpaBaseConfiguration, HibernateJpaAutoConfiguration)
spring.jpa.properties.*= # properties to set on the JPA connection
spring.jpa.openInView=true
spring.jpa.show-sql=true
spring.jpa.database-platform=
spring.jpa.database=
spring.jpa.generate-ddl=false # ignored by Hibernate, might be useful for other vendors
spring.jpa.hibernate.naming-strategy= # naming classname
spring.jpa.hibernate.ddl-auto= # defaults to create-drop for embedded dbs
spring.data.jpa.repositories.enabled=true # if spring data repository support is enabled

# SOLR (SolrProperties})
spring.data.solr.host=http://127.0.0.1:8983/solr
spring.data.solr.zkHost=
spring.data.solr.repositories.enabled=true # if spring data repository support is enabled

# ELASTICSEARCH (ElasticsearchProperties})
spring.data.elasticsearch.cluster-name= # The cluster name (defaults to elasticsearch)
spring.data.elasticsearch.cluster-nodes= # The address(es) of the server node (comma-separated; if not specified starts a client node)
spring.data.elasticsearch.repositories.enabled=true # if spring data repository support is enabled



# FLYWAY (FlywayProperties)
flyway.locations=classpath:db/migrations # locations of migrations scripts
flyway.schemas= # schemas to update
flyway.initVersion= 1 # version to start migration
flyway.sql-migration-prefix=V
flyway.sql-migration-suffix=.sql
flyway.enabled=true
flyway.url= # JDBC url if you want Flyway to create its own DataSource
flyway.user= # JDBC username if you want Flyway to create its own DataSource
flyway.password= # JDBC password if you want Flyway to create its own DataSource

# LIQUIBASE (LiquibaseProperties)
liquibase.change-log=classpath:/db/changelog/db.changelog-master.yaml
liquibase.contexts= # runtime contexts to use
liquibase.default-schema= # default database schema to use
liquibase.drop-first=false
liquibase.enabled=true
liquibase.url= # specific JDBC url (if not set the default datasource is used)
liquibase.user= # user name for liquibase.url
liquibase.password= # password for liquibase.url

# JMX
spring.jmx.enabled=true # Expose MBeans from Spring

# RABBIT (RabbitProperties)
spring.rabbitmq.host= # connection host
spring.rabbitmq.port= # connection port
spring.rabbitmq.addresses= # connection addresses (e.g. myhost:9999,otherhost:1111)
spring.rabbitmq.username= # login user
spring.rabbitmq.password= # login password
spring.rabbitmq.virtualHost=
spring.rabbitmq.dynamic=

# REDIS (RedisProperties)
spring.redis.host=localhost # server host
spring.redis.password= # server password
spring.redis.port=6379 # connection port
spring.redis.pool.max-idle=8 # pool settings ...
spring.redis.pool.min-idle=0
spring.redis.pool.max-active=8
spring.redis.pool.max-wait=-1

# ACTIVEMQ (ActiveMQProperties)
spring.activemq.broker-url=tcp://localhost:61616 # connection URL
spring.activemq.user=
spring.activemq.password=
spring.activemq.in-memory=true # broker kind to create if no broker-url is specified
spring.activemq.pooled=false

# HornetQ (HornetQProperties)
spring.hornetq.mode= # connection mode (native, embedded)
spring.hornetq.host=localhost # hornetQ host (native mode)
spring.hornetq.port=5445 # hornetQ port (native mode)
spring.hornetq.embedded.enabled=true # if the embedded server is enabled (needs hornetq-jms-server.jar)
spring.hornetq.embedded.serverId= # auto-generated id of the embedded server (integer)
spring.hornetq.embedded.persistent=false # message persistence
spring.hornetq.embedded.data-directory= # location of data content (when persistence is enabled)
spring.hornetq.embedded.queues= # comma separate queues to create on startup
spring.hornetq.embedded.topics= # comma separate topics to create on startup
spring.hornetq.embedded.cluster-password= # customer password (randomly generated by default)

# JMS (JmsProperties)
spring.jms.pub-sub-domain= # false for queue (default), true for topic

# SPRING BATCH (BatchDatabaseInitializer)
spring.batch.job.names=job1,job2
spring.batch.job.enabled=true
spring.batch.initializer.enabled=true
spring.batch.schema= # batch schema to load

# AOP
spring.aop.auto=
spring.aop.proxy-target-class=

# FILE ENCODING (FileEncodingApplicationListener)
spring.mandatory-file-encoding=false

# SPRING SOCIAL (SocialWebAutoConfiguration)
spring.social.auto-connection-views=true # Set to true for default connection views or false if you provide your own

# SPRING SOCIAL FACEBOOK (FacebookAutoConfiguration)
spring.social.facebook.app-id= # your application's Facebook App ID
spring.social.facebook.app-secret= # your application's Facebook App Secret

# SPRING SOCIAL LINKEDIN (LinkedInAutoConfiguration)
spring.social.linkedin.app-id= # your application's LinkedIn App ID
spring.social.linkedin.app-secret= # your application's LinkedIn App Secret

# SPRING SOCIAL TWITTER (TwitterAutoConfiguration)
spring.social.twitter.app-id= # your application's Twitter App ID
spring.social.twitter.app-secret= # your application's Twitter App Secret

# SPRING MOBILE SITE PREFERENCE (SitePreferenceAutoConfiguration)
spring.mobile.sitepreference.enabled=true # enabled by default

# SPRING MOBILE DEVICE VIEWS (DeviceDelegatingViewResolverAutoConfiguration)
spring.mobile.devicedelegatingviewresolver.enabled=true # disabled by default
spring.mobile.devicedelegatingviewresolver.normalPrefix=
spring.mobile.devicedelegatingviewresolver.normalSuffix=
spring.mobile.devicedelegatingviewresolver.mobilePrefix=mobile/
spring.mobile.devicedelegatingviewresolver.mobileSuffix=
spring.mobile.devicedelegatingviewresolver.tabletPrefix=tablet/
spring.mobile.devicedelegatingviewresolver.tabletSuffix=

# ----------------------------------------
# ACTUATOR PROPERTIES
# ----------------------------------------

# MANAGEMENT HTTP SERVER (ManagementServerProperties)
management.port= # defaults to 'server.port'
management.address= # bind to a specific NIC
management.contextPath= # default to '/'
management.add-application-context-header= # default to true

# ENDPOINTS (AbstractEndpoint subclasses)
endpoints.autoconfig.id=autoconfig
endpoints.autoconfig.sensitive=true
endpoints.autoconfig.enabled=true
endpoints.beans.id=beans
endpoints.beans.sensitive=true
endpoints.beans.enabled=true
endpoints.configprops.id=configprops
endpoints.configprops.sensitive=true
endpoints.configprops.enabled=true
endpoints.configprops.keys-to-sanitize=password,secret
endpoints.dump.id=dump
endpoints.dump.sensitive=true
endpoints.dump.enabled=true
endpoints.env.id=env
endpoints.env.sensitive=true
endpoints.env.enabled=true
endpoints.health.id=health
endpoints.health.sensitive=false
endpoints.health.enabled=true
endpoints.info.id=info
endpoints.info.sensitive=false
endpoints.info.enabled=true
endpoints.metrics.id=metrics
endpoints.metrics.sensitive=true
endpoints.metrics.enabled=true
endpoints.shutdown.id=shutdown
endpoints.shutdown.sensitive=true
endpoints.shutdown.enabled=false
endpoints.trace.id=trace
endpoints.trace.sensitive=true
endpoints.trace.enabled=true

# MVC ONLY ENDPOINTS
endpoints.jolokia.path=jolokia
endpoints.jolokia.sensitive=true
endpoints.jolokia.enabled=true # when using Jolokia

# JMX ENDPOINT (EndpointMBeanExportProperties)
endpoints.jmx.enabled=true
endpoints.jmx.domain= # the JMX domain, defaults to 'org.springboot'
endpoints.jmx.unique-names=false
endpoints.jmx.staticNames=

# JOLOKIA (JolokiaProperties)
jolokia.config.*= # See Jolokia manual

# REMOTE SHELL
shell.auth=simple # jaas, key, simple, spring
shell.command-refresh-interval=-1
shell.command-path-patterns= # classpath*:/commands/**, classpath*:/crash/commands/**
shell.config-path-patterns= # classpath*:/crash/*
shell.disabled-plugins=false # don't expose plugins
shell.ssh.enabled= # ssh settings ...
shell.ssh.keyPath=
shell.ssh.port=
shell.telnet.enabled= # telnet settings ...
shell.telnet.port=
shell.auth.jaas.domain= # authentication settings ...
shell.auth.key.path=
shell.auth.simple.user.name=
shell.auth.simple.user.password=
shell.auth.spring.roles=

# GIT INFO
spring.git.properties= # resource ref to generated git info properties file

원본 : http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html

[spring boot + security oauth] 세팅 + 테스트

개요

먼저 OAuth의 개념에 관한 자료는 http://helloworld.naver.com/helloworld/textyle/24942http://earlybird.kr/1584 를 참고하길 바란다. 이 글에서는 개념보다는 Provider쪽 세팅 예제와 테스트를 해보겠다. 테스트는 client_credentials, password, authorization_code 인증 방식을 사용하겠다.

참고 : 예제에서 curl 을 사용하기 때문에 윈도우용 curl을 설치 하시거나 cygwin 혹은 git bash 에서 curl을 사용하셔도 된다.

예제

먼저 STS 플러그인에 있는 Spring Starter Project로 프로젝트를 생성하자.

Web과 Security를 체크하고 생성한 다음, spring-security-oauth2 라이브러리를 추가한다. 그러면 pom.xml은 다음과 같다.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.tistory.jekalmin</groupId>
    <artifactId>boot_oauth</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>boot_oauth</name>
    <description></description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.8.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.0.3.RELEASE</version>
        </dependency>
    </dependencies>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>com.tistory.jekalmin.Application</start-class>
        <java.version>1.8</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

다음 Application.java를 아래와 같이 수정한다.


Application.java

package com.tistory.jekalmin;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Configuration
@ComponentScan
@EnableAutoConfiguration
@RestController
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @RequestMapping("/")
    public String home() {
        return "Hello World";
    }

    @Configuration
    @EnableResourceServer
    protected static class ResourceServer extends ResourceServerConfigurerAdapter {

        @Override
        public void configure(HttpSecurity http) throws Exception {
            // @formatter:off
            http
                // Just for laughs, apply OAuth protection to only 2 resources
                .requestMatchers().antMatchers("/","/admin/beans").and()
                .authorizeRequests()
                .anyRequest().access("#oauth2.hasScope('read')");
            // @formatter:on
        }

        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            resources.resourceId("sparklr");
        }

    }

    @Configuration
    @EnableAuthorizationServer
    protected static class OAuth2Config extends AuthorizationServerConfigurerAdapter {

        @Autowired
        private AuthenticationManager authenticationManager;

        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.authenticationManager(authenticationManager);
        }

        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            // @formatter:off
            clients.inMemory()
                .withClient("my-trusted-client")
                    .authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
                    .authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
                    .scopes("read", "write", "trust")
                    .resourceIds("sparklr")
                    .accessTokenValiditySeconds(60)
            .and()
                .withClient("my-client-with-registered-redirect")
                    .authorizedGrantTypes("authorization_code")
                    .authorities("ROLE_CLIENT")
                    .scopes("read", "trust")
                    .resourceIds("sparklr")
                    .redirectUris("http://localhost:8080")
            .and()
                .withClient("my-client-with-secret")
                    .authorizedGrantTypes("client_credentials", "password")
                    .authorities("ROLE_CLIENT")
                    .scopes("read")
                    .resourceIds("sparklr")
                    .secret("secret");
        // @formatter:on
        }

    }

    @Configuration
    @EnableWebMvcSecurity
    protected static class SecurityConfig extends WebSecurityConfigurerAdapter{

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication().withUser("min").password("min").roles("USER");
        }

        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean()
                throws Exception {
            return super.authenticationManagerBean();
        }

    }

}

이러면 서버쪽 세팅은 끝이다. 이제 AccessToken을 받아오자.



인증 방법

  • client_credentials

    my-client-with-secret 라는 클라이언트에 client_credentials 인증방법으로 인증해보자.

      curl -u my-client-with-secret:secret http://localhost:8080/oauth/token -d "grant_type=client_credentials"
    

    결과는 다음과 같았다.

      {"access_token":"4444455c-a8a5-4988-ae61-c4b15080e433","token_type":"bearer","expires_in":43200,"scope":"read"}
    



  • password

    이번엔 my-trusted-client 클라이언트에 password 방법으로 인증해보자.

      curl -u my-trusted-client: http://localhost:8080/oauth/token -d "grant_type=password&username=min&password=min"
    

    결과는 다음과 같다.

      {"access_token":"c09bfac0-274b-4dc6-a15c-ec46d2680d44","token_type":"bearer","refresh_token":"9b5c996c-eede-4111-b3d1-aaff28e5c063","expires_in":49,"scope":"read write trust"}
    



  • authorization_code

    my-client-with-registered-redirect 클라이언트에 authorization_code 방법으로 인증하자.
    이전과는 다르게 AccessToken을 발급 받기 위해서는 권한 승인해서 코드를 가져오는 과정이 필요하다. http://localhost:8080/oauth/authorize?client_id=my-client-with-registered-redirect&response_type=code 로 가서 먼저 등록된 min / min 계정으로 로그인 하면 다음과 같은 창이 뜬다.

    둘 다 허용하고 승인하면 아래 화면으로 이동한다.

    보시는 바와 같이 http://localhost:8080/?code=h6cdAM 로 리다이렉트 된다. 이제 code를 이용해서 AccessToken을 발급 받아보자.

      curl -u my-client-with-registered-redirect: http://localhost:8080/oauth/token -d "grant_type=authorization_code&code=h6cdAM"
    

    결과는 다음과 같았다.

      {"access_token":"1ce32b00-05fa-40f2-a1f5-f6ed2719df4e","token_type":"bearer","expires_in":43199,"scope":"trust read"}
    

어떤 방법이든 인증에 성공했으면, AccessToken을 받급 받았을 것이다. 토큰을 가지고 테스트 하기에 앞서 Application.java에서 설정했던 부분을 다시 살펴보자.

@RequestMapping("/")
public String home() {
    return "Hello World";
}

접근에 성공하면 Hello World가 출력되야 한다.
이제 http://localhost:8080 에 접근해보자. 먼저 토큰을 사용하지 않고 접근해서 보호된 자원임을 확인하자.

C:\Users\Min>curl http://localhost:8080
{"error":"unauthorized","error_description":"An Authentication object was not found in the SecurityContext"}

접근이 안된다. 이제 위에서 받았던 AccessToken을 사용해서 접근해보자.

C:\Users\Min>curl -H "authorization: bearer 1ce32b00-05fa-40f2-a1f5-f6ed2719df4e" http://localhost:8080
Hello World

AccessToken을 가지고 요청할 때에만 접근이 되는 것을 확인할 수 있다.

결론

인증 방법에 따라 파라미터로 넘겨야 하는 필수 항목들이 있는데, 최소한의 파라미터로 테스트를 했다. 필수 파라미터들은 http://tutorials.jenkov.com/oauth2/authorization-code-request-response.html 에서 확인할 수 있다.

spring boot를 사용해서 spring security oauth 설정해서 간단해 보이지만, xml 설정을 해보면 TokenEndPoint, TokenStore, Filter 등 설정해야 할 부분이 많다. 그렇게 설정을 해도 결과가 좋지 않았다. 나중에 xml 기반 세팅도 완료 되는데로 포스팅 하겠다.

ps : 예제는 https://github.com/dsyer/sparklr-boot 소스에서 security 세팅을 추가했다.

참고

[spring boot] 로그 설정

간단하지만 매번 까먹어서 새로운 프로젝트 생성할 때마다 찾게 되는 스프링 부트의 로그 설정 방법이다. 물론 resources 밑에 logback.xml에 설정할 수도 있겠지만, 이 글에서는 application.properties에 설정하는 예제이다.

logging.level.org.hibernate.SQL: DEBUG
logging.level.org.springframework: DEBUG

예제와 같이 logging.level 다음에 패키지명으로 설정하면 된다.

[Spring Boot] yaml 설정파일 불러오기(@ConfigurationProperties)

개요

properties를 사용하면 map처럼 key와 value 형식으로 가능하지만, list 형태로 쓰려면 prefix로 구분해야 하는 불편함이 있다. 계층형 구조를 나타내는 설정은 yaml이 매우 편리해 보인다. Spring Boot에서 yaml을 불러올 때 제약사항이 꽤 많아서 정리해본다.

예제1:Map

먼저 src/main/resource 아래에 yml 파일을 하나 생성했다.


servers.yml

servers:
    local:
        ip: 127.0.0.1
        port: 8080
    real:
        ip: xxx.xxx.xxx.xxx
        port: 80

그 다음, yml 파일을 불러올 클래스를 생성한다.


ServerHolder.java

package com.tistory.jekalmin;

import java.util.HashMap;
import java.util.Map;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(locations="classpath:servers.yml")
public class ServerHolder {

    public Map<String, Map<String, String>> servers = new HashMap<String, Map<String, String>>();

    public Map<String, Map<String, String>> getServers() {
        return servers;
    }

}

이제 외부에서 데이터가 잘 들어갔는지 확인한다.


TestController.java

package com.tistory.jekalmin;

import javax.annotation.Resource;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/test")
public class TestController {

    @Resource
    ServerHolder serverHolder;

    @RequestMapping("/index")
    public void index(){
        System.out.println("servers : " + serverHolder.getServers());
    }
}


결과는 다음과 같다.

servers : {real={ip=xxx.xxx.xxx.xxx, port=80}, local={port=8080, ip=127.0.0.1}}



에제2:List

위와 같은 예제를 리스트 형식으로 바꿔보자.


server.yml

servers:
  - name: local
    ip: 127.0.0.1
    port: 8080
  - name: real
    ip: xxx.xxx.xxx.xxx
    port: 80


ServerHolder.java

package com.tistory.jekalmin;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(locations="classpath:servers.yml")
public class ServerHolder {

    public List<Map<String, String>> servers = new ArrayList<Map<String, String>>();

    public List<Map<String, String>> getServers() {
        return servers;
    }

}


TestController.java는 그대로 사용해서 출력했더니 결과는 아래와 같았다.

servers : [{ip=127.0.0.1, port=8080, name=local}, {name=real, ip=xxx.xxx.xxx.xxx, port=80}]

간단한 예제인데도 많은 삽질을 했는데, 그 내용들은 다음과 같았다.

주의사항:
  • yaml 언어는 공백 하나에도 민감하다. 일단 하위 계층으로 내려갈 때 tab이 아닌 스페이스바를 사용하고, 콜론(:)이나 하이픈(-) 이후에 공백 한칸도 필요하다. yaml이 민감한건지 아니면 자바에서 사용한 라이브러리인 snakeyaml이 민감한건지는 잘 모르겠다.
  • 다음은 위에 ServerHolder.java에 해당하는 @ConfigurationProperties를 사용한 클래스이다. 먼저 어노테이션 안에 locations="classpath:servers.yml" 이 되기 전에 locations="servers.yml" 로 테스트를 했었는데, 유닛테스트 할때는 잘 작동하다가 Application.java를 이용해서 접근하려 하면 파일을 찾을 수 없었다.
  • ServerHolder.java 안에 servers가 List이던 Map이던 new를 꼭 시켜줘야 한다.
  • ServerHolder.java 안에 getServer()와 같은 getter가 꼭 있어야 한다.

결론

주의사항에 나열한 내용들과 yml의 민감한 문법 때문에 오늘 하루종일 삽질한 것 같다. 이 글을 읽고 계신 분들은 조금이나마 수월하게 진행 하셨으면 하는 바람이다. 그리고 지금은 Map<Strig, String>으로 받았는데, 클래스로 받을 수도 있다.


참고 : http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-external-config-yaml

[Spring Boot] properties 등록하기 (@PropertySource)

개요

Spring Boot로 프로젝트를 실행하면 기본적으로 application.properties를 참조하여 스프링이 실행 되지만, 스프링에 관련 없는 properties는 따로 설정파일을 만들어 빼고 싶은데, xml 설정파일이 없으니 어디에 추가해야 할지 막연했다. 검색을 하다보니 어노테이션에 properties 파일을 등록하면 사용이 가능했다.

예제

먼저 config.properties라는 파일을 resource 폴더에 생성했다.


config.properties

name=jekalmin


그리고 Application.java에 어노테이션을 이용하여 등록해주었다.


Application.java

package com.tistory.jekalmin;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

@Configuration
@ComponentScan
@EnableAutoConfiguration
@PropertySource("config.properties")
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

마지막으로 컨트롤러를 생성하여 잘 가져오는지 테스트하였다.


TestController.java

package com.tistory.jekalmin;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/test")
public class TestController {

    @Value("${name}")
    String name;

    @RequestMapping("/index")
    public void index(){
        System.out.println("name : " + name);
    }
}


결과는 다음과 같았다.

name : jekalmin

config.properties가 무사히 스프링에 등록된 것을 확인할 수 있다.

커스텀 ReturnValueHandler 등록하기

개요

이전에는 컨트롤러에 들어오는 파라미터를 가공하는 방법에 대해 알아보았는데, 이번에는 반대로 리턴타입을 기준으로 분기할 수 있는 ReturnValueHandler를 구현해보려 한다. 기존에는 ModelAndViewResolver를 구현해서 AnnotationMethodHandlerAdapter에 등록을 해야 했는데, 스프링 3.2버전 부터는 deprecated 된 상태다. 지금은 ModelAndViewResolver는 남아 있기는 하나 앞으로 HandlerMethodReturnValueHandler 사용하기를 권하고 있다. 모든 HandlerMethodReturnValueHandler를 검색해봐도 리턴된 클래스를 처리 하는 핸들러를 못 찾을 경우에만 ModelAndViewResolver를 검색하기 시작한다.

예제

이전에 파라미터를 Map 형태로 수집하는 클래스를 만들었는데, 이번에는 그 안에 내용에 따라 view를 분기 시켜보려 한다. 파라미터 수집하는 예제는 (http://jekalmin.tistory.com/entry/%EC%BB%A4%EC%8A%A4%ED%85%80-ArgumentResolver-%EB%93%B1%EB%A1%9D%ED%95%98%EA%B8%B0) 이 글을 참고하길 바란다. 먼저 HandlerMethodReturnValueHandler를 구현한 클래스를 생성하고 dispatcherServlet에 등록해주자.


ParamCollectorReturnValueHandler.java

package com.tistory.jekalmin;

import org.springframework.core.MethodParameter;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;

public class ParamCollectorReturnValueHandler implements HandlerMethodReturnValueHandler {

    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        // TODO Auto-generated method stub
        return ParamCollector.class.isAssignableFrom(returnType.getParameterType());
    }

    @Override
    public void handleReturnValue(Object returnValue,
            MethodParameter returnType, ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest) throws Exception {
        // TODO Auto-generated method stub
        ParamCollector collector = (ParamCollector)returnValue;
        if(collector.get("viewType").equals("json")){
            // json View 등록
            System.out.println("json View 등록하는 곳");
        }
    }

}

다음 dispatcherServlet의 설정이다.


mvc-config.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.tistory.jekalmin"/>


    <mvc:annotation-driven>
        <mvc:argument-resolvers>
            <bean class="com.tistory.jekalmin.ParamCollectorArgumentResolver"></bean>
        </mvc:argument-resolvers>
        <mvc:return-value-handlers>
            <bean class="com.tistory.jekalmin.ParamCollectorReturnValueHandler"></bean>
        </mvc:return-value-handlers>
    </mvc:annotation-driven>

    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
            <!-- Example: a logical view name of 'showMessage' is mapped to '/WEB-INF/jsp/showMessage.jsp' -->
            <property name="prefix" value="/WEB-INF/view/"/>
            <property name="suffix" value=".jsp"/>
    </bean>

</beans>

마지막으로 TestController 이다.


TestController.java

package com.tistory.jekalmin;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/test")
public class TestController {

    protected final Logger logger = LoggerFactory.getLogger(getClass());

    @RequestMapping("/index")
    public ParamCollector index(ParamCollector collector){
        System.out.println(collector);
        return collector;
    }
}


http://localhost:8080/test/index?viewType=json 일때만 아래 글이 출력되는 것을 확인할 수 있다.

json View 등록하는 곳

결론

HandlerMethodReturnValueHandler를 구현하면 리턴타입이나 리턴된 데이터에 따라서 내가 원하는 view로 보낼 수 있다. 그 View는 흔히 사용하는 JstlView가 될 수도 있지만, AbstractView 등을 구현하여 커스터마이징 해서 사용할 수도 있다.