728x90

자바라 쓰고 Springframework를 공부한다.

Spring의 주요 3가지 컨셉은 아래와 같습니다.

1) IOC : 제어의 역전 / 의존성 주입
2) AOP : 관심의 분리
3) PSA : 일관성 있는 추상화

그럼 실제로 어떻게 사용될까?

IOC : 제어의 역전 / 의존성 주입 사용 예제

Bean은 IoC 컨테이너 안에 등록된 객체들을 의미 합니다.
모든 클래스의 객체가 Bean으로 등록되지 않습니다. @Repository, @Component, @Service, @Bean 등의 어노테이션을 통해서 Bean으로 등록 가능합니다.

사용 예로는 @Autowired 어노테이션을 이용해서 Bean을 주입할 수 있습니다.
Bean 주입이 주는 이점은, 객체의 관리를 스프링 컨테이너가 하기에 개발자가 언제 빈을 생성하고 소멸시킬지 신경쓰지 않아도 됩니다.

AOP : 관심의 분리 예제

가장 큰 예로 @Transctional을 이야기 할 수 있습니다.
connection에 대하여 rollback, commit등을 신경쓰지 않고 처리 하게 됩니다.
결국 트렌젝션 처리는 위임하고 구현 기능에 집중하는 코드를 작성하면 됩니다.

PSA : 일관성 있는 추상화

Service Abstraction으로 제공되는 기술을 다른 기술 스택으로 간편하게 바꿀 수 있는 확장성을 갖고 있는 것이 Portable Service Abstraction. 줄여서 PSA라고 합니다.
예로는 서블릿을 직접 사용하는 것이 아니라, Controller의 @GetMapping이나 @PostMapping을 통해 특정 url로 요청이 들어왔을 때, 해당 블록이 요청을 처리하도록 구현 되어 있습니다.
이렇게 추상화 계층을 사용해 어떤 기술을 내부에 숨기고 개발자에게 편의성을 제공하는 것을 Service Abstraction이라고 합니다.

스프링은 MVC라는 추상화 기법을 사용. Spring Web MVC를 사용하면 서블릿을 직접 구현할 필요가 없습니다.

참고 자료

Posted by lahuman

댓글을 달아 주세요

728x90

프로젝트 내에 여러 profile이 있을 경우 Mavne 빌드시 하나를 선택해야 합니다.

Maven 프로젝트의 Root 디렉토리에서 다음 명령어로 packaging 을 하면 TEST 코드에 대하여 profile을 적용하게 됩니다.

# dev profile을 사용하여 packaging 진행
./mvnw clean package -Dspring.profiles.active=dev

이후 결과 jar 파일을 실행시 profile을 설정하는 것도 동일 합니다.

# dev profile을 사용하여 packaging 진행
java -jar -Dspring.profiles.active=dev result.jar 
Posted by lahuman

댓글을 달아 주세요

728x90

회사내에서 배치 작업을 잠시 하게 되었습니다. 오랜만에 자바를 사용해보는거라 재미있네요. 스프링 설정은 이젠 javaconfig 만으로 웬만한 설정은 다 할수 있습니다.

추가 정보 : 스프링 배치에 대한 문서는 토리맘님의 한글 라이즈 프로젝트에 한글로 번역된 문서가 있습니다.

spring initializr를 활용한 프로젝트 생성

SpringBatch의 초기 프로젝트를 spring initializr에서 생성 합니다.

제가 준 옵션은 아래와 같습니다.

Gradle을 사용하고, Jdk는 1.8을 지정하였습니다.
Mysql은 Spring Batch의 Meta데이터를 저장하는 용도이며, H2 DB는 개발용으로, Oracle는 배치 데이터의 조회 & 적재용으로 사용하게 됩니다.
개발툴은 intellij 기준으로 진행합니다.

초기 환경 구성 진행

  1. spring initializr 에서 프로젝트 정보를 입력하고 생성 & 다운로드합니다.

  2. 다운로드한 파일의 압축을 풉니다.

  3. intellij에서 open 을 선택하고 압축 푼 디렉토리를 선택합니다.

  4. Gradle에서 관련 모듈 다운로드가 완료되기를 기다립니다.

Spring Batch 설정

기본적인 Spring Batch에 대한 내용은 Spring Batch Introduction을 읽어보시기를 권장합니다.

Spring Batch는 Job, Step, 사용자가 개발하는 처리 유닛으로 나누어져 있습니다. 이렇게 구현함으로 다음과 같은 장점을 얻을 수 있습니다.

  • 명확한 관심사 분리
  • 인터페이스로 제공하는 명확한 아키텍처 레이어와 서비스
  • 빠르게 적용하고 쉽게 응용할 수 있는 간단한 디폴트 구현체
  • 크게 향상된 확장성

