JWT 구조 이해

  • 헤더(Header): 토큰의 타입 (주로 JWT)과 사용된 해싱 알고리즘 (예: HMAC, SHA256, RSA)을 명시합니다.
  • 페이로드(Payload): 토큰에 담을 클레임(claims)을 포함합니다. 클레임은 토큰 사용자에 대한 속성이나 추가적인 필요한 정보입니다.
  • 서명(Signature): 헤더의 인코딩된 값과 페이로드의 인코딩된 값을 합쳐 해싱 알고리즘과 비밀키로 서명합니다.

JWT 라이브러리 선택

다양한 프로그래밍 언어를 위한 JWT 라이브러리가 있습니다. 예를 들어, Java의 경우 java-jwt, Node.js의 경우 jsonwebtoken 등이 있습니다.
 
pom.xml

<dependencies>
    <!-- Spring Boot Starter Security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <!-- JJWT Library -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>
</dependencies>

 
gradle:

dependencies {
    // Spring Boot Starter for Security
    implementation 'org.springframework.boot:spring-boot-starter-security'

    // JJWT for handling JWT
    implementation 'io.jsonwebtoken:jjwt:0.9.1'

    // 기타 필요한 Spring Boot 및 프로젝트 의존성들...
}

JWT 생성

  1. 헤더 설정: 알고리즘과 토큰 타입 설정
  2. 페이로드 설정: 사용자 식별 정보, 유효 기간, 발행자 등을 설정
  3. 서명 생성: 지정한 알고리즘과 비밀키를 사용하여 서명
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;

public class JwtUtil {
    private String secretKey = "your-secret-key"; // 비밀키 설정

    public String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 유효 기간 10시간
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }

    public Claims extractAllClaims(String token) throws Exception {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody();
    }

    public String extractUsername(String token) throws Exception {
        return extractAllClaims(token).getSubject();
    }

    public Boolean isTokenExpired(String token) throws Exception {
        return extractAllClaims(token).getExpiration().before(new Date());
    }

    public Boolean validateToken(String token, String username) throws Exception {
        final String usernameInToken = extractUsername(token);
        return (usernameInToken.equals(username) && !isTokenExpired(token));
    }
}

 
 
  함수 설명

public String generateToken(String username) // 사용자 이름을 받아 jwt를 생성
public Claims extractAllClaims(String token) // 주어진 jwt로부터 클레임을 추출
public String extractUsername(String token)  // 주어진 토큰에서 사용자 이름 추출
public Boolean isTokenExpired(String token)   // 토큰 만료 확인
public Boolean validateToken(String token, String username) //토큰 유효성 확인

generatetoken 함수

public String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 유효 기간 10시간
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }

setSubject :토큰 주제설정. 예시 코드에서는 username을 이용
setIssuedAt :토큰의 issuedat 시간설정을합니다.
setExpiration: 토큰의 만료시간을 설정
signWith : 사용할 서명 알고리즘과 비밀키를 지정(HMAC을 사용하는 HS256 알고리즘을 이용)
compact : 위설정을 바탕으로  JWT 문자을  생성하여 반환한다.
 
extractAllClaims 함수

public Claims extractAllClaims(String token) throws Exception {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody();
    }

Jwts.parser().setSigningKey(secretKey) : 비밀키를 사용하여 파싱를 설정 
parserClaimsJws(token)  : jwt를 파싱하여 클레임을 반환 
 
 
extractUsername 함수

public String extractUsername(String token) throws Exception {
        return extractAllClaims(token).getSubject();
    }

extractAllClaims(token).getSubject() : 주어진 토큰에서 subject  추출 ( 예시 코드는 username)
 
isTokenExpired  함수

public Boolean isTokenExpired(String token) throws Exception {
        return extractAllClaims(token).getExpiration().before(new Date());
    }

extractAllClaims(token).getExpiration().before(new Date()) : 주어진 토큰 만료시간 확인
 
validateToken 함수

public Boolean validateToken(String token, String username) throws Exception {
        final String usernameInToken = extractUsername(token);
        return (usernameInToken.equals(username) && !isTokenExpired(token));
    }

usernameInToken.equals(username) && !isTokenExpired(token) : 토큰이 만료되었는지 확인 

JWT 인증 및 발급

spring security 클래스 생성
 

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests().antMatchers("/authenticate").permitAll()
            .anyRequest().authenticated();
            // 여기에 필터 추가 및 기타 구성
    }
}

configure의 /authenticate 권한 허용

  .authorizeRequests().antMatchers("/authenticate").permitAll()

 
인증 및 발급 클래스 AuthenticationController

