자바 스프링 부트 14일차 /b01Security 회원가입(Security) 핵심 설정 및 커스텀 인증 시스템 구축

2026. 4. 27. 21:03대우개발원 수업 내용/spring boot, framework

반응형

https://docs.spring.io/spring-security/reference/index.html

를 참고 했습니다.


Spring Security 핵심 설정 및 커스텀 인증 시스템 구축

1. 보안 의존성 추가 및 기본 필터 체인(SecurityFilterChain) 설정

  • 의존성: spring-boot-starter-security 추가 시 모든 경로가 잠기며 초기 비밀번호가 콘솔에 출력됨
  • 필터 체인: HttpSecurity를 통해 인증·인가 정책을 정의하며, 거의 모든 보안 처리가 이 체인 내부에서 실행됨
  • 동작: 서블릿 필터 단계에서 DelegatingFilterProxy가 스프링 빈과 연동하여 보안 로직을 위임함

2. 정적 리소스 예외 및 패스워드 암호화(PasswordEncoder)

  • 정적 리소스: webSecurityCustomizer를 통해 CSS/JS 등은 보안 필터를 거치지 않도록 설정해 성능을 최적화함
  • BCrypt: 비밀번호는 반드시 BCryptPasswordEncoder를 통해 암호화되어야 하며, 시큐리티는 이 빈을 자동 감지해 로그인 시 비교에 활용함

3. 사용자 정의 인증 로직(CustomUserDetailsService) 구현

  • UserDetailsService: DB 연동 전, 특정 아이디("user1")와 암호화된 비번을 가진 UserDetails 객체를 수동 생성해 인증 통로를 마련함
  • 유연성: 기본 메모리 방식 인증을 넘어, 향후 실제 DB의 회원 테이블과 연동할 수 있는 핵심 인터페이스임

4. 메서드 보안 및 @PreAuthorize 권한 제어

  • 활성화: @EnableMethodSecurity를 설정 클래스에 추가하여 메서드 단위의 세밀한 보안 설정을 허용함
  • 권한 체크: 컨트롤러 메서드 상단에 @PreAuthorize("hasRole('USER')")를 작성해 로그인된 특정 권한 사용자만 접근 가능하도록 제한함

5. 커스텀 로그인 페이지 연동 및 CSRF 최적화

  • Login Page: 기본 제공 창 대신 /member/login 경로의 커스텀 페이지를 호출하도록 설정하고 MemberController로 폼을 띄움
  • CSRF: 개발 편의 및 비동기 통신 테스트를 위해 CSRF 보호를 임시 비활성화(disable())하여 정상적인 폼 제출을 확인함

[요약]

Spring Security 핵심 설정 및 커스텀 인증 시스템 구축

1. SecurityFilterChain 설정을 통한 전역 보안 정책 및 필터 체인 제어

2. 정적 리소스(CSS/JS) 보안 제외 및 BCrypt 비밀번호 암호화 빈 등록

3. UserDetailsService 커스텀 구현으로 사용자 정보 로딩 및 인증 객체 생성

4. @EnableMethodSecurity와 @PreAuthorize 기반의 메서드 단위 접근 권한 통제

5. 사용자 정의 로그인 UI(/member/login) 연결 및 CSRF 비활성화 설정


b01Upload폴더를 복사해서 b01Security로 이름을 바꿔 붙여넣는다.


정상적으로 github에 업로드된 모습


bulid.gradle

더보기
implementation 'org.springframework.boot:spring-boot-starter-security'

코드를 추가


CustomSecurityConfig 클래스를 추가

 

CustomSecurityConfig에 아래 코드를 입력

더보기
package com.example.b01.config;

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

@Log4j2
@Configuration
@RequiredArgsConstructor

public class CustomSecurityConfig {
}

 

실행하면

로그인을 요청하는 화면을 볼 수 있음

패스워드가 로그에 뜨고 아이디는 기본 user이다


로그를 찍을 수 있게 아래 코드를 추가

더보기
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    log.info("-----------------configure---------------");
    return http.build();
}

Security Filter Chain

