Search

JWT x SpringBoot

SpringBoot 둜 JWT 토큰 생성 및 ν•΄μ„ν•˜κΈ°

JWT 라이브러리

build.gradle

// jwt implementation 'io.jsonwebtoken:jjwt-api:0.12.3' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3'
Plain Text
볡사

ν”„λ‘œμ νŠΈ ꡬ쑰

μž‘μ—… μ½”λ“œ

1.
build.gradle
2.
application.properties
3.
SecurityConfig.java
4.
SecurityConatants.java
5.
JwtProps.java
6.
AuthenticationRequest.java
7.
LoginController.java

build.gradle

plugins { id 'java' id 'war' id 'org.springframework.boot' version '2.7.17' id 'io.spring.dependency-management' version '1.1.4' } group = 'com.joeun' version = '0.0.1-SNAPSHOT' java { sourceCompatibility = '11' } configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' annotationProcessor 'org.projectlombok:lombok' providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' // spring-boot-configuration-processor implementation 'org.springframework.boot:spring-boot-configuration-processor' // jwt implementation 'io.jsonwebtoken:jjwt-api:0.12.3' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3' } tasks.named('test') { useJUnitPlatform() }
Plain Text
볡사

application.properties

# HS512 μ•Œκ³ λ¦¬μ¦˜μ˜ μ‹œν¬λ¦Ών‚€ : 512λΉ„νŠΈ (64λ°”μ΄νŠΈ) 이상 # https://passwords-generator.org/kr/ # βœ… μœ„ μ‚¬μ΄νŠΈμ—μ„œ 길이:64 둜 생성함. com.joeun.jwt.secret-key=|+<T%0h;[G97|I$5Lr?h]}`8rUX.7;0gw@bF<R/|"-U0n:_6j={'.T'GHs~<AxU9
Plain Text
볡사

μ°Έμ‘° - μ•Œκ³ λ¦¬μ¦˜ μ‹œν¬λ¦Ών‚€ 생성 μ‚¬μ΄νŠΈ

SecurityConfig.java

package com.joeun.jwt.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; // TODO : deprecated μ—†μ• κΈ° (version : before SpringSecurity 5.4 ⬇) // @EnableWebSecurity // public class SecurityConfig extends WebSecurityConfigurerAdapter { // OK : (version : after SpringSecurity 5.4 ⬆) @Configuration @EnableWebSecurity public class SecurityConfig { // OK : (version : after SpringSecurity 5.4 ⬆) @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { // 폼 기반 둜그인 λΉ„ν™œμ„±ν™” http.formLogin(login ->login.disable()); // HTTP κΈ°λ³Έ 인증 λΉ„ν™œμ„±ν™” http.httpBasic(basic ->basic.disable()); // CSRF(Cross-Site Request Forgery) 곡격 λ°©μ–΄ κΈ°λŠ₯ λΉ„ν™œμ„±ν™” http.csrf(csrf ->csrf.disable()); // μ„Έμ…˜ 관리 μ •μ±… μ„€μ •: STATELESS둜 μ„€μ •ν•˜λ©΄ μ„œλ²„λŠ” μ„Έμ…˜μ„ μƒμ„±ν•˜μ§€ μ•ŠμŒ // πŸ” μ„Έμ…˜μ„ μ‚¬μš©ν•˜μ—¬ μΈμ¦ν•˜μ§€ μ•Šκ³ , JWT λ₯Ό μ‚¬μš©ν•˜μ—¬ μΈμ¦ν•˜κΈ° λ•Œλ¬Έμ—, μ„Έμ…˜ λΆˆν•„μš” http.sessionManagement(management ->management .sessionCreationPolicy(SessionCreationPolicy.STATELESS)); // ꡬ성이 μ™„λ£Œλœ SecurityFilterChain을 λ°˜ν™˜ν•©λ‹ˆλ‹€. return http.build(); } // TODO : deprecated μ—†μ• κΈ° (version : before SpringSecurity 5.4 ⬇) // @Override // protected void configure(HttpSecurity http) throws Exception { // // 폼 기반 둜그인 λΉ„ν™œμ„±ν™” // http.formLogin().disable() // // HTTP κΈ°λ³Έ 인증 λΉ„ν™œμ„±ν™” // .httpBasic().disable(); // // CSRF(Cross-Site Request Forgery) 곡격 λ°©μ–΄ κΈ°λŠ₯ λΉ„ν™œμ„±ν™” // http.csrf().disable(); // // μ„Έμ…˜ 관리 μ •μ±… μ„€μ •: STATELESS둜 μ„€μ •ν•˜λ©΄ μ„œλ²„λŠ” μ„Έμ…˜μ„ μƒμ„±ν•˜μ§€ μ•ŠμŒ // // πŸ” μ„Έμ…˜μ„ μ‚¬μš©ν•˜μ—¬ μΈμ¦ν•˜μ§€ μ•Šκ³ , JWT λ₯Ό μ‚¬μš©ν•˜μ—¬ μΈμ¦ν•˜κΈ° λ•Œλ¬Έμ—, μ„Έμ…˜ λΆˆν•„μš” // http.sessionManagement() // .sessionCreationPolicy(SessionCreationPolicy.STATELESS); // } }
Java
볡사