Creating a Batch Service를 기준으로 코드를 작성 할 예정입니다.

src/main/resources/sample-data.cvs에 아래 내용을 작성하고 저장합니다.

Jill,Doe
Joe,Doe
Justin,Doe
Jane,Doe
John,Doe

데이터가 적재될 table을 생성하는 스크립트를 src/main/resources/schema-all.sql에 아래 내용을 작성하고 저장합니다.

DROP TABLE people IF EXISTS;

CREATE TABLE people  (
    person_id BIGINT IDENTITY NOT NULL PRIMARY KEY,
    first_name VARCHAR(20),
    last_name VARCHAR(20)
);

Spring Boot는 schema-@@platform@@.sql 파일을 시작시 실행합니다. -all은 모든 플랫폼을 의미합니다.

저장 데이터 구조가 되는 Person.java를 생성합니다.

package com.gsshop.batch.work;

public class Person {

    private String lastName;
    private String firstName;

    public Person() {
    }

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    @Override
    public String toString() {
        return "firstName: " + firstName + ", lastName: " + lastName;
    }

}

데이터를 받아서 대문자로 처리 하는 PersonItemProcessor을 생성합니다.

package com.gsshop.batch.work;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.batch.item.ItemProcessor;

public class PersonItemProcessor implements ItemProcessor<Person, Person> {

    private static final Logger log = LoggerFactory.getLogger(PersonItemProcessor.class);

    @Override
    public Person process(final Person person) throws Exception {
        final String firstName = person.getFirstName().toUpperCase();
        final String lastName = person.getLastName().toUpperCase();

        final Person transformedPerson = new Person(firstName, lastName);

        log.info("Converting (" + person + ") into (" + transformedPerson + ")");

        return transformedPerson;
    }

}

작업에 대한 처리가 완료 되었을대 확인이 가능한 리스너를 제작합니다.
해당 리스너의 역할은 단순히 작업이 완료 되었을 경우 저장된 데이터를 출력하게 됩니다.

package com.gsshop.batch.work;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.listener.JobExecutionListenerSupport;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

@Component
public class JobCompletionNotificationListener extends JobExecutionListenerSupport {

    private static final Logger log = LoggerFactory.getLogger(JobCompletionNotificationListener.class);

    private final JdbcTemplate jdbcTemplate;

    @Autowired
    public JobCompletionNotificationListener(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
        if(jobExecution.getStatus() == BatchStatus.COMPLETED) {
            log.info("!!! JOB FINISHED! Time to verify the results");

            jdbcTemplate.query("SELECT first_name, last_name FROM people",
                    (rs, row) -> new Person(
                            rs.getString(1),
                            rs.getString(2))
            ).forEach(person -> log.info("Found <" + person + "> in the database."));
        }
    }
}

Batch의 시작점인 BatchConfiguration을 작성합니다. 해당 내용에는 reader, processor, writer, 조립 설정 등이 포함 됩니다.

package com.gsshop.batch.work;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider;
import org.springframework.batch.item.database.JdbcBatchItemWriter;
import org.springframework.batch.item.database.builder.JdbcBatchItemWriterBuilder;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

import javax.sql.DataSource;

@Configuration
@EnableBatchProcessing
public class BatchConfiguration {
    @Autowired
    public JobBuilderFactory jobBuilderFactory;
    @Autowired
    public StepBuilderFactory stepBuilderFactory;

    @Bean
    public FlatFileItemReader<Person> reader() {
        return new FlatFileItemReaderBuilder<Person>()
                .name("personItemReader")
                .resource(new ClassPathResource("sample-data.csv"))
                .delimited()
                .names(new String[]{"firstName", "lastName"})
                .fieldSetMapper(new BeanWrapperFieldSetMapper<Person>() {{
                    setTargetType(Person.class);
                }})
                .build();
    }

    @Bean
    public PersonItemProcessor processor() {
        return new PersonItemProcessor();
    }

    @Bean
    public JdbcBatchItemWriter<Person> writer(DataSource dataSource) {
        return new JdbcBatchItemWriterBuilder<Person>()
                .itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>())
                .sql("INSERT INTO people (first_name, last_name) VALUES (:firstName, :lastName)")
                .dataSource(dataSource)
                .build();
    }

    @Bean
    public Job importUserJob(JobCompletionNotificationListener listener, Step step1) {
        return jobBuilderFactory.get("importUserJob")
                .incrementer(new RunIdIncrementer())
                .listener(listener)
                .flow(step1)
                .end()
                .build();
    }

    @Bean
    public Step step1(JdbcBatchItemWriter<Person> writer) {
        return stepBuilderFactory.get("step1")
                .<Person, Person> chunk(10)
                .reader(reader())
                .processor(processor())
                .writer(writer)
                .build();
    }
}