Spring Security 에서 제공하는 인증,인가를 위한 필터들의 모음
Spring Security 에서 가장 핵심이 되는 기능을 제공하며, 거의 대부분의 서비스는 Security Filter Chain 에서 실행
기본적으로 제공하는 필터들이 있으며, 사용자는 개발의 취지와 목적에 맞게 커스텀 필터 또한 필터 체인으로 포함시켜 사용가능

Http 요청 -> Web Application Server(Servlet Container) -> 필터 1 -> 필터 2 ..... -> 필터 n -> Servlet -> 컨트롤러

 

더보기

Security Filter Chain 정의
Spring Security에서 제공하는 인증 및 인가를 위한 필터들의 모음임. 

서비스의 핵심 기능을 담당하며 거의 대부분의 기능이 이 체인 내에서 실행됨. 

기본 필터 제공 외에도 사용자의 목적에 따른 커스텀 필터 포함이 가능함.

동작 흐름 및 초기화
Http 요청이 WAS의 필터들을 거쳐 서블릿과 컨트롤러에 도달하는 구조임. 

사용자의 인증 및 인가 과정이 어떻게 진행되는지 파악하는 것이 핵심임. 

Application Context 초기화 과정에서 사용자가 정의한 설정에 따라 생성됨. 

설정 클래스 내 HttpSecurity 객체가 설정을 기반으로 필터 체인을 구성함.

주요 필터 구성

1. SecurityContextPersistenceFilter
SecurityContextRepository에서 SecurityContext를 가져오거나 생성하는 역할임. 

SecurityContext는 인증 객체인 Authentication을 저장하는 저장소이며 SecurityContextHolder를 통해 전역적 접근이 가능인증 이력에 따라 기존 컨텍스트를 가져오거나 새로 생성하여 저장함.

2. LogoutFilter
로그아웃 요청을 전담하여 처리하는 필터임. 요청 URI가 로그아웃 경로인지 확인한 후 세션 무효화, 

쿠키 삭제, SecurityContext 내 토큰 삭제 등의 작업을 수행함. 설정 시 HttpSecurity 객체를 통해 관리함.

3. UsernamePasswordAuthenticationFilter
폼 기반 인증을 처리하며 아이디와 패스워드 데이터를 파싱하여 인증을 위임함. 인증 전 객체인 UsernamePasswordAuthenticationToken을 생성하여 AuthenticationManager에 전달함. 

API 기반 인증 시에는 해당 필터를 비활성화하고 직접 구현한 필터를 사용함.

4. ExceptionTranslationFilter
필터 체인 실행 중 발생하는 예외를 처리함. 

인증 실패 시에는 AuthenticationEntryPoint를 호출하여 로그인 페이지 리다이렉트나 상태 코드 반환을 수행함. 

인가 예외 시에는 AccessDeniedHandler를 호출하여 권한 부족에 대한 예외 처리를 진행함.


application.properties 에 아래 코드 추가

더보기
logging.level.org.springframework.security=trace

실행하면

로그에서 적용되는 필터의 정보를 다 확인이 가능하다


CustomSecurityConfig 코드를 수정

Static에서는 필터를 사용하지 않겠다는 코드를 적용할 수 있다

더보기
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
    log.info("-----------------web configure---------------");
    return (WebSecurity web) -> web.ignoring().requestMatchers(PathRequest.
            toStaticResources().atCommonLocations());
}

코드를 추가하고

상단에

@Bean

을 추가함

실행하면

http://localhost:8080/css/styles.css에 접근했을때

필터가 사용되지 않는다는 로그가 뜬다

 


🌱 DelegatingFilterProxy

더보기

사용자의 요청이 Spring MVC 에 도달하기 전, 즉 Servlet Container 에서 Delegating Filter Proxy 가 요청을 받습니다.
DelegatingFilterProxy 는 Servlet Container 와 Spring 의 Spring Container 을 연결해주는 필터입니다.
DelegatingFilterProxy 는 Servlet 스펙에 있는 기술이기 때문에 Servlet Container 에서만 생성되고 실행됩니다.
Spring 의 Spring Container 와는 다름, Spring Bean 으로 주입하거나 Spring 에서 사용되는 기술을 Servlet 에서 사용🚫
DelegatingFilterProxy 는 실제 보안 처리를 하지 않고 위임만 하는 Servlet Container 에서 동작하는 Servlet Filter

 


