반응형
보안 점검을 진행하면서 사용자 인증 관련 부분의 취약점을 보완하면서 좀더 깊게 공부하고자 하여 작성하였다.
<참고 : YoonHwan Kim님의 블로그>
프로젝트 환경
더보기
프로젝트 환경
spring boot : 3.2.2
spring security : 6.2.1
java : 17
database : h2
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.2'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'com.together'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
application.properties
spring.h2.console.enabled=true
spring.h2.console.path=/h2
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
코드 변화
Security를 사용할때 H2를 그냥 사용하면 iframe에 대한 접근이 막혀 안되고 CSRF와 SameOrigin 정책을 허용시켜야 한다고 한다.
해당 부분이 .csrf와 .headers의 내용이다.
또한 Spring Security 6.1이전 버전에서는 antMatches(), mvcMatchers()가 사용 가능했지만 이후 버전에서는 requestMatchers()를 사용하도록 바뀌었다. 변경하면서 Lambda DSL로 변경을 같이 하면 가독성이 높아진다.
AntPathRequestMatcher클래스를 사용하지 않는다면 filterChain에서 exception을 던지는 경우도 많다고 한다. 이를 방지하기 위해 AntPathRequestMatcher클래스를 사용했다.
해당 부분이 .authorizeHttpRequests부분이다.
수정사항
이제 내부적으로 인증을 하는 프로세스를 작성할 것이다.(이 글을 쓰게된 이유...)
Login인증 필터
public class LoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public LoginAuthenticationFilter(final String defaultFilterProcessesUrl,
final AuthenticationManager authenticationManager) {
super(defaultFilterProcessesUrl, authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
String method = request.getMethod();
if (!method.equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
ServletInputStream inputStream = request.getInputStream();
LoginRequestDto loginRequestDto = new ObjectMapper().readValue(inputStream, LoginRequestDto.class);
return this.getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(
loginRequestDto.username,
loginRequestDto.password
));
}
// json에서 email로 오는 값을 username으로 처리
public record LoginRequestDto(@JsonProperty("email") String username, String password){
}
}
위의 코드 중 아래에 있는 부분이 인증을 시도하는 부분이다.
...
// AuthenticationManager : ProviderManager의 인스턴스 -> Provider 순회 인증시도
return this.getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(
loginRequestDto.username,
loginRequestDto.password
));
...
AuthenticationManager (보통 ProviderManager의 인스턴스)는 등록된 AuthenticationProvider들을 순회하며 인증 시도
일반적인 Spring Security 구성에서는 DaoAuthenticationProvider가 사용되며, 이는 UserDetailsService를 통해 사용자 정보를 불러와 제출된 인증 정보와 비교
따라서, 사용자 정보의 데이터베이스 비교 로직을 찾고자 한다면 UserDetailsService 구현 확인 필요
UserDetailsService는 보통 loadUserByUsername 메서드를 구현하여, 제공된 사용자명 (이 경우 username)을 사용해 데이터베이스에서 사용자 정보를 조회하고, 이 정보를 기반으로 UserDetails 객체를 생성 후 반환
CustomUserDetailService & CustomUserDetailServiceImpl 구현
@Service
@RequiredArgsConstructor
public class CustomUserDetailServiceImpl implements UserDetailsService {
private final MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String username) {
Member member = memberRepository.selectMember(username);
return User.withUsername(member.getEmail())
.password("{noop}"+member.getPassword())
.roles(String.valueOf(member.getMemberAuth()))
.build();
}
}
public interface CustomUserDetailService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
Config파일 수정
private final AuthenticationConfiguration authenticationConfiguration;
public SecurityConfig(final AuthenticationConfiguration authenticationConfiguration) {
this.authenticationConfiguration = authenticationConfiguration;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 추가된 코드
//AuthenticationManagerBuilder sharedObject = http.getSharedObject(AuthenticationManagerBuilder.class);
//AuthenticationManager authenticationManager = sharedObject.build();
AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();
http.authenticationManager(authenticationManager);
http
.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorizeRequest ->
authorizeRequest
.requestMatchers(new AntPathRequestMatcher("/login")).authenticated()
.requestMatchers(new AntPathRequestMatcher("/h2/**")).permitAll()
)
// 추가된 코드 (필터 변경하기)
.addFilterAt(
this.abstractAuthenticationProcessingFilter(authenticationManager),
UsernamePasswordAuthenticationFilter.class).headers(
headersConfigurer -> headersConfigurer.frameOptions(
HeadersConfigurer.FrameOptionsConfig::sameOrigin
).contentSecurityPolicy( policyConfig ->
policyConfig.policyDirectives(
"script-src 'self'; img-src 'self'; font-src 'self' data:; default-src 'self'; frame-src 'self'"
)
)
);
return http.build();
}
// 앞서 생성한 로그인 필터 호출
public AbstractAuthenticationProcessingFilter abstractAuthenticationProcessingFilter(
final AuthenticationManager authenticationManager) {
return new LoginAuthenticationFilter("/login", authenticationManager); // "/login"에 접속했을 때
}
반응형
'Backend > Spring | SpringBoot' 카테고리의 다른 글
[Spring Security] 인증 중 발생에러 (0) | 2024.03.21 |
---|---|
[Spring Security] CSP위반 에러 (0) | 2024.03.21 |
[Spring Security] 인증 표현식 (0) | 2024.03.20 |
[Spring Security] Survlet 기반 Application 아키텍쳐(2) (0) | 2024.03.19 |
[Spring Security] Survlet 기반 Application 아키텍쳐(1) (0) | 2024.03.19 |