SecurityConatants.java

package com.joeun.jwt.constants; // ν•΄λ‹Ή ν΄λž˜μŠ€λŠ” Spring Security 및 JWT κ΄€λ ¨ μƒμˆ˜λ₯Ό μ •μ˜ν•œ μƒμˆ˜ ν΄λž˜μŠ€μž…λ‹ˆλ‹€. /** * HTTP * headers : { * Authorization : Bearer ${jwt} * } */ public final class SecurityConstants { // JWT 토큰을 HTTP ν—€λ”μ—μ„œ μ‹λ³„ν•˜λŠ” 데 μ‚¬μš©λ˜λŠ” 헀더 이름 public static final String TOKEN_HEADER = "Authorization"; // JWT ν† ν°μ˜ 접두사. 일반적으둜 "Bearer " λ‹€μŒμ— μ‹€μ œ 토큰이 μ˜΅λ‹ˆλ‹€. public static final String TOKEN_PREFIX = "Bearer "; // JWT ν† ν°μ˜ νƒ€μž…μ„ λ‚˜νƒ€λ‚΄λŠ” μƒμˆ˜ public static final String TOKEN_TYPE = "JWT"; // 이 클래슀λ₯Ό final둜 μ„ μ–Έν•˜μ—¬ 상속을 λ°©μ§€ν•˜κ³ , μƒμˆ˜λ§Œμ„ μ •μ˜ν•˜λ„λ‘ λ§Œλ“­λ‹ˆλ‹€. }
Java
볡사

JwtProps.java

package com.joeun.jwt.prop; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import lombok.Data; // ν•΄λ‹Ή ν΄λž˜μŠ€λŠ” Spring Boot의 `@ConfigurationProperties` // μ–΄λ…Έν…Œμ΄μ…˜μ„ μ‚¬μš©ν•˜μ—¬, application.properties(속성 μ„€μ • 파일) λ‘œλΆ€ν„° // JWT κ΄€λ ¨ ν”„λ‘œνΌν‹°λ₯Ό κ΄€λ¦¬ν•˜λŠ” ν”„λ‘œνΌν‹° ν΄λž˜μŠ€μž…λ‹ˆλ‹€. @Data @Component @ConfigurationProperties("com.joeun.jwt") // com.joeun.jwt 경둜 ν•˜μœ„ 속성듀을 지정 public class JwtProps { // com.joeun.jwt.secretKey둜 μ§€μ •λœ ν”„λ‘œνΌν‹° 값을 μ£Όμž…λ°›λŠ” ν•„λ“œ // βœ… com.joeun.jwt.secret-key ➑ secretKey : {μΈμ½”λ”©λœ μ‹œν¬λ¦Ώ ν‚€} private String secretKey; }
Java
볡사

AuthenticationRequest.java

package com.joeun.jwt.domain; import lombok.Getter; import lombok.Setter; import lombok.ToString; @Getter @Setter @ToString public class AuthenticationRequest { private String username; private String password; }
Java
볡사

