From a79dffb82fe5654bbbd54b5f0e0c4f237aeaa46a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johnny=20Miller=20=28=E9=94=BA=E4=BF=8A=29?= Date: Tue, 5 May 2020 13:43:22 +0800 Subject: [PATCH] perf($POM): upgrade JJWT@0.11.1 --- api-portal/pom.xml | 18 +++- .../configuration/JwtConfiguration.java | 7 +- .../WebSecurityConfiguration.java | 2 +- .../controller/CommonController.java | 6 ++ .../filter/JwtAuthenticationFilter.java | 18 +++- .../universal/service/CommonService.java | 8 ++ .../service/impl/CommonServiceImpl.java | 8 ++ .../apiportal/universal/util/JwtUtil.java | 90 ++++++++++--------- .../application-development-local.yml | 2 + 9 files changed, 108 insertions(+), 51 deletions(-) diff --git a/api-portal/pom.xml b/api-portal/pom.xml index 17662245..3f3e2e95 100644 --- a/api-portal/pom.xml +++ b/api-portal/pom.xml @@ -132,16 +132,28 @@ 1.1.22 - redis.clients jedis + io.jsonwebtoken - jjwt - 0.9.1 + jjwt-api + 0.11.1 + + + io.jsonwebtoken + jjwt-impl + 0.11.1 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.1 + runtime diff --git a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/configuration/JwtConfiguration.java b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/configuration/JwtConfiguration.java index 68c87b4e..e8c19b51 100644 --- a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/configuration/JwtConfiguration.java +++ b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/configuration/JwtConfiguration.java @@ -5,6 +5,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; +import java.nio.charset.StandardCharsets; + /** *

JwtConfiguration

*

@@ -19,8 +21,9 @@ @ConfigurationProperties(prefix = "jwt.configuration") public class JwtConfiguration { public JwtConfiguration(ProjectProperty projectProperty) { - this.signingKey = projectProperty.getParentArtifactId(); - log.info("Initiated JWT signing key: {}", this.signingKey); + this.signingKey = String.format("%s %s", projectProperty.getParentArtifactId(), projectProperty.getVersion()); + log.info("Initiated JWT signing key: {}. The specified key byte array is {} bits", this.signingKey, + this.signingKey.getBytes(StandardCharsets.UTF_8).length * 8); } /** diff --git a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/configuration/WebSecurityConfiguration.java b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/configuration/WebSecurityConfiguration.java index a60b2778..5784ba92 100644 --- a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/configuration/WebSecurityConfiguration.java +++ b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/configuration/WebSecurityConfiguration.java @@ -43,8 +43,8 @@ public BCryptPasswordEncoder encoder() { return new BCryptPasswordEncoder(); } - @Override @Bean + @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } diff --git a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/controller/CommonController.java b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/controller/CommonController.java index 20c70c94..600f7d2a 100644 --- a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/controller/CommonController.java +++ b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/controller/CommonController.java @@ -38,4 +38,10 @@ public ResponseBodyBean validationTest(@RequestBody ValidationTestPayloa commonService.validateObject(payload); return ResponseBodyBean.ofDataAndMessage(payload.getName(), "validationTest()"); } + + @GetMapping("/get-jwt") + @ApiOperation(value = "/get-jwt", notes = "Get JWT") + public ResponseBodyBean getJwt(String username) { + return ResponseBodyBean.ofData(commonService.generateJwt(username)); + } } diff --git a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/filter/JwtAuthenticationFilter.java b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/filter/JwtAuthenticationFilter.java index f8300b75..8ba2e536 100644 --- a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/filter/JwtAuthenticationFilter.java +++ b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/filter/JwtAuthenticationFilter.java @@ -50,23 +50,33 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - log.info("JWT authentication is filtering [{}] client requested access. URL: {}", - RequestUtil.getRequestIpAndPort(request), - request.getServletPath()); + log.info("JWT authentication is filtering [{}] client requested access. URL: {}, HTTP method: {}", + RequestUtil.getRequestIpAndPort(request), request.getServletPath(), request.getMethod()); if (customConfiguration.getWebSecurityDisabled()) { + log.warn("The web security is disabled! Might face severe web security issue."); filterChain.doFilter(request, response); return; } if (checkIgnores(request)) { + log.info("The request can be ignored. URL: {}", request.getRequestURI()); filterChain.doFilter(request, response); return; } String jwt = jwtUtil.getJwtFromRequest(request); if (StrUtil.isBlank(jwt)) { + log.error("Invalid JWT, the JWT of request is empty."); + ResponseUtil.renderJson(response, HttpStatus.UNAUTHORIZED, null); + return; + } + String username; + try { + username = jwtUtil.getUsernameFromJwt(jwt); + } catch (Exception e) { + log.error("Exception occurred when getting username from JWT. JWT: {}, exception message: {}", jwt, + e.getMessage()); ResponseUtil.renderJson(response, HttpStatus.UNAUTHORIZED, null); return; } - String username = jwtUtil.getUsernameFromJwt(jwt); UserDetails userDetails; try { userDetails = customUserDetailsServiceImpl.loadUserByUsername(username); diff --git a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/service/CommonService.java b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/service/CommonService.java index b116e806..ed786f91 100644 --- a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/service/CommonService.java +++ b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/service/CommonService.java @@ -26,4 +26,12 @@ public interface CommonService { * @param payload the payload */ void validateObject(ValidationTestPayload payload); + + /** + * Generate jwt string. + * + * @param username the username + * @return the string + */ + String generateJwt(String username); } diff --git a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/service/impl/CommonServiceImpl.java b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/service/impl/CommonServiceImpl.java index ceabd4e2..f075952e 100644 --- a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/service/impl/CommonServiceImpl.java +++ b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/service/impl/CommonServiceImpl.java @@ -1,9 +1,11 @@ package com.jmsoftware.apiportal.universal.service.impl; +import com.google.common.collect.Lists; import com.jmsoftware.apiportal.universal.aspect.ValidateArgument; import com.jmsoftware.apiportal.universal.configuration.ProjectProperty; import com.jmsoftware.apiportal.universal.domain.ValidationTestPayload; import com.jmsoftware.apiportal.universal.service.CommonService; +import com.jmsoftware.apiportal.universal.util.JwtUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -29,6 +31,7 @@ @RequiredArgsConstructor public class CommonServiceImpl implements CommonService { private final ProjectProperty projectProperty; + private final JwtUtil jwtUtil; @Override public Map getApplicationInfo() { @@ -49,6 +52,11 @@ public void validateObject(@Valid ValidationTestPayload payload) { log.info("Validation passed! {}", payload); } + @Override + public String generateJwt(String username) { + return jwtUtil.createJwt(false, Long.MAX_VALUE, username, Lists.newLinkedList(), Lists.newLinkedList()); + } + /** * Gets field value by name. * diff --git a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/util/JwtUtil.java b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/util/JwtUtil.java index e28da320..540bb6dc 100644 --- a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/util/JwtUtil.java +++ b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/util/JwtUtil.java @@ -1,6 +1,7 @@ package com.jmsoftware.apiportal.universal.util; import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import com.jmsoftware.apiportal.universal.configuration.Constants; import com.jmsoftware.apiportal.universal.configuration.JwtConfiguration; @@ -10,17 +11,21 @@ import com.jmsoftware.common.exception.BaseException; import com.jmsoftware.common.exception.SecurityException; import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Configuration; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; +import javax.annotation.PostConstruct; +import javax.crypto.SecretKey; import javax.servlet.http.HttpServletRequest; +import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.Date; import java.util.List; -import java.util.Objects; +import java.util.Optional; import java.util.concurrent.TimeUnit; /** @@ -33,9 +38,20 @@ @Slf4j @Configuration @RequiredArgsConstructor +@SuppressWarnings("unused") public class JwtUtil { private final JwtConfiguration jwtConfiguration; private final RedisService redisService; + private SecretKey secretKey; + private JwtParser jwtParser; + + @PostConstruct + public void init() { + log.info("Start to init class members of {}.", this.getClass().getSimpleName()); + secretKey = Keys.hmacShaKeyFor(jwtConfiguration.getSigningKey().getBytes(StandardCharsets.UTF_8)); + log.warn("Secret key for JWT was generated. Algorithm: {}", secretKey.getAlgorithm()); + jwtParser = Jwts.parserBuilder().setSigningKey(secretKey).build(); + } /** * Create JWT. @@ -47,35 +63,34 @@ public class JwtUtil { * @param authorities Granted Authority * @return JWT */ - @SuppressWarnings("unused") public String createJwt(Boolean rememberMe, Long id, String subject, List roles, Collection authorities) { - Date now = new Date(); + var now = new Date(); JwtBuilder builder = Jwts.builder() - .setId(id.toString()) - .setSubject(subject) - .setIssuedAt(now) - .signWith(SignatureAlgorithm.HS256, jwtConfiguration.getSigningKey()) - .claim("roles", roles); + .setId(id.toString()) + .setSubject(subject) + .setIssuedAt(now) + .signWith(secretKey) + .claim("roles", roles); // TODO: Don't generate authority information in JWT. // .claim("authorities", authorities) - // Set expire duration of JWT. Long ttl = rememberMe ? jwtConfiguration.getTtlForRememberMe() : jwtConfiguration.getTtl(); if (ttl > 0) { builder.setExpiration(DateUtil.offsetMillisecond(now, ttl.intValue())); } - String jwt = builder.compact(); // Store new JWT in Redis - Boolean result = redisService.set(Constants.REDIS_JWT_KEY_PREFIX + subject, jwt, ttl, TimeUnit.MILLISECONDS); + var result = redisService.set(Constants.REDIS_JWT_KEY_PREFIX + subject, jwt, ttl, + TimeUnit.MILLISECONDS); if (result) { return jwt; + } else { + throw new BaseException(HttpStatus.ERROR, "Cannot persist JWT into Redis"); } - throw new BaseException(HttpStatus.ERROR); } /** @@ -98,45 +113,38 @@ public String createJwt(Authentication authentication, Boolean rememberMe) { * @return {@link Claims} */ public Claims parseJwt(String jwt) { + Claims claims; try { - Claims claims = Jwts.parser() - .setSigningKey(jwtConfiguration.getSigningKey()) - .parseClaimsJws(jwt) - .getBody(); - - String username = claims.getSubject(); - String redisKey = Constants.REDIS_JWT_KEY_PREFIX + username; - - // Check if JWT exists - Long expire = redisService.getExpire(redisKey, TimeUnit.MILLISECONDS); - if (Objects.isNull(expire) || expire <= 0) { - throw new SecurityException(HttpStatus.TOKEN_EXPIRED); - } - - // Check if current JWT is equal to the one in Redis. - // If it's noe equal, that indicates current user has signed out or logged in before. - // Both situations reveal the JWT expired. - String redisToken = redisService.get(redisKey); - if (!StrUtil.equals(jwt, redisToken)) { - throw new SecurityException(HttpStatus.TOKEN_OUT_OF_CONTROL); - } - return claims; + claims = Optional.ofNullable(jwtParser.parseClaimsJws(jwt).getBody()) + .orElseThrow(() -> new BaseException(HttpStatus.TOKEN_PARSE_ERROR, "The JWT Claims Set is null")); } catch (ExpiredJwtException e) { - log.error("Token expired."); + log.error("JWT is expired. JWT:{}, message: {}", jwt, e.getMessage()); throw new SecurityException(HttpStatus.TOKEN_EXPIRED); } catch (UnsupportedJwtException e) { - log.error("Token not supported."); + log.error("JWT is unsupported. JWT:{}, message: {}", jwt, e.getMessage()); throw new SecurityException(HttpStatus.TOKEN_PARSE_ERROR); } catch (MalformedJwtException e) { - log.error("Invalid token."); - throw new SecurityException(HttpStatus.TOKEN_PARSE_ERROR); - } catch (SignatureException e) { - log.error("Invalid signature of token."); + log.error("JWT is invalid. JWT:{}, message: {}", jwt, e.getMessage()); throw new SecurityException(HttpStatus.TOKEN_PARSE_ERROR); } catch (IllegalArgumentException e) { - log.error("Token parameter not exists."); + log.error("The parameter of JWT is invalid. JWT:{}, message: {}", jwt, e.getMessage()); throw new SecurityException(HttpStatus.TOKEN_PARSE_ERROR); } + String username = claims.getSubject(); + String redisKeyOfJwt = Constants.REDIS_JWT_KEY_PREFIX + username; + // Check if JWT exists + Long expire = redisService.getExpire(redisKeyOfJwt, TimeUnit.MILLISECONDS); + if (ObjectUtil.isNull(expire) || expire <= 0) { + throw new SecurityException(HttpStatus.TOKEN_EXPIRED); + } + // Check if the current JWT is equal to the one in Redis. + // If it's noe equal, that indicates current user has signed out or logged in before. + // Both situations reveal the JWT has expired. + String jwtInRedis = redisService.get(redisKeyOfJwt); + if (!StrUtil.equals(jwt, jwtInRedis)) { + throw new SecurityException(HttpStatus.TOKEN_OUT_OF_CONTROL); + } + return claims; } /** diff --git a/api-portal/src/main/resources/application-development-local.yml b/api-portal/src/main/resources/application-development-local.yml index a2ffa55f..ad746e68 100644 --- a/api-portal/src/main/resources/application-development-local.yml +++ b/api-portal/src/main/resources/application-development-local.yml @@ -43,6 +43,7 @@ logging: custom: configuration: + super-user: "admin" # Make `web-security-disabled` equal to true to disable web security. We suggest you do not turn off web security # feature unless development environment. web-security-disabled: false @@ -59,6 +60,7 @@ custom: - "/auth/check-email-uniqueness" - "/auth/validate-username/**" - "/user/get-avatar" + - "/common/get-jwt" # Request need to be ignored that matches pattern. pattern: - "/druid/**"