@RestController
public class AuthenticationController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtUtil jwtTokenUtil;

    @RequestMapping(value = "/authenticate", method = RequestMethod.POST)
    public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {
        try {
            authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword())
            );
        }
        catch (BadCredentialsException e) {
            throw new Exception("Incorrect username or password", e);
        }

        final String jwt = jwtTokenUtil.generateToken(authenticationRequest.getUsername());

        return ResponseEntity.ok(new AuthenticationResponse(jwt));
    }
}

AuthenticationManager : spring security에서 제공하는 인터페이스로, 사용자 인증을 관리합니다.
JwtUtil : JWT 생성 및 검증 로직이 포함된 유틸리티 클래스입니다.
 
인증과정:
authenticationManager.authenticate(...)  :
username과 password를 포함하는 UsernamePasswordAuthenticationToken을 생성하여 전달
 
예외처리 :
BadCredentialsException :
이 발생하면, 잘못된 사용자명 또는 비밀번호로 인한 인증 실패를 나타냅니다. 이 경우 사용자에게 적절한 예외 메시지를 반환
 
jwt 생성 :
jwtTokenUtil.generateToken(authenticationRequest.getUsername())
유저명 입력 토큰생성
 
응답 반환: 
 return ResponseEntity.ok(new AuthenticationResponse(jwt));
토큰 생성 완료 200 반환 및 jwt 토큰 반환 

JWT 필터의 추가  및 전송

필터의 추가 클래스

public class JwtRequestFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private MyUserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        final String authorizationHeader = request.getHeader("Authorization");

        String username = null;
        String jwt = null;

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            jwt = authorizationHeader.substring(7);
            username = jwtUtil.extractUsername(jwt);
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
            if (jwtUtil.validateToken(jwt, userDetails.getUsername())) {
                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            }
        }

        chain.doFilter(request, response);
    }
}

 
 
JwtUtilJWT를 생성하고 검증하는 메서드를 제공
MyUserDetailsService : 사용자 정보를 로드하는 서비스
UserDetailsService인터페이스의 구현체
 
실제 필터링 로직이 구현되는 곳

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain):

 
 
 
필터링 로직 
 
 
생성된 JWT는 주로 HTTP 헤더를 통해 전송됩니다. 일반적으로 'Authorization' 헤더에 'Bearer' 스키마를 사용하여 추가됩니다.
 
JWT 추출:
Authorization 헤더를 확인 jwt는 보통 Bearer[토큰] 형식으로 전송

 final String authorizationHeader = request.getHeader("Authorization");

사용자 인증 정보 확인 및 설정:
 
정보 확인 :

 if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            jwt = authorizationHeader.substring(7);
            username = jwtUtil.extractUsername(jwt);
        }

 
UserDetailsService를 사용하여 사용자의 UserDetails를 로드합니다.

UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

 
jwt 유효성을 검증 :

  if (jwtUtil.validateToken(jwt, userDetails.getUsername())) {
     ...
  }

 
UserNamePasswordAuthenticationToken을 생성 및 spring security 보안 컨택스트에 설정 

UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);

 
필터체인의 다음 필터로 요청과 응답

chain.doFilter(request, response);

 

JWT 검증 및 사용

서버 측에서는 받은 JWT의 서명을 검증하고, 페이로드의 클레임을 사용하여 사용자 인증이나 권한 부여 등을 수행합니다.
예시 고객정보를 가져온다고 가정합니다.
 
고객  정보 api :

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.http.ResponseEntity;

@RestController
@RequestMapping("/user")
public class UsersController {

    @PostMapping("/auth/createtoken")
    public ResponseEntity<Users> savetoken(@Validated @RequestBody UsersDTO usersDTO){
        // usersService.save(usersDTO)
        return ResponseEntity.ok(  authApplication.signup(usersDTO) );
    }
    @GetMapping("/auth/securitytest")
    public ResponseEntity<?> securityTest(Authentication authentication){
        String getName = authentication.getName();
        return ResponseEntity.ok(getName + " Authentication에서 인증인 확인되었습니다");
    }

    @GetMapping("/auth/createtoken")
    public ResponseEntity<String> getToken(@Validated @RequestBody UsersDTO usersDTO){
        // usersService.save(usersDTO)
        try {
            String jwtToken = authApplication.signin(usersDTO);
            if (jwtToken != null) {
                return ResponseEntity.ok(jwtToken);
            } else {
                return ResponseEntity.badRequest().build();
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

 

보안 고려사항

  • 암호화된 연결: JWT를 전송할 때는 HTTPS와 같은 암호화된 연결을 사용하는 것이 중요합니다.
  • 서명 비밀키 보안: 서명에 사용되는 비밀키는 안전하게 보관해야 합니다.
  • 유효기간 설정: 토큰의 유효 기간을 짧게 설정하여 보안 위험을 줄일 수 있습니다.

결과화면

 

+ Recent posts