CustomSecurityConfig 클래스 안에 있는

SecurityFilterChain 메소드에 아래 코드를 추가

더보기
http.formLogin(withDefaults());

security 패키지를 추가하고

CustomUserDetailsService 클래스를 추가

CustomUserDetailsService 클래스에 아래 코드를 추가

 

스프링 시큐리티(Spring Security)에서 사용자 인증을 처리하기 위해 기본적으로 제공하는 설정 대신,

개발자가 직접 사용자 정보를 조회하고 검증할 수 있도록 설계된 사용자 정의 인증 서비스.

더보기
  1. 인증 인터페이스 구현: UserDetailsService 인터페이스를 상속받아 구현함으로써,
    스프링 시큐리티의 표준 인증 프로세스 내에서 커스텀 로직(DB 연동 등)이 작동할 수 있는 통로를 마련함.
  2. 사용자 식별 로직: loadUserByUsername 메서드를 오버라이드하여, 로그인 시 입력받은 아이디(username)를
    기반으로 데이터베이스나 외부 저장소에서 사용자 정보를 가져오는 역할을 수행함.
  3. 로깅 및 모니터링: log.info를 활용하여 실제 인증 요청이 들어온 사용자의 정보를 로그에 기록함으로써,
    인증 프로세스의 진입 단계에서 정상 동작 여부를 확인할 수 있도록 함.
  4. 인증 객체 반환 지점: 최종적으로 사용자의 비밀번호와 권한 정보가 담긴 UserDetails 객체를 반환해야 하며,
    현재는 null을 반환하고 있어 실제 서비스 시에는 고유 객체를 구성하여 리턴하는 로직이 필요함.
  5. 확장성 확보: 기본 메모리 방식의 사용자 관리가 아닌, 실제 애플리케이션의 도메인 모델(Member 등)과 연동하여
    보안 시스템을 유연하게 확장할 수 있는 기초 구조를 제공
package com.example.b01.security;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
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;

@Log4j2
@Service
@RequiredArgsConstructor

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

실행하고

http://localhost:8080/login

에 접속하고

더보기

틀린 비밀번호를 입력하면


CustomUserDetailsService 에 코드를 수정

스프링 시큐리티의 인증 과정에서 특정 아이디로 로그인을 시도할 때,

시스템이 인식할 수 있는 사용자 정보(UserDetails)를 메모리상에 임시로 생성하여 반환하는 테스트용 인증 로직임.

더보기
  1. 사용자 정보 객체 생성: 스프링 시큐리티에서 제공하는 User.builder()를 사용하여 인증에 필요한
    최소한의 정보인 아이디, 비밀번호, 권한을 포함하는 객체를 구성함.
  2. 하드코딩된 인증 데이터: 데이터베이스 조회 대신 아이디는 "user1", 비밀번호는 "1111"로 고정하여,
    실제 DB 연동 전 단계에서 로그인 기능의 정상 작동 여부를 확인하기 위한 용도로 사용함.
  3. 권한 부여(Authorities): 해당 사용자에게 "ROLE_USER"라는 권한을 할당함으로써,
    시큐리티 설정에서 특정 경로에 대해 접근 제어가 적절히 이루어지는지 테스트할 수 있게 함.
  4. 인증 객체 반환: 생성된 userDetails 객체를 리턴함으로써 스프링 시큐리티는
    이 정보와 사용자가 입력한 정보를 비교하여 로그인 성공 여부를 결정하게 됨.
  5. 실제 연동을 위한 교두보: 현재는 고정된 값을 반환하고 있으나,
    이후 단계에서는 이 메서드 내부에서 Repository를 호출하여 실제 DB에 저장된 회원 정보를 조회하는 로직으로 대체
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("loadUserByUsername: " + username);
        UserDetails userDetails = User.builder()
                .username("user1")
                .password("1111")
                .authorities("ROLE_USER")
                .build();
        return userDetails;
