Spring Security란?
스프링 기반 애플리케이션의 인증(Authentication)과 인가(Authorization)를 담당하는 보안 프레임워크
로그인, 접근 제어, CSRF 방어, 세션 관리, OAuth2 지원 등의 다양한 보안 기능을 제공한다.
Gradle 추가
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
}
Spring Security 설정 변경 (SecurityConfig)
Spring Boot 3.x부터는 SecurityFilterChain을 사용하여 보안 설정을 변경해야 한다.
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig {
private final AuthenticationConfiguration configuration;
// 비밀번호 암호화
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
// <script>alert("asd")</script>, 일반적으로 XSS
// <script>게시글 작성 스크립트</script>, 일반적으로 CSRF
// CSRF : 크로스 사이트 요청 변조
http.csrf(AbstractHttpConfigurer::disable);
// http 기본 인증(아이디/비밀번호 입력 창) 비활성화
http.httpBasic(AbstractHttpConfigurer::disable);
// 폼 로그인 비활성화 (기본적으로 제공되는 로그인 폼 사용 x)
http.formLogin(AbstractHttpConfigurer::disable);
// 접근 권한 설정 (인가 - Authorization)
http.authorizeHttpRequests(
(auth) -> auth
.requestMatchers("/login").permitAll() // 누구나 접근 가능
.requestMatchers("/user/signup").permitAll() // 누구나 접근 가능
.requestMatchers("/a/ex01").hasRole("USER") // "ROLE_USER"인 사용자만 접근 가능
.anyRequest().authenticated() // 그 외 요청은 로그인 필요
);
// 커스텀 로그인 필터 추가
http.addFilterAt(new LoginFilter(configuration.getAuthenticationManager()), UsernamePasswordAuthenticationFilter.class);
// JWT 필터 추가
http.addFilterBefore(new JwtFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
인증(Authentication)
누구인지 확인하는 과정 (로그인)
- 사용자의 아이디와 비밀번호를 확인하고, 시스템에 접근할 수 있도록 인증하는 과정
인증 수행 과정 (UsernamePasswordAuthenticationFilter)
1. 클라이언트 요청 -> AuthenticationFilter
- 클라이언트가 HTTP 요청을 보내면 서블릿 필터에서 SecurityFilterChain으로 작업이 위임되고, 그 중UsernamePasswordAuthenticationFilter(위 그림에서는AuthenticationFilter)에서 인증을 처리한다.
// MemberDto.SignupRequest MemberDto =
// new MemberDto.SignupRequest(request.getParameter("Membername"), request.getParameter("password"));
// JSON 형태의 데이터를 처리하도록 변경
MemberDto.SignupRequest MemberDto = new ObjectMapper().readValue(request.getInputStream(), MemberDto.SignupRequest.class);
2. AuthenticationFilter에서 토큰 생성
- AuthenticationFilter는 요청 객체(HttpServletRequest)에서 username과 password를 추출해서 토큰을 생성한다.
authToken = new UsernamePasswordAuthenticationToken(MemberDto.getEmail(), MemberDto.getPassword(), null);
3. 토큰 생성 후 AutheicationManager에게 토큰 전달
- AutheicationManager는 인터페이스다.
- 일반적으로 사용되는 구현체는 ProviderManager
return authenticationManager.authenticate(authToken);
4. ProviderManager는 인증을 위해 AuthenticationProvider로 토큰을 전달한다.
5. AuthenticationProvider는 토큰의 정보를 UserDetailsService에 전달한다.
6. UserDetailsService는 전달받은 정보를 통해 데이터베이스에서 일치하는 사용자를 찾아 UserDetails 객체를 생성한다.
@RequiredArgsConstructor
@Service
public class MemberService implements UserDetailsService {
//...
}
// UserDetailsService 인터페이스
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
7. 생성된 UserDetails 객체는 AuthenticationProvider로 전달되며, 해당 Provider에서 인증을 수행하고 성공하게 되면 ProviderManger로 권한을 담은 토큰을 전달한다.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Member> result = memberRepository.findByEmail(username);
if (result.isPresent()) {
Member member = result.get();
return member;
}
return null;
}
8. ProviderManager는 검증된 토큰을 AuthenticationFilter로 전달한다.
9. AuthenticationFilter는 검증된 토큰을 SecurityContextHolder에 있는 SecurityContext에 저장한다.
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
LoginFilter
Member 엔티티에서 이메일과 비밀번호를 가져와 토큰을 생성하고 인증을 처리해준다.
@RequiredArgsConstructor
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
// 원래는 form-data 형식으로 사용자 정보를 입력받았는데
// JSON 형태로 입력을 받기 위해서 재정의
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
System.out.println("LoginFilter 실행");
UsernamePasswordAuthenticationToken authToken;
// 그림에서 1번 로직
// MemberDto.SignupRequest MemberDto =
// new MemberDto.SignupRequest(request.getParameter("Membername"), request.getParameter("password"));
try {
// 그림에서 원래 1번이었던 로직을 JSON 형태의 데이터를 처리하도록 변경
MemberDto.SignupRequest MemberDto = new ObjectMapper().readValue(request.getInputStream(), MemberDto.SignupRequest.class);
// 그림에서 2번 로직
authToken = new UsernamePasswordAuthenticationToken(MemberDto.getEmail(), MemberDto.getPassword(), null);
} catch (IOException e) {
throw new RuntimeException(e);
}
return authenticationManager.authenticate(authToken);
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
Member Member = (Member) authResult.getPrincipal();
String jwtToken = JwtUtil.generateToken(Member.getIdx(), Member.getEmail(), Member.getNickName(), Member.getRole());
// 빌더 패턴으로 객체를 생성
ResponseCookie cookie = ResponseCookie
.from("ATOKEN", jwtToken)
.path("/")
.httpOnly(true)
.secure(true)
.maxAge(Duration.ofHours(1L))
.build();
response.setHeader(HttpHeaders.SET_COOKIE, cookie.toString());
}
}
필터들을 추가해준다.
http.addFilterAt(new LoginFilter(configuration.getAuthenticationManager()), UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(new JwtFilter(), UsernamePasswordAuthenticationFilter.class);
Sprint Security 주요 모듈
Authentication
현재 접근하는 주체의 정보와 권한을 담는 인터페이스
public interface Authentication extends Principal, Serializable {
// 현재 사용자의 권한 목록
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
// 인증 여부
boolean isAuthenticated();
// 인증 여부 설정
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
SecurityContext
Authentication을 보관하는 역할
SecurityContext를 통해 Authentication 객체를 꺼내올 수 있음
public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication authentication);
}
SecurityContextHolder
보안 주체의 세부 정보를 포함하여 응용 프로그램의 현재 SecurityContext에 대한 세부 정보가 저장된다.
- 인증에 성공하면 principal과 credential 정보를 Authentication에 담는다.
- 그리고 Spring Security에서 Authetication을 SecurityContext에 보관한다.
- 보관한 SecurityContext를 SecurityContextHolder에 담아 보관한다.
UserDetails
인증에 성공하여 생성된 UserDetails 객체는 토큰을 생성하기 위해 사용된다.
엔티티에 UserDetails를 implements하여 처리한다.
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Entity
public class Member implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long idx;
private String email;
private String password;
private String nickName;
private String role;
private boolean enabled;
@OneToMany(mappedBy = "member")
private List<EmailVerify> emailVerifyList = new ArrayList<>();
@OneToMany(mappedBy = "member")
private List<Course> courseList = new ArrayList<>();
public void verify() {
this.enabled = true;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
GrantedAuthority authority = new SimpleGrantedAuthority(role);
authorities.add(authority);
return authorities;
}
@Override
public String getUsername() {
return email;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
}
UserDetailsService
UserDetails 객체를 반환하는 하나의 메서드만 가지고 있다.
일반적으로 이것을 UserRepository 를 주입받아 DB와 연결하여 처리한다.
@RequiredArgsConstructor
@Service
public class MemberService implements UserDetailsService {
private final MemberRepository memberRepository;
public void signup(MemberDto.SignupRequest dto) {
// 회원가입 로직
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Member> result = memberRepository.findByEmail(username);
if (result.isPresent()) {
Member member = result.get();
return member;
}
return null;
}
}
UsernamePasswordAuthenticationToken
Authentication을 implements한 AbstractAuthenticationToken의 하위 클래스로,
User의 ID가 Principal 역할을 하고 Password가 Credential의 역할을 한다.
UsernamePasswordAuthenticationToken의 첫 번째 생성자는 인증 전의 객체를 생성하고, 두 번째는 인증이 완료된 객체를 생성한다.
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 620L;
private final Object principal;
private Object credentials;
// 인증 완료 전 객체 생성
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
// 인증 완료 후 객체 생성
public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
}
AuthenticationManager
인증에 대한 부분은 AuthenticationManager를 통해서 처리하게 되는데,
실질적으로는 AuthenticationManager에 등록된 AuthenticationProvider에 의해 처리된다.
인증에 성공하면 생성자를 이용해 객체를 생성하여 SecurityContext에 저장한다
@FunctionalInterface
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
AuthenticationProvider
AuthenticationProvider에서는 실제 인증에 대한 부분을 처리하는데,
인증 전의 Authentication 객체를 받아서 인증이 완료된 객체를 반환하는 역할을 한다.
'BE > Spring Boot' 카테고리의 다른 글
[Spring Boot] 연관관계 매핑 (0) | 2025.02.10 |
---|---|
[Java] Lombok이란? / 주요 어노테이션 정리 (0) | 2025.02.10 |
[Spring Boot] DTO 생성 방법 (단일 파일, 이너 클래스) / 생성 시 주의사항 (0) | 2025.02.10 |
[Spring Boot] Repository 메서드 생성 규칙 (findAll, findBy) (0) | 2025.02.07 |
[Spring Boot] JPA란? / JPA 구성 요소(Entity, Repository) / JPA로 DB 다루기 (1) | 2025.02.06 |