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;
}
}