2016년 9월 30일 금요일

annotation을 사용한 controller aop 설정하기

annotation을 이용하면 전역 처리가 편리하다.

아래와 같이 어노테이션을 선언했다.


@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ROLE_USER')")
public @interface PreAccountAuthorize {
 boolean checkBanInfo() default false; //제재 여부 체크
}

해당 타겟이 annotation type이며 실행 시 annotation이 사라지지 않기 위해 RetentionPolicy는 runtime 으로 지정되어 있다.

해당 어노테이션을 사용하는 부분이 class와 method 라는 선언을 하였다.

기존 인증 체크에 추가적인 조건 체크를 더하기 위해 기존 인증 체크를 추가하였다.
@PreAuthorize("hasRole('ROLE_USER')")

사실 @PreAuthorize를 선언하면 해당 어노테이션에 위 선언들이 있기 때문에 아래와 같이 써도 무방하다.
@PreAuthorize("hasRole('ROLE_USER')")
public @interface PreAccountAuthorize {
   boolean checkBanInfo() default false; //제재 여부 체크
}
@Slf4j
@Aspect
@Component
public class PreAccountAuthorizeAspect {
 
   @Setter private BanInfoService banInfoService;

   @Before("@annotation(preAccountAuthorize)")
   public void methodBefore(JoinPoint joinPoint, PreAccountAuthorize preAccountAuthorize) {
      checkBefore(joinPoint, preAccountAuthorize);
    }
 
    @Before("@target(preAccountAuthorize) && bean(*Controller)")
    public void classBefore(JoinPoint joinPoint, PreAccountAuthorize preAccountAuthorize) {
        checkBefore(joinPoint, preAccountAuthorize);
    }
 
    public void checkBefore(JoinPoint joinPoint, PreAccountAuthorize preAccountAuthorize) {
        if (banInfoService == null) {
            throw new BlueskyException("banInfoService is not set");
        }
        checkBanInfo(preAccountAuthorize);
    }
 
    private void checkBanInfo(PreAccountAuthorize preAccountAuthorize) {
        log.debug("checkBanInfo");
        if (!preAccountAuthorize.checkBanInfo()) {
            return;
        }
        List banInfo = banInfoService.checkBanInfo();
        log.debug("checkBanInfo banInfo : {}", banInfo);
        if (!banInfo.isEmpty()) {
            throw new BlueskyException("security.BANNED_ACCOUNT");
        }
    }
}

@target 을 이용한 class annotation은 전체 bean을 전부 훝어서 해당 어노테이션을 체크하는 듯 하다.
이 경우 spring security 를 사용하면 GlobalMethodSecurityConfiguration 관련해서 에러가 발생한다.

aop 관련 동작을 전체 bean 대상으로 체크하다 security쪽 bean의 non-visible class를 대상으로 cglib 동작이 수행되면서 에러가 발생하는데 이를 막기 위해 controller로 제한하였다.
관련한 에러는 다음과 같다.

Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration': Unsatisfied dependency expressed through method 'setObjectPostProcessor' parameter 0: Error creating bean with name 'objectPostProcessor' defined in class path resource [org/springframework/security/config/annotation/configuration/ObjectPostProcessorConfiguration.class]: Initialization of bean failed; nested exception is org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class [class org.springframework.security.config.annotation.configuration.AutowireBeanFactoryObjectPostProcessor]: Common causes of this problem include using a final class or a non-visible class; nested exception is java.lang.IllegalArgumentException: Cannot subclass final class org.springframework.security.config.annotation.configuration.AutowireBeanFactoryObjectPostProcessor; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'objectPostProcessor' defined in class path resource [org/springframework/security/config/annotation/configuration/ObjectPostProcessorConfiguration.class]: Initialization of bean failed; nested exception is org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class [class org.springframework.security.config.annotation.configuration.AutowireBeanFactoryObjectPostProcessor]: Common causes of this problem include using a final class or a non-visible class; nested exception is java.lang.IllegalArgumentException: Cannot subclass final class org.springframework.security.config.annotation.configuration.AutowireBeanFactoryObjectPostProcessor

Spring Boot 1.4.1-RELEASE에서 spring-data-jpa의 auditing 이 동작하지 않는 문제 발생

https://github.com/spring-projects/spring-boot/issues/6983

현재 Spring Boot 1.4.1-RELEASE를 사용하면 spring-data-jpa의 @CreatedDate, @LastModifiedDate, @CreatedBy, @LastModifiedBy 같은 auditing 처리가 제대로 동작하지 않는 문제가 있다.
orm.xml을 로드하는 boot의 JpaBaseConfiguration 설정에 버그가 있다.

임시 해결책으로 아래의 빈을 선언해서 쓰는 방법이 있다.


@Bean
public EntityManagerFactoryBuilder entityManagerFactoryBuilder(JpaProperties properties, JpaVendorAdapter jpaVendorAdapter, ObjectProvider persistenceUnitManagerProvider) {
 return new EntityManagerFactoryBuilder(jpaVendorAdapter, properties.getProperties(), persistenceUnitManagerProvider.getIfAvailable(), null);
}

