SecurityContextHolder.getContext().getAuthentication()에서 null이 발생하는 문제

김가빈·2023년 12월 12일
0

springsecurity

목록 보기
18/23

springsecurity filterchain에서 유저 정보를 검사하는 로직을 작성하던 중 SecurityContext에서 authentication 객체를 들고와서 검사해야하는 일이 있었다.

이 과정에서 SecurityContextHolder.getContext().getAuthentication(); 코드에서 계속 null이 떴다.

public UserDetails getUserInfoInSecurityContext() {
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		UserDetails principal = (UserDetails) authentication.getPrincipal();
		return principal;
	}

원인은 securityContext를 설정하는 부분에서 UserDetails객체가 아니라 UserDTO객체로 UsernamePasswordAuthenticationToken을 생성했기 때문이었다. 그 부분을 이렇게 바꾸어주었다.

private void saveUserInSecurityContext(String socialId, String socialProvider) {
		UserDetails userDetails = loadUserBySocialIdAndSocialProvider(socialId, socialProvider);
		Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
		UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
		
		if(authentication != null) {
			SecurityContext context = SecurityContextHolder.createEmptyContext();
			context.setAuthentication(authentication);
			SecurityContextHolder.setContext(context);
		}
	}

이렇게 해서 계속 null이 떴다.

알고보니 securityContext는 default설정이 단일 쓰레드 내에서만 유효하다고 했다.

방법은

  1. SecurityContext를 다른 쓰레드들도 공유하도록하는 것. 이 방법은 안전하지 못하다.
  2. 각 요청시 마다 유저 정보를 db에서 불러와 SecurityContext 객체를 생성하는 것

따라서 2번 방법에 따라서 Spring Security를 도는 customFilter에 요청 시 마다 SecurityContext객체를 생성하도록 코드를 고쳐주었다.

@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		
		if(checkAccessTokenValid(request)) {
			filterChain.doFilter(request, response);
		} else {
			throw new TokenException(TokenErrorResult.TOKEN_NEED);
		}
	}



	private boolean checkAccessTokenValid(HttpServletRequest request) {
		String accessToken = jwtUtil.extractTokenFromHeader(request);
		securityService.saveUserInSecurityContext(accessToken);
		if(jwtUtil.validateAccessToken(accessToken)) {
			return true;			
		}
		
		return false;
	}
@Service
public class SecurityServiceImpl implements SecurityService{
	
	
	private final UserRepository userRepository;
	private final JwtUtil jwtUtil;
	
	@Autowired
	private SecurityServiceImpl(UserRepository userRepository, JwtUtil jwtUtil) {
		this.userRepository = userRepository;
		this.jwtUtil = jwtUtil;
	}
    
    
	public void saveUserInSecurityContext(String accessToken) {
		if(accessToken == null) {
			throw new TokenException(TokenErrorResult.TOKEN_NEED);
		}
		
		String socialId = jwtUtil.extractClaim(accessToken,  Claims::getSubject);
		String socialProvider = jwtUtil.extractClaim(accessToken, Claims::getIssuer);
		saveUserInSecurityContext(socialId, socialProvider);
	}
    
   private void saveUserInSecurityContext(String socialId, String socialProvider) {
		UserDetails userDetails = loadUserBySocialIdAndSocialProvider(socialId, socialProvider);
		Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
		UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
		
		if(authentication != null) {
			SecurityContext context = SecurityContextHolder.createEmptyContext();
			context.setAuthentication(authentication);
			SecurityContextHolder.setContext(context);
		}
	}

	private UserDetails loadUserBySocialIdAndSocialProvider(String socialId, String socialProvider) {
		User user = userRepository.findBySocialIdAndSocialProvider(socialId, socialProvider);
		
		if(user == null) {
			throw new TokenException(TokenErrorResult.TOKEN_EXPIRED);
		} else {
			UserDetailsImpl userDetails = new UserDetailsImpl();
			userDetails.setUser(user);
			return userDetails;
		}
		
	}

이렇게했더니 에러가 해결되었다.

사실 전에는 jwt토큰을 구현할 때에는 매 요청시마다 db에서 정보를 가지고와서 비교를 한다는 정보를 머리로는 알고 있었는데, 이렇게 직접 구현을 해보니 새로웠다.

jwt가 훼손되는 것을 방지할 뿐만 아니라 각 요청에 대해서 안정적인 보안을 구현하기 위해서 SecurityContext가 단일 쓰레드에만 적용된다는 새로운 사실을 알아서 좋았다.

profile
신입 웹개발자입니다.

0개의 댓글