이지은님의 블로그
250314 - Java Spring Security: Spring Security의 특징과 구조, JWT를 이용한 Spring Security구현(SecurityContext, AbstractAuthenticationToken) 본문
250314 - Java Spring Security: Spring Security의 특징과 구조, JWT를 이용한 Spring Security구현(SecurityContext, AbstractAuthenticationToken)
queenriwon3 2025. 3. 14. 16:57▷ 오늘 배운 것
세션으로 배운 Spring Security와 JWT를 활용한 간단한 Spring Security에 대해 작성해보려고 한다.
<<목차>>
1. Spring Security
1) Spring Security란?
2) Spring Security의 구조
3) Spring Security 필터 흐름도
4) FilterChainProxy와 SecurityFilterChain의 관계
5) Spring Security 인증 과정 흐름도
2. Stateless Spring Security
1) Spring Security + JWT
2) JWT 인증 및 인가 과정
3. Stateless Spring Security 분석
1) JwtAuthenticationFilter 클래스
2) JwtAuthenticationToken 클래스
3) SecurityConfig 클래스
4) UserRole
5) AuthUser
6) Controller에서의 사용
7) 인증객체가 없을 때 발생하는 에러
1. Spring Security
1) Spring Security란?
스프링 시큐리티(Spring Security)는 스프링 기반 애플리케이션에 강력한 인증(Authentication)과 인가(Authorization) 기능을 제공하는 보안 프레임워크
- 서블릿 API 통합
- Spring Web MVC와의 선택적 통합
- 인증과 권한 부여를 확장 가능하게 지원
- 세션 고정, CSRF(사이트 간 요청 위조) , ClickJacking 등을 보호
라는 특징이 있다.
👉 로그인 인증 방법
Security 보안을 통과하기 위해 SecurityContext에 AbstractAuthenticationToken을 set 하는 방법으로 로그인인증을 확인한다.
2) Spring Security의 구조
1️⃣ Security Filter Chain: 요청이 들어오면 여러 개의 필터를 거쳐 보안 검사를 수행함
2️⃣ UserDetailsService: 사용자 정보를 로드하는 인터페이스
3️⃣ AuthenticationManager: 인증을 처리하는 핵심 컴포넌트
4️⃣ PasswordEncoder: 비밀번호를 암호화 및 비교하는 기능 제공
1️⃣ Security Filter Chain
서블릿 필터 체인(Servlet Filter Chain) 흐름
→ Spring Security의 필터 체인(Filter Chain) 동작
- 사용자의 Request(요청)이 들어오면 여러 개의 Servlet Filter(FilterChain)을 생성한다.
- 각 필터는 doFilter() 메서드를 호출하며 체인을 따라 다음 필터로 요청을 전달
- 최종적으로 모든 필터를 거친 후 servlet이 실행된다.(모든 필터를 통과하면 DispatcherServlet이 컨트롤러를 호출하여 응답을 생성)
➡️ 필터의 역할: JWT 검증, 인증(Authentication), 권한 확인(Authorization) 등 요청을 차단하거나 응답을 수정할 수도 있음
인증단계에서 에러를 발생시키면 다음과 같이 여러개의 필터들이 연달아 진행된다는 것을 확인할 수 있다.
💡 Servlet 컨테이너와 Spring의 차이점
- 서블릿 컨테이너(톰캣 등)는 필터(Filter)를 자체적으로 등록하는 방식이 있음
- 하지만 서블릿 컨테이너는 Spring의 Bean 을 모름 → 직접 Spring의 Bean을 등록할 수 없음
3) Spring Security 필터 흐름도
1️⃣ DelegatingFilterProxy
: 실제 필터 체인의 시작점, Spring Security의 여러 보안 필터를 실행(FilterChainProxy 위임)
: Spring에서 제공하는 DelegatingFilterProxy는 “ 서블릿 컨테이너 “와 Spring의 “ ApplicationContext “를 연결하는 다리 역할을 함. 서블릿 컨테이너에 DelegatingFilterProxy를 등록하면, 이 필터가 Spring Bean으로 등록된 필터를 대신 실행해준다.
DelegatingFilterProxy는 먼저 Spring의 “ApplicationContext”에서 특정 필터 Bean을 찾아서 실행함.
DelegatingFilterProxy가 Filter0이라는 Spring Bean을 찾고 실행
➡️ DelegatingFilterProxy는 "서블릿 컨테이너가 Spring의 필터를 모르는 문제" 를 해결하는 "중간 연결자(Proxy)"
2️⃣ FilterChainProxy
: Spring Security의 중심 필터 체인(핵심 필터)
사용자의 요청이 들어오면, 설정된 WebSecurity 설정을 기반으로 필터 체인을 구성하여 실행
FilterChainProxy가 여러 개의 SecurityFilterChain 안에 있는 다양한 필터들을 실행
FilterChainProxy는 Spring Bean으로 등록되어 있으나 Servlet이 이를 알지 못하기 때문에 DeletatingFilterProxy가 FilterChainProxy를 감싸서 호출한다.
➡️ DelegatingFilterProxy → FilterChainProxy → 여러 개의 Security 필터 가 실행
이러한 방식은 다양한 보인기능을 FilterChainProxy가 관리하여 필요한 필터만 실행할 수 있도록하기 위함
3️⃣ 인증관련 - UsernamePasswordAuthenticationFilter
: 사용자의 아이디와 비밀번호를 검증하는 필터
4️⃣ 인증관련 - JwtAuthenticationFilter(jwt 사용시)
: JWT 기반 인증을 처리하는 커스텀 필터
5️⃣ 인증관련 - SecurityContextPersistenceFilter
: 보안 컨텍스트(SecurityContext)를 유지하는 역할
6️⃣ 인가관련 - SessionManagementFilter
: 세션을 관리하며, JWT를 사용하는 경우 STATELESS로 설정
7️⃣ 인가관련 - FilterSecurityInterceptor
: 최종적으로 접근 권한을 체크하고 요청을 허용할지 결정하는 필터
➡️ 요청이 들어오면 DelegatingFilterProxy → FilterChainProxy가 이를 관리 -> 필터를 거치며 인증, 인가 수행
4) FilterChainProxy와 SecurityFilterChain의 관계
FilterChainProxy는 요청이 들어올 때 실행할 보안 필터를 선택하는 관리자인 반면,
SecurityFilterChain는 url패턴에 따라 보안 필터들을 결정하는 역할
각 SecurityFilterChain은 서로 다른 필터 구성을 가질 수 있다.
그래서 url에 따라 어떤것은 보안 필터를 3개만 실행할 수도, 4개만 실행할 수도 또는 아예 적용하지 않도록 설정할 수 있다.(유연성)
FilterChainProxy는 SecurityContext를 자동으로 정리해 메모리 누수를 방지한다.
HttpFirewall을 사용하여 특정 보안 공격을 방어할 수 있다.
일반적인 서블릿 컨테이너는 URL 패턴만 보고 필터를 실행하지만, FilterChainProxy는 요청의 다양한 정보를 기반으로 실행할 보안 필터를 결정할 수 있음.(RequestMatcher 사용)
💡 Multiple SecurityFilterChain이란?
- Spring Security에서는 여러 개의 SecurityFilterChain을 설정할 수 있음.
- Spring Security에서는 하나의 보안 설정을 적용
= 특정 URL 패턴마다 다른 보안 설정을 적용해야 할 때 여러 개의 SecurityFilterChain을 사용
5) Spring Security 인증 과정 흐름도
1️⃣ 사용자의 HTTP 요청 (Http Request)
: Spring Security의 Security Filter Chain이 이를 가로챔
2️⃣ AuthenticationFilter (인증 필터)
: 인증 정보를 확인하는 역할(JWT 기반 -> 토큰 검증)
-> 인증정보가 있을 경우 AuthenticationManager로 넘김
3️⃣ AuthenticationManager (인증 관리자, 인증처리)
: 실제 인증을 처리하는 역할을 담당
-> 여러 개의 AuthenticationProvider 중 적절한 것을 찾아서 인증을 위임
4️⃣ AuthenticationProvider (인증 제공자)
: UserDetailsService를 사용하여 데이터베이스에서 사용자 정보를 가져오고, 비밀번호를 검증(PasswordEncoder)
-> 인증이 성공하면, 인증된 사용자 정보를 반환
5️⃣ UserDetailsService (사용자 정보 로드, 데이터베이스)
: 데이터베이스에서 사용자 정보를 조회하고, UserDetails 객체로 반환. 일반적으로 loadUserByUsername(String username) 메서드를 통해 사용자 정보를 가져옴
6️⃣ SecurityContextHolder (보안 컨텍스트 저장)
: 인증이 성공하면, SecurityContextHolder에 인증 정보를 저장
-> 요청에서 인증이 필요할 때마다 여기에서 인증 정보를 가져옴
=> FilterChain을 거쳐 컨트롤러(Spring Controller)로 요청 전달
@AuthenticationPrincipal 어노테이션을 사용
- JWT 기반 인증을 사용할 경우, AuthenticationFilter에서 JWT 검증을 수행하고, 인증이 완료되면 SecurityContext에 저장하여 이후 인증 요청 시 활용.
2. Stateless Spring Security
1) Spring Security + JWT
Spring Security는 기본적으로 세션 기반인증을 사용하지만, JWT를 활용하면 세션 없이도 stateless인증을 할 수 있다.
JWT를 활용하면 복잡한 세션 인증과정을 생략할 수 있으며,
Security Filter Chain에서 인증 필터를 추가하여 요청마다 토큰을 검증하고, SecurityContextHolder에 인증정보를 저장한다.
2) JWT 인증 및 인가 과정
1️⃣ 사용자가 로그인 요청 (/api/auth/login)
- 사용자가 아이디와 비밀번호를 입력하여 로그인 요청을 보냄
- AuthenticationManager가 UserDetailsService를 통해 사용자 정보를 조회하고 인증 수행
- 인증이 성공하면 JWT Access Token과 Refresh Token을 생성하여 반환
2️⃣ 클라이언트가 Access Token을 포함하여 API 요청
- 클라이언트는 Authorization: Bearer <Access Token> 헤더를 포함하여 요청을 보냄
- 요청이 Security Filter Chain을 거치면서 JwtAuthenticationFilter에서 JWT 검증 수행
3️⃣ JWT 검증 및 SecurityContextHolder에 저장
- JwtAuthenticationFilter에서 JWT의 서명을 검증하고, 토큰이 유효하면 SecurityContextHolder에 사용자 인증 정보 저장
- 이후의 모든 요청은 SecurityContext에서 인증 정보를 가져와 처리됨
4️⃣ 인가(Authorization) 처리
- @PreAuthorize, @Secured, @RolesAllowed 등의 어노테이션을 활용하여 사용자의 권한을 확인하고 접근 제어 수행
- 예: @PreAuthorize("hasRole('ADMIN’)”), @Secured(UserRole.Authority.ADMIN)
5️⃣ Access Token 만료 시 Refresh Token을 이용한 재발급
- 클라이언트가 Access Token이 만료되었을 경우, 저장된 Refresh Token을 이용하여 새로운 Access Token 요청 (/api/auth/refresh)
- 서버는 Refresh Token을 검증하고 새로운 Access Token을 생성하여 반환
3. Stateless Spring Security 분석
1) JwtAuthenticationFilter 클래스
: 기존 로그인 필터를 대체하는 필터
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(
HttpServletRequest httpRequest,
@NonNull HttpServletResponse httpResponse,
@NonNull FilterChain chain
) throws ServletException, IOException {
String authorizationHeader = httpRequest.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);
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "유효하지 않는 JWT 서명입니다.");
} catch (ExpiredJwtException e) {
log.error("Expired JWT token, 만료된 JWT token 입니다.", e);
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "만료된 JWT 토큰입니다.");
} catch (UnsupportedJwtException e) {
log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.", e);
httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "지원되지 않는 JWT 토큰입니다.");
} catch (Exception e) {
log.error("Internal server error", e);
httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
chain.doFilter(httpRequest, httpResponse);
}
private void setAuthentication(Claims claims) {
Long userId = Long.valueOf(claims.getSubject());
String email = claims.get("email", String.class);
String nickname = claims.get("nickname", String.class);
UserRole userRole = UserRole.of(claims.get("userRole", String.class));
AuthUser authUser = new AuthUser(userId, email, nickname, userRole);
JwtAuthenticationToken authenticationToken = new JwtAuthenticationToken(authUser);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
코드를 하나씩 뜯어보자면
1️⃣ Filter는 요청당 한번씩 사용될 수 있도록 OncePerRequestFilter을 상속받아 doFilterInternal()메서드를 사용한다.
@Override
protected void doFilterInternal(HttpServletRequest httpRequest, @NonNull HttpServletResponse httpResponse, @NonNull FilterChain chain) throws ServletException, IOException {
String authorizationHeader = httpRequest.getHeader("Authorization");
// 헤더에 토큰이 있음 + 그리고 그 토큰이 Bearer 토큰임
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
// 토큰의 값을 가져옴
String jwt = jwtUtil.substringToken(authorizationHeader);
try {
// 토큰의 사용자 정보를 가져옴
Claims claims = jwtUtil.extractClaims(jwt);
// SecurityContextHolder 에 아무 인증체도 없으면
if (SecurityContextHolder.getContext().getAuthentication() == null) {
setAuthentication(claims);
}
} ... catch (Exception e) {
log.error("Internal server error", e);
httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
chain.doFilter(httpRequest, httpResponse);
}
2️⃣ doFilterInternal() 메서드에서는 헤더의 토큰을 가져온뒤 토큰에서 가져온 사용자 정보 값이 유효한지 판단해서 SecurityContextHolder에 저장하는 기능을 담당한다.
이 과정에서 SecurityContextHolder에 유효하지 않은 값이 저장될 경우 예외를 반환 결국 doFilter에서 Forbidden 예외코드를 출력시킨다.
private void setAuthentication(Claims claims) {
// 사용자 정보 값 추출
Long userId = Long.valueOf(claims.getSubject());
String email = claims.get("email", String.class);
String nickname = claims.get("nickname", String.class);
UserRole userRole = UserRole.of(claims.get("userRole", String.class));
AuthUser authUser = new AuthUser(userId, email, nickname, userRole);
// 인증 토큰을 생성 및 등록
JwtAuthenticationToken authenticationToken = new JwtAuthenticationToken(authUser);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
3️⃣ setAuthentication()에서는 사용자 정보값을 추출하고 이를 통해 인증 토큰을 생성한다.
이렇게 생성한 인증토큰은 SecurityContext에 저장된다.
Security 보안을 통과하려면 SecurityContext에 AbstractAuthenticationToken을 set해주어야한다.
SecurityContext는 보통 하나의 `Authentication` 객체를 포함하며,
이 객체는 사용자(Principal), 인증 방식, 인증 상태, 그리고 사용자가 가진 권한 목록(GrantedAuthorities) 등을 포함한다.
한 요청 내에서 현재 인증된 사용자가 누구인지, 그리고 어떤 권한을 가지고 있는지 알 수가 있다.
만약 set되어있지 않다면 바로 없는채로 chain.doFiltre() -> forbidden 에러 발생
2) JwtAuthenticationToken 클래스
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;
}
}
JwtAuthenticationToken 클래스는 인증 토큰을 생성하는 클래스이며 사용했던 커스텀 ArgumentResolver를 대체할 수 있다.
이 클래스를 통해 @AuthenticationPrincipal를 통한 @AuthenticationPrincipal AuthUser authUser를 사용가능하다.
AbstractAuthenticationToken의 경우 public AbstractAuthenticationToken(Collection<? extends GrantedAuthority> authorities)로, 여러 개의 권한을 설정할 수 있도록 한다.
// AuthUser 등록(사용자 정보 등록)
public JwtAuthenticationToken(AuthUser authUser) {
super(authUser.getAuthorities()); // 부모클래스가 권한을 인식할 수 있음
this.authUser = authUser;
setAuthenticated(true); // 객체가 인증되었다는 사실을 참값으로 설정
}
1️⃣ JwtAuthenticationToken의 생성자를 사용하면서 사용자 정보를 인증 토큰으로 만들 수 있다.
setAuthenticated를 사용하면서 객체가 인증됨을 참값으로 설정할 수 있다.
@Override
public Object getCredentials() {
return null;
}
2️⃣ getCredentials()은 비밀번호 같은 자격 증명을 반환하는 메서드이다.
하지만 JWT 같은 토큰 기반 인증에서는 자격 증명이 필요 없으므로 null을 반환
@Override
public Object getPrincipal() {
return authUser;
}
3️⃣ @AuthenticationPrincipal를 이용해 어떤 인증 객체를 받을 지 설정한다.
@AuthenticationPrincipal AuthUser authUser 를 사용(ArgumentResolver)
3) SecurityConfig 클래스
@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();
}
}
커스텀한 Filter를 설정하는 역할
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfig {
}
1️⃣ @EnableWebSecurity: Spring Security 설정을 활성화하는 어노테이션
Spring Security를 활성화하고 HTTP 보안 설정을 적용하는 역할을 함.
2️⃣ @EnableMethodSecurity(securedEnabled = true)
: 메서드 단위에서 보안(인증 및 권한 검사)을 활성화하는 역할 (true: @Secured 어노테이션을 사용)
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
3️⃣ 스프링 시큐리티에서 제공하는 BCryptPasswordEncoder(), @Configuration 과 @Bean으로 스프링 빈으로 등록되어 의존성 주입으로 사용가능
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
...
}
4️⃣ @EnableWebSecurity를 설정하면서, securityFilterChain을 설정할 수 있다.
보안필터 체인을 정의하는 bean등록 할 수 있다.
HttpSecurity 객체를 설정하여 웹 보안 규칙을 적용함
return http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
5️⃣ .csrf(AbstractHttpConfigurer::disable): CSRF(Cross-Site Request Forgery) 공격 방지를 비활성화
보통 JWT 기반 인증 방식에서는 CSRF 보호가 필요하지 않아서 끄는 경우가 많음
6️⃣.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
: SessionCreationPolicy.STATELESS 설정 → 서버가 세션을 생성하거나 유지하지 않음
JWT 기반 인증에서는 세션이 필요 없으므로, 모든 요청은 HTTP 헤더의 토큰으로 인증해야 함
.addFilterBefore(jwtAuthenticationFilter, SecurityContextHolderAwareRequestFilter.class)
.formLogin(AbstractHttpConfigurer::disable)
.anonymous(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.logout(AbstractHttpConfigurer::disable)
.rememberMe(AbstractHttpConfigurer::disable)
7️⃣ .addFilterBefore(jwtAuthenticationFilter, SecurityContextHolderAwareRequestFilter.class)
: 필터 체인에 추가 (SecurityContextHolderAwareRequestFilter 앞에 jwtAuthenticationFilter가 실행되도록 설정 → JWT 검증 후 보안 컨텍스트(SecurityContext)에 사용자 정보 저장)
8️⃣ .formLogin(AbstractHttpConfigurer::disable)
: SSR이 아니기에 폼 기반 로그인 기능이 필요하지 않으므로 비활성화
-> UsernamePasswordAuthenticationFilter, DefaultLoginPageGeneratingFilter 비활성화
9️⃣ .anonymous(AbstractHttpConfigurer::disable)
: 익명 사용자 권한은 필요 없으므로 비활성화 (인증되지 않은 사용자는 접근 불가)
-> AnonymousAuthenticationFilter 비활성화
🔟 .httpBasic(AbstractHttpConfigurer::disable)
: HTTP Basic(아이디/비번 인증) 사용 X (난이도 상승으로 인한 커스텀 필터 이용)
-> BasicAuthenticationFilter 비활성화
1️⃣1️⃣ .logout(AbstractHttpConfigurer::disable)
: 로그아웃은 세션을 지우는 요청이므로 jwt에는 필요하지 않음
-> LogoutFilter 비활성화
1️⃣2️⃣ .rememberMe(AbstractHttpConfigurer::disable)
: Stateless이기 때문에 Remember를 할 수가 없다.
-> RememberMeAuthenticationFilter 비활성화
.authorizeHttpRequests(auth -> auth
.requestMatchers(request -> request.getRequestURI().startsWith("/auth")).permitAll()
.requestMatchers("/test").hasAuthority(UserRole.Authority.ADMIN)
.requestMatchers("/open").permitAll()
.anyRequest().authenticated()
)
.build();
1️⃣3️⃣ request.getRequestURI().startsWith("/auth")).permitAll()
: url이 "/auth"로 시작한다면, 무조건 통과
1️⃣4️⃣ .requestMatchers("/test").hasAuthority(UserRole.Authority.ADMIN)
: url이 "/test"라면 ADMIN 권한이 있어야함
1️⃣5️⃣ .requestMatchers("/open").permitAll()
: url이 "/open"라면 무조건 통과
1️⃣6️⃣ .anyRequest().authenticated()
: SecurityContext에 AbstractAuthenticationToken 이 set이 되어있다면 통과를 시키겠단 뜻
4) UserRole
@Getter
@RequiredArgsConstructor
public enum UserRole {
ROLE_USER(Authority.USER),
ROLE_ADMIN(Authority.ADMIN);
private final String userRole;
public static UserRole of(String role) {
return Arrays.stream(UserRole.values())
.filter(r -> r.getUserRole().equalsIgnoreCase(role))
.findFirst()
.orElseThrow(() -> new InvalidRequestException("유효하지 않은 UserRole"));
}
public static class Authority {
public static final String USER = "ROLE_USER";
public static final String ADMIN = "ROLE_ADMIN";
}
}
Spring Security의 인가를 사용하게되면 인가emum을 다음과 같이 작성했다.
스프링 시큐리티에서 제공하는 권한 기능을 사용하려면, 반드시 prefix로 "ROLE_”을 붙여야한다.
5) AuthUser
@Getter
public class AuthUser {
private final Long userId;
private final String email;
private final String nickname;
private final Collection<? extends GrantedAuthority> authorities;
public AuthUser(Long userId, String email, String nickname, UserRole role) {
this.userId = userId;
this.email = email;
this.nickname = nickname;
this.authorities = List.of(new SimpleGrantedAuthority(role.name()));
}
}
AuthUser에서 여러개의 권한을 저장하기 위해서 Collection<? extends GrantedAuthority> authorities를 필드로 저장할 수 있다.
6) Controller에서의 사용
@PostMapping("/todos")
public ResponseEntity<TodoSaveResponse> saveTodo(
@AuthenticationPrincipal AuthUser authUser, // @AuthenticationPrincipal 사용
@Valid @RequestBody TodoSaveRequest todoSaveRequest
) {
return ResponseEntity.ok(todoService.saveTodo(authUser, todoSaveRequest));
}
@Secured(UserRole.Authority.ADMIN) // @Secured 사용(인가)
@PatchMapping("/admin/users/{userId}")
public void changeUserRole(
@PathVariable long userId,
@RequestBody UserRoleChangeRequest userRoleChangeRequest
) {
userAdminService.changeUserRole(userId, userRoleChangeRequest);
}
7) 인증객체가 없을 때 발생하는 에러
org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
만약 인증객체가 없을때 다음과 같은 에러가 발생한다. 그럴 경우 Forbidden 403에러 발생으로 응답을 받을 수가 없다.