LoginController.java

package com.joeun.jwt.controller; import java.util.ArrayList; import java.util.Date; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; import com.joeun.jwt.constants.SecurityConstants; import com.joeun.jwt.domain.AuthenticationRequest; import com.joeun.jwt.prop.JwtProps; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; // μΆ”κ°€: μƒˆλ‘œμš΄ λ°©μ‹μœΌλ‘œ SecretKey 생성을 μœ„ν•œ 클래슀 import lombok.extern.slf4j.Slf4j; @Slf4j @RestController public class LoginController { @Autowired private JwtProps jwtProps; /** * πŸ‘©β€πŸ’Όβž‘πŸ” JWT 을 μƒμ„±ν•˜λŠ” Login μš”μ²­ * [GET] - /login * body : { "username" : "joeun", "password" : "123456" } * @param authReq * @return */ @PostMapping("login") public ResponseEntity<?> login(@RequestBody AuthenticationRequest authReq) { // μ‚¬μš©μžλ‘œλΆ€ν„° 전달받은 인증 정보 String username = authReq.getUsername(); String password = authReq.getPassword(); log.info("username : " + username); log.info("password : " + password); // μ‚¬μš©μž μ—­ν•  정보 List<String> roles = new ArrayList<>(); roles.add("ROLE_USER"); roles.add("ROLE_ADMIN"); // μ„œλͺ…에 μ‚¬μš©ν•  ν‚€ 생성 (μƒˆλ‘œμš΄ 방식) String secretKey = jwtProps.getSecretKey(); byte[] signingKey = jwtProps.getSecretKey().getBytes(); log.info("secretKey : " + secretKey); log.info("signingKey : " + signingKey); // JWT 토큰 생성 String jwt = Jwts.builder() .signWith(Keys.hmacShaKeyFor(signingKey), Jwts.SIG.HS512) // μ„œλͺ…에 μ‚¬μš©ν•  킀와 μ•Œκ³ λ¦¬μ¦˜ μ„€μ • // .setHeaderParam("typ", SecurityConstants.TOKEN_TYPE) // deprecated (version: before 1.0) .header() // update (version : after 1.0) .add("typ", SecurityConstants.TOKEN_TYPE) // 헀더 μ„€μ • .and() .expiration(new Date(System.currentTimeMillis() + 864000000)) // 토큰 만료 μ‹œκ°„ μ„€μ • (10일) .claim("uid", username) // ν΄λ ˆμž„ μ„€μ •: μ‚¬μš©μž 아이디 .claim("rol", roles) // ν΄λ ˆμž„ μ„€μ •: μ—­ν•  정보 .compact(); // μ΅œμ’…μ μœΌλ‘œ 토큰 생성 log.info("jwt : " + jwt); // μƒμ„±λœ 토큰을 ν΄λΌμ΄μ–ΈνŠΈμ—κ²Œ λ°˜ν™˜ return new ResponseEntity<String>(jwt, HttpStatus.OK); } /** * πŸ”βž‘πŸ‘©β€πŸ’Ό JWT λ₯Ό ν•΄μ„ν•˜λŠ” μš”μ²­ * * @param header * @return */ @GetMapping("/user/info") public ResponseEntity<String> userInfo(@RequestHeader(name="Authorization") String header) { log.info("===== header ====="); log.info("Authorization : " + header); String jwt = header.substring(7); // "Bearer " + jwt ➑ jwt μΆ”μΆœ log.info("jwt : " + jwt); String secretKey = jwtProps.getSecretKey(); byte[] signingKey = jwtProps.getSecretKey().getBytes(); log.info("secretKey : " + secretKey); log.info("signingKey : " + signingKey); // TODO : deprecated μ—…μ• κΈ° (version: before 1.0) // Jws<Claims> parsedToken = Jwts.parser() // .setSigningKey(signingKey) // .build() // .parseClaimsJws(jwt); // OK : deprecated 된 μ½”λ“œ μ—…λ°μ΄νŠΈ (version : after 1.0) // - setSigningKey(byte[]) ➑ verifyWith(SecretKey) // - parseClaimsJws(CharSequence) ➑ parseSignedClaims(CharSequence) Jws<Claims> parsedToken = Jwts.parser() .verifyWith(Keys.hmacShaKeyFor(signingKey)) .build() .parseSignedClaims(jwt); log.info("parsedToken : " + parsedToken); String username = parsedToken.getPayload().get("uid").toString(); log.info("username : " + username); Claims claims = parsedToken.getPayload(); Object roles = claims.get("rol"); log.info("roles : " + roles); return new ResponseEntity<String>(parsedToken.toString(), HttpStatus.OK); } }
Java
볡사

