17์ฃผ์ฐจ(๋ถ์ฐ ์์คํ ) ์ดํ Claude๊ฐ ์์๋ก ๊ตฌ์ฑํ ํ์ต ๊ฒฝ๋ก.
๋ฉด์ ๊ฑฐ์ 100% ์ถ์ ์์ญ์ธ Spring Security์ ์ธ์ฆ/์ธ๊ฐ๋ฅผ ์ ๋ณตํ๋ค.
- Filter Chain ๋ฉ์ปค๋์ฆ (15์ฃผ์ฐจ Filter์ ์ง์ง ํ์ฉ)
- ์ธ์ฆ(Authentication) vs ์ธ๊ฐ(Authorization)
- Session ๊ธฐ๋ฐ vs Token ๊ธฐ๋ฐ (๋๊ท๋ชจ ์์คํ ์ ๊ฒฐ์ )
- OAuth2์ JWT (ํ๋ ํ์ค)
4๋ ์ฐจ ํ์คํ ๊ฐ๋ฐ์๊ฐ ๊ฐ์ฅ ์์ฃผ ๋ง์ฃผํ์ง๋ง "์ ๋๋ก๋ ๋ชจ๋ฅด๋" ์์ญ.
1~17์ฃผ์ฐจ์ ๋น ๊ณต๊ฐ:
| ์์ญ | ์ฃผ์ฐจ | ์ํ |
|---|---|---|
| Java/Spring/JPA | 1-12์ฃผ์ฐจ | โ |
| DB | 13-14์ฃผ์ฐจ | โ |
| Spring MVC | 15์ฃผ์ฐจ | โ |
| ๋ถ์ฐ ์์คํ | 16-17์ฃผ์ฐจ | โ |
| Spring Security + ์ธ์ฆ/์ธ๊ฐ | ๋ฏธ๋ฑ์ฅ | โ |
์ ๊ฒฐ์ ์ ์ธ๊ฐ:
ILIC ๊ด์ :
[Phase 1] ์ธ์ฆ/์ธ๊ฐ์ ๋ณธ์ง (Authentication vs Authorization)
โ
[Phase 2] Spring Security ์ํคํ
์ฒ์ Filter Chain โ ์ ์ 1
โ
[Phase 3] ์ธ์ฆ ์ฒ๋ฆฌ ํ๋ฆ (UserDetails, AuthenticationProvider)
โ
[Phase 4] ์ธ๊ฐ ์ฒ๋ฆฌ (URL ํจํด, ๋ฉ์๋ ๋ณด์, AOP)
โ
[Phase 5] Session ๊ธฐ๋ฐ vs Token ๊ธฐ๋ฐ โ ์ ์ 2 (โ
๋ฉด์ ๋จ๊ณจ)
โ
[Phase 6] JWT ์์ ์ ๋ณต (๊ตฌ์กฐ, ๊ฒ์ฆ, ๋ณด์ ์ทจ์ฝ์ )
โ
[Phase 7] OAuth2์ OpenID Connect
โ
[Phase 8] ๋ณด์ ์ทจ์ฝ์ ๊ณผ ๋ฐฉ์ด (CSRF, XSS, CORS)
์ด 8 Phase ร 27 Unit โ ๋ฉด์ ๋จ๊ณจ ์ ์ 2๊ฐ๋ฅผ ๊ฐ์ง ๋จ์ผ ์ฃผ์ฐจ.
| ์ฃผ์ฐจ | ์ฃผ์ | ์๋ฏธ |
|---|---|---|
| 1~17์ฃผ์ฐจ | Java + Spring + JPA + DB + MVC + ๋ถ์ฐ | ๊ธฐ๋ฅ ๊ตฌํ |
| 18์ฃผ์ฐจ (์ง๊ธ) | Spring Security + ์ธ์ฆ/์ธ๊ฐ | ๋ณด์ ์์ญ |
ํต์ฌ ์ฐ๊ฒฐ:
@PreAuthorize)| Day | Phase | ํ์ต ๋ชฉํ |
|---|---|---|
| 1์ผ์ฐจ | Phase 1 + 2 | ์ธ์ฆ/์ธ๊ฐ ๋ณธ์ง + Filter Chain (โ ) |
| 2์ผ์ฐจ | Phase 3 | ์ธ์ฆ ํ๋ฆ (UserDetails ๋ฑ) |
| 3์ผ์ฐจ | Phase 4 | ์ธ๊ฐ (URL + ๋ฉ์๋) |
| 4์ผ์ฐจ | Phase 5 | Session vs Token (โ ๋ฉด์ ๋จ๊ณจ) |
| 5์ผ์ฐจ | Phase 6 | JWT ๊น์ด |
| 6์ผ์ฐจ | Phase 7 | OAuth2/OIDC |
| 7์ผ์ฐจ | Phase 8 + ์ข ํฉ | CSRF/XSS/CORS + ์๊ธฐ ์ ๊ฒ |
์ฌ์ ์ผ์ (10์ผ): Phase 2, 5์ +1์ผ์ฉ. ์ง์ ๋๋ฒ๊ฑฐ๋ก Filter Chain step-through ๊ถ์ฅ.
๋ชฉํ: ๊ฐ์ฅ ์์ฃผ ํท๊ฐ๋ฆฌ๋ ๋ ๊ฐ๋ ์ ์ ํํ ๋ถ๋ฆฌํ๋ค.
์ ์ ์ง์: ์์ (๊ฐ์ฅ ๊ธฐ์ด)
ํต์ฌ ์ ์
Authentication (์ธ์ฆ) โ "๋๊ตฌ์ธ๊ฐ?":
"์ด ์์ฒญ์ ๋ณด๋ธ ์ฌ๋์ด ๋ณธ์ธ์ด ๋ง๋๊ฐ" ๊ฒ์ฆ
Authorization (์ธ๊ฐ) โ "๋ฌด์์ ํ ์ ์๋๊ฐ?":
"์ธ์ฆ๋ ์ฌ์ฉ์๊ฐ ์ด ์์์ ์ ๊ทผ ๊ฐ๋ฅํ๊ฐ" ๊ฒ์ฆ
๋น์ โ ํ์ฌ ์ถ์ :
โ ์ธ์ฆ OK์ฌ๋ ์ธ๊ฐ๋ NO์ผ ์ ์์
ํ๋ฆ:
[Request]
โ
[Authentication]
- ๋๊ตฌ์ธ๊ฐ? (ID/PW, JWT, ์ธ์ฆ์ ๋ฑ)
- ์คํจ โ 401 Unauthorized
โ (ํต๊ณผ)
[Authorization]
- ๊ถํ์ด ์๋๊ฐ?
- ์คํจ โ 403 Forbidden
โ (ํต๊ณผ)
[Business Logic]
HTTP ์ํ ์ฝ๋ (15์ฃผ์ฐจ ๋ณต์ต) โญ :
401๊ณผ 403์ ์ฐจ์ด๋ ๋ฉด์ ๋จ๊ณจ์ ๋๋ค.
ILIC ์๋๋ฆฌ์ค:
Authentication:
Authorization:
์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: Unit 1.1
์ญ์ฌ์ ์งํ โญ :
Authorization: Basic dXNlcjpwYXNzd29yZA== (Base64)
๋ฌธ์ :
ํ์ฌ:
[Login]
โ ID/PW ๊ฒ์ฆ
[Server]
โ Session ์์ฑ + Session ID
[Browser] โ Cookie: SESSIONID=abc123
โ ์ดํ ์์ฒญ๋ง๋ค Cookie ์๋ ์ ์ก
[Server] โ Session ID๋ก ์ฌ์ฉ์ ์๋ณ
์ฅ์ : ๋จ์, Spring ๊ธฐ๋ณธ
๋จ์ : ์๋ฒ ์ํ ๋ณด์ โ ๋ถ์ฐ ํ๊ฒฝ์์ ์ด๋ ค์ (Phase 5์์)
[Login]
โ ID/PW ๊ฒ์ฆ
[Server]
โ JWT ์์ฑ (์๋ช
)
[Browser] โ JWT ์ ์ฅ
โ Authorization: Bearer eyJ...
[Server] โ JWT ๊ฒ์ฆ (์๋ช
๋ง ํ์ธ, ์ํ ์์)
์ฅ์ : Stateless, ๋ถ์ฐ ํ๊ฒฝ ์ ํฉ
๋จ์ : ํ ํฐ ํ๊ธฐ ์ด๋ ค์, ํฌ๊ธฐ โ
[Client] โ "Google๋ก ๋ก๊ทธ์ธ"
โ
[Google] (Authorization Server) โ ์ฌ์ฉ์ ๋์
โ
[Client] โ Access Token + Refresh Token
โ
[Resource Server (API)] โ Access Token์ผ๋ก ์ธ์ฆ
์ฅ์ :
ํ์ฌ ํ์ค:
ILIC ์ถ์ :
์๊ธฐ ์ ๊ฒ
๋ชฉํ: ๋ฉด์ ๋จ๊ณจ โ Spring Security๊ฐ ์ด๋ป๊ฒ ๋์ํ๋์ง Filter Chain์ ํตํด ์ดํดํ๋ค.
์ ์ ์ง์: 15์ฃผ์ฐจ Phase 5 (Filter)
ํต์ฌ ๊ทธ๋ฆผ โญ :
[Client]
โ HTTP Request
[Servlet Container (Tomcat)]
โ
[Filter Chain] โโโโโโโ โ
Spring Security๊ฐ ์ฌ๊ธฐ โ
โโโ DelegatingFilterProxy
โ โโโ FilterChainProxy (Spring Security)
โ โโโ SecurityContextPersistenceFilter
โ โโโ UsernamePasswordAuthenticationFilter
โ โโโ BasicAuthenticationFilter
โ โโโ ExceptionTranslationFilter
โ โโโ FilterSecurityInterceptor (์ธ๊ฐ)
โ
[DispatcherServlet] (15์ฃผ์ฐจ)
โ
[Controller]
ํต์ฌ ํต์ฐฐ:
"Spring Security๋ Servlet Filter ๋ก ๋์ โ DispatcherServlet ๋๋ฌ ์ ์ ์ฐจ๋จ/ํต๊ณผ ๊ฒฐ์ "
์ Filter์ธ๊ฐ (15์ฃผ์ฐจ ๋ณต์ต):
DelegatingFilterProxy:
FilterChainProxy:
์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: Unit 2.1
์ฃผ์ Filter ์์ (์ค์ ๋ก๋ ๋ ๋ง์ง๋ง ํต์ฌ๋ง):
1. SecurityContextPersistenceFilter
โ SecurityContext๋ฅผ Session์์ ๋ณต์
2. UsernamePasswordAuthenticationFilter
โ /login POST ์์ฒญ ์ฒ๋ฆฌ
3. BasicAuthenticationFilter
โ Basic Auth ํค๋ ์ฒ๋ฆฌ
4. RememberMeAuthenticationFilter
โ Remember-me ์ฟ ํค ์ฒ๋ฆฌ
5. AnonymousAuthenticationFilter
โ ์ต๋ช
์ฌ์ฉ์ ์ฒ๋ฆฌ
6. ExceptionTranslationFilter
โ ์ธ์ฆ/์ธ๊ฐ ์์ธ๋ฅผ HTTP ์๋ต์ผ๋ก
7. FilterSecurityInterceptor
โ ์ต์ข
์ธ๊ฐ ๊ฒฐ์
๊ฐ Filter์ ์ญํ :
/login ์์ฒญ ๊ฐ๋ก์ฑAuthenticationException โ ๋ก๊ทธ์ธ ํ์ด์ง (๋๋ 401)AccessDeniedException โ 403 ํ์ด์งhasRole, hasAuthority ๋ฑ)JWT ์ฌ์ฉ ์:
UsernamePasswordAuthenticationFilter ๋์ JwtAuthenticationFilter ์ถ๊ฐSessionCreationPolicy.STATELESS)@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
// ...
return http.build();
}
}
์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: Unit 2.2, 4์ฃผ์ฐจ ThreadLocal
ํต์ฌ ๊ฐ๋
SecurityContext:
Authentication ๊ฐ์ฒด ๋ณด์ SecurityContextHolder:
// ์ด๋์๋ ํ์ฌ ์ฌ์ฉ์ ์ ๊ทผ ๊ฐ๋ฅ
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
์ ThreadLocal?:
4์ฃผ์ฐจ ThreadLocal ํจ์ ์ฌ๋ฑ์ฅ โ ๏ธ :
@Async, CompletableFuture)์์ SecurityContext ์์คDelegatingSecurityContextRunnable ๋ฑ์ผ๋ก ์ ํ ํ์Authentication ์ธํฐํ์ด์ค:
public interface Authentication extends Principal {
Collection<? extends GrantedAuthority> getAuthorities(); // ๊ถํ ๋ชฉ๋ก
Object getCredentials(); // ๋น๋ฐ๋ฒํธ (๊ฒ์ฆ ํ ๋ณดํต null)
Object getDetails(); // ์ถ๊ฐ ์ ๋ณด (IP ๋ฑ)
Object getPrincipal(); // ์ฌ์ฉ์ ๋ณธ์ฒด (UserDetails)
boolean isAuthenticated();
}
๊ตฌํ์ฒด ์:
UsernamePasswordAuthenticationToken (form ๋ก๊ทธ์ธ)JwtAuthenticationToken (JWT)OAuth2AuthenticationToken (OAuth2)Spring Boot์์ ํ์ฌ ์ฌ์ฉ์ ๊ฐ์ ธ์ค๊ธฐ (์ค์ฉ):
๋ฐฉ๋ฒ 1 โ SecurityContextHolder ์ง์ :
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
๋ฐฉ๋ฒ 2 โ @AuthenticationPrincipal (๊ถ์ฅ) โญ :
@GetMapping("/me")
public User getCurrentUser(@AuthenticationPrincipal UserDetails userDetails) {
return userService.findByUsername(userDetails.getUsername());
}
๋ฐฉ๋ฒ 3 โ Authentication ์ง์ ์ฃผ์ :
@GetMapping("/me")
public User getCurrentUser(Authentication authentication) {
return userService.findByUsername(authentication.getName());
}
์๊ธฐ ์ ๊ฒ
๋ชฉํ: Spring Security๊ฐ ID/PW๋ฅผ ์ด๋ป๊ฒ ๊ฒ์ฆํ๋์ง, ํ๋ฆ์ ๋ฐ๋ผ๊ฐ๋ค.
์ ์ ์ง์: Phase 2
ํต์ฌ ์ธํฐํ์ด์ค
UserDetails โ ์ฌ์ฉ์ ์ ๋ณด:
public interface UserDetails {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
๊ตฌํ ์์:
@Getter
public class CustomUserDetails implements UserDetails {
private final User user; // JPA ์ํฐํฐ
@Override
public String getUsername() { return user.getEmail(); }
@Override
public String getPassword() { return user.getPasswordHash(); }
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
.toList();
}
@Override
public boolean isAccountNonLocked() { return !user.isLocked(); }
// ... ๋๋จธ์ง
}
UserDetailsService โ ์ฌ์ฉ์ ์กฐํ:
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
๊ตฌํ ์์:
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String email) {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + email));
return new CustomUserDetails(user);
}
}
โ Spring Security๊ฐ ์๋์ผ๋ก ์ด ๊ตฌํ์ฒด๋ฅผ ์ฌ์ฉ
์ ์ธํฐํ์ด์ค๋ฅผ ๋ถ๋ฆฌํ๋ (5์ฃผ์ฐจ OCP, DI):
์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: Unit 3.1
ํต์ฌ ํ๋ฆ
AuthenticationManager โ ์ธ์ฆ ์ฑ ์์:
ProviderManager ๊ตฌํ์ฒด ์ฌ์ฉAuthenticationProvider โ ์ค์ ๊ฒ์ฆ:
์ ์ฒด ์ธ์ฆ ํ๋ฆ โญ :
1. UsernamePasswordAuthenticationFilter
โ ์์ฒญ์์ ID/PW ์ถ์ถ
โ UsernamePasswordAuthenticationToken ์์ฑ (๋ฏธ์ธ์ฆ)
2. AuthenticationManager.authenticate(token)
โ ProviderManager๊ฐ Provider ๋ชฉ๋ก์์ ์ ํ
3. DaoAuthenticationProvider (๋ํ Provider)
โ UserDetailsService.loadUserByUsername() ํธ์ถ
โ DB์์ UserDetails ๊ฐ์ ธ์ด
โ PasswordEncoder.matches(rawPassword, hashedPassword) ๊ฒ์ฆ
โ ์ฑ๊ณต โ UsernamePasswordAuthenticationToken (์ธ์ฆ๋จ) ๋ฐํ
4. SecurityContextHolder์ ์ ์ฅ
5. ํ์ ์์ฒญ์์ ์๋ ์ธ์ฆ
PasswordEncoder โญ :
๋ํ ๊ตฌํ์ฒด:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // strength
}
// ์ฌ์ฉ
String hash = passwordEncoder.encode("rawPassword");
// โ "$2a$12$abc..." ๊ฐ์ ํํ
boolean match = passwordEncoder.matches("rawPassword", hash);
BCrypt์ ํน์ง:
์ปค์คํ AuthenticationProvider:
@Component
public class CustomAuthProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication auth) {
// ์ปค์คํ
๊ฒ์ฆ ๋ก์ง (์: ์ธ๋ถ API ํธ์ถ)
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
ILIC ์ ์ฉ:
์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: Unit 3.2
Form ๋ก๊ทธ์ธ (์ ํต ์น):
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/login") // POST ์ฒ๋ฆฌ URL
.defaultSuccessUrl("/home")
.failureUrl("/login?error")
);
return http.build();
}
ํ๋ฆ:
1. GET /login โ ๋ก๊ทธ์ธ ํ์ด์ง ํ์
2. POST /login (ID/PW) โ UsernamePasswordAuthenticationFilter ๊ฐ๋ก์ฑ
3. ์ฑ๊ณต โ /home ๋ฆฌ๋ค์ด๋ ํธ + Session ์์ฑ
4. ์คํจ โ /login?error ๋ฆฌ๋ค์ด๋ ํธ
API ๋ก๊ทธ์ธ (REST + JWT):
@RestController
@RequiredArgsConstructor
public class AuthController {
private final AuthenticationManager authManager;
private final JwtTokenProvider jwtProvider;
@PostMapping("/api/login")
public LoginResponse login(@RequestBody LoginRequest request) {
// 1. ์ธ์ฆ ์๋
Authentication auth = authManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getEmail(), request.getPassword()
)
);
// 2. JWT ์์ฑ
String accessToken = jwtProvider.createAccessToken(auth);
String refreshToken = jwtProvider.createRefreshToken(auth);
return new LoginResponse(accessToken, refreshToken);
}
}
ํ๋ฆ:
1. POST /api/login (JSON ID/PW)
2. AuthenticationManager๋ก ์ง์ ์ธ์ฆ
3. JWT ์์ฑ ํ ์๋ต
4. ํด๋ผ์ด์ธํธ๊ฐ JWT ์ ์ฅ (LocalStorage / HttpOnly Cookie)
5. ์ดํ ์์ฒญ์ Authorization: Bearer ...
ILIC ์๋๋ฆฌ์ค:
์๊ธฐ ์ ๊ฒ
๋ชฉํ: URL ๊ธฐ๋ฐ๊ณผ ๋ฉ์๋ ๊ธฐ๋ฐ ์ธ๊ฐ์ ์ฐจ์ด์ ํ์ฉ์ ๋ง์คํฐํ๋ค.
์ ์ ์ง์: Phase 3
ํต์ฌ ํจํด
Spring Security 6 (ํ๋):
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/products/**").hasRole("ADMIN")
.anyRequest().authenticated()
);
return http.build();
}
์ฃผ์ ๋ฉ์๋:
| ๋ฉ์๋ | ์๋ฏธ |
|---|---|
permitAll() | ๋๊ตฌ๋ ์ ๊ทผ |
denyAll() | ๋ชจ๋ ๊ฑฐ๋ถ |
authenticated() | ์ธ์ฆ๋ ์ฌ์ฉ์ |
hasRole("ADMIN") | ํน์ ์ญํ |
hasAuthority("READ_PRODUCT") | ํน์ ๊ถํ |
hasAnyRole("ADMIN", "MANAGER") | ์ฌ๋ฌ ์ญํ ์ค ํ๋ |
access("hasRole('ADMIN') and hasIpAddress('192.168.1.0/24')") | SpEL ๋ณต์ก ํํ |
Role vs Authority โญ :
| Role | Authority | |
|---|---|---|
| ์๋ฏธ | ์ญํ ๊ทธ๋ฃน | ์ธ๋ถ ๊ถํ |
| ์ | ROLE_ADMIN, ROLE_USER | READ_PRODUCT, DELETE_USER |
| ๋ฉ์๋ | hasRole("ADMIN") | hasAuthority("READ_PRODUCT") |
| Prefix | ์๋ ROLE_ ์ถ๊ฐ | ๊ทธ๋๋ก |
ํต์ฌ:
hasRole("ADMIN") โ DB์ ROLE_ADMIN ์ ์ฅhasAuthority("ROLE_ADMIN") โ DB์ ROLE_ADMIN ์ ์ฅ (๊ฐ์ ๊ฒฐ๊ณผ)ILIC ์๋๋ฆฌ์ค:
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/fares/**").hasAnyRole("USER", "PARTNER", "ADMIN")
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
์์๊ฐ ์ค์ โ ๏ธ :
์๊ธฐ ์ ๊ฒ
/api/admin/users ์์ฒญ์ ์ผ๋ฐ ์ฌ์ฉ์๊ฐ ํ๋ค๋ฉด ์ด๋ค ์๋ต? (ํํธ: 403)/api/admin/users ์์ฒญ์ ์ธ์ฆ ์์ด ํ๋ค๋ฉด? (ํํธ: 401)์ ์ ์ง์: Unit 4.1, 8-9์ฃผ์ฐจ AOP
ํต์ฌ ๊ฐ๋
๋ฉ์๋ ๋ณด์:
ํ์ฑํ:
@Configuration
@EnableMethodSecurity
public class SecurityConfig { ... }
@PreAuthorize โ ๋ฉ์๋ ํธ์ถ ์ ๊ฒ์ฆ:
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long userId) { ... }
@PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
public User getUser(Long userId) { ... }
ํต์ฌ:
#userId)authentication ๋ณ์๋ก ํ์ฌ ์ฌ์ฉ์@PostAuthorize โ ๋ฉ์๋ ํธ์ถ ํ ๊ฒ์ฆ:
@PostAuthorize("returnObject.owner == authentication.principal.username")
public Document getDocument(Long id) { ... }
โ ๋ฐํ๋ ๊ฐ์ฒด ๊ฒ์ฌ ํ ๊ถํ ๊ฒฐ์
@PreFilter / @PostFilter โ ์ปฌ๋ ์
ํํฐ๋ง:
@PostFilter("filterObject.owner == authentication.principal.username")
public List<Document> getMyDocuments() {
return documentRepository.findAll(); // ๋ชจ๋ ๊ฐ์ ธ์จ ํ ํํฐ
}
โ ๏ธ ์ฃผ์: ์ฑ๋ฅ โ DB์์ ๋ชจ๋ ๊ฐ์ ธ์์ ํํฐ๋ง
โ ๊ฐ๋ฅํ๋ฉด ์ฟผ๋ฆฌ ๋จ๊ณ์์ ํํฐ๋ง
๋ฉ์๋ ๋ณด์์ ๋ณธ์ง โญ :
@Around advice๋ก ๋ฉ์๋ ๊ฐ๋ก์ฑILIC ํ์ฉ:
@Service
public class FareService {
@PreAuthorize("hasRole('USER')")
public List<Fare> findAllByCurrentUser() { ... }
@PreAuthorize("hasRole('ADMIN')")
public void delete(Long id) { ... }
@PreAuthorize("@fareSecurity.canEdit(#fareId, authentication)")
public Fare update(Long fareId, FareDto dto) { ... }
}
@Component("fareSecurity")
public class FareSecurity {
public boolean canEdit(Long fareId, Authentication auth) {
// ๋ณต์กํ ๊ถํ ๋ก์ง (์์ ์ ๋๋ ๊ด๋ฆฌ์)
}
}
์๊ธฐ ์ ๊ฒ
๋ชฉํ: ๋ฉด์ ์์ ๊ฑฐ์ 100% ์ถ์ ๋๋ ๋น๊ต๋ฅผ ๋ช ํํ ์ก๋๋ค.
์ ์ ์ง์: Phase 2
ํต์ฌ ํ๋ฆ
1. [Login Request] POST /login (id, pw)
โ
2. [Server] ๊ฒ์ฆ โ Session ์์ฑ
- Session ID (๋๋ค ๋ฌธ์์ด) ์์ฑ
- Server ๋ฉ๋ชจ๋ฆฌ ๋๋ Redis์ Session ์ ์ฅ
- Session ๋ฐ์ดํฐ: { user_id: 42, role: "USER" }
โ
3. [Response] Set-Cookie: SESSIONID=abc123; HttpOnly; Secure
โ
4. [Browser] Cookie ์๋ ์ ์ฅ
โ
5. [Subsequent Request] Cookie: SESSIONID=abc123
โ
6. [Server] Session ID๋ก Session ์กฐํ โ ์ฌ์ฉ์ ์๋ณ
Session ์ ์ฅ์:
1. ์๋ฒ ๋ฉ๋ชจ๋ฆฌ:
2. Redis (๋ถ์ฐ) โญ :
spring:
session:
store-type: redis
redis:
host: localhost
port: 6379
@EnableRedisHttpSession
@Configuration
public class SessionConfig { }
3. JDBC:
Cookie ๋ณด์ ์์ฑ โญ :
| ์์ฑ | ์๋ฏธ |
|---|---|
| HttpOnly | JavaScript์์ ์ ๊ทผ X (XSS ๋ฐฉ์ด) |
| Secure | HTTPS์์๋ง ์ ์ก |
| SameSite | ๋ค๋ฅธ ๋๋ฉ์ธ ์์ฒญ ์ ์ฐจ๋จ (CSRF ๋ฐฉ์ด) |
| Path | ์ ์ฉ ๊ฒฝ๋ก |
| Max-Age | ๋ง๋ฃ ์๊ฐ |
๋ชจ๋ฒ ์ฌ๋ก:
Set-Cookie: SESSIONID=abc; HttpOnly; Secure; SameSite=Lax; Max-Age=3600
Session์ ์ฅ์ โญ :
1. ์ฆ์ ๋ฌดํจํ ๊ฐ๋ฅ โ ์๋ฒ์์ ์ญ์ ํ๋ฉด ๋
2. ๋ฏผ๊ฐ ์ ๋ณด ์๋ฒ ๋ณด๊ด โ Cookie์๋ ID๋ง
3. ๋จ์ โ Spring ๊ธฐ๋ณธ
Session์ ๋จ์ โ ๏ธ :
1. ์๋ฒ ์ํ ๋ณด์ (Stateful) โ ๋ถ์ฐ ํ๊ฒฝ ๋ถ๋ด
2. ์ํ ํ์ฅ ์ด๋ ค์ โ Sticky Session ๋๋ ๊ณต์ ์ ์ฅ์ ํ์
3. CSRF ๊ณต๊ฒฉ ์ํ (์๋ Cookie ์ ์ก)
4. ๋ชจ๋ฐ์ผ ์ฑ๊ณผ ๋ถ์์ฐ์ค๋ฌ์
์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: Unit 5.1
ํต์ฌ ํ๋ฆ
1. [Login Request] POST /api/login (email, pw)
โ
2. [Server] ๊ฒ์ฆ โ JWT ์์ฑ
- Header + Payload + Signature
- Payload: { sub: 42, role: "USER", exp: 1700000000 }
- Signature: ์๋ฒ ๋น๋ฐํค๋ก ์๋ช
โ
3. [Response] { "accessToken": "eyJhbGc..." }
โ
4. [Browser/App] ํ ํฐ ์ ์ฅ
- LocalStorage / SessionStorage / HttpOnly Cookie
โ
5. [Subsequent Request] Authorization: Bearer eyJhbGc...
โ
6. [Server] JWT ๊ฒ์ฆ (์๋ช
๋ง ํ์ธ)
- ์๋ฒ ๋ฉ๋ชจ๋ฆฌ ์กฐํ X (Stateless!)
- Payload์์ ์ฌ์ฉ์ ID ์ถ์ถ
ํต์ฌ ์ฐจ์ด โ ์๋ฒ ์ํ:
Session:
{ sessionId123: userInfo } ๋ณด๊ดJWT:
JWT์ ์ฅ์ โญ :
1. Stateless โ ์๋ฒ ํ์ฅ ์์
2. MSA ์นํ โ ์๋น์ค ๊ฐ ํ ํฐ ์ ๋ฌ ์ฌ์
3. ๋ค์ํ ํด๋ผ์ด์ธํธ โ ๋ชจ๋ฐ์ผ, IoT ๋ฑ
4. SSO ๊ฐ๋ฅ โ ํ ํฐ์ ์ฌ๋ฌ ์๋น์ค์์ ์ธ์
JWT์ ๋จ์ โ ๏ธ :
1. ์ฆ์ ํ๊ธฐ ์ด๋ ค์ โ ๋ง๋ฃ ์ ๊น์ง ์ ํจ
2. ํ ํฐ ํฌ๊ธฐ โ (Header์ ๋งค๋ฒ ์ ์ก)
3. ๋ณด์ ์ค์ ์ด๋ ค์ โ ์ ์ฅ ์์น, ๋ง๋ฃ ๋ฑ
4. ๋ฏผ๊ฐ ์ ๋ณด ๋
ธ์ถ โ Payload๋ Base64 (์ํธํ X)
์ฆ์ ํ๊ธฐ ๋ฌธ์ ํด๊ฒฐ:
1. Short-lived Access Token + Refresh Token:
2. Token Blacklist:
3. ์งง์ ๋ง๋ฃ ์๊ฐ (15๋ถ ์ ๋) + ๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ ์ ๋ชจ๋ ํ ํฐ ๋ฌดํจ:
์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: Unit 5.1, 5.2
์์ ๋น๊ต โญ :
| ์ธก๋ฉด | Session | JWT |
|---|---|---|
| ์ํ | Stateful (์๋ฒ ๋ณด๊ด) | Stateless |
| ์ ์ฅ ์์น | ์๋ฒ ๋ฉ๋ชจ๋ฆฌ/Redis | ํด๋ผ์ด์ธํธ |
| ํ์ฅ์ฑ | Sticky ๋๋ ๊ณต์ ์ ์ฅ์ | ์์ |
| ์ฆ์ ํ๊ธฐ | โ ๊ฐ๋ฅ | โ ์ด๋ ค์ |
| ๊ณต๊ฒฉ ๋ฐฉ์ด | CSRF ์ํ | XSS ์ํ (์ ์ฅ ์์น ๋ฐ๋ผ) |
| ๋ชจ๋ฐ์ผ | ๋ถ์์ฐ์ค๋ฌ์ | ์์ฐ์ค๋ฌ์ |
| MSA | ๋ถ์ ํฉ | ์ ํฉ |
| ํธ๋ํฝ | DB ์กฐํ โ | ๊ฒ์ฆ๋ง |
| ํฌ๊ธฐ | Cookie ID๋ง | ๋งค ์์ฒญ ํ ํฐ ์ ์ก |
์ ํ ๊ฐ์ด๋ โญ :
| ์๋๋ฆฌ์ค | ์ถ์ฒ |
|---|---|
| ์ ํต ์น ๋ชจ๋๋ฆฌ์, B2C | Session (๋จ์) |
| SPA (React/Vue) + REST API | JWT |
| ๋ชจ๋ฐ์ผ ์ฑ | JWT |
| MSA / ๋ถ์ฐ ์์คํ | JWT |
| ๊ธ์ต / ์ฆ์ ์ฐจ๋จ ํ์ | Session ๋๋ ์งง์ JWT |
| SSO ํ์ | JWT (๋๋ OAuth2) |
ILIC ์๋๋ฆฌ์ค ๋ถ์:
๋ฉด์ ๋ชจ์ ๋ต๋ณ (3๋ถ ๋ต๋ณ ์ค๋น) โญ :
"Session๊ณผ JWT์ ๊ฐ์ฅ ํฐ ์ฐจ์ด๋ ์ํ ๋ณด์ ์ฌ๋ถ์ ๋๋ค.
Session์ ์๋ฒ์ ์ธ์ฆ ์ ๋ณด๋ฅผ ๋ณด๊ดํด์ ์ฆ์ ํ๊ธฐ๊ฐ ๊ฐ๋ฅํ์ง๋ง, ๋ถ์ฐ ํ๊ฒฝ์์๋ ์๋ฒ ๊ฐ ๊ณต์ ๋ฅผ ์ํด Redis ๊ฐ์ ์ธ๋ถ ์ ์ฅ์๊ฐ ํ์ํฉ๋๋ค.
JWT๋ ํ ํฐ ์์ฒด์ ์ฌ์ฉ์ ์ ๋ณด๊ฐ ๋ด๊ฒจ ์๋ฒ๋ ์๋ช ๋ง ๊ฒ์ฆํ๋ฉด ๋๋ Stateless ๋ฐฉ์์ ๋๋ค. ํ์ฅ์ฑ์ด ์ข๊ณ ๋ง์ดํฌ๋ก์๋น์ค์ ์ ํฉํ์ง๋ง, ์ฆ์ ํ๊ธฐ๊ฐ ์ด๋ ค์์ ์งง์ ๋ง๋ฃ ์๊ฐ + Refresh Token ํจํด์ ๋ณดํต ์ฌ์ฉํฉ๋๋ค.
์ ๋ Vue SPA ํ๊ฒฝ์์๋ JWT๋ฅผ, ์ ํต ์น์์๋ Session์ ๊ถ์ฅํ์ง๋ง, ์ฆ์ ์ฐจ๋จ์ด ์ค์ํ ๊ฒฐ์ ๊ฐ์ ์์ญ์ JWT์ฌ๋ ๋ง๋ฃ๋ฅผ ๋งค์ฐ ์งง๊ฒ ํ๊ฑฐ๋ Token Blacklist๋ฅผ ๋๋ ๋ฑ ์ถ๊ฐ ์ค๊ณ๊ฐ ํ์ ํ๋ค๊ณ ์๊ฐํฉ๋๋ค."
์๊ธฐ ์ ๊ฒ
๋ชฉํ: JWT์ ๊ตฌ์กฐ๋ถํฐ ๋ณด์ ์ทจ์ฝ์ ๊น์ง ๊น์ด ์๊ฒ ๋ง์คํฐํ๋ค.
์ ์ ์ง์: Phase 5
ํต์ฌ ๊ตฌ์กฐ:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI0MiJ9.signature
โโโ Header โโโ.โโโ Payload โโโ.โโโ Signature โโโ
์ธ ๋ถ๋ถ์ด ๋ง์นจํ(.) ๋ก ๊ตฌ๋ถ.
{
"alg": "HS256",
"typ": "JWT"
}
์๊ณ ๋ฆฌ์ฆ ์ข ๋ฅ:
๋์นญ vs ๋น๋์นญ:
| ๋์นญ (HS256) | ๋น๋์นญ (RS256) | |
|---|---|---|
| ํค | ๋จ์ผ ๋น๋ฐํค | ๊ณต๊ฐํค + ๋น๋ฐํค |
| ๋ฐ๊ธ/๊ฒ์ฆ | ๊ฐ์ ํค | ๋น๋ฐํค ๋ฐ๊ธ, ๊ณต๊ฐํค ๊ฒ์ฆ |
| ๋ถ์ฐ ํ๊ฒฝ | ํค ๊ณต์ ์ด๋ ค์ | ๊ฒ์ฆ์๊ฐ ๊ณต๊ฐํค๋ง |
| ์ ํฉ | ๋จ์ผ ์๋ฒ | MSA, OAuth2 |
โ MSA์์๋ RS256 ๊ถ์ฅ โญ
{
"sub": "42", // Subject โ ์ฌ์ฉ์ ID
"name": "Alice",
"role": "USER",
"iat": 1700000000, // Issued At
"exp": 1700003600 // Expiration (๋ง๋ฃ)
}
ํ์ค Claim โญ :
| Claim | ์๋ฏธ |
|---|---|
| iss (Issuer) | ๋ฐ๊ธ์ |
| sub (Subject) | ์ฃผ์ฒด (๋ณดํต ์ฌ์ฉ์ ID) |
| aud (Audience) | ๋์ |
| exp (Expiration) | ๋ง๋ฃ ์๊ฐ โญ |
| iat (Issued At) | ๋ฐ๊ธ ์๊ฐ |
| nbf (Not Before) | ์ ํจ ์์ ์๊ฐ |
| jti (JWT ID) | ๊ณ ์ ์๋ณ์ |
Custom Claim:
role, email ๋ฑ ์์ ๋กญ๊ฒ ์ถ๊ฐ ๊ฐ๋ฅโ ๏ธ ์ฃผ์ โญ :
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret_key
)
์ญํ โญ :
ํต์ฌ ํต์ฐฐ:
"JWT๋ ์ํธํ ๊ฐ ์๋ ์๋ช ์ด๋ค. ๋๊ตฌ๋ ๋ด์ฉ์ ๋ณผ ์ ์์ง๋ง, ๋ณ์กฐํ๋ฉด ๋คํจ๋ค."
๋์ฝ๋ฉ ๋๊ตฌ:
์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: Unit 6.1
๋ผ์ด๋ธ๋ฌ๋ฆฌ โ JJWT (๊ฐ์ฅ ์ธ๊ธฐ):
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'
JwtTokenProvider ๊ตฌํ:
@Component
public class JwtTokenProvider {
private final SecretKey secretKey;
private final long accessTokenValidity = 1000 * 60 * 60; // 1์๊ฐ
private final long refreshTokenValidity = 1000 * 60 * 60 * 24 * 7; // 7์ผ
public JwtTokenProvider(@Value("${jwt.secret}") String secret) {
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
// ํ ํฐ ์์ฑ
public String createAccessToken(Authentication auth) {
UserDetails userDetails = (UserDetails) auth.getPrincipal();
String authorities = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
return Jwts.builder()
.subject(userDetails.getUsername())
.claim("authorities", authorities)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + accessTokenValidity))
.signWith(secretKey, Jwts.SIG.HS256)
.compact();
}
// ํ ํฐ ๊ฒ์ฆ + Authentication ๋ฐํ
public Authentication getAuthentication(String token) {
Claims claims = Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get("authorities").toString().split(","))
.map(SimpleGrantedAuthority::new)
.toList();
UserDetails principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}
// ํ ํฐ ์ ํจ์ฑ ๊ฒ์ฌ
public boolean validateToken(String token) {
try {
Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token);
return true;
} catch (ExpiredJwtException e) {
log.info("Expired JWT");
} catch (JwtException | IllegalArgumentException e) {
log.error("Invalid JWT");
}
return false;
}
}
JwtAuthenticationFilter (์ปค์คํ Filter):
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtProvider;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
String token = resolveToken(request);
if (token != null && jwtProvider.validateToken(token)) {
Authentication auth = jwtProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String bearer = request.getHeader("Authorization");
if (StringUtils.hasText(bearer) && bearer.startsWith("Bearer ")) {
return bearer.substring(7);
}
return null;
}
}
SecurityConfig ํตํฉ:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
ํต์ฌ:
STATELESS โ ์ธ์
์์ฑ Xcsrf().disable() โ JWT๋ CSRF ์ํ ๋ฎ์UsernamePasswordAuthenticationFilter ์์์๊ธฐ ์ ๊ฒ
OncePerRequestFilter๋ฅผ ์์? (ํํธ: ํ ์์ฒญ๋น ํ ๋ฒ๋ง โ Filter ์ค๋ณต ํธ์ถ ๋ฐฉ์ง)์ ์ ์ง์: Unit 6.2
๋ฌธ์ :
ํด๊ฒฐ โ Access + Refresh ํจํด โญ :
[Login]
โ
Access Token (15๋ถ) + Refresh Token (7์ผ)
โ
Access ๋ง๋ฃ ์:
โ
Refresh Token์ผ๋ก ์ Access Token ๋ฐ๊ธ
โ
Refresh ๋ง๋ฃ ์: ์ฌ๋ก๊ทธ์ธ
๊ตฌํ:
1. ๋ก๊ทธ์ธ ์ ๋ ๋ค ๋ฐ๊ธ:
@PostMapping("/api/auth/login")
public LoginResponse login(@RequestBody LoginRequest request) {
Authentication auth = authManager.authenticate(...);
String accessToken = jwtProvider.createAccessToken(auth);
String refreshToken = jwtProvider.createRefreshToken(auth);
// Refresh Token์ Redis์ ์ ์ฅ (์ฆ์ ํ๊ธฐ ๊ฐ๋ฅ)
refreshTokenRepository.save(
new RefreshToken(auth.getName(), refreshToken, Duration.ofDays(7))
);
return new LoginResponse(accessToken, refreshToken);
}
2. Access ๋ง๋ฃ ์ ๊ฐฑ์ :
@PostMapping("/api/auth/refresh")
public AccessTokenResponse refresh(@RequestBody RefreshRequest request) {
String refreshToken = request.getRefreshToken();
if (!jwtProvider.validateToken(refreshToken)) {
throw new InvalidTokenException();
}
String username = jwtProvider.getUsername(refreshToken);
// Redis ๊ฒ์ฆ (ํ์ทจ๋ ํ ํฐ ๋ฐฉ์ด)
if (!refreshTokenRepository.existsByUsernameAndToken(username, refreshToken)) {
throw new InvalidTokenException();
}
// ์ Access Token ๋ฐ๊ธ
Authentication auth = jwtProvider.getAuthentication(refreshToken);
return new AccessTokenResponse(jwtProvider.createAccessToken(auth));
}
Refresh Token Rotation (๋ณด์ ๊ฐํ) โญ :
// refresh ์
String newRefresh = jwtProvider.createRefreshToken(auth);
refreshTokenRepository.delete(oldRefresh);
refreshTokenRepository.save(newRefresh);
์ ์ฅ ์์น ๊ฒฐ์ โ ๏ธ :
LocalStorage:
HttpOnly Cookie:
Memory (๋ณ์):
์ค๋ฌด ํจํด:
ILIC ๊ถ์ฅ:
์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: Unit 6.1~6.3
์ฃผ์ ์ทจ์ฝ์ โญ :
"alg": "none" ๋ก ๋ณ์กฐ๋ฐฉ์ด:
// JJWT๋ ๊ธฐ๋ณธ์ ์ผ๋ก none ์ฐจ๋จ (์๋)
// ๊ทธ๋ฌ๋ ๋ช
์ ๊ถ์ฅ
.parser()
.verifyWith(secretKey)
.requireIssuer("ilic")
.build()
๋ฐฉ์ด:
"mysecret" ๊ฐ์ ์งง์ ํค๋ฐฉ์ด:
// ์์ ํ ํค ์์ฑ
SecretKey key = Jwts.SIG.HS256.key().build();
String secretString = Encoders.BASE64.encode(key.getEncoded());
๋ฐฉ์ด:
๋ฐฉ์ด:
.parser()
.clockSkewSeconds(60) // 60์ด ํ์ฉ
.build()
๋ฐฉ์ด:
๋ณด์ ์ฒดํฌ๋ฆฌ์คํธ โญ :
exp) ํญ์ ํฌํจ์๊ธฐ ์ ๊ฒ
๋ชฉํ: ํ๋ ์ธ์ฆ์ ํ์ค์ธ OAuth2์ ํ๋ฆ์ ์ดํดํ๋ค.
์ ์ ์ง์: Phase 6
๋ฌธ์ :
"์ฌ์ฉ์๊ฐ ๋งค๋ฒ ID/PW ์ ๋ ฅํ์ง ์๊ณ , ๋ค๋ฅธ ์๋น์ค์ ์ธ์ฆ์ ํ์ฉ ํ๊ณ ์ถ๋ค"
์: "Google๋ก ๋ก๊ทธ์ธ", "GitHub์ผ๋ก ๋ก๊ทธ์ธ"
์ ํต ๋ฐฉ์์ ์ํ:
OAuth2์ ํด๊ฒฐ:
"๋น๋ฐ๋ฒํธ๋ฅผ ๊ณต์ ํ์ง ์๊ณ ๊ถํ๋ง ์์"
๋น์ :
ํธํ ๋ฐ๋ ํค โ ์๋๋ง ๊ฑธ ์ ์๊ณ ํธ๋ ํฌ๋ ๋ชป ์ถ.
4๊ฐ์ง ์ญํ โญ :
| ์ญํ | ์๋ฏธ |
|---|---|
| Resource Owner | ์ฌ์ฉ์ (๋ณธ์ธ) |
| Client | ์ฌ์ฉํ๋ ค๋ ์ฑ (์: Spotify) |
| Authorization Server | ๊ถํ ๋ฐ๊ธ (์: Google) |
| Resource Server | ๋ณดํธ๋ ์์ (์: Google Drive API) |
์๋๋ฆฌ์ค โ Spotify์์ Google ๋ก๊ทธ์ธ:
์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: Unit 7.1
๊ฐ์ฅ ํํ OAuth2 ํ๋ฆ (์๋ฒ ์ฑ):
1. [User] Spotify์์ "Google๋ก ๋ก๊ทธ์ธ" ํด๋ฆญ
โ
2. [Spotify] ์ฌ์ฉ์๋ฅผ Google ๋ก๊ทธ์ธ ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ํธ
GET https://accounts.google.com/o/oauth2/auth?
client_id=spotify_client_id&
redirect_uri=https://spotify.com/callback&
response_type=code&
scope=email profile
โ
3. [User] Google์ ๋ก๊ทธ์ธ + ๋์ ํ๋ฉด
โ
4. [Google] Spotify๋ก ๋ฆฌ๋ค์ด๋ ํธ + Authorization Code
GET https://spotify.com/callback?code=AUTH_CODE
โ
5. [Spotify Server] Code๋ก Access Token ์์ฒญ
POST https://oauth2.googleapis.com/token
{ code, client_id, client_secret, redirect_uri, grant_type }
โ
6. [Google] Access Token + (Refresh Token) ์๋ต
โ
7. [Spotify] Access Token์ผ๋ก Google API ํธ์ถ
GET https://googleapis.com/userinfo
Authorization: Bearer ACCESS_TOKEN
โ
8. [Google] ์ฌ์ฉ์ ์ ๋ณด ๋ฐํ
โ
9. [Spotify] ์ฌ์ฉ์ ๋ฑ๋ก/๋ก๊ทธ์ธ ์๋ฃ
ํต์ฌ:
๋ค๋ฅธ Grant Type:
| Grant | ์ฉ๋ |
|---|---|
| Authorization Code โญ | ์ผ๋ฐ ์น ์ฑ |
| Authorization Code + PKCE | SPA, ๋ชจ๋ฐ์ผ |
| Client Credentials | ์๋ฒ ๊ฐ (์ฌ์ฉ์ X) |
| Resource Owner Password | ๋ ๊ฑฐ์ (๊ถ์ฅ X) |
| Implicit | ์๋ ๋ฐฉ์ (๊ถ์ฅ X) |
PKCE (Proof Key for Code Exchange):
์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: Unit 7.2
๋ฌธ์ :
OIDC (OpenID Connect):
"OAuth2 ์์ ์ธ์ฆ ๋ ์ด์ด๋ฅผ ํ์คํ"
ํต์ฌ ์ถ๊ฐ:
/userinfo)ID Token vs Access Token:
| ID Token | Access Token | |
|---|---|---|
| ์ฉ๋ | ์ฌ์ฉ์ ์ ๋ณด | API ํธ์ถ ๊ถํ |
| ํ์ | ํญ์ JWT | JWT ๋๋ ๋ถํฌ๋ช ๋ฌธ์์ด |
| ๊ฒ์ฆ | Client๊ฐ ๊ฒ์ฆ | Resource Server๊ฐ ๊ฒ์ฆ |
| ๋ด์ฉ | ์ฌ์ฉ์ ์๋ณ | ๊ถํ |
// ID Token Payload ์
{
"iss": "https://accounts.google.com",
"sub": "10769150350006150715113082367",
"email": "alice@example.com",
"name": "Alice",
"picture": "https://...",
"iat": 1700000000,
"exp": 1700003600
}
Spring Security OAuth2 Client:
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope: email,profile
github:
client-id: ${GITHUB_CLIENT_ID}
client-secret: ${GITHUB_CLIENT_SECRET}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth -> oauth
.defaultSuccessUrl("/home")
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService) // ์ฌ์ฉ์ ์ฒ๋ฆฌ ์ปค์คํ
)
);
return http.build();
}
โ ์๋์ผ๋ก OAuth2/OIDC ํ๋ฆ ์ฒ๋ฆฌ
ILIC ์๋๋ฆฌ์ค:
์๊ธฐ ์ ๊ฒ
๋ชฉํ: ์น ๋ณด์์ 3๋ ์ํ โ CSRF, XSS, CORS โ ๋ฅผ ๊น์ด ์ดํดํ๋ค.
์ ์ ์ง์: Phase 5
ํต์ฌ ๊ฐ๋
CSRF:
"๋ค๋ฅธ ์ฌ์ดํธ์์ ์ฌ์ฉ์์ ์ธ์ฆ ์ ๋ณด๋ฅผ ํ์ฉํด ์์น ์๋ ์์ฒญ์ ๋ณด๋ด๊ฒ ํจ"
์๋๋ฆฌ์ค:
1. ์ฌ์ฉ์๊ฐ Bank.com์ ๋ก๊ทธ์ธ โ Cookie ์ ์ฅ
2. ์ฌ์ฉ์๊ฐ Evil.com ๋ฐฉ๋ฌธ
3. Evil.com์ ์จ๊ฒจ์ง ํผ:
<form action="https://bank.com/transfer" method="POST">
<input name="to" value="hacker">
<input name="amount" value="1000000">
</form>
4. ์๋ ์ ์ถ โ Bank.com์ ์์ฒญ
5. Bank.com: "์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ๋์ด ์๋ค" โ ์ก๊ธ ์ฒ๋ฆฌ!
ํต์ฌ:
๋ฐฉ์ด ๋ฐฉ๋ฒ โญ :
<form>
<input type="hidden" name="_csrf" value="abc123">
<!-- ... -->
</form>
Spring Security ๊ธฐ๋ณธ ํ์ฑํ:
http.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
Set-Cookie: SESSIONID=abc; SameSite=Lax
| ๊ฐ | ์๋ฏธ |
|---|---|
| Strict | ๊ฐ์ ์ฌ์ดํธ๋ง ์ ์ก (๊ฐ์ฅ ์์ ) |
| Lax | GET ๋ฑ ์์ ๋ฉ์๋๋ง cross-site (๊ธฐ๋ณธ) |
| None | ๋ชจ๋ cross-site (Secure ํ์) |
JWT ์ฌ์ฉ ์:
http.csrf(AbstractHttpConfigurer::disable);
// JWT๋ CSRF ์ํ ๋ฎ์
์ธ์ CSRF ํ์ฑํ/๋นํ์ฑํ?:
์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: Unit 8.1
ํต์ฌ ๊ฐ๋
XSS:
"์ ์์ JavaScript ๋ฅผ ๋ค๋ฅธ ์ฌ์ฉ์์ ๋ธ๋ผ์ฐ์ ์์ ์คํ"
3๊ฐ์ง ์ ํ:
1. ๊ณต๊ฒฉ์: ๊ฒ์๊ธ์ <script>fetch('/api/cookies?c=' + document.cookie)</script>
2. DB ์ ์ฅ
3. ๋ค๋ฅธ ์ฌ์ฉ์๊ฐ ๊ฒ์๊ธ ์กฐํ โ ์คํฌ๋ฆฝํธ ์คํ
4. ์ฌ์ฉ์์ Cookie๋ฅผ ๊ณต๊ฒฉ์ ์๋ฒ๋ก ์ ์ก
URL: /search?q=<script>...</script>
์๋ฒ๊ฐ ๊ฒ์์ด๋ฅผ ๊ทธ๋๋ก ํ์ด์ง์ ๋
ธ์ถ โ ์คํ
๋ฐฉ์ด ๋ฐฉ๋ฒ โญ :
Thymeleaf (์๋ ์ด์ค์ผ์ดํ):
<p th:text="${userInput}"> <!-- ์๋ ์ด์ค์ผ์ดํ โ
-->
<p th:utext="${userInput}"> <!-- ์ด์ค์ผ์ดํ X โ ๏ธ -->
JSP:
<c:out value="${userInput}"/> <!-- ์๋ ์ด์ค์ผ์ดํ -->
React/Vue: ๊ธฐ๋ณธ ์ด์ค์ผ์ดํ ์ ์ฉ ({userInput} vs dangerouslySetInnerHTML)
@PostMapping("/comment")
public Comment create(@Valid @RequestBody CommentRequest request) {
// <, >, " ๋ฑ ์ฐจ๋จ ๋๋ ์ธ์ฝ๋ฉ
}
Content-Security-Policy: default-src 'self'; script-src 'self'
โ ์ธ๋ผ์ธ ์คํฌ๋ฆฝํธ ์ฐจ๋จ
Set-Cookie: SESSIONID=abc; HttpOnly
JWT ์ ์ฅ๊ณผ XSS โ ๏ธ :
์๊ธฐ ์ ๊ฒ
์ ์ ์ง์: Phase 1
ํต์ฌ ๊ฐ๋
Same-Origin Policy (๋ธ๋ผ์ฐ์ ๋ณด์ ๊ธฐ๋ณธ):
"JS๋ ๊ฐ์ origin(scheme + host + port) ์ ๋ฆฌ์์ค๋ง ์ ๊ทผ ๊ฐ๋ฅ"
์:
https://ilic.com:443 ์ JS โ https://ilic.com:443/api โ
https://ilic.com:443 ์ JS โ https://api.ilic.com:443 โ (๋ค๋ฅธ host)https://ilic.com:443 ์ JS โ http://ilic.com:443 โ (๋ค๋ฅธ scheme)์ ์ด ์ ์ฑ ?:
๋ฌธ์ โ ํ๋ ์น์ ํ์ค:
localhost:3000) โ API (localhost:8080) โ ๋ค๋ฅธ originCORS โ Same-Origin ์์ธ ํ์ฉ:
"์๋ฒ๊ฐ ํน์ ๋ค๋ฅธ origin์ ์์ฒญ์ ๋ช ์์ ์ผ๋ก ํ์ฉ"
CORS ํ๋ฆ โญ :
Preflight ํ๋ฆ:
1. [Browser] OPTIONS /api/users
Origin: https://ilic.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Authorization
โ
2. [Server] 200 OK
Access-Control-Allow-Origin: https://ilic.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization
Access-Control-Allow-Credentials: true
โ
3. [Browser] PUT /api/users (์ค์ ์์ฒญ)
Spring Security CORS ์ค์ :
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// ...
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://ilic.com", "https://admin.ilic.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true); // Cookie ์ ์ก ํ์ฉ
config.setMaxAge(3600L); // Preflight ์บ์ฑ
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
* ์์ด๋์นด๋ vs ๋ช
์์ origin โ ๏ธ :
config.setAllowedOrigins(List.of("*")); // โ ์ํ
config.setAllowedOrigins(List.of("https://ilic.com")); // โ
์์
allowCredentials=true ์ผ ๋ * ์ฌ์ฉ ๋ถ๊ฐ (๋ธ๋ผ์ฐ์ ๊ฐ ์ฐจ๋จ).
ILIC ์๋๋ฆฌ์ค:
localhost:3000 (Vue) โ localhost:8080 (Spring) โ CORS ํ์ilic.com โ api.ilic.com โ CORS ํ์์๊ธฐ ์ ๊ฒ
/api)์ ์ ์ง์: Unit 8.1~8.3
ILIC ๋ณด์ ์ ๊ฒ ๋ฆฌ์คํธ โญ :
์๊ธฐ ์ ๊ฒ
โ โ โ ๋ฉด์ ๋จ๊ณจ (๋ฐ๋์):
โ โ ๋งค์ฐ ๊ถ์ฅ:
Phase 2 (Filter Chain):
Phase 5 (Session vs Token):
์ด๋ฒ ์ฃผ์ฐจ๋ ๋ฐ๋์ ์์ ํ๋ก์ ํธ๋ฅผ ์ง์ ๋ง๋ค์ด๋ณด์ธ์:
Spring Boot + JWT ์ธ์ฆ ๋ฏธ๋ ํ๋ก์ ํธ:
OAuth2 ํตํฉ:
๋ณด์ ์ทจ์ฝ์ ์ค์ต:
์ด 3๊ฐ์ง๋ฅผ ๊ฑฐ์น๋ฉด ๋ฉด์ ๋ต๋ณ์ด ์์ฐ์ค๋ฌ์์ง๋๋ค.
์ด์ ๋ง๋ฌด๋ฆฌ ๋จ๊ณ๋ก ๊ฐ๊ณ ์์ต๋๋ค:
| ์์ญ | ์ฃผ์ฐจ | ๊น์ด |
|---|---|---|
| Java/Spring/JPA | 1-12 | โ โ โ |
| DB | 13-14 | โ โ โ |
| Spring MVC | 15 | โ โ โ |
| ๋ถ์ฐ ์์คํ | 16-17 | โ โ โ |
| Spring Security | 18 | โ โ โ |