프로젝트

일반

사용자정보

Spring Security

Prof. Jong Min Lee이(가) 17일 전에 추가함

Spring Security는 Spring 기반 애플리케이션에서 인증 및 권한 부여를 처리하는 강력한 보안 프레임워크입니다. 주요 기능을 간략히 정리하면 다음과 같습니다:

  • 인증(Authentication): 사용자의 신원을 확인하는 과정으로, 로그인 기능을 제공하며 다양한 인증 방식(예: 폼 로그인, OAuth2, JWT, LDAP 등)을 지원합니다.
  • 권한 부여 또는 인가(Authorization): 인증된 사용자가 특정 리소스에 접근할 수 있는지 결정합니다. 역할 기반 접근 제어(RBAC) 및 표현식 기반 접근 제어를 지원합니다.
  • 보안 필터(Security Filters): 요청 및 응답을 가로채어 필요한 보안 검사를 수행합니다. 여러 개의 필터 체인을 통해 세분화된 보안 제어가 가능합니다.
  • 보안 설정(Security Configuration): Java 및 XML 기반으로 설정할 수 있으며, 최신 버전에서는 @EnableWebSecuritySecurityFilterChain을 사용한 설정 방식이 주를 이룹니다.
  • 비밀번호 암호화: BCryptPasswordEncoder 등을 이용해 안전한 비밀번호 저장을 지원합니다.
  • 세션 관리(Session Management): 세션 고정 보호, 동시 세션 제어 등 다양한 세션 보안 기능을 제공합니다.
  • OAuth2 및 JWT 지원: API 보안 및 소셜 로그인 같은 인증 방식을 구현할 때 널리 사용됩니다.

Spring Security를 활용하면 애플리케이션의 보안을 효과적으로 강화할 수 있습니다.

schema.sql (407 Bytes)

application.properties Magnifier (2.22 KB)


답글 (4)

인증과 인가의 차이점 - Prof. Jong Min Lee이(가) 17일 전에 추가함

인증(Authentication)과 인가(Authorization)는 Spring Security에서 보안의 핵심적인 두 가지 개념이지만, 그 목적과 과정에서 명확한 차이를 보입니다.

인증 (Authentication): "당신은 누구입니까?"

  • 목적: 사용자가 주장하는 신원을 확인하는 과정입니다. 즉, 사용자가 누구인지 증명하는 것입니다.
  • 과정: 일반적으로 사용자 이름과 비밀번호, 생체 인식, OTP(One-Time Password) 등 다양한 방법을 통해 사용자의 신원을 확인합니다.
  • 결과: 인증에 성공하면 시스템은 해당 사용자가 누구인지 알게 됩니다. 이 정보는 Principal 객체 형태로 표현됩니다.
  • 예시: 웹사이트에 로그인하는 과정 (아이디와 비밀번호를 입력하여 본인임을 증명).
  • Spring Security 관련: AuthenticationManager, AuthenticationProvider, UserDetailsService 등의 컴포넌트가 인증 과정을 처리합니다. 인증 성공 시 Authentication 객체가 생성되어 SecurityContextHolder에 저장됩니다.
  • 실패 시: 일반적으로 401 Unauthorized 에러가 발생합니다.

인가 (Authorization): "당신이 그것을 할 권한이 있습니까?"

  • 목적: 인증된 사용자가 특정 리소스에 접근하거나 특정 작업을 수행할 권한이 있는지 확인하는 과정입니다.
  • 과정: 인증된 사용자의 역할(Role)이나 권한(Authority)을 기반으로 접근 제어 규칙을 적용하여 권한을 확인합니다.
  • 결과: 인가에 성공하면 사용자는 요청한 리소스에 접근하거나 작업을 수행할 수 있습니다.
  • 예시: 로그인한 사용자가 자신의 프로필 정보를 조회하는 것은 허용되지만, 다른 사용자의 정보를 수정하는 것은 허용되지 않을 수 있습니다.
  • Spring Security 관련: AccessDecisionManager, AccessDecisionVoter, @PreAuthorize, @PostAuthorize 등의 어노테이션, HttpSecurityauthorizeHttpRequests() 설정 등을 통해 인가를 처리합니다.
  • 실패 시: 일반적으로 403 Forbidden 에러가 발생합니다.

핵심적인 차이점:

