Spring Security 프레임워크는 스프링 서버에 필요한 인증 및 인가를 위해 많은 기능을 제공.
Spring Security 적용하는 법.
build.gradle에 implementation 추가
// 스프링 시큐리티
implementation 'org.springframework.boot:spring-boot-starter-security'
Spring Security 활성화
WebSecurityConfig
package com.sparta.springsecurity.config;
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.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함
public class WebSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// CSRF 설정
http.csrf().disable();
http.authorizeRequests().anyRequest().authenticated();
// 로그인 사용
http.formLogin();
return http.build();
}
}
CSRF(사이트 간 요청 위조, Cross-site request forgery)
Spring Security는 요청이 들어오면 Servlet FilterChain을 자동으로 구성 후 거치게 함.
FilterChain은 여러 Filter를 Chain형태로 묶어놓은 것.
Filter?
톰캣과 같은 웹 컨테이너에서 관리되는 서블릿 기술.
Filter는 Client 요청이 전달되기 전후의 URL 패턴에 맞는 모든 요청에 필터링.
CSRF, XSS 등의 보안 검사를 통해 올바른 요청이 아닐 시 이를 차단.
Spring Security -> Filter를 사용해 인증/인가를 구현.
Spring의 보안 Filter를 결정하는 데 사용되는 Filter
session, jwt 등의 인증방식들을 사용하는데 필요한 설정을 완전히 분리할 수 있는 환경 제공.
사용자의 credential을 인증하기 위한 베이스 Filter
사용자 인증을 하는 filter
AbstractAuthenticationProcessingFilter를 상속한 Filter.
기본적으로 아래와 같은 Form Login 기반 사용할 때 username과 password 확인해 인증.
Form Login 기반은 인증이 필요한 URL 요청이 들어왔을 때 인증 되지 않았다면 로그인 페이지 반환.
SecurityContextHolder에는 스프링 시큐리티로 인증을 한 사용자의 상세 정보 저장
SecurityContext?
SecurityContextHolder로 접근할 수 있고, Authentication 객체를 가짐.
// 예시코드
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication = new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
<UserDetails>
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
UserRoleEnum role = user.getRole();
String authority = role.getAuthority();
System.out.println("authority = " + authority);
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(simpleGrantedAuthority);
return authorities;
}
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
UsernamePasswordAuthenticationToken은
Authentication을 implements한 AbstractAuthenticationToken의 하위 클래스.
-> 인증 객체를 만드는데 사용.
UserDetailsService는 username/password 인증방식을 사용 시 사용자를 조회하고 검증한 후 UserDetails를 반환.
Custom하여 Bean으로 등록 후 사용 가능!
검증된 UserDetails는 UsernamePasswordAuthenticationToken 타입의 Authentication(인증객체)을 만들 때 사용.
해당 인증 객체는 SecurityContextHolder에 세팅.
Custom해 사용가능.
회원 등록 시 비밀번호는 사용자가 입력한 문자 그대로 DB에 등록하면 안된다.
'정보통신망법, 개인정보보호법'에 의해 비밀번호 암호화(Encryption)가 의무.
Spring Security가 제공하는 적응형 단방향 함수인 bCrypt 를 사용해 비밀번호 암호화
적응형 단방향 함수는 내부적으로 리소스의 낭비가 매우 심해, API 요청마다 사용자의 이름과 비밀번호를 검증하면 애플리케이션 성능이 크게 떨어질 수 있다.
따라서 세션, 토큰 과 같은 인증방식을 사용해 검증하는 것이 속도 및 보안 측면에서 유리.
Spring Security에서는 비밀번호를 암호화하는 함수를 제공할 뿐만 아니라 사용자가 입력한 비밀번호를 저장된 비밀번호와 비교해 일치여부를 확인해주는 함수도 제공.
// 사용예시
// 비밀번호 확인
if(!passwordEncoder.matches("사용자가 입력한 비밀번호", "저장된 비밀번호")) {
throw new IllegalAccessError("비밀번호가 일치하지 않습니다.");
}
boolean matches(CharSequence rawPassword, String encodedPassword);
* rawPassword: 사용자가 입력한 비밀번호.
*** encodedPassword: 암호화되어 DB에 저장된 비밀번호.
즉, 실습에서는 토큰방식이 적용 X -> Filter에서 사용자가 요청한 로그인을 검증해 인증.
토큰방식 적용 시 -> 사용자의 로그인, 회원가입과 같은 요청은 Filter에서 인증되지 않게 permitAll 처리 -> 실제 검증 및 인증처리는 service 에서 수행, 그외의 인증이 필요한 요청 -> 로그인을 통해 발급받은 토큰을 같이 보내 Filter에서 토큰을 검증 후 인증처리.
1.1 회원 상세정보(UserServicelmpl)를 통해 "권한(Authority)" 설정 가능
1.2 권한을 1개 이상 설정 가능
1.3 "권한 이름" 규칙
a. "ROLE_" 로 시작하게 만듬
ex) "ADMIN" 권한 부여 -> "ROLE_ADMIN"
"USER" 권한부여 -> "ROLE_USER"
package com.sparta.springsecurity.entity;
public enum UserRoleEnum {
USER(Authority.USER), // 사용자 권한
ADMIN(Authority.ADMIN); // 관리자 권한
private final String authority;
UserRoleEnum(String authority) {
this.authority = authority;
}
public String getAuthority() {
return this.authority;
}
public static class Authority {
public static final String USER = "ROLE_USER";
public static final String ADMIN = "ROLE_ADMIN";
}
}
public class UserDetailsImpl implements UserDetails {
// ...
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
SimpleGrantedAuthority adminAuthority = new SimpleGrantedAuthority("ROLE_ADMIN");
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(adminAuthority);
return authorities;
}
}
// (관리자용) 등록된 모든 상품 목록 조회
@Secured("ROLE_ADMIN")
@GetMapping("/api/admin/products")
public List<Product> getAllProducts() {
return productService.getAllProducts();
}
@Configuration
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함
@EnableGlobalMethodSecurity(securedEnabled = true) // @Secured 어노테이션 활성화
public class WebSecurityConfig {
클라이언트 오류 상태. 서버에 요청이 전달되었지만, 권한 때문에 거절됨.
MDN Docs) https://developer.mozilla.org/ko/docs/Web/HTTP/Status/403