어제 오늘 내일

[Spring Security] 9편 - JWT 로그인 API 구현 & Postman 테스트 본문

IT/SpringBoot

[Spring Security] 9편 - JWT 로그인 API 구현 & Postman 테스트

hi.anna 2026. 2. 20. 07:20

지난 시간까지 보안 설정(Config)을 바꾸고, 필터(Filter)를 장착했습니다.
하지만 기존의 Form Login을 삭제했기 때문에, 현재는 로그인도 회원가입도 할 수 없는 상태입니다.

이번 마지막 시간에는 사용자가 JSON으로 아이디/비번을 보내면 토큰을 발급해 주는 API 컨트롤러를 만들고,

Postman으로 전체 흐름을 테스트하며 대장정을 마무리하겠습니다.


Step 1. 로그인 요청용 DTO 만들기

로그인할 때 받을 데이터(ID, PW)를 담을 객체입니다.

  • 위치: src/main/java/com/example/board/dto/MemberLoginDto.java
package com.example.board.dto;

import lombok.Data;

@Data
public class MemberLoginDto {
    private String username;
    private String password;
}

Step 2. 로그인 로직 구현 (MemberService)

여기가 "실제 인증"이 일어나는 핵심 로직입니다.
사용자가 입력한 정보가 맞는지 확인하고, 맞다면 토큰 발급기(JwtTokenProvider)를 통해 출입증을 발급합니다.

  • 위치: src/main/java/com/example/board/service/MemberService.java
package com.example.board.service;

import com.example.board.domain.entity.Member;
import com.example.board.domain.repository.MemberRepository;
import com.example.board.dto.MemberJoinDto;
import com.example.board.dto.MemberLoginDto;
import com.example.board.jwt.JwtToken;
import com.example.board.jwt.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MemberService {

    private final MemberRepository memberRepository;
    private final AuthenticationManagerBuilder authenticationManagerBuilder;
    private final JwtTokenProvider jwtTokenProvider;
    private final PasswordEncoder passwordEncoder;

    @Transactional
    public void join(MemberJoinDto memberJoinDto) {
        if (memberRepository.findByUsername(memberJoinDto.getUsername()).isPresent()) {
            throw new IllegalStateException("이미 존재하는 아이디입니다.");
        }

        Member member = memberRepository.save(memberJoinDto.toEntity());
        member.encodePassword(passwordEncoder);
    }

    // ★ 실제 로그인 처리 로직
    @Transactional
    public JwtToken login(String username, String password) {
        // 1. Login ID/PW 를 기반으로 Authentication 객체 생성
        // 이때 authentication 은 인증 여부를 확인하는 authenticated 값이 false
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);

        // 2. 실제 검증 (사용자 비밀번호 체크)이 이루어지는 부분
        // authenticate 매서드가 실행될 때 CustomUserDetailsService 에서 만든 loadUserByUsername 메서드가 실행
        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);

        // 3. 인증 정보를 기반으로 JWT 토큰 생성
        JwtToken jwtToken = jwtTokenProvider.createToken(authentication);

        return jwtToken;
    }
}

Step 3. 로그인 API 컨트롤러 (MemberController)

기존 컨트롤러를 수정합니다. 화면(HTML)을 반환하는 게 아니라, JSON 데이터를 주고받아야 하므로 @RequestBody를 사용합니다.

[수정 포인트]

  1. 회원가입(join): JSON 데이터를 받아서 처리하도록 변경.
  2. 로그인(login): ID/PW를 JSON으로 받아 토큰을 반환.
  3. 경로(RequestMapping): /members로 묶어서 관리.
  • 위치: src/main/java/com/example/board/controller/MemberController.java
package com.example.board.controller;

import com.example.board.dto.MemberJoinDto;
import com.example.board.dto.MemberLoginDto;
import com.example.board.jwt.JwtToken;
import com.example.board.service.MemberService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/members")
public class MemberController {

    private final MemberService memberService;

