회원파트

이상민·2025년 5월 22일
0

Spring Boot JWT 로그인 & 인증 정리 노트

전체 구조

회원 인증 기능은 다음과 같은 디렉토리 구조로 구성되어 있습니다.

.
├── controller
│   └── MemberController.java
├── domain
│   ├── Member.java
│   └── Role.java
├── dto
│   ├── MemberListResDto.java
│   ├── MemberLoginReqDto.java
│   └── MemberSaveReqDto.java
├── repository
│   └── MemberRepository.java
└── service
    └── MemberService.java

로그인 흐름 요약

1. 클라이언트 → 서버 : 로그인 요청

async doLogin(){
  const loginData = {
    email : this.email,
    password : this.password
  };
  const response = await axios.post(`${process.env.VUE_APP_API_BASE_URL}/member/login`, loginData);
  const token = response.data.token;
  localStorage.setItem("token", token); // 토큰 저장
  window.location.href = "/";
}
  • 클라이언트는 email, password로 로그인 요청을 보냄
  • 응답으로 받은 JWT 토큰을 localStorage에 저장

2. 서버: 로그인 처리 & 토큰 발급

@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody MemberLoginReqDto dto) {
    try {
        Member member = memberService.login(dto);

        String jwtToken = jwtTokenProvider.createToken(
            member.getEmail(), 
            member.getRole().toString()
        );

        Map<String, Object> loginInfo = new HashMap<>();
        loginInfo.put("id", member.getId());
        loginInfo.put("token", jwtToken);

        return ResponseEntity.ok(loginInfo);

    } catch (AuthenticationException e) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                             .body("이메일 또는 비밀번호가 틀렸습니다.");
    }
}

JWT 토큰 생성

public String createToken(String email, String role){
    Claims claims = Jwts.claims().setSubject(email); // 이메일을 subject로 설정
    claims.put("role", role); // 권한 추가
    claims.put("id", member.getId()); // 사용자 ID 추가

    Date now = new Date();
    return Jwts.builder()
            .setClaims(claims)
            .setIssuedAt(now)
            .setExpiration(new Date(now.getTime() + expiration * 60 * 1000L)) // 만료 시간
            .signWith(SECRET_KEY) // 서명
            .compact();
}
  • subject: 사용자 식별자 (이메일)
  • claims: 추가 정보(권한, ID 등)
  • expiration: 만료시간 (예: 30분)

JWT 토큰 검증 (Authentication Filter)

String token = httpServletRequest.getHeader("Authorization");

try {
    if (StringUtils.hasText(token) && token.startsWith("Bearer ")) {
        String jwtToken = token.substring(7);

        Claims claims = Jwts.parserBuilder()
                            .setSigningKey(secretKey)
                            .build()
                            .parseClaimsJws(jwtToken)
                            .getBody();

        List<GrantedAuthority> authorities = List.of(
            new SimpleGrantedAuthority("ROLE_" + claims.get("role"))
        );

        UserDetails userDetails = new User(claims.getSubject(), "", authorities);
        Authentication auth = new UsernamePasswordAuthenticationToken(
            userDetails, "", userDetails.getAuthorities());

        SecurityContextHolder.getContext().setAuthentication(auth);
    }

    filterChain.doFilter(request, response);

} catch (ExpiredJwtException e) {
    response.setStatus(HttpStatus.UNAUTHORIZED.value());
    response.getWriter().write("만료된 토큰입니다.");
} catch (JwtException | IllegalArgumentException e) {
    response.setStatus(HttpStatus.UNAUTHORIZED.value());
    response.getWriter().write("유효하지 않은 토큰입니다.");
}
  • Bearer prefix 체크
  • 토큰 파싱 및 검증 후 인증 객체 생성
  • 예외 상황 처리 (만료, 위조, 형식 오류 등)

인증된 사용자 정보 접근

인증된 사용자는 @AuthenticationPrincipal로 접근할 수 있습니다.

@GetMapping("/me")
public ResponseEntity<?> myInfo(@AuthenticationPrincipal User user) {
    return ResponseEntity.ok(user.getUsername()); // 이메일 반환
}

보안 및 유의사항

항목설명
HTTPS토큰이 평문으로 노출되지 않도록 HTTPS 사용 필수
토큰 저장소XSS에 취약한 localStorage보다 HttpOnly Cookie 고려 가능
만료 처리클라이언트에서 만료 여부 확인하고 자동 로그아웃 또는 재로그인 처리 필요
Refresh Token장기 세션을 위해 Access + Refresh 토큰 방식 적용 가능

profile
잘하자

0개의 댓글