Recording/멋쟁이사자처럼 BE 13기

[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_57일차_"Spring Security - 권한, 쿠키"

LEFT 2025. 2. 28. 17:49

🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [57]일차

🚀57차에는 Spring Security에서 사용자 별 권한을 부여하는 것과 쿠키에 대한 정보를 로그아웃을 통해서 실습해보았다.

학습 목표 : Spring Security 사용자 권한 부여 및 그에 따른 인가된 URL 페이지 실습 가능

학습 과정 : 회고를 통해 작성


Spring Security 로그인 

 

▶️실습 - 로그인 페이지 리다이렉트 (loginPage())

// 2. 인증없이 접근 가능한 URL 지정 및 (.requestMatchers())
// 로그인 페이지와 로그인 성공 시 이동할 페이지 설정 (.formLogin())
http
        .authorizeHttpRequests( auth -> auth
			.requestMatchers("/hello","/loginForm", "fail", "/test/*").permitAll()
			.anyRequest().authenticated()
                // 모든 요청에 대해서 인증을 요구
        )
        .formLogin(formLogin -> formLogin
                .loginPage("/loginForm") // 원하는 로그인 페이지 설정
                .defaultSuccessUrl("/success") // 인증에 성공하면 가고싶은 페이지 설정
                .failureUrl("/fail") // 인증에 실패하면 가고싶은 페이지 설정
                .usernameParameter("userId") // 로그인 폼에서의 Input 상자의 ID부분과 일치해야함
                .passwordParameter("password") // 로그인 폼에서의 Input 상자의 PASSWORD부분과 일치해야함
        );
return http.build();
  • permitAll() : 인증하지 않아도 접속하게 해달라는 것

  • loginPage("loginForm")
    ➡️
    기본으로 제공해주는 로그인 페이지가 아닌 사용자 정의의 로그인 페이지로 이동하게끔 할 것

 

로그인 성공 페이지, 로그인 실패 페이지 리다이렉트

  • defaultSuccessUrl("/success"), failureUrl("/fail")
    ➡️기본으로 제공하는 로그인 폼에서 로그인 성공시, 실패시 각각 success, fail페이지로 이동하도록 구현

  • fail페이지는 로그인 실패된 사용자도 보여지도록 해야하므로 requestMatchers()에 fail페이지를 추가해야함

▶️실습 - 로그인 성공/실패 시 할 동작 추가

.formLogin(formLogin -> formLogin
        .loginProcessingUrl("/login_proc") // 기본 login
        .loginPage("/loginForm") // 원하는 로그인 페이지 설정
        .defaultSuccessUrl("/success") // 인증에 성공하면 가고싶은 페이지 설정
        .failureUrl("/fail") // 인증에 실패하면 가고싶은 페이지 설정
        .usernameParameter("userId") // 로그인 폼에서의 Input 상자의 ID부분과 일치해야함
        .passwordParameter("password") // 로그인 폼에서의 Input 상자의 PASSWORD부분과 일치해야함
            
        .successHandler((request, response, authentication) -> {
            log.info("로그인 성공!" + authentication.getName());
            response.sendRedirect("/info"); // 로그인 성공 시 이동할 페이지 설정
        })

);
  • loginProcessingUrl("/login_proc")
    ➡️
    로그인 요청을 처리할 URL을 "/login_proc"으로 지정
    ➡️사용자가 로그인하면 이 URL로 POST 요청을 보내야 함
  • successHandler ( (...) ) : 로그인 성공 시 할 동작 추가
  • .authorizeHttpRequests( auth -> auth ... )
    ➡️HttpSecurity 객체 http는 보안 관련 설정을 정의함
    ➡️람다 표현식으로 auth 객체를 활용하여 권한 설정을 정의

  • .anyRequest().authenticated()
    ➡️
    .anyRequest() : 명시적으로 설정하지 않은 모든 요청
    ➡️
    .authenticated() : 인증된 사용자만 접근 가능

  • .usernameParameter("userId")
    ➡️로그인 폼에서의 Input 상자의 ID부분과 일치해야함
    ➡️로그인 폼에서 사용자 ID 필드명을 "userId"로 설정 (기본값은 "username")

  • response.sendRedirect("/info") : 로그인 성공 후 /info 페이지로 리다이렉트

  • 이 후 반환 값은 return http.build();
    ➡️설정을 마친 HttpSecurity 객체를 SecurityFilterChain으로 반환
  •  

❓.formLogin()  메소드

  • 특정 URL에 대해서는 인증 없이 접근 가능하도록 설정
  • 그 외의 모든 요청은 인증이 필요하도록 제한

Spring Security를 통한 인증 과정 흐름

-----------------------------------
사용자가 요청
-----------------------------------
		▼
-----------------------------------        
Spring Security 필터 체인
-----------------------------------
		▼
-----------------------------------        
1) 요청 URL 확인
-----------------------------------
		▼
