Spring Security

calis_ws·2023년 7월 30일
0

Spring Security

  • Spring에서 회원가입, 로그인, 로그아웃, 사용자의 로그인에 따른 기능 변화 등을 구현해줌

  • 인증 (Authentication) : 사용자가 자신이 누구인지를 증명하는 과정

  • 권한 (Authorization) : 사용자가 어떤 작업을 수행할 수 있는지를 결정하는 과정

auth 프로젝트 생성

http://localhost:8080/login

접속 하면 login 페이지가 나온다.
spring security가 모든 url을 인증된 유저에게만 허용하기 때문이다.

@RestController
public class RootController {
    //  http://localhost:8080/
    @GetMapping
    public String root() {
        return "hello";
    }

    //  http://localhost:8080/no-auth
    //  no-auth는 누구나 접근 가능하도록
    @GetMapping("/no-auth")
    public String noAuth() {
        return "no auth success!";
    }

    //  http://localhost:8080/re-auth
		//  re-auth는 인증된 사용자만 접근 가능하도록
    @GetMapping("/re-auth")
    public String reAuth() {
        return "re auth success!";
    }
}

Spring Security 는 인증, 인가에 대한 처리를 여러 개의 필터를 통해서 연쇄적으로 실행한다.

따라서 특정 요청에 대한 필터를 처리하고 싶다면, HttpSecurity 를 통해서 커스텀이 가능하다.

SecurityFilterChain

  • Spring Security 가 Bean 객체를 통해 설정을 변경할 수 있도록 @Configuration 을 사용해서 설정에 사용할 클래스를 만들어 Bean 객체로 등록한다.

  • 클래스 내에서는 SecurityFilterChain 메서드를 만들어서 메서드의 반환 값을 Bean 객체에 등록해서 사용할 수 있도록 @Bean 을 사용한다.

  • SecurityFilterChain 은 스프링 시큐리티에서 보안 관련 기능을 제공하는 인터페이스로 해당 인터페이스의 구현체를 Bean 객체로 등록해서 설정으로 사용할 수 있도록 한다.

  • HttpSecurity 는 SecurityFilterChain 의 보안 필터를 구성할 수 있는 클래스로 이 객체를 반환해서 스프링 시큐리티의 보안 설정을 구성할 수 있다.

public class WebSecurityConfig {
    @Bean // 메서드의 결과를 BEAN 객체로 등록해주는 어노테이션
    public SecurityFilterChain securityFilterChain(
            // DI 자동으로 설정됨, 빌더 패턴처럼 사용
            HttpSecurity http
    ) throws Exception
    {
        // requestMatchers == 어떤 URL로 오는 요청에 대하여 설정하는지
        // permitAll() == 누가 요청해도 허가한다.
        // authenticated() == 인증이 된 사용자만 허가
        // anonymous() == 인증되지 않은 사용자만 허가
        http.authorizeHttpRequests(authHttp -> authHttp
                .requestMatchers("/no-auth")
                .permitAll()
                .requestMatchers("/re-auth")
                .authenticated()
                .requestMatchers("/")
                .anonymous()
                // HTTP 요청 허가 관련 설정
        );
        return http.build();
    }
  • authorizeHttpRequest() 를 사용해서 HTTP 요청 허가 관련 설정을 할 수 있다.
  • requestMatcher(”URL”).permitAll() → 해당 URL 요청을 모두에게 허용
  • requestMatcher(”URL”).authenticated() → 해당 URL 요청을 인증된 사용자에게만 허용
  • requestMatcher(”URL”).anonymous() → 해당 URL 요청을 인증되지 않은 사용자에게만 허용

Test

http://localhost:8080/no-auth

no-auth 요청의 결과는 누구든지 볼 수 있다.

http://localhost:8080/re-auth

인증된 사용자만 re-auth 요청의 결과를 볼 수 있다.

로그인

가장 기본적인 사용자 인증 방식이다.

  1. 사용자가 로그인이 필요한 페이지로 이동

  2. 서버는 사용자를 로그인 페이지로 이동

  3. 사용자는 로그인 페이지를 통해 아이디와 비밀번호 전달

  4. 아이디와 비밀번호 확인 후 사용자를 인식

Http 요청은 사용자의 인증 사실 여부를 저장하지 않고 있기 때문에 새로운 요청을 할 때 또 다시 인증을 해야 하는 문제점이 발생한다.

위 문제를 해결하기 위해 쿠키에 인증 사실 여부를 저장해서 요청마다 쿠키를 포함해서 요청을 보낸다.

쿠키에 저장된 데이터를 바탕으로 유지되는 상태를 세션이라고 부른다.

FormLogin

Form 에서 입력 받은 데이터를 이용해서 인증 받은 사용자임을 판단하기 위해서 HttpSecurity의 .formLogin() 을 사용한다.

  • .loginPage(URL) : 해당 URL 요청을 통해 로그인 폼 (html) 을 띄운다.

