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
โข
โข
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
๋ณต์ฌ