-----------------------------------        
인증 없이 접근 가능한 URL 확인
("/hello", "/loginForm", "/fail",
"/test/*" 요청이면 그대로 통과
-----------------------------------
		▼
-----------------------------------        
2) 인증이 필요한 경우                 
- 로그인 페이지로 이동       
- 인증 성공 시 지정된 페이지로 이동
-----------------------------------

❓defaultSuccessUrl() vs successHandler() 차이점

  • .defaultSuccessUrl("/success") : 로그인 성공 후 특정 URL로 리디렉트
    ➡️단순 리디렉트만 수행

    로그인 성공 시 무조건 /success 페이지로 이동
    예외적으로, 사용자가 로그인 페이지 전에 특정 보호된 페이지에 접근하려 했다면, 로그인 후 해당 페이지로 자동 이동

    ex. /보호된페이지 → /loginForm(로그인) → /보호된페이지(자동 이동)
  • .successHandler((request, response, authentication) -> { ... }) : 로그인 성공 후 커스텀 로직 실행 가능
    ➡️리디렉트뿐만 아니라 추가적인 작업 수행 가능 (ex: 로깅, 세션 설정, 권한에 따라 다른 URL 이동 등)

    로그인 성공 후 사용자가 정의한 동작(=커스텀 로직)을 실행할 수 있음

    ➡️커스텀 로직의 예시
    - 로그인 성공 로그 남기기
    - 특정 세션 값 설정
    - 사용자 권한(Role)에 따라 다른 페이지로 이동
    - 비밀번호 변경이 필요한 사용자 감지 후 강제 이동

만약 둘 다 설정하면 Handler()가 우선 적용된다.


로그아웃 로직 추가

// 3. 로그아웃 기능 추가
http
        .logout(logout-> logout
                .logoutUrl("/logout")
                .logoutSuccessUrl("/hello") // 1) 단순히 로그아웃 시 이동할 페이지 설정

                // 2) 로그아웃 시 수행할 일 지정 가능
                .addLogoutHandler((request, response, authentication) -> {
                    log.info("로그아웃 세션, 쿠키 삭제!");
                    HttpSession session = request.getSession();
                    if(session != null){
                        session.invalidate(); // 세션 삭제
                    }
                })
                .deleteCookies("JSESSIONID") // 로그아웃 시에 원하는 쿠키를 삭제할 수 있음
);

  • 로그인 했을 경우 쿠키의 정보 확인 가능
  • NAME으로 JSESSIONID이므로, deleteCookies()로 로그아웃 시 쿠키가 삭제되도록 구현 가능

logback.xml 설정

<logger name="org.springframework.security" level="debug" additivity="false">
    <appender-ref ref="FILE" />
</logger>

<logger name="org.springframework.security" level="trace" additivity="false">
    <appender-ref ref="STDOUT" />
