[Spring Security] 기본

JUJU·2024년 7월 2일
0

Spring

목록 보기
17/21

들어가기 전에..

프롬프트 엔지니어링 사이트를 제작하는 프로젝트에서, 필자는 교육용 페이지와 댓글, 마이페이지를 만드는 작업을 수행했다. login/register 기능은 다른 백엔드 팀원이 작업했다.

하지만... 스프링 세큐리티에 대한 지식이 없던 나는 팀원이 작성한 코드를 전혀 이해하지 못했다.

적어도 본인이 한 프로젝트의 기능들은 모두 이해해야 한다고 생각하기 때문에, Spring Security를 따로 공부하면서 코드를 하나씩 이해해보려 한다.


⚠️ 코드를 이해하려면 Spring과 JPA 지식이 필요합니다!




✏️ Spring Security

Spring Security

: 애플리케이션에서 인증과 권한 부여를 관리하는 프레임워크

인증(Authentication): 해당 사용자가 본인인지 확인하는 절차

인가(Authorization): 인증된 사용자가 요청한 자원에 접근 가능한지 확인하는 절차

권한 부여: 인증된 사용자가 애플리케이션의 기능을 사용할 수 있게 권한을 주는 것


■ 동작 원리

이 사이트를 참고했습니다.

Spring Security를 사용하지 않으면?

DispatcherServlet 이 HTTP 요청을 받아서 각 컨트롤러에 전달한다.

Spring Security를 사용하면?

DispatcherServlet 으로 전달되기 전에, Spring Security가 모든 요청을 intercept한다.

자세한 동작 원리는 다음과 같다.

  1. HTTP 요청이 들어온다. (로그인)

  2. UsernamePasswordAuthenticationFilter가 사용자의 이름과 비밀번호를 캡쳐하고, AuthenticationManager 에 넘긴다.

  3. AuthenticationManager 는 해당 인증을 처리할 수 있는 AuthenticationProvider 에게 넘긴다.

  4. AuthenticationProviderUserDetailsService를 통해 사용자 정보를 로드하고 검증한다.

  5. 인증 성공시 Authentication 객체가 SecurityContextHolder에 저장된다.


■ UserDetailService

AuthenticationProviderUserDetailsService를 통해 사용자 정보를 로드하고 검증한다.

  • UserDetailService 는 DB에서 사용자 정보를 로드한다.
  • DB에서 로드된 사용자 정보는 UserDetails 타입으로 리턴된다.

그렇다면, UserDetailService 는 어떻게 사용자 정보를 로드하는 걸까?

UserDetailService 인터페이스를 구현한 구현객체는 UserDetails 타입을 반환하는 메소드를 오버라이딩한다.

UserDetailService 는 해당 메소드를 호출해서 DB에 있는 사용자 정보를 UserDetails 타입으로 반환한다.




✏️ 간단한 구현

build.gradle 이나 DB 연결 설정이 완료되어 있다는 가정 하에 Spring Security를 사용하는 방법을 알아보자.

우선, 간단한 컨트롤러를 하나 만들어보자.

@Controller
public class SimpleController {
	@GetMapping("/")
	public @ResponseBody String index() {
		return "인덱스 페이지";
	}

	@GetMapping("/user")
	public @ResponseBody String user()
		return "유저 페이지";
	}

	@GetMapping("/admin")
	public @ResponseBody String admin() {
		return "어드민 페이지";
	}
	
	@GetMapping("/manager")
	public @ResponseBody String manager() {
		return "매니저 페이지";
	}

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

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

현재의 컨트롤러는 아무런 기능이 없다.
그냥 각 페이지에 대한 설명을 return할 뿐이다.


여기에 Spring Security를 적용해보자!

우선, Config 파일을 하나 만들어줘야 한다.

  • Config 파일을 통해서 보안 정책을 구성할 수 있다.
@Configuration // 이 클래스가 스프링 설정 클래스임을 나타냅니다.
@EnableWebSecurity // 필터체인에 등록
public class SecurityConfig {

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

