📚 목차
🧩 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 요청을 하게 된다면?
- 해결 방법
- Sticky Session: Client 마다 요청 Server 고정
- 세션 저장소
- Session storage가 모든 Client 의 로그인 정보 소유하고 있기 때문에 모든 서버에서 모든 Client의 API 요청을 처리 가능함
- JWT 사용
- 로그인 정보를 Server 에 저장하지 않고, Client 에 로그인 정보를 JWT 로 암호화하여 저장 → JWT 통해 인증/인가
- 모든 서버에서 동일한 Secret Key를 소유함 → 암호화/복호화 시 위조 검증
JWT 장단점
- 장점
- 동시 접속자가 많을 때 서버 측 부하 낮춤
- Client, Sever가 다른 도메인을 사용할 때 (예: 카카오 OAuth2 로그인 시 JWT Token 사용)
- 단점
- 구현의 복잡도 증가
- JWT에 담는 내용이 커질 수록 네트워크 비용 증가 (클라이언트 → 서버)
- 이미 생성된 JWT를 일부만 만료시킬 방법이 없음
- Secret key 유출 시 JWT 조작 가능
JWT 사용 흐름
- Client가 username, password로 로그인 성공 시
- Server에서 로그인 정보를 JWT로 암호화
- Server에서 직접 쿠키를 생성해 JWT를 담아 Client 응답에 전달
- 브라우저 쿠키 저장소에 자동으로 JWT가 저장됨
- Client 에서 JWT 통해 인증방법
- 서버에서 API 요청할 때마다 쿠키에 포함된 JWT를 찾아서 사용
- 서버는 클라이언트가 전달한 JWT의 위조 여부 검증 및 유효기간 검증
- 검증 성공 시 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 조회 테스트

🧩 회원가입 구현
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;
}
'내일배움캠프' 카테고리의 다른 글
| [내일배움캠프] RestTemplate, Open API (0) | 2026.04.09 |
|---|---|
| [내일배움캠프] 필터, Spring Security, Validation (0) | 2026.04.08 |
| [내일배움캠프] Bean 수동 등록, 쿠키와 세션 (0) | 2026.04.07 |
| [내일배움캠프] JPA와 Entity, 영속성 컨텍스트 (0) | 2026.04.06 |
| [내일배움캠프] IoC, DI, Bean (0) | 2026.04.06 |

