Backend

<Backend> Spring / Spring Security

이게왜 2024. 7. 11. 00:01

1. Spring Security란?

Spring Security는 Java 애플리케이션에 보안 기능을 추가하는 강력한 프레임워크입니다. 이를 통해 인증(Authentication)과 권한 부여(Authorization) 기능을 손쉽게 구현할 수 있습니다.


2. Spring Security의 목적

Spring Security는 웹 애플리케이션의 보안을 강화하기 위해 설계된 프레임워크입니다. 

 

2.1 주요목적

인증(Authentication)  사용자가 누구인지 확인하는 과정
권한 부여(Authorization)  인증된 사용자가 특정 리소스에 접근할 수 있는지 여부를 결정하는 과정
보안 정책 구현  세밀한 보안 규칙을 정의하여 애플리케이션의 다양한 부분에서 보안을 적용

2.2 인증(Authentication)

 

인증은 사용자가 누구인지를 확인하는 과정입니다. Spring Security에서는 다양한 인증 방법을 지원하며, 기본적으로 다음과 같은 흐름으로 인증을 처리합니다.

 

1. 인증 요청

사용자가 로그인 폼을 통해 아이디와 비밀번호를 입력하고, 서버에 인증 요청을 보냅니다.

 

2. 인증 필터(Authentication Filter)

Spring Security는 인증 요청을 인터셉트하여 인증 필터를 통해 처리합니다. 기본적으로 UsernamePasswordAuthenticationFilter

가 사용됩니다.

 

3. 인증 매니저(Authentication Manager)

인증 필터는 인증 매니저에게 인증 요청을 전달합니다. 인증 매니저는 여러 AuthenticationProvider를 사용하여 인증을 시도합니다.

 

4. AuthenticationProvider

AuthenticationProvider는 실제 인증 로직을 수행하는 컴포넌트입니다. 주로 DaoAuthenticationProvider가 사용되며, 이는 사용자 정보를 데이터베이스에서 조회하여 인증을 수행합니다.

 

5. 인증 성공 및 실패

인증이 성공하면 Authentication 객체가 생성되어 SecurityContext에 저장됩니다. 이 객체는 사용자의 인증 정보를 담고 있습니다.

인증이 실패하면 인증 실패 핸들러가 호출되어 적절한 응답을 반환합니다.


2.3 권한 부여(Authorization)

 

권한 부여는 인증된 사용자가 특정 리소스에 접근할 수 있는지 여부를 결정하는 과정입니다. Spring Security에서는 사용자의 역할(Role)과 권한(Authority)을 기반으로 접근을 제어합니다.

 

2.3.1 보안 설정(Security Configuration)

Spring Security 설정 클래스에서 URL 경로, 메서드, 리소스에 대한 접근 권한을 설정합니다.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasRole("USER")
                .antMatchers("/", "/home").permitAll()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .permitAll();
    }
}

2.3.2 역할과 권한

Spring Security에서는 GrantedAuthority 인터페이스를 통해 권한을 부여합니다. 주로 SimpleGrantedAuthority 클래스가 사용되며, 역할은 ROLE_ 접두어를 가집니다.

예를 들어, ROLE_ADMIN은 관리자 권한을 의미합니다.


 

2.3.3 메서드 수준 보안(Method Level Security)

Spring Security는 메서드 수준에서도 권한을 제어할 수 있습니다. 이를 위해 @PreAuthorize@Secured 어노테이션을 사용할 수 있습니다.

@Service
public class MyService {
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    public void adminMethod() {
        // 관리자만 접근 가능
    }

    @Secured("ROLE_USER")
    public void userMethod() {
        // 사용자만 접근 가능
    }
}

2.4 동작방식


3. 예제 코드

기능

- 회원가입

- 로그인

 

회원가입

- email, password를 local DB에 저장 (password는 암호화)

 

로그인

- 회원확인 후 회원페이지로 이동


 

