프로젝트

스프링 부트 프로젝트(로그인)

이제하네 2024. 6. 3. 22:56

Controller 부분

  @PostMapping("/member/login")
    public ResponseEntity<? super ResponseTokenDto> login(@RequestBody Map<String, String> paramMap) {
            String email = paramMap.get("email");
            String password = paramMap.get("password");
            ResponseEntity<? super ResponseTokenDto> response = memberService.login(email,password, passwordEncoder);

            return response;
    }

 

현재는 Map으로 받아오지만 나중에 리팩토링으로 별도 DTO를 만들어서 가져올 예정 

DTO 부분

@Getter
public class ResponseTokenDto extends ResponseDto{

    private String token;

     private ResponseTokenDto (String token) {
        super();
        this.token = token;
    }

    public static ResponseEntity<ResponseTokenDto> success (String token){
        ResponseTokenDto responseBody = new ResponseTokenDto(token);

        return ResponseEntity.status(HttpStatus.OK).body(responseBody);
    }

    public static ResponseEntity<ResponseDto> signInFail () {
        ResponseDto responseBody = new ResponseDto(ResponseCode.SIGN_IN_FAIL , ResponseMessage.SIGN_IN_FAIL);
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(responseBody) ;
    }
}
  • super()를 사용한 이유
    1. 부모 클래스의 초기화 : 부모 클래스의 정의된 변수나 메서드가 자식 클래스에서 사용될수 있기 때문에 부모클래스의 초기화가 필요
    2. 코드의 재사용: 부모 클래스에서 이미 정의된 초기화 로직을 재사용 가능
    3. 구조적인 일관성: 모든 클래스는 Object 클래스를 상속받으며, 이는 Java의 최상위 클래스 , super()를 통해 부모 클래스의 생성자를 호출하는 것은 클래스 상속 구조의 일관성을 유지


Service 부분

ResponseEntity<? super ResponseTokenDto> login(String email, String password , PasswordEncoder passwordEncoder);

비밀번호를 암호화해서 저장했기 떄문에 PasswordEncoder도 같이 전달

ServiceImpl 부분

  @Override
    public ResponseEntity<? super ResponseTokenDto> login(String email, String password , PasswordEncoder passwordEncoder) {

        String token = null;

        try{
            if(email == null ||email.isEmpty()){
                return ResponseDto.validationFail();
            }

            if(password == null ||password.isEmpty()){
                return ResponseDto.validationFail();
            }

            Member findMember = memberRepository.findByEmail(email);

            if(findMember == null){
                return ResponseTokenDto.signInFail();
            }
            if (!passwordEncoder.matches(password, findMember.getPassword())) {
                return  ResponseTokenDto.signInFail();
            }

            token = jwtProvider.create(findMember.getEmail());

        } catch (Exception exception){
            exception.printStackTrace();
            return ResponseDto.databaseError();
        }
        return ResponseTokenDto.success(token);
    }

 

JwtProvider 부분

@Component
public class JwtProvider {

    @Value("${secret-key}")
    private String secretKey;

    public String create(String email) {

        Date expiredDate = Date.from(Instant.now().plus(1,ChronoUnit.HOURS));
        Key key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));

        String jwt = Jwts.builder()
                .signWith(key, SignatureAlgorithm.HS256)
                .setSubject(email)
                .setIssuedAt(new Date()).setExpiration(expiredDate)
                .compact();

        return jwt;
    }

    public String validate(String jwt) {
        String subject = null;
        Key key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));

        try{
             subject = Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(jwt)
                    .getBody()
                     .getSubject();
        }catch (Exception exception){
            exception.printStackTrace();
            return null;
        }

        return subject;
    }
}

@Value 는 어플 application.properties 또는 application.yml 파일에 있는 r값을 읽고 적용된 필드에 주입
JWT 토큰에는 민감한 정보를 저장하면 안되기에 지금은 간단히 이메일로만 체크하도록 설정

 

JwtAuthenticationFilter 부분

@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtProvider jwtProvider;
    private final MemberRepository memberRepository;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        try {
		// 토큰 추출
            String token = parseBearerToken(request);

            if( token == null){
                filterChain.doFilter(request,response);
                return;
            }
		// 토큰 검증
            String email = jwtProvider.validate(token);
            if(email == null){
                filterChain.doFilter(request,response);
                return;
            }
		//사용자 조회 및 권한 설정
            Member member = memberRepository.findByEmail(email);
            String role = member.getRole();

            List<GrantedAuthority> authorities = new ArrayList<>();
            authorities.add(new SimpleGrantedAuthority(role));
			
        //인증 객체 생성 및설정
            SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
            AbstractAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(member.getEmail() , /*이부분에 패스워드 넣어도됨*/null ,authorities );
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

            securityContext.setAuthentication(authenticationToken);
            SecurityContextHolder.setContext(securityContext);
		//예외처리
        } catch (Exception exception){
            exception.printStackTrace();
        }
		//다음 필터로 전달
        filterChain.doFilter(request,response);
    }

    private String parseBearerToken(HttpServletRequest request){

        String authorization = request.getHeader("Authorization");

        boolean hasAuthorization = StringUtils.hasText(authorization);
        if(!hasAuthorization){
            return null;
        }

        boolean isBearer = authorization.startsWith("Bearer ");
        if(!isBearer){
            return null;
        }

        String token = authorization.substring(7);

        return token;

    }
}
  • doFilterInternal가 작동하는 순서
    1. 토큰 추출 : parseBearerToken 메서드를 호출하여 요청 헤더에서 "Authorization" 헤더를 읽고, "Bearer "로 시작하는 토큰을 추출합니다. 토큰이 없거나 형식이 잘못된 경우, 필터 체인의 다음 필터로 요청을 전달합니다.
    2. 토큰 검증: jwtProvider.validate(token)를 호출하여 토큰을 검증하고, 유효한 경우 이메일 주소를 반환받습니다. 이메일이 없으면 다음 필터로 요청을 전달합니다
    3. 사용자 조회 및 권한 설정: memberRepository.findByEmail(email)을 호출하여 사용자 정보를 조회합니다. 사용자의 역할(role)을 가져와서 GrantedAuthority 리스트를 만듭니다.
    4. 인증 객체 생성 및 설정: UsernamePasswordAuthenticationToken을 사용하여 인증 객체를 생성하고, 요청의 세부 정보를 설정한 후, SecurityContextHolder에 인증 객체를 저장합니다.
    5. 예외처리: 예외가 발생하면 스택 트레이스를 출력하고, 필터 체인의 다음 필터로 요청을 전달합니다.
    6. 다음 필터로 전달: 모든 작업이 끝난 후 filterChain.doFilter(request, response)를 호출하여 요청을 체인의 다음 필터로 전달합니다. 이 호출이 필수적인 이유는 필터 체인이 중단되지 않고 끝까지 실행되도록 보장하기 위해서입니다

 

 

로그인에 성공할시 토큰과 함께 성공 메시지 보냄
이메일이 틀렸을때
비밀번호를 틀렸을때

이메일과 비밀번호를 틀렸을때 무엇을 알려주면 좋을수 있으나 보안상의 이유로 로그인이 잘못된다는 정보만 출력

 

전달되는 파라미터가 잘못되면 유효성 에러 발생

 

'프로젝트' 카테고리의 다른 글

스프링 부트 프로젝트(소셜로그인)  (0) 2024.06.16
스프링 부트 프로젝트 (회원 가입)  (0) 2024.05.29