add jwt support #30

This commit is contained in:
Francis Dong
2021-02-01 17:12:03 +08:00
parent 245caf6ce4
commit 6c7eb45412
6 changed files with 476 additions and 0 deletions

View File

@@ -212,6 +212,12 @@
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.12.1</version>
</dependency>
<!--<dependency>
<groupId>io.netty</groupId>

View File

@@ -0,0 +1,53 @@
/*
* Copyright (C) 2021 the original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package we.plugin.jwt;
/**
*
* @author Francis Dong
*
*/
public class GlobalConfig {
/**
* Global secret key for HS256/HS384/HS512 algorithm
*/
private String secretKey;
/**
* Global public key for RS256/RS384/RS512/ES256/ES256K/ES384/ES512 algorithm
*/
private String publicKey;
public String getSecretKey() {
return secretKey;
}
public void setSecretKey(String secretKey) {
this.secretKey = secretKey;
}
public String getPublicKey() {
return publicKey;
}
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
}

View File

@@ -0,0 +1,230 @@
/*
* Copyright (C) 2021 the original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package we.plugin.jwt;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPublicKey;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import reactor.core.publisher.Mono;
import we.plugin.PluginFilter;
import we.util.JacksonUtils;
import we.util.PemUtils;
import we.util.WebUtils;
/**
*
* @author Francis Dong
*
*/
@Component(JwtAuthPluginFilter.JWT_AUTH_PLUGIN_FILTER)
public class JwtAuthPluginFilter extends PluginFilter {
private static final Logger log = LoggerFactory.getLogger(JwtAuthPluginFilter.class);
public static final String JWT_AUTH_PLUGIN_FILTER = "jwtAuthPlugin";
public static final String RSA = "RSA";
public static final String EC = "EC";
public static final String KEY = "key";
public static final String PASS_HEADER = "passHeader";
public static final String EXTRACT_CLAIMS = "extractClaims";
public static final String STATUS_CODE = "statusCode";
public static final String CONTENT_TYPE = "contentType";
public static final String RESP_BODY = "respBody";
public static final String JWT_CLAIMS = "jwt.claims";
/**
* Plugin global custom config, example: <br/>
* <br/>
* {<br/>
* "secretKey": "secret key for HS256/HS384/HS512 Algorithm", <br/>
* "publicKey": "public key for RSA or ECDSA Algorithm" <br/>
* }<br/>
* <br/>
*/
private GlobalConfig globalConfig = null;
private String fixedConfigCache = null;
@SuppressWarnings("unchecked")
@Override
public Mono<Void> doFilter(ServerWebExchange exchange, Map<String, Object> config, String fixedConfig) {
try {
if (globalConfig == null || fixedConfigCache == null
|| (fixedConfigCache != null && !fixedConfigCache.equals(fixedConfig))) {
if (StringUtils.isNotBlank(fixedConfig)) {
globalConfig = JacksonUtils.readValue(fixedConfig, GlobalConfig.class);
} else {
globalConfig = null;
}
fixedConfigCache = fixedConfig;
}
String secretKey = (String) config.get(KEY);
String publicKey = (String) config.get(KEY);
secretKey = StringUtils.isBlank(secretKey) ? globalConfig.getSecretKey() : secretKey;
publicKey = StringUtils.isBlank(publicKey) ? globalConfig.getPublicKey() : publicKey;
Boolean passHeader = null;
List<Object> passHeaderList = (List<Object>) config.get(PASS_HEADER);
if(passHeaderList != null && passHeaderList.size() > 0) {
passHeader = (Boolean) passHeaderList.get(0);
}
Boolean extractClaims = null;
List<Object> extractClaimsList = (List<Object>) config.get(EXTRACT_CLAIMS);
if(extractClaimsList != null && extractClaimsList.size() > 0) {
extractClaims = (Boolean) extractClaimsList.get(0);
}
Integer statusCode = (Integer) config.get(STATUS_CODE);
String contentType = (String) config.get(CONTENT_TYPE);
String respBody = (String) config.get(RESP_BODY);
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", contentType);
// JSON Web Token from header
HttpHeaders reqHeaders = exchange.getRequest().getHeaders();
// Auth header format: Bearer eyJhbG...
String token = reqHeaders.getFirst(HttpHeaders.AUTHORIZATION);
if (StringUtils.isNotBlank(token) && token.length() > 7
&& token.substring(0, 7).equalsIgnoreCase("Bearer ")) {
token = token.substring(7);
} else {
if (StringUtils.isBlank(token)) {
log.warn("JWT Auth plugin - Token is missing");
return WebUtils.responseErrorAndBindContext(exchange, JWT_AUTH_PLUGIN_FILTER,
HttpStatus.valueOf(statusCode), headers, respBody);
} else {
log.warn("JWT Auth plugin - invalid token");
return WebUtils.responseErrorAndBindContext(exchange, JWT_AUTH_PLUGIN_FILTER,
HttpStatus.valueOf(statusCode), headers, respBody);
}
}
DecodedJWT jwt = this.verify(token, secretKey, publicKey);
if (jwt == null) {
// failed
return WebUtils.responseErrorAndBindContext(exchange, JWT_AUTH_PLUGIN_FILTER,
HttpStatus.valueOf(statusCode), headers, respBody);
} else {
// passed
// remove jwt header
if (passHeader != null && !passHeader) {
reqHeaders.remove(HttpHeaders.AUTHORIZATION);
}
if (extractClaims != null && extractClaims) {
exchange.getAttributes().put(JWT_CLAIMS, jwt.getClaims());
}
}
return WebUtils.transmitSuccessFilterResultAndEmptyMono(exchange, JWT_AUTH_PLUGIN_FILTER, null);
} catch (Exception e) {
log.error("JWT Auth plugin Exception", e);
return WebUtils.responseErrorAndBindContext(exchange, JWT_AUTH_PLUGIN_FILTER,
HttpStatus.INTERNAL_SERVER_ERROR);
}
}
/**
* Verify JWT
*
* @param token
* @param secretKey key for HS256/HS384/HS512
* @param publicKey pub key for RSA or ECDSA
* @return
* @throws Exception
*/
public DecodedJWT verify(String token, String secretKey, String publicKey) {
try {
DecodedJWT jwt = JWT.decode(token);
String alg = jwt.getAlgorithm();
Algorithm algorithm = null;
switch (alg) {
case "HS256":
algorithm = Algorithm.HMAC256(secretKey);
break;
case "HS384":
algorithm = Algorithm.HMAC384(secretKey);
break;
case "HS512":
algorithm = Algorithm.HMAC512(secretKey);
break;
case "RS256":
algorithm = Algorithm.RSA256((RSAPublicKey) PemUtils.readPublicKeyFromString(publicKey, RSA), null);
break;
case "RS384":
algorithm = Algorithm.RSA384((RSAPublicKey) PemUtils.readPublicKeyFromString(publicKey, RSA), null);
break;
case "RS512":
algorithm = Algorithm.RSA512((RSAPublicKey) PemUtils.readPublicKeyFromString(publicKey, RSA), null);
break;
case "ES256":
algorithm = Algorithm.ECDSA256((ECPublicKey) PemUtils.readPublicKeyFromString(publicKey, EC), null);
break;
case "ES256K":
algorithm = Algorithm.ECDSA256K((ECPublicKey) PemUtils.readPublicKeyFromString(publicKey, EC), null);
break;
case "ES384":
algorithm = Algorithm.ECDSA384((ECPublicKey) PemUtils.readPublicKeyFromString(publicKey, EC), null);
break;
case "ES512":
algorithm = Algorithm.ECDSA512((ECPublicKey) PemUtils.readPublicKeyFromString(publicKey, EC), null);
break;
}
if (algorithm == null) {
// Algorithm NOT Supported
log.warn("{} Algorithm NOT Supported", alg);
} else {
JWTVerifier verifier = JWT.require(algorithm).build();
try {
return verifier.verify(token);
} catch (JWTVerificationException e) {
// Verification failed
log.warn("JWT verification failed: {}", e.getMessage());
}
}
} catch (Exception e) {
log.warn("JWT verification exception", e);
}
return null;
}
}

