본문 바로가기

Server Programming/Spring Boot Backend Programming

8장-1. 스프링 시큐리티 (+ SecurityFilterChain, webSecurityCustomizer(), @EnableGlobalMethodSecurity, CSRF 토큰)

반응형

목적 로그인, 세션 트래킹
스프링 이전 HttpSession, Cookie
스프링 적용 스프링 시큐리티

 

스프링 시큐리티 중 Spring Web Security에 구현되어있는 기능

  • 로그인
  • 자동 로그인
  • 로그인 후 페이지 이동
  • 그 외, HttpSession과 Cookie로 구현했던 기능들을 자동으로 처리

스프링 시큐리티 기본 설정

  • 경로 제외 설정
  • 로그 레벨 설정
  • 정적 자원 필터 제외

 

 

1. 의존성 추가

//스프링 웹 시큐리티
implementation 'org.springframework.boot:spring-boot-starter-security'

 

2. 스프링 시큐리티 설정

  • 스프링 시큐리티 설정의 경우 application.properties 설정보다 자바 코드를 이용하는 경우가 많다.
  • 따라서 별도의 Config클래스 작성

(1) CustomSecurityConfig 클래스 작성

package org.zerock.b01.config;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Configuration;

@Log4j2
@Configuration
@RequiredArgsConstructor
public class CustomSecurityConfig {
    
}

스프링 시큐리티 기본값

스프링 시큐리티 기본 유저 : user

스프링 시큐리티 기본 비밀번호 출력

스프링 기본값
유저 user
비밀번호 랜덤생성 후 출력
별도 설정 없을 경우 모든 자원에 권한과 로그인 여부 확인
로그인 하지 않도록 별도 설정 SecurityFilterChain 객체 반환 메서드 작성

 

(2) 로그인 하지 않도록 설정하는 filterChain() 메서드

-모든 사용자가 모든 경로에 접근

-추후에 경로를 제한해야한다.

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{

    return http.build();
}

 

(3) application.properties에 스프링 시큐리티 로그 레벨 설정

-스프링 시큐리티는 웹에서 사용하는 필터로 동작

-단계별로 동작하는 수많은 필터들이 존재하므로, 로그레벨을 낮춰서 에러를 출력하도록 설정한다.

logging.level.org.springframework=info
logging.level.org.zerock=debug

logging.level.org.springframework.security=trace

 

(4) 정적 자원의 처리

-기본값으로 css 파일 / js 파일 등에도 필터가 적용

-정적 파일에 시큐리티 적용을 해제해야 한다.

@Bean
public WebSecurityCustomizer webSecurityCustomizer(){
    log.info("---web configure---");

    //정적 파일을 시큐리티 설정에서 제외하는 설정
    return (web) -> web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    //: 해당 요청을 필터 적용에서 제외한다.
}

 

  • ignoring()을 통해, 필터에서 제외할 패턴 설정하는 방법
    1. mvcMatchers
    2. antMatchers
    3. regexMatchers
    4. requestMatchers

스프링 시큐리티의 핵심 원리

인증과 인가/권한

 

  • 인증(Authentication)
    • 자신을 증명하기 위해 자신의 정보를 제공한다.
    • ex) Login
  • 인가(Authorization)
    • 인증된 사용자가 접근 가능한 권한이 있는지 확인한다.
    • ex) 관리자 페이지

 

인증 처리 단계

  1. 사용자 아이디로 먼저 아이디에 해당하는 사용자 정보 로딩
  2. 로딩된 사용자의 정보로 패스워드 검증(Validation)

 

미리 구현된 인증 제공자 (Authentication Provider)를 이용해 인증처리를 수행하지만,

-> 일반적으로 인증 제공자는 그대로 사용하며, 인증 처리를 담당하는 객체를 커스터마이징해 사용한다.

 


UserDetailsService

인증 처리 객체 구현을 위한 인터페이스

 

UserDetailsService의 loadUserByUsername() 메서드

: 실제 인증을 처리하기 위해 호출되는 메서드로 아이디를 이용해 사용자 정보를 불러오는 메서드이다.

 

인증 구현 단계

  1. 시큐리티 설정 클래스에서 로그인 처리 추가
  2. UserDetailService 인터페이스 구현
  3. username을 이용해 사용자 정보를 불러온다.
  4. 불러온 사용자 정보의 패스워드를 검증한다.

 

1. CustomSecurityConfig의 filterChain()메서드에 로그인 메서드 호출 추가

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
    log.info("---configure---");
    
    //로그인 처리
    http.formLogin();

    return http.build();
}

 

2. security 패키지를 추가하고, UserDetailService 인터페이스를 구현한 CustomUserDetailsService 클래스 작성

