Просмотр исходного кода

refining the backend jwt session

reset-do-picking-order
Fai Luk 1 неделю назад
Родитель
Сommit
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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.Scope;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
@@ -30,13 +31,14 @@ public class JwtTokenUtil implements Serializable {


private static final long serialVersionUID = -2550185165626007488L; 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 AES_SECRET = "ffii";
public static final String TOKEN_SEPARATOR = "@@"; 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); 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) // 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 // compaction of the JWT to a URL-safe string
private String doGenerateToken(Map<String, Object> claims, String subject) { 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())) 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(); .signWith(secretKey).compact();
} }


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


public RefreshToken createRefreshToken(String username) { public RefreshToken createRefreshToken(String username) {
long refreshExpirationMs = (long) refreshExpirationDays * 24 * 60 * 60 * 1000;
RefreshToken refreshToken = new RefreshToken(); RefreshToken refreshToken = new RefreshToken();
refreshToken.setUserName(username); 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)); refreshToken.setToken(AES.encrypt(username + TOKEN_SEPARATOR + instantNum, AES_SECRET));
return refreshToken; 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 com.ffii.fpsms.config.security.jwt.JwtRequestFilter;


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

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


@@ -79,10 +84,19 @@ public class SecurityConfig {
.requestMatchers(org.springframework.http.HttpMethod.OPTIONS, "/**").permitAll() .requestMatchers(org.springframework.http.HttpMethod.OPTIONS, "/**").permitAll()
.anyRequest().authenticated()) .anyRequest().authenticated())
.httpBasic(httpBasic -> httpBasic.authenticationEntryPoint( .httpBasic(httpBasic -> httpBasic.authenticationEntryPoint(
(request, response, authException) -> response.sendError(HttpStatus.UNAUTHORIZED.value())))
(request, response, authException) -> sendUnauthorizedJson(response, "Unauthorized", "UNAUTHORIZED")))
.sessionManagement( .sessionManagement(
sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class) .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
.build(); .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; package com.ffii.fpsms.config.security.jwt;


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


import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails; 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 com.ffii.fpsms.config.security.jwt.service.JwtUserDetailsService;


import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
@@ -42,10 +45,14 @@ public class JwtRequestFilter extends OncePerRequestFilter {
jwtToken = requestTokenHeader.substring(7).replaceAll("\"", ""); jwtToken = requestTokenHeader.substring(7).replaceAll("\"", "");
try { try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken); username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) {
logger.error("Unable to get JWT Token");
} catch (ExpiredJwtException e) { } catch (ExpiredJwtException e) {
logger.error("JWT Token has expired"); 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 { } else {
logger.warn("JWT Token does not begin with Bearer String"); logger.warn("JWT Token does not begin with Bearer String");
@@ -72,4 +79,14 @@ public class JwtRequestFilter extends OncePerRequestFilter {
chain.doFilter(request, response); 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: spring:
datasource: datasource:
jdbc-url: jdbc:mysql://127.0.0.1:3306/fpsmsdb?useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT%2B8 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: dialect:
storage_engine: innodb 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: logging:
config: 'classpath:log4j2.yml' config: 'classpath:log4j2.yml'




Загрузка…
Отмена
Сохранить