여기까지 설정하고 실행하면 잘 동작하는 것을 확인 할 수 있습니다.

Multi Datasoruce 설정

Multi datasource는 springboot의 multi Datasource를 사용하는 것과 동일합니다.

Datasource 생성을 하는 설정파일을 생성합니다.

package com.gsshop.batch.config;

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.autoconfigure.batch.BatchDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.transaction.ChainedTransactionManager;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

@Configuration
public class DatabaseConfig {
    @Bean
    @Primary
    @ConfigurationProperties("spring.datasource.default")
    DataSource springBatchDb(){
        DataSourceBuilder builder = DataSourceBuilder.create();
        builder.type(HikariDataSource.class);
        return builder.build();
    }

    @Bean
    @BatchDataSource
    @ConfigurationProperties("spring.datasource.work")
    DataSource workDb(){
        DataSourceBuilder builder = DataSourceBuilder.create();
        builder.type(HikariDataSource.class);
        return builder.build();
    }

    // Transaction Setting

    @Bean
    PlatformTransactionManager springBatchTxManager() {
        return new DataSourceTransactionManager(springBatchDb());
    }

    @Bean
    PlatformTransactionManager workTxManager() {
        return new DataSourceTransactionManager(workDb());
    }

    @Bean
    PlatformTransactionManager chainTxManager() {
        ChainedTransactionManager txManager = new ChainedTransactionManager(springBatchTxManager(), workTxManager());
        return txManager;
    }

}
  • @Primary는 기본으로 DatasSource를 이용할 경우 자동으로 주입되는 DataSource 입니다.
  • @BatchDataSource 는 SpringBatch의 Meta정보를 저장할 때 사용하는 DataSsource 입니다.

특정 DataSource 사용할 경우 @Qualifier를 이용합니다. 다음 예제를 확인하셔요.

@Bean
public JdbcBatchItemWriter<Person> writer(@Qualifier("workDb") DataSource dataSource) {
    return new JdbcBatchItemWriterBuilder<Person>()
            .itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>())
            .sql("INSERT INTO people (first_name, last_name) VALUES (:firstName, :lastName)")
            .dataSource(dataSource)
            .build();
}

전체 코드 확인하기

참고자료

Posted by lahuman

댓글을 달아 주세요

728x90

SpringBoot에서 Websocket 사용하기

Websocket 이란?

서버와 클라이언트 사이에 양방향 통신 채널을 구축할 수 있는 통신 프로토콜이다. 동작 방식은 먼저 HTTP 통신을 연결하고 이후 Upgrade 헤더를 보내 양방향 연결로 업그레이드한다. Websocket은 최신 브라우저에서는 대부분 지원한다.

전체 소스는 참고 내역에 있는 소스를 확인하면 된다.

주요 설정은 다음과 같다.

1. WebSocket Configuration

package com.example.websocketdemo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.*;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setApplicationDestinationPrefixes("/app");
        registry.enableSimpleBroker("/topic");
    }
}

@EnableWebSocketMessageBroker 은 websocket 서버를 사용한다는 설정이다. 또한 WebSocketMessageBrokerConfigure를 상속 받아 몇몇 메소드를 구현하여 websocket 연결 속성을 설정한다. registerStompEndpoints를 이용하여 클라이언트에서 websocket에 접속하는 endpoint를 등록한다. withSockJS()를 이용시에는 브라우져에서 websocket을 지원하지 않을 경우 fallback 옵션을 활성화 하는데 사용됩니다.

메소드 이름에 STOMP(Simple Text Oriented Messaging Protocol)라는 단어가 있다. 이는 스프링프레임워크의 STOMP 구현체를 사용한다는 의미다. STOMP가 필요 한 이유는 websocket은 통신 프로토콜이지 특정 주제에 가입한 사용자에게 메시지를 전송하는 기능을 제공하지 않는다. 이를 쉽게 사용하기 위해 STOMP를 사용한다.

두변째 메소드configureMessageBroker는 한 클라이언트에서 다른 클라이언트로 메시지를 라우팅 할 때 사용하는 브로커를 구성한다. 첫번째 라인에서 정의된 /app로 시작하는 메시지만 메시지 헨들러로 라우팅한다고 정의한다. 두번째 라인에서 정의된 /topic로 시작하는 주제를 가진 메시지를 핸들러로 라우팅하여 해당 주제에 가입한 모든 클라이언트에게 메시지를 방송한다.