View File

@@ -0,0 +1,97 @@
//Copyright 2017 - https://github.com/lbalmaceda
//Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package we.util;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
public class PemUtils {
private static byte[] parsePEMString(String pemStr) throws IOException {
PemReader reader = new PemReader(new InputStreamReader(new ByteArrayInputStream(pemStr.getBytes())));
PemObject pemObject = reader.readPemObject();
byte[] content = pemObject.getContent();
reader.close();
return content;
}
private static byte[] parsePEMFile(File pemFile) throws IOException {
if (!pemFile.isFile() || !pemFile.exists()) {
throw new FileNotFoundException(String.format("The file '%s' doesn't exist.", pemFile.getAbsolutePath()));
}
PemReader reader = new PemReader(new FileReader(pemFile));
PemObject pemObject = reader.readPemObject();
byte[] content = pemObject.getContent();
reader.close();
return content;
}
private static PublicKey getPublicKey(byte[] keyBytes, String algorithm) {
PublicKey publicKey = null;
try {
KeyFactory kf = KeyFactory.getInstance(algorithm);
EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
publicKey = kf.generatePublic(keySpec);
} catch (NoSuchAlgorithmException e) {
System.out.println("Could not reconstruct the public key, the given algorithm could not be found.");
} catch (InvalidKeySpecException e) {
System.out.println("Could not reconstruct the public key");
}
return publicKey;
}
private static PrivateKey getPrivateKey(byte[] keyBytes, String algorithm) {
PrivateKey privateKey = null;
try {
KeyFactory kf = KeyFactory.getInstance(algorithm);
EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
privateKey = kf.generatePrivate(keySpec);
} catch (NoSuchAlgorithmException e) {
System.out.println("Could not reconstruct the private key, the given algorithm could not be found.");
} catch (InvalidKeySpecException e) {
System.out.println("Could not reconstruct the private key");
}
return privateKey;
}
public static PublicKey readPublicKeyFromFile(String filepath, String algorithm) throws IOException {
byte[] bytes = PemUtils.parsePEMFile(new File(filepath));
return PemUtils.getPublicKey(bytes, algorithm);
}
public static PrivateKey readPrivateKeyFromFile(String filepath, String algorithm) throws IOException {
byte[] bytes = PemUtils.parsePEMFile(new File(filepath));
return PemUtils.getPrivateKey(bytes, algorithm);
}
public static PublicKey readPublicKeyFromString(String pemStr, String algorithm) throws IOException {
byte[] bytes = PemUtils.parsePEMString(pemStr);
return PemUtils.getPublicKey(bytes, algorithm);
}
public static PrivateKey readPrivateKeyFromString(String pemStr, String algorithm) throws IOException {
byte[] bytes = PemUtils.parsePEMString(pemStr);
return PemUtils.getPrivateKey(bytes, algorithm);
}
}