  • .defaultSuccessUrl(URL) : 로그인 성공 시 실행할 URL 요청을 설정한다.

  • .failureUrl(URL) : 로그인 실패 시 실행할 URL 요청을 설정한다.

  • 마지막으로 .permitAll(), authenticated(), anonymous() 중 하나를 사용해서 formLogin 을 어떤 사용자에게 허용 할 지를 설정 할 수 있는데 로그인 시도는 모든 사용자가 가능해야 하기 때문에 permitAll() 로 설정한다.

public class WebSecurityConfig {
    @Bean // 메서드의 결과를 BEAN 객체로 등록해주는 어노테이션
    public SecurityFilterChain securityFilterChain(
            // DI 자동으로 설정됨, 빌더 패턴처럼 사용
            HttpSecurity http
    ) throws Exception
    {
        // requestMatchers == 어떤 URL로 오는 요청에 대하여 설정하는지
        // permitAll() == 누가 요청해도 허가한다.
        // authenticated() == 인증이 된 사용자만 허가
        // anonymous() == 인증되지 않은 사용자만 허가
        http.authorizeHttpRequests(authHttp -> authHttp
                .requestMatchers("/no-auth")
                .permitAll()
                .requestMatchers("/re-auth")
                .authenticated()
                .requestMatchers("/")
                .anonymous()
                // HTTP 요청 허가 관련 설정
								// form 을 이용한 로그인 관련 설정
                .formLogin(
                        formLogin -> formLogin
	                               // 로그인 하는 페이지 ( 경로 ) 를 지정
                                .loginPage("/users/login")
                                // 로그인 성공 시 이동하는 페이지 ( 경로 )
                                .defaultSuccessUrl("/users/my-profile")
                                // 로그인 실패 시 이동하는 페이지
                                .failureUrl("/users/login?fail")
                                // 로그인 과정에서 필요한 경로 ( 요청 ) 들을 모두가 사용할 수 있게 권한 설정
                                .permitAll()
                )
        );
        return http.build();
    }

http://localhost:8080/users/login

로그인 후
http://localhost:8080/users/my-profile

WebSecurityConfig: CSRF

Cross Site Request Forgery의 약자이며 사이트 간 요청 위조, 웹 애플리케이션의 취약점 중 하나로
사용자가 자신의 의지와 무관하게 공격자가 의도한 행동을 하여 특정 웹페이지를 보안에 취약하게 한다거나 수정, 삭제 등의 작업을 하게 만드는 공격 방법을 의미한다.

Spring Security에서 form 전송 시 CSRF 토큰을 함께 전송해주어야 한다. 해당 토큰 없이 요청하면 막아버린다.

http.csrf(AbstractHttpConfigurer::disable);

UserController.userDetailsManager()

UserDetailsManager은 사용자 관련 CRUD 매서드를 가지고 있는 인터페이스로 매개변수로는 UserDetails라는 객체가 들어있다. 사용자를 생성, 업데이트, 삭제, 비밀번호 변경, 유저유무 확인 등을 할때 UserDetailManager를 상속받아서 사용할 수 있다.

@Bean
// 사용자 관리를 위한 인터페이스 구현체 Bean
public UserDetailsManager userDetailsManager(
        PasswordEncoder passwordEncoder
) {
    // 임시 User 생성
    UserDetails user1 = User.withUsername("user1")
            .password(passwordEncoder.encode("1234"))
            .build();
    // Spring에서 미리 만들어놓은 사용자 인증 서비스
    return new InMemoryUserDetailsManager(user1);
}

UserController.passwordEncoder

PasswordEncoder는 Spring Security에서 비밀번호를 안전하게 저장할 수 있도록 비밀번호의 단방향 암호화를 지원하는 인터페이스이다.

@Bean
// 비밀번호 암호화를 위한 Bean
public PasswordEncoder passwordEncoder() {
    // 기본적으로 사용자 비밀번호는 해독 가능한 형태로 데이터베이스에 저장되면 안됨
    // 그래서 기본저긍로 단방향 암호화하는 인코더를 사용한다.
    return new BCryptPasswordEncoder();
}

회원가입

  1. 사용자가 register 페이지 (회원가입 페이지) 로 온다.

  2. 사용자가 register 페이지에 id, 비밀번호, 비밀번호 확인을 입력한다.

