본문 바로가기

TIL(Today I Learned)

TIL-230815(항해99 실전 프로젝트-행동대장(11))

728x90

📝오늘 공부한 것

  • 실전 프로젝트 - '행동대장' 로그인 api 수정
  • 실전 프로젝트 - '행동대장' Spring Common Response(Response 공통화)

 

⛔문제점

지금까지 로그인 기능을 구현할 때 Spring Security의 filter인 UsernamePasswordAuthenticationFilter를 상속받아 JwtAuthenticationFilter를 만들어서 사용하였다.

 

 JwtAuthenticationFilter

public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private final JwtUtil jwtUtil;

    public JwtAuthenticationFilter(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
        setFilterProcessesUrl("/api/user/login");
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);

            return getAuthenticationManager().authenticate(
                    new UsernamePasswordAuthenticationToken(
                            requestDto.getUsername(),
                            requestDto.getPassword(),
                            null
                    )
            );
        } catch (IOException e) {
            log.error(e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }


    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException{
        String nickname = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
        UserRoleEnum role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getRole();

        String token = jwtUtil.createToken(nickname, role);
        response.addHeader(JwtUtil.AUTHORIZATION_HEADER, token);
        response.setContentType("text/plain;charset=UTF-8");
        response.getWriter().write("로그인에 성공하였습니다.");
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed)  throws IOException {
        response.setStatus(401);
        response.setContentType("text/plain;charset=UTF-8");
        response.getWriter().write("로그인에 실패하였습니다.");
    }

}

JwtAuthenticationFilter는 user의 로그인 요청을 처리하고, 로그인이 성공하면 JWT 토큰을 생성하여 헤더로 보내고, 실패하면 상태코드와 실패메시지를 보낸다.

 

이렇게 되면 비밀번호가 틀려서 로그인에 실패했는지, 가입되지 않은 계정이어서 로그인에 실패했는지 알수없다. 그래서 프론트쪽에서 어떤 이유인지 알면 처리하기 편할 것 같다고 하셔서 로직을 따로 구현을 했다.

filter에서 로그인을 처리하기 때문에 controller의 url과 맞는 메서드가 호출되지 않는다. 따라서 service에 로직을 구현하더라도 filter에 끝나기 때문에 내가 만든 로직을 타지 않는다.

 

💯해결

✔ UserService

    public LoginResponseDto login(LoginRequestDto requestDto){
        User user = userRepository.findByEmail(requestDto.getEmail()).orElseThrow(() ->
                new LoginException(ClientErrorCode.NO_ACCOUNT));
        if(!passwordEncoder.matches(requestDto.getPassword(), user.getPassword())){
            throw new LoginException(ClientErrorCode.INVALID_PASSWORDS);
        }
        String accessToken = jwtUtil.createToken(user.getEmail(), user.getRole());
        TokenDto tokenDto = new TokenDto(accessToken);
        return new LoginResponseDto(tokenDto.getAccessToken());
    }

filter를 삭제하고 service에서 비밀번호가 일치하지 않을 때, 계정이 존재하지 않을 때에 대한 처리를 하였다.

 

개선할 점💪🏻

Spring Security에서는 보안상의 이유로 로그인 실패 원인을 클라이언트에게 자세하게 노출시키는 것을 권장하지 않는다고 한다. 해커들이 시스템에 대한 정보를 더 많이 수집할 수 있는 기회를 제공할 수 있기때문이다. 그래서 로그인 실패와 관련된 상세한 로깅을 서버측에서 수행하고, 클라이언트에게는 단순히 "로그인에 실패하였습니다" 같은 일반적인 메시지만 반환하는 것이 일반적인 방법이라고 한다.

지금은 그냥 access token만 보낸다. refresh token을 사용하면 access token이 탈취당하더라도 피해가 최소화 된다고 한다. 보안을 위해 refresh token을 사용하는 방법에 대해서 공부해보고 적용해봐야 겠다. 

728x90