2. ChatController

package com.example.websocketdemo.controller;

import com.example.websocketdemo.model.ChatMessage;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.stereotype.Controller;

@Controller
public class ChatController {

    @MessageMapping("/chat.sendMessage")
    @SendTo("/topic/public")
    public ChatMessage sendMessage(@Payload ChatMessage chatMessage) {
        return chatMessage;
    }

    @MessageMapping("/chat.addUser")
    @SendTo("/topic/public")
    public ChatMessage addUser(@Payload ChatMessage chatMessage, 
                               SimpMessageHeaderAccessor headerAccessor) {
        // Add username in web socket session
        headerAccessor.getSessionAttributes().put("username", chatMessage.getSender());
        return chatMessage;
    }

}

@MessageMapping는 클라이언트에서 보내는 메시지를 매핑한다. 호출 되는 주소는 /app/chart.addUer/app/chat.sendMessage가 된다.

3. main.js

javascript 에서 실제 사용은 다음 같이 사용한다.

function connect(event) {
    username = document.querySelector('#name').value.trim();

    if(username) {
        usernamePage.classList.add('hidden');
        chatPage.classList.remove('hidden');

        var socket = new SockJS('/ws');
        stompClient = Stomp.over(socket);

        stompClient.connect({}, onConnected, onError);
    }
    event.preventDefault();
}


function onConnected() {
    // Subscribe to the Public Topic
    stompClient.subscribe('/topic/public', onMessageReceived);

    // Tell your username to the server
    stompClient.send("/app/chat.addUser",
        {},
        JSON.stringify({sender: username, type: 'JOIN'})
    )
    connectingElement.classList.add('hidden');
}

.... 중략

connect를 통해 클라이언트는 websocket을 연결 합니다. 연결에 성공하면 /topic/public 주제에 가입하여 메시지를 주고 받습니다.

참고 내역


Posted by lahuman

댓글을 달아 주세요

728x90

Spring Controller에 대하여 표 형식의 문서를 만들어 주는 프로그램입니다.


Source 바로가기


기본 사항 

JRE 1.8 이상이 설치되어 있어야 합니다.

실행은 junit으로 GeneratorSpringControllerDoc.java 을 실행합니다.

docx 형식의 샘플 템플릿은 resource 에 있습니다.


주요 기능

* Spring Controller 에 연결 정보를 표 형식으로 표출


샘플 출력 결과

Posted by lahuman

댓글을 달아 주세요

728x90

Spring MVC 에서 ResponsBody로 String 을 전달시 한글 깨짐 현상 해결

Controller에서 단순한 문자열(String)을 ResponseBody로 전달 할 경우, 깨지는 현상이 발생할수 있습니다. 코드는 다음과 같습니다.

@RequestMapping(value="/preview/{id}", method=RequestMethod.GET)
public @ResponseBody String getContent(@PathVariable("id") long id) {
    return service.getContent(id);
}

한글이깨지는 원인은 브라우져에서 해당 요청에 대한 응답의 헤더 값을 보면 다음과 같이 표현 되어 있습니다.

Content-Type:application/json;charset=ISO-8859-1

위의 문제를 해결 하기 위해서는 다음과 같이 spring servlet xml 설정을 추가 해야 합니다.

<mvc:annotation-driven>
    <mvc:message-converters>
        <!-- @ResponseBody Content-Type:application/json;charset=UTF-8  -->
        <bean class="org.springframework.http.converter.StringHttpMessageConverter">
            <property name="supportedMediaTypes">
                <list>
                    <value>text/html;charset=UTF-8</value>
                </list>
            </property>
        </bean>
    </mvc:message-converters>
</mvc:annotation-driven>

위와 같이 설정을 한 후 응답의 해더 값을 확인 하면 다음과 같습니다.

Content-Type:application/json;charset=UTF-8

이후 Content 값을 확인하면, 한글이 깨지지 않고 제대로 표출 되는 것을 확인 할 수 있습니다.

Posted by lahuman

댓글을 달아 주세요

728x90

XSS를 네이버에서 만든 Lucy의 servlet-filter 를 이용하여 지금까지 쉽게 처리 해 왔습니다.


XSS 관련해서 POST로 처리 하는 부분이 제대로 동작 하지 않는 것을 확인하여 체크해본 결과 Lucy는 RequestParameter관련한 지원만 합니다.


따라서 Spring에서 @RequestBody를 이용한 부분은 처리할 수 없습니다.


