반응형
반응형

일단 Spring Boot App을 Tomcat에 올려서 사용하는 분들은 잘 될것이므로, 뒤로 가기를 눌러주시길..


Embedded Server로 띄우는 경우, 아래와 같은 구문을 freemarker상단에 넣어서 taglib을 가져오겠다고하면,

url이 정확한데도 불구하고, 찾을 수 없다고 나온다.


<#assign security=JspTaglibs["http://www.springframework.org/security/tags"] >



이런 경우 기본적으로 아래와 같은 두 가지의 라이브러리를 필요로 한다.


<dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>jstl</artifactId>
      <scope>provided</scope>
</dependency>
<dependency>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-jasper</artifactId>
</dependency>



이렇게 추가를 하고, 돌려보면 jsp만 사용하는 환경에서는 잘 돌아갈 것이다.


하지만, Boot 환경에서의 Freemarker는 Spring Security의 TagLib에 대한 고려가 되어 있지 않기 때문에,

이미 설정된 부분을 불러와서 Tld를 로딩할 수 있도록 수동처리해주어야 한다.


아래와 같은 클래스를 만들어서 기존 Freemarker 설정(FreeMarkerConfigurer)을 불러와서, Tld를 직접 인식하도록 해준다.


import java.util.Arrays;
import java.util.List;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;

public class ClassPathTldsLoader  {
    
    private static final String SECURITY_TLD = "/META-INF/security.tld";
    
    final private List<String> classPathTlds;

    public ClassPathTldsLoader(String... classPathTlds) {
        super();
        if(classPathTlds.length == 0){
            this.classPathTlds = Arrays.asList(SECURITY_TLD);
        }else{
            this.classPathTlds = Arrays.asList(classPathTlds);
        }
    }

    @Autowired
    private FreeMarkerConfigurer freeMarkerConfigurer;
    
    @PostConstruct
    public void loadClassPathTlds() {
        freeMarkerConfigurer.getTaglibFactory().setClasspathTlds(classPathTlds);
    }
}


 


그 후, 아래와 같이 Bean으로 등록해주면 해결이 된다.


@Bean
@ConditionalOnMissingBean(ClassPathTldsLoader.class)
public ClassPathTldsLoader classPathTldsLoader(){
    return new ClassPathTldsLoader();
}


Spring Issue Tracker에는 2014년부터 문제가 되고 있던 사항(https://github.com/spring-projects/spring-boot/issues/907)

인데, 공식적으로 해결을 안해준다. ㅠㅠ

반응형
,
반응형

jsp는 <sec:csrfMetaTags /> 이거만 붙이면 됨..


freemarker는 <@sec.scrfMetaTags /> 이걸로 되야되는데... 안된다. tld파일에도 분명히 들어있는데.

freemarker를 참 싫어하는 듯


이럴 땐 아래처럼, 그냥 명시해준다.

어차피 View들어올때 csrf관련 객체가 같이 들어오기 때문에, 값 활용이 가능하다.


<meta name="_csrf_parameter" content="${_csrf.parameterName}" />
<meta name="_csrf_header" content="${_csrf.headerName}" />
<meta name="_csrf" content="${_csrf.token}" />



이제 form형식이 아닌, jquery로 ajax통신을 하여 데이터를 땡겨오는 경우에 한 번 더 문제가 발생한다.


http://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/ 아래 링크를 참조해보면,

jquery 초기화 구문에, 아래 구문을 넣어서 이용하라고 한다. 

$(function () {
    var token = $("meta[name='_csrf']").attr("content");
    var header = $("meta[name='_csrf_header']").attr("content");
    $(document).ajaxSend(function(e, xhr, options) {
         xhr.setRequestHeader(header, token);
    });
});



다 맞는 말이긴한데, jquery 1.4버전 이하에서나 사용이 가능하고,

이후버전을 사용하면 뜬금없이 jquery가 toLowerCase()를 찾지 못했다고 나온다.


이런 경우! 아래와 같이 토큰과 헤더가 모두 있을 경우에만 처리하도록, 예외처리 구문을 넣으면 해결이 된다.

$(function () {
    var token = $("meta[name='_csrf']").attr("content");
    var header = $("meta[name='_csrf_header']").attr("content");
    $(document).ajaxSend(function(e, xhr, options) {
        if(token && header) {
            xhr.setRequestHeader(header, token);
        }
    });
});



이후, 모든 $.ajax요청들에는 beforeSend구문에 csrf헤더 정보가 들어가게 되어,
ajax api 처리 시 csrf 토큰을 정상적으로 이용할 수 있게 된다.


반응형
,
반응형

Swagger SpringMvc가 어느 순간 SpringFox라는 이름으로 Swagger OpenAPI 2.0의 스펙을 가지고 새롭게 바뀌었다.

기존에는 Swagger ui도 github에서 다운받아서 어거지로 넣는 느낌이었지만, ui도 같이 maven에 올라가 있다.


SpringFox의 존재를 알게 된건, Legacy인 Swagger SpringMVC를 오랫만에 적용해보니, 2.0으로 업그레이드 하라는 문구가 나와서,

링크를 클릭해보니 이쪽으로 연결을 시켜주었다.


모든 것은 Spring Boot + Maven 기준임.


그럼 Migration작업을 시작해보자.


1. Maven 의존성 변경

 - 기존 (2013년을 마지막으로 더이상 버전업이 되지 않음)

<dependency>
    <groupId>com.mangofactory</groupId>
    <artifactId>swagger-springmvc</artifactId>
    <version>1.0.2</version>
</dependency>



 - 변경 (2016년 3월에 2.4버전이 나옴. 꾸준히 업데이트 중)

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.4.0</version>
    <scope>compile</scope>
</dependency>




2. Annotation이 변경됨

 - 기존 : @EnableSwagger

 - 변경 : @EnableSwagger2

 ※ 2.4 버전 기준으로 deprecated되었다는 warning이 발생하지만, 무시해도 됨.



3. 생성해야될 Bean이 달라짐.

 - 기존 : SpringSwaggerConfig를 Bean으로 불러와서, SwaggerSpringMvcPlugin을 생성하여 Bean으로 등록

          SwaggerUI에서 context-path가 두번 붙는 문제도 있어서, SwaggerPathProvider도 구현해서 넣어줘야 했었다.

          api 패턴도 정규식으로 넣어야 제대로 찾았었다.

예시 코드) 

@Autowired
private SpringSwaggerConfig springSwaggerConfig;

@Bean
public SwaggerSpringMvcPlugin swaggerSpringMvcPlugin() {
    return new SwaggerSpringMvcPlugin(this.springSwaggerConfig)
            .apiInfo(new ApiInfo("ApiInfo", null, null, null, null, null))
            .useDefaultResponseMessages(false)
            .includePatterns(".*api.*")
            .pathProvider(new SwaggerPathProvider() {               
                    @Override
                    protected String getDocumentationPath() {
                        return "/";
                    }
                    
                    @Override
                    protected String applicationPath() {
                        return "";
                    }
            });
}



 - 변경 : Docket이라는 애만 만들어서 Bean으로 등록

          api 패턴도 정규식이 아닌 누구나 알아볼 수 있도록 *.*로 바뀜 (*만 넣으면 못찾는 문제가 있음..)

예시 코드)

@Bean
public Docket api() {
    return new Docket(DocumentationType.SWAGGER_2)
            .select()
            .apis(RequestHandlerSelectors.any())
            .paths(PathSelectors.regex("/api/*.*"))             
            .build()
            .apiInfo(new ApiInfoBuilder().title("ApiInfo").build());
}




4. Swagger UI 

 - 기존 : https://github.com/swagger-api/swagger-ui 가서 git clone해다가 dist폴더명을 swagger로 바꾸어주고, src/main/resources/static 폴더에 옮겨주어야 한다. 

          접속주소 => http://ip:port/{context-path}/swagger/index.html

          favicon이 swagger의 icon으로 붙음.


 - 변경 : maven 의존성만 추가 (Spring boot가 아니라면 resource handling 설정이 필요함)

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.4.0</version>
    <scope>compile</scope>
</dependency>


          접속주소 => http://ip.port/{context-path}/swagger-ui.html

          favicon 없음 (아마 webapp의 favicon을 따라가지 않을까 생각됨)


아직 Spring 공식홈 Project목록에는 보이지 않으나, 사용하는 사람이 꽤 많아서 언젠간 생기지 않을까 싶음.

반응형
,
반응형

java webapp의 최대 단점인, java소스 수정 후 restart 해야 반영된다는 점이 spring-loaded라는 것을 사용하면 어느정도 해결이 된 듯 하다.


난 이제 무조건 Spring Boot를 사용하므로, Spring Boot위주로 설명한다.


maven을 사용하는 경우 plugin만 아래와 같이 추가를 하고, mvn spring-boot:run 으로 실행하면 된다.


<plugin>
    <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>springloaded</artifactId>
                <version>1.2.6.RELEASE</version>
            </dependency>
        </dependencies>
</plugin>



각 template 파일들은 caching이 되어 있으면, 기존 정보를 불러올 수 있으므로 아예 꺼버리도록 하자.

본인은 freemarker를 사용하므로, 아래와 같은 구문을 properties에 추가

spring.freemarker.cache=false


java 소스를 수정하고, ftl파일에서 뿌려보니 바로바로 적용이 잘 되는 것을 확인!

이제 돈내고 Jrebel 쓸 필요가 없어진 것 같다.


추가)

이클립스 또는 sts를 사용하는 경우 mvn spring-boot:run 으로 실행한 후, terminate로 shutdown을 시키면

javaw.exe 프로세스는 terminate되지만, java.exe 프로세스는 살아있어서, 강제로 kill 하기 전까지 다시 실행을 못하는 이슈가 있다.


이런 경우에 아래와 같이 configuration 설정을 추가 해준다.

(출처 : https://benlazaro.wordpress.com/2016/01/16/terminating-mvn-spring-bootrun-tomcat-from-eclipse/ )


<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>springloaded</artifactId>
                <version>1.2.6.RELEASE</version>
            </dependency>
        </dependencies>
    <configuration>
        <executable>true</executable>
        <fork>false</fork>
    </configuration>
</plugin>


반응형
,
반응형

WebSecurityConfigurerAdapter를 통해서 설정파일을 구성하는데,

csrf옵션을 임의로 disable해주지 않으면, 기본적으로 enable 설정으로 들어가게 된다.


따라서, 모든 api들이 csrf token을 필요로 하게 되고, 이게 없이 요청이 온 client에게는 405 method allow 라는 오류를 리턴한다.


api허용을 하지 않겠다는 의미면, 405보다는 403이 되어야 하지 않나라는 의문이 들어서,

csrf token이 없거나 만료된 경우 403을 주도록 수정을 해보았고, swagger-ui를 통해 운영 관련한 API를 이용해야 하는데,

swagger를 뜯어서 csrf token을 넣어주지 않는 이상 이용할 수 없겠다 싶어서, 우회하는 방법까지 찾아보게 되었다.


일단 간단한, csrf token을 검사하지 않도록 우회하는 방법부터!


WebSecurityConfigurerAdapter를 상속받아서 클래스를 하나 만들게되면, configure메소드를 구현하라고 나온다.

아래와 같이 csrf 관련 옵션에 ignore시키는 구문을 추가해준다. 

admin/ 이후의 모든 api들이 csrf token검사 없이 사용가능하게 허용되었다!


@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().ignoringAntMatchers("/admin/**");
}



그 다음으로, 잘못된 csrf token인 경우에 대한 403처리.


csrf token이 잘못된 경우 log level을 debug로 변경해서 확인해보면,

InvalidCsrfTokenException이라는 Exception을 발생시키는 것을 확인해볼 수 있다.


보통 Exception Handling은 어떤 시점에서 해야되느냐가 가장 관건인데, 

Spring Security에서는 해당 Exception을 쉽게 Handling할 수 있도록 제공을 한다.


방금 수정했던 configure 메소드에 다음과 같은 부분을 추가해보도록 하자.


@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().ignoringAntMatchers("/admin/**").and()
        .exceptionHandling().accessDeniedHandler(new CustomAccessDeniedHandler());
}



아마 CustomAccessDeniedHandler가 없다고 할텐데, 이건 해당 설정 값에서 원하는 interface를 받아서 직접 구현을 해주어야 한다. 


import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.csrf.InvalidCsrfTokenException;

public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
            AccessDeniedException accessDeniedException) throws IOException, ServletException {
        
        if(accessDeniedException instanceof InvalidCsrfTokenException) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        }
          // 추가 (서버가 재시작된 경우 csrfToken이 없어지게 되므로, 아래와 같은 Exception이 발생함
        if(accessDeniedException instanceof MissingCsrfTokenException) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        }
    }

}




이제 csrfToken이 잘못된 경우에도 403 오류를 받을 수 있도록 수정이 되었다.

반응형
,
반응형