Spring Security
Prof. Jong Min Lee이(가) 17일 전에 추가함
- 강의 동영상 URL: https://youtu.be/VnHMfbaCol8
Spring Security는 Spring 기반 애플리케이션에서 인증 및 권한 부여를 처리하는 강력한 보안 프레임워크입니다. 주요 기능을 간략히 정리하면 다음과 같습니다:
- 인증(Authentication): 사용자의 신원을 확인하는 과정으로, 로그인 기능을 제공하며 다양한 인증 방식(예: 폼 로그인, OAuth2, JWT, LDAP 등)을 지원합니다.
- 권한 부여 또는 인가(Authorization): 인증된 사용자가 특정 리소스에 접근할 수 있는지 결정합니다. 역할 기반 접근 제어(RBAC) 및 표현식 기반 접근 제어를 지원합니다.
- 보안 필터(Security Filters): 요청 및 응답을 가로채어 필요한 보안 검사를 수행합니다. 여러 개의 필터 체인을 통해 세분화된 보안 제어가 가능합니다.
- 보안 설정(Security Configuration): Java 및 XML 기반으로 설정할 수 있으며, 최신 버전에서는
@EnableWebSecurity
와SecurityFilterChain
을 사용한 설정 방식이 주를 이룹니다. - 비밀번호 암호화:
BCryptPasswordEncoder
등을 이용해 안전한 비밀번호 저장을 지원합니다. - 세션 관리(Session Management): 세션 고정 보호, 동시 세션 제어 등 다양한 세션 보안 기능을 제공합니다.
- OAuth2 및 JWT 지원: API 보안 및 소셜 로그인 같은 인증 방식을 구현할 때 널리 사용됩니다.
Spring Security를 활용하면 애플리케이션의 보안을 효과적으로 강화할 수 있습니다.
답글 (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
등의 어노테이션,HttpSecurity
의authorizeHttpRequests()
설정 등을 통해 인가를 처리합니다. - 실패 시: 일반적으로 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";
}
}