ソースを参照

refining the backend jwt session

reset-do-picking-order
Fai Luk 1週間前
コミット
2e6cd6a5ff
5個のファイルの変更56行の追加11行の削除
  1. +12
    -8
      src/main/java/com/ffii/core/utils/JwtTokenUtil.java
  2. +15
    -1
      src/main/java/com/ffii/fpsms/config/security/SecurityConfig.java
  3. +19
    -2
      src/main/java/com/ffii/fpsms/config/security/jwt/JwtRequestFilter.java
  4. +5
    -0
      src/main/resources/application-prod.yml
  5. +5
    -0
      src/main/resources/application.yml

+ 12
- 8
src/main/java/com/ffii/core/utils/JwtTokenUtil.java ファイルの表示

@@ -10,6 +10,7 @@ import java.util.function.Function;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.security.core.userdetails.UserDetails;
@@ -30,13 +31,14 @@ public class JwtTokenUtil implements Serializable {

private static final long serialVersionUID = -2550185165626007488L;

// * 60000 = 1 Min
public static final long JWT_TOKEN_EXPIRED_TIME = 60000 * 14400;
public static final String AES_SECRET = "ffii";
public static final String TOKEN_SEPARATOR = "@@";

// @Value("${jwt.secret}")
// private String secret;
@Value("${jwt.expiration-minutes:14400}")
private long expirationMinutes = 14400;

@Value("${jwt.refresh-expiration-days:30}")
private int refreshExpirationDays = 30;

private static final Key secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS512);

@@ -79,9 +81,10 @@ public class JwtTokenUtil implements Serializable {
// Serialization(https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1)
// compaction of the JWT to a URL-safe string
private String doGenerateToken(Map<String, Object> claims, String subject) {
logger.info((new Date(System.currentTimeMillis() + JWT_TOKEN_EXPIRED_TIME)).toString());
long expirationMs = expirationMinutes * 60 * 1000;
logger.info((new Date(System.currentTimeMillis() + expirationMs)).toString());
return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_EXPIRED_TIME))
.setExpiration(new Date(System.currentTimeMillis() + expirationMs))
.signWith(secretKey).compact();
}

@@ -92,10 +95,11 @@ public class JwtTokenUtil implements Serializable {
}

public RefreshToken createRefreshToken(String username) {
long refreshExpirationMs = (long) refreshExpirationDays * 24 * 60 * 60 * 1000;
RefreshToken refreshToken = new RefreshToken();
refreshToken.setUserName(username);
refreshToken.setExpiryDate(Instant.now().plusMillis(JWT_TOKEN_EXPIRED_TIME * 60 * 24));
long instantNum = Instant.now().plusMillis(JWT_TOKEN_EXPIRED_TIME * 60 * 24).toEpochMilli();
refreshToken.setExpiryDate(Instant.now().plusMillis(refreshExpirationMs));
long instantNum = Instant.now().plusMillis(refreshExpirationMs).toEpochMilli();
refreshToken.setToken(AES.encrypt(username + TOKEN_SEPARATOR + instantNum, AES_SECRET));
return refreshToken;
}


+ 15
- 1
src/main/java/com/ffii/fpsms/config/security/SecurityConfig.java ファイルの表示

@@ -23,6 +23,10 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic

import com.ffii.fpsms.config.security.jwt.JwtRequestFilter;

import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@@ -36,6 +40,7 @@ public class SecurityConfig {
INDEX_URL,
LOGIN_URL,
LDAP_LOGIN_URL,
"/refresh-token",
"/py/**"
};

@@ -79,10 +84,19 @@ public class SecurityConfig {
.requestMatchers(org.springframework.http.HttpMethod.OPTIONS, "/**").permitAll()
.anyRequest().authenticated())
.httpBasic(httpBasic -> httpBasic.authenticationEntryPoint(
(request, response, authException) -> response.sendError(HttpStatus.UNAUTHORIZED.value())))
(request, response, authException) -> sendUnauthorizedJson(response, "Unauthorized", "UNAUTHORIZED")))
.sessionManagement(
sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}

/** Send 401 with JSON body so frontend can consistently handle session timeout / missing token. */
private static void sendUnauthorizedJson(HttpServletResponse response, String message, String code) throws IOException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
String body = String.format("{\"message\":\"%s\",\"code\":\"%s\"}", message.replace("\"", "\\\""), code);
response.getWriter().write(body);
}
}

+ 19
- 2
src/main/java/com/ffii/fpsms/config/security/jwt/JwtRequestFilter.java ファイルの表示

@@ -1,8 +1,10 @@
package com.ffii.fpsms.config.security.jwt;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
@@ -14,6 +16,7 @@ import com.ffii.core.utils.JwtTokenUtil;
import com.ffii.fpsms.config.security.jwt.service.JwtUserDetailsService;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
@@ -42,10 +45,14 @@ public class JwtRequestFilter extends OncePerRequestFilter {
jwtToken = requestTokenHeader.substring(7).replaceAll("\"", "");
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) {
logger.error("Unable to get JWT Token");
} catch (ExpiredJwtException e) {
logger.error("JWT Token has expired");
sendUnauthorizedJson(response, "JWT Token has expired", "TOKEN_EXPIRED");
return;
} catch (JwtException | IllegalArgumentException e) {
logger.error("Invalid JWT Token: {}", e.getMessage());
sendUnauthorizedJson(response, "Invalid JWT Token", "TOKEN_INVALID");
return;
}
} else {
logger.warn("JWT Token does not begin with Bearer String");
@@ -72,4 +79,14 @@ public class JwtRequestFilter extends OncePerRequestFilter {
chain.doFilter(request, response);
}

/** Send 401 with JSON body so frontend can detect session/timeout and redirect to login or refresh. */
private void sendUnauthorizedJson(HttpServletResponse response, String message, String code) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
String body = String.format("{\"message\":\"%s\",\"code\":\"%s\"}",
message.replace("\"", "\\\""), code);
response.getWriter().write(body);
}

}

+ 5
- 0
src/main/resources/application-prod.yml ファイルの表示

@@ -1,3 +1,8 @@
# Shorter session in production; frontend should call /refresh-token or re-login.
jwt:
expiration-minutes: 30 # 30 min access token
refresh-expiration-days: 7

spring:
datasource:
jdbc-url: jdbc:mysql://127.0.0.1:3306/fpsmsdb?useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT%2B8


+ 5
- 0
src/main/resources/application.yml ファイルの表示

@@ -31,6 +31,11 @@ spring:
dialect:
storage_engine: innodb

# JWT: access token expiry and refresh token expiry. Frontend should call /refresh-token before access token expires.
jwt:
expiration-minutes: 14400 # access token: 10 days (default); override in application-prod for shorter session
refresh-expiration-days: 30 # refresh token validity (days)

logging:
config: 'classpath:log4j2.yml'



読み込み中…
キャンセル
保存