its_jh_stroy

[Springboot] Spring Security로 인증과 권한 부여 본문

Java

[Springboot] Spring Security로 인증과 권한 부여

_J_H_ 2024. 7. 15. 23:29

Spring Security에 대해 공식 문서에서는 아래와 같이 정의한다.

Spring Security is a powerful and highly customizable authentication and access-control framework. 

It is the de-facto standard for securing Spring-based applications.

 

커스터마이징이 가능한 인증과 접근 제어를 제공하는 사실상 표준이라고 되어있다.

인증이란 사용자가 누구인지 확인하는 것이고, 접근 제어는 권한 부여(authorization)를 의미한다.

인증과 권한 부여를 조금 더 쉽게 구현할 수 있도록 도와주는 것이다.

 

Spring Security를 이용한 로그인 인증 구현하기

로그인을 구현하는 방식은 대표적으로 세션, 토근, OAuth를 이용한 방식이 있다.

그중 여기서는 세션을 이용한 방식으로 구현할 것이다.

인증을 구현하기 위해서는 아래와 같은 특별한 파일이 필요하다.

파일명은 다르게 해도 상관없다.

- Spring Security 설정 파일(SecurityConfig.java)

- UserDetailsService 인터페이스를 상속받는 서비스 클래스(MyUserDetailsService.java)

 

의존성 설치하고 설정 파일 만들기

스프링부트 내부에서 지원하는 H2 데이터베이스를 Spring Data JPA로 연결하여 사용할 것이다.

따라서 그에 맞는 의존성도 함께 추가한다.

// JPA와 H2 데이터베이스
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2' 

// Spring Security 라이브러리
implementation 'org.springframework.boot:spring-boot-starter-security'
// thymeleaf 템플릿 엔진과 Spring Security를 호환해주는 라이브러리
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.1.RELEASE'

 

 

의존성 추가가 끝나면 모든 페이지가 아래와 같이 로그인 창으로 막히게 된다.

 

이것을 해제하고 로그인 관련 설정을 지정하기 위해 SecurityConfig.java 파일을 만들 것이다.

SecurityConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

// Spring Security 설정을 지정할 수 있는 어노테이션
@Configuration
@EnableWebSecurity 
public class SecurityConfig {
    @Bean
    PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }


    // CSRF 공격 방지를 위해 랜덤한 토큰 생성
    @Bean
    public CsrfTokenRepository csrfTokenRepository() {
        HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
        repository.setHeaderName("X-XSRF-TOKEN");
        return repository;
    }

    @Bean
    SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        // CSRF 보안 기능 비활성화
        // 보통 세션 방식에서는 활성화하고 토큰 인증 방식에서는 비활성화 하는 경우가 많음
        // httpSecurity.csrf(csrf -> csrf.disable());
        
        // CSRF 보안 기능 활성화
        // CSRF는 사용자 인증 정보를 이용하여 악의적인 요청을 보내는 공격
        // 가능 활성화 시 뷰에 두 가지 작업 필요
        // 1. <form> 태그 내부에 아래 태그 추가하기 
        //    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
        // 2. ajax 요청 시에도 CSRF 토큰을 넣어서 요청 보내기
        httpSecurity.csrf(csrf -> csrf.csrfTokenRepository(csrfTokenRepository())
               .ignoringRequestMatchers("/login") // CSRF 기능에서 제외할 경로
        );

        // 인증 여부에 상관 없이 모든 경로(/**)에 대해 접근을 허용한다.
        httpSecurity.authorizeHttpRequests(
            authorize -> authorize.requestMatchers("/**").permitAll()
        );

        // HTML의 <form> 태그를 이용한 로그인 설정
        // 로그인 경로와 성공, 실패 시 이동할 경로 설정
        httpSecurity.formLogin(
            login -> login.loginPage("/login")
            .defaultSuccessUrl("/")
            .failureUrl("/fail") // 기본 경로 /login?error
        );

        // 로그아웃 설정, 해당 경로로 POST 요청 필요
        httpSecurity.logout(logout -> logout
            .logoutUrl("/logout")
            .logoutSuccessUrl("/")
            .deleteCookies("JSESSIONID") // 쿠키 삭제
            .invalidateHttpSession(true)); // 세션 무효화

        return httpSecurity.build();
    }
}

 

 

