나는 스프링 시큐리티에 대해 아무것도 모르는 초짜이기 때문에 https://velog.io/@dh1010a/Spring-Spring-Security%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-3.X-%EB%B2%84%EC%A0%84-1
이분의 블로그를 보고 따라 치면서 모르는 부분은 찾아가며 공부를 해보려고 한다. 우선 완성된 코드를 올리고 코드마다 어떻게 작동하고 어떤 의미인지 해석을 해보도록 하자.
SecurityConfig
package com.security.start.security.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
//private final UserDetailsService userDetailsService;
private final ObjectMapper objectMapper;
// 스프링 시큐리티 기능 비활성화 (H2 DB 접근을 위해)
// @Bean
// public WebSecurityCustomizer configure() {
// return (web -> web.ignoring()
// .requestMatchers(toH2Console())
// .requestMatchers("/h2-console/**")
// );
// }
// 특정 HTTP 요청에 대한 웹 기반 보안 구성
@Bean
public SecurityFilterChain filiterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/signup", "/", "/login").permitAll()
.anyRequest().authenticated())
// 폼 로그인은 현재 사용하지 않음
// .formLogin(formLogin -> formLogin
// .loginPage("/login")
// .defaultSuccessUrl("/home"))
.logout((logout) -> logout
.logoutSuccessUrl("/login")
.invalidateHttpSession(true))
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.builder()
.username("sss")
.password(bCryptPasswordEncoder().encode("1234"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
TestController
package com.security.start.security.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@GetMapping("/")
public String home() {
return "Welcome to public page!";
}
@GetMapping("/secure")
public String secure() {
return "This is a secure page!";
}
}
User
package com.security.start.security.domain.user;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDate;
@Entity
@Getter
@Builder
@AllArgsConstructor
@Table(name = "USERS")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false, unique = true, length = 30)
private String email;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String phoneNum;
private String imgUrl;
@Enumerated(EnumType.STRING)
@JsonIgnore
private PublicStatus publicStatus;
@JsonIgnore
@Enumerated(EnumType.STRING)
private ShareStatus shareStatus;
private LocalDate createdAt;
// @JsonIgnore
// @OneToMany(mappedBy = "users")
// private List<SharedAlbum> sharedAlbums = new ArrayList<>();
}
시큐리티의 동작을 보기 위해서 간단한 엔티티와 컨트롤러를 만들었다. 해당 서버를 실행하면 나는 아래와 같은 동작들을 볼 수 있다.
요청 URL | 결과 이유 |
---|---|
/ | 정상 접근 (컨트롤러에 매핑되어 있고, 인증 없이 허용됨) |
/secure | 401 Unauthorized (인증 필요하지만 로그인 기능 없음) |
/signup | 인증 없이 허용은 되어 있지만, 실제 핸들러 없음 → 404 또는 빈 응답 |
/login | 인증 없이 허용되어 있음, 하지만 로그인 기능 없음 → 아무 동작 안 함 |
/ss, /14, 기타 주소 | 전부 인증 필요하지만 컨트롤러도 없고 로그인 기능도 없음 → 401 Unauthorized |
이제 자세한 이유들도 코드와 함께 살펴보도록 하겠다. Controller와 Entity에 대한 설명은 생략한다.
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/signup", "/", "/login").permitAll()
.anyRequest().authenticated())
이 부분에서 permitAll()로 허용한 경로 3개를 제외한 모든 경로는 인증된 사용자만 접근 가능하도록 되어 있다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 설정들...
return http.build(); // 최종 필터 체인 완성
}
다른 부분들도 내가 이해할 수 있게 GPT의 도움을 받아 하나씩 풀어서 보도록 하겠다.
1. .csrf(AbstractHttpConfigurer::disable)
💣 CSRF(Cross Site Request Forgery) 공격 방지 비활성화
.authorizeHttpRequests((authorize) -> ...)
🔐 요청 경로별 인증/인가 규칙 설정
4-1. .requestMatchers("/signup", "/", "/login").permitAll()
/signup, /, /login → 이 3개 경로는 인증 없이 누구나 접근 가능하게 허용
즉, 로그인 안 한 사람도 이 페이지들은 볼 수 있어
4-2. .anyRequest().authenticated()
위에서 허용한 경로를 제외한 모든 요청은 로그인(인증)이 되어 있어야 접근 가능 인증 안 된 사용자가 접근하면 401 Unauthorized 에러 발생
.logout((logout) -> ...)
🚪 로그아웃 기능 설정
5-1. .logoutSuccessUrl(\"/login\")
로그아웃에 성공하면 /login 페이지로 리다이렉트됨
5-2. .invalidateHttpSession(true)
로그아웃 시 세션 완전 제거 (로그인 상태 유지 X)
.sessionManagement(session -> ...)
🧠 세션 관리 정책 설정
6-1. .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
세션을 아예 생성하지 않음 즉, 로그인 정보를 세션에 저장하지 않고, 요청마다 인증이 필요하다는 뜻 이건 JWT 토큰 인증 방식을 쓸 때 꼭 필요해!
return http.build();
🚧 지금까지 설정한 내용을 기반으로 시큐리티 필터 체인 완성해서 스프링이 사용하게 함
🧠 요약 그림 느낌으로 설명하면:
css
Copy code
[CSRF] - OFF
[기본인증창] - OFF
[로그인폼] - OFF
[/, /login, /signup] → 누구나 접근 OK
[그 외 URL] → 로그인 필수
[세션] → 만들지 마 (JWT 전용 느낌)
[로그아웃] → 성공하면 /login으로 이동 + 세션 비우기
설명을 들어도 모르겠는게 많다. 우선 JWT랑 csrf는 잘 이해가 안되고 내가 지금 따라 친 코드가 특정 URL에 대해서만 모든 사용자에게 개방을 해준다는 것은 이해 했다.
CSRF = Cross Site Request Forgery
(크로스 사이트 요청 위조)
로그인된 사용자의 세션을 몰래 이용해서, 공격자가 의도하지 않은 요청을 하도록 유도하는 공격이다.
EX) 사용자가 로그인 상태로 https://bank.com에 접속해 있다. (세션이 유지되고 있음)
이때 사용자가 악성 사이트 evil.com에 접속했는데,
그 사이트에 숨겨진 form이 자동으로 https://bank.com/transfer?to=hacker&amount=10000 요청을 보내버림!
사용자는 아무것도 하지 않았지만, 로그인 세션이 있으므로 요청이 실행됨.
💸 해커에게 돈이 이체됨!
위 같은 상황을 방지하기 위해서 기본적으로 스프링 시큐리티는 CSRF 보호 기능을 켜놓는다.
JWT는 세 가지 파트로 구정되어 있다.
헤더.페이로드.서명
ex) eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJreW... .sdT9ksdaflj0923...
흐름 요약:
1. 사용자가 아이디/비번으로 로그인
2. 서버가 인증 성공 → JWT 발급
3. 사용자가 이후 요청마다 JWT를 Authorization 헤더에 담아서 보냄
4.서버는 매 요청마다 토큰을 검증해서 권한 확인
장점
단점
위 설명들에서 계속해서 세션이라는 단어가 쓰이는거 같아서 다시 세션에 대해서도 자세히 정리를 하겠다.
서버가 "이 사용자 = 이 사람" 이라는 것을 기억하기 위해 유지하는 저장 공간
HTTP 프로토콜은 기억을 못하는(stateless) 방식이다. 즉, 사용자가 로그인해서 요청을 하나 보내고 나면, 다음 요청에서는 누군지 전혀 모르게 됨
그래서 로그인 상태 유지를 하려면, 서버가 사용자를 기억해야 한다. 이걸 해결한게 바로 세션 기반 인증 방식이다.
전체 과정:
1. 사용자가 로그인 폼에서 아이디/비밀번호 입력
2. 서버가 로그인 성공을 확인하고,
3. 서버 메모리(DB 또는 세션 저장소) 에 사용자 정보를 저장함
4. 그리고 클라이언트에게 세션 ID 쿠키를 하나 줌
Set-Cookie: JSESSIONID=abc123
Cookie: JSESSIONID=abc123
항목 | 내용 |
---|---|
저장 위치 | 서버 (메모리 or DB) |
클라이언트 저장 정보 | 세션 ID만 저장 (쿠키에) |
장점 | 사용자는 아무 것도 몰라도 로그인 유지 가능 |
단점 | 서버에 사용자 수만큼 정보 저장해야 함 (=메모리 부담) |
단점 떄문에 등장한 JWT
세션 기반 인증은:
사용자가 많아지면 서버 메모리에 세션이 너무 많아짐 → 확장성 떨어짐
서버가 재시작되거나, 서버가 여러 대면 세션 공유가 어려움 → 그래서 서버가 기억하지 않아도 되는 JWT 토큰 기반 인증이 등장하게 됨
비교 항목 | 세션 기반 | JWT 기반 |
---|---|---|
인증 상태 저장 위치 | 서버 메모리 (세션) | 클라이언트(토큰 안에 정보 포함) |
서버 확장 | 어려움 (세션 공유 필요) | 쉬움 (토큰만 있으면 됨) |
요청 무게 | 가벼움 (쿠키만 보냄) | 무거움 (JWT는 큰 문자열) |
취소/만료 | 서버에서 언제든 삭제 가능 | 토큰 만료 전까지 무효화 어려움 |
보안 위험 | 세션 탈취 | 토큰 탈취 |
즉, 세션은 서버가 로그인 상태를 기억하는 구조이다 HTTP는 원래 stateless(기억X)이기 때문에 세션이 등장 했다. 서버 부하 문제 때문에 나중엔 JWT 기반 인증이 널리 쓰이게 됐다.
언급이 되었기 때문에 쿠키에 대해서도 알아보고 가도록 하겠다.
웹 브라우저가 로컬에 저장하는 작은 데이터 조작
서버가 응답할 때: Set-Cookie: JSESSIONID=abc123; Path=/; HttpOnly
이걸 받은 브라우저는 JSESSIONID=abc123라는 쿠키를 저장하고, 이후 요청마다 자동으로 서버에 보내준다(Cookie: JSESSIONID=abc123)
속성 | 설명 |
---|---|
Name=Value | 쿠키 이름과 값 |
Path=/login | 쿠키가 적용되는 경로 |
Domain=example.com | 어느 도메인에서 쿠키를 보낼지 |
Expires / Max-Age | 쿠키의 만료 시간 (세션 쿠키는 브라우저 닫으면 삭제) |
Secure | HTTPS에서만 전송 |
HttpOnly | JavaScript로 접근 불가 → XSS 공격 방지용 |
SameSite | 다른 사이트로부터 오는 요청에 대해 쿠키를 보낼지 제한 (CSRF 방지) |
비유하자면
쿠키 = 클라이언트에 저장하는 작은 열쇠
세션 = 서버에 있는 진짜 사용자 정보
쿠키 보안 이슈에는 XSS 공격, CSRF 공격, 쿠키 탈취 등이 있다.
쿠키의 종류
항목 | 쿠키 | 세션 |
---|---|---|
저장 위치 | 클라이언트(브라우저) | 서버 |
용도 | 상태 저장, 인증 유지 | 사용자 정보 저장, 인증 |
사이즈 제한 | 약 4KB | 서버 메모리 한계 |
보안 | 탈취 위험 있음 (보안 설정 필수) | 서버 쪽 설정으로 제어 가능 |
가능은 하지만 조심해야 한다.