스프링 시큐리티 공부
사실 프로젝트를 계속하면서 시큐리티를 해야지 해야지 했는데… 제대로 하질 못했던것 같아서 이번에 제대로 하려고 정리하려한다.
그래서 스프링 시큐리티 인 액션 이라는 책의 예제를 바탕으로 까먹지 않도록 계속해서 핵심 내용만 정리하려고 한다.
책을 다 때기까지 화이팅이다!!!
cURL
- 우리는 HTTP 테스트를 postman등으로 하는 경우도 있지만, 리눅스 혹은 mac 환경이라면
curl
을 통해서도 확인을 할 수 있다. - 예제에서 현재는
HelloController
에 다음과 같이 코드를 짠경우입니다.- 의존성으로는, Spring-web과 Spring-security를 넣었습니다.
package com.example.example1;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello!";
}
}
- 이후 실행하고 확인을 하려고한다면 다음과 같은 명령어로 확인이 가능합니다.
curl -v http://localhost:8080/hello
- 확인을 해보면 401 이 반환이 된것을 알 수 있습니다. 이는
Unauthorized
입니다. - 이후 스프링에서 나온 값과 함게
-u
attribute를 통해서 접근을 한다면 인증이 됩니다.
-u
를 사용한다면,<username>:<password>
문자열을 Base64로 인코딩을 하고 Basic이 붙은Authorization
헤더의 값으로 보냅니다.
스프링 시큐리티의 인증 프로세스
- 우리는 다음 그림을 통해서 다음과 같은 사실을 알 수 있습니다.
- 인증 필터는 인증 요청을 인증 관리자에게 위임하고 응답을 바탕으로 보안 컨텍스트를 구성합니다/
- 인증 관리자는 인증 공급자를 이용해 인증을 처리합니다
- 인증 공급자는 인증 논리를 구현합니다
- 인증 공급자는 사용자 관리 책임을 구현하는 사용자 세부 정보 서비스를 인증 논리에 이용합니다.
- 인증 공급자는 암호 관리를 구현하는 암호 인코더를 인증 논리에 이용합니다.
- 보안 컨텍스트는 인증 프로세스 후 인증 데이터를 유지합니다.
→ 우리는 위오아 같은 예시에서 자동으로 구성되는 UserDetailService
, PasswordEncoder
라는 빈이 생깁니다.
PasswordEncoder
- 이러한
passwordEncoder
는 다음과 같은 일을 합니다.- 암호를 인코딩합니다.
- 암호가 기존 인코딩과 일치하는지를 확인합니다.
- 이때
AuthenticationProvider
는 인증 논리를 정의하고 사용자와 암호의 관리를 위임합니다.- 이는
UserDetailsService
,PsswordEncoder
에 제공된 기본 구현을 이용해서 구현이 되어있습니다.
- 이는
기본 구성 재정의
- 이제 기본적으로 구성되어있던,
UserDetailService
와PasswordEncoder
에 대해서 재정의를 해서 새로운 방법으로 구성하는 방법을 공부할것 입니다.
UserDetailsService 재정의
- 이를 재정의 하기 위해서 우선
InMemoryUserDetailManager
을 이용해서 구현을 해봅니다.
package com.example.example1;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
public class ProjectConfig {
@Bean
public UserDetailsService userDetailsService() {
var userDetailsService = new InMemoryUserDetailsManager();
var user = User.withUsername("haechan")
.password("12345")
.authorities("read") // 권한 목록
.build();
userDetailsService.createUser(user);
return userDetailsService;
}
}
- 우리는 새로운 haechan이라는 이름을 가진 유저를 read 권한을 가지도록 생성을 했습니다.
- 이후
createUser
를 통해서 유저를 생성합니다. - 이대로만 실행하면
PasswordEncoder
가 비어있으므로, 이도 재정의 해야합니다
- 이후
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
- 이때 사용하는
NoOpPasswordEncoder
는 그냥 암호화하지 않고 기본적인 텍스트로 문자열비교만하는것입니다.
- 이제 값이 제대로 나오게 됩니다.
엔드포인트 권한 부여로 구성 재정의
- 앞에서는 user를 생성하고 password를 encoding을 해서 인증을 통과하는 방법을 배웠습니다.
- 이번에는 엔드포인트 권한을 통해서 어떤 엔드포인트에 접근할 때 인증을 허용하는 방법을 배워보겠습니다.
- 우리는 이를 통해
WebSecurityConfigureAdapter
클래스를 확장해서configure(HttpSecurity http)
메서드를 재정의해 권한을 허용시킨다- 하지만 현재는
@Deprecated
되어있는 것을 알 수 있다.
- 하지만 현재는
- 이를 해결하기 위해서는 이제,
SecurityFilterChain
을 Bean으로 등록해서 사용을 하도록 변경이 되었습니다.
@Configuration
public class ProjectConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.httpBasic(Customizer.withDefaults())
.authorizeRequests((authz) -> authz
.anyRequest().permitAll());
return http.build();
}
}
- 이후 실행을 하면,
permitAll()
을 해놓았기 때문에 어떤 요청에도 다음과 같이 자격증명없이 모든 엔드포인트를 접근할 수 있는 것을 볼 수 있습니다.
다른 방법으로 구성을 설정하는 법
- 우리는 위에서
UserDetailsService
와PsswordEncoder
를 재정의 했었는데, 이에 대해서 ㄱ동일한 구성을 수행하는 다른 방법도 존재합니다. - 두 객체를 빈으로 정의하지 않고 원래는
WebSecurityConfigurerAdapter
클래스를 상속받아configure(AuthenticationManagerBuilder
로 설정이 가능합니다.- 하지만, 현재는 Deprecate 되어있는 상황이라 다음과 같은 코드를 다믕ㅁ으로 변경을 합니다.
/*** 수정 전 **/
package com.example.example1;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
var userDetailsService = new InMemoryUserDetailsManager(); // 사용자를 메모리에 저장하기 위한 방식
var user = User.withUsername("haechan")
.password("12345")
.authorities("read")
.build();
userDetailsService.createUser(user); // 사용자를 추가함
auth.userDetailsService(userDetailsService) // configure 메서드에서 UserdetailsService와 PasswordEncoder를 설정
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
}
/** 수정 후 **/
package com.example.example1;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
public class ProjectConfig {
@Bean
public InMemoryUserDetailsManager userDetailsService() {
var user = User.withUsername("haechan")
.password("{noop}12345")
.authorities("read")
.build();
return new InMemoryUserDetailsManager(user);
}
}
- 다음 링크를 참고하면, 변경된 사항이 존재한다.
- 비밀번호는
{noop}
을 넣어서 작성하면NoOpPasswordEncoder
로 사용이 가능합니다.
- 비밀번호는
/** 최종 코드 **/
package com.example.example1;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class ProjectConfig {
@Bean
public InMemoryUserDetailsManager userDetailsService() {
var user = User.withUsername("haechan")
.password("{noop}12345")
.authorities("read")
.build();
return new InMemoryUserDetailsManager(user);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests((authz) -> authz.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults());
return http.build();
}
}
- 우리는 현재 인-메모리 방식으로 사용자를 구성하였지만, 보통은 데이터베이스에 저장하거나 다른 시스템을 가져와야합니다.
- 결론적으로 이 방법은 권장하지 않습니다
AuthenticationProvider 구현 재정의
- 이번은 이 그림에서 인증 관리자를 재정의해서 이번에는 구현해보려고합니다.
- 우리가 재정의할
AuthenticationProvider
는 인증 논리를 구현하며,AuthenticationManager
에서 요청을 받고 사용자를 찾는 것을UserDetailsService
에서 암호검증을PasswordEncoder
에 위임을합니다.
package com.example.example1;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = String.valueOf(authentication.getCredentials());
if ("haechan".equals(username) && "12345".equals(password)) {
return new UsernamePasswordAuthenticationToken(username, password, Arrays.asList());
}
throw new AuthenticationCredentialsNotFoundException("authentication에서 오류가 발생했습니다");
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication);
}
}
- 우리는 다음과 같이, “haechan”, “12345”를 username과 password로 받으면, 새로운
UsernamePasswordAuthenticationToken
을 생성하고 반환을 합니다. - 이후 커스텀하는 Configuration은 다음과 같이 작성을 합니다. (스프링 시큐리티 5부터 변경되었습니다.)
package com.example.example1;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@Configuration
@EnableWebSecurity
public class ProjectConfig {
@Autowired
private CustomAuthenticationProvider authenticationProvider;
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder =
http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.authenticationProvider(authenticationProvider);
return authenticationManagerBuilder.build();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests((authz) -> authz.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults());
return http.build();
}
}
- 이 또한, 제대로 되는 것을 볼 수 있습니다.
프로젝트에 여러 구성 클래스 이용
- 우리는 지금까지 하나의 구성 클래스만 사용을 했지만, 구성 클래스도 책임을 분리하는 것이 좋습니다.
- 이제
UserManagementConfig
와WebAuthorizationConfig
로 나누어서 환경을 구성할 예정입니다UserManagementConfig
는UserDetailsService
및PasswordEncoder
의 Bean만 포함을 합니다.- 이는, WebSecurityConfigureAdapter를 확장할 수 있기 때문에 두 객체를 빈으로 구성을 했습니다.
- (현재는
WebSecurityConfigureAdapater
를 상속받지 않습니다. Security 5부터)
- (현재는
package com.example.example1;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
public class UserManagementConfig {
@Bean
public InMemoryUserDetailsManager userDetailsManager() {
var user = User.withUsername("haechan")
.password("{noop}12345")
.authorities("read")
.build();
return new InMemoryUserDetailsManager(user);
}
}
package com.example.example1;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class WebAuthorizationConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests((authz) -> authz.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults());
return http.build();
}
}
Reference
- 스프링 시큐리티 인 액션 - 로렌티우 스필카
'Spring > 스프링 시큐리티' 카테고리의 다른 글
[스프링 시큐리티] 소신대로 Spring Security JWT 방식 구현하기 (1) (0) | 2023.03.27 |
---|---|
스프링 시큐리티 인 액션 (2) (1) | 2023.01.31 |