특징 인증 (Authentication) 인가 (Authorization)
질문 "당신은 누구입니까?" "당신이 그것을 할 권한이 있습니까?"
목적 사용자 신원 확인 사용자의 리소스 접근 권한 확인
시점 먼저 수행됨 인증 후에 수행됨
정보 신원 증명 정보 (아이디, 비밀번호 등) 역할, 권한 정보
결과 사용자 식별 (Principal 객체) 접근 허용 또는 거부
실패 시 에러 401 Unauthorized 403 Forbidden

간단한 비유:

  • 인증: 클럽의 회원인지 확인하기 위해 신분증을 제시하는 것과 같습니다.
  • 인가: 회원의 등급에 따라 클럽 내 특정 구역에 출입하거나 특정 서비스를 이용할 수 있는 권한을 부여받는 것과 같습니다.

Spring Security에서는 일반적으로 인증 과정을 먼저 거친 후, 인증된 사용자에 대해 인가 과정을 수행하여 애플리케이션의 보안을 강화합니다.

SpringSecurityConfig.java 수정 - Prof. Jong Min Lee이(가) 17일 전에 추가함

/*
 * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
 * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
 */
package deu.se.security_demo.security;

import jakarta.servlet.DispatcherType;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collection;
import javax.sql.DataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Service;

/**
 * https://docs.spring.io/spring-security/reference/index.html
 *
 * @author jongmin
 */
@Configuration
@EnableWebSecurity
@Slf4j
public class SpringSecurityConfig {

    @Autowired
    ServletContext sc;

    @Bean
    public MyUserDetailsService myUserDetailsService(DataSource dataSource) {

        UserDetails user1
                = User.withUsername("admin")
                        .password(passwordEncoder().encode("admin"))
                        .roles("ADMIN")
                        .build();
        UserDetails user2
                = User.withUsername("manager")
                        .password(passwordEncoder().encode("manager"))
                        .roles("MANAGER")
                        .build();

        log.info("user1 = {},\nuser2 = {}", user1, user2);
        InMemoryUserDetailsManager inMemoryManager = new InMemoryUserDetailsManager(user1, user2);

        JdbcUserDetailsManager jdbcManager = new JdbcUserDetailsManager(dataSource);
        if (!jdbcManager.userExists("gildong")) {
            UserDetails user3 = User.builder()
                    .username("gildong")
                    .password(passwordEncoder().encode("gildong"))
                    .roles("USER")
                    .build();
            UserDetails user4 = User.builder()
                    .username("woochi")
                    // .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
                    .password(passwordEncoder().encode("woochi"))
                    .roles("USER")
                    .build();

            jdbcManager.createUser(user3);
            jdbcManager.createUser(user4);
        }
        return new MyUserDetailsService(inMemoryManager, jdbcManager);
    }

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

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        // return (web) -> web.ignoring().requestMatchers("/js/**", "/css/**");
        return (web) -> web.ignoring()
                .requestMatchers(new AntPathRequestMatcher("/js/**"))
                .requestMatchers(new AntPathRequestMatcher("/css/**"));
    }

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

        // Cross-Site Request Forgery 보호 설정
        // .csrf.disable()을 할 경우 csrf 보호가 비활성화됨.
        http.csrf(csrf -> csrf
                // 일부 URL만 CSRF 보호를 비활성화
                .ignoringRequestMatchers("/")
                .ignoringRequestMatchers("/welcome")
                .ignoringRequestMatchers("/user/**")
                .ignoringRequestMatchers("/logout")
        );  // 로그인 시 invalid CSRF token found 오류 메시지 발생 안 함.

        http.authorizeHttpRequests((authorize) -> authorize
                // too many redirects 오류 제거하기 위해 필요!
                .dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll()

                // 루트 및 공개 페이지에 대한 모든 접근 허용
                .requestMatchers(new AntPathRequestMatcher("/")).permitAll()
                .requestMatchers(new AntPathRequestMatcher("/public/**")).permitAll()  // 이런 식으로 가능하다는 의미!
                // Spring Security 제공 로그인 페이지 사용 시 필요
                // .requestMatchers(new AntPathRequestMatcher("/login")).permitAll()  
                // 앱에서 제공하는 로그인 페이지 사용 시 필요 --> 로그인 페이지에 대한 허가는
                // 아래의 formLogin()에서 하므로 여기서는 제외하도록 함.
                // .requestMatchers(new AntPathRequestMatcher("/welcome")).permitAll()

                // URL과 역할       
                .requestMatchers(new AntPathRequestMatcher("/admin/**")).hasRole("ADMIN")
                .requestMatchers(new AntPathRequestMatcher("/manager/**")).hasRole("MANAGER")
                .requestMatchers(new AntPathRequestMatcher("/user/**")).hasRole("USER")
                .anyRequest().authenticated()
        );

        http.formLogin(formLogin -> formLogin
                // 디폴트 로그인 페이지 사용하는 경우 아랫 줄 주석 처리해야 함.
                .loginPage("/welcome") // 명시적으로 앱에서 제공하는 로그인 페이지 사용할 때 필요함. 
                .loginProcessingUrl("/login")  // 사용자 인증을 위해서 반드시 필요함.
                .defaultSuccessUrl("/")
                .successHandler(new MySimpleUrlAuthenticationSuccessHandler())
                .failureUrl("/login?error=true")
                .permitAll()
        );

        http.httpBasic(Customizer.withDefaults());

        // 로그아웃 설정
        http.logout(logout -> logout
                // .logoutUrl("/logout")
                .logoutSuccessUrl("/")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
                .permitAll()
        );

        return http.build();
    }

    private class MySimpleUrlAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

        @Override
        public void onAuthenticationSuccess(HttpServletRequest request,
                HttpServletResponse response,
                Authentication authentication) throws IOException, ServletException {

            log.info("\n\n\nMySimpleUrlAuthenticationSuccessHandler:onAuthenticationSuccess()...\n\n");

            final Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            log.info("authories = {}", authorities);

            log.info("context path = {}", sc.getContextPath());
            String loginUrl = sc.getContextPath() + "/user/main";
            for (final GrantedAuthority grantedAuthority : authorities) {
                String authorityName = grantedAuthority.getAuthority();
                log.debug("auth name = {}", authorityName);
                if ("ROLE_ADMIN".equals(authorityName)) {
                    loginUrl = sc.getContextPath() + "/admin/main";
                    break;
                } else if ("ROLE_MANAGER".equals(authorityName)) {
                    loginUrl = sc.getContextPath() + "/manager/main";
                    break;
                }
            }
            log.info("loginUrl = {}", loginUrl);
            response.sendRedirect(loginUrl);
        }

    }

}

