내일배움캠프

[내일배움캠프] JWT을 사용한 회원가입과 로그인 구현

munsik22 2026. 4. 8. 14:39

📚 목차

    🧩 JWT란?

    JWT(Json Web Token)

    • JSON 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token
    • 일반적으로 쿠키 저장소를 사용하여 JWT를 저장함

    JWT를 쓰는 이유

    • 서버를 2대 이상 운영하는 경우, 세션마다 다른 Client 로그인 정보를 가지고 있을 수도 있다.
      • Session1: Client1, Client2, Client3
      • Session2: Client4
      • Session3: Client5, Client6
    • 만약 Client1의 로그인 정보를 가지고 있지 않은 Sever2에 API 요청을 하게 된다면?
    • 해결 방법
      1. Sticky Session: Client 마다 요청 Server 고정
      2. 세션 저장소
        • Session storage가 모든 Client 의 로그인 정보 소유하고 있기 때문에 모든 서버에서 모든 Client의 API 요청을 처리 가능함
      3. JWT 사용
        • 로그인 정보를 Server 에 저장하지 않고, Client 에 로그인 정보를 JWT 로 암호화하여 저장 → JWT 통해 인증/인가
        • 모든 서버에서 동일한 Secret Key를 소유함 → 암호화/복호화 시 위조 검증

    JWT 장단점

    • 장점
      • 동시 접속자가 많을 때 서버 측 부하 낮춤
      • Client, Sever가 다른 도메인을 사용할 때 (예: 카카오 OAuth2 로그인 시 JWT Token 사용)
    • 단점
      • 구현의 복잡도 증가
      • JWT에 담는 내용이 커질 수록 네트워크 비용 증가 (클라이언트 → 서버)
      • 이미 생성된 JWT를 일부만 만료시킬 방법이 없음
      • Secret key 유출 시 JWT 조작 가능

    JWT 사용 흐름

    1. Client가 username, password로 로그인 성공 시
      1. Server에서 로그인 정보를 JWT로 암호화
      2. Server에서 직접 쿠키를 생성해 JWT를 담아 Client 응답에 전달
      3. 브라우저 쿠키 저장소에 자동으로 JWT가 저장됨
    2. Client 에서 JWT 통해 인증방법
      1. 서버에서 API 요청할 때마다 쿠키에 포함된 JWT를 찾아서 사용
      2. 서버는 클라이언트가 전달한 JWT의 위조 여부 검증 및 유효기간 검증
      3. 검증 성공 시 JWT에서 사용자 정보를 가져와 확인

    JWT의 구조

    • Header: 알고리즘(HS256), 타입(JWT)
    • Payload: username, admin 여부 등 실제 유저 정보
    • Signature

    🧩 JWT 다루기

    • build.gradle 의존성 추가
    compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
    • application.properties
    jwt.secret.key=
    • /jwt/JwtUtil.java: JWT Provider 생성
    @Component
    public class JwtUtil {
        public static final String AUTHORIZATION_HEADER = "Authorization";
        public static final String AUTHORIZATION_KEY = "auth";
        public static final String BEARER_PREFIX = "Bearer ";
        private final long TOKEN_TIME = 60 * 60 * 1000L;
    
        @Value("${jwt.secret.key}")
        private String secretKey;
        private Key key;
        private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
    
        public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그");
    
        @PostConstruct
        public void init() {
            byte[] bytes = Base64.getDecoder().decode(secretKey);
            key = Keys.hmacShaKeyFor(bytes);
        }
    
        public String createToken(String username, UserRoleEnum role) {
            Date date = new Date();
    
            return BEARER_PREFIX +
                    Jwts.builder()
                            .setSubject(username)
                            .claim(AUTHORIZATION_KEY, role)
                            .setExpiration(new Date(date.getTime() + TOKEN_TIME))
                            .setIssuedAt(date)
                            .signWith(key, signatureAlgorithm)
                            .compact();
        }
    
        public void addJwtToCookie(String token, HttpServletResponse res) {
            try {
                token = URLEncoder.encode(token, "utf-8").replaceAll("\\+", "%20");
                
                Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token);
                cookie.setPath("/");
    
                res.addCookie(cookie);
            } catch (UnsupportedEncodingException e) {
                logger.error(e.getMessage());
            }
        }
    
        public String substringToken(String tokenValue) {
            if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
                return tokenValue.substring(7);
            }
            logger.error("Not Found Token");
            throw new NullPointerException("Not Found Token");
        }
    
        public boolean validateToken(String token) {
            try {
                Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
                return true;
            } catch (SecurityException | MalformedJwtException | SignatureException e) {
                logger.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
            } catch (ExpiredJwtException e) {
                logger.error("Expired JWT token, 만료된 JWT token 입니다.");
            } catch (UnsupportedJwtException e) {
                logger.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
            } catch (IllegalArgumentException e) {
                logger.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
            }
            return false;
        }
    
        public Claims getUserInfoFromToken(String token) {
            return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
        }
    }
    • entity/UserRoleEnum.java
    @Getter
    public enum UserRoleEnum {
        USER(Authority.USER),
        ADMIN(Authority.ADMIN);
    
        private final String authority;
    
        UserRoleEnum(String authority) {
            this.authority = authority;
        }
    
        public static class Authority {
            public static final String USER = "ROLE_USER";
            public static final String ADMIN = "ROLE_ADMIN";
        }
    }
    • AutoController
    private final JwtUtil jwtUtil;
    
    public AuthController(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }
    
    @GetMapping("/create-jwt")
    public String createJwt(HttpServletResponse res) {
        String token = jwtUtil.createToken("Robbie", UserRoleEnum.USER);
    
        jwtUtil.addJwtToCookie(token, res);
    
        return "createJwt : " + token;
    }
    
    @GetMapping("/get-jwt")
    public String getJwt(@CookieValue(JwtUtil.AUTHORIZATION_HEADER) String tokenValue) {
        String token = jwtUtil.substringToken(tokenValue);
    
        if(!jwtUtil.validateToken(token)){
            throw new IllegalArgumentException("Token Error");
        }
    
        Claims info = jwtUtil.getUserInfoFromToken(token);
        String username = info.getSubject();
        System.out.println("username = " + username);
        String authority = (String) info.get(JwtUtil.AUTHORIZATION_KEY);
        System.out.println("authority = " + authority);
    
        return "getJwt : " + username + ", " + authority;
    }
    • JWT 생성 테스트

    jwt.io에서 본 모습

    • JWT 조회 테스트

    🧩 회원가입 구현

    • build.gradle
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'com.mysql:mysql-connector-j'
    • .env
    DB_URL=jdbc:mysql://localhost:3306/auth
    DB_USERNAME=사용자명
    DB_PASSWORD=비밀번호
    • Intellij에서 환경 변수 설정하기

    • .gitignore: .env 추가하기
    • application.properties
    spring.config.import=optional:file:.env[.properties]
    spring.datasource.url=${DB_URL}
    spring.datasource.username=${DB_USERNAME}
    spring.datasource.password=${DB_PASSWORD}
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    
    spring.jpa.hibernate.ddl-auto=update
    spring.jpa.properties.hibernate.show_sql=true
    spring.jpa.properties.hibernate.format_sql=true
    spring.jpa.properties.hibernate.use_sql_comments=true
    • 데이터베이스 생성: CMD나 MySQL Workbench에서 실행
    CREATE DATABASE auth;
    • HomeController 생성
    @Controller
    public class HomeController {
    
        @GetMapping("/")
        public String home(Model model) {
            model.addAttribute("username", "username");
            return "index";
        }
    }
    • UserController 생성
    @Controller
    @RequestMapping("/api")
    public class UserController {
    
        private final UserService userService;
    
        public UserController(UserService userService) {
            this.userService = userService;
        }
    
        @GetMapping("/user/login-page")
        public String loginPage() {
            return "login";
        }
    
        @GetMapping("/user/signup")
        public String signupPage() {
            return "signup";
        }
    
        @PostMapping("/user/signup")
        public String signup(SignupRequestDto signupRequestDto) {
            userService.signup(signupRequestDto);
            return "redirect:/api/user/login-page";
        }
    }
    • templates에 index.html, login.html, signup.html 생성
    /api/user/login-page /api/user/signup
    • static/css/style.css, /js/basic.js 생성
    • User 엔티티 생성
    @Entity
    @Getter @Setter
    @NoArgsConstructor
    @Table(name = "users")
    public class User {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        @Column(nullable = false, unique = true)
        private String username;
    
        @Column(nullable = false)
        private String password;
    
        @Column(nullable = false, unique = true)
        private String email;
    
        @Column(nullable = false)
        @Enumerated(value = EnumType.STRING)
        private UserRoleEnum role;
    
        public User(String username, String password, String email, UserRoleEnum role) {
            this.username = username;
            this.password = password;
            this.email = email;
            this.role = role;
        }
    }
    • 회원가입 API 설계
    이름 메서드 URL 설명
    회원가입 페이지 GET /api/user/signup 회원가입 페이지 호출
    회원가입 POST /api/user/signup 회원가입
    • SignupRequestDto
    @Getter @Setter
    public class SignupRequestDto {
        private String username;
        private String password;
        private String email;
        private boolean admin = false;
        private String adminToken = "";
    }
    • UserService
    @Service
    public class UserService {
    
        private final UserRepository userRepository;
        private final PasswordEncoder passwordEncoder;
    
        public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
            this.userRepository = userRepository;
            this.passwordEncoder = passwordEncoder;
        }
    
        private final String ADMIN_TOKEN = "AAABnvxRVklrnYxKZ0aHgTBcXukeZygoC";
    
        public void signup(SignupRequestDto requestDto) {
            String username = requestDto.getUsername();
            String password = passwordEncoder.encode(requestDto.getPassword());
    
            Optional<User> checkUsername = userRepository.findByUsername(username);
            if (checkUsername.isPresent()) {
                throw new IllegalArgumentException("중복된 사용자가 존재합니다.");
            }
    
            String email = requestDto.getEmail();
            Optional<User> checkEmail = userRepository.findByEmail(email);
            if (checkEmail.isPresent()) {
                throw new IllegalArgumentException("중복된 Email 입니다.");
            }
    
            UserRoleEnum role = UserRoleEnum.USER;
            if (requestDto.isAdmin()) {
                if (!ADMIN_TOKEN.equals(requestDto.getAdminToken())) {
                    throw new IllegalArgumentException("관리자 암호가 틀려 등록이 불가능합니다.");
                }
                role = UserRoleEnum.ADMIN;
            }
    
            User user = new User(username, password, email, role);
            userRepository.save(user);
        }
    }
    • UserRepository
    public interface UserRepository extends JpaRepository<User, Long> {
    
        Optional<User> findByUsername(String username);
    
        Optional<User> findByEmail(String email);
    }

    🧩 로그인 구현

    • 로그인 API 설계
    이름 메서드 URL 설명
    로그인 페이지 GET /api/user/login-page 로그인 페이지 호출
    로그인 POST /api/user/login 로그인
    • UserService
    public void login(LoginRequestDto requestDto, HttpServletResponse res) {
        String username = requestDto.getUsername();
        String password = requestDto.getPassword();
    
        // 사용자 확인
        User user = userRepository.findByUsername(username).orElseThrow(
                () -> new IllegalArgumentException("등록된 사용자가 없습니다.")
        );
    
        // 비밀번호 확인
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
        }
    
        // JWT 생성 및 쿠키에 저장 후 Response 객체에 추가
        String token = jwtUtil.createToken(user.getUsername(), user.getRole());
        jwtUtil.addJwtToCookie(token, res);
    }
    • UserController
    @PostMapping("/user/login")
    public String login(LoginRequestDto loginRequestDto, HttpServletResponse res) {
        try {
            userService.login(loginRequestDto, res);
        } catch (Exception e) {
            return "redirect:/api/user/login-page?error";
        }
    
        return "redirect:/";
    }
    • LoginRequestDto
    @Setter @Getter
    public class LoginRequestDto {
        private String username;
        private String password;
    }