# 개요
이전 글에서 인증/인가를 구현하기 위해 가장 기본적인 User Entity, 회원가입 API, 로그인 API를 구현했다.
이번 글에서는 본격적으로 Spring Security를 도입한 과정을 작성해보겠다.
선 요약
- Spring Security 라이브러리 추가
- Spring Security Config 작성
- formLogin disable
- csrf disable
- api 및 사용할 url permitAll으로 등록
- h2 console을 위한 x-frame-options 설정
# Session vs Token
팀 프로젝트 회의를 하면서 내가 Session 방식 대신에 Token 방식을 사용해보자는 제안을 했다. 물론 어짜피 내가 구현해야하는 것이니 팀원들은 별 의견없이 그러자고 했다. (의견을 좀 내주었으면 좋겠다 싶었는데.. ㅜ)
세션 방식과 토큰 방식의 차이점을 잘 모르는 상태로 토큰 방식을 도입하려고 했다. 단순히 "세션 방식은 구현해봤고 토큰 방식은 한 번도 구현해보지 않았으니까 이번 기회에 토큰 방식을 사용하자!"라는 생각으로 구현하려고 했다.
그냥 구현만하고 배경지식이 없으면 안된다고 생각해서 세션 방식과 토큰 방식의 차이도 공부했다.
# Spring Security 추가 및 설정
책과 블로그를 참고하여 Spring Security를 추가하고 Config.java를 추가하여 SecurityFilterChain을 작성했다. 그 과정을 코드와 설명으로 작성해보겠다.
## Spring Security 추가
책과 블로그를 참고하여 프로젝트를 할 때는 Spring Security Dependency를 추가하고 바로 Config.java를 추가했는데 좀 더 상세한 흐름을 작성해보겠다.
Spring Security Dependency 추가
build.gradle
// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-security'
추가 후 프로젝트를 실행하면 다음과 같이 못보던 로그도 나오고, localhost:8080으로 접속하면 로그인 창이 나온다.
기본 주소로 접속하면 자동으로 로그인 페이지가 나오며 id에 "user", password에 로그에서 제시한 password를 입력하면 메인페이지로 이동한다.
SignUp API를 구현하면서 BCryptPasswordEncoder 빈 주입을 추가했었는데, 이것 때문에 로그인을 시도하면 "Encoded password does not look like BCrypt"라는 로그가 나오면서 로그인이 되지 않는다. 참고하자
Spring Security Config 추가
Spring Security에서 기본적인 로그인 페이지를 제공해주긴 하지만, 나는 프론트엔드와 백엔드를 나누어서 UI/UX와 API서버를 따로 구현했기 때문에 사용하지 않을것이다.
그래서 Spring Security에서 제공해주는 로그인 페이지를 사용하지 않기 위해서 Security Config를 작성했다.
SecurityConfig.java 추가
package com.example.securityexample.global.config;
// 생략...
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.formLogin(AbstractHttpConfigurer::disable);
return httpSecurity.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Spring Security 설정을 사용하기 위해 @EnableWebSecurity를 붙여준다.
Spring Security 기본 제공하는 formLogin을 사용하지 않기위해 disable설정했다.
PasswordEncoderConfig.java에 있던 PasswordEncoder 설정도 SecuriyConfig.java로 옮겼다.
다시 기본 주소인 localhost:8080으로 접속하면
localhost:8080/login으로도 접속해보면
API 요청 권한 오류 수정
내가 Controller에서 작성했던 Login API를 다시 사용하기 위해 Postman으로 테스트를 했지만 403 Forbidden으로 응답이 왔다.
403 Forbidden 이면 권한 오류라는 건데 갑자기 이런게 왜 나왔나..? 찾아보니 Spring Security를 사용하면 각 API마다 권한 설정을 해주어야 한다고 한다.
SecurityConfig.java의 filterChain()에 권한 관련 설정 추가
package com.example.securityexample.global.config;
// 생략...
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.formLogin(AbstractHttpConfigurer::disable);
httpSecurity.authorizeHttpRequests(requests -> requests
// .anyRequest().permitAll()
.requestMatchers(HttpMethod.POST, "/api/v1/login", "/api/v1/signup").permitAll()
.requestMatchers("/").permitAll()
);
return httpSecurity.build();
}
// 생략...
}
anyRequest().permitAll()을 사용해도 되지만 좀 더 명확하게 작성하기 위해 HTTP 메서드와 url 전부 작성해주었다.
그리고 기본 페이지(localhost:8080)도 접근을 허용하기위해 추가해주었다.
하지만 그래도 403 Forbidden으로 응답이 왔다.
프로젝트할 때는 그냥 무지성으로 filterChain()에 설정 작성 해 주고 잘 되는걸 확인했었는데 글을 작성하며 순차적으로 설정을 추가하니까 안되었고 그 이유도 몰랐다.
그래서 팀 프로젝트를 할 때 작성했던 설정들에 대해 검색해보니 권한 설정을 해주었어도 403 응답이 온 이유는 CSRF 설정을 안해주었기 때문이었다.
요약하자면 Spring Security는 기본적으로 CSRF 보호 설정이 되어있기 때문에 클라이언트에서 요청을 보낼 때 CSRF 토큰을 포함해서 요청을 보내지 않으면 안된다. 하지만 내가 API 테스트할 때는 CSRF 토큰을 포함하지 않았기 때문에 계속 403 Forbidden으로 응답이 왔던 것이었다.
따라서 요청시 CSRF 토큰을 포함시키거나, Security FilterChain에 CSRF 보호 기능을 명시적으로 해제하여 이를 해결할 수 있다. 내가 본 블로그들에서도 그래서 CSRF 기능을 해제했던 것이었다. 그냥 무지성으로 따라했으니 몰랐던거였다. 모지리...
SecurityConfig.java의 filterChain()에 CSRF 기능 해제 설정 추가
package com.example.securityexample.global.config;
// 생략...
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.formLogin(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable);
// 생략...
}
// 생략...
}
H2 DB 관련 설정 추가
DB를 직접 조회하기 위해 localhost:8080/h2-console로 접속해보니 접속이 안됐다. (/h2-console인 이유는 application-db.yml에 그렇게 설정했기 때문)
이전 api url 권한을 permitAll()을 통해 모두 허용한 것처럼 h2-console도 등록해주었다.
SecurityConfig.java의 filterChain()에 권한 설정 추가
package com.example.securityexample.global.config;
// 생략...
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
// 생략...
httpSecurity.authorizeHttpRequests(requests -> requests
.requestMatchers(HttpMethod.POST, "/api/v1/login", "/api/v1/signup").permitAll()
.requestMatchers("/h2-console/**").permitAll()
.requestMatchers("/").permitAll()
);
return httpSecurity.build();
}
// 생략...
}
그러고 /h2-console로 접속하여 로그인을 해보았더니 이런 화면이 나왔다
팀 프로젝트를 할 당시에는 H2가 아닌 Mysql이라서 이런 문제를 전혀 몰랐는데, 이 글을 작성하기 위해 H2를 사용하려고 하니 이런 오류가 발생했다.
관련 검색을 해보니 Spring Security를 사용하면 기본적으로 Header의 X-Frame-Options설정이 되어있어서 iframe나 object 요소를 통해 다른 컨텐츠가 embedded 되는것을 방지한다고 한다. H2 콘솔의 UI는 iframe으로 이루어져 있기 때문에 위와 같이 embedded가 되지 않는 것이었다.
해결 방법은 X-Frame-Options를 disable하거나 same-origin으로 바꾸는 것이다. 나는 same-origin으로 바꿔주었다.
SecurityConfig.java의 filterChain()에 Header 설정 추가
package com.example.securityexample.global.config;
// 생략...
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.formLogin(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.headers(frame -> frame.frameOptions(FrameOptionsConfig::sameOrigin));
// 생략...
);
return httpSecurity.build();
}
// 생략...
}
# 결론
이번 글에서 추가 한 내용은 다음과 같다.
- Spring Security 라이브러리 추가
- Spring Security Config 작성
- formLogin disable
- csrf disable
- api 및 사용할 url permitAll으로 등록
- h2 console을 위한 x-frame-options 설정
다음 글에는 Controller 계층에서 처리하던 Login API를 Spring Security의 Filter에서 처리하도록 바꿔보겠다.
# 전체 코드
'Java > Spring' 카테고리의 다른 글
[Spring] Security 사용기(3) LoginController → Security Filter (0) | 2024.07.09 |
---|---|
[Spring] Security 사용기(1) Login API(Controller) (0) | 2024.07.02 |
[Spring] Security 사용기(0) 개요 + User 도메인 생성 + 회원가입 API (0) | 2024.06.03 |
[Spring Boot] Thymeleaf properties 설정 (0) | 2023.12.27 |