- build.gradle 설정
plugins {
id 'org.springframework.boot' version '2.3.3.RELEASE'
id 'io.spring.dependency-management' version '1.0.10.RELEASE'
id 'java'
}
group = 'kr.seok'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
// https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity5
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.0.4.RELEASE'
// https://mvnrepository.com/artifact/org.modelmapper/modelmapper
implementation 'org.modelmapper:modelmapper:2.3.0'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// runtimeOnly 'org.postgresql:postgresql'
/* h2 */
implementation 'com.h2database:h2'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
testImplementation 'org.springframework.security:spring-security-test'
}
test {
useJUnitPlatform()
}
- SecurityConfig 설정
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/* Password Encode */
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
/* AuthenticationManagerBuilder InMemoryAuthentication 등록 */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
String password = passwordEncoder().encode("1234");
auth.inMemoryAuthentication().withUser("user").password(password).roles("USER");
auth.inMemoryAuthentication().withUser("manager").password(password).roles("MANAGER");
auth.inMemoryAuthentication().withUser("admin").password(password).roles("ADMIN");
}
/* HttpSecurity Authentication AntMatcher */
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/mypage").hasRole("USER")
.antMatchers("/messages").hasRole("MANAGER")
.antMatchers("/config").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
;
}
}
- SecurityConfig 설정
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/* Password Encode */
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
/* static resource security 에서 제외 처리 */
@Override
public void configure(WebSecurity web) {
web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
}
/* AuthenticationManagerBuilder InMemoryAuthentication 등록 */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
String password = passwordEncoder().encode("1234");
auth.inMemoryAuthentication().withUser("user").password(password).roles("USER");
auth.inMemoryAuthentication().withUser("manager").password(password).roles("MANAGER");
auth.inMemoryAuthentication().withUser("admin").password(password).roles("ADMIN");
}
/* HttpSecurity Authentication AntMatcher */
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/mypage").hasRole("USER")
.antMatchers("/messages").hasRole("MANAGER")
.antMatchers("/config").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
;
}
}
-
PasswordEncoder
- 비밀번호를 안전하게 암호화 하도록 제공
- SpringSecurity 5.0 이전에는 기본 PasswordEncoder 가 평문을 지원하는 NoOpPasswordEncoder (현재는 Deprecated)
-
생성
- PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
- 여러 PasswordEncoder 유형을 선언한 뒤, 상황에 맞게 선택해서 사용할 수 있도록 지원하는 Encoder이다.
-
암호화 포맷: {id}encodedPassword
- 알고리즘 종류: bcrypt, noop, pbkdf2, scrypt, sha256 (기본 포맷은 Bcrypt : {bcrypt}
-
인터페이스
- encode(password)
- 패스워드 암호화
- matches(rawPassword: 원래 비밀번호, encodedPassword: 암호화된 비밀번호)
- 패스워드 비교
- encode(password)
-
사용자 등록기능 구현 시나리오
- PasswordEncoder를 사용하여 비밀번호 암호화 및 DB 저장
-
사용자 로그인을 위한 domain, service, controller viewTemplate 생성
-
사용자 등록을 위한 domain(Account) 생성
-
사용자의 등록 요청을 위한 domain(AccountDto) 생성
-
사용자 등록 requestMapping을 위한 controller method 생성
- password 암호화를 위한 PasswordEncoder 생성 및 encode 추가
-
사용자 Entity 를 저장하는 Service, Repository 생성
-
사용자 회원가입처리를 위한 View 생성
-
회원가입 view에 접근하기 위한 URL Path에 대해서 permitAll 설정
-
-
UserDetailsService의 역할
- 사용자 정보 및 권한 정보 DB 저장
- 사용자 조회 인증 처리
-
UserDetailsService 시나리오 Flow
- 사용자의 정보를 DB에 저장
- DB에 등록된 사용자의 정보로 로그인 시 UserDetailsService 메서드 loadUserByUsername 의 진행 절차 확인
-
사용자 DB 관리를 위한 기존의 인 메모리 사용자 설정 삭제
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
String password = passwordEncoder().encode("1234");
auth.inMemoryAuthentication().withUser("user").password(password).roles("USER");
auth.inMemoryAuthentication().withUser("manager").password(password).roles("MANAGER");
auth.inMemoryAuthentication().withUser("admin").password(password).roles("ADMIN");
}
- AuthenticationProvider
- DB를 통한 사용자 정보의 유효성을 검증(password) 받은 UserDetails를 인증 토큰으로 생성하여 반환하는 역할을 하는 사용자 정의 클래스
- 시큐리티에서 제공하는 로그인 페이지를 연결
- 커스텀한 페이지에서 로그아웃 페이지에 대한 링크를 연결
- 로그아웃 된 사용자의 세션 정보를 비우고 로그인 페이지로 redirect
- 인증 부가 기능
- WebAuthenticationDetails
- 사용자의 요청 정보 중 인증에 필요한 값 외에 추가적인 파라미터를 Authentication
- AuthenticationDetailsSource
- WebAuthenticationDetails 클래스를 생성하는 클래스
- WebAuthenticationDetails
- 인증 성공 시 redirect 처리
- default Url 설정
- 사용자의 인증 성공 후 기본으로 설정되어 있는 url로 이동 (이전에 resource 요청이 없는 경우)
- RequestCache Url 설정
- 사용자의 이전 요청 정보를 세션에 저장하였다가 인증에 성공하는 경우 세션에 저장된 resource 요청 정보(url)를 기억하고 있다가 redirect
- default Url 설정
- 인증 실패 시 처리 방법을 정의
- 비밀번호아 맞지 않는 예외 발생 시
- 비밀번호 외에 사용자의 추가 파라미터 요청정보가 잘못된 경우
- 예외 발생시 나타낼 페이지 또는 resource를 정의하여 반환가능
- 인가 거부 시 예외 발생과 인가 예외 페이지 이동 Handler 작성
- 인가 거부에 대한 응답 메시지를 페이지로 전달할 수 있도록 Controller 작성
-
Ajax 인증과 Form 인증의 전반적인 처리과정은 비슷하다.
-
동일하게 Filter 기반으로 인증처리를 수행한다.
-
필터가 사용하는 각각의 클래스도 역할이 차이가 있을 뿐 전반적인 Flow는 동일하다.
-
Form 인증은 동기적인 방식으로 처리되고 Ajax 인증은 비동기 방식으로 처리된다.
-
인증 Flow
- AjaxAuthenticationFilter
- 비동기 인증처리 필터
- AjaxAuthenticationToken
- 사용자의 인증처리 관련 파라미터로 인증 토큰을 생성하기 위한 클래스
- AuthenticationManager
- AjaxAuthenticationFilter가 인증 토큰을 전달하여 인증 처리를 위임
- AjaxAuthenticationProvider
- 실 인증 처리를 담당하는 클래스
- UserDetailsService
- DB 내에 저장된 사용자의 조회하는 역할
- AjaxAuthenticationSuccessHandler
- 인증 성공 시 처리
- AjaxAuthenticationFailureHandler
- 인증 실패 시 처리
- AjaxAuthenticationFilter
-
인가 Flow
-
FilterSecurityInterceptor
- AuthenticationException
- 인증 예외
- AccessDeniedException
- 인가 예외
- AuthenticationException
-
ExceptionTranslationFilter
- AjaxAuthenticationEntryPoint
- 인증 실패 시 다시 로그인 페이지로 리다이렉트 처리
- AjaxAccessDeniedHandler
- 인가 실패 시 접근 불가 페이지 노출
- AjaxAuthenticationEntryPoint
-
-
Ajax 요청에 대한 인증 처리 가능한 Filter
- AjaxFilter를 생성하는 방법
- AbstractAuthenticationProcessingFilter
- Form 인증 처리하는 클래스도 해당 추상 클래스를 상속받음
- AjaxFilter의 작동 조건
- AntPathRequestMatcher("/api/login")로 요청정보와 매칭하고 요청 방식이 Ajax이면 필터 적용
- AjaxFilter 처리 방식
- AjaxAuthenticationToken을 생성하여 AuthenticationManager에게 전달하여 인증처리
- Ajax Filter를 FilterChain에 추가하는 방식
- http.addFilterBefore(AjaxAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
- AjaxFilter를 생성하는 방법
-
Custom Filter 등록 코드 작성 요약
- SecurityConfig
- authenticationManagerBean 재정의를 통해 AuthenticationManager 등록
- ajaxLoginProcessingFilter Bean 등록
- Custom 한 AjaxLoginProcessingFilter에 AuthenticationManager 설정
- configure 설정으로 addFilterBefore())
- Custom한 ajaxLoginProcessingFilter를 UsernamePasswordAuthenticationFilter 전에 실행 될 수 있도록 설정
- SecurityConfig
-
Ajax 인증 요청에 대해 실제 인증처리를 수행하는 클래스
-
Filter에서 특정 인증 요청 resource에 접근하여 요청한 request 정보를 인증 토큰으로 생성하여 AuthenticationManager에게 전달
-
AuthenticationManager는 인증 토큰 처리가 가능한 Provider를 찾아 Authentication 인증 객체를 전달
-
인증 처리 메서드 authenticate()
- 사용자 정의된 UserDetailsService 를 통해 DB에서 Username에 해당하는 Entity를 조회
- UserDetailsService
- 조회된 사용자의 정보를 User 클래스를 상속받은 사용자 정의 클래스 형태로 반환 (User 클래스는 UserDetails 인터페이스의 구현체)
- username, password의 유효성 검증 처리
- PasswordEncoder
- Password를 암호화 할 때 현재 프로젝트에서는 Bcrypt 방식을 사용
- 검증된 사용자의 요청값으로 AuthenticationToken을 생성
- AjaxAuthenticationToken
- UsernamePasswordAuthenticationToken 과 같은 방식으로 Token 생성하기 위해 그대로 생성
- 두 개의 생성자가 존재
- 사용자의 request값으로 생성되어 provider 쪽으로 전달하기 위한 생성자
- Provider를 통해 username, password 값의 유효성이 검증되어 UserDetails credentials, authorities 등을 저장할 생성자
-
-
Provider가 실행되기 위한 사전 정보
- Entity로 사용될 Account 클래스
- Account DB 조회 JpaRepository 인터페이스 구현 UserRepository
- UserDetailsService를 인터페이스를 구현한 CustomUserDetailsService
- Ajax 방식의 인증 성공 후 처리
- 인증 성공 status 값, ContentType, 인증 성공한 객체 값 반환
- Ajax 방식의 인증 실패 후 처리
- 계정 여부 및 비밀번호 검증
- 계정 사용 여부
- 비밀번호 기한 만료 처리
-
FilterSecurityInterceptor
- 인가 처리를 하는 필터 클래스
- 익명 사용자가 접근하는 경우
- 인가 예외 발생하는 경우
- AuthenticationEntryPoint
- 정상 인가 처리
- AccessDeniedException
- 인가 처리를 하는 필터 클래스
-
시나리오
- 인증되지 않은 사용자의 접근
- 인증이 되었으나 인가되지 않은 사용자의 접근
- 인증 & 인가가 정상처리된 사용자의 접근
- AbstractHttpConfigurer
- 시큐리티 초기화 설정 클래스
- 필터, 핸들러, 메서드, 속성 등을 한 곳에 정의하여 처리할 수 있는 편리함 제공
- public void init(H http) throws Exception - 초기화
- public void configure(H http) - 설정
-
시나리오
-
인증되지 않은 사용자의 페이지 접근
- 인증되지 않은 사용자의 접근 시, 인증 예외 발생하여 로그인 페이지로 리다이렉트
-
인증은 되었으나 인가되지 않은 자원에 접근
- 권한이 부족한 사용자는 인가 예외로 권한 부족에 대한 메시지 노출
-
-
AjaxFilter가 추가된 FilterChain 0 = {WebAsyncManagerIntegrationFilter@12087} 1 = {SecurityContextPersistenceFilter@12088} 2 = {HeaderWriterFilter@12089} 3 = {CsrfFilter@12090} 4 = {LogoutFilter@12091} 5 = {AjaxLoginProcessingFilter@12092} 6 = {RequestCacheAwareFilter@12093} 7 = {SecurityContextHolderAwareRequestFilter@12094} 8 = {AnonymousAuthenticationFilter@12095} 9 = {SessionManagementFilter@12096} 10 = {ExceptionTranslationFilter@12097} 11 = {FilterSecurityInterceptor@12098}