</logger>
  • ref=”STDOUT” : 콘솔에 로그를 출력하겠다는 것
  • ref=”FILE” : 파일에 로그를 저장한다는 것

  • 로그 레벨이 debug이면 FILE에 저장하고 로그 레벨이 trace이면 콘솔에 출력하겠다는 설정

로그 파일 관리 설정 추가

<appender name="DAILY_ROLLING_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>c:/logs/security_debug.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>c:/logs/security-%d{yyyy-MM-dd}.log</fileNamePattern>
        <maxHistory>30</maxHistory>
    </rollingPolicy>
    <encoder>
        <pattern>[%d{yyyy-MM-dd HH:mm:ss}] %-5level [%thread] %logger{36} - %msg%n</pattern>
    </encoder>
</appender>
  • 하나의 파일에 로그가 계속쌓이는 것을 방지하기 위해 파일의 용량에 따라 다른 로그 파일을 생성하도록 함

  • <fileNamePattern>c:/logs/security-%d{yyyy-MM-dd}.log</fileNamePattern>
    ➡️파일명생성에 날짜를 포함한 .log 파일을 만들도록 하는 설정 추가

자동로그인 기능 rememberMe()

@Configuration
@EnableWebSecurity
@Slf4j
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, UserDetailsService userDetailsService) throws Exception{
        http
                .authorizeHttpRequests(auth -> auth
                        .anyRequest().authenticated()
                )
                .formLogin(Customizer.withDefaults())
                .rememberMe(rememberMe -> rememberMe
                        .userDetailsService(userDetailsService)
                );

        return http.build();
    }
}

  • rememberMe() 메소드가 체크박스를 수행할 수 있는 자동로그인 기능을 구현 가능
    ➡️기억하기(자동 로그인) 기능 활성화 (.rememberMe())
  • UserDetailsService userDetailsService
    ➡️rememberMe() 기능을 위해 사용자 정보를 로드하는 서비스를 주입받음

  • .formLogin(Customizer.withDefaults())
    ➡️기본적인 "폼 로그인" 기능 활성화
    ➡️Customizer.withDefaults() : Spring Security의 기본 로그인 페이지 사용

즉 rememberMe() 기능을 사용 후 사용자가 "로그인 유지"를 체크하면 쿠키를 통해 자동 로그인 가능


토큰 기한 설정

.rememberMe(rememberMe -> rememberMe
        .tokenValiditySeconds(3600) // 하루 정도 토큰을 기억할 수 있게 설정
        .userDetailsService(userDetailsService)
);
  • .tokenValiditySeconds(3600)
    ➡️세션이 사라져도 기억할 수 있는 토큰을 어느 정도 가지고 있을지를 설정 가능
    3600: 하루 정도 토큰을 기억할 수 있게 설정

    rememberMe가 있으면 세션ID가 없어져도 자동로그인은 그대로 기능하는 것을 확인할 수 있다.

사용자 권한 부여

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, UserDetailsService userDetailsService) throws Exception{
    http
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/hello").permitAll()
                    .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                    .requestMatchers("/admin/abc").hasAnyRole("ADMIN")
                    .anyRequest().authenticated()
            );
    return http.build();
}
  • .requestMatchers("/hello").permitAll() .requestMatchers("/user/**").hasAnyRole("User", "ADMIN")
  • hasRole() : 역할 하나만 부여
  • hasAnyRole() : 역할 여러개 부여 (ex. hasAnyRole(”USER”, “ADMIN”))

  • .requestMatchers("/user/**")
    ➡️/user/로 시작하는 모든 URL을 포함
    ex. /user/profile | /user/settings | /user/dashboard | /user/orders/123

    /user/ 하위 경로는 모두 해당

비밀번호 인코딩

// 비밀번호 인코딩
@Bean
public PasswordEncoder passwordEncoder(){
    return new BCryptPasswordEncoder();
}
  • Security를 쓸때는 비밀번호로 쓸 값을 반드시 인코딩해서 보내야한다.
    (=Security는 암호화되지 않은 비밀번호를 받아들이지 않음)

    ➡️암호화되어있는 것만 취급

