Skip to content

Commit 39034db

Browse files
committed
[Improve] Improve login authentication
1 parent 81b8161 commit 39034db

File tree

10 files changed

+279
-135
lines changed

10 files changed

+279
-135
lines changed

streampark-common/src/main/scala/org/apache/streampark/common/util/FileUtils.scala

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ package org.apache.streampark.common.util
1818

1919
import java.io._
2020
import java.net.URL
21+
import java.nio.ByteBuffer
22+
import java.nio.channels.Channels
23+
import java.nio.charset.StandardCharsets
24+
import java.nio.file.Files
2125
import java.util
2226
import java.util.Scanner
2327

@@ -153,6 +157,44 @@ object FileUtils {
153157
}
154158
}
155159

160+
@throws[IOException]
161+
def readFile(file: File): String = {
162+
if (file.length >= Int.MaxValue) {
163+
throw new IOException("Too large file, unexpected!")
164+
} else {
165+
val len = file.length
166+
val array = new Array[Byte](len.toInt)
167+
val is = Files.newInputStream(file.toPath)
168+
readInputStream(is, array)
169+
val content = new String(array, StandardCharsets.UTF_8)
170+
Utils.close(is)
171+
content
172+
}
173+
}
174+
175+
@throws[IOException]
176+
def readInputStream(in: InputStream, array: Array[Byte]): Unit = {
177+
var toRead = array.length
178+
var ret = 0
179+
var off = 0
180+
while (toRead > 0) {
181+
ret = in.read(array, off, toRead)
182+
if (ret < 0) throw new IOException("Bad inputStream, premature EOF")
183+
toRead -= ret
184+
off += ret
185+
}
186+
Utils.close(in)
187+
}
188+
189+
@throws[IOException]
190+
def writeFile(content: String, file: File): Unit = {
191+
val outputStream = Files.newOutputStream(file.toPath)
192+
val channel = Channels.newChannel(outputStream)
193+
val buffer = ByteBuffer.wrap(content.getBytes(StandardCharsets.UTF_8))
194+
channel.write(buffer)
195+
Utils.close(channel, outputStream)
196+
}
197+
156198
@throws[IOException]
157199
def readString(file: File): String = {
158200
require(file != null && file.isFile)

streampark-console/streampark-console-service/src/main/assembly/bin/mvnw

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/base/util/EncryptUtils.java

Lines changed: 0 additions & 73 deletions
This file was deleted.

streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/system/authentication/JWTFilter.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717

1818
package org.apache.streampark.console.system.authentication;
1919

20-
import org.apache.streampark.console.base.util.EncryptUtils;
21-
2220
import org.apache.shiro.authz.UnauthorizedException;
2321
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
2422

@@ -58,7 +56,7 @@ protected boolean executeLogin(ServletRequest request, ServletResponse response)
5856
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
5957
String token = httpServletRequest.getHeader(TOKEN);
6058
try {
61-
token = EncryptUtils.decrypt(token);
59+
token = JWTUtil.decrypt(token);
6260
JWTToken jwtToken = new JWTToken(token);
6361
getSubject(request, response).login(jwtToken);
6462
return true;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.streampark.console.system.authentication;
19+
20+
import org.apache.streampark.common.util.FileUtils;
21+
22+
import lombok.extern.slf4j.Slf4j;
23+
24+
import java.io.File;
25+
import java.io.IOException;
26+
import java.nio.charset.StandardCharsets;
27+
import java.nio.file.Files;
28+
import java.nio.file.Path;
29+
import java.nio.file.Paths;
30+
import java.nio.file.StandardCopyOption;
31+
import java.nio.file.attribute.PosixFilePermissions;
32+
import java.security.SecureRandom;
33+
import java.util.Base64;
34+
35+
@Slf4j
36+
public class JWTSecret {
37+
38+
private static final int KEY_LENGTH = 32;
39+
40+
public static byte[] getJWTSecret() {
41+
Path keyPath = Paths.get(System.getProperty("user.home"), "streampark.jwt.key");
42+
File keyFile = keyPath.toFile();
43+
44+
// Try to load existing key
45+
byte[] keyBytes = loadExistingKey(keyFile);
46+
if (keyBytes != null) {
47+
return keyBytes;
48+
}
49+
50+
// Generate new key
51+
keyBytes = generateNewKey();
52+
saveNewKey(keyBytes, keyPath);
53+
return keyBytes;
54+
}
55+
56+
private static byte[] loadExistingKey(File keyFile) {
57+
if (!keyFile.exists()) {
58+
return null;
59+
}
60+
61+
try {
62+
String secret = FileUtils.readFile(keyFile).trim();
63+
byte[] keyBytes = Base64.getDecoder().decode(secret);
64+
65+
if (keyBytes.length != KEY_LENGTH) {
66+
log.error(
67+
"Invalid HMAC key length: {} bytes (expected {} bytes)", keyBytes.length, KEY_LENGTH);
68+
return null;
69+
}
70+
return keyBytes;
71+
} catch (Exception e) {
72+
log.error("Failed to read JWT key file", e);
73+
}
74+
// Clean up invalid file
75+
safelyDeleteFile(keyFile);
76+
return null;
77+
}
78+
79+
private static byte[] generateNewKey() {
80+
byte[] key = new byte[KEY_LENGTH];
81+
new SecureRandom().nextBytes(key);
82+
return key;
83+
}
84+
85+
private static void saveNewKey(byte[] keyBytes, Path keyPath) {
86+
String encodedKey = Base64.getEncoder().encodeToString(keyBytes);
87+
try {
88+
// Ensure the directory exists
89+
Files.createDirectories(keyPath.getParent());
90+
// Safely write to a temporary file before renaming
91+
Path tempFile = Files.createTempFile(keyPath.getParent(), "streampark", ".tmp");
92+
Files.write(tempFile, encodedKey.getBytes(StandardCharsets.UTF_8));
93+
94+
// Atomically move after setting permissions
95+
setStrictPermissions(tempFile);
96+
Files.move(
97+
tempFile, keyPath, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
98+
99+
} catch (Exception e) {
100+
throw new SecurityException("Failed to generate JWT key", e);
101+
}
102+
}
103+
104+
private static void setStrictPermissions(Path path) {
105+
try {
106+
Files.setPosixFilePermissions(path, PosixFilePermissions.fromString("rw-------"));
107+
} catch (UnsupportedOperationException e) {
108+
log.warn("POSIX permissions not supported for {}", path);
109+
} catch (IOException e) {
110+
log.error("Failed to set permissions for {}", path, e);
111+
}
112+
}
113+
114+
private static void safelyDeleteFile(File keyFile) {
115+
try {
116+
if (keyFile.exists() && !keyFile.delete()) {
117+
log.warn("Failed to delete invalid key file: {}", keyFile.getAbsolutePath());
118+
}
119+
} catch (SecurityException e) {
120+
log.error("Security exception when deleting key file", e);
121+
}
122+
}
123+
}

0 commit comments

Comments
 (0)