Skip to content

Commit ff82a89

Browse files
authored
feat: implement JWT utility class with configurable properties and token management (#293)
1 parent c79478a commit ff82a89

9 files changed

Lines changed: 224 additions & 22 deletions

File tree

bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/BigtopManagerServer.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,17 @@
1818
*/
1919
package org.apache.bigtop.manager.server;
2020

21+
import org.apache.bigtop.manager.server.config.JwtProperties;
22+
2123
import org.springframework.boot.SpringApplication;
2224
import org.springframework.boot.autoconfigure.SpringBootApplication;
25+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
2326
import org.springframework.scheduling.annotation.EnableAsync;
2427
import org.springframework.scheduling.annotation.EnableScheduling;
2528

2629
@EnableAsync
2730
@EnableScheduling
31+
@EnableConfigurationProperties(JwtProperties.class)
2832
@SpringBootApplication(
2933
scanBasePackages = {
3034
"org.apache.bigtop.manager.server",
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.bigtop.manager.server.config;
20+
21+
import org.springframework.boot.context.properties.ConfigurationProperties;
22+
23+
import lombok.Data;
24+
25+
@Data
26+
@ConfigurationProperties(prefix = "bigtop-manager.security.jwt")
27+
public class JwtProperties {
28+
29+
/**
30+
* JWT signing secret.
31+
* <p>
32+
* Must be set in production (e.g. via env var) to prevent forged tokens.
33+
*/
34+
private String secret;
35+
36+
/** Issuer to embed and to require during verification. */
37+
private String issuer = "bigtop-manager";
38+
39+
/** Audience to embed and to require during verification. */
40+
private String audience = "bigtop-manager";
41+
42+
/** Token validity period in days. */
43+
private int expirationDays = 7;
44+
45+
/**
46+
* Whether to allow a built-in dev secret as fallback.
47+
* <p>
48+
* Keep this false in production.
49+
*/
50+
private boolean allowDefaultSecret = false;
51+
}

bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/interceptor/AuthInterceptor.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ public class AuthInterceptor implements HandlerInterceptor {
4848
@Autowired
4949
private UserService userService;
5050

51+
@Autowired
52+
private JWTUtils jwtUtils;
53+
5154
private ResponseEntity<?> responseEntity;
5255

5356
@Override
@@ -89,7 +92,7 @@ private Boolean checkLogin(HttpServletRequest request) {
8992
}
9093

9194
try {
92-
DecodedJWT decodedJWT = JWTUtils.resolveToken(token);
95+
DecodedJWT decodedJWT = jwtUtils.resolveToken(token);
9396
Long userId = decodedJWT.getClaim(JWTUtils.CLAIM_ID).asLong();
9497
Integer tokenVersion =
9598
decodedJWT.getClaim(JWTUtils.CLAIM_TOKEN_VERSION).asInt();

bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/LoginServiceImpl.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ public class LoginServiceImpl implements LoginService {
4343
@Resource
4444
private UserDao userDao;
4545

46+
@Resource
47+
private JWTUtils jwtUtils;
48+
4649
@Override
4750
public LoginVO login(LoginDTO loginDTO) {
4851
String username = loginDTO.getUsername();
@@ -75,7 +78,7 @@ public LoginVO login(LoginDTO loginDTO) {
7578
CacheUtils.setCache(
7679
Caches.CACHE_USER, user.getId().toString(), userVO, Caches.USER_EXPIRE_TIME_DAYS, TimeUnit.DAYS);
7780

78-
String token = JWTUtils.generateToken(user.getId(), user.getUsername(), user.getTokenVersion());
81+
String token = jwtUtils.generateToken(user.getId(), user.getUsername(), user.getTokenVersion());
7982
LoginVO loginVO = new LoginVO();
8083
loginVO.setToken(token);
8184
return loginVO;

bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/utils/JWTUtils.java

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,21 @@
1818
*/
1919
package org.apache.bigtop.manager.server.utils;
2020

21+
import org.apache.bigtop.manager.server.config.JwtProperties;
22+
23+
import org.springframework.stereotype.Component;
24+
2125
import com.auth0.jwt.JWT;
26+
import com.auth0.jwt.JWTVerifier;
2227
import com.auth0.jwt.algorithms.Algorithm;
28+
import com.auth0.jwt.exceptions.JWTVerificationException;
2329
import com.auth0.jwt.interfaces.DecodedJWT;
2430

2531
import java.time.Instant;
2632
import java.time.temporal.ChronoUnit;
33+
import java.util.Date;
2734

35+
@Component
2836
public class JWTUtils {
2937

3038
public static final String CLAIM_ID = "id";
@@ -33,23 +41,69 @@ public class JWTUtils {
3341

3442
public static final String CLAIM_TOKEN_VERSION = "token_version";
3543

36-
protected static final String SIGN = "r0PGVyvjKOxUBwGt";
44+
/**
45+
* Dev-only fallback secret to preserve local boot for contributors.
46+
* <p>
47+
* In production, configure `bigtop-manager.security.jwt.secret`.
48+
*/
49+
static final String DEFAULT_DEV_SECRET = "r0PGVyvjKOxUBwGt";
50+
51+
private final JwtProperties jwtProperties;
3752

38-
// Token validity period (days)
39-
private static final int TOKEN_EXPIRATION_DAYS = 7;
53+
public JWTUtils(JwtProperties jwtProperties) {
54+
this.jwtProperties = jwtProperties;
55+
}
4056

41-
public static String generateToken(Long userId, String username, Integer tokenVersion) {
42-
Instant expireTime = Instant.now().plus(TOKEN_EXPIRATION_DAYS, ChronoUnit.DAYS);
57+
public String generateToken(Long userId, String username, Integer tokenVersion) {
58+
Instant now = Instant.now();
59+
Instant expireTime = now.plus(jwtProperties.getExpirationDays(), ChronoUnit.DAYS);
4360

4461
return JWT.create()
62+
.withIssuer(jwtProperties.getIssuer())
63+
.withAudience(jwtProperties.getAudience())
64+
.withIssuedAt(Date.from(now))
4565
.withClaim(CLAIM_ID, userId)
4666
.withClaim(CLAIM_USERNAME, username)
4767
.withClaim(CLAIM_TOKEN_VERSION, tokenVersion)
48-
.withExpiresAt(expireTime)
49-
.sign(Algorithm.HMAC256(SIGN));
68+
.withExpiresAt(Date.from(expireTime))
69+
.sign(Algorithm.HMAC256(getSigningSecret()));
70+
}
71+
72+
public DecodedJWT resolveToken(String token) throws JWTVerificationException {
73+
Algorithm algorithm = Algorithm.HMAC256(getSigningSecret());
74+
JWTVerifier verifier = JWT.require(algorithm)
75+
.withIssuer(jwtProperties.getIssuer())
76+
.withAudience(jwtProperties.getAudience())
77+
.build();
78+
79+
DecodedJWT decodedJWT = verifier.verify(token);
80+
81+
// Enforce issued-at to mitigate tokens without freshness metadata.
82+
Date issuedAt = decodedJWT.getIssuedAt();
83+
if (issuedAt == null) {
84+
throw new JWTVerificationException("Missing iat");
85+
}
86+
87+
// Reject tokens issued too far in the future (clock skew).
88+
Instant now = Instant.now();
89+
if (issuedAt.toInstant().isAfter(now.plus(5, ChronoUnit.MINUTES))) {
90+
throw new JWTVerificationException("iat is in the future");
91+
}
92+
93+
return decodedJWT;
5094
}
5195

52-
public static DecodedJWT resolveToken(String token) {
53-
return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
96+
private String getSigningSecret() {
97+
String secret = jwtProperties.getSecret();
98+
if (secret != null && !secret.isBlank()) {
99+
return secret;
100+
}
101+
102+
if (jwtProperties.isAllowDefaultSecret()) {
103+
return DEFAULT_DEV_SECRET;
104+
}
105+
106+
throw new IllegalStateException(
107+
"JWT secret is not configured. Please set bigtop-manager.security.jwt.secret (or enable allowDefaultSecret for dev only).");
54108
}
55109
}

bigtop-manager-server/src/main/resources/application.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,15 @@ springdoc:
6767
pagehelper:
6868
reasonable: false
6969
params: count=countSql
70-
support-methods-arguments: true
70+
support-methods-arguments: true
71+
72+
bigtop-manager:
73+
security:
74+
jwt:
75+
# IMPORTANT: Set a strong, random secret in production (e.g. via env var BIGTOP_MANAGER_JWT_SECRET)
76+
secret: ${BIGTOP_MANAGER_JWT_SECRET:}
77+
issuer: bigtop-manager
78+
audience: bigtop-manager
79+
expiration-days: 7
80+
# Dev-only compatibility switch. Keep false in production.
81+
allow-default-secret: false

bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/service/LoginServiceTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.apache.bigtop.manager.server.model.dto.LoginDTO;
2828
import org.apache.bigtop.manager.server.service.impl.LoginServiceImpl;
2929
import org.apache.bigtop.manager.server.utils.CacheUtils;
30+
import org.apache.bigtop.manager.server.utils.JWTUtils;
3031
import org.apache.bigtop.manager.server.utils.PasswordUtils;
3132
import org.apache.bigtop.manager.server.utils.Pbkdf2Utils;
3233

@@ -51,6 +52,9 @@ public class LoginServiceTest {
5152
@Mock
5253
private UserDao userDao;
5354

55+
@Mock
56+
private JWTUtils jwtUtils;
57+
5458
@InjectMocks
5559
private LoginService loginService = new LoginServiceImpl();
5660

@@ -107,6 +111,7 @@ public void testLogin_WhenValidCredentials_ShouldReturnToken() {
107111
LoginDTO loginDTO = createLoginDTO(RAW_PASSWORD);
108112

109113
when(userDao.findByUsername(any())).thenReturn(mockUser);
114+
when(jwtUtils.generateToken(any(), any(), any())).thenReturn("test-token");
110115

111116
Object result = loginService.login(loginDTO);
112117
assertNotNull(result);

bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/utils/JWTUtilsTest.java

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
*/
1919
package org.apache.bigtop.manager.server.utils;
2020

21+
import org.apache.bigtop.manager.server.config.JwtProperties;
22+
2123
import org.junit.jupiter.api.Test;
2224

2325
import com.auth0.jwt.JWT;
@@ -35,15 +37,26 @@
3537

3638
public class JWTUtilsTest {
3739

40+
private static JWTUtils newJwtUtilsWithDevSecretAllowed() {
41+
JwtProperties props = new JwtProperties();
42+
props.setAllowDefaultSecret(true);
43+
props.setIssuer("bigtop-manager");
44+
props.setAudience("bigtop-manager");
45+
props.setExpirationDays(7);
46+
return new JWTUtils(props);
47+
}
48+
3849
@Test
3950
public void testGenerateTokenNormal() {
51+
JWTUtils jwtUtils = newJwtUtilsWithDevSecretAllowed();
52+
4053
Long id = 1L;
4154
String username = "testUser";
4255
Integer tokenVersion = 1;
43-
String token = JWTUtils.generateToken(id, username, tokenVersion);
56+
String token = jwtUtils.generateToken(id, username, tokenVersion);
4457
assertNotNull(token);
4558

46-
DecodedJWT decodedJWT = JWTUtils.resolveToken(token);
59+
DecodedJWT decodedJWT = jwtUtils.resolveToken(token);
4760
assertEquals(id, decodedJWT.getClaim(JWTUtils.CLAIM_ID).asLong());
4861
assertEquals(username, decodedJWT.getClaim(JWTUtils.CLAIM_USERNAME).asString());
4962
assertEquals(
@@ -52,40 +65,76 @@ public void testGenerateTokenNormal() {
5265

5366
@Test
5467
public void testResolveTokenExpired() {
68+
JWTUtils jwtUtils = newJwtUtilsWithDevSecretAllowed();
69+
5570
Long id = 2L;
5671
String username = "expiredUser";
5772
Calendar calendar = Calendar.getInstance();
5873
calendar.add(Calendar.HOUR_OF_DAY, -1);
5974
Date date = calendar.getTime();
6075

6176
String token = JWT.create()
77+
.withIssuer("bigtop-manager")
78+
.withAudience("bigtop-manager")
79+
.withIssuedAt(new Date())
6280
.withClaim(JWTUtils.CLAIM_ID, id)
6381
.withClaim(JWTUtils.CLAIM_USERNAME, username)
6482
.withExpiresAt(date)
65-
.sign(Algorithm.HMAC256(JWTUtils.SIGN));
83+
.sign(Algorithm.HMAC256(JWTUtils.DEFAULT_DEV_SECRET));
6684

67-
assertThrows(JWTVerificationException.class, () -> JWTUtils.resolveToken(token));
85+
assertThrows(JWTVerificationException.class, () -> jwtUtils.resolveToken(token));
6886
}
6987

7088
@Test
7189
public void testResolveTokenIllegal() {
90+
JWTUtils jwtUtils = newJwtUtilsWithDevSecretAllowed();
91+
7292
String illegalToken =
7393
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
74-
assertThrows(JWTVerificationException.class, () -> JWTUtils.resolveToken(illegalToken));
94+
assertThrows(JWTVerificationException.class, () -> jwtUtils.resolveToken(illegalToken));
7595
}
7696

7797
@Test
7898
public void testResolveTokenWrongFormat() {
99+
JWTUtils jwtUtils = newJwtUtilsWithDevSecretAllowed();
100+
79101
String wrongFormatToken = "wrong_format_token";
80-
assertThrows(JWTDecodeException.class, () -> JWTUtils.resolveToken(wrongFormatToken));
102+
assertThrows(JWTDecodeException.class, () -> jwtUtils.resolveToken(wrongFormatToken));
81103
}
82104

83105
@Test
84106
public void testGenerateTokenUsernameEmpty() {
85-
String token = JWTUtils.generateToken(1L, "", 1);
107+
JWTUtils jwtUtils = newJwtUtilsWithDevSecretAllowed();
108+
109+
String token = jwtUtils.generateToken(1L, "", 1);
86110
assertNotNull(token);
87111

88-
DecodedJWT decodedJWT = JWTUtils.resolveToken(token);
112+
DecodedJWT decodedJWT = jwtUtils.resolveToken(token);
89113
assertEquals("", decodedJWT.getClaim(JWTUtils.CLAIM_USERNAME).asString());
90114
}
115+
116+
@Test
117+
public void testResolveTokenMissingIatRejected() {
118+
JWTUtils jwtUtils = newJwtUtilsWithDevSecretAllowed();
119+
120+
String token = JWT.create()
121+
.withIssuer("bigtop-manager")
122+
.withAudience("bigtop-manager")
123+
.withClaim(JWTUtils.CLAIM_ID, 1L)
124+
.withClaim(JWTUtils.CLAIM_TOKEN_VERSION, 1)
125+
.withExpiresAt(new Date(System.currentTimeMillis() + 60_000))
126+
// intentionally no iat
127+
.sign(Algorithm.HMAC256(JWTUtils.DEFAULT_DEV_SECRET));
128+
129+
assertThrows(JWTVerificationException.class, () -> jwtUtils.resolveToken(token));
130+
}
131+
132+
@Test
133+
public void testSecretRequiredByDefault() {
134+
JwtProperties props = new JwtProperties();
135+
props.setAllowDefaultSecret(false);
136+
JWTUtils jwtUtils = new JWTUtils(props);
137+
138+
assertThrows(IllegalStateException.class, () -> jwtUtils.generateToken(1L, "u", 1));
139+
}
91140
}

0 commit comments

Comments
 (0)