다양한 사용자 추가

  • 사용자 정보를 저장하고 인증하는 역할
  • 메모리 방식으로 유저정보를 관리하는 설정을 담음
 // 비밀번호 인코딩
@Bean
public PasswordEncoder passwordEncoder(){
    return new BCryptPasswordEncoder();
}
  • PasswordEncoder passwordEncoder
    ➡️
    비밀번호를 암호화하기 위한 인코더를 주입받음
    ➡️BCryptPasswordEncoder와 같은 암호화 알고리즘을 사용하여 비밀번호를 저장

 

@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder){
    UserDetails user = User.withUsername("user")
            .password(passwordEncoder.encode("1234")) // 원하는 패스워드를 넣음
            .roles("USER")
            .build();

    UserDetails user2 = User.withUsername("Gordon")
            .password(passwordEncoder.encode("1234")) // 원하는 패스워드를 넣음
            .roles("USER", "ADMIN")
            .build();

    return new InMemoryUserDetailsManager(user, user2);
}
  • UserDetails : Spring Security에서 사용자 정보를 담는 객체

  • .password() : 사용자의 비밀번호를 설정
    passwordEncoder.encode("1234")
    ➡️
    "1234"라는 문자열을 암호화(해싱)하여 저장
    ➡️암호화된 비밀번호만 저장하고, 원본 비밀번호를 직접 저장하지 않음

  • .roles("USER") : 사용자의 역할(Role)을 설정
    ➡️내부적으로 ROLE_USER로 변환됨 (roles()를 사용하면 자동으로 ROLE_이 붙음)
    ➡️사용자는 ROLE_USER 권한을 가지게 됨
  • InMemoryUserDetailsManager
    ➡️메모리에 사용자 정보를 저장하는 Spring Security의 기본 구현체
    이 설정을 통해 Spring Security가 메모리에 사용자 정보를 저장하고 관리
    메모리로 관리하기때문에 애플리케이션을 재시작 시 모든 유저 정보가 초기화

▶️실습 - 로그인 과정
1) http://localhost:8080/login 접속
2) 유저 로그인 : user(1234), admin(admin1234), superuser(super1234)
3) 특정 URL 각각 접속하여 접근 가능 여부 확인


▶️실습 - 로그인된 사용자 가져오기

@GetMapping("/info")
public String info() {
    String message = null;
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    if (authentication == null || !authentication.isAuthenticated()) {
        message = "로그인된 사용자가 없습니다.";
    }
    Object principal = authentication.getPrincipal();
    if (principal instanceof UserDetails) {
        UserDetails userDetails = (UserDetails) principal;
        message = "현재 로그인 한 사용자 : " + userDetails.getUsername();
    } else {
        message = "현재 로그인 한 사용자 : " + principal.toString();
    }
    return message;
}
  • SecurityContextHolder
    ➡️현재 로그인한 사용자의 정보를 가져와서 로그인 여부 및 사용자명을 확인하는 역할
    (=현재 실행 중인 스레드의 보안 컨텍스트를 관리하는 클래스)
  • 사용자가 로그인하지 않았다면 "로그인된 사용자가 없습니다." 메시지 반환
  • 사용자가 로그인했다면 "현재 로그인한 사용자: [유저네임]" 반환

  • SecurityContextHolder.getContext().getAuthentication()
    ➡️
    Spring Security에서 현재 로그인한 사용자의 인증(Authentication) 정보를 가져옴

    getContext().getAuthentication() : 현재 로그인한 사용자의 인증 정보를 가져올 수 있음

  • authentication == null : 로그인하지 않은 경우 authentication 객체가 null일 수 있음
  • !authentication.isAuthenticated() : false이면 로그인되지 않은 상태

  • authentication.getPrincipal() : 현재 로그인한 사용자의 주체(Principal) 정보를 가져옴
    ➡️principal은 로그인한 사용자의 UserDetails 객체 또는 문자열일 수 있음
    ➡️현재 로그인한 사용자의 정보를 가져와 principal 변수에 저장

  • if (principal instanceof UserDetails) { ... }
    ➡️
    principal이 UserDetails 타입인지 확인

    UserDetails : Spring Security에서 사용자의 정보를 담는 인터페이스 (username, password, roles 등의 정보 포함)

    principal이 UserDetails 타입이면, 로그인한 사용자의 정보를 가져올 수 있게됨

  • UserDetails userDetails = (UserDetails) principal;
    ➡️UserDetails 객체로 캐스팅 후, getUsername()을 호출하여 사용자 이름을 가져옴

