Spring Security
- 스프링 기반 애플리케이션에 인증과 인가 기능을 제공하는 보안 프레임워크
- 2003년에 나왔음 → 세션 방식으로 이루어져 있음 (JWT가 없던 시기)
Spring Security에서 인증을 통과하는 방식
- SecurityContext : 현재 로그인한 사용자의 정보를 저장하는 객체
- SecurityContext에 AbstractAuthenticationToken을 set해야 한다
- 로그인을 하면 → SecurityContext 내부에 Authentication 객체가 저장
- 필요할 때 SecurityContext에서 현재 로그인한 사용자의 정보를 꺼낼 수 있음
- SecurityContextHolder를 통해 SecurityContext에 접근 가능
- 로그아웃하면 SecurityContext가 초기화
SecurityContextHolder

- SecurityContext를 저장하고 관리하는 저장소
📌 "할 일 등록 API" 요청 (/todos)
[ 새로운 SecurityContextHolder 생성 ]
-> 인증 정보 저장 (SecurityContext에 저장됨), 이 SecurityContext를 SecurityContextHolder에 저장
-> 할 일 등록 진행
-> 요청 종료 후 SecurityContext 초기화
이 과정은 각각의 스레드별로 독립적으로 진행됨!
Spring Security 문제점 : 세션 기반으로 사용하려는 것
Spring Security → 2003년에 나옴 → 세션 방식
JWT → 2015년 표준화됨 → 서버는 stateless
즉 JWT는 stateless 하지만 Security는 stateless하지 않다!
UserDetailsServiceImpl의 문제
- user 의 정보를 알기 위해서 UserRepository를 통해 디비에 다녀옴 → 세션이랑 다를 게 없음 → 애초에 jwt에 사용자 id, 권한 등등을 넣어 놓는데 이렇게 되면 jwt는 세션키의 역할만 될 뿐! (차라리 세션키 쓰는게 훨 낫다 덜 뚱뚱하기 때문이다)
- 또한 데이터베이스를 나눠놓는 아키텍쳐인 MSA에도 적용하지 못함 (만약 user db가 죽는다면 서비스는 아예 죽게 됨)
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
public UserDetailsServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));
return new UserDetailsImpl(user);
}
}
UserDetailsServiceImpl Stateless 적용
사용자의 정보를 userRepository가 아닌, 이미 정보를 가지고 있는 JWT 토큰에서 까서 가져오면 된다
- 헤더에서 JWT 토큰을 가져오고
- JWT를 해석(파싱)하여 토큰 내부의 데이터(payload)를 반환한 Claims객체를 반환한다
- Claims객체를 이용하여 JwtAuthenticationToken을 생성하고 SecurityContext에 set 해준다
@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) {
...
}
}
chain.doFilter(httpRequest, httpResponse);
}
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));
String nickName = claims.get("nickName", String.class);
AuthUser authUser = new AuthUser(userId, email, userRole, nickName);
JwtAuthenticationToken authenticationToken = new JwtAuthenticationToken(authUser);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
위와 같이 적용하면 더 이상 UserDetailService를 사용하지 않아도 된다
또한! 사용자를 조회할 때 respository를 사용하지 않아도 된다
기존 코드
사용자를 조회하기 위해서 userId로 데이터베이스에 접근해서 사용자를 찾는다

하지만 더 이상 데이터베이스에 접근해서 사용자를 찾을 필요가 없다
JWT 활용 코드
db를 거치는 것이 아닌 jwt를 이용하여 얻은 객체인 AuthUser를 이용하여 사용자객체를 생성하면 된다

fromAuthUser

사용자의 권한을 변경했다면 데이터베이스에서 사용자의 정보를 대조해야 한다
만약 사용자의 권한을 admin에서 user로 바꿨다고 한다면, 권한이 바뀐 사용자는 아직 발급받은 jwt에서 admin으로 되어 있을 것이다
그렇기 때문에 이런 경우에는 컨트롤러에서 사용자의 권한을 한번 더 체크하는 로직을 짜줘야 한다 (필터는 통과했을 것이니)

'Spring' 카테고리의 다른 글
테스트 코드 작성하기 (1) | 2025.03.23 |
---|---|
JPA와 Transaction (1) | 2025.03.21 |
User 데이터 100만건 생성하고 성능 개선하기 (1) | 2025.03.21 |
intellij 프로젝트 내부에 작업물이 안 보일 때 해결방법 (0) | 2025.03.12 |
양방향 연관관계와 영속성 전이 (Cascade) / Fetch / 페이징 정렬 / Dynamic Insert,Update (0) | 2025.03.12 |