본 글은 토이 프로젝트에 Spring Security를 적용하는 과정에 대해 포스팅한다.
Spring Security에 관한 디테일한 이론 지식이나 동작 원리 등에 대해서는 생략한다.
1. 개요
Spring Security
: Java 환경에서 인증과 권한 부여와 관련된 기능을 제공해주는 프레임워크
Spring Security를 적용하게 된 계기는... 그냥 써보고 싶었다.😉
Spring만 봐도 배울게 산더미인데 Spring Security까지 파자니 이도저도 못할 것 같아 공부 우선 순위에서 밀려났었다....
그러다 토이 프로젝트 팀원 분이 저희 Spring Security 적용하나요? 물어보시길래 어떻게 쓰는지 모르겠지만!! 어차피 써보고 싶었던거 일단 부딪혀 봐야겠다고 생각했다.
2. 개발 환경
- Spring Boot 2.5.7
- Java 8
- Gradle
나는 Spring Boot, JPA를 기반으로 토이 프로젝트를 개발하고 있다.
IDE로는 IntelliJ를 사용하고 있으며 특정 서버에 배포하지는 않고 로컬에서 작업중이다.
3. Spring Security 적용하기
build.gradle 수정 후 refresh
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
🔻 Maven의 경우
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Spring Security는 내부에 여러 Filter들이 동작하고 있다.
이 Filter를 이용하기 위해 다음과 같은 클래스를 생성해보자.
SecurityConfig 클래스 생성
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder encodePwd() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/users").permitAll()
.antMatchers("/users/**").authenticated()
.anyRequest().permitAll()
.and()
.formLogin()
.loginProcessingUrl("/login");
}
}
- @Configuration
- 해당 클래스를 Configuration으로 등록
- @EnableWebSecurity
- Spring Security 활성화
- WebSecurityConfigurerAdapter
- Spring Security 설정을 위해 오버라이딩하는 클래스
- BCryptPasswordEncoder
- 비밀번호 암호화를 위한 객체 생성 후 Bean으로 등록
- 로그인 시 해당 객체를 통해 암호화되어 진행
- HttpSecurity
- HTTP 요청에 대한 보안 설정
method | description |
authorizeRequests() | HttpServletRequest 요청 URL에 따라 접근 권한 설정 |
antMatchers("url") | 요청 URL 경로 패턴 지정 |
authenticated() | 인증 유저만 접근 허용 |
permitAll() | 모든 유저에게 접근 허용 |
anonymous() | 인증 안한 유저만 접근 허용 |
denyAll() | 모든 유저의 접근 불가 |
formLogin() | form login 설정 |
loginPage("url") | 커스텀 로그인 페이지 경로와 로그인 인증 경로 등록 |
loginProcessingUrl("url") | 사용자 이름과 암호를 제출할 URL |
defaultSuccessUrl("url") | 로그인 성공 시 이동 페이지 |
logoutUrl("url") | 사용자 정의 로그 아웃 |
🔻 csrf().diable()한 이유
CSRF; Cross-Site Request Forgery
- 사이트 간 요청 위조
- 사용자가 자신의 의지와 무관하게 공격자가 의도한 행위(등록, 수정, 삭제 등)를 요청하게 만듦
- ex: 2008년 옥션의 개인정보 유출 사건
@EnableWebSecurity은 기본적으로 이 CSRF 공격을 방지하는 기능을 지원한다.
임의의 csrf 토큰을 지급한 후 이를 이용해 요청에 csrf 토큰이 없거나 불일치 하면 4XX 상태 코드를 리턴한다.
이를 비활성화하기 위해 csrf().disable()를 기입했다.
CSRF는 사이트 간 요청이 발생하기 쉬운 웹에 대해 요청할 때 필요하다.
이는 보통 Thymeleaf, JSP 등 서버 측에서 html을 생성하는 구조가 많다.
하지만 내가 진행하는 프로젝트는 REST API 형식으로 JSON을 이용해 통신하도록 설계되어 있다.
서버가 클라이언트의 정보를 저장하지 않는 무상태(stateless)이고 서버 쪽의 세션이나 브라우저 쿠키에 의존하지 않기 때문에 CSRF 공격을 받을 수 없다.
추가로 "/users"에 대해 모든 권한을 허용한 이유는 해당 url이 회원가입이기 때문이다...
restful한 URL에 대해 많은 고민을 하고 있는데 적절한 이름이 아닌 것 같아 많은 고민 중에 있다.
4. 기존 로직 수정
로그인을 시도해도 자꾸만 실패하는 현상이 발생했다.
바로 BCryptPasswordEncoder 때문인데 기존의 실행 환경에는 암호화 없이 비밀번호가 직접 DB에 들어갔다.
로그인 시도 시에는 암호화 된 비밀번호랑 DB에 저장된 비밀번호를 비교를 하니 로그인이 실패할 수 밖에 없었다.
회원가입, 비밀번호 변경이 발생하면 암호화할 수 있도록 하기 위해 로직을 바꿨다.
User 엔티티
@Getter
@NoArgsConstructor
@Entity
@Table(name = "USER")
@DynamicInsert
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
@Column(nullable = false)
private String password;
...
// 비밀번호 암호화
public void encodePassword(PasswordEncoder encoder) {
this.password = encoder.encode(this.password);
}
}
UserService 로직 수정
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder encoder;
@Transactional
public Long register(UserRegisterRequestDto requestDto) {
User entity = requestDto.toEntity();
entity.encodePassword(encoder);
return userRepository.save(entity).getId();
}
@Transactional
public void changePassword(Long id, UserPasswordRequestDto requestDto) {
User entity = findUser(id);
entity.changePassword(requestDto.getPassword());
entity.encodePassword(encoder);
}
...
}
위 문제 외에는 딱히 발생하는 이슈는 없었으나 단위 테스트 코드들이 모두 실패한다...😢
해당 문제에 대해서도 차차 수정하고 포스트를 업데이트 할 예정이다.
참고
'Develop > Spring' 카테고리의 다른 글
[Spring] @ExceptionHandler로 API 예외 한번에 처리하기 (7) | 2022.04.21 |
---|---|
[Spring Security] 초간단 로그인 만들기 (2) | 2022.03.07 |
[Spring] NestedServletException - Name for argument type [java.lang.Long] not available 에러 (0) | 2022.01.28 |
PATCH 메소드는 언제 사용하는가? (3) | 2022.01.25 |
[Spring/MariaDB] 연동 시 자주 발생하는 오류 (0) | 2021.12.26 |