WebSecurityConfig

더보기
package org.example.springsecuritydemo.config;

import lombok.RequiredArgsConstructor;
import org.example.springsecuritydemo.service.UserDetailService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {


    private final UserDetailService userService;


    @Bean
    public WebSecurityCustomizer configure() {
        return (web) -> web.ignoring()
                .requestMatchers(new AntPathRequestMatcher("/static/**"));
    }


    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers(
                                new AntPathRequestMatcher("/signup"),
                                new AntPathRequestMatcher("/login"),
                                new AntPathRequestMatcher("/user")
                        ).permitAll()
                        .anyRequest().authenticated())
                .formLogin(formLogin -> formLogin
                        .loginPage("/login")
                        .defaultSuccessUrl("/articles")
                )
                .logout(logout -> logout
                        .logoutSuccessUrl("/login")
                        .invalidateHttpSession(true)
                )
                .csrf(AbstractHttpConfigurer::disable)
                .build();
    }

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder,
                                                       UserDetailService userDetailService) throws Exception {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userService);
        authProvider.setPasswordEncoder(bCryptPasswordEncoder);
        return new ProviderManager(authProvider);
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

 

UserApiController

더보기
package org.example.springsecuritydemo.controller;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.example.springsecuritydemo.dto.AddUserRequest;
import org.example.springsecuritydemo.service.UserService;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@RequiredArgsConstructor
@Controller
public class UserApiController {
    private final UserService userService;

    @PostMapping("/user")
    public String signup(AddUserRequest request) {
        userService.save(request);
        return "redirect:/login";
    }

    @GetMapping("/logout")
    public String logout(HttpServletRequest request, HttpServletResponse response) {
        new SecurityContextLogoutHandler().logout(request, response,
                SecurityContextHolder.getContext().getAuthentication());
        return "redirect:/login";
    }
}

UserViewController

더보기
package org.example.springsecuritydemo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class UserViewController {
    @GetMapping("/login")
    public String login() {
        return "login";
    }

    @GetMapping("/signup")
    public String signup() {
        return "signup";
    }
}

User

더보기
package org.example.springsecuritydemo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class UserViewController {
    @GetMapping("/login")
    public String login() {
        return "login";
    }

    @GetMapping("/signup")
    public String signup() {
        return "signup";
    }
}

AddUserRequest

더보기
package org.example.springsecuritydemo.dto;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class AddUserRequest {
    private String email;
    private String password;
}

UserRepository

더보기
package org.example.springsecuritydemo.repository;

import java.util.Optional;
import org.example.springsecuritydemo.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
}

 

UserDetailService

더보기
package org.example.springsecuritydemo.service;

import lombok.RequiredArgsConstructor;
import org.example.springsecuritydemo.repository.UserRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class UserDetailService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        return userRepository.findByEmail(email)
                .orElseThrow(() -> new UsernameNotFoundException(email));
    }
}

UserService

더보기
package org.example.springsecuritydemo.service;

import lombok.RequiredArgsConstructor;
import org.example.springsecuritydemo.domain.User;
import org.example.springsecuritydemo.dto.AddUserRequest;
import org.example.springsecuritydemo.repository.UserRepository;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Service
public class UserService {
    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    public Long save(AddUserRequest dto) {
        return userRepository.save(User.builder()
                .email(dto.getEmail())
                .password(bCryptPasswordEncoder.encode(dto.getPassword()))
                .build()).getId();
    }
}

실행 결과 및 로그

더보기

 

 

 

'Backend' 카테고리의 다른 글

<Backend> Spring / Batch  (0) 2024.07.10
<Backend> Spring / QueryDSL  (0) 2024.07.09
<Backend> Java / 상속  (0) 2024.04.05
<Backend> Java / Class (3) 접근제한자와 변수의 타입  (0) 2024.04.04
<Backend> Java / Class (2) this와 메소드  (0) 2024.04.04