diff --git a/epmet-commons/epmet-commons-security/pom.xml b/epmet-commons/epmet-commons-security/pom.xml new file mode 100644 index 0000000000..a4bebe4c54 --- /dev/null +++ b/epmet-commons/epmet-commons-security/pom.xml @@ -0,0 +1,26 @@ + + + + epmet-commons + com.epmet + 2.0.0 + + 4.0.0 + + epmet-commons-security + + + + com.epmet + epmet-commons-tools + 2.0.0 + + + io.jsonwebtoken + jjwt + 0.7.0 + + + \ No newline at end of file diff --git a/epmet-commons/epmet-commons-security/src/main/java/com/epmet/commons/security/jwt/JwtTokenUtils.java b/epmet-commons/epmet-commons-security/src/main/java/com/epmet/commons/security/jwt/JwtTokenUtils.java new file mode 100644 index 0000000000..8cbae20749 --- /dev/null +++ b/epmet-commons/epmet-commons-security/src/main/java/com/epmet/commons/security/jwt/JwtTokenUtils.java @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2018 人人开源 All rights reserved. + *

+ * https://www.renren.io + *

+ * 版权所有,侵权必究! + */ + +package com.epmet.commons.security.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.joda.time.DateTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * Jwt工具类 + * + * @author Mark sunlightcs@gmail.com + * @since 1.0.0 + */ +@Component +public class JwtTokenUtils { + private static final Logger logger = LoggerFactory.getLogger(JwtTokenUtils.class); + + public Claims getClaimByToken(String token, String secret) { + try { + return Jwts.parser() + .setSigningKey(secret) + .parseClaimsJws(token) + .getBody(); + } catch (Exception e) { + logger.debug("validate is token error, token = " + token, e); + return null; + } + } + + /** + * @return java.util.Date + * @param token + * @Author yinzuomei + * @Description 获取token的有效期截止时间 + * @Date 2020/3/18 22:17 + **/ + public Date getExpiration(String token, String secret){ + try { + return Jwts.parser() + .setSigningKey(secret) + .parseClaimsJws(token) + .getBody().getExpiration(); + } catch (Exception e) { + logger.debug("validate is token error, token = " + token, e); + return null; + } + } + + /** + * @return java.lang.String + * @Author yinzuomei + * @Description 根据app+client+userId生成token + * @Date 2020/3/18 22:29 + **/ + public String createToken(Map claims, int expire, String secret) { + return Jwts.builder() + .setHeaderParam("typ", "JWT") + .setClaims(claims) + .setIssuedAt(new Date()) + .setExpiration(DateTime.now().plusSeconds(expire).toDate()) + .signWith(SignatureAlgorithm.HS512, secret) + .compact(); + } + + /** + * token是否过期 + * + * @return true:过期 + */ + public boolean isTokenExpired(Date expiration) { + return expiration.before(new Date()); + } + + public static void main(String[] args) { + Map map=new HashMap<>(); + map.put("app","gov"); + map.put("client","wxmp"); + map.put("userId","100526ABC"); + String tokenStr=Jwts.builder() + .setHeaderParam("typ", "JWT") + .setClaims(map) + .setIssuedAt(new Date()) + .setExpiration(DateTime.now().plusSeconds(604800).toDate()) + .signWith(SignatureAlgorithm.HS512, "7016867071f0ebf1c46f123eaaf4b9d6[elink.epmet]") + .compact(); + System.out.println(tokenStr); + Claims claims= Jwts.parser() + .setSigningKey("7016867071f0ebf1c46f123eaaf4b9d6[elink.epmet]") + .parseClaimsJws(tokenStr) + .getBody(); + System.out.println("app="+ claims.get("app")); + System.out.println("client="+ claims.get("client")); + System.out.println("userId="+ claims.get("userId")); + } + +} diff --git a/epmet-commons/epmet-commons-security/src/main/java/com/epmet/commons/security/sign/openapi/OpenApiSignUtils.java b/epmet-commons/epmet-commons-security/src/main/java/com/epmet/commons/security/sign/openapi/OpenApiSignUtils.java new file mode 100644 index 0000000000..33b82b1a23 --- /dev/null +++ b/epmet-commons/epmet-commons-security/src/main/java/com/epmet/commons/security/sign/openapi/OpenApiSignUtils.java @@ -0,0 +1,76 @@ +package com.epmet.commons.security.sign.openapi; + +import com.epmet.commons.tools.utils.Md5Util; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * OpenApi签名工具 + */ +public class OpenApiSignUtils { + + /** + * @Description 创建签名 + * @return + * @author wxz + * @date 2021.03.22 16:47 + */ + public static String createSign(Map contentMap, String signKey) { + String str2beSigned = mapToSignStr(contentMap); + str2beSigned = str2beSigned.concat("&signKey=").concat(signKey); + return Md5Util.md5(str2beSigned); + } + + /** + * @Description 验签 + * @return + * @author wxz + * @date 2021.03.22 16:51 + */ + public static boolean checkSign(Map contentMap, String signKey) { + String newSign = createSign(contentMap, signKey); + return newSign.equals(contentMap.get("sign")); + } + + /** + * @Description map转化为签名明文 + * @return + * @author wxz + * @date 2021.03.22 16:47 + */ + public static String mapToSignStr(Map map) { + Set keySet = map.keySet(); + String[] keyArray = (String[])keySet.toArray(new String[keySet.size()]); + Arrays.sort(keyArray); + StringBuilder sb = new StringBuilder(); + + for(int i = 0; i < keyArray.length; ++i) { + String key = keyArray[i]; + String val = (String)map.get(key); + if (val != null && val.trim().length() > 0 && !"sign".equals(key)) { + if (!sb.toString().isEmpty()) { + sb.append("&"); + } + + sb.append(key).append("=").append((String)map.get(key)); + } + } + + return sb.toString(); + } + + + public static void main(String[] args) { + + HashMap content = new HashMap<>(); + content.put("appId", "7d98b8af2d05752b4225709c4cfd4bd0"); + + String secret = "3209ee9f41704482be1a1fb5873a25376f2899191ca846119d44168316bc3e44"; + + String sign = createSign(content, secret); + System.out.println(sign); + } +} diff --git a/epmet-commons/epmet-commons-tools/src/main/java/com/epmet/commons/tools/exception/EpmetErrorCode.java b/epmet-commons/epmet-commons-tools/src/main/java/com/epmet/commons/tools/exception/EpmetErrorCode.java index ac6d50bd9f..47546c0e81 100644 --- a/epmet-commons/epmet-commons-tools/src/main/java/com/epmet/commons/tools/exception/EpmetErrorCode.java +++ b/epmet-commons/epmet-commons-tools/src/main/java/com/epmet/commons/tools/exception/EpmetErrorCode.java @@ -143,7 +143,10 @@ public enum EpmetErrorCode { TOPIC_SHIFTED_TO_ISSUE_UNDER_AUDITING(9004,"当前话题正在转议题审核"), TOPIC_ALREADY_SHIFTED_TO_ISSUE(9005,"该话题已被转为议题,请勿重复操作"), TOPIC_IS_HIDDEN(9006,"该话题已被屏蔽,请先解除屏蔽"), - TOPIC_IS_CLOSED(9008,"该话题已关闭,无法转为议题"); + TOPIC_IS_CLOSED(9008,"该话题已关闭,无法转为议题"), + + // open api异常 + OPEN_API_SIGN_ERROR(9100, "签名错误"); private int code; private String msg; diff --git a/epmet-commons/epmet-commons-tools/src/main/java/com/epmet/commons/tools/redis/RedisKeys.java b/epmet-commons/epmet-commons-tools/src/main/java/com/epmet/commons/tools/redis/RedisKeys.java index 098dd1c14d..20bde9e05f 100644 --- a/epmet-commons/epmet-commons-tools/src/main/java/com/epmet/commons/tools/redis/RedisKeys.java +++ b/epmet-commons/epmet-commons-tools/src/main/java/com/epmet/commons/tools/redis/RedisKeys.java @@ -370,4 +370,14 @@ public class RedisKeys { public static String getCustomerApiServiceKey(String customerId) { return rootPrefix.concat("customer:thirdplat:apiservice:").concat(customerId); } + + /** + * @Description 获取openApi的accessToken的key + * @return + * @author wxz + * @date 2021.03.23 10:25 + */ + public static String getOpenApiAccessTokenKey(String appId) { + return rootPrefix.concat("openapi:accesstoken:").concat(appId); + } } diff --git a/epmet-commons/epmet-commons-tools/src/main/java/com/epmet/commons/tools/utils/ConvertUtils.java b/epmet-commons/epmet-commons-tools/src/main/java/com/epmet/commons/tools/utils/ConvertUtils.java index 93c653e178..6d2e509797 100644 --- a/epmet-commons/epmet-commons-tools/src/main/java/com/epmet/commons/tools/utils/ConvertUtils.java +++ b/epmet-commons/epmet-commons-tools/src/main/java/com/epmet/commons/tools/utils/ConvertUtils.java @@ -12,11 +12,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeanUtils; +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; /** * 转换工具类 @@ -87,4 +90,33 @@ public class ConvertUtils { } return t; } + + /** + * @Description entity转map + * @return + * @author wxz + * @date 2021.03.22 16:38 + */ + public static Map entityToMap(Object bean) throws IntrospectionException, InvocationTargetException, IllegalAccessException { + Class type = bean.getClass(); + Map returnMap = new HashMap(16); + BeanInfo beanInfo = Introspector.getBeanInfo(type); + PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors(); + + for(int i = 0; i < propertyDescriptors.length; ++i) { + PropertyDescriptor descriptor = propertyDescriptors[i]; + String propertyName = descriptor.getName(); + if (!"class".equals(propertyName)) { + Method readMethod = descriptor.getReadMethod(); + Object result = readMethod.invoke(bean); + if (result != null) { + returnMap.put(propertyName, String.valueOf(result)); + } else { + returnMap.put(propertyName, ""); + } + } + } + + return returnMap; + } } diff --git a/epmet-commons/pom.xml b/epmet-commons/pom.xml index 636480c48f..b495dd438e 100644 --- a/epmet-commons/pom.xml +++ b/epmet-commons/pom.xml @@ -25,6 +25,7 @@ epmet-commons-extapp-auth epmet-commons-thirdplat epmet-commons-rocketmq - + epmet-commons-security + diff --git a/epmet-module/epmet-ext/epmet-ext-client/src/main/java/com/epmet/dto/form/AccessTokenFormDTO.java b/epmet-module/epmet-ext/epmet-ext-client/src/main/java/com/epmet/dto/form/AccessTokenFormDTO.java new file mode 100644 index 0000000000..4b315ae326 --- /dev/null +++ b/epmet-module/epmet-ext/epmet-ext-client/src/main/java/com/epmet/dto/form/AccessTokenFormDTO.java @@ -0,0 +1,19 @@ +package com.epmet.dto.form; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +@Data +public class AccessTokenFormDTO { + + public interface GetAccessTokenGroup {} + + // 签名字符串密文 + @NotBlank(message = "签名字段不能为空", groups = { GetAccessTokenGroup.class }) + private String sign; + + // 应用id + @NotBlank(message = "AppId字段不能为空", groups = { GetAccessTokenGroup.class }) + private String appId; +} diff --git a/epmet-module/epmet-ext/epmet-ext-server/pom.xml b/epmet-module/epmet-ext/epmet-ext-server/pom.xml index f33a428e27..d12766c5e9 100644 --- a/epmet-module/epmet-ext/epmet-ext-server/pom.xml +++ b/epmet-module/epmet-ext/epmet-ext-server/pom.xml @@ -21,6 +21,16 @@ + + com.epmet + common-service-client + 2.0.0 + + + com.epmet + epmet-commons-security + 2.0.0 + com.epmet epmet-ext-client diff --git a/epmet-module/epmet-ext/epmet-ext-server/src/main/java/com/epmet/config/OpenApiConfig.java b/epmet-module/epmet-ext/epmet-ext-server/src/main/java/com/epmet/config/OpenApiConfig.java new file mode 100644 index 0000000000..706c01af35 --- /dev/null +++ b/epmet-module/epmet-ext/epmet-ext-server/src/main/java/com/epmet/config/OpenApiConfig.java @@ -0,0 +1,15 @@ +package com.epmet.config; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + +@Component +@Data +public class OpenApiConfig { + + @Value("${openApi.accessToken.expire}") + private int accessTokenExpire; + +} diff --git a/epmet-module/epmet-ext/epmet-ext-server/src/main/java/com/epmet/controller/OpenApiAccessTokenController.java b/epmet-module/epmet-ext/epmet-ext-server/src/main/java/com/epmet/controller/OpenApiAccessTokenController.java new file mode 100644 index 0000000000..1580ca1b6a --- /dev/null +++ b/epmet-module/epmet-ext/epmet-ext-server/src/main/java/com/epmet/controller/OpenApiAccessTokenController.java @@ -0,0 +1,88 @@ +package com.epmet.controller; + +import com.epmet.commons.security.sign.openapi.OpenApiSignUtils; +import com.epmet.commons.tools.exception.EpmetErrorCode; +import com.epmet.commons.tools.exception.ExceptionUtils; +import com.epmet.commons.tools.exception.RenException; +import com.epmet.commons.tools.redis.RedisKeys; +import com.epmet.commons.tools.redis.RedisUtils; +import com.epmet.commons.tools.utils.ConvertUtils; +import com.epmet.commons.tools.utils.Result; +import com.epmet.commons.tools.validator.ValidatorUtils; +import com.epmet.dto.form.AccessTokenFormDTO; +import com.epmet.feign.EpmetCommonServiceOpenFeignClient; +import com.epmet.service.OpenApiAccessTokenService; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.beans.IntrospectionException; +import java.lang.reflect.InvocationTargetException; + +@RestController +@RequestMapping("open-api") +public class OpenApiAccessTokenController { + + @Autowired + private OpenApiAccessTokenService openApiAccessTokenService; + + @Autowired + private EpmetCommonServiceOpenFeignClient commonServiceOpenFeignClient; + + @Autowired + private RedisUtils redisUtils; + + private Logger logger = LoggerFactory.getLogger(OpenApiAccessTokenController.class); + + /** + * @Description 获取AccessToken + * @return + * @author wxz + * @date 2021.03.23 09:52 + */ + @PostMapping("get-access-token") + public Result getAccessToken(@RequestBody AccessTokenFormDTO input) { + // 1.校验参数 + ValidatorUtils.validateEntity(input); + String appId = input.getAppId(); + + // 2.取secret + String secret = (String)redisUtils.get(RedisKeys.getExternalAppSecretKey(appId)); + if (StringUtils.isBlank(secret)) { + Result result = commonServiceOpenFeignClient.getSecret(appId); + if (!result.success()) { + throw new RenException("调用common service查询secret失败"); + } + secret = result.getData(); + if (StringUtils.isBlank(secret)) { + throw new RenException(String.format("根据appId%s没有找到对应的secret", appId)); + } + redisUtils.set(RedisKeys.getExternalAppSecretKey(appId), secret); + } + + // 3.验签 + try { + if (!OpenApiSignUtils.checkSign(ConvertUtils.entityToMap(input), secret)) { + throw new RenException(EpmetErrorCode.OPEN_API_SIGN_ERROR.getCode(), EpmetErrorCode.OPEN_API_SIGN_ERROR.getMsg()); + } + } catch (RenException e) { + // 如果是自己抛出的异常则继续抛出 + throw e; + } catch (Exception e) { + // 是其他意外发生的异常 + String errorStackTrace = ExceptionUtils.getErrorStackTrace(e); + logger.error("验签发生未知异常:{}", errorStackTrace); + throw new RenException("验签发生未知异常,请查看ext服务详细日志"); + } + + //4.生成token + String accessToken = openApiAccessTokenService.getAccessToken(appId, secret); + return new Result().ok(accessToken); + } + +} diff --git a/epmet-module/epmet-ext/epmet-ext-server/src/main/java/com/epmet/service/OpenApiAccessTokenService.java b/epmet-module/epmet-ext/epmet-ext-server/src/main/java/com/epmet/service/OpenApiAccessTokenService.java new file mode 100644 index 0000000000..56c3bd4630 --- /dev/null +++ b/epmet-module/epmet-ext/epmet-ext-server/src/main/java/com/epmet/service/OpenApiAccessTokenService.java @@ -0,0 +1,15 @@ +package com.epmet.service; + +/** + * access token的service + */ +public interface OpenApiAccessTokenService { + + /** + * @Description 获取AccessToken + * @return + * @author wxz + * @date 2021.03.22 22:57 + */ + String getAccessToken(String appId, String secret); +} diff --git a/epmet-module/epmet-ext/epmet-ext-server/src/main/java/com/epmet/service/impl/OpenApiAccessTokenServiceImpl.java b/epmet-module/epmet-ext/epmet-ext-server/src/main/java/com/epmet/service/impl/OpenApiAccessTokenServiceImpl.java new file mode 100644 index 0000000000..8610eb2ce0 --- /dev/null +++ b/epmet-module/epmet-ext/epmet-ext-server/src/main/java/com/epmet/service/impl/OpenApiAccessTokenServiceImpl.java @@ -0,0 +1,44 @@ +package com.epmet.service.impl; + +import com.epmet.commons.security.jwt.JwtTokenUtils; +import com.epmet.commons.tools.exception.EpmetErrorCode; +import com.epmet.commons.tools.exception.RenException; +import com.epmet.commons.tools.redis.RedisKeys; +import com.epmet.commons.tools.redis.RedisUtils; +import com.epmet.commons.tools.utils.Result; +import com.epmet.commons.tools.utils.SpringContextUtils; +import com.epmet.config.OpenApiConfig; +import com.epmet.feign.EpmetCommonServiceOpenFeignClient; +import com.epmet.service.OpenApiAccessTokenService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.HashMap; + +@Service +public class OpenApiAccessTokenServiceImpl implements OpenApiAccessTokenService { + + @Autowired + private JwtTokenUtils jwtTokenUtils; + + @Autowired + private OpenApiConfig openApiConfig; + + @Autowired + private RedisUtils redisUtils; + + @Override + public String getAccessToken(String appId, String secret) { + HashMap claim = new HashMap<>(); + claim.put("appId", appId); + + String token = jwtTokenUtils.createToken(claim, openApiConfig.getAccessTokenExpire(), secret); + + // 缓存token + redisUtils.set(RedisKeys.getOpenApiAccessTokenKey(appId), token, openApiConfig.getAccessTokenExpire()); + + return token; + } +} diff --git a/epmet-module/epmet-ext/epmet-ext-server/src/main/resources/bootstrap.yml b/epmet-module/epmet-ext/epmet-ext-server/src/main/resources/bootstrap.yml index 7a067d1606..9696f37468 100644 --- a/epmet-module/epmet-ext/epmet-ext-server/src/main/resources/bootstrap.yml +++ b/epmet-module/epmet-ext/epmet-ext-server/src/main/resources/bootstrap.yml @@ -110,3 +110,8 @@ shutdown: graceful: enable: true #是否开启优雅停机 waitTimeSecs: 30 # 优雅停机等待时间,超过30秒,发出告警 + +# 对外开放接口配置 +openApi: + accessToken: + expire: 7200000