Authorization 헀더

HTTP μš”μ²­μ—μ„œ ν΄λΌμ΄μ–ΈνŠΈκ°€ μ„œλ²„μ—κ²Œ μžμ‹ μ˜ 인증 정보λ₯Ό μ œκ³΅ν•˜λŠ” 데 μ‚¬μš©λ˜λŠ” 헀더
Authorization: <type> <credentials>
Plain Text
볡사
β€’
<type>: 인증 νƒ€μž…μ„ λ‚˜νƒ€λƒ…λ‹ˆλ‹€. 일반적으둜 Basic λ˜λŠ” Bearerκ°€ μ‚¬μš©λ©λ‹ˆλ‹€.
β—¦
Basic: ν΄λΌμ΄μ–ΈνŠΈκ°€ 아이디와 λΉ„λ°€λ²ˆν˜Έμ˜ 쑰합을 Base64둜 μΈμ½”λ”©ν•œ 값을 μ œκ³΅ν•˜λŠ” 경우 μ‚¬μš©λ©λ‹ˆλ‹€.
β—¦
Bearer: 토큰 기반의 μΈμ¦μ—μ„œ 토큰을 λ‚˜νƒ€λƒ…λ‹ˆλ‹€.
β€’
<credentials>: μ‹€μ œ 인증 정보λ₯Ό λ‚˜νƒ€λƒ…λ‹ˆλ‹€. μΈμ½”λ”©λœ 아이디와 λΉ„λ°€λ²ˆν˜Έ λ˜λŠ” 토큰이 여기에 λ“€μ–΄κ°‘λ‹ˆλ‹€.

μ˜ˆμ‹œ

Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
Plain Text
볡사
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Plain Text
볡사

ν…ŒμŠ€νŠΈ

둜그인 μš”μ²­ (JWT 토큰 생성)

[POST] - /login

Request
β€’
body
{ "username" : "user", "password" : "123456" }
Java
볡사
Response
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJleHAiOjE3MDMyMDk2NjYsInVpZCI6InVzZXIiLCJyb2wiOlsiUk9MRV9VU0VSIiwiUk9MRV9BRE1JTiJdfQ.FYseLDzbDlhy0aEe4ufzDAPaSYzQaTjgpfqsaDpqMigddJ6rNny_o1E7tE5Br5xzLKyGGvW5e5qOr1mxHBwrLQ
Plain Text
볡사

μ‚¬μš©μž 인증 및 κΆŒν•œ 정보 확인 (JWT 토큰 해석)

[GET] - /user/info

Request
β€’
url : http://localhost:8080/user/info
β€’
header
{ "Authorization" : "Bearer " + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJleHAiOjE3MDMyMDk2NjYsInVpZCI6InVzZXIiLCJyb2wiOlsiUk9MRV9VU0VSIiwiUk9MRV9BRE1JTiJdfQ.FYseLDzbDlhy0aEe4ufzDAPaSYzQaTjgpfqsaDpqMigddJ6rNny_o1E7tE5Br5xzLKyGGvW5e5qOr1mxHBwrLQ" }
Java
볡사
Response
header={typ=JWT, alg=HS512}, payload={exp=1703209666, uid=user, rol=[ROLE_USER, ROLE_ADMIN]}, signature=FYseLDzbDlhy0aEe4ufzDAPaSYzQaTjgpfqsaDpqMigddJ6rNny_o1E7tE5Br5xzLKyGGvW5e5qOr1mxHBwrLQ
Plain Text
볡사