  3. register 페이지에서 사용자가 입력한 정보를 /users/register로 POST 요청하고, UserDetailsManager에 새로운 사용자 정보를 추가한다.

register 페이지 조회 기능 구현하기

UserController.java에 register-form.html을 반환하는 GET 메소드를 추가한다.

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

GET 메소드를 추가하고 실행하면 로그인 페이지에서 회원가입 클릭 시 register 페이지로 이동되지 않는 것을 확인할 수 있다.

이는 해당 페이지에 접근할 수 있는 권한이 없어서 생기는 문제로, 이 문제를 해결하기 위해 WebSecurityConfig.java 에서 securityFilterChain 메소드의 anonymous 메소드에 "/users/register"를 추가하여 인증이 되지 않은 사용자도 register 페이지에 접근할 수 있도록 권한을 부여해준다.

@Bean
    public SecurityFilterChain securityFilterChain(
            
            HttpSecurity http
    )
            throws Exception {
                http
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(
                        authHttp -> authHttp
                                .requestMatchers("/no-auth")
                                .permitAll()
                                .requestMatchers(
                                        "/re-auth",
                                        "/users/my-profile"
                                )
                                .authenticated()  // 인증이 된 사용자만 허가
                                .requestMatchers("/", "/users/register") 
                                .anonymous()  // 인증이 되지 않은 사용자만 허가
                )
/* 생략 */

사용자가 입력한 회원가입 정보 처리하기

사용자가 입력한 정보를 회수할 수 있도록 UserController.java 에 POST 메소드를 추가한다.

@PostMapping("/register")
    public String registerPost(
            @RequestParam("username") String username,
            @RequestParam("password") String password,
            @RequestParam("password-check") String passwordCheck
    ) {
				// 사용자가 입력한 비밀번호와 비밀번호 확인값이 일치하는지 확인
        if (password.equals(passwordCheck)) {
            log.info("password match!");
            return "redirect:/users/login";
        }
    }

이제 사용자가 입력한 정보를 새로운 사용자로 등록해줘야 한다.

아래와 같이 생성자를 통해 의존성이 주입될 수 있도록 하고, Post 메소드 내 createUser()를 통해 UserDetailsManager인터페이스에 새로운 사용자를 등록한다.

이로써 회원가입 기능 구현이 완료되었다.

		private final UserDetailsManager manager;
    private  final PasswordEncoder passwordEncoder;
    
    public UserController(UserDetailsManager manager, PasswordEncoder passwordEncoder) {
        this.manager = manager;
        this.passwordEncoder = passwordEncoder;
    }
@PostMapping("/register")
    public String registerPost(
            @RequestParam("username") String username,
            @RequestParam("password") String password,
            @RequestParam("password-check") String passwordCheck
    ) {
				// 사용자가 입력한 비밀번호와 비밀번호 확인값이 일치하는지 확인
        if (password.equals(passwordCheck)) {
            log.info("password match!");

				// 입력받은 사용자 정보로 새로운 사용자 등록
            manager.createUser(User.withUsername(username)
                   .password(passwordEncoder.encode(password))
                   .build());
						// 로그인 성공 시 로그인페이지로 이동
            return "redirect:/users/login";
        }
        log.warn("password does not match...");
        // 로그인 실패 시 (error 파라미터가 붙는)회원가입 페이지에 머무름
				return "redirect:/users/register?error";
    }

어떤 사람이 로그인 했는지 확인하기

누가 로그인했는지 알기 위해서는 UserController.java의 myprofile 메소드 (GET 메소드)의 인자로 Authentication 객체를 받아 해당 객체의 이름을 로그로 출력하면 된다.

	@GetMapping("/my-profile")
    public String myProfile(Authentication authentication) {
        log.info(authentication.getName());
        return "my-profile";
    }
  • Authentication이란?

Spring Security에서 현재 인증된 사용자의 인증정보를 나타내는 인터페이스이다. Spring Security는 사용자 인증 후에 Authentication 객체를 생성하고, 이 객체는 사용자의 인증 정보 (사용자 이름, 권한 등) 를 포함하고 있어 현재 사용자의 정보를 확인하거나 활용할 수 있도록 제공된다.

출처 : 멋사 5기 백엔드 위키 10팀 유난히 내성적이었던 10팀

인사이트 타임

나이 출력

https://school.programmers.co.kr/learn/courses/30/lessons/120820

class Solution {
    public int solution(int age) {
        return 2023 - age;
    }
}

배열의 평균값

https://school.programmers.co.kr/learn/courses/30/lessons/120817

class Solution {
    public double solution(int[] numbers) {
        float sum = 0;
        for (int i = 0; i < numbers.length; i++) sum += numbers[i];
        return sum / numbers.length;
    }
}

배열 뒤집기

https://school.programmers.co.kr/learn/courses/30/lessons/120821

class Solution {
    public int[] solution(int[] num_list) {
        int[] answer = new int[num_list.length];
        int idx = 0;
        for (int i = num_list.length - 1; i >= 0; i--) answer[idx++] = num_list[i];
        return answer;
    }
}

배열 자르기

https://school.programmers.co.kr/learn/courses/30/lessons/120833

class Solution {
    public int[] solution(int[] numbers, int num1, int num2) {
        int[] answer = new int[num2 - num1 + 1];
        int idx = 0;
        for (int i = num1; i <= num2; i++) answer[idx++] = numbers[i];
        return answer;
    }
}
profile
반갑습니다람지

2개의 댓글

comment-user-thumbnail
2023년 7월 30일

좋은 정보 얻어갑니다, 감사합니다.

1개의 답글