//        return null;
    }

실행하면

user1

1111으로 로그인을 하면 오류가 뜨는데 아직 패스워드 인코딩(  DelegatingPasswordEncoder )을 안해서 그럼


CustomSecurityConfig 에 아래 코드 추가

스프링 시큐리티에서 비밀번호를 안전하게 암호화하기 위해 사용하는 BCryptPasswordEncoder를

스프링 빈(Bean)으로 등록하는 설정 로직임.

더보기
  1. 암호화 알고리즘 지정: 단방향 해시 함수인 BCrypt 알고리즘을 사용하는 객체를 생성하여, 비밀번호를 평문이 아닌 복잡한 암호문으로 저장할 수 있게 함.
  2. 빈 등록의 목적: @Bean 어노테이션을 통해 해당 객체를 스프링 컨테이너에 등록함으로써, 애플리케이션 어디에서든 PasswordEncoder를 주입받아 사용할 수 있는 공용 컴포넌트로 만듦.
  3. 보안 표준 준수: 사용자가 입력한 비밀번호를 데이터베이스에 저장하거나 로그인 시 비교할 때, 보안상 권장되는 암호화 방식을 적용하여 시스템의 보안 강도를 높임.
  4. 자동 설정과 연동: 스프링 시큐리티는 빈으로 등록된 PasswordEncoder를 자동으로 감지하여, 로그인 과정에서 제출된 비밀번호와 저장된 비밀번호를 비교할 때 내부적으로 활용함.
  5. 유연한 관리: 서비스 전체에서 동일한 암호화 방식을 일관되게 적용할 수 있으며, 나중에 암호화 정책이 변경되더라도 이 설정 부분만 수정하면 되므로 유지보수가 용이함.
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

CustomUserDetailsService 의 코드를 수정

스프링 시큐리티의 인증 과정에서 사용자 비밀번호를 안전하게 처리하기 위해 암호화 알고리즘을 도입하고,

인증 객체를 생성하는 로직임.

더보기
  1. 암호화 도구 설정: BCryptPasswordEncoder를 직접 생성하여 passwordEncoder 멤버 변수에 할당함으로써, 평문 비밀번호를 해시화된 문자열로 변환할 수 있는 기반을 마련함.
  2. 비밀번호 보안 강화: User.builder()를 통해 사용자 정보를 만들 때, 단순 문자열인 "1111"을 그대로 사용하지 않고 encode 메서드를 거쳐 암호화된 상태로 저장함.
  3. 시큐리티 표준 준수: 스프링 시큐리티는 저장된 암호와 입력된 암호를 비교할 때 암호화된 상태를 기대하므로, 인코딩 로직을 추가하여 실제 로그인 인증이 가능하도록 수정함.
  4. 사용자 정보 빌드: 아이디(user1), 암호화된 비밀번호, 그리고 ROLE_USER라는 권한을 하나의 UserDetails 객체로 묶어 시큐리티 인증 엔진에 전달함.
  5. 로깅을 통한 추적: 메서드 시작 부분에 로그를 남겨 어떤 아이디로 인증 요청이 들어왔는지 확인하며, 리턴된 객체는 이후 시큐리티 시스템에 의해 비밀번호 일치 여부 검증에 쓰임
//@RequiredArgsConstructor