1.4.2 가 나오기 전까진 당분간 저 선언을 유지해야 한다.




1.4.2-RELEASE가 나온 이후 해당 버그는 수정되었다.

Spring Boot 1.4.2-RELEASE

2016년 9월 28일 수요일

thymeleaf 에서 spring security login 여부를 변수로 사용하기

spring security 정보를 thymeleaf 에서 사용하는 기본적인 방법은 다음과 같다.

thymeleaf-extras-springsecurity4 를 사용하고 다음과 같이 호출한다.
<div sec:authorize="hasRole('ROLE_ADMIN')">
    This content is only shown to administrators.
</div>
<div sec:authorize="hasRole('ROLE_USER')">
    This content is only shown to users.
</div>

이와 같이 사용하면 영역별로 처리하기 편하지만 영역을 나누기 싫은 경우가 있다.

이럴 때엔 thymeleaf의 변수로 사용하는 것이 편하다.

다음과 같이 사용이 가능하다.

<div th:with="isLogin = ${#authorization.expression('isAuthenticated()')}">
[[${isLogin}]]
    </div>

일반적으로 위와 같이 사용하면 아무 문제가 없다.
하지만 security가 적용되지 않은 요청 범위의 페이지(예를 들면 /error/...와 같은 에러 페이지)를 호출할 경우
Caused by: java.lang.IllegalArgumentException: Authentication object cannot be null
에러가 발생한다.

spring security를 통해 authorization이 생성되지 않은 상태에서 expression을 호출하기 때문인데 이를 방지하기 위해 아래처럼 선언하는 것이 좋다.

<div th:with="isLogin = ${#authorization.getAuthentication() != null and #authorization.expression('isAuthenticated()')}">
[[${isLogin}]]
</div>

2016년 9월 5일 월요일

html에서 bindException 결과 처리 시 순서 지정하기

사용자에게 입력을 받은 데이터의 정규식 체크가 올바른가에 대해 기존에는 client, server 양쪽다 체크해야 한다고 생각한 때가 있다.

요즘은 에러에 대해 공통화 처리로 개발을 하면서 굳이 client에서 체크할 필요가 있을까 싶다.

bindException의 경우 array 형태로 에러 결과를 반환한다.

{
    "result": [{
        "errorCode": null,
        "exceptionClassName": "bindException",
        "message": "소개글을 입력해주세요.",
        "object": "fooClass",
        "field": "intro",
        "displayableMessage": true
    }, {
        "errorCode": null,
        "exceptionClassName": "bindException",
        "message": "이미지를 선택해주세요.",
        "object": "fooClass",
        "field": "imgSrc",
        "displayableMessage": true
    }, {
        "errorCode": null,
        "exceptionClassName": "bindException",
        "message": "길드이름을 입력해주세요.",
        "object": "fooClass",
        "field": "guildName",
        "displayableMessage": true
    }, {
        "errorCode": null,
        "exceptionClassName": "bindException",
        "message": "서버를 선택해주세요.",
        "object": "fooClass",
        "field": "serverId",
        "displayableMessage": true
    }, {
        "errorCode": null,
        "exceptionClassName": "bindException",
        "message": "내용을 입력해주세요.",
        "object": "fooClass",
        "field": "contents",
        "displayableMessage": true
    }]
}


각각의 에러를 표시하는 위치가 개별로 존재한다면 별 문제가 없지만 특정 단일 영역에 저 많은 에러들 중 우선순위를 정해 하나만 표시하고 싶은 경우 순서가 중요해진다.

문제는 bindException은 순서를 지정하는 기능이 없다는 점이다.

이에 대해서 javascript의 sort 처리로 순서를 정하는 식으로 해결을 하였다.

function sortFunc(a, b, targetKey, targetArray) {
    var aCodeIndex = $.inArray(a[targetKey], targetArray);
    var bCodeIndex = $.inArray(b[targetKey], targetArray);
    if (aCodeIndex > -1 && bCodeIndex > -1) {
        return aCodeIndex > bCodeIndex;
    } else if (aCodeIndex > -1) {
        return false;
    } else if (bCodeIndex > -1) {
        return true;
    } else {
        return a[targetKey] > b[targetKey];
    }
}

function errorMessageSort(a, b) {
    var sortKeyOrder = ["serverId", "guildName", "intro", "imgSrc", "contents", "phoneNumber"];
    return sortFunc(a, b, "field", sortKeyOrder);
}

$.ajax({
    url : "[호출주소]",
    type : "POST",
    data : [데이터파라메터]
}).done(function(data) {
    //성공한 경우 처리
}).fail(function(jqXHR, textStatus, errorThrown) {
    jqXHR.responseJSON.result.sort(errorMessageSort);
 
    $("[메세지를 보여줄 영역]").text(jqXHR.responseJSON.result[0].message).show();
}