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설정이 단일 쓰레드 내에서만 유효하다고 했다.
방법은
따라서 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가 단일 쓰레드에만 적용된다는 새로운 사실을 알아서 좋았다.