public class CustomUserDetailsService implements UserDetailsService {
    private PasswordEncoder passwordEncoder;
    public CustomUserDetailsService() {
        this.passwordEncoder = new BCryptPasswordEncoder();
    }


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("loadUserByUsername: " + username);
        UserDetails userDetails = User.builder()
                .username("user1")
//                .password("1111")
                .password(passwordEncoder.encode("1111")) // 패스워트 인코드

실행하면


Spring Security @EnableMethodSecurity

더보기

Spring Security의 메서드 수준 보안을 활성화하는 데 사용되는 어노테이션입니다.

이 어노테이션을 통해 특정 메서드 또는 클래스에 접근 제어를 적용할 수 있습니다. 

@EnableMethodSecurity는 다양한 메서드 보안 어노테이션(@PreAuthorize, @PostAuthorize, @Secured 등)을 

사용할 수 있게 해줍니다.


• 주요 기능
@PreAuthorize: 메서드가 호출되기 전에 접근을 허용할지 결정합니다.
@PostAuthorize: 메서드가 호출된 후에 접근을 허용할지 결정합니다.
@Secured: 특정 롤(role)로 접근을 제한합니다.
@RolesAllowed: 특정 롤(role)로 접근을 제한합니다 (JSR-250)

 

• 어노테이션 내 사용 가능한 함수
 hasRole([role]) : 현재 사용자의 권한이 파라미터의 권한과 동일한 경우 true
 hasAnyRole([role1,role2 ...]) : 현재 사용자의 권한 파라미터의 권한 중 일치하는 것이 있는 경우 true
 principal: 사용자를 증명하는 주요객체(User)를 직접 접근할 수 있다.
 authentication : SecurityContext에 있는 authentication 객체에 접근 할 수 있다.
 permitAll : 모든 접근 허용
 denyAll : 모든 접근 비허용
 isAnonymous() : 현재 사용자가 익명(비로그인)인 상태인 경우 true
 isRememberMe() : 현재 사용자가 RememberMe 사용자라면 true
 isAuthenticated() : 현재 사용자가 익명이 아니라면 (로그인 상태라면) true
 isFullyAuthenticated() : 현재 사용자가 익명이거나 RememberMe 사용자가 아니라면 true


CustomUserDetailsService 클래스에

메서드 단위에서 @PreAuthorize나 @PostAuthorize 같은 보안 어노테이션을 사용하여 권한 제어를

세밀하게 설정할 수 있도록 활성화하는 설정

@EnableMethodSecurity(prePostEnabled = true)

를 사용할 수 있게 true를 주는 어노테이션을 추가


BoardController  @GetMapping("/register")위에 아래 코드를 추가

 

메서드 실행 전 호출자의 권한을 체크하여, USER 권한(ROLE_USER)을

가진 사용자만 해당 기능을 사용할 수 있도록 제한하는 보안 설정

더보기
@PreAuthorize("hasRole('USER')")

실행하면

더보기

를 삭제하고

다시 http://localhost:8080/board/register 로 접속하면

로그인창이 먼저 뜨고

USER / 1111를 입력해서 register화면으로 들어갈 수 있게 된다.


CustomSecurityConfig 코드를 수정

스프링 시큐리티의 기본 로그인 페이지 대신 사용자가 직접 만든 /member/login 경로의

커스텀 로그인 페이지를 사용하도록 설정하는 코드

더보기
//            http.formLogin(withDefaults());
            http.formLogin(httpSecurityFormLoginConfigurer -> {
                httpSecurityFormLoginConfigurer.loginPage("/member/login");
            });

하지만 현재 member는 없고 board 밖에 없기 때문에


member에 접근하는 MemberController가 있어야 함

MemberController  생성 한다.

더보기
package com.example.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 errorCode, String logout) {
        log.info("login get.................");
        log.info("logout : " + logout);
    }
}

member폴더 추가 후 login.html생성

더보기
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>LOGIN Page</h1>
</body>
</html>

프로젝트를 실행해서 http://localhost:8080/board/register에 접속하면

member/login으로 리다이렉트가 되고

http://localhost:8080/login으로 접속하면 에러 페이지가 뜬다.


이제 로그인 페이지의 디자인을 바꿔야하는데

login.html

더보기
<!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>

바꾸고 실행하면

디자인은 폼은 제대로 나온다

하지만 아직 controller에서 로그만 보내고 보내주질 않기때문에


CustomSecurityConfig 코드에 추가

더보기
http.csrf(httpSecurityCsrfConfigurer -> {
    httpSecurityCsrfConfigurer.disable();
});

이 코드를 추가하고 로그를 확인해보면 csrf에 대한 요청을 더 이상 하지 않는것을 확인할 수 있다.

실행하면

 http://localhost:8080/board/register에 접속하면

member/login으로 리다이렉트가 되고 로그인을 했을때, http://localhost:8080/board/register로 정상적으로 이동이된다.