본문 바로가기
Develop/Spring+JPA

[Spring Security] 스프링 시큐리티 간단 적용기

by 연로그 2022. 2. 7.
반응형

본 글은 토이 프로젝트에 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 공격을 받을 수 없다.

 

더 자세한 정보: https://portswigger.net/web-security/csrf

 

What is CSRF (Cross-site request forgery)? Tutorial & Examples | Web Security Academy

In this section, we'll explain what cross-site request forgery is, describe some examples of common CSRF vulnerabilities, and explain how to prevent CSRF ...

portswigger.net

 

추가로 "/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);
    }
    ...
}

 

위 문제 외에는 딱히 발생하는 이슈는 없었으나 단위 테스트 코드들이 모두 실패한다...😢

해당 문제에 대해서도 차차 수정하고 포스트를 업데이트 할 예정이다.


참고

  1. https://bamdule.tistory.com/53
  2. https://zzang9ha.tistory.com/341
  3. https://www.baeldung.com/spring-security-csrf
반응형