	@Bean
	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		http.csrf().disable();
		// CSRF(Cross-Site Request Forgery) 보호를 비활성화합니다.
		http.authorizeRequests()
				.antMatchers("/user/**").authenticated()
				// 위 경로의 모든 요청은 인증된 사용자만 접근할 수 있습니다.
				.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_USER')")
				// 위 경로의 요청은 ROLE_ADMIN 또는 ROLE_USER 권한을 가진 사용자만 접근할 수 있습니다.
				.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
				// ROLE_ADMIN 권한을 가진 사용자만 위 경로에 접근할 수 있습니다.
				.anyRequest().permitAll()
				/* 나머지 모든 요청은 인증 없이 접근할 수 있습니다. */
				.and()
				.formLogin()
				.loginPage("/loginForm")
				/* 로그인 페이지의 URL을 설정. 사용자가 로그인하지 않은 상태에서 인증이 필요한 페이지에 접근하면 이 페이지로 리다이렉트된다. */
				
		return http.build();
	}
}

위의 설정을 통해 Spring Security는 지정된 URL 패턴에 따라 접근 권한을 관리할 수 있게 되었다.


■ 회원가입

간단한 회원가입 페이지를 만들어보자.

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>회원가입 페이지</title>
</head>
<body>
<h1>회원가입 페이지</h1>
<hr/>
<form action="/register" method="post">
	<input type="text" name="username" placeholder="Username"/> <br/>
	<input type="password" name="password" placeholder="Password"/> <br/>
	<input type="email" name="email" placeholder="Email"/> <br/>
	<button>회원가입</button>
</form>
</body>
</html>
@GetMapping("registerForm")
public String registerForm(){
	return "registerForm";
}

위의 폼에 내용을 입력한 뒤, 버튼을 누르면 /register URL로 POST 요청을 보낸다.
컨트롤러는 해당 요청을 받아서 회원가입을 진행해야 한다.

이때, 한 가지 주의해야 할 사항이 있다.

⚠️ 비밀번호를 DB에 그대로 저장하면 안된다!!!

비밀번호가 암호화되지 않으면, 시큐리티로 로그인을 할 수 없다.
∴ Config 파일에서 빈으로 등록한 BCryptPasswordEncoder을 사용해서 비밀번호를 암호화 해야 한다.


회원가입을 위해 사용자 정보를 DB에 저장하는 코드는 다음과 같다.

	@PostMapping("/register")
    // user는 폼에 입력한 정보를 통해 바인딩
	public String register(User user) {
		String rawPassword = user.getPassword();
		String encPassword = bCryptPasswordEncoder.encode(rawPassword); // 비밀번호 암호화
		user.setPassword(encPassword);
		user.setRole("ROLE_USER");
		userRepository.save(user);
		return "redirect:/";
	}

■ 로그인

앞서 설명했듯이, Spring Security는 UserDetailService 를 통해 DB에서 사용자 정보를 로드한다.

UserDetailService 는 인터페이스이기 때문에, 이를 구현한 구현객체를 만들어줘야 한다.
또한, UserDetails 인터페이스를 구현하여 사용자 정보를 캡슐화해야 한다.

public class PrincipalDetails implements UserDetails {

    private User user;

    public PrincipalDetails(User user) {
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.singleton(new SimpleGrantedAuthority(user.getRole()));
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
@Service
public class PrincipalDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found"));
        return new PrincipalDetails(user);
    }
}

이제 사용자 검증을 수행할 비즈니스 로직들은 다 작성했다.
그렇다면, 로그인을 처리하기 위한 컨트롤러는 어떻게 구현해야 할까?

컨트롤러는 구현할 필요가 없다!

Spring Security에서 자동적으로 로그인을 처리하도록 만들어주면 된다.


Config 파일의 http.authorizeRequests() 에 다음과 같은 코드를 추가하면 된다.

	.loginProcessingUrl("/login") // 이 URL로 요청이 오면 시큐리티가 로그인을 처리해준다.
    .defaultSuccessUrl("/") // login 이후 리다이렉트 될 위치



✏️ 권한 처리

Config 파일에 @EnableGlobalMethodSecurity(securityEnabled = true) 어노테이션을 추가하면, 간편한 권한 처리가 가능하다.


예를 들어, /info URL로 들어오는 요청을 제한하고 싶다면, 아래와 같은 어노테이션을 추가해주기만 하면 된다.

@Secured("ROLE_ADMIN")
@GetMapping("/info")
public @ResponseBody String info() {
	return "info";
}

이제, /info URL로 접속하려면 ADMIN 권한이 필요하다.


다른 방법으로는 다음과 같은 어노테이션을 사용할 수도 있다.

@PreAuthorize("hasRole(ROLE_ADMIN)")
@GetMapping("/info")
public @ResponseBody String info() {
	return "info";
}

REFERENCE

스프링부트 시큐리티 & JWT 강의

profile
개발자 지망생

0개의 댓글

관련 채용 정보