-해당 유저 이름에 해당하는 정보가 존재하면, 확인된 유저 이름 출력하는 loadUserByUsername 메서드

-UserDetails 인터페이스 반환

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    log.info("loadUserByUsername : "+username);
    return null;
}

 

3. UserDetails 반환 타입

-해당 인터페이스 객체를 이용해 패스워드를 검사 후, 사용자 권한을 확인하는 방식으로 동작

  • UserDetails 인터페이스의 추상 메서드 목록
    1. getAuthorities() : 모든 인가 정보 반환
    2. getPassword() / getUserName()
    3. isAccountNonExpired() / isAccountNonLocked()
    4. isCredentialNonExpired()
    5. isEnabled()

 

 

(1) CustomUserDetailsService에서 UserDetails 인터페이스의 구현체를 반환하는 메서드 작성

-스프링 시큐리티가 제공하는 UserDetails 인터페이스 구현한 User 클래스 이용해 임시 로그인 처리 구현

-빌더 방식을 지원으로 User 클래스 반환

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    log.info("loadUserByUsername : "+username);
    UserDetails userDetails = User.builder()
            .username("user1")
            .password("1111")
            .build();

    return userDetails;
}

-> Password는 반드시 인코딩 필요

 

(2) PasswordEncoder 작성

-PasswordEncoder 인터페이스를 구현한 BCryptPasswordEncoder 클래스 반환하는 메서드 작성

@Bean
public PasswordEncoder passwordEncoder(){
    return new BCryptPasswordEncoder();
}

 

(2-1) CustomSecurityConfig에서 PasswordEncoder 확인

-BCryptPasswordEncoder 생성해 동작 확인

@Log4j2
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    //패스워드 인코더 의존성 주입
    private final PasswordEncoder passwordEncoder;

    public CustomUserDetailsService(){
        this.passwordEncoder=new BCryptPasswordEncoder();
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        log.info("loadUserByUsername: " + username);
//        return null;

        UserDetails userDetails = User.builder()
                .username("user1")
                //.password("1111")
                .password(passwordEncoder.encode("1111"))
                //권한 부여 필수
                .authorities("ROLE_USER")
                .build();

        return userDetails;
    }
}

 


4. 특정 경로에 시큐리티 적용

 

-어노테이션을 이용해 권한 체크

-코드로 작성하면 나중에 관리의 어려움이 있기 때문에, 어노테이션으로 이용하는 방식을 사용한다.

 

인증과 권한 여부에 따른 분류

  • 로그인 여부 관계 없이 :목록
  • 로그인이 필요한 작업 : 글쓰기
  • 로그인과 인가 둘다 필요 : 관리자 페이지

 

-@EnableGlobalMethodSecurity : 어노테이션으로 권한을 설정하는 어노테이션

  • prePostEnabled 속성 : @PreAuthorize, @PostAuthorize 사용 가능
  • securedEnabled 속성 : @Secured 사용 가능 
  • jsr250Enabled 속성 : @RoleAllowed 사용 가능

 

(1) CustomSecurityConfig 클래스에 어노테이션 추가

-prePostEnabled =true 설정으로, 사전 혹은 사후의 권한을 체크하는 @PreAuthorize, @PostAuthorize 어노테이션을 사용한다.

package org.zerock.b01.config;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.zerock.b01.security.CustomUserDetailsService;

@Log4j2
@Configuration
//사전 사후에 권한을 체크하는 속성을 위한 어노테이션
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class CustomSecurityConfig {

    //의존성 주입
    private final CustomUserDetailsService customUserDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        log.info("---configure---");

        //로그인 처리
        http.formLogin();

        return http.build();
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer(){
        log.info("---web configure---");

        //정적 파일을 시큐리티 설정에서 제외하는 설정
        return (web) -> web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
        //: 해당 요청을 필터 적용에서 제외한다.
    }



}

 

(2) 게시물 등록 URL 매핑 시, USER 권한 체크하도록 BoardController의 registerGet()에 어노테이션 추가

@Controller
@RequestMapping("/board")
@Log4j2
//사전 사후에 권한을 체크하는 속성을 위한 어노테이션
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class BoardController {

    //사전에 권한 체크하도록 추가
    @PreAuthorize("hasRole('USER')")
    @GetMapping("/register")
    public void registerGet(){

    }
}

 

로그인 하지 않는 사용자가 게시물 등록 시도시

  1. '/board/register' 호출
  2. @PreAuthorize에 걸려서 로그인 페이지로 이동
    1. 권한을 가진 사용자인지 체크
    2. 권한을 가진 사용자면 인가를 부여하고, @PreAuthorize에서 hasRole('USER')는 true가 된다.
  3. 스프링 시큐리티가 어떤 페이지에서 리다이렉트되었는지 저장
  4. 로그인 후에, 저장된 페이지로 자동 이동

 