View File

@@ -437,6 +437,21 @@ public abstract class WebUtils {
transmitFailFilterResult(exchange, filter);
return buildDirectResponseAndBindContext(exchange, httpStatus, new HttpHeaders(), Constants.Symbol.EMPTY);
}
public static Mono<Void> responseErrorAndBindContext(ServerWebExchange exchange, String filter, HttpStatus httpStatus,
HttpHeaders headers, String content) {
ServerHttpResponse response = exchange.getResponse();
String rid = exchange.getRequest().getId();
StringBuilder b = ThreadContext.getStringBuilder();
request2stringBuilder(exchange, b);
b.append(Constants.Symbol.LINE_SEPARATOR);
b.append(filter).append(Constants.Symbol.SPACE).append(httpStatus);
log.error(b.toString(), LogService.BIZ_ID, rid);
transmitFailFilterResult(exchange, filter);
headers = headers == null ? new HttpHeaders() : headers;
content = StringUtils.isBlank(content) ? Constants.Symbol.EMPTY : content;
return buildDirectResponseAndBindContext(exchange, httpStatus, headers, content);
}
public static String getOriginIp(ServerWebExchange exchange) {
String ip = exchange.getAttribute(originIp);

View File

@@ -0,0 +1,75 @@
/*
* Copyright (C) 2021 the original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package we.plugin.jwtAuth;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import com.auth0.jwt.interfaces.DecodedJWT;
import we.plugin.jwt.JwtAuthPluginFilter;
/**
*
* @author Francis Dong
*
*/
public class JwtAuthPluginFilterTests {
private JwtAuthPluginFilter plugin = new JwtAuthPluginFilter();
@Test
public void testVerify() {
String secretKey = "123456";
String rsaPublicKey = "-----BEGIN PUBLIC KEY-----\n"
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuGbXWiK3dQTyCbX5xdE4\n"
+ "yCuYp0AF2d15Qq1JSXT/lx8CEcXb9RbDddl8jGDv+spi5qPa8qEHiK7FwV2KpRE9\n"
+ "83wGPnYsAm9BxLFb4YrLYcDFOIGULuk2FtrPS512Qea1bXASuvYXEpQNpGbnTGVs\n"
+ "WXI9C+yjHztqyL2h8P6mlThPY9E9ue2fCqdgixfTFIF9Dm4SLHbphUS2iw7w1JgT\n"
+ "69s7of9+I9l5lsJ9cozf1rxrXX4V1u/SotUuNB3Fp8oB4C1fLBEhSlMcUJirz1E8\n"
+ "AziMCxS+VrRPDM+zfvpIJg3JljAh3PJHDiLu902v9w+Iplu1WyoB2aPfitxEhRN0\n"
+ "YwIDAQAB\n"
+ "-----END PUBLIC KEY-----";
String ecPublicKey = "-----BEGIN PUBLIC KEY-----\n"
+ "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9\n"
+ "q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==\n"
+ "-----END PUBLIC KEY-----";
// HS256
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.keH6T3x1z7mmhKL1T3r9sQdAxxdzB6siemGMr_6ZOwU";
DecodedJWT jwt = plugin.verify(token, secretKey, null);
assertNotNull(jwt);
// RS256
token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.FsPP7skBe3RgWESameUrpffWSapIIkx4gWAuLPumsFNu4Kqzekt0eyiyGaxHjicBY4UhAlvbSo7GzBANO40x3fkEkpA2YnigOMH9CB4qTzehhg0liMhPuqAmmtKMLpJzT5dboixRjY316KSsrtY6LSJgModG4K21-zufJ-AJZS9bOaBNo_5TKbHRI-vZ0I4QFDjVDZxpsDITe2FOSc4uIdaXns67ZUlvjoDXeAgaMCZtDUxxR2j7s_jefajUQPHt8lc2eecdD8S91RTt3lboFKnWO9r5Ygvl21mZo_WjEyVs01XU4Zd5Pk-B8aH2B8d2MkG7wyPaX-q-cD4mOEsRvA";
jwt = plugin.verify(token, secretKey, rsaPublicKey);
assertNotNull(jwt);
// ES256
token = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.tyh-VfuzIxCyGYDlkBA7DfyjrqmSHu6pQ2hoZuFqUSLPNY2N0mpHb3nk5K17HWP_3cYHBw7AhHale5wky6-sVA";
jwt = plugin.verify(token, secretKey, ecPublicKey);
assertNotNull(jwt);
}
}