이에 구글링으로 검색해본 결과 MessageConverter를 이용하여 처리 하는 방법을 확인 하였습니다.


처리 방법은 spring 4.2.5 기준으로 다음과 같습니다.


먼저 다음과 같은 ObjectMapper를 가진 FactoryBean을 생성 합니다.


package kr.pe.lahuman;

import org.apache.commons.lang3.StringEscapeUtils;

import org.springframework.beans.factory.FactoryBean;


import com.fasterxml.jackson.core.SerializableString;

import com.fasterxml.jackson.core.io.CharacterEscapes;

import com.fasterxml.jackson.core.io.SerializedString;

import com.fasterxml.jackson.databind.DeserializationFeature;

import com.fasterxml.jackson.databind.ObjectMapper;


public class HtmlEscapingObjectMapperFactory implements FactoryBean<ObjectMapper> {


    private final ObjectMapper objectMapper;


public HtmlEscapingObjectMapperFactory() {

        objectMapper = new ObjectMapper();

        objectMapper.getFactory().setCharacterEscapes(new HTMLCharacterEscapes());

        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

    }


    @Override

    public ObjectMapper getObject() throws Exception {

        return objectMapper;

    }


    @Override

    public Class<?> getObjectType() {

        return ObjectMapper.class;

    }


    @Override

    public boolean isSingleton() {

        return true;

    }


    public static class HTMLCharacterEscapes extends CharacterEscapes {


        private final int[] asciiEscapes;


        public HTMLCharacterEscapes() {

            // start with set of characters known to require escaping (double-quote, backslash etc)

            asciiEscapes = CharacterEscapes.standardAsciiEscapesForJSON();

            // and force escaping of a few others:

            asciiEscapes['<'] = CharacterEscapes.ESCAPE_CUSTOM;

            asciiEscapes['>'] = CharacterEscapes.ESCAPE_CUSTOM;

            asciiEscapes['&'] = CharacterEscapes.ESCAPE_CUSTOM;

            asciiEscapes['"'] = CharacterEscapes.ESCAPE_CUSTOM;

            asciiEscapes['\''] = CharacterEscapes.ESCAPE_CUSTOM;

        }



        @Override

        public int[] getEscapeCodesForAscii() {

            return asciiEscapes;

        }


        // and this for others; we don't need anything special here

        @Override

        public SerializableString getEscapeSequence(int ch) {

            return new SerializedString(StringEscapeUtils.escapeHtml4(Character.toString((char) ch)));

        }

    }

}



그리고 spring-servlet.xml 설정에 다음과 같이 처리 합니다.

<bean id="htmlEscapingObjectMapper" class="kr.pe.lahuman.HtmlEscapingObjectMapperFactory" />


<mvc:annotation-driven>

   <mvc:message-converters>

       <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" >

        <property name="objectMapper" ref="htmlEscapingObjectMapper"></property>

       </bean>

   </mvc:message-converters>

</mvc:annotation-driven>




출처 : http://stackoverflow.com/questions/25403676/initbinder-with-requestbody-escaping-xss-in-spring-3-2-4



방법 2: RequestWrapper를 이용하여 처리하는 방법


request의 값을 변조할 경우 많이 사용 하는 방법으로 Request를 Wrapping 하여 사용자가 원하는 데이터를 가공 하는 방식이다.


처리 코드는 다음과 같다.


먼저 Request를 변환 하기 위한 Filter를 생성 한다.

package kr.pe.lahuman;


import java.io.IOException;


import javax.servlet.Filter;

import javax.servlet.FilterChain;

import javax.servlet.FilterConfig;

import javax.servlet.ServletException;

import javax.servlet.ServletRequest;

import javax.servlet.ServletResponse;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;


public class RequestBodyXSSFIleter implements Filter {

@Override

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)

throws IOException, ServletException {

HttpServletRequest request = (HttpServletRequest)req;

  HttpServletResponse response = (HttpServletResponse)res;

  RequestWrapper requestWrapper = null;

  try{

  requestWrapper = new RequestWrapper(request);

  }catch(Exception e){

  e.printStackTrace();

  }

  chain.doFilter(requestWrapper, response);

}

@Override

public void init(FilterConfig filterConfig) throws ServletException {}

@Override

public void destroy() {}

}



RequestWrapper를 생성한다.

package kr.pe.lahuman;


import java.io.BufferedReader;

import java.io.ByteArrayInputStream;

import java.io.IOException;

import java.io.InputStream;

import java.io.InputStreamReader;


import javax.servlet.ServletInputStream;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletRequestWrapper;


import org.apache.commons.io.IOUtils;