    // 1. 로그인 API
    @PostMapping("/login")
    public JwtToken login(@RequestBody MemberLoginDto memberLoginDto) {
        String username = memberLoginDto.getUsername();
        String password = memberLoginDto.getPassword();
        JwtToken jwtToken = memberService.login(username, password);
        log.info("request username = {}, password = {}", username, password);
        log.info("jwtToken accessToken = {}, refreshToken = {}", jwtToken.getAccessToken(), jwtToken.getRefreshToken());
        return jwtToken;
    }

    // 2. 회원가입 API
    @PostMapping("/join")
    public String join(@RequestBody MemberJoinDto memberJoinDto) {
        try {
            memberService.join(memberJoinDto);
            return "회원가입 성공";
        } catch (Exception e) {
            return e.getMessage();
        }
    }

    // 3. 테스트 API (로그인 한 사람만 접근 가능)
    @PostMapping("/test")
    public String test() {
        return "success";
    }
}

Step 4. SecurityConfig 경로 수정 (중요!)

컨트롤러의 주소가 /members/...로 시작하게 바뀌었으므로, 시큐리티 설정에서 허용할 주소(requestMatchers)도 맞춰줘야 합니다. 이걸 안 하면 403 에러가 뜹니다.

  • 위치: src/main/java/com/example/board/config/SecurityConfig.java
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .httpBasic((basic) -> basic.disable())
            .csrf((csrf) -> csrf.disable())
            .sessionManagement((session) -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .authorizeHttpRequests((auth) -> auth
                // ▼▼▼ 여기 경로를 잘 봐주세요! (/members/...) ▼▼▼
                .requestMatchers("/members/login", "/members/join").permitAll()
                .requestMatchers("/members/test").hasRole("USER")
                .anyRequest().authenticated()
            )
            .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

Step 5. Postman으로 테스트하기 (대망의 순간)

이제 브라우저가 아닌 Postman을 켭니다. (없으면 설치해주세요!)

1. 회원가입 (JSON 전송)

  • Method: POST
  • URL: http://localhost:8080/members/join
  • Body: Raw -> JSON 선택
  • Data:
{
    "username": "silver",
    "password": "1234",
    "role": "USER"
}

(DTO 필드명과 일치해야 합니다. role 등은 DTO 구성에 따라 생략 가능)

  • Send -> "회원가입 성공" 문자열 도착.

2. 로그인 시도 (토큰 발급)

  • Method: POST
  • URL: http://localhost:8080/members/login
  • Body: Raw -> JSON 선택
    {
        "username": "silver",
        "password": "1234"
    }


  • Send 클릭!

 

[결과 확인]
하단 응답창에 아래와 같이 나오면 성공입니다!

{
    "grantType": "Bearer",
    "accessToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzaWx2ZXIiLCJhdXRoIjoiUk9MRV9VU0VSIiw...",
    "refreshToken": null
}

저 긴 문자열이 바로 여러분의 출입증(JWT)입니다.

3. 토큰 사용하여 요청 보내기

이제 저 토큰을 가지고, 로그인한 사람만 갈 수 있는 /members/test 페이지를 뚫어봅시다.

  1. Method: POST
  2. URL: http://localhost:8080/members/test
  3. 그냥 Send -> 403 Forbidden (출입금지) 뜸. (정상)
  4. Header 탭 이동
  • Key: Authorization
  • Value: Bearer eyJhbGciOiJIUz... (아까 받은 토큰 복붙, 앞에 Bearer와 띄어쓰기 한 칸 필수!)
  1. 다시 Send -> "success" 문자열 도착! 🎉

🎉 시리즈 완결

축하드립니다! 여러분은 이제:

  1. Spring Security의 기본 설정을 이해하고,
  2. DB와 연동하여 회원을 관리하며,
  3. 비밀번호를 암호화하여 저장하고,
  4. 최신 트렌드인 JWT 토큰 기반 인증 시스템까지 직접 구축했습니다.

 

반응형
Comments