▶ LEVEL 2
▷ 9. Spring Security
● 문제의 요구사항
1. 기존 Filter와 Argument Resolver를 사용하던 코드들을 Spring Security로 변경해라
- 접근 권한 및 유저 권한 기능은 그대로
- 권한은 Spring Security의 기능을 사용
2. 토큰 기반 인증 방식은 유지, JWT는 그대로 사용
우선 Spring Security에 관하여 알아보자
● Spring Security란?
애플리케이션의 인증과 권한을 처리하는 강력한 보안 프레임워크
● Spring Security의 주요 개념
1. 인증(Authentication)
- 사용자가 누구인지 확인하는 과정
- 아이디/비밀번호 기반 로그인 또는 JWT, OAuth2 등을 사용
- AuthenticationManager가 인증을 담당
2. 권한
- 인증된 사용자가 특정 리소스에 접근할 수 있는지 확인하는 과정
- 스프링 시큐리티는 역활 기반 접근 제어를 제공
- @PreAuthorize, @Secured 같은 어노테이션을 활용할 수 있음
● Spring Security의 동작 방식
1. 사용자가 요청을 보냄
2. Spring Security 필터가 요청을 가로챔
3. AuthenticationManager를 통해 인증을 시도
4. 성공하면 SecurityContextHolder에 인증 정보 저장
5. Authorization을 통해 요청된 리소스에 대한 접근 권한 확인
6. 허가되면 요청 처리, 아니면 403 Forbidden응답 반환
이제 Spring Security를 적용시켜보자
우선, Security Config를 제작하자
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(jwtAuthenticationFilter, SecurityContextHolderAwareRequestFilter.class)
.formLogin(AbstractHttpConfigurer::disable)
.anonymous(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.logout(AbstractHttpConfigurer::disable)
.rememberMe(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers(request -> request.getRequestURI().startsWith("/auth")).permitAll()
.requestMatchers("/test").hasAuthority(UserRole.Authority.ADMIN)
.requestMatchers("/open").permitAll()
.anyRequest().authenticated()
)
.build();
}
}
코드를 작성하면
UserRole.Authority.ADMIN
코드에서 오류가 나타나기에 UserRole을 수정한다.
@Getter
@RequiredArgsConstructor
public enum UserRole {
ROLE_ADMIN(Authority.ADMIN),
ROLE_USER(Authority.USER);
private final String userRole;
public static UserRole of(String role) {
return Arrays.stream(UserRole.values())
.filter(r -> r.name().equalsIgnoreCase(role))
.findFirst()
.orElseThrow(() -> new InvalidRequestException("유효하지 않은 UerRole"));
}
public static class Authority {
public static final String ADMIN = "ROLE_ADMIN";
public static final String USER = "ROLE_USER";
}
}
이렇게 고치면 되는데
1. 스프링 시큐리티에서 제공하는 권한 기능을 사용하기 위해 prefix로 "ROLE_"을 붙였고
2. Authority클래스는 @Secured안에 enum을 곧바로 넣지 못하기 때문에 구현하였다.
다음으로 JwtAuthenticationFilter와 JwtAuthenticationToken을 구현한다.
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
String authorizationHeader = request.getHeader("Authorization");
if(authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
String jwt = jwtUtil.substringToken(authorizationHeader);
try{
Claims claims = jwtUtil.extractClaims(jwt);
if(SecurityContextHolder.getContext().getAuthentication() == null) {
setAuthentication(claims);
}
}catch (SecurityException | MalformedJwtException e) {
log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.", e);
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "유효하지 않는 JWT 서명입니다.");
} catch (ExpiredJwtException e) {
log.error("Expired JWT token, 만료된 JWT token 입니다.", e);
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "만료된 JWT 토큰입니다.");
} catch (UnsupportedJwtException e) {
log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.", e);
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "지원되지 않는 JWT 토큰입니다.");
} catch (Exception e) {
log.error("Internal server error", e);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
filterChain.doFilter(request, response);
}
private void setAuthentication(Claims claims) {
Long userId = Long.valueOf(claims.getSubject());
String email = claims.get("email", String.class);
UserRole userRole = UserRole.of(claims.get("userRole", String.class));
AuthUser authUser = new AuthUser(userId, email, userRole);
JwtAuthenticationToken authenticationToken = new JwtAuthenticationToken(authUser);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
private final AuthUser authUser;
public JwtAuthenticationToken(AuthUser authUser) {
super(authUser.getAuthorities());
this.authUser = authUser;
setAuthenticated(true);
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return authUser;
}
}
JwtAuthenticationFilter는 기존의 Filter를 대체하고 JwtAuthenticationToken은 기존의 AuthUserArgumentResolver를 대체한다.
FilterConfig는 Spring Security에서 자체적으로 SecurityFilterChain을 사용하여 보안 필터 체인을 관리하기 때문에 필요가 없다.
WebConfig의 경우에는 AuthUserArgumentResolver를 JwtAuthenticationToken이 대체했는데 해당 코드에서
@Override
public Object getPrincipal() {
return authUser;
}
위의 코드가 SecurityContextHolder에서 직접 사용자 정보를 제공하기 때문에 필요가 없다.
'TIL' 카테고리의 다른 글
| 플러스 주차 개인 과제 TIL 7. (0) | 2025.03.18 |
|---|---|
| 플러스 주차 개인 과제 TIL 6. (0) | 2025.03.17 |
| 플러스 주차 개인과제 TIL 4. (0) | 2025.03.13 |
| 플러스 주차 개인과제 TIL 3. (0) | 2025.03.12 |
| 플러스 주차 개인과제 TIL 2. (0) | 2025.03.11 |