import com.nhncorp.lucy.security.xss.XssFilter;


public class RequestWrapper extends HttpServletRequestWrapper {

private byte[] b;

public RequestWrapper(HttpServletRequest request) throws IOException {

super(request);

  XssFilter filter = XssFilter.getInstance("lucy-xss-sax.xml");

  b = new String(filter.doFilter(getBody(request))).getBytes();

}

public ServletInputStream getInputStream() throws IOException {

  final ByteArrayInputStream bis = new ByteArrayInputStream(b);

 

  return new ServletInputStreamImpl(bis);

  }

 

  class ServletInputStreamImpl extends ServletInputStream{

  private InputStream is;

 

  public ServletInputStreamImpl(InputStream bis){

  is = bis;

  }

 

  public int read() throws IOException {

  return is.read();

  }

 

  public int read(byte[] b) throws IOException {

  return is.read(b);

  }

  }


 

  public static String getBody(HttpServletRequest request) throws IOException {


     String body = null;

     StringBuilder stringBuilder = new StringBuilder();

     BufferedReader bufferedReader = null;


     try {

         InputStream inputStream = request.getInputStream();

         if (inputStream != null) {

             bufferedReader = new BufferedReader(new InputStreamReader(inputStream));

             char[] charBuffer = new char[128];

             int bytesRead = -1;

             while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {

                 stringBuilder.append(charBuffer, 0, bytesRead);

             }

         } else {

             stringBuilder.append("");

         }

     } catch (IOException ex) {

         throw ex;

     } finally {

         if (bufferedReader != null) {

             try {

                 bufferedReader.close();

             } catch (IOException ex) {

                 throw ex;

             }

         }

     }


     body = stringBuilder.toString();

     return body;

  }

}


마지막으로 web.xml에 해당 filter를 추가 한다.

  <filter>

  <filter-name>RequestBodyXSSFilter</filter-name>

  <filter-class>kr.pe.lahuman.RequestBodyXSSFIleter</filter-class>

  </filter>

    <filter-mapping>

    <filter-name>RequestBodyXSSFilter</filter-name>

    <url-pattern>/*</url-pattern>

  </filter-mapping>


출처 : http://shonm.tistory.com/549

Posted by lahuman

댓글을 달아 주세요

  1. 도움맨 2016.10.24 17:26  댓글주소  수정/삭제  댓글쓰기

    밑에 BodyWrapper 로 하면 @RequestBody는 잘 필터링이 되기 시작하나,
    일반적인 application/x-www-form-urlencoded 등의 파라미터를 스프링이 @ModelAttribute 로 파싱하지 못하는 현상이 생기는데요..
    혹시 주인장님께선 이런증상이 없으신지요.

  2. 2017.12.14 17:55  댓글주소  수정/삭제  댓글쓰기

    contentType json 인 경우만 되게 하면 되지 않을까요?

728x90

Aspect 를 이용한 공통 BindingResult 처리 방법

validation 을 다음과 같이 처리를 한다.

VO에서 Vaildation 관련 설정
@Data
@JsonInclude(Include.NON_NULL)
public class RolesVO {
    @NotNull(groups={ValidationGroup.Update.class})
    private Long roleSeq;
    @NotBlank(groups={ValidationGroup.Insert.class, ValidationGroup.Update.class})
    private String roleId;
    @NotBlank(groups={ValidationGroup.Insert.class, ValidationGroup.Update.class})
    private String roleName;
    private String roleComment;
    private String regUserId;
    private Date regDate;
    private String modUserId;
    private Date modDate;
}
Controller 에서 Vaild 처리
    @RequestMapping(method = RequestMethod.POST)
    public ResponseEntity add(@RequestBody @Validated(ValidationGroup.Insert.class) RolesVO vo, BindingResult result) throws Exception {
        /* validation */
         if(result.hasErrors()){
            ErrorResponse errorResponse = new ErrorResponse();
            errorResponse.setMessage("Wrong request!");
            errorResponse.setCode("bad.request");
            return new ResponseEntity(errorResponse, HttpStatus.BAD_REQUEST);
        }
        /* validation */
        return new ResponseEntity(modelMapper.map(service.addtRoles(vo), RolesVO.class), HttpStatus.CREATED);
    }

/ validation / 사이에 보이는 값은 대부분 중복 처리가 된다.
이 경우 중복된 코드가 너무 많이 반복되어 다음과 같이 처리 하였다.

Aspect를 이용한 공통 validat 처리

ValidCheckingInterceptor

@Aspect
@Component
public class ValidCheckingInterceptor {

