검색

레이블이 Spring-Security인 게시물을 표시합니다. 모든 게시물 표시
레이블이 Spring-Security인 게시물을 표시합니다. 모든 게시물 표시

2023년 3월 22일

코딩 - Serverless 을 위한 Spring Boot 기반 응용프로그램 개발하기 : Part 2

다중모듈 프로젝트 구성

먼저 모듈의 의미를 정의해보면, 모듈이란 ...
  • 다른 모듈의 코드와 분리된 코드베이스를 갖는다.
  • 빌드 결과물은 개별 아티팩트(JAR 파일)로 변환된다.
  • 다른 모듈이나 외부 라이브러리에 대한 의존성을 정의할 수 있다.
다중모듈 프로젝트는 여러 모듈로 구성된 프로젝트로, 각 모듈은 애플리케이션의 개별적인 기능 또는 구성 요소를 의미한다. 다음은 Spring Boot 멀티모듈 프로젝트를 사용할 때의 몇 가지 장점과 단점이다. 

■ 장점
  1. 모듈식 구조: 멀티모듈 프로젝트를 사용하면 우려 사항을 명확하게 분리할 수 있으며 모듈식 구조로 애플리케이션을 유지 관리하고 확장하기가 더 쉬워진다. 
  2. 개선된 구조: 멀티모듈 프로젝트는 코드들이 기능별로 단일 모듈에 그룹화되어 있어 개발자가 코드를 더 쉽게 찾고 수정할 수 있다.
  3. 재사용 가능성: 멀티모듈 프로젝트의 모듈은 다른 프로젝트에서 재 사용할 수 있으므로 새 애플리케이션을 개발할 때 시간과 노력을 절약할 수 있다.
  4. 명확한 종속성: 멀티모듈 프로젝트를 사용하면 모듈 간의 종속성을 명확하게 정의할 수 있으므로 복잡성을 관리하고 애플리케이션의 구조가 잘 잡혀 있는지 확인할 수 있다.

■ 단점
  1. 복잡성 증가: 멀티모듈 프로젝트는 단일 모듈 프로젝트보다 더 복잡할 수 있으므로 이해하고 유지 관리하기가 더 어려울 수 있다. 프로젝트에 많은 모듈이 있는 경우 특히 그렇다.
  2. 빌드 시간 증가: 멀티모듈 프로젝트는 각 모듈을 개별적으로 빌드해야 하므로 단일 모듈 프로젝트보다 빌드 시간이 더 오래 걸릴 수 있다. Gradle 또는 Maven을 사용하여 빌드 프로세스를 관리하면 이 문제를 완화할 수 있다.
  3. 설정 시간 증가: 다중모듈 프로젝트를 설정하는 데는 관리해야 할 구성 파일과 빌드 스크립트가 더 많기 때문에 단일 모듈 프로젝트를 설정하는 것보다 시간이 더 오래 걸릴 수 있다.
  4. 순환 종속성 위험: 멀티모듈 프로젝트에서는 모듈 간에 순환 종속성이 발생할 위험이 있으며, 이로 인해 런타임 오류 및 성능 저하와 같은 문제가 발생할 수 있다. 이 문제를 방지하려면 모듈 간의 종속성에 세심한 주의를 기울여야 한다.

요약하면 Spring Boot 멀티모듈 프로젝트는 모듈성, 재사용성, 명확한 종속성 등 여러 가지 이점을 제공할 수 있지만 복잡성이 증가하고 빌드 시간이 길어질 수 있다는 단점도 있다. 

멀티모듈 프로젝트를 구현하기로 결정한 것은 프로젝트 구성을 복잡성을 고려하더라도 명확한 모듈화를 위함이다. 아래 프로젝트는 3개의 멀티모듈로 구성되어 있다. 



각 모듈은 Java 소스, build.gradle 파일로 구성된 별도의 폴더에 위치한다. 

  1. 최상위 build.gradle 파일은 모든 하위 모듈 간에 공유되는 빌드 동작을 구성하여 하위 모듈에서 중복할 필요가 없도록 한다.
  2. studio 모듈에는 실제 Spring Boot 애플리케이션과 컨텍스트 구성을 위한 JavaConfig 이 포함되어 있다. 
  3. architecture-ee 모듈은 JDBC 프로그램밍 및 공통에 해당하는 클래스와 스프링 컨텍스트 구성을 돕는 JavaConfig 클래스를 포함하고 있다. 
  4. architecture-community 웹 모듈은 애플리케이션의 서비스 와 웹 계층을 구현한다. 

부모빌드 파일

① 최상위에 위치한 부모빌드에 자식에 해당하는 모듈을 포함 하려면 부모 settings.gradle 파일에  include  키워드를 사용하여 모듈들을 추가한다. 

rootProject.name = 'studio-server'
include "architecture-ee"
include "architecture-community"
include "studio"

이제 상위 폴더에서 ./gradlew 빌드를 실행하면 Gradle 은 자동으로 모듈 간 종속성을 해결하고 settings.gradle 에 나열된 순서와 관계없이 (모듈간의 의존성을 파악하여) 올바른 순서로 빌드를 수행한다. 

예를 들어, 공통에 해당하는 architecture-ee  모듈은 다른 모든 모듈이 종속되어 있으므로 다른 모든 모듈보다 먼저 빌드된다.

부모 build.gradle 파일에서 모든 하위 모듈에서 공유되는 기본 구성을 정의한다: 이를 통하여 하위 모듈의 설정을 단순화할 수 있다.

데이터베이스 마이그레이션 : Flyway

Flyway 는 데이터베이스 스키마 변경을 관리하기 위해 Spring Boot와 함께 사용할 수 있는 인기 있는 데이터베이스 마이그레이션 도구이다. 다음은 Spring Boot에서 Flyway를 사용할 때의 몇 가지 장점과 단점이다.

■ 장점
  1. 손쉬운 통합: Flyway는 간단한 구성을 통해 Spring Boot 애플리케이션에 쉽게 통합할 수 있다.
  2. 버전 관리: Flyway를 사용하면 데이터베이스 스키마 변경 사항을 형상관리와 유시한 버전 제어 방식으로 관리할 수 있으므로 시간 경과에 따른 변경 사항을 쉽게 추적하고 관리할 수 있다.
  3. 일관된 데이터베이스 상태: Flyway는 프로젝트에 참여하는 개발자 수나 데이터베이스가 배포된 환경에 관계없이 데이터베이스의 모든 인스턴스가 동일한 상태를 유지하도록 돕는다.
  4. 반복 가능한 마이그레이션: Flyway는 반복 가능한 마이그레이션을 지원하므로 새 열 또는 인덱스 추가와 같은 작업에 사용할 수 있으며, 이러한 변경 사항이 데이터베이스의 모든 인스턴스에 일관되게 적용되도록 보장한다.
  5. 여러 데이터베이스를 지원: Flyway는 Oracle, MySQL, PostgreSQL, SQL Server 등 다양한 데이터베이스를 지원하므로 데이터베이스 스키마 변경을 관리할 수 있는 다목적 솔루션이다.