@PreAuthorize / @PostAuthorize의 속성은 표현식으로 사용된 형태

표현식 설명
authenticated() 인증된 사용자들만 허용
permitAll() 모두 허용
anonymous() 익명의 사용자 허용
hasRole(표현식) 특정한 권한이 있는 사용자 허용
hasAnyRole(표현식) 여러 권한 중 하나만 존재해도 허용

 

5. 커스텀 로그인 페이지

-커스터마이징 가능한 별도의 로그인 페이지 적용

 

(1) CustomSecurityConfig의 filterChain() 메서드에 별도의 로그인 사용하도록 설정

-별도의 로그인 페이지는 사용자를 통한 로그인을 위해 "/member/login"으로 설정한다.

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
    log.info("---configure---");

    //별도의 로그인 페이지를 이용해 로그인 처리하도록 변경
    http.formLogin().loginPage("/member/login");

    return http.build();
}

 

(2) 사용자 로그인을 위해 MemberController 작성

-로그인 페이지 URL 매핑을 위한 loginGet() 메서드

  • 메서드 명 : loginGET
  • 파라미터 : error, logout
  • 특징 : 로그인 문제 발생 시, 로그아웃 시 사용하기 위한 파리미터를 전달한다.
package org.zerock.b01.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/member")
@Log4j2
@RequiredArgsConstructor
public class MemberController {
    
    @GetMapping("/login")
    //로그인 과정에서 문제 발생 시, 로그아웃 시 사용하기 위한 파라미터
    public void loginGET(String error, String logout){
        log.info("login get...");
        log.info("logout : "+logout);
    }
}

 

(3) 별도의 로그인 페이지 작성

-사용자를 통한 로그인을 위해 /member/login.html 작성

 

  • <form> 태그의 action 속성값은 '/member/login'으로 동일한 경로 지정 후, POST 방식으로 전송
  • <input> 태그의 name 속성값은 username과 password로 지정되어 있다.
    (추후에 password는 type속성값을 이용하도록 변경)

 

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <meta name="description" content="" />
    <meta name="author" content="" />
    <title>Simple Sidebar - Login</title>
    <!-- Favicon-->
    <link rel="icon" type="image/x-icon" th:href="@{/assets/favicon.ico}" />
    <!-- Core theme CSS (includes Bootstrap)-->
    <link th:href="@{/css/styles.css}" rel="stylesheet" />
</head>
<body class="align-middle" >
<div class="container-fluid d-flex justify-content-center" style="height: 100vh">

    <div class="card align-self-center">
        <div class="card-header">
            LOGIN Page
        </div>
        <div class="card-body">


            <form id="registerForm" action="/member/login" method="post">
                <div class="input-group mb-3">
                    <span class="input-group-text">아이디</span>
                    <input type="text" name="username" class="form-control" placeholder="USER ID">
                </div>
                <div class="input-group mb-3">
                    <span class="input-group-text">패스워드</span>
                    <input type="text" name="password" class="form-control" placeholder="PASSWORD">
                </div>

                <div class="my-4">
                    <div class="float-end">
                        <button type="submit" class="btn btn-primary submitBtn">LOGIN</button>
                    </div>
                </div>
            </form>
        </div><!--end card body-->
    </div><!--end card-->
</div>
</body>
</html>

 

(4) CSRF 토큰을 요구하는 스프링 시큐리티

-스프링 시큐리티는 GET 방식을 제외한 요청에 CSRF 토큰을 요구한다.

 

CSRF 토큰 (Cross-Site Request Forgery)

  • 크로스 사이트 간 요청 위조로 권한이 있는 사용자를 통해 사용자 모르게 요청 전송하는 공격
    : CSRF 토큰을 이용해 사용자가 사이트 이용시 매번 변경되는 문자열을 생성하고, 이를 요청시에 검증해 막는다.
  • 로그인 페이지 화면 내부에 '_csrf'라는 CSRF 토큰을 사용한다.
  • Ajax로 POST 방식 이용시에도 추가 작업이 필요하다.

 

CSRF 토큰 비활성화

-CustomSecurityConfig 클래스에 CSRF 비활성화 설정

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
    log.info("---configure---");

    //별도의 로그인 페이지를 이용해 로그인 처리하도록 변경
    http.formLogin().loginPage("/member/login");
    
    //CSRF 토큰 비활성화 설정
    http.csrf().disable();

    return http.build();
}

-> POST 방식의 로그인 처리 없이도 스프링 시큐리티가 로그인을 대신 수행한다.

 

 

반응형