From ce5fcf76a35b474bd221de7ff1b5c1a981c9c961 Mon Sep 17 00:00:00 2001 From: CaptainB Date: Fri, 2 Jun 2023 12:59:26 +0800 Subject: [PATCH] build: shiro api key config --- .../metersphere/sdk/config/ShiroConfig.java | 11 ++- .../sdk/constants/ApiKeyConstants.java | 5 ++ .../sdk/security/ApiKeyFilter.java | 47 ++++++++++ .../sdk/security/ApiKeyHandler.java | 63 ++++++++++++++ .../sdk/security/SSOSessionHandler.java | 36 -------- .../sdk/service/UserKeyService.java | 85 +++++++++++++++++++ 6 files changed, 205 insertions(+), 42 deletions(-) create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/ApiKeyConstants.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/security/ApiKeyFilter.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/security/ApiKeyHandler.java delete mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/security/SSOSessionHandler.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/service/UserKeyService.java diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/config/ShiroConfig.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/config/ShiroConfig.java index 62f544fd0e..bf798725bc 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/config/ShiroConfig.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/config/ShiroConfig.java @@ -1,6 +1,7 @@ package io.metersphere.sdk.config; +import io.metersphere.sdk.security.ApiKeyFilter; import io.metersphere.sdk.security.CsrfFilter; import io.metersphere.sdk.security.MsPermissionAnnotationMethodInterceptor; import io.metersphere.sdk.security.realm.LocalRealm; @@ -39,17 +40,15 @@ public class ShiroConfig { shiroFilterFactoryBean.setUnauthorizedUrl("/403"); shiroFilterFactoryBean.setSuccessUrl("/"); -// shiroFilterFactoryBean.getFilters().put("apikey", new ApiKeyFilter()); + shiroFilterFactoryBean.getFilters().put("apikey", new ApiKeyFilter()); shiroFilterFactoryBean.getFilters().put("csrf", new CsrfFilter()); Map filterChainDefinitionMap = shiroFilterFactoryBean.getFilterChainDefinitionMap(); filterChainDefinitionMap.putAll(FilterChainUtils.loadBaseFilterChain()); - // todo ignore csrf -// filterChainDefinitionMap.putAll(FilterChainUtils.ignoreCsrfFilter()); - filterChainDefinitionMap.put("/**", "csrf, authc"); - // todo -// filterChainDefinitionMap.put("/**", "apikey, csrf, authc"); + filterChainDefinitionMap.putAll(FilterChainUtils.ignoreCsrfFilter()); + + filterChainDefinitionMap.put("/**", "apikey, csrf, authc"); return shiroFilterFactoryBean; } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/ApiKeyConstants.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/ApiKeyConstants.java new file mode 100644 index 0000000000..4940ea8b0f --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/ApiKeyConstants.java @@ -0,0 +1,5 @@ +package io.metersphere.sdk.constants; + +public enum ApiKeyConstants { + ACTIVE, DISABLED +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/security/ApiKeyFilter.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/security/ApiKeyFilter.java new file mode 100644 index 0000000000..9470733397 --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/security/ApiKeyFilter.java @@ -0,0 +1,47 @@ +package io.metersphere.sdk.security; + + +import io.metersphere.sdk.constants.SessionConstants; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.StringUtils; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.apache.shiro.web.filter.authc.AnonymousFilter; +import org.apache.shiro.web.util.WebUtils; + +public class ApiKeyFilter extends AnonymousFilter { + + @Override + protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) { + HttpServletRequest httpRequest = WebUtils.toHttp(request); + // 不是apikey的通过 + if (!ApiKeyHandler.isApiKeyCall(httpRequest) && !SecurityUtils.getSubject().isAuthenticated()) { + return true; + } + + // apikey 验证 + if (!SecurityUtils.getSubject().isAuthenticated()) { + String userId = ApiKeyHandler.getUser(WebUtils.toHttp(request)); + if (StringUtils.isNotBlank(userId)) { + SecurityUtils.getSubject().login(new UsernamePasswordToken(userId, "no_pass")); + } + } + + if (!SecurityUtils.getSubject().isAuthenticated()) { + ((HttpServletResponse) response).setHeader(SessionConstants.AUTHENTICATION_STATUS, "invalid"); + } + + return true; + } + + @Override + protected void postHandle(ServletRequest request, ServletResponse response) throws Exception { + // apikey 退出 + if (ApiKeyHandler.isApiKeyCall(WebUtils.toHttp(request)) && SecurityUtils.getSubject().isAuthenticated()) { + SecurityUtils.getSubject().logout(); + } + } +} \ No newline at end of file diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/security/ApiKeyHandler.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/security/ApiKeyHandler.java new file mode 100644 index 0000000000..ea756a6d6d --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/security/ApiKeyHandler.java @@ -0,0 +1,63 @@ +package io.metersphere.sdk.security; + +import io.metersphere.sdk.service.UserKeyService; +import io.metersphere.sdk.util.CodingUtil; +import io.metersphere.sdk.util.CommonBeanFactory; +import io.metersphere.system.domain.UserKey; +import jakarta.servlet.http.HttpServletRequest; +import org.apache.commons.lang3.StringUtils; + +public class ApiKeyHandler { + + public static final String API_ACCESS_KEY = "accessKey"; + + public static final String API_SIGNATURE = "signature"; + + public static String getUser(HttpServletRequest request) { + if (request == null) { + return null; + } + return getUser(request.getHeader(API_ACCESS_KEY), request.getHeader(API_SIGNATURE)); + } + + public static Boolean isApiKeyCall(HttpServletRequest request) { + if (request == null) { + return false; + } + return !StringUtils.isBlank(request.getHeader(API_ACCESS_KEY)) && !StringUtils.isBlank(request.getHeader(API_SIGNATURE)); + } + + public static String getUser(String accessKey, String signature) { + if (StringUtils.isBlank(accessKey) || StringUtils.isBlank(signature)) { + return null; + } + UserKey userKey = CommonBeanFactory.getBean(UserKeyService.class).getUserKey(accessKey); + if (userKey == null) { + throw new RuntimeException("invalid accessKey"); + } + String signatureDecrypt; + try { + signatureDecrypt = CodingUtil.aesDecrypt(signature, userKey.getSecretKey(), accessKey); + } catch (Throwable t) { + throw new RuntimeException("invalid signature"); + } + String[] signatureArray = StringUtils.split(StringUtils.trimToNull(signatureDecrypt), "|"); + if (signatureArray.length < 2) { + throw new RuntimeException("invalid signature"); + } + if (!StringUtils.equals(accessKey, signatureArray[0])) { + throw new RuntimeException("invalid signature"); + } + long signatureTime; + try { + signatureTime = Long.parseLong(signatureArray[signatureArray.length - 1]); + } catch (Exception e) { + throw new RuntimeException(e); + } + if (Math.abs(System.currentTimeMillis() - signatureTime) > 1800000) { + //签名30分钟超时 + throw new RuntimeException("expired signature"); + } + return userKey.getCreateUser(); + } +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/security/SSOSessionHandler.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/security/SSOSessionHandler.java deleted file mode 100644 index 0c74967990..0000000000 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/security/SSOSessionHandler.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.metersphere.sdk.security; - -import io.metersphere.sdk.constants.SessionConstants; -import io.metersphere.sdk.dto.SessionUser; -import io.metersphere.sdk.util.CodingUtil; -import jakarta.servlet.http.HttpServletRequest; -import org.apache.commons.lang3.StringUtils; - -import java.util.UUID; - -public class SSOSessionHandler { - - public static String random = UUID.randomUUID() + UUID.randomUUID().toString(); - - public static String validate(HttpServletRequest request) { - try { - String v = request.getHeader(SessionConstants.CSRF_TOKEN); - if (StringUtils.isNotBlank(v)) { - return validate(v); - } - } catch (Exception e) { -// LogUtil.error("failed to validate", e); - } - - return null; - } - - private static String validate(String csrfToken) { - csrfToken = CodingUtil.aesDecrypt(csrfToken, SessionUser.secret, SessionUser.iv); - String[] signatureArray = StringUtils.split(StringUtils.trimToNull(csrfToken), "|"); - if (signatureArray.length != 4) { - throw new RuntimeException("invalid token"); - } - return signatureArray[0]; - } -} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/UserKeyService.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/UserKeyService.java new file mode 100644 index 0000000000..ce6430abad --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/UserKeyService.java @@ -0,0 +1,85 @@ +package io.metersphere.sdk.service; + +import io.metersphere.sdk.constants.ApiKeyConstants; +import io.metersphere.sdk.exception.MSException; +import io.metersphere.sdk.util.Translator; +import io.metersphere.system.domain.UserKey; +import io.metersphere.system.domain.UserKeyExample; +import io.metersphere.system.mapper.UserKeyMapper; +import jakarta.annotation.Resource; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import java.util.List; +import java.util.UUID; + +@Service +@Transactional(rollbackFor = Exception.class) +public class UserKeyService { + + @Resource + private UserKeyMapper userKeyMapper; + + @Resource + private BaseUserService baseUserService; + + public List getUserKeysInfo(String userId) { + UserKeyExample userKeysExample = new UserKeyExample(); + userKeysExample.createCriteria().andCreateUserEqualTo(userId); + userKeysExample.setOrderByClause("create_time"); + return userKeyMapper.selectByExample(userKeysExample); + } + + public UserKey generateUserKey(String userId) { + if (baseUserService.getUserDTO(userId) == null) { + MSException.throwException(Translator.get("user_not_exist") + userId); + } + UserKeyExample userKeysExample = new UserKeyExample(); + userKeysExample.createCriteria().andCreateUserEqualTo(userId); + List userKeysList = userKeyMapper.selectByExample(userKeysExample); + + if (!CollectionUtils.isEmpty(userKeysList) && userKeysList.size() >= 5) { + MSException.throwException(Translator.get("user_apikey_limit")); + } + + UserKey userKeys = new UserKey(); + userKeys.setId(UUID.randomUUID().toString()); + userKeys.setCreateUser(userId); + userKeys.setStatus(ApiKeyConstants.ACTIVE.name()); + userKeys.setAccessKey(RandomStringUtils.randomAlphanumeric(16)); + userKeys.setSecretKey(RandomStringUtils.randomAlphanumeric(16)); + userKeys.setCreateTime(System.currentTimeMillis()); + userKeyMapper.insert(userKeys); + return userKeyMapper.selectByPrimaryKey(userKeys.getId()); + } + + public void deleteUserKey(String id) { + userKeyMapper.deleteByPrimaryKey(id); + } + + public void activeUserKey(String id) { + UserKey userKeys = new UserKey(); + userKeys.setId(id); + userKeys.setStatus(ApiKeyConstants.ACTIVE.name()); + userKeyMapper.updateByPrimaryKeySelective(userKeys); + } + + public void disableUserKey(String id) { + UserKey userKeys = new UserKey(); + userKeys.setId(id); + userKeys.setStatus(ApiKeyConstants.DISABLED.name()); + userKeyMapper.updateByPrimaryKeySelective(userKeys); + } + + public UserKey getUserKey(String accessKey) { + UserKeyExample userKeyExample = new UserKeyExample(); + userKeyExample.createCriteria().andAccessKeyEqualTo(accessKey).andStatusEqualTo(ApiKeyConstants.ACTIVE.name()); + List userKeysList = userKeyMapper.selectByExample(userKeyExample); + if (!CollectionUtils.isEmpty(userKeysList)) { + return userKeysList.get(0); + } + return null; + } +}