Authentication (인증 정보 객체)

  • 현재 로그인한 사용자의 인증 정보를 저장하는 객체입니다.
  • 로그인하면 Authentication 객체가 생성되어 SecurityContext에 저장됨
  • 주요 메소드
    - getPrincipal()
    : 로그인한 사용자의 객체 (UserDetails 또는 문자열)
    - getAuthorities() : 사용자의 역할(Role) 목록 
    - isAuthenticated()
    : 사용자가 인증되었는지 여부 (true or false)

SecurityContextHolder (보안 컨텍스트 저장소)

  • 현재 실행 중인 스레드에서 SecurityContext를 관리하는 클래스
  • SecurityContext는 로그인한 사용자의 Authentication 객체를 보관
  • 주요 메소드
    - getContext() : 현재 스레드의 보안 컨텍스트(SecurityContext) 가져오기
    - setContext(SecurityContext context)
    : 보안 컨텍스트 설정

AuthenticationManager (인증 매니저)

  • AuthenticationManager는 사용자의 인증을 담당하는 인터페이스
  • 로그인 요청이 들어오면 authenticate()를 호출하여 인증 수행
  • authenticate(Authentication authentication) : 인증 처리 후 Authentication 객체 반환

▶️실습 - 사용자 정보 페이지 생성

의존성 추가

implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.2.RELEASE'
  • 뷰에 출력하기 위한 타임리프+시큐리티 추가

 

사용자 정보페이지 HTML

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>사용자 정보 페이지</title>
</head>
<body>
<h2>로그인한 사용자 정보</h2>
<div sec:authorize="isAuthenticated()">
    <!--로그인한 사용자는 isAuthenticated()의 결과가 true이므로 이 부분이 실행됨-->
    <p>사용자 명 : <span sec:authentication="name"></span></p>
    <p>권한 : <span sec:authentication="principal.authorities"></span></p>
</div>

<div sec:authorize="!isAuthenticated()">
    <!-- 로그인하지 않은 사용자를 보여주는 부분-->
    <p>로그인이 필요합니다.</p>
    <a th:href="@{/login}">로그인 화면으로 이동</a>
</div>
</body>
</html>
  • sec : security가 가지고 있는 객체 중에 가져오는 문법

이를 통해 로그인한 사용자, 사용자 명, 로그인 필요 시 하이퍼링크 출력까지 확인할 수 있다.


🚀회고 결과 :
이번 회고에서는 로그아웃, 사용자 추가 및 로그아웃 등에 대한 실습을 해볼 수 있었고 

관련한 클래스들과 메소드들의 개념에 대해서 더 익힐 수 있었다. 

- 로그인, 로그아웃의 과정 파악
- 자동 로그인 기능

느낀 점 : 
학습하면서 부족했던 메소드와 클래스들에 대한 개념을 회고를 통해 익힐 수 있었다.
Thymeleaf+Security 의존성에서는 sec (=security)와 같은 문법을 통해 가져오는 것 또한 배울 수 있던 회고였다.

향후 계획 : 

- 각 메소드들에 대한 활용법
- 각각의 사용자에 권한을 부여 후 다양한 페이지에서 실습 필요