ํ ํฐ(token)์ ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ฅผ ๊ตฌ๋ถํ๊ธฐ ์ํ ์ ์ผํ ๊ฐ์ด๋ฉฐ ์๋ฒ๊ฐ ํ ํฐ์ ์์ฑํด์ ํด๋ผ์ด์ธํธ์๊ฒ ์ ๊ณตํ๋ฉด, ํด๋ผ์ด์ธํธ๋ ์ด ํ ํฐ์ ๊ฐ๊ณ ์๋ค๊ฐ ์ฌ๋ฌ ์์ฒญ์ ์ด ํ ํฐ๊ณผ ํจ๊ป ์ ์ฒญํ๋ค.
์๋ฒ๋ ํ ํฐ๋ง ๋ณด๊ณ ์ ํจํ ์ฌ์ฉ์์ธ์ง ๊ฒ์ฆํ๋ ๊ฒ์ ํ ํฐ ๊ธฐ๋ฐ ์ธ์ฆ์ด๋ผ๊ณ ํ๋ค.
aaaaa(ํค๋) . bbbbb(๋ด์ฉ) . cccccc(์๋ช )
.
๊ธฐ์ค์ผ๋ก ํค๋(header), ๋ด์ฉ(payload), ์๋ช
(signature)์ผ๋ก ์ด๋ฃจ์ด์ ธ ์๋ค.// ํ ํฐ ํ์
๊ณผ ํด์ฑ ์๊ณ ๋ฆฌ์ฆ ์
{
"typ" : "JWT",
"alg" : "HS256"
}
์ด๋ฆ | ์ค๋ช |
---|---|
typ | ํ ํฐ์ ํ์ ์ ์ง์ ํ๋ค. JWT๋ผ๋ ๋ฌธ์์ด์ด ๋ค์ด๊ฐ๊ฒ ๋๋ค. |
alg | ํด์ฑ ์๊ณ ๋ฆฌ์ฆ์ ์ง์ ํ๋ค. |
ํด๋ ์(Claim)
์ด๋ผ๊ณ ๋ถ๋ฅด๋ฉฐ, ํด๋ ์์ ํค๊ฐ์ ํ ์์ผ๋ก ์ด๋ฃจ์ด์ ธ ์๋ค.๋ฑ๋ก๋ ํด๋ ์์ ํ ํฐ์ ๋ํ ์ ๋ณด๋ฅผ ๋ด๋๋ฐ ์ฌ์ฉํ๋ค.
์ด๋ฆ | ์ค๋ช |
---|---|
iss | ํ ํฐ ๋ฐ๊ธ์(issue) |
sub | ํ ํฐ ์ ๋ชฉ(subject) |
aud | ํ ํฐ ๋์์(audience) |
exp | ํ ํฐ์ ๋ง๋ฃ ์๊ฐ(expiration), ์๊ฐ์ NumericDate ํ์์ผ๋ก ํ๋ฉฐ, ํญ์ ํ์ฌ ์๊ฐ ์ดํ๋ก ์ค์ ํ๋ค. |
nbf | ํ ํฐ์ ํ์ฑ ๋ ์ง์ ๋น์ทํ ๊ฐ๋ ์ผ๋ก, nbf๋ Not Before๋ฅผ ์๋ฏธํ๋ค. NumericDate ํ์์ผ๋ก ๋ ์ง๋ฅผ ์ง์ ํ๋ฉฐ, ์ด ๋ ์ง๊ฐ ์ง๋๊ธฐ ์ ๊น์ง๋ ํ ํฐ์ด ์ฒ๋ฆฌ๋์ง ์๋๋ค. |
iat | ํ ํฐ์ด ๋ฐ๊ธ๋ ์๊ฐ์ผ๋ก iat์ issued at์ ์๋ฏธํ๋ค. |
jti | jwt์ ๊ณ ์ ์๋ณ์๋ก์ ์ฃผ๋ก ์ผํ์ฉ ํ ํฐ์ ์ฌ์ฉํ๋ค. |
๊ณต๊ฐ ํด๋ ์ ์ ๊ณต๊ฐ๋์ด๋ ์๊ด์๋ ํด๋ ์์ ์๋ฏธํ๋ค.
๋น๊ณต๊ฐ ํด๋ ์ ์ ๊ณต๊ฐ๋๋ฉด ์๋๋ ํด๋ ์์ ์๋ฏธํ๋ค.
{
"iss" : "kkongdo@gmail.com", // ๋ฑ๋ก๋ ํด๋ ์
"iat" : 1622370878, // ๋ฑ๋ก๋ ํด๋ ์
"exp" : 1622372678, // ๋ฑ๋ก๋ ํด๋ ์
"https://kkongdo.com/jwt_claims/is_admin" : true,
"email" : "kkongdo@gmail.com",
"hello" : "์๋
ํ์ธ์!"
}
iss, iat, exp๋ JWT ์์ฒด์์ ๋ฑ๋ก๋ ํด๋ ์์ด๊ณ , URL๋ก ๋ค์ด๋ฐ๋ https://kkongdo.com/jwt_claims/is_admin์ ๊ณต๊ฐ ํด๋ ์์ด๋ค. ๊ทธ ์ธ์ ๋ฑ๋ก๋ ํด๋ ์๋, ๊ณต๊ฐ ํด๋ ์๋ ์๋ email๊ณผ hello๋ ๋น๊ณต๊ฐ ํด๋ ์ ๊ฐ์ด๋ค.
์๋ช (Signature)๋ ํด๋น ํ ํฐ์ด ์กฐ์๋์๊ฑฐ๋ ๋ณ๊ฒฝ๋์ง ์์์์ ํ์ธํ๋ ์ฉ๋๋ก ์ฌ์ฉํ๋ฉฐ, ํค๋(Header)์ ์ธ์ฝ๋ฉ ๊ฐ๊ณผ ๋ด์ฉ(Payload)์ ์ธ์ฝํ ๊ฐ์ ํฉ์น ํ์ ์ฃผ์ด์ง ๋น๋ฐํค๋ฅผ ์ฌ์ฉํด์ ํด์๊ฐ์ ์์ฑํ๋ค.
๋ฆฌํ๋ ์ฌ ํ ํฐ(Refresh token)์ ์ฌ์ฉ์๋ฅผ ์ธ์ฆํ๊ธฐ ์ํ ์ฉ๋๊ฐ ์๋ ์ก์ธ์ค ํ ํฐ์ด ๋ง๋ฃ๋์์ ๋ ์๋ก์ด ์ก์ธ์ค ํ ํฐ์ ๋ฐ๊ธํ๊ธฐ ์ํด ์ฌ์ฉํ๋ ํ ํฐ์ด๋ค.
// jwt ์ค์ ํ๊ธฐ ์ํ dependencies
implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-gson:0.11.5'
jwt.issuer=kkongdo@gmail.com
jwt.secret_key=study-springboot
package com.springboot.springsecurity.config.jwt;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Setter
@Getter
@Component
@ConfigurationProperties("jwt") // ์๋ฐ ํด๋์ค์ ํ๋กํผํฐ๊ฐ์ ๊ฐ์ ธ์์ ์ฌ์ฉํ๋ ์ด๋
ธํ
์ด์
์ด๋ค.
public class JwtProperties {
private String issuer; // kkongdo@gmail.com์ด ๋ค์ด๊ฐ๋ค.
private String secretKey; // study-springboot๊ฐ ๋ค์ด๊ฐ๋ค.
}
package com.springboot.springsecurity.config.jwt;
import io.jsonwebtoken.*;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import com.springboot.springsecurity.domain.User;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.Collections;
import java.util.Date;
import java.util.Set;
// ํ ํฐ์ ์์ฑํ๊ณ ์ฌ๋ฐ๋ฅธ ํ ํฐ์ธ์ง ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ํ๊ณ , ํ ํฐ์์ ํ์ํ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ ํด๋์ค
@RequiredArgsConstructor
@Service
public class TokenProvider {
private final JwtProperties jwtProperties;
public String generateToken(User user, Duration expiredAt){
Date now = new Date();
return makeToken(new Date(now.getTime() + expiredAt.toMillis()), user);
}
// 1. JWT ํ ํฐ ์์ฑ ๋ฉ์๋
private String makeToken(Date expiry, User user){
Date now = new Date();
return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.setIssuer(jwtProperties.getIssuer()) // ๋ฐ๊ธ์
.setIssuedAt(now) //๋ฐ๊ธ๋ ์ง
.setExpiration(expiry) // ๋ง๋ฃ์ผ
.setSubject(user.getEmail()) // ํ ํฐ ์ ๋ชฉ
.claim("id", user.getId()) // ํด๋ ์์ ์ ์ id์ ์ฅ
.signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
.compact();
}
// 2. JWT ํ ํฐ ์ ํจ์ฑ ๊ฒ์ฆ ๋ฉ์๋
public boolean validToken(String token){
try{
Jwts.parser()
.setSigningKey(jwtProperties.getSecretKey()) //๋น๋ฐํค๋ก ๋ณตํธํ
.parseClaimsJws(token);
return true;
}catch (Exception e){
return false;
}
}
// 3. ํ ํฐ ๊ธฐ๋ฐ์ผ๋ก ์ธ์ฆ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฉ์๋
public Authentication getAuthentication(String token){
Claims claims = getClaims(token);
Set<SimpleGrantedAuthority> authorities = Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"));
return new UsernamePasswordAuthenticationToken(new org.springframework.security.core.userdetails.User(
claims.getSubject(), "", authorities),token, authorities);
}
// 4. ํ ํฐ ๊ธฐ๋ฐ์ผ๋ก ์ ์ ID๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฉ์๋
public Long getUserId(String token){
Claims claims = getClaims(token);
return claims.get("id",Long.class);
}
private Claims getClaims(String token) {
return Jwts.parser() // ํด๋ ์ ์กฐํ
.setSigningKey(jwtProperties.getSecretKey())
.parseClaimsJws(token)
.getBody();
}
}
package com.springboot.springsecurity.domain;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class RefreshToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column
private Long id;
@Column(name = "user_id", nullable = false, unique = true)
private Long userId;
@Column(name = "refresh_token", nullable = false)
private String refreshToken;
public RefreshToken(Long userId, String refreshToken){
this.userId = userId;
this.refreshToken = refreshToken;
}
public RefreshToken update(String newRefreshToken){
this.refreshToken = newRefreshToken;
return this;
}
}
package com.springboot.springsecurity.config;
import com.springboot.springsecurity.config.jwt.TokenProvider;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private final TokenProvider tokenProvider;
private final static String HEADER_AUTHORIZATION = "Authorization";
private final static String TOKEN_PREFIX = "Bearer";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// ์์ฒญ ํค๋์ Authorization ํค์ ๊ฐ์ ์กฐํํ๋ค.
String authorizationHeader = request.getHeader(HEADER_AUTHORIZATION);
// ๊ฐ์ ธ์จ ๊ฐ์์ ์ ๋์ฌ ์ ๊ฑฐํ๋ค.
String token = getAccessToken(authorizationHeader);
// ๊ฐ์ ธ์จ ํ ํฐ์ด ์ ํจํ์ง ํ์ธ ๋ฐ ์ ํจํ ๋๋ ์ธ์ฆ ์ ๋ณด ์ค์ ์ ํ๋ค.
if(tokenProvider.validToken(token)){
Authentication authentication = tokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String getAccessToken(String authorizationHeader) {
if(authorizationHeader != null && authorizationHeader.startsWith(TOKEN_PREFIX)){
return authorizationHeader.substring(TOKEN_PREFIX.length());
}
return null;
}
}
package com.springboot.springsecurity.service;
import com.springboot.springsecurity.domain.User;
import com.springboot.springsecurity.dto.AddUserRequest;
import com.springboot.springsecurity.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
public Long save(AddUserRequest dto) {
return userRepository.save(User.builder()
.email(dto.getEmail())
// ํจ์ค์๋ ์ํธํ
.password(bCryptPasswordEncoder.encode(dto.getPassword()))
.build()).getId();
}
public User findById(Long userId) {
return userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("UnExpected user"));
}
}
package com.springboot.springsecurity.service;
import com.springboot.springsecurity.domain.RefreshToken;
import com.springboot.springsecurity.repository.RefreshTokenRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class RefreshTokenService {
private final RefreshTokenRepository refreshTokenRepository;
public RefreshToken findByRefreshToken(String refreshToken) {
return refreshTokenRepository.findByRefreshToken(refreshToken).orElseThrow(() -> new IllegalArgumentException("Unexpected toekn"));
}
}
package com.springboot.springsecurity.service;
import com.springboot.springsecurity.config.jwt.TokenProvider;
import com.springboot.springsecurity.domain.User;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.Duration;
@Service
@RequiredArgsConstructor
public class TokenService {
private final TokenProvider tokenProvider;
private final RefreshTokenService refreshTokenService;
private final UserService userService;
public String createNewAccessToken(String refreshToken) throws IllegalAccessException {
// ํ ํฐ ์ ํจ์ฑ ๊ฒ์ฌ์ ์คํจํ๋ฉด ์์ธ ๋ฐ์
if(!tokenProvider.validToken(refreshToken)){
throw new IllegalAccessException("Unexpected token");
}
Long userId = refreshTokenService.findByRefreshToken(refreshToken).getUserId();
User user = userService.findById(userId);
return tokenProvider.generateToken(user, Duration.ofHours(2));
}
}
package com.springboot.springsecurity.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class CreateAccessTokenRequest {
private String refreshToken;
}
package com.springboot.springsecurity.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class CreateAccessTokenResponse {
private String accessToken;
}
package com.springboot.springsecurity.controller;
import com.springboot.springsecurity.dto.CreateAccessTokenRequest;
import com.springboot.springsecurity.dto.CreateAccessTokenResponse;
import com.springboot.springsecurity.service.TokenService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RequiredArgsConstructor
@RestController
public class TokenApiController {
private final TokenService tokenService;
@PostMapping("/api/token")
public ResponseEntity<CreateAccessTokenResponse> createNewAccessToken(@RequestBody CreateAccessTokenRequest request) throws IllegalAccessException {
String newAccessToken = tokenService.createNewAccessToken(request.getRefreshToken());
return ResponseEntity.status(HttpStatus.CREATED).body(new CreateAccessTokenResponse(newAccessToken));
}
}
JWT(JSON Web Token)๋ ์ฃผ๋ก ์ธ์ฆ ๋ฐ ์ ๋ณด ๊ตํ ๋ชฉ์ ์ผ๋ก ์ฌ์ฉ๋๋ ํ ํฐ ๊ธฐ๋ฐ์ ์ธ์ฆ ๋ฉ์ปค๋์ฆ์ด๊ธฐ ๋๋ฌธ์ ํด๋ผ์ด์ธํธ์ ์๋ฒ ๊ฐ์ ์ ๋ขฐํ ์ ์๋ ์ ๋ณด ๊ตํ์ ์ํด ์ฌ์ฉ๋๋ค.
์ฌ์ฉ์ ์ธ์ฆ (Authentication)
์ฌ์ฉ์ ๋ก๊ทธ์ธ ํ ์๋ฒ๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก JWT๋ฅผ ์์ฑํ์ฌ ํด๋ผ์ด์ธํธ์๊ฒ ์ ๋ฌํ๋ค. ์ดํ ํด๋ผ์ด์ธํธ๋ ํด๋น JWT๋ฅผ ์์ฒญ ํค๋์ ํฌํจ์์ผ ์๋ฒ์ ์์ฒญ์ ๋ณด๋ธ๋ค. ์๋ฒ๋ JWT๋ฅผ ๊ฒ์ฆํ์ฌ ์ฌ์ฉ์๋ฅผ ์ธ์ฆํ๋ค.
๊ถํ ๋ถ์ฌ (Authorization)
JWT๋ ์ฌ์ฉ์์ ๊ถํ ์ ๋ณด๋ฅผ ํฌํจํ ์ ์๊ธฐ ๋๋ฌธ์ ๋ฐ๋ผ์ ์๋ฒ๋ JWT๋ฅผ ํตํด ํด๋ผ์ด์ธํธ์ ๊ถํ์ ํ์ธํ๊ณ , ํน์ ์์์ ๋ํ ์ ๊ทผ์ ํ์ฉํ๊ฑฐ๋ ์ ํํ ์ ์๋ค.
์ ๋ณด ๊ตํ
JWT๋ ์ ๋ขฐํ ์ ์๋ ์ ๋ณด ๊ตํ์ ์ํด ์ฌ์ฉ๋๋ค.
๊ทธ๋ฌ๋ฏ๋ก ์๋ฒ ๊ฐ์ ์ฌ์ฉ์ ์ ๋ณด๋ ์ค์ ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ์ ๋ JWT๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฐ์ดํฐ์ ๋ฌด๊ฒฐ์ฑ์ ๋ณด์ฅํ ์ ์๋ค.
JWT๋ ์ ๋ขฐํ ์ ์๋ ์ ๋ณด ๊ตํ์ ์ํด ์ฌ์ฉ๋๋ค. ์๋ฅผ ๋ค์ด, ์๋ฒ ๊ฐ์ ์ฌ์ฉ์ ์ ๋ณด๋ ์ค์ ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ์ ๋ JWT๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฐ์ดํฐ์ ๋ฌด๊ฒฐ์ฑ์ ๋ณด์ฅํ ์ ์์ ๊ฒ ๊ฐ๋ค.
https://joie-kim.github.io/JWT-Auth/
์คํ๋ง ๋ถํธ 3 ๋ฐฑ์๋ ๊ฐ๋ฐ์ ๋๊ธฐ: ์๋ฐ ํธ - ์ ์ ์ -
ํ... ์ด๋ฉ์ผ ๊ฐ์ ๋ฑ ๊ธฐ๋ค๋ ค ์ด๊ฑฐ ๋ณด๊ณ ๊ตฌํํ๋ค.