1. Spring Security
1) Spring Security란?
- Spring 기반의 애플리케이션의 보안(인증과 권한, 인가 등)을 담당하는 스프링 하위 프레임워크
- Spring Security는 '인증'과 '권한'에 대한 부분을 Filter 흐름에 따라 처리하고 있다.
- Filter는 Dispatcher Servlet으로 가기 전에 적용되므로 가장 먼저 URL 요청을 받지만, Interceptor는 Dispatcher와 Controller사이에 위치한다는 점에서 적용 시기의 차이가 있다.
- Spring Security는 보안과 관련해서 체계적으로 많은 옵션을 제공해주기 때문에 개발자 입장에서는 일일이 보안관련 로직을 작성하지 않아도 된다는 장점이 있다.
2) 인증과 인가
3) Spring Security 모듈
- SecurityContextHolder
- 보안 주체의 세부 정보를 포함하여 응용프래그램의 현재 보안 컨텍스트에 대한 세부 정보가 저장된다.
- SecurityContextHolder는 기본적으로 SecurityContextHolder.MODE_INHERITABLETHREADLOCAL 방법과SecurityContextHolder.MODE_THREADLOCAL 방법을 제공한다.
- SecurityContext
- Authentication을 보관하는 역할을 하며, SecurityContext를 통해 Authentication 객체를 꺼내올 수 있다.
- Authentication
- Authentication는 현재 접근하는 주체의 정보와 권한을 담는 인터페이스
- Authentication 객체는 Security Context에 저장되며, SecurityContextHolder를 통해 SecurityContext에 접근하고, SecurityContext를 통해 Authentication에 접근할 수 있다.
public interface Authentication extends Principal, Serializable {
// 현재 사용자의 권한 목록을 가져옴
Collection<? extends GrantedAuthority> getAuthorities();
// credentials(주로 비밀번호)을 가져옴
Object getCredentials();
Object getDetails();
// Principal 객체를 가져옴.
Object getPrincipal();
// 인증 여부를 가져옴
boolean isAuthenticated();
// 인증 여부를 설정함
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
- UsernamePasswordAuthenticationToken
- Authentication을 implements한 AbstractAuthenticationToken의 하위 클래스로, User의 ID가 Principal 역할을 하고, Password가 Credential의 역할을 한다.
- UsernamePasswordAuthenticationToken의 첫 번째 생성자는 인증 전의 객체를 생성하고, 두번째 생성자는 인증이 완려된 객체를 생성한다.
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
// 주로 사용자의 ID에 해당함
private final Object principal;
// 주로 사용자의 PW에 해당함
private Object credentials;
// 인증 완료 전의 객체 생성
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
// 인증 완료 후의 객체 생성
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
}
public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {
}
- AuthenticationProvider
- .제 인증에 대한 부분을 처리하는데, 인증 전의 Authentication객체를 받아서 인증이 완료된 객체를 반환하는 역할을 한다.
- 아래와 같은 AuthenticationProvider 인터페이스를 구현해서 Custom한 AuthenticationProvider을 작성해서 AuthenticationManager에 등록하면 된다.
public interface AuthenticationProvider {
// 인증 전의 Authenticaion 객체를 받아서 인증된 Authentication 객체를 반환
Authentication authenticate(Authentication var1) throws AuthenticationException;
boolean supports(Class<?> var1);
}
- Authentication Manager
- 인증에 대한 부분은 SpringSecurity의 AuthenticatonManager를 통해서 처리하게 되는데, 실질적으로는 AuthenticationManager에 등록된 AuthenticationProvider에 의해 처리된다.
- 인증이 성공하면 2번째 생성자를 이용해 인증이 성공한(isAuthenticated=true) 객체를 생성하여 Security Context에 저장한다. 그리고 인증 상태를 유지하기 위해 세션에 보관하며, 인증이 실패한 경우에는 AuthenticationException를 발생시킨다.
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
- --> AuthenticationManager를 implements한 ProviderManager는 실제 인증 과정에 대한 로직을 가지고 있는 AuthenticaionProvider를 List로 가지고 있으며, ProividerManager는 for문을 통해 모든 provider를 조회하면서 authenticate 처리를 한다.
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
public List<AuthenticationProvider> getProviders() {
return providers;
}
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
//for문으로 모든 provider를 순회하여 처리하고 result가 나올 때까지 반복한다.
for (AuthenticationProvider provider : getProviders()) {
....
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
}
....
}
throw lastException;
}
}
- --> 위에서 설명한 ProviderManager에 우리가 직접 구현한 CustomAuthenticationProvider를 등록하는 방법은 WebSecurityConfigurerAdapter를 상속해 만든 SecurityConfig에서 할 수 있다. WebSecurityConfigurerAdapter의 상위 클래스에서는 AuthenticationManager를 가지고 있기 때문에 우리가 직접 만든 CustomAuthenticationProvider를 등록할 수 있다.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public AuthenticationManager getAuthenticationManager() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public CustomAuthenticationProvider customAuthenticationProvider() throws Exception {
return new CustomAuthenticationProvider();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthenticationProvider());
}
}
- Userdetails
- 인증에 성공하여 생성된 UserDetails 객체는 Authentication객체를 구현한 UsernamePasswordAuthenticationToken을 생성하기 위해 사용된다.
- UserDetails 인터페이스를 살펴보면 아래와 같이 정보를 반환하는 메소드를 가지고 있다. UserDetails 인터페이스의 경우 직접 개발한 UserVO 모델에 UserDetails를 implements하여 이를 처리하거나 UserDetailsVO에 UserDetails를 implements하여 처리할 수 있다.
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
- UserDetailsService
- UserDetailsService 인터페이스는 UserDetails 객체를 반환하는 단 하나의 메소드를 가지고 있는데, 일반적으로 이를 구현한 클래스의 내부에 UserRepository를 주입받아 DB와 연결하여 처리한다.
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
- Password Encoding
- AuthenticationManagerBuilder.userDetailsService().passwordEncoder() 를 통해 패스워드 암호화에 사용될 PasswordEncoder 구현체를 지정할 수 있다.
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// TODO Auto-generated method stub
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
- GrantedAuthority
- GrantAuthority는 현재 사용자(principal)가 가지고 있는 권한을 의미한다.
- ROLE_ADMIN나 ROLE_USER와 같이 ROLE_*의 형태로 사용하며, 보통 "roles" 이라고 한다.
- GrantedAuthority 객체는 UserDetailsService에 의해 불러올 수 있고, 특정 자원에 대한 권한이 있는지를 검사하여 접근 허용 여부를 결정한다.
2. OAuth
1) OAuth(Open Standard for Authorization)란?
- 개방형 Authorization 의 표준이며 API 허가(Authorize)를 목적으로 JSON 형식으로 개발된 HTTP 기반의 보안 프로토콜
- 사용자들이 사용하고자 하는 웹사이트 및 애플리케이션에 비밀번호를 제공하지 않고 접근 권한을 부여 받을 수 있게 해주는 공통적 수단으로서 사용 되어지는 기술입니다.
- 다양한 클라이언트 환경에 적합한 인증(Authentication) 및 인가(Authorization) 의 위임 방법을 제공하고 그 결과로 클라이언트에게 접근 토큰 (Access Token) 을 발급하는 것에 대한 구조다.
2) OAuth 1.0
- 인증 순서
- 소비자가 서비스 제공자에게 요청 토큰을 요청한다.
- 서비스 제공자가 소비자에게 요청 토큰을 발급해준다.
- 소비자가 사용자를 서비스 제공자로 이동시킨다. 여기서 사용자 인증이 수행
- 서비스 제공자가 사용자를 소비자로 이동시킨다.
- 소비자가 접근 토큰을 요청한다.
- 제공자가 접근 토큰을 발행한다.
- 발근된 접근 토큰을 이용하여 소비자가 사용자 정보에 접근한다.
- 고객, 고객이 이용하려는 애플리케이션, 고객 정보를 가지고 있는 애플리케이션 3개가 상호작용한다.
- 구현이 복잡한 단점이 있다.
- HMAC(keyed-hash message authentication code, hash-based message authentication code)를 통한 암호화를 하는 번거로움
- 인증토큰이 만료가 되지 않아 토큰을 만료하려면 제공자 애플리케이션의 비밀번호를 바꿔야 함
3) OAuth 2.0
- OAuth 2.0 인증 과정
- OAuth 2.0 프로세스
- 1.0 과의 차이
- 기능의 단순화, 기능과 규모의 확장성 등을 지원하기 위해 만들어졌다.
- https를 통해 암호화를 하여 과정의 단수화를 하였다.
- 다양한 인증 방식이 제공된다.
- api서버에서 인증서버와 리소스 서버가 분리됐다.
3. Test
1) 테스트의 종류
- 블랙박스 테스팅 : 소프트웨어 내부 구조나 동작원리를 모르는 블랙박스와 같은 상태에서, 즉 웹 서비스의 사용자 입장에서 동작을 검사하는 방법
- 장점
- 누구나 테스트가 가능합니다 - 개발자부터 디자이너, 베타 테스터 혹은 사장님까지!
- 단점
- 기능이 증가될 수록 테스트의 범위가 증가, 시간이 갈수록 테스트하는 사람이 계속 늘어나야함
- 테스트 하는 사람에 따라 테스트 퀄러티가 다를 수 있습니다. → QA 직군이 있는 이유
- 장점
- 개발자 테스팅 : 개발자가 직접 '본인이 작성한 코드'를 검증하기 위해 "테스트 코드"를 작성
- 장점
- 빠르고 정확한 테스트가 가능합니다. (예상 동작 VS 실제 동작)
- 테스트 자동화가 가능, 배포 절차 시 테스트 코드가 수행되어 동작 검증
- 리팩토링이나 기능 추가를 할 때 더욱 편리합니다.
- 단점
- 개발 시간이 오래 걸림
- 테스트 코드를 유지보수하는 비용
- 장점
2) 단위 테스트
- 프로그램을 작은 단위로 쪼개서 각 단위가 정확하게 동작하는지 검사하고 이를 통해 문제 발생 시 정확하게 어느 부분이 잘못되었는지를 재빨리 확인할 수 있게 해준다.
- Development : 개발
- Unit Test(단위 테스트) : 개발자 테스트
- QA Testing: 블랙박스 테스팅, 주로 QA팀이 Production 환경과 유사한 환경(stage)에서 테스팅
- Production : 실 서비스 운영 환경
3) TDD(Test-Driven-Development, 테스트 주도 개발)
- TDD는 작가가 책을 쓰는 과정과 유사하다. 책을 쓸 때는 목차를 처음 구성한다. 이후 각 목차에 맞는 내용을 구상하여 초안을 작성하고 고쳐쓰기를 반복한다. 이 과정을 TDD에 비유하면 목차구성은 “테스트코드 작성”, 초안작성은 “코드개발”, 고쳐쓰기는 “코드수정”에 해당한다. 반복적인 검토와 고쳐쓰기를 통해서 좋은 글이 완성되는 것처럼 소프트웨어도 반복적인 테스트와 수정을 통해서 고품질의 소프트웨어를 만들 수 있다.
- TDD를 하는 이유 : 불확실성이 높을 때 “피드백”과 “협력”이 중요하기 때문에 피드백과 협력이 자주 이루어진다면 더 좋은 결과가 나올 수 있다.
- TDD를 시행하면 좋은 상황
- 처음해보는 프로그램 주제-나에 대한 불확실성이 높은 경우
- 고객의 요구조건이 바뀔 수 있는 프로젝트- 외부적인 불확실성이 높은 경우
- 개발하는 중에 코드를 많이 바꿔야 된다고 생각하는 경우
- 내가 개발하고 나서 이 코드를 누가 유지보수할지 모르는 경우- 외부적인 불확실성이 즉, 불확실성이 높을 때 TDD를 하면 된다.
- TDD 개발 방식의 장점
- 보다 튼튼한 객체 지향적인 코드 생산
- 재설계 시간의 단축
- 디버깅 시간의 단축
- 테스트 문서의 대체 가능
- 추가 구현의 용이함
4) JUnit 단위 테스트
- 자바 프로그래밍 언어 용 단위 테스트 프레임 워크
- 테스트 파일을 생성하여 테스트를 하는 방식
- 주로 원하는 Entity에서 Generate를 통해 Test 생성하여 진행
- 엣지 케이스 : 알고리즘이 처리하는 데이터의 값이 알고리즘의 특성에 따른 일정한 범위를 넘을 경우에 발생하는 문제
- ex)fixnum이라는 변수의 값이 -128 ~ 127의 범위를 넘는 순간 문제가 발생하는 경우가 있을 수 있다. 어떤 분모가 0이 되는 상황처럼 데이터의 특정값에 대해 문제가 발생하는 경우도 마찬가지다.
- 알고리즘의 특성에 따라 개발자가 면밀히 검토하여 예상할 수 있는 문제다. 이런 문제는 디버그가 쉽기도 하고 테스트를 통해 미리 방지하기도 쉽다.
- 코너 케이스 : 여러 가지 변수와 환경의 복합적인 상호작용으로 발생하는 문제다.
- ex)fixnum이라는 변수의 값으로 128이 입력되었을 때, A 기계에서 테스트했을 때는 정상작동하지만 B 기계에서는 오류가 발생한다면 코너 케이스라고 할 수 있다. 같은 장치에서라도 시간이나 다른 환경에 따라 오류가 발생하기도 하고 정상작동 하기도 한다면 이것도 코너 케이스다.
- 특히 멀티코어 프로그래밍에서 만나기 쉬운 오류일 것이다.
- 코너 케이스는 오류가 발생하는 상황을 재현하기가 쉽지 않아 디버그와 테스트가 어렵다.
4. Spring AOP
- 핵심 기능 : 각 API 별 수행해야 할 비즈니스 로직
- 부가 기능 : 핵심 기능을 보조 하는 기능
- 문제점
- 모든 '핵심기능'의 Controller 에 '부가기능' 코드를 추가했을 때..
- '핵심기능' 이 100개라면??
- 100개의 '핵심기능' 모두에 동일한 내용의 코드 추가 필요
- '핵심기능' 이 100개라면??
- '핵심기능' 수정 시
- 같은 함수 내에 '핵심기능'과 '부가기능'이 섞여 있음
- '핵심기능' 이해를 위해 '부가기능'까지 이해 필요
- '부가기능'의 변경이 필요하다면??
- '핵심기능'의 개수만큼 '부가기능'도 수정해 줘야 함
- '부가기능' 삭제
- 모든 '핵심기능'의 Controller 에 '부가기능' 코드를 추가했을 때..
- AOP (Aspect Oriented Programming) 를 통해 부가기능을 모듈화
- 부가기능은 핵심기능과는 관점(Aspect), 관심이 다름
- 따라서 '핵심기능'과 분리해서 '부가기능' 중심으로 설계, 구현 가능
1) Spring AOP Annotation
- @Aspect
- 스프링 빈 (Bean) 클래스에만 적용 가능 , 포인트컷과 어드바이스의 결합이다. 어떤 포인트컷 메소드에 대해 어떤 어드바이스 메소드를 실행할지 결정한다.
- 어드바이스 종류
- @Around: '핵심기능' 수행 전과 후 (@Before + @After)
- @Before: '핵심기능' 호출 전 (ex. Client 의 입력값 Validation 수행)
- @After: '핵심기능' 수행 성공/실패 여부와 상관없이 언제나 동작 (try, catch 의 finally() 처럼 동작)
- @AfterReturning: '핵심기능' 호출 성공 시 (함수의 Return 값 사용 가능)
- @AfterThrowing: '핵심기능' 호출 실패 시. 즉, 예외 (Exception) 가 발생한 경우만 동작 (ex. 예외가 발생했을 때 개발자에게 email 이나 SMS 보냄)
- 포인트컷 : 특정 조건에 의해 필터링된 조인포인트, 수많은 조인포인트 중에 특정 메소드에서만 횡단 공통기능을 수행시키기 위해서 사용한다.
- 표현식 : 리턴타입 패키지경로 클래스명 메소드명(매개변수)
- 조인포인트 : 클라이언트가 호출하는 모든 비즈니스 메소드, 조인포인트 중에서 포인트컷되기 때문에 포인트컷의 후보로 생각할 수 있다.
- 포인트컷과 조인포인트의 사용법은 최하단 참조를 통해 이해하자
5. 트랜잭션
- 트랜잭션이란 : 데이터베이스에서 데이터에 대한 하나의 논리적 실행단계, ACID (원자성, 일관성, 고립성, 지속성)는 데이터베이스 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 성질을 가리키는 약어
- 트랜잭션의 특징
- 더 이상 쪼갤 수 없는 최소단위의 작업입니다.
- 하나의 최소 단위의 작업에 여러가지 데이터 변경을 넣으면, 모두 저장되거나, 아무 것도 저장되지 않거나를 보장합니다.
- 좋은 예시 : 아래 예시 단계에서 도중에 에러가 난다면 그저 A계좌만 손해가 날 것이다. 하지만 모든 단계를 하나의 트랜잭션으로 묶어 모두 성공시 트랜잭션 Commit, 중간에 하나라도 실패시 트랜잭션 Rollback으로 문제를 해결한다.
- A계좌 잔고 200원이상 확인
- A계좌 잔고 200원 금액 감소
- B계좌 잔고 200원 증가
- Primary와 Replica
- Primary : 쓰기 전용이다. Write된 Data(CUD)가 Replica로 Sync 된다.(Replication)
- Replica : 읽기 전용(readOnly = true)
- Primary 중 1개에 문제가 생겼으 때 Replica중 1개가 primary가 된다.
6. 참조
1) Spring Security란? : https://mangkyu.tistory.com/76
2)OAuth 1.0과 2.0 그리고 인증과 토큰의 종류 : https://velog.io/@hyg8702/OAuth%EB%9E%80-OAuth1-vs-OAuth2
3)TDD의 특징과 장단점 : http://clipsoft.co.kr/wp/blog/tddtest-driven-development-%EB%B0%A9%EB%B2%95%EB%A1%A0/
4)포인트컷과 조인포인트 : https://sjh836.tistory.com/157