■ 단점
  1. 추가 구성: Flyway를 Spring Boot 애플리케이션에 통합하려면 몇 가지 추가 구성이 필요하므로 애플리케이션이 더 복잡해질 수 있다.
  2. 학습: Spring Boot와 함께 Flyway를 사용하려면 새로운 명령과 규칙을 배워야 하므로 일부 개발자에게는 새로운 기술에 대한 학습이 필요하다.
  3. 기존 스키마와 충돌: Flyway에서 관리하지 않는 기존 스키마 변경 사항이 있는 경우 충돌 및 잠재적인 데이터 손실의 위험이 있다.
  4. 제한된 롤백 지원: Flyway는 마이그레이션 실패를 감지하고 오류 메시지를 제공할 수 있지만, 마이그레이션의 자동 롤백 기능은 제공하지 않으므로 수동 개입이 필요할 수 있다.

요약하면, Spring Boot와 함께 Flyway를 사용하면 손쉬운 통합, 버전 제어, 일관성, 반복 가능한 마이그레이션, 여러 데이터베이스 지원 등 여러 가지 이점을 얻을 수 있다. 하지만 추가 구성, 학습, 기존 스키마와의 충돌, 제한된 롤백 지원 등 몇 가지 잠재적인 단점도 있다. 

초기 개발 및 배포시 Flyway 가 제공하는 데이터베이스 마이그레이션 간소화 기능을 사용하여 실행과 동시에 필요한 테이블들과 기초 데이터들에 대한 마이그레이션을 자동화는 제한된 목적이라면 좋은 선택일 것 같다.

Flyway에서 마이그레이션 SQL 파일은 반드시 다음 이름 패턴을 준수해야한다: ( ➜ 공식 문서)

  • Part 1:  "V" 는 버전 "U" 는 실행취소 마지막으로 "R" 은 반복 작업을 의미하며 스프립트 파일 언제나 이 문자로 시작해야 한다. 이들 시작 문자는 변경할 수 있다. 
  • Part 2:  R (반복작업) 을 제외하고 점 또는 밑줄로 버전을 원하는 만큼 구분하여 사용할 수 있다. (예: 0, 1, 001,  1.2.3, 2021.09.24.12.55.32, ...)
  • Part 3: "__" (밑줄 2개) 버전 파트와 설명 파트를 구분한다. 구분자는 변경할 수 있다.
  • Part 4: 작업 설명으로 밑줄 또는 공백으로 단어를 구분할 수 있다. 
  • Part 5:  스크립트 확장자을 의미하며 변경이 할 수 있다.

 Spring Boot 에서 Part 1 ~ Part 5 항목은 application.properties (또는 application.yml)  에 설정을 통하여 변경할 수 있다. ➜ 공식문서

Flyway 는 아래와 같이 의존성을 build.gradle 에 추가하는 작업으로 시작한다. (MySql 을 사용한다면 flyway-mysql 의존성 추가 필요)

dependencies {
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-mysql'

}

데이터베이스 마이그레이션 SQL 파일 명명 규칙 중에서 변경 가능한 것들은  application.properties (또는 application.yml) 속성을 사용하여 필요에 맞게 조정한다. 


위 설정의 경우 jar 파일에서 schema/mysql 스클립트 파일을 검색하여 마이그레이션을 실행하고 파일 이력 관리를 위하여 "flyway_schema_history" 테이블을 생성하여 관리하게 된다.



보안 : Spring Security

Spring Security 5.7.0-M2  버전 부터는 사용자의 컴포넌트 기반의 보안 설정 권장을 목적으로 WebSecurityConfigurerAdapter 을 더이상 사용하지 않는다.  ➜ 스프링 공식 블로그

WebSecurityConfigurerAdapter 를 상속받고 configure 메서드를 오버라이딩하여 설정들을 정의하는 방법이 아닌 설정들을 하나의 객체로 정의하고 SecurityFilterChain 을 리턴하는 스프링에서 새롭게 권장하는 새로운 방법을 사용했다. 기존에 사용하던 XML 방식 정의를 Java 형식으로 변환을 하여 사용하였는데 xml 과 대응하는 방법들이 제공되어 어렵지 않게 설정할 수 있었다.



템블릿엔진 : Freemarker

Thymeleaf, FreeMarker, Velocity 와 같은 템플릿 엔진은 일반적으로 HTML 페이지의 서버 측 렌더링을 위해 Spring Boot 애플리케이션에서 사용된다. 

다음과 같은 경우라면 은 Spring Boot 애플리케이션에서 템플릿 엔진 사용을 고려할 충분한 이유가 된다. 

  1. 서버 측 렌더링: 템플릿 엔진을 사용하면 서버 측에서 HTML을 렌더링할 수 있으므로 SEO, 접근성 및 성능을 개선할 수 있다.
  2. 재사용 가능성: 템플릿은 웹사이트의 여러 페이지 또는 섹션에서 재사용할 수 있으므로 코드 중복을 줄일 수 있다.
  3. 유지 보수성: 템플릿 엔진을 사용하면 프레젠테이션 로직과 비즈니스 로직을 분리할 수 있어 코드 유지 관리 및 수정이 용의하다.
  4. Spring Boot와의 손쉬운 통합: 많은 템플릿 엔진이 Spring Boot와 통합되어 있어 Spring Boot 애플리케이션에서 쉽게 사용할 수 있다.

반면 애플리케이션이 HTML 페이지의 서버 측 렌더링이 필요하지 않거나 프런트엔드 구축에 React 또는 Angular와 같은 클라이언트 측 프레임워크를 사용하는 것을 선호하는 경우 템플릿 엔진을 사용할 필요가 없을 수 있다.

freemarker 템플릿 엔진은  Spring Boot application properties (application.yml) 에 정의하거나  커스텀이 필요한 경우 WebMvcConfigurer 을 직접 구현하여 정의 하는 방법으로 사용한다.  


