Project TroubleShooting _ Could not resolve placeholder 'jwt.secret' in value "${jwt.secret}" + JWT Complete Setup

#Foreword

In conclusion, Solution for this issue below was really simple.

I've tried to implement JWT, but Jwt Secret Key caused errors. Unlike my expectation, It's very simple..... I misplaced the line of configuration.

It took me almost a day, and I've never imagined this is the cause for this..........................

So, to remind myself of this, I will make a record for my future reference.

#Error Message With Simple Solution

Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'jwt.secret' in value "${jwt.secret}"

misplaced......................it took me almost a day......................


#3 approaches for Jwt Secret Key

+ I couldn't figure out what's the cause. To resolve that issues, I've tried approaches below. Those approaches are valid for JWT, but this problem was from application.yml misconfiguration by misplacement.

1. Generating a Secret Key : I used this.

: generate a strong secret key using various methods. Here are a few options:

1. Using an Online Generator and Use it on application.yml : There are online tools available to generate secure keys. For example, RandomKeygen to generate a secure key. Select a key of sufficient length and complexity, such as a 256-bit key. (refer to image below)

2.Using OpenSSL:

If you have OpenSSL installed, you can generate a secure key using the following command:

: reference Url https://developers.yubico.com/PIV/Guides/Generating_keys_using_OpenSSL.html

3.Using Java Code

: to generate a secure key:

javaCopy codeimport java.security.SecureRandom;
import java.util.Base64;

public class SecretKeyGenerator {
    public static void main(String[] args) {
        SecureRandom secureRandom = new SecureRandom();
        byte[] key = new byte[32]; // 256 bits
        secureRandom.nextBytes(key);
        String encodedKey = Base64.getEncoder().encodeToString(key);
        System.out.println("Generated Key: " + encodedKey);
    }
}

#JWT Complete Setup

(application.yml + JWT code)

Here's application.yml and JwtTokenProvider class might look:

build.gradle dependencies


dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.mysql:mysql-connector-j'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

    // Security
    implementation 'org.springframework.boot:spring-boot-starter-security'
    // jwt
    implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
    // Send Email Verification Code
    implementation 'org.springframework.boot:spring-boot-starter-mail'
    // .env
    implementation 'io.github.cdimascio:java-dotenv:+'
}

application.yml:

spring:
  mail:
    username: ${EMAIL_ID}
    password: ${EMAIL_PASSWORD}
    sender-email: ${SENDER_EMAIL}
  #.env 파일 인식 설정 추가
  config:
    import: optional:file:.env[.properties]

  datasource:
    url: ${DB_URL}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    database-platform: org.hibernate.dialect.MySQLDialect
    hibernate:
      ddl-auto: create
      show-sql: true

jwt:
  secret: c2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQtc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQK
  token-validity-in-seconds: 86400

cookie:
  domain: localhost

#spring:
  servlet:
    multipart:
      max-file-size: 16MB  # Set desired maximum file size
      max-request-size: 16MB  # Set desired maximum request size

server:
  tomcat:
    max-swallow-size: 16MB  # This sets the maximum size, Tomcat will swallow

JwtTokenProvider.java Code :


import com.FC.SharedOfficePlatform.domain.member.entity.Member;
import com.FC.SharedOfficePlatform.global.security.CustomMemberDetails;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import java.security.Key;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class JwtProvider {

    private static final String AUTHORITIES_KEY = "auth";
    private final String secret;
    private final long tokenValidityInMilliseconds;
    private Key key;

    public JwtProvider(
        @Value("${jwt.secret}") String secret,
        @Value("${jwt.token-validity-in-seconds}") long tokenValidityInSeconds
    ) {
        this.secret = secret;
        this.tokenValidityInMilliseconds = tokenValidityInSeconds * 1000;
    }

    @PostConstruct
    public void postConstruct() {
        byte[] keyBytes = Decoders.BASE64.decode(secret);
        this.key = Keys.hmacShaKeyFor(keyBytes);
    }

    public String createToken(Member member) {
        String authorities = member.getRole().name();

        long now = (new Date()).getTime();
        Date validity = new Date(now + this.tokenValidityInMilliseconds);

        return Jwts.builder()
            .setSubject(String.valueOf(member.getId()))
            .claim(AUTHORITIES_KEY, authorities)
            .claim("email", member.getEmail())
            .signWith(key, SignatureAlgorithm.HS512)
            .setExpiration(validity)
            .compact();
    }
    public Authentication getAuthentication(String token) {
        Claims claims = Jwts
            .parserBuilder()
            .setSigningKey(key)
            .build()
            .parseClaimsJws(token)
            .getBody();

        Collection<? extends GrantedAuthority> authorities =
            Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());

        CustomMemberDetails userDetails = CustomMemberDetails.builder()
            .id(Long.parseLong(claims.getSubject()))
            .email(claims.get("email").toString())
            .password("") // Password is not included in the token for security reasons
            .authorities(authorities)
            .build();

        /*
        Above(userDetails) can be replaced with below line of code(userDetails) With MemberDetails DI
        MemberDetails userDetails = memberDetailsService.loadUserById(Long.parseLong(claims.getSubject()));
         */

        return new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            log.info("잘못된 JWT 서명입니다.");
        } catch (ExpiredJwtException e) {
            log.info("만료된 JWT 토큰입니다.");
        } catch (UnsupportedJwtException e) {
            log.info("지원되지 않는 JWT 토큰입니다.");
        } catch (IllegalArgumentException e) {
            log.info("JWT 토큰이 잘못되었습니다.");
        }
        return false;
    }
}
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.GenericFilterBean;

@Component
@Slf4j
@RequiredArgsConstructor
public class JwtFilter extends GenericFilterBean {

    private static final String ACCESS_TOKEN_COOKIE_NAME = "accessToken";

    private final JwtProvider jwtProvider;

    @Override
    public void doFilter(
        ServletRequest request, ServletResponse response, FilterChain chain
    ) throws IOException, ServletException {

        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String requestURI = httpServletRequest.getRequestURI();
        String accessToken = resolveToken(httpServletRequest);
        if (accessToken == null) {
            chain.doFilter(request, response);
            return;
        }
        // If the token is valid, it retrieves the authentication object and sets it in the SecurityContext.
        if (StringUtils.hasText(accessToken) && jwtProvider.validateToken(accessToken)) {
            Authentication authentication = jwtProvider.getAuthentication(accessToken);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            log.info(
                "Security Context에 'memberId: {}' 인증 정보를 저장했습니다, uri: {}",
                authentication.getName(), requestURI
            );
        } else {
            log.info("유효한 JWT 토큰이 없습니다, uri: {}", requestURI);
        }
        chain.doFilter(request, response);
    }

    private String resolveToken(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        if (cookies == null) return null;

        //iterates through them to find the cookie named accessToken, Returns the value of the accessToken cookie if found.
        for (Cookie cookie : cookies) {
            if (ACCESS_TOKEN_COOKIE_NAME.equals(cookie.getName())) {
                return cookie.getValue();
            }
        }
        return null;
    }
}

Using jwtFilter on SecurityConfig

AuthService For Login