이 포스팅에서는 스프링 부트 3.2.2 버전을 사용하고, 스프링 시큐리티 6.2.1 버전을 사용합니다.
이번에 유엠씨 프로젝트를 진행하며 회원가입/로그인 파트를 진행하게 되었습니다. 이번 기회에 스프링 시큐리티에 대해 낱낱히 파헤쳐보고 앞으로 자유자재로 사용할 수 있도록 해보려고 합니다.
긴 시리즈가 될거같은 기분이 벌써 듭니다. 내용이 어렵다 보니 마음의 준비를 하고 보시는걸 추천합니다.
먼저, 기본 개념에 대해 정리해둔 글이 있습니다.여기에 제가 간단하게 개념을 정리해두었습니다. 먼저 보고 오면 이해가 더 잘될 것입니다.
직접 구현해보기에 앞서, 개념을 다시 한번 간단하게 복습해보겠습니다.
스프링 시큐리티는 스프링 기반의 애플리케이션 보안(인증, 인가, 권한)을 담당하는 스프링 하위 프레임워크입니다. 보안 옵션을 많이 제공해주고 복잡한 로직 없이도 어노테이션으로도 설정이 가능합니다. 여러 보안 위협 방어 및 요청 헤더도 보안처리를 해줍니다. 기본적으로 스프링 시큐리티는 세션 기반 인증을 제공합니다.
인증은 사용자의 신원을 입증하는 과정입니다. 쉽게 말하면 어떤 사이트에 아이디와 비밀번호를 입력하여 로그인 하는 과정입니다.
'권한부여'나 '허가'와 같은 의미로 사용됩니다. 즉, 어떤 대상이 특정 목적을 실현하도록 허용(Access) 하는 것을 의미합니다. 예를 들면, 파일 공유 시스템에서 권한별로 접근할 수 있는 폴더가 상이합니다. 관리자는 접속이 가능하지만 일반 사용자는 접속할 수 없는 경우에서 사용자의 권한을 확인하게 되는데, 이 과정을 인가라고 합니다.
스프링 시큐리티는 필터 기반으로 동작합니다.
HTTPServletRequest
에 아이디, 비밀번호 정보가 전달됩니다. 이때 AuthenticationFilter
가 넘어온 아이디와 비밀번호의 유효성 검사를 실시하게 됩니다.UsernamePasswordAuthenticationToken
을 만들어 넘겨줍니다.UsernamePasswordAuthenticationToken
을 AuthenticationManager
에게 전달합니다.UsernamePasswordAuthenticationToken
을 AuthenticationProvider
에게 전달합니다.UserDetailsService
로 보냅니다. UserDetailService
는 사용자 아이디로 찾은 사용자의 정보를 UserDetails
객체로 만들어 AuthenticationProvider
에게 전달합니다.UserDetails
의 정보를 비교해 실제 인증 처리를 진행합니다.SecurityContextHolder
에 Authentication
을 저장합니다. 인증 성공 여부에 따라 성공 시 AuthenticationSuccessHandler
, 실패 시 AuthenticationFailureHandler
핸들러를 실행합니다.위에 언급하였듯이 스프링 부트 3.2.2 버전을 사용하고, 스프링 시큐리티 6.2.1 버전을 사용합니다. 그래서 다른 분들이 해놓은 글이 많이 없어 공식 문서를 참고하여 해보았습니다. 그러다 보니 틀린 부분이 있을 수 있는데, 있다면 적극적인 피드백 부탁드립니다!!
스프링 시큐리티를 사용하기 위해 필요한 의존성입니다. build.gradle
에 추가하시면 됩니다.
//== 스프링 시큐리티 ==//
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
testImplementation 'org.springframework.security:spring-security-test'
유저 엔티티를 먼저 구현하였습니다.
@Entity
@Getter
@Builder
@AllArgsConstructor
@Table(name = "USERS")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Users {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false, unique = true, length = 30)
private String email;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String phoneNum;
private String imgUrl;
@Enumerated(EnumType.STRING)
@JsonIgnore
private PublicStatus publicStatus;
@JsonIgnore
@Enumerated(EnumType.STRING)
private ShareStatus shareStatus;
private LocalDate createdAt;
// @JsonIgnore
// @OneToMany(mappedBy = "users")
// private List<SharedAlbum> sharedAlbums = new ArrayList<>();
}
위와 같이 기본적인 필드로 구성을 하였고, 아직 다른 엔티티가 구현이 완료된 상태가 아니라서 연관관계 매핑 부분은 주석처리를 해놓았습니다.
앞으로 인증을 진행할때, 아이디는 이메일로 진행할 예정입니다.
email과 password의 조합입니다.
먼저 config 패키지에 SecurityConfig
라는 시큐리티 설정 파일을 만들어 줍니다. 필요한 @bean
들을 추가해 사용할 수 있습니다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final UserDetailsServiceImpl userDetailsService;
private final ObjectMapper objectMapper;
// 스프링 시큐리티 기능 비활성화 (H2 DB 접근을 위해)
// @Bean
// public WebSecurityCustomizer configure() {
// return (web -> web.ignoring()
// .requestMatchers(toH2Console())
// .requestMatchers("/h2-console/**")
// );
// }
// 특정 HTTP 요청에 대한 웹 기반 보안 구성
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http .csrf(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/signup", "/", "/login").permitAll()
.anyRequest().authenticated())
// 폼 로그인은 현재 사용하지 않음
// .formLogin(formLogin -> formLogin
// .loginPage("/login")
// .defaultSuccessUrl("/home"))
.logout((logout) -> logout
.logoutSuccessUrl("/login")
.invalidateHttpSession(true))
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
Configure()
: 스프링 시큐리티의 모든 기능(인증, 인가 등)을 사용하지 않게 설정
filterChain()
: 특정 Http 요청에 대해 웹 기반 보안 구성. 인증/인가 및 로그아웃을 설정한다.
csrf(Cross site Request forgery) : 공격자가 인증된 브라우저에 저장된 쿠키의 세션 정보를 활용하여 웹 서버에 사용자가 의도하지 않은 요청을 전달하는 것. 즉, 정상적인 사용자가 의도치 않은 위조요청을 보내는 것을 의미한다.
HttpBasic(), FormLogin() : Json을 통해 로그인을 진행하는데, 로그인 이후 refresh 토큰이 만료되기 전까지 토큰을 통한 인증을 진행할것 이기 때문에 비활성화 하였습니다.
authorizeHttpRequests() : 인증, 인가가 필요한 URL 지정
.hasAuthority(UserRole.ADMIN.name())
와 같이 사용 가능formLogin() : Form Login 방식 적용
logout() : 로그아웃에 대한 정보
sessionManagement() : 세션 생성 및 사용여부에 대한 정책 설정
로그인(/login), 회원가입 (/signup), 메인 페이지(/)에 대해서는 인증 없이도 접근할 수 있게 만들었습니다.
기존의 많은 분들은 스프링 2.X 버전을 사용중이실텐데요, 스프링 3.0 이상의 버전부터는 스프링 시큐리티 버전도 바뀌어서 기존의 Configuration과는 다르게 작성해야합니다.
WebSecurity, HttpSecurity 모두 큰 변화를 맞이 했는데, 그중 하나가 lambdas 형식의 작성법인거 같습니다.
위에 Http 설정의 구성을 보면 알 수 있듯이, 비활성화를 위해 csrf().disable()
같은 형식으로 사용 했었던 과거와 비교하여, 파라미터로 함수를 전달(.csrf(AbstractHttpConfigurer::disable)
)하는 것을 볼 수있습니다.
formLogin()
와 같은 설정의 경우도 마찬가지로 기존에 formLogin().loginPage()
형식으로 사용했다면 이제는 람다식을 파라미터로 전달하여 아래와 같이 사용합니다.
.formLogin(formLogin -> formLogin
.loginPage("/login")
.defaultSuccessUrl("/home"))
Spring Security 간단한 복습과 설정만 봤는데도 내용이 너무 길어졌습니다. 다음 포스팅에 이어서 비밀번호 암호화 및 동작 원리에 대해 쭉 알아보도록 하겠습니다.
글 솜씨가 없어 두서 없이 작성한 긴 글 보시느라 고생 많으셨고, 감사합니다.
참고 사이트
formlogin 말고 json을 통한 로그인 과정에 대한 내용이 궁금했는데 덕분에 잘 읽어보겠습니다!