https://docs.spring.io/spring-boot/docs/2.7.9/reference/html/application-properties.html

코딩 - Serverless 을 위한 Spring Boot 기반 응용프로그램 개발하기 : Part 1

◼︎ 환경 

  • Model : MacBook Pro (14-inch, 2021)
  • CPU : Apple M1 Pro
  • MENORY : 16GB
  • DISK : 512 GB SSD
  • OS : macOS 13.2.1 (22D68)
  • TOOLS : Visual Studio Code


왜 Spring Boot 인가 ?

Spring Boot 는 개발자에게 여러 가지 이점을 제공하는 강력한  프레임워크이다. 아래는 몇 가지 주요 이점이다.

  1. 더 빠른 개발: Spring Boot는 애플리케이션을 빠르고 쉽게 빌드하고 배포할 수 있는 방법을 제공. 사전 구성된 템플릿 세트와 다양한 라이브러리가 있어 개발 속도 향상.
  2. 간소화된 구성: Spring Boot는 작성해야 하는 상용구 코드의 양을 줄여주는 간소화된 구성 메커니즘을 제공.
  3. 자동 구성: Spring Boot의 자동 구성 기능은 프로젝트에 추가된 종속성을 기반으로 애플리케이션을 자동으로 구성. 수동 구성이 필요하지 않으므로 오류 발생 가능성이 줄어들고 시간이 절약.
  4. 프로덕션 준비 완료: Spring Boot 는 상태 확인, 메트릭 및 모니터링과 같이 애플리케이션을 프로덕션에 바로 사용할 수 있도록 하는 데 필요한 기능을 제공.
  5. 임베드 가능한 서버: Spring Boot 는 별도의 서버에 애플리케이션을 배포할 필요가 없는 임베디드 서버를 포함되어 배포가 더 쉽고 빠릅니다.
  6. 다른 Spring 프로젝트와의 손쉬운 통합: Spring Boot는 Spring Data 및 Spring Security와 같은 다른 Spring 프로젝트와 원활하게 통합되므로 복잡한 애플리케이션을 더 쉽게 빌드할 수 있음.
  7. 대규모 커뮤니티 지원: Spring Boot 는  대규모 개발자 커뮤니티 보유. 이를 통해 프레임워크가 지속적으로 개선되고 발전.

전반적으로 Spring Boot는 여러 가지 장점을 제공하는 강력한 프레임워크로서 현대적이고 확장 가능하며 강력한 애플리케이션을 구축하는 데 이상적인 선택이 될 수 있다. 

이미 Spring 을 사용하는 경우 Spring Boot 로 변경할 이유가 있을까?
Spring 을 기반으로하는 애플리케이션을 더 간단하고 빠르게 개발하고 배포할 수 있는 방법을 찾고 있다면 Spring Boot가 좋은 선택이 될 수 있다.  그러나 이미 구성 프로세스에 익숙하고 Spring Boot 가 제공하는 추가 기능이 필요하지 않다면 전환할 필요가 없을 수도 있다.

개발 도구 준비하기 

Visual Studio Code 환경에서 Spring Boot 개발을 위해 ⑴ VS Code 를 설치하고 ⑵ Spring Boot Extension Pack 확장팩을 설치한다.
 

Install the Spring Boot Extension Pack




어떤 버전의 Spring Boot 를 사용할 것인가 ?

Spring Boot 는 버전에 따라 호환되는 자바 버전에 차이가 있다. 이런 이유에서 사용(희망)하는 자바 버전을 고려하여 Spring Boot 버전을 선택해야한다.

