Search
Duplicate

JWT x SpringBoot

SpringBoot ๋กœ JWT ํ† ํฐ ์ƒ์„ฑ ๋ฐ ํ•ด์„ํ•˜๊ธฐ

JWT ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

build.gradle

// jwt implementation 'io.jsonwebtoken:jjwt-api:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
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 '3.4.1' id 'io.spring.dependency-management' version '1.1.7' } group = 'com.aloha' version = '0.0.1-SNAPSHOT' java { toolchain { languageVersion = JavaLanguageVersion.of(17) } } configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.4' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.4' testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' // spring-boot-configuration-processor implementation 'org.springframework.boot:spring-boot-configuration-processor' // jwt implementation 'io.jsonwebtoken:jjwt-api:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' } 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
๋ณต์‚ฌ