    @Around("execution(* kr.pe.lahuman.*.controller.*.add*(..)) or execution(* kr.pe.lahuman.*.controller.*.modify*(..))")
    public Object anyMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] objs = joinPoint.getArgs();
        for(Object obj : objs){
            if(obj instanceof BindingResult){
                BindingResult result = (BindingResult)obj;
                if(result.hasErrors()){
                    Map<String, Map<String, String>> userdata = new HashMap<String, Map<String,String>>();
                    Map<String, String> errors = new LinkedHashMap<String, String>();
                    for (FieldError error : result.getFieldErrors()) {
                        errors.put(error.getField(), error.getDefaultMessage());
                    }
                    userdata.put("userdata", errors);
                    return new ResponseEntity(userdata, HttpStatus.BAD_REQUEST);
                }                 
            }
        }
        return joinPoint.proceed(joinPoint.getArgs());
    }
}

Around 어노테이션에서 add와 modify로 시작되는 메소드 중 인자로 BindingResult 가 있을 경우 유효성 체크를 하여 에러가 있을 경우 문제 필드와 기본 메시지를 Map 형식으로 return 한다.

변경후 Controller에서 Vaild 제거 된 소스

    @RequestMapping(method = RequestMethod.POST)
    public ResponseEntity add(@RequestBody @Validated(ValidationGroup.Insert.class) RolesVO vo, BindingResult result) throws Exception {
        return new ResponseEntity(modelMapper.map(service.addtRoles(vo), RolesVO.class), HttpStatus.CREATED);
    }

반복해서 작성 되었던 유효성 처리가 제거 되었다. 이후 Vaild 관련 처리는 ValidCheckingInterceptor에서만 진행 된다.


MD FILE : 

Aspect_BindingResult.md



Posted by lahuman

댓글을 달아 주세요

728x90

[TIP]SpringBoot 포트 및 ERROR 페이지 관련

원본 문서

1. HTTP Port 변경 방법

SpringBoot를 이용해서 서비스를 구축 하는 경우, PORT를 변경하고 싶을때 다음과 같이 하시면 됩니다.

  • application.properties 파일에서 server.port=8888 를 추가
  • Main(SpringBootApplication) Run 시 VM options 에 -Dserver.port=8888 를 추가

원본 문서에서는 management.port를 사용하면 된다고 하지만, 테스트 결과 server.port 만 동작하였습니다.

2. 사용자 정의 ‘whitelable’ 에러 페이지를 생성 하는 방법

SpringBoot는 서버에서 에러(클라이언트에서 JSON 형식이나 다른 미디어 타입으로 요청했을 경우 해당 타입에 맞는 올바른 에러 코드)가 발생할 경우 사용자에게 whitelabe 에러 페이지를 제공한다.
또한 server.error.whitelabel.enabled=false를 application.properties에 추가 하면 해당 에러 페이지를 사용하지 않을 수도 있다.
그러나 보통의 경우 사용자 정의 whilelabel 에러 페이지를 추가 하거나 대체 합니다. 정확하게 사용하는 템플릿 기술에 따라 달라집니다.
예를 들면 만약 Thymeleaf를 사용한다면 error.html 템플릿을 FreeMarker를 사용한다면 error.ftl 템플릿을 추가 합니다.
일반적으로 Viewerror이름으로 정의된 것과 @Controller에서 /error 주소가 필요합니다.
어떤 기본 구성은 당신의 ApplicationContext안에 BeanNameViewResolver를 찾아서 error id를 가진 @Bean 을 변경하는 하여야 됩니다.
더 많은 옵션은 ErrorMvcAutoConfiguration을 확인하셔요.

또한 서블릿 컨테이너에서 어떻게 Error Handling을 등록 하는 지를 보세요.

간단한 prototype을 작성할 경우는 기본 에러 페이지를 사용하는 것을 추천 드립니다.

참고 자료

- 포트 변경 관련 참고


MD FILE :

TIP_SpringBoot_port_error_change.md



Posted by lahuman

댓글을 달아 주세요

728x90

참고 URL : http://docs.spring.io/spring-boot/docs/current/reference/html/howto-hotswapping.html

72.6.2. Spring Loaded를 Gradle와 IntelliJ에서 설정하기

몇가지 단계를 지나면 Spring Loaded를 Gradle와 IntelliJ 에 결합하여 사용 하고 싶을 것이다. 기본적으로 Spring Loaded가 바라보는 classes의 컴파일되는 위치가 IntelliJ에서 Gradle일 경우 달라서 실패 할 것이다.

IntellJ에서 idea를 사용하여 Gradle plugin을 정확하게 설정 할 수 있다.