@Service
class MyUserDetailsService implements UserDetailsService {

    private final InMemoryUserDetailsManager inMemoryManager;
    private final JdbcUserDetailsManager jdbcManager;

    public MyUserDetailsService(InMemoryUserDetailsManager inMemoryManager, JdbcUserDetailsManager jdbcManager) {
        this.inMemoryManager = inMemoryManager;
        this.jdbcManager = jdbcManager;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (username.startsWith("admin") || username.startsWith("manager")) {
            return inMemoryManager.loadUserByUsername(username);
        } else {
            return jdbcManager.loadUserByUsername(username);
        }
    }
}

my_login.jsp 수정 - Prof. Jong Min Lee이(가) 17일 전에 추가함

<%-- 
    Document   : my_login
    Created on : Apr 8, 2024, 12:47:55 PM
    Author     : jongmin
--%>

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>My Index Page</title>
        <script>
        <% 
        if (java.util.Collections.list(request.getParameterNames()).contains("error")) {
        %>
            alert("로그인 오류가 발생하였습니다!");
        <%
         }
        %>
        </script>
    </head>
    <body>

        <h1>Login</h1>

        <form action="login" method="post">
            Username: <input name="username" type="text"> <br/>
            Password: <input name="password" type="password"> <br/>
            <input name="로그인" type="submit">
            <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">
        </form>
    </body>
</html>

SystemController.java 수정 - Prof. Jong Min Lee이(가) 17일 전에 추가함

/*
 * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
 * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
 */
package deu.se.security_demo.control;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

/**
 *
 * @author jongmin
 */
@Controller
@Slf4j
public class SystemController {
    @GetMapping({"/welcome"})
    public String myLogin() {
        log.info("my_login");
        return "my_login";
    }

    @GetMapping("/admin/main")
    public String adminHome() {
        return "admin_home";
    }

    @GetMapping("/manager/main")
    public String managerHome() {
        return "manager_home";
    }

    @GetMapping("/user/main")
    public String userHome() {
        return "user_home";
    }

}
    (1-4/4)