2022 자바 생태계 현황 보고서 (https://newrelic.com/resources/report/2022-state-of-java-ecosystem)에 따르면 현재 48% 이상의 애플리케이션이 프로덕션 환경에서 Java 11을 사용하고 있으며(2020년 11.11%에서 증가), Java 8이 근소한 차이로 2위를 차지하며 46.45%의 애플리케이션이 프로덕션 환경에서 이 버전을 사용하고 있다.


운영 환경의 LTS 버전과 비교했을 때 비-LTS Java 버전에 대한 사용률은 극히 낮았으며, 비-LTS Java 버전을 사용하는 애플리케이션의 비율은 2.7%에 불과했다. 


최신 Spring Boot 버전은 3.x 이며 (https://www.marcobehler.com/guides/spring-and-spring-boot-versions) 최소 17 이상의 자바 버전을 요구하고 있다.  참여 프로젝트에서 주로 Spring 5.x 를 사용하고 있었고 운영 배포를 고려하여 가장 많이 사용하고 있는 자바 11 버전을 지원하는 Spring Boot  는 2.x  버전을 선택했다.


Spring Boot 프로젝트 만들기

요즘 개발 도구는 VS Code 를 주력으로 사용하고 있어 고민없이 VS Code 에서 새로운 Spring Boot 프로젝트 생성하는 것만 고려했다. VS Code 에서 Spring Boot 프로젝트 생성은 ① Control + Shift + P 키를 입력하여 명령 팔레트 창을 실행 Spring Initializer 을 선택하여 프로젝트를 생성한다. 또는 ② EXPLORER 에서 Create Java Project 버튼을 클릭하고 Spring Boot 을 선택한다. (참고: https://spring.io/quickstart )

① Control + Shift + P > pring Initializer : Create a Gradle Project .. 선택

 EXPLORER 에서 Create Java Project 버튼을 클릭하여 프로젝트 생성
 




Spring Boot 프로젝트 생성은 아래와 같은 옵션을 선택했다.  지금까지 자바 프로젝트는 Maven 을 사용했었는데 이번에는 Gradle 을 선택했다. 

  • Project : Gradle - Groovy
  • Language : Java
  • Spring Boot : 2.7.9
  • Packaging : Jar
  • Java : 11

프로젝트 생성시에 스타터(Starters) 의존성을 세팅하는 과정이 있는데 Spring Boot 의 우수성 중 하나가 스타터을 기반으로하는 편리한 의존성에 관리에 있는것 같다.  기존 Spring 개발 환경에서는 많은 시간을 투자하여 의존성을 관리해야 했었는데, 이러한 문제를 해결하기 위하여 도입된 Spring Boot 스타터는  약 50개 이상의 다양한 사전에 정의된 스타터들이 제공되고 있다.  (참고 : Starters) Starter 사용의 장점은 아래와 같다. 

  1. 개발자의 의존성 구성 시간이 줄어들어 생산성이 향상
  2. 추가해야 할 종속성 수가 줄어들기 때문에 POM 관리가 더 쉬워짐.
  3. 테스트를 거쳐 프로덕션에 사용할 수 있고 지원되는 종속성 구성.
  4. 종속 요소의 이름과 버전을 기억할 필요가 없음.

스타터는 아래와 같이 dependecies 항목에 추가하여 사용할 수 있다. 단지 이것으로 필요한 관련 된 모든 라이브러리들의 의존성이 해결된다. 
 
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'


DataSource 추가하기 

데이터접속을 위한 DataSource 설정이 조금 다른 부분이 있는데 기존 Spring 방식을 사용할 수도 있고 Spring Boot 에 제공하는 기본 설정을 사용할 수 도 있다. 가능한 Spring Boot 의 기능을 최대한 사용하려는 목적이 있어 Spring Boot 의 기본 설정(application.properties)을 사용하여 설정하였다. 설정은 가독성을 고려하여 추가설정없이 지원되는 yml 형식을 사용하였다. 리소스 위치는 config 폴더 이하에 저장하여 사용하였다. ( 기본적으로 Spring Boot 는 자동으로 기본 설정 파일을 로드하는 규칙이 있는데 리소스의 루트 또는 config 폴더를 검색하여 로드하도록 되어있다.)
 
Spring Boot 는 다양한 Connection Pool 기술은 제공하고 있는데 HikariCP 을 권장하고 있다.  HikariCP 성능 팁 설정(MySQL) 을 참고하여 다음과 같이 설정하였다. (설정을 위한 예시가 없어 조금 시간이 소요되었다.) 

➜ 편리하게도 spring-boot-starter-jdbc 또는 spring-boot-starter-data-jpa “starters” 추가하면 HikariCP 의존성도 자동으로 추가된다. *-data-jpa starter 를 사용하면 디폴트로 Hibernate  기반 JPA 을 사용할 수 있도록 관련 빈들이 자동으로 구성 및 생성된다.


JDBC Programming

Spring Boot JDBC는 (*-data-jdbc starter 을 사용한 경우) 시작 중에 DataSource, JdbcTemplate 및 NamedParameterJdbcTemplate와 같은 데이터베이스 관련 빈이 구성 및 생성되며, 이를 사용하려면 원하는 빈을 @Autowired 하고 사용하면 된다. ( 코드 참고 : https://mkyong.com/spring-boot/spring-boot-jdbc-examples/) 



Logging

Spring Boot는  로깅을 구현하는 log4j, logback 등과 같은 로깅 프레임워크에 대한 추상화 기능 제공을 위하여 SLF4J (Simple Logging Facade for Java) 를 사용하고 있다. Lombok (어노테이션 기반의 유틸리티 라이브러리) 을 사용하면 좀더 간단하게 @Slf4j을 클래스 상단에 정의하는 것 만으로 바로 로깅 기능을 할 수 있다.


다음은 SLF4J 기반의 로깅 예이다. Lombok 을 사용하는 경우 Logger 선언 없이 @Slf4j 어노테이션만 선언하여 바로 log 변수로 사용할 수 있다.


Lombok 는 dependecies 항목에 아래와 같이 추가하여 이용한다. 로깅 이외에도 다양한 어노테이션들을 제공하고 있고 잘 사용하면 코딩 더욱 쉬워질 수 있다.

dependencies {
implementation 'org.projectlombok:lombok'
compileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}

2022년 8월 4일

Tips - Spring Security 환경에서 HTTPS 로그인 페이지가 HTTP로 리다이랙션 되는 문제

스프링 기반의 웹프로그램을 아래와 같이 동일 도메인을 사용하면서 포트를 다르게 하여 사용자와 관리자를 분리하도록 구성을 하였다. 

  • https://www.demo.co.kr 
  • https://www.demo.co.kr:8888 
주요 환경은 아래와 같다.

  • 네이버 클라우드 LB 을 이용한 https 구현
  • CentOS 7.8.2003
  • Apache 2.4.53 
  • Tomcat 9.0.63 
  • Open JDK 11.0.15.x
  • Spring 4.3.1.RELEASE
  • Spring Security 4.1.2.RELEASE 

이슈는 권한이 없는 경우 로그인 페이지로 리다이렉션이 되는데  https://www.demo.co.kr:8888 접속을 하였으나 http://www.demo.co.kr:8888/login 로 리다이렉션이 되는 이슈가 발생했다.

 가장 많이 검색되는 방법이  <intercept-url pattern = "/**" filters = "none" requires-channel = "https" > 와 같이 requires-channel 을 추가하는 것이었지만 문제는 해결되지 않았다.

이문제는 톰켓에 RemoteIpValve 설정을 아래와 같이 추가하는 것으로 해결할 수 있었다.


다만 이런한 문제해결 방식은 LB 를 기반으로 WEB/WAS 이중화 구조 환경에서는 또다른 문제를 야기하는데 이는 클라이언트가 http로 요청을 하는 경우 톰켓에서  위 설정을 통하여 https redirect 가 되도록 구현되기 때문에 웹 서버를 통하여 다시 톰켓으로 요청이 될때 어떤 톰켓으로 요청 발생할지 알수가 없게된다. 이는 클라우드 환경에서는 멀티케스팅 기반의 세션 클러스터링을 사용할 수 없기 때문에 큰 문제가 된다.
 

2020년 7월 26일

JWT ( JSON WEB TOKEN ) 인증환경에서 이미지 다운로드

서버 세션이 아닌 JWT 토큰을 기반으로 하는 웹 프로그램은 서버와의 통신이 필요하면 HTTP 헤더에 JWT 토큰을 포함하여  요청하고 서버는 클라이언트 요청 헤더의  JWT 토큰을 꺼내어 인증 정보를 꺼네어 사용하게 된다. 이미지의 경우는 어떻까 ? 

HTML 에서 img  태그는 JWT 와 같은 보안 토큰을 헤더에 포함하는 기능을 제공하지 않는다. 그러나 이미지 데이터를 스크립트에서 다운로드하고  data : image / FILETYPE; base64 URI 체계를 사용하여 이미지를 웹 페이지에 포함시키는 방법을 사용하면 간단하게 제한을 극복할 수 있다. ( vue-auth-image 모듈이나 관련 글들을 참고하는 것이 많은 도움이 되었다.) 



다수의 이미지를 다운로드 하는 경우 요청간 지연을 주기 위하여 아래와 같은 방법을 사용했다.
  var delay = 600 ;
  $("#grid figure img[is-secure]").each(function () {
    var $this = $(this);
    delay = delay + 50 ;
    $this.parent().addClass('is-loading');
    studio.ui.setSecureImage($this, delay);
  });
  
이미지를 가져오지 못하는 경우에는 재요청을 수행하도록 코드하였다.
  
  function setSecureImage(elemment, delay, callback) {
  var delay = delay || 1000;
  let url = elemment.attr("secure-image");
  setImageAsBase64(
    url,
    function (data) {
      elemment.attr("src", data);
      elemment.parent().removeClass("is-loading");
      if (isFunction(callback)) callback();
    },
    function () {
      let maxRetry = 3,
        retry = 0;
      if (defined(elemment.data("max-retry"))) {
        maxRetry = elemment.data("max-retry");
      }
      if (defined(elemment.data("retry"))) {
        retry = elemment.data("retry");
      }
      if (retry < maxRetry) {
        elemment.data("retry", retry + 1);
        sleep(delay);
        setSecureImage(elemment);
      } else {
        elemment.parent().removeClass("is-loading");
      }
    }
  );
  }

  async function setImageAsBase64(url, successHandler, errorHandler) {
  const headers = {};
  // JWT 토큰 값 설정
  Object.assign(headers, studio.services.accounts.authHeader()); 
  await axios
    .get(url, { headers: headers, responseType: "arraybuffer" })
    .then((response) => {
      var mineType = response.headers["content-type"].toLowerCase();
      var b64encoded = Buffer.from(response.data, "binary").toString("base64");
      var prefix = "data:" + mineType + ";base64,";
      if (isFunction(successHandler)) successHandler(prefix + b64encoded);
    })
    .then((error) => {
      if (isFunction(errorHandler)) errorHandler();
    });
  } 
  



아쉽지만 이미지를 다운로드하여 data 방식으로 설정하는데 있어 오류가 발생되어 다시 시도하도록 추가한 코드는 효용성이 없는 것 같다. 테스트 환경 이슈일 수도 있으나 다운로드에 있어 무작위로 오류가 발생하며 일단 오류가 발생되면 다시 시도한 경우 역시 오류가 발생한다. (→ 서버 이슈인지 코드 이슈인지는 확실하지 않다.)

참고자료

2017년 12월 1일

스프링 시큐리티(Spring Security) - 도메인 객체 보안 (Domain Object Security ACL) 활용하기

대부분의  스프링 시큐리티를 사용하는 웹 응용프로그램들은  누가(Who)  어떤 URL 또는 어떤 메소드(Where)에 대한 호출에 대한 접근을 제안하는 방법으로 스프링 시큐리티를 사용하고 있다.


이런 접근 방식에서는 롤(ROLE)를 정의하고 사용자에게 롤(ROLE) 부여하고 특정 URL 또는 특정 클래스 함수에 대하여 ROLE 에 따른 접근제어를 하는 것으로 권한 관리를 구현하게 된다.

그러나 웹 프로그램들은 우리가 생각하는 이상으로 복잡하기 때문에 누가(Who) , 어디를 (Where) 뿐 아니라 무엇을 (What) 포함하여 권한을 설정할 수 있어야 한다.

예를 들어 아래와 게시판 프로그램을 디자인 한다고 가정해보자.

  • REQ1: Q&A게시판(B1)은 누구나(U1) 읽기(P1) / 쓰기(P2)가 가능하고 , 
  • REQ2: 자료실 게시판(B2)은 누구나(U1) 읽기(P1)는 가능하지만 관리자(U2)만 쓰기(P2)가 가능하고,  
  • REQ3: 고객지원 게시판(B3) 은 지정된 특정 사용자(U3) 만 읽기(P1) / 쓰기(P2)가 가능해야 한다.

누가(Who) , 어디를 (Where) 정보를 사용하여 권한을 결정하는 방식에서는 위의 기능을 구현하게 위하여 각기 다른 URL 또는 함수들 만들고 REQ3 요구사항을 위하여 추가로 권한 검사를 위하여 응용 프로그램 레벨에서 다시 권한결정을 위한  하드코딩이 필요하게 된다.

(Who) , 어디를 (Where) 뿐 아니라 무엇을 (What) 포함하여 권한을 결정하는 접근 방식에서는 게시판 객체 (What)에대한 권한을 설정할 수 있기 때문에 응용 프로그램 레벨에서의 하드 코딩 없이 구현이 가능하다. 이를 위하여 스프링 시큐리티는 도메인 객체 보안 (ACL) 서비스를 제공하고 있다.

스프링 시큐리티 ACL 사용하기 

도메인 객체 보안 (ACL) 서비스는   spring-security-acl-xxx.jar  라이브러리를 통하여 제공된다.  Spring 기반 웹 프로그램 개발 Part 2 - SpringSecurity 사용하기 와 같이 pom.xml 파일을 기술하였다면 관련 라이브러리는 포함되어 있다. (spring-security-taglibs 가 spring-security-acl 에 대한 의존성을 가지고 있기 때문에 자동으로 포함된다.)


    org.springframework.security
    spring-security-core
    ${project.dependency.spring-security.version}
    compile
  

    org.springframework.security
    spring-security-web
    ${project.dependency.spring-security.version}
    compile


    org.springframework.security
    spring-security-config
    ${project.dependency.spring-security.version}


    org.springframework.security
    spring-security-test
    ${project.dependency.spring-security.version}
    test  



또는 직접 다음과 같이 spring-security-acl 라이브러리를 추가해도 된다.


    org.springframework.security
    spring-security-core
    4.1.3.RELEASE
    compile
  

    org.springframework.security
    spring-security-web
    4.1.3.RELEASE
    compile


    org.springframework.security
    spring-security-config
    4.1.3.RELEASE


    org.springframework.security
    spring-security-acl
    4.1.3.RELEASE
    test  


다음으로 도메인 객체 보안 (ACL) 서비스를 사용하려면 ACL 정보를 어딘가에 저장해야 한다. 이를 위하여 ACL 데이터를 저장할 데이터베이스와 관련 테이블 생성이 필요하다. 테이블 생성은 spring-security-acl-xxx.jar 에 포함된 sql 파일을 이용하면 된다. ( mysql, oracle, postgres, SqlServer 에 대항하는 스크립트가 별도로 지원)



다음은 스크립트를 통하여 생성되는 테이블에 대한 다이어그램이다.



  • ACL_SID : 롤, 사용자에 대한 키 정보가 저장되는 테이블이다. ID 는 유니크한 숫자값, PRINCIPAL 는 롤의 경우는 0 사용자의 경우는 1 , SID 는 사용자 아이디 또는 롤 정보를 의미한다. 
  • ACL_CLASS : 도메인 객체 클래스에 대한 정보가 저장되는 테이블이다. ID 는 유니크한 숫자값, CLASS 는 클래스 이름을 의미한다. 
  • ACL_OBJECT_IDENTITY : 도메인 객체 인스턴스 정보가 저장되는 테이블이다. ID 는 유니크한 숫자값, OBJECT_ID_CLASS 는 클래스에 해당하는 ACL_CLASS.ID, OWNER_SID 는 생성자를 나타내는 ACL_SID.ID를 의미한다. 
  • ACL_ENTRY : 접근 권한 데이터가 저장되는 테이블이다. ID 는 유니크한 숫자값, OBJECT_ID_CLASS 는 클래스에 해당하는 ACL_CLASS.ID, OBJECT_ID_IDENTITY 는 객체 인스턴스를 나타내는 ACL_OBJECT_IDENTITY.ID , SID 는 권한이 부여된 대상을 의미하는 ACL_SID.ID, MASK 는 권한을 의미하는 마스크 값이다. 기본적으로 부여되는 모든 권한들은 유니크한 마스크 값을 갖는다.

예를 들어 dhson 사용자에게 ID 값이 9 인 architecture.community.board.Board 객체에 마스크 값이 1에 해당하는 READ 권한을 부여한다고 하면 다음과 같은 데이터들이 저장된다.

ACL_SID 
ID : 1, PRINCIPAL: 1, SID:dhson

ACL_CLASS
ID: 1, CLASS: architecture.community.board.Board

ACL_OBJECT_IDENTITY
ID: 1, OBJECT_ID_CLASS: 1, OBJECT_ID_IDENTITY: 9

ACL_ENTRY
ID: 1, ACL_OBJECT_IDENTITY: 1, SID: 1, MASH : 1

이제 아래와 같이 도메인 객체 보안 (ACL) 서비스 사용을 위하여 객체들을 **-context.xml 파일에 기술한다.

permissionSubsystemContext.xml


 Spring Security Domain Object ACL 설정

 
        
        
  
 
 
 
  
 
 
 
  
  
  
  
  
 
 
 
  
  
  
  
 
 
 
  
  
 
 
  
   
  
  
 
 
  
 
  
  
 
  
     
 



이제 자바 클래스에서 communityAclService 를 직접 사용하여 권한을 확인 하거나 @PreAuthorize 어노테이션을 사용하여 손쉽게 권한 제어가 가능하다.


  @PreAuthorize("hasPermission(#message, write) or hasPermission(#message, admin)")
  public void editMessage(Message message) { ... }


또는 아래와 같은 어노테이션도 가능하다.

  @PreAuthorize("hasPermission(#boardId, 'architecture.community.board.Board', 'READ')")
  @RequestMapping(value = {"/{boardId:[\\p{Digit}]+}/list"}, method = { RequestMethod.POST, RequestMethod.GET } )
  public String displayThreadList ( @PathVariable Long boardId,  HttpServletRequest request, HttpServletResponse response,  Model model) throws BoardNotFoundException{  
    / ***
  }


2017년 10월 23일

Postman 을 이용한 REST API 개발 & 테스트

이제는 HTTP 를 기반으로하는 REST API 개발은 아주 일상적인 일이 되었다.

웹 기반의 RESR API 를 손쉽게 개발 & 테스트하는 도구로는 Postman 소프트웨어가 있다. (대부분의 모바일 개발자들은 다 알고 있을 것 같다)

Postman 은 www.getpostman.com 또는 크롬 웹 스토어 에서 설치할 수 있다.


먼저 스프링을 기반으로 개발된 다음과 같은 API 가 있다고 하자.

1. 게시판 목록 조회 API
/boards/list.json

2. 게시판의 게시물 목록 조회 API
/boards/{boardId:[\\p{Digit}]+}/threads/list.json

Postman 을 실행하고 호출 방법(1) , URL (2) 입력하고 Send(3) 버튼을 클릭하면 결과(4) 를 확인 할 수 있다. AJAX 기반의 웹 프로그램을 개발하거나 모바일 API 를 개발하는 경우라면 아주 쉽게 개발된 API 를 테스트할 수 있다.

그림1. Postman 실행화면

인증이 필요한 API 이고 Spring Security 를 사용하고 있다면 간단한 설정 추가를 통하여 Basic Authentication 기능을 추가할 수 있고 Postman 에서는 요청을 만들때 Authorization 정보를 입력(1)하여 손쉽게 인증이 요구되는 API 들에 대한 테스트가 가능하다.

그림2. Basic Auth 를 통한 요청
기본적으로 한번 Basic Authentication 이 처리되면 이후 요청부터는 세션이 생성되어 인증된 상태에서 요청이 전달된다.

앱을 개발하거나 또는 AJAX 기반의 웹 API 개발시에는 UI 개발이후에 테스트하고 디버깅을 진행하였으나 Postman 을 사용하여 API 개발과 통시에 테스트를 진행할 수 있어 개발 생산성을 향상시킬수 있었다.



2017년 6월 10일

Spring Security - Login 시 사용자 정의 입력 값 전달하기

SpringSecurity 기반의 인증을 사용하는 환경에서 username 과 password 이외의 값을 추가로 전달하여 사용자를 인증을 할 수 없을까? (예를들어 회사번호, 아이디, 비밀번호를 전달받아 인증을 하는 경우 )

환경

Spring 4.3.1.RELEASE
Spring Security 4.1.2.RELEASE

Stack overflow 및 몇가지 자료를 참고하면 아래와 같이 구현을 한면 되다고 한다.

How to pass an additional parameter with spring security login page
  1. UsernamePasswordAuthenticationFilter 를 확장하여 사용자 정의 필터를 구현.
  2. 사용자 정의 UserDetailsService 구현.
  3. <http/> 설정에 사용자 정의 필터를 추가
아쉽게도  SpringSecurity 4.1 부터는 내부 구현 로직을 변경으로 UsernamePasswordAuthenticationFilter 를 위와 같은 방식으로 사용자 정의 필터로 변경할 수 없다.

SpringSecurity 4.1 이상의 환경에서는 필터를 수정하지 않고 RequestContextFilter 를 사용하여 사용자 정의 AuthenticationProvider 구현체에서  RequestContextHolder 를 사용하여 로그인시에 전달된 추가적인 파라메터 값을 꺼내어 원하는 방법으로 인증을 구현하면 된다.

RequestContextFilter 필터는 web.xml 에 필터 설정을 추가하여 사용할 수 있다.

WEB-INF/web.xml

 
 
   requestContextFilter
   org.springframework.web.filter.RequestContextFilter
 
 
   requestContextFilter
   /*
      
 
 
 
  springSecurityFilterChain
  org.springframework.web.filter.DelegatingFilterProxy
 
 
  springSecurityFilterChain
  /*
 


또는 Spring Security 설정 컨텍스트 ( 문서에서는 WEB-INF/context-config/securitySubsystemContext.xml 사용함 ) 에 아래와 같이 필터를 추가한다.

WEB-INF/context-config/securitySubsystemContext.xml
     
            
    
    
    


사용자 정의 인증 구현에서는 아래와 같이 RequestContextHolder 클래스를 사용하여 클라이언트에서 전달된 파라메터 값을 접근할 수 있다.

    ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();  
    if( attr!=null && attr.getRequest() != null ) {
        long companyId = ParamUtils.getLongParameter(attr.getRequest(), "companyId", 0L);
        return super.retrieveUser(companyId, username, authentication);
    }else{  
        return super.retrieveUser(username, authentication);
    }



참고자료
Spring 기반 웹 프로그램 개발 Part 2 - SpringSecurity 사용하기

2017년 3월 16일

Spring 기반 웹 프로그램 개발 Part 2 - SpringSecurity 사용하기

SpringSecurity 기술을 사용하면
  •  인증, ROLE 기반 접근제어, 
  •  여러 알고리즘을 지원하는 비밀 번호 암호화 및 확인 
  •  어노테이션을 이용한 함수 수준의 접근 제어 
 등의 기능들을 쉽게 구현할 수 있다.

개인적으로 SpringSecurity 을 사용하는 이유는
  • 첫번째 OWASP TOP 10 취약점 중 최소한 A2, A4, A6, A7, A8 에 해당하는 보안 이슈 해결에 도움을 준다는 점이다.  
  • 두번째 레어어화 된 보안 아키텍처를 응용프로그램에 도입하므로써  개발 단계 부터 안전한 응용프로그램 구현이 가능해진다는 점이다.  왜 우리가 Struts2, Spring MVC 와 같은 MVC 프레임워크를 사용하는 가를 생각해보면 보안 프레임워크의 도입의 이유도 설명이 될 것 같다.  

환경 설정
개인적으로 Maven 기반의 개발환경을 선호하기 때문에 여기에서는 아래의 Maven Dependency 을 프로젝트에 추가한다.



    org.springframework.security
    spring-security-core
    ${project.dependency.spring-security.version}
    compile
  

    org.springframework.security
    spring-security-web
    ${project.dependency.spring-security.version}
    compile


    org.springframework.security
    spring-security-config
    ${project.dependency.spring-security.version}


    org.springframework.security
    spring-security-test
    ${project.dependency.spring-security.version}
    test  


SpringSecurity 사용하기 
SpringSecurity 기반의 인증 및 접근 제어를 위하여 다음 인터페이스들을 구현한다.

  • AuthenticationProvider : 사용자 인증을 수행하는 인터페이스 클래스로 자신만의 비밀번호 기반의 인증을 위해서는 커스텀 클래스 구현이 필요하다.
  • AuthenticationSuccessHandler : 인증에 성공한 경우 다음 행위를 제어하기 위한 인터페이스 클래스. API형식의 인증 서비스 구현을 위해서는 커스텀 구현 클래스가 필요하다.
  • UserDetailsService : 사용자 정보를 로드하기 위한 인터페이스 클래스로 자신의 사용자 정보를 사용하여 인증 및 권한을 관리하고자 하는 경우는 커스텀 구현 클래스가 필요하다.

SpringSecurity 설정 
환경에 따라 다른 설정을 통하여 웹 프로그램을 제어하는 방법을 선호하기 때문에 XML 방식을 선호한다. 어노테이션은 부분적으로만 사용한다.

SpringSecurity 커스터 구현과 설정

AuthenticationProvider & AuthenticationSuccessHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package architecture.community.spring.security.authentication;
 
import javax.inject.Inject;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
 
import com.google.common.eventbus.EventBus;
 
import architecture.community.i18n.CommunityLogLocalizer;
import architecture.community.spring.security.userdetails.CommuintyUserDetails;
import architecture.community.user.UserManager;
import architecture.community.user.event.UserActivityEvent;
 
public class CommunityAuthenticationProvider extends DaoAuthenticationProvider {
    
    private Logger logger = LoggerFactory.getLogger(getClass());
    
    @Inject
    @Qualifier("userManager")
    private UserManager userManager;    
    
    @Autowired(required = false)
    @Qualifier("eventBus")
    private EventBus eventBus;
    
    
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
 
        if (authentication.getCredentials() == null)
            throw new BadCredentialsException(CommunityLogLocalizer.getMessage("010101"));        
        super.additionalAuthenticationChecks(userDetails, authentication);
        try {
            CommuintyUserDetails user = (CommuintyUserDetails) userDetails;
            if(eventBus!=null){
                eventBus.post(new UserActivityEvent(this, user.getUser(), UserActivityEvent.ACTIVITY.SIGNIN ));
            }
        } catch (Exception e) {
            logger.error(CommunityLogLocalizer.getMessage("010102"), e);
            throw new BadCredentialsException( messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials""Bad credentials"));
        }        
    }    
}
cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package architecture.community.spring.security.authentication;
 
import java.io.IOException;
import java.util.Map;
 
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.ui.ModelMap;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
 
import architecture.community.web.model.json.Result;
import architecture.community.web.util.ServletUtils;
import architecture.ee.util.StringUtils;
 
public class CommunityAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
 
    private Logger logger = LoggerFactory.getLogger(getClass());
    
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
 
        if (ServletUtils.isAcceptJson(request)) {
            Result result = Result.newResult();
            result.getData().put("success"true);
            result.getData().put("returnUrl", ServletUtils.getReturnUrl(request, response));
            String referer = request.getHeader("Referer");
            if (StringUtils.isNullOrEmpty(referer))
                result.getData().put("referer", referer);
            Map<String, Object> model = new ModelMap();
            model.put("item", result);
            
            MappingJackson2JsonView view = new MappingJackson2JsonView();
            view.setExtractValueFromSingleKeyModel(true);
            view.setModelKey("item");
            try {
                createJsonView().render(model, request, response);
            } catch (Exception e) {}        
            return;
        }
        super.onAuthenticationSuccess(request, response, authentication);
    }
 
    protected View createJsonView(){
        MappingJackson2JsonView view = new MappingJackson2JsonView();
        view.setExtractValueFromSingleKeyModel(true);
        view.setModelKey("item");
        return view;
    }
 
}
 
cs

UserDetailsService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package architecture.community.spring.security.userdetails;
 
import java.util.Collections;
import java.util.List;
 
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
 
import com.fasterxml.jackson.annotation.JsonIgnore;
 
/**
 * 
 * @author donghyuck
 *
 */
public class CommuintyUserDetails extends User {
 
    @JsonIgnore
    private final architecture.community.user.User communityUser;
 
    public CommuintyUserDetails(architecture.community.user.User communityUser) {
        super(communityUser.getUsername(), communityUser.getPassword(), communityUser.isEnabled(), truetruetrue, AuthorityUtils.NO_AUTHORITIES);
        this.communityUser = communityUser;
    }
 
    public CommuintyUserDetails(architecture.community.user.User communityUser, List<GrantedAuthority> authorities) {
        super(communityUser.getUsername(), communityUser.getPassword(), communityUser.isEnabled(), truetruetrue, authorities);
        this.communityUser = communityUser;
    }
 
    public boolean isAnonymous() {
        return communityUser.isAnonymous();
    }
 
    public architecture.community.user.User getUser() {
        return communityUser;
    }
 
    public long getUserId() {
        return communityUser.getUserId();
    }
 
    public long getCreationDate() {
        return communityUser.getCreationDate() != null ? communityUser.getCreationDate().getTime() : -1L;
    }
 
}
 
cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package architecture.community.spring.security.userdetails;
 
import java.util.ArrayList;
import java.util.List;
 
import javax.inject.Inject;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
 
import architecture.community.user.Role;
import architecture.community.user.RoleManager;
import architecture.community.user.User;
import architecture.community.user.UserManager;
import architecture.community.user.UserNotFoundException;
import architecture.community.util.CommunityConstants;
import architecture.ee.service.ConfigService;
import architecture.ee.util.StringUtils;
public class CommunityUserDetailsService implements UserDetailsService {
 
    private Logger logger = LoggerFactory.getLogger(getClass());
    
    @Inject
    @Qualifier("userManager")
    private UserManager userManager;    
    
    @Inject
    @Qualifier("roleManager")
    private RoleManager roleManager;    
    
    
    @Inject
    @Qualifier("configService")
    private ConfigService configService;    
    
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 사용자 환경에 적절하게 구현..
        try {            
            User user = userManager.getUser(username);            
            CommuintyUserDetails details = new CommuintyUserDetails(user, getFinalUserAuthority(user));            
            return details ;            
        } catch (UserNotFoundException e) {
            throw new UsernameNotFoundException("User not found.", e);    
        }
    }
 
    protected List<GrantedAuthority> getFinalUserAuthority(User user) {            
         // 사용자 환경에 적절하게 구현..    
        String authority = configService.getLocalProperty(CommunityConstants.SECURITY_AUTHENTICATION_AUTHORITY_PROP_NAME);
        List<String> roles = new ArrayList<String>();        
        if(! StringUtils.isNullOrEmpty( authority ))
        {
            authority = authority.trim();
            if (!roles.contains(authority)) {
                roles.add(authority);
            }
        }
        for(Role role : roleManager.getFinalUserRoles(user.getUserId())){
            roles.add(role.getName());
        }
        return AuthorityUtils.createAuthorityList(StringUtils.toStringArray(roles));
    }
    
}
 
cs

Context.xml(스프링 설정 파일)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
    xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p" xmlns:c="http://www.springframework.org/schema/c"
    xmlns:aop="http://www.springframework.org/schema/aop" xmlns:jee="http://www.springframework.org/schema/jee"
    xmlns:tx="http://www.springframework.org/schema/tx" xmlns:lang="http://www.springframework.org/schema/lang"
    xmlns:util="http://www.springframework.org/schema/util"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd
       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
       http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang.xsd
       http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd 
       http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">
    <beans:description><![CDATA[
       Spring Security 설정  
    ]]></beans:description>
    <global-method-security secured-annotations="enabled" pre-post-annotations="enabled" />
    <http auto-config="true" use-expressions="true" disable-url-rewriting="true">        
        <intercept-url pattern="/*"                        access="permitAll"/>
        <intercept-url pattern="/data/*"                access="permitAll"/> 
        <intercept-url pattern="/secure/data/**"         access="hasRole('ROLE_USER')" />    
        <!-- 로그인 페이지 지정 -->
        <form-login 
            login-page="/accounts/login" 
            username-parameter="username" 
            password-parameter="password"
            login-processing-url="/accounts/auth/login_check"
            authentication-success-handler-ref="authenticationSuccessHandler"
            authentication-failure-url="/error/401" />
        <!-- 로그아웃 설정  -->
        <logout invalidate-session="true" logout-url="/accounts/logout" logout-success-url="/" delete-cookies="JSESSIONID" />
        <!-- CSRF ATTACK  -->
        <csrf disabled="true" />
        <anonymous enabled="true" username="ANONYMOUS" />      
        <!-- 중복 로그인 방지 설정 -->
        <session-management session-fixation-protection="newSession" >
            <concurrency-control max-sessions="1" expired-url="/error/login_duplicate"/>
        </session-management>        
        <!-- 접근 불허시 보여줄 페이지 설정 -->         
        <access-denied-handler error-page="/error/unauthorized" />  
    </http>
    <authentication-manager id="authenticationManager">
        <authentication-provider ref="authenticationProvider"/>
    </authentication-manager>    
    <beans:bean id="authenticationProvider" 
        class="architecture.community.spring.security.authentication.CommunityAuthenticationProvider" 
        p:passwordEncoder-ref="passwordEncoder" p:userDetailsService-ref="userDetailsService" />    
    <beans:bean id="authenticationSuccessHandler" 
        class="architecture.community.spring.security.authentication.CommunityAuthenticationSuccessHandler"/>            
    <beans:bean id="userDetailsService" class="architecture.community.spring.security.userdetails.CommunityUserDetailsService" />    
    <beans:bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></beans:bean>    
</beans:beans> 
cs



소스 자료  
Community 1.0.0-BETA


참고 자료 

OWASP top ten attacks and Spring Security
Spring Security Tutorial
Securing a Web Application