Model과 Repository 작성하기

최소한의 코드로 간단한 모델과 레포지토리를 작성하였다.

@NoArgsConstructor(access = AccessLevel.PROTECTED) 
@AllArgsConstructor
@Builder
@Getter
@Entity
@Table(name="my_user")
public class MyUser {
    @Id
    @Column(name = "user_id", updatable = false)
    private String userId;

    @Column(name = "password", nullable = false)
    private String password;

    @Column(name = "user_name")
    private String userName;
}
public interface MyUserRepository extends JpaRepository<MyUser, String> 
{  
    Optional<MyUser> findByUserId(String userId);
}

 

 

Controller와 View 작성하기

마찬가지로 컨트롤러와 화면을 작성하였다.

비밀번호를 암호화하여 저장할 계획이라 data.sql을 사용하지 않고 사용자 정보를 저장하는 메서드를 따로 작성하였다.

나중에 프로그램을 실행할 때 '/save_user'로 한 번 이동하여 사용자를 추가한 후 인증을 테스트할 것이다.

 

MyUserController.java

@RequiredArgsConstructor
@Controller
public class MyUserController {
    private final MyUserRepository myUserRepository;
    private final PasswordEncoder passwordEncoder;

    @GetMapping("/login")
    public String login() {
        return "login.html";
    }

    // 비밀번호 암호화 때문에 로그인할 사용자 정보를 저장하는 경로 작성
    @GetMapping("/save_user")
    @ResponseBody
    public List<MyUser> saveUser() {
        Optional<MyUser> oUser = myUserRepository.findById("admin");
        MyUser user;
        if (oUser.isEmpty()) {
            user = MyUser.builder()
                .userId("admin")
                .password(passwordEncoder.encode("1234"))
                .userName("kim")
                .build();

            myUserRepository.save(user);
        } 

        List<MyUser> list = myUserRepository.findAll();
        return list;
    }
}

 

 

login.html

<form action="/login" method="POST">
	<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
	<input name="username">
	<input name="password" type="password">
	<button type="submit">login</button>
</form>

 

 

UserDetailsService를 상속받는 서비스 클래스 만들기

login.html의 form 태그에 명시된 규칙에 맞게 UserDetailsService를 상속받는 서비스 클래스를 만들 것이다.

원래 데이터베이스에 연결하여 Repository 클래스를 통해 로그인한 사용자 정보를 가져오는 것을 추천한다.

하지만 데이터베이스 포스팅이 아니기 때문에 여기서는 그냥 MyUser 클래스를 만들어 진행하였다.

 

MyUserDetailsService.java

@Service
@RequiredArgsConstructor
public class MyUserDetailsService implements UserDetailsService {
    private final MyUserRepository myUserRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<MyUser> user = myUserRepository.findByUserId(username);

        if (user.isEmpty()) {
            throw new UsernameNotFoundException("그런 아이디 없어요.");
        }
        
        // 주고 싶은 권한 주기
        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
        
        // 사용자 아이디, 비밀번호, 권한 리스트        
        return new User(user.get().getUserId(), user.get().getPassword(), authorities);
        // 여기서 반환된 UserDetails 객체를 SecurityConfig에서 주입한
        // PasswordEncoder 객체를 통해 비밀번호 검사
    }
}

 

 

세션 시간 설정하기

마지막으로 세션 시간을 설정하고 싶다면 application.properties 파일에 아래와 같이 추가하면 된다.

// 초 단위 세션은 s 접미어 사용, ex) 30초 -> 30s
// 세션 시간 1분
server.servlet.session.timeout=1m
server.servlet.session.cookie.max-age=1m

 

 

프로그램 실행해보기

이제 프로그램을 실행하여 '/save_user'에서 데이터를 추가한 후, '/login'에서 로그인을 시도하면 인증에 성공한다.