buildscript {
    repositories { jcenter() }
    dependencies {
        classpath "org.springframework.boot:spring-boot-gradle-plugin:1.2.7.RELEASE"
        classpath 'org.springframework:springloaded:1.2.0.RELEASE'
    }
}

apply plugin: 'idea'

idea {
    module {
        inheritOutputDirs = false
        outputDir = file("$buildDir/classes/main/")
    }
}

// ...

IntelliJ 는 Gradle task의 명령에서 동일한 Java 버젼을 사용하도록 설정 해야 하고, springloadedbuildscript 의 의존성을 포함 하고 있어야 한다.

또한 Make Project Automatically 가 사용하도록 하여 IntelliJ 안에서 파일이 저장되어 코드가 자동으로 컴파일 되게 해야 한다.

IntelliJ 설정 해보기

  1. 프로젝트 생성 하기
    • Gradle Project 생성
      • 생성시 디렉토리 자동 생성 설정
  2. 간단한 Spring-boot web 만들기

    • build.gradle 설정
      compile('org.springframework.boot:spring-boot-starter-web:1.2.7.RELEASE')
      
    • Application 만들기
      @SpringBootApplication
      public class Application {
      public static void main(String[] args){
         SpringApplication.run(Application.class, args);
      }
      }
      
    • IndexController 만들기
      @RestController
      public class IndexController {
      @RequestMapping("/")
      public String index(){
         return "Hello World";
      }
      }
      
    • 테스트
      http://localhost:8080/
      
  3. Spring Loaded 설정하기

    • build.gradle 설정 추가하기
      dependencies {
      testCompile group: 'junit', name: 'junit', version: '4.11'
      compile('org.springframework.boot:spring-boot-starter-web:1.2.7.RELEASE')
      compile('org.springframework:springloaded:1.2.4.RELEASE') //추가하여 javaagent 옵션 추가시 위치 가져옴 변경
      }
      buildscript {
      repositories { jcenter() }
      dependencies {
         classpath "org.springframework.boot:spring-boot-gradle-plugin:1.2.7.RELEASE"
         classpath 'org.springframework:springloaded:1.2.0.RELEASE'
      }
      }
      apply plugin: 'idea'
      idea {
      module {
         inheritOutputDirs = false
         outputDir = file("$buildDir/classes/main/")
      }
      }
      
    • Make Project Automatically 설정하기
      File > Settings > Build, Execution,Deployment > Commpiler > check MakeProject automatically
      Settings [이미지 중간에 Make Project automatically 체크]
    • Controller Method 추가 하기
      @RequestMapping("/addMethod")
      public String addMethod(){
         return "Add Method";
      }
      
    • 실행 설정에 javagent 추가 하기 참조정보
      javaagent [VM options: 추가]
      -javaagent:<pathTo>/springloaded-{VERSION}.jar -noverify
      
    • 추가한 내역 반영하기(아주 중요합니다!)
      Alt + F9 를 눌러 주세요.
    • 테스트
      http://localhost:8080/addMethod
  4. 추가 테스트 내용

    • method의 추가 삭제 변경에 대하여 재반영 됩니다.
    • 일반 class를 추가하는 것은 재반영 되지만, Controller Class를 추가 하는 것은 annotation 이 인식을 하지 않아서 재반영 되지 않습니다.
      • 이부분은 ApplicationContext에서 refresh() 를 호출할 경우 해결 할 수 있습니다.(Spring-boot 제외) 자세한 내용은 추후 다루겠습니다. 테스트만 해봐서, 사실은 저도 잘 몰라서 봐야 합니다.
    • 기존 class에 annotation 추가 수정 삭제는 동작 하였습니다.

설정 관련 동영상



Spring-Loaded Eclipse 설정 - WINDOWS

  1. Spring-loaded DOWNLOAD
  2. Tomcat VM 설정에 다음과 같이 추가
    -javaagent:<pathTo>\springloaded-1.2.5.RELEASE.jar -noverify
    
  3. 반영시 Servers 에서 Tomcat 서버를 선택하고 Ctrl + Alt + P 클릭



MD 파일 :


HotSwapping.md


Spring-Loaded_설정.md


Posted by lahuman

댓글을 달아 주세요

  1. BlogIcon lahuman 2016.02.21 23:20 신고  댓글주소  수정/삭제  댓글쓰기

    IDE에서는 꼭 ALT+ F9 (MAC: COMMAND + F9)를 이용하거나, build > make project 를 이용해아 정상적으로 된다.

    http://stackoverflow.com/questions/24371111/spring-boot-spring-loaded-intellij-gradle