diff --git a/db/增量SQL/sas升级脚本.sql b/db/增量SQL/sas升级脚本.sql index d0e29525..6417a915 100644 --- a/db/增量SQL/sas升级脚本.sql +++ b/db/增量SQL/sas升级脚本.sql @@ -37,7 +37,7 @@ now(), null, '3eacac0e-0de9-4727-9a64-6bdd4be2ee1f', 'client_secret_basic', -'refresh_token,authorization_code,password', +'refresh_token,authorization_code,password,app,phone,social', 'http://127.0.0.1:8080/jeecg-', 'http://127.0.0.1:8080/', '*', diff --git a/jeecg-boot-base-core/pom.xml b/jeecg-boot-base-core/pom.xml index 018a026c..738728d0 100644 --- a/jeecg-boot-base-core/pom.xml +++ b/jeecg-boot-base-core/pom.xml @@ -173,63 +173,6 @@ ${java-jwt.version} - - - org.apache.shiro - shiro-spring-boot-starter - ${shiro.version} - - - org.apache.shiro - shiro-spring - - - - - - org.crazycake - shiro-redis - ${shiro-redis.version} - - - org.apache.shiro - shiro-core - - - checkstyle - com.puppycrawl.tools - - - - jedis - redis.clients - - - - - - redis.clients - jedis - 2.9.0 - - - - org.apache.shiro - shiro-spring - jakarta - ${shiro.version} - - - - org.apache.shiro - shiro-core - - - org.apache.shiro - shiro-web - - - org.springframework.boot @@ -244,25 +187,6 @@ org.springframework.security spring-security-cas - - - org.apache.shiro - shiro-core - jakarta - ${shiro.version} - - - org.apache.shiro - shiro-web - jakarta - ${shiro.version} - - - org.apache.shiro - shiro-core - - - diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootExceptionHandler.java b/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootExceptionHandler.java index f69b8d04..67f96fec 100644 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootExceptionHandler.java +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootExceptionHandler.java @@ -2,8 +2,6 @@ package org.jeecg.common.exception; import cn.hutool.core.util.ObjectUtil; import lombok.extern.slf4j.Slf4j; -import org.apache.shiro.authz.AuthorizationException; -import org.apache.shiro.authz.UnauthorizedException; import org.jeecg.common.api.vo.Result; import org.jeecg.common.enums.SentinelErrorInfoEnum; import org.springframework.dao.DataIntegrityViolationException; @@ -87,12 +85,6 @@ public class JeecgBootExceptionHandler { return Result.error("数据库中已存在该记录"); } - @ExceptionHandler({UnauthorizedException.class, AuthorizationException.class}) - public Result handleAuthorizationException(AuthorizationException e){ - log.error(e.getMessage(), e); - return Result.noauth("没有权限,请联系管理员授权"); - } - @ExceptionHandler(AccessDeniedException.class) public Result handleAuthorizationException(AccessDeniedException e){ log.error(e.getMessage(), e); diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/base/controller/JeecgController.java b/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/base/controller/JeecgController.java index 4f703754..b143c4ac 100644 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/base/controller/JeecgController.java +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/base/controller/JeecgController.java @@ -7,7 +7,6 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.IService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.beanutils.PropertyUtils; -import org.apache.shiro.SecurityUtils; import org.jeecg.common.api.vo.Result; import org.jeecg.common.system.query.QueryGenerator; import org.jeecg.common.system.vo.LoginUser; diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/encryption/AesEncryptUtil.java b/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/encryption/AesEncryptUtil.java index 670f3ebd..e75092cc 100644 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/encryption/AesEncryptUtil.java +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/encryption/AesEncryptUtil.java @@ -1,10 +1,9 @@ package org.jeecg.common.util.encryption; -import org.apache.shiro.codec.Base64; - import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; +import java.util.Base64; /** * @Description: AES 加密 @@ -49,7 +48,7 @@ public class AesEncryptUtil { cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec); byte[] encrypted = cipher.doFinal(plaintext); - return Base64.encodeToString(encrypted); + return Base64.getEncoder().encodeToString(encrypted); } catch (Exception e) { e.printStackTrace(); @@ -67,7 +66,7 @@ public class AesEncryptUtil { */ public static String desEncrypt(String data, String key, String iv) throws Exception { //update-begin-author:taoyan date:2022-5-23 for:VUEN-1084 【vue3】online表单测试发现的新问题 6、解密报错 ---解码失败应该把异常抛出去,在外面处理 - byte[] encrypted1 = Base64.decode(data); + byte[] encrypted1 = Base64.getDecoder().decode(data); Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES"); diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/config/JeecgBaseConfig.java b/jeecg-boot-base-core/src/main/java/org/jeecg/config/JeecgBaseConfig.java index 4597d216..a6cb28e5 100644 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/config/JeecgBaseConfig.java +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/config/JeecgBaseConfig.java @@ -32,10 +32,6 @@ public class JeecgBaseConfig { */ private Firewall firewall; - /** - * shiro拦截排除 - */ - private Shiro shiro; /** * 上传文件配置 */ @@ -88,14 +84,6 @@ public class JeecgBaseConfig { this.signatureSecret = signatureSecret; } - public Shiro getShiro() { - return shiro; - } - - public void setShiro(Shiro shiro) { - this.shiro = shiro; - } - public Path getPath() { return path; } diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/config/firewall/interceptor/LowCodeModeInterceptor.java b/jeecg-boot-base-core/src/main/java/org/jeecg/config/firewall/interceptor/LowCodeModeInterceptor.java index 071d1406..404e24e4 100644 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/config/firewall/interceptor/LowCodeModeInterceptor.java +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/config/firewall/interceptor/LowCodeModeInterceptor.java @@ -2,7 +2,6 @@ package org.jeecg.config.firewall.interceptor; import com.alibaba.fastjson.JSON; import lombok.extern.slf4j.Slf4j; -import org.apache.shiro.SecurityUtils; import org.jeecg.common.api.CommonAPI; import org.jeecg.common.api.vo.Result; import org.jeecg.common.constant.CommonConstant; @@ -11,6 +10,7 @@ import org.jeecg.common.system.vo.LoginUser; import org.jeecg.common.util.CommonUtils; import org.jeecg.common.util.SpringContextUtils; import org.jeecg.config.JeecgBaseConfig; +import org.jeecg.config.security.utils.SecureUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.servlet.HandlerInterceptor; @@ -63,7 +63,7 @@ public class LowCodeModeInterceptor implements HandlerInterceptor { if (jeecgBaseConfig.getFirewall()!=null && LowCodeModeInterceptor.LOW_CODE_MODE_PROD.equals(jeecgBaseConfig.getFirewall().getLowCodeMode())) { String requestURI = request.getRequestURI().substring(request.getContextPath().length()); log.info("低代码模式,拦截请求路径:" + requestURI); - LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + LoginUser loginUser = SecureUtil.currentUser(); Set hasRoles = null; if (loginUser == null) { loginUser = commonAPI.getUserByName(JwtUtil.getUserNameByToken(SpringContextUtils.getHttpServletRequest())); diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/config/mybatis/MybatisInterceptor.java b/jeecg-boot-base-core/src/main/java/org/jeecg/config/mybatis/MybatisInterceptor.java index 90cca20b..825d0f25 100644 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/config/mybatis/MybatisInterceptor.java +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/config/mybatis/MybatisInterceptor.java @@ -6,11 +6,11 @@ import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.plugin.*; -import org.apache.shiro.SecurityUtils; import org.jeecg.common.config.TenantContext; import org.jeecg.common.constant.TenantConstant; import org.jeecg.common.system.vo.LoginUser; import org.jeecg.common.util.oConvertUtils; +import org.jeecg.config.security.utils.SecureUtil; import org.springframework.stereotype.Component; import java.lang.reflect.Field; @@ -173,7 +173,7 @@ public class MybatisInterceptor implements Interceptor { private LoginUser getLoginUser() { LoginUser sysUser = null; try { - sysUser = SecurityUtils.getSubject().getPrincipal() != null ? (LoginUser) SecurityUtils.getSubject().getPrincipal() : null; + sysUser = SecureUtil.currentUser() != null ? SecureUtil.currentUser() : null; } catch (Exception e) { //e.printStackTrace(); sysUser = null; diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/LoginType.java b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/LoginType.java index 06e59a8a..c5ee9620 100644 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/LoginType.java +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/LoginType.java @@ -28,4 +28,9 @@ public class LoginType { * 扫码登录 */ public static final String SCAN = "scan"; + + /** + * 所有联合登录,比如github\钉钉\企业微信\微信 + */ + public static final String SOCIAL = "social"; } diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/SecurityConfig.java b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/SecurityConfig.java index 809cca92..7f22a93e 100644 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/SecurityConfig.java +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/SecurityConfig.java @@ -6,8 +6,14 @@ import com.nimbusds.jose.jwk.source.ImmutableJWKSet; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.proc.SecurityContext; import lombok.AllArgsConstructor; +import org.jeecg.config.security.app.AppGrantAuthenticationConvert; +import org.jeecg.config.security.app.AppGrantAuthenticationProvider; import org.jeecg.config.security.password.PasswordGrantAuthenticationConvert; import org.jeecg.config.security.password.PasswordGrantAuthenticationProvider; +import org.jeecg.config.security.phone.PhoneGrantAuthenticationConvert; +import org.jeecg.config.security.phone.PhoneGrantAuthenticationProvider; +import org.jeecg.config.security.social.SocialGrantAuthenticationConvert; +import org.jeecg.config.security.social.SocialGrantAuthenticationProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; @@ -17,6 +23,7 @@ import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.jwt.JwtDecoder; @@ -47,7 +54,7 @@ import java.util.UUID; */ @Configuration @EnableWebSecurity -@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true) +@EnableMethodSecurity @AllArgsConstructor public class SecurityConfig { @@ -62,6 +69,12 @@ public class SecurityConfig { http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) .tokenEndpoint(tokenEndpoint -> tokenEndpoint.accessTokenRequestConverter(new PasswordGrantAuthenticationConvert()) .authenticationProvider(new PasswordGrantAuthenticationProvider(authorizationService, tokenGenerator()))) + .tokenEndpoint(tokenEndpoint -> tokenEndpoint.accessTokenRequestConverter(new PhoneGrantAuthenticationConvert()) + .authenticationProvider(new PhoneGrantAuthenticationProvider(authorizationService, tokenGenerator()))) + .tokenEndpoint(tokenEndpoint -> tokenEndpoint.accessTokenRequestConverter(new AppGrantAuthenticationConvert()) + .authenticationProvider(new AppGrantAuthenticationProvider(authorizationService, tokenGenerator()))) + .tokenEndpoint(tokenEndpoint -> tokenEndpoint.accessTokenRequestConverter(new SocialGrantAuthenticationConvert()) + .authenticationProvider(new SocialGrantAuthenticationProvider(authorizationService, tokenGenerator()))) //开启OpenID Connect 1.0(其中oidc为OpenID Connect的缩写)。 访问 /.well-known/openid-configuration即可获取认证信息 .oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0 http @@ -153,6 +166,7 @@ public class SecurityConfig { config.setAllowedMethods(Arrays.asList("HEAD", "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); return config; })) + .csrf(AbstractHttpConfigurer::disable) .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())); return http.build(); } diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/app/AppGrantAuthenticationProvider.java b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/app/AppGrantAuthenticationProvider.java index 29ef8b12..2d403401 100644 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/app/AppGrantAuthenticationProvider.java +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/app/AppGrantAuthenticationProvider.java @@ -68,11 +68,11 @@ public class AppGrantAuthenticationProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { - PasswordGrantAuthenticationToken passwordGrantAuthenticationToken = (PasswordGrantAuthenticationToken) authentication; - Map additionalParameter = passwordGrantAuthenticationToken.getAdditionalParameters(); + AppGrantAuthenticationToken appGrantAuthenticationToken = (AppGrantAuthenticationToken) authentication; + Map additionalParameter = appGrantAuthenticationToken.getAdditionalParameters(); // 授权类型 - AuthorizationGrantType authorizationGrantType = passwordGrantAuthenticationToken.getGrantType(); + AuthorizationGrantType authorizationGrantType = appGrantAuthenticationToken.getGrantType(); // 用户名 String username = (String) additionalParameter.get(OAuth2ParameterNames.USERNAME); // 密码 @@ -105,7 +105,7 @@ public class AppGrantAuthenticationProvider implements AuthenticationProvider { throw new JeecgCaptchaException(HttpStatus.PRECONDITION_FAILED.value(), "验证码错误"); } - OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(passwordGrantAuthenticationToken); + OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(appGrantAuthenticationToken); RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); if (!registeredClient.getAuthorizationGrantTypes().contains(authorizationGrantType)) { @@ -132,7 +132,7 @@ public class AppGrantAuthenticationProvider implements AuthenticationProvider { .authorizationServerContext(AuthorizationServerContextHolder.getContext()) .authorizationGrantType(authorizationGrantType) .authorizedScopes(requestScopeSet) - .authorizationGrant(passwordGrantAuthenticationToken); + .authorizationGrant(appGrantAuthenticationToken); OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient) .principalName(clientPrincipal.getName()) @@ -212,7 +212,7 @@ public class AppGrantAuthenticationProvider implements AuthenticationProvider { @Override public boolean supports(Class authentication) { - return PasswordGrantAuthenticationToken.class.isAssignableFrom(authentication); + return AppGrantAuthenticationToken.class.isAssignableFrom(authentication); } private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) { diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/phone/PhoneGrantAuthenticationConvert.java b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/phone/PhoneGrantAuthenticationConvert.java index 85f4b14f..66d92927 100644 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/phone/PhoneGrantAuthenticationConvert.java +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/phone/PhoneGrantAuthenticationConvert.java @@ -36,8 +36,7 @@ public class PhoneGrantAuthenticationConvert implements AuthenticationConverter // 验证码 String captcha = parameters.getFirst("captcha"); - if (!StringUtils.hasText(captcha) || - parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) { + if (!StringUtils.hasText(captcha)) { throw new OAuth2AuthenticationException("无效请求,验证码不能为空!"); } diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/phone/PhoneGrantAuthenticationProvider.java b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/phone/PhoneGrantAuthenticationProvider.java index e2ed2355..08cd27f1 100644 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/phone/PhoneGrantAuthenticationProvider.java +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/phone/PhoneGrantAuthenticationProvider.java @@ -68,11 +68,11 @@ public class PhoneGrantAuthenticationProvider implements AuthenticationProvider @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { - PasswordGrantAuthenticationToken passwordGrantAuthenticationToken = (PasswordGrantAuthenticationToken) authentication; - Map additionalParameter = passwordGrantAuthenticationToken.getAdditionalParameters(); + PhoneGrantAuthenticationToken phoneGrantAuthenticationToken = (PhoneGrantAuthenticationToken) authentication; + Map additionalParameter = phoneGrantAuthenticationToken.getAdditionalParameters(); // 授权类型 - AuthorizationGrantType authorizationGrantType = passwordGrantAuthenticationToken.getGrantType(); + AuthorizationGrantType authorizationGrantType = phoneGrantAuthenticationToken.getGrantType(); // 手机号 String phone = (String) additionalParameter.get("mobile"); @@ -102,7 +102,7 @@ public class PhoneGrantAuthenticationProvider implements AuthenticationProvider throw new JeecgBootException("手机验证码错误"); } - OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(passwordGrantAuthenticationToken); + OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(phoneGrantAuthenticationToken); RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); if (!registeredClient.getAuthorizationGrantTypes().contains(authorizationGrantType)) { @@ -118,7 +118,7 @@ public class PhoneGrantAuthenticationProvider implements AuthenticationProvider .authorizationServerContext(AuthorizationServerContextHolder.getContext()) .authorizationGrantType(authorizationGrantType) .authorizedScopes(requestScopeSet) - .authorizationGrant(passwordGrantAuthenticationToken); + .authorizationGrant(phoneGrantAuthenticationToken); OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient) .principalName(clientPrincipal.getName()) @@ -195,7 +195,7 @@ public class PhoneGrantAuthenticationProvider implements AuthenticationProvider @Override public boolean supports(Class authentication) { - return PasswordGrantAuthenticationToken.class.isAssignableFrom(authentication); + return PhoneGrantAuthenticationToken.class.isAssignableFrom(authentication); } private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) { diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/social/SocialGrantAuthenticationConvert.java b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/social/SocialGrantAuthenticationConvert.java new file mode 100644 index 00000000..67a43bd3 --- /dev/null +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/social/SocialGrantAuthenticationConvert.java @@ -0,0 +1,80 @@ +package org.jeecg.config.security.social; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.AllArgsConstructor; +import org.jeecg.config.security.LoginType; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author EightMonth + * @date 2024/1/1 + */ +@AllArgsConstructor +public class SocialGrantAuthenticationConvert implements AuthenticationConverter { + @Override + public Authentication convert(HttpServletRequest request) { + + String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE); + if (!LoginType.SOCIAL.equals(grantType)) { + return null; + } + + Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication(); + + //从request中提取请求参数,然后存入MultiValueMap + MultiValueMap parameters = getParameters(request); + + String token = parameters.getFirst("token"); + if (!StringUtils.hasText(token)) { + throw new OAuth2AuthenticationException("无效请求,三方token不能为空!"); + } + + String source = parameters.getFirst("thirdType"); + if (!StringUtils.hasText(source)) { + throw new OAuth2AuthenticationException("无效请求,三方来源不能为空!"); + } + + //收集要传入PhoneGrantAuthenticationToken构造方法的参数, + //该参数接下来在PhoneGrantAuthenticationProvider中使用 + Map additionalParameters = new HashMap<>(); + //遍历从request中提取的参数,排除掉grant_type、client_id、code等字段参数,其他参数收集到additionalParameters中 + parameters.forEach((key, value) -> { + if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) && + !key.equals(OAuth2ParameterNames.CLIENT_ID) && + !key.equals(OAuth2ParameterNames.CODE)) { + additionalParameters.put(key, value.get(0)); + } + }); + + //返回自定义的PhoneGrantAuthenticationToken对象 + return new SocialGrantAuthenticationToken(clientPrincipal, additionalParameters); + + } + + /** + *从request中提取请求参数,然后存入MultiValueMap + */ + private static MultiValueMap getParameters(HttpServletRequest request) { + Map parameterMap = request.getParameterMap(); + MultiValueMap parameters = new LinkedMultiValueMap<>(parameterMap.size()); + parameterMap.forEach((key, values) -> { + if (values.length > 0) { + for (String value : values) { + parameters.add(key, value); + } + } + }); + return parameters; + } + +} diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/social/SocialGrantAuthenticationProvider.java b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/social/SocialGrantAuthenticationProvider.java new file mode 100644 index 00000000..9371dcd4 --- /dev/null +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/social/SocialGrantAuthenticationProvider.java @@ -0,0 +1,253 @@ +package org.jeecg.config.security.social; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.interfaces.DecodedJWT; +import lombok.extern.slf4j.Slf4j; +import org.jeecg.common.api.CommonAPI; +import org.jeecg.common.constant.CommonConstant; +import org.jeecg.common.exception.JeecgBootException; +import org.jeecg.common.system.vo.LoginUser; +import org.jeecg.common.system.vo.SysDepartModel; +import org.jeecg.common.util.RedisUtil; +import org.jeecg.common.util.oConvertUtils; +import org.jeecg.config.JeecgBaseConfig; +import org.jeecg.config.security.password.PasswordGrantAuthenticationToken; +import org.jeecg.modules.base.service.BaseCommonService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.core.*; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder; +import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; +import org.springframework.util.Assert; + +import java.security.Principal; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @author EightMonth + * @date 2024/1/1 + */ +@Slf4j +public class SocialGrantAuthenticationProvider implements AuthenticationProvider { + + private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2"; + + private final OAuth2AuthorizationService authorizationService; + private final OAuth2TokenGenerator tokenGenerator; + @Autowired + private CommonAPI commonAPI; + @Autowired + private RedisUtil redisUtil; + @Autowired + private JeecgBaseConfig jeecgBaseConfig; + @Autowired + private BaseCommonService baseCommonService; + + public SocialGrantAuthenticationProvider(OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator tokenGenerator) { + Assert.notNull(authorizationService, "authorizationService cannot be null"); + Assert.notNull(tokenGenerator, "tokenGenerator cannot be null"); + this.authorizationService = authorizationService; + this.tokenGenerator = tokenGenerator; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + SocialGrantAuthenticationToken socialGrantAuthenticationToken = (SocialGrantAuthenticationToken) authentication; + Map additionalParameter = socialGrantAuthenticationToken.getAdditionalParameters(); + + // 授权类型 + AuthorizationGrantType authorizationGrantType = socialGrantAuthenticationToken.getGrantType(); + // 三方token + String token = (String) additionalParameter.get("token"); + // 三方来源 + String source = (String) additionalParameter.get("thirdType"); + + //请求参数权限范围 + String requestScopesStr = (String)additionalParameter.getOrDefault(OAuth2ParameterNames.SCOPE, "*"); + //请求参数权限范围专场集合 + Set requestScopeSet = Stream.of(requestScopesStr.split(" ")).collect(Collectors.toSet()); + + DecodedJWT jwt = JWT.decode(token); + String username = jwt.getClaim("username").asString(); + + LoginUser loginUser = commonAPI.getUserByName(username); + // 检查用户可行性 + checkUserIsEffective(loginUser); + + OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(socialGrantAuthenticationToken); + RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); + + if (!registeredClient.getAuthorizationGrantTypes().contains(authorizationGrantType)) { + throw new JeecgBootException("非法登录"); + } + + //由于在上面已验证过用户名、密码,现在构建一个已认证的对象UsernamePasswordAuthenticationToken + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken.authenticated(loginUser,clientPrincipal,new ArrayList<>()); + + DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder() + .registeredClient(registeredClient) + .principal(usernamePasswordAuthenticationToken) + .authorizationServerContext(AuthorizationServerContextHolder.getContext()) + .authorizationGrantType(authorizationGrantType) + .authorizedScopes(requestScopeSet) + .authorizationGrant(socialGrantAuthenticationToken); + + OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient) + .principalName(clientPrincipal.getName()) + .authorizedScopes(requestScopeSet) + .attribute(Principal.class.getName(), loginUser.getUsername()) + .authorizationGrantType(authorizationGrantType); + + + // ----- Access token ----- + OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build(); + OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext); + if (generatedAccessToken == null) { + OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, + "无法生成访问token,请联系管理系。", ERROR_URI); + throw new OAuth2AuthenticationException(error); + } + OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(), + generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes()); + if (generatedAccessToken instanceof ClaimAccessor) { + authorizationBuilder.token(accessToken, (metadata) -> { + metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()); + }); + } else { + authorizationBuilder.accessToken(accessToken); + } + + // ----- Refresh token ----- + OAuth2RefreshToken refreshToken = null; + if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) && + // 不向公共客户端颁发刷新令牌 + !clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) { + + tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build(); + OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext); + if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) { + OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, + "无法生成刷新token,请联系管理员。", ERROR_URI); + throw new OAuth2AuthenticationException(error); + } + + refreshToken = (OAuth2RefreshToken) generatedRefreshToken; + authorizationBuilder.refreshToken(refreshToken); + } + + OAuth2Authorization authorization = authorizationBuilder.build(); + + authorizationService.save(authorization); + + baseCommonService.addLog("用户名: " + loginUser.getUsername() + ",登录成功!", CommonConstant.LOG_TYPE_1, null,loginUser); + + Map addition = new HashMap<>(); + // 设置登录用户信息 + addition.put("userInfo", loginUser); + addition.put("sysAllDictItems", commonAPI.queryAllDictItems()); + + List departs = commonAPI.queryUserDeparts(loginUser.getId()); + addition.put("departs", departs); + if (departs == null || departs.size() == 0) { + addition.put("multi_depart", 0); + } else if (departs.size() == 1) { + commonAPI.updateUserDepart(loginUser.getUsername(), departs.get(0).getOrgCode(),null); + addition.put("multi_depart", 1); + } else { + //查询当前是否有登录部门 + if(oConvertUtils.isEmpty(loginUser.getOrgCode())){ + commonAPI.updateUserDepart(loginUser.getUsername(), departs.get(0).getOrgCode(),null); + } + addition.put("multi_depart", 2); + } + + return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, addition); + } + + @Override + public boolean supports(Class authentication) { + return SocialGrantAuthenticationToken.class.isAssignableFrom(authentication); + } + + private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) { + OAuth2ClientAuthenticationToken clientPrincipal = null; + if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) { + clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal(); + } + if (clientPrincipal != null && clientPrincipal.isAuthenticated()) { + return clientPrincipal; + } + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT); + } + + /** + * 登录失败超出次数5 返回true + * @param username + * @return + */ + private boolean isLoginFailOvertimes(String username){ + String key = CommonConstant.LOGIN_FAIL + username; + Object failTime = redisUtil.get(key); + if(failTime!=null){ + Integer val = Integer.parseInt(failTime.toString()); + if(val>5){ + return true; + } + } + return false; + } + + /** + * 记录登录失败次数 + * @param username + */ + private void addLoginFailOvertimes(String username){ + String key = CommonConstant.LOGIN_FAIL + username; + Object failTime = redisUtil.get(key); + Integer val = 0; + if(failTime!=null){ + val = Integer.parseInt(failTime.toString()); + } + // 10分钟 + redisUtil.set(key, ++val, 10); + } + + /** + * 校验用户是否有效 + */ + private void checkUserIsEffective(LoginUser loginUser) { + //情况1:根据用户信息查询,该用户不存在 + if (Objects.isNull(loginUser)) { + baseCommonService.addLog("用户登录失败,用户不存在!", CommonConstant.LOG_TYPE_1, null); + throw new JeecgBootException("该用户不存在,请注册"); + } + //情况2:根据用户信息查询,该用户已注销 + //update-begin---author:王帅 Date:20200601 for:if条件永远为falsebug------------ + if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) { + //update-end---author:王帅 Date:20200601 for:if条件永远为falsebug------------ + baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已注销!", CommonConstant.LOG_TYPE_1, null); + throw new JeecgBootException("该用户已注销"); + } + //情况3:根据用户信息查询,该用户已冻结 + if (CommonConstant.USER_FREEZE.equals(loginUser.getStatus())) { + baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已冻结!", CommonConstant.LOG_TYPE_1, null); + throw new JeecgBootException("该用户已冻结"); + } + } + +} diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/social/SocialGrantAuthenticationToken.java b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/social/SocialGrantAuthenticationToken.java new file mode 100644 index 00000000..455824d0 --- /dev/null +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/social/SocialGrantAuthenticationToken.java @@ -0,0 +1,20 @@ +package org.jeecg.config.security.social; + +import org.jeecg.config.security.LoginType; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken; + +import java.util.Map; + +/** + * @author EightMonth + * @date 2024/1/1 + */ +public class SocialGrantAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken { + + public SocialGrantAuthenticationToken(Authentication clientPrincipal, Map additionalParameters) { + super(new AuthorizationGrantType(LoginType.SOCIAL), clientPrincipal, additionalParameters); + } + +} diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/JwtToken.java b/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/JwtToken.java deleted file mode 100644 index 0507c541..00000000 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/JwtToken.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.jeecg.config.shiro; - -import org.apache.shiro.authc.AuthenticationToken; - -/** - * @Author Scott - * @create 2018-07-12 15:19 - * @desc - **/ -public class JwtToken implements AuthenticationToken { - - private static final long serialVersionUID = 1L; - private String token; - - public JwtToken(String token) { - this.token = token; - } - - @Override - public Object getPrincipal() { - return token; - } - - @Override - public Object getCredentials() { - return token; - } -} diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java b/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java deleted file mode 100644 index 136751b7..00000000 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java +++ /dev/null @@ -1,301 +0,0 @@ -package org.jeecg.config.shiro; - -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.pool2.impl.GenericObjectPoolConfig; -import org.apache.shiro.mgt.DefaultSessionStorageEvaluator; -import org.apache.shiro.mgt.DefaultSubjectDAO; -import org.apache.shiro.mgt.SecurityManager; -import org.apache.shiro.spring.LifecycleBeanPostProcessor; -import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; -import org.apache.shiro.spring.web.ShiroFilterFactoryBean; -import org.apache.shiro.web.mgt.DefaultWebSecurityManager; -import org.crazycake.shiro.*; -import org.jeecg.common.constant.CommonConstant; -import org.jeecg.common.util.oConvertUtils; -import org.jeecg.config.JeecgBaseConfig; -import org.jeecg.config.shiro.filters.CustomShiroFilterFactoryBean; -import org.jeecg.config.shiro.filters.JwtFilter; -import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.data.redis.RedisProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.DependsOn; -import org.springframework.core.env.Environment; -import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; -import redis.clients.jedis.HostAndPort; -import redis.clients.jedis.JedisCluster; - -import jakarta.annotation.Resource; -import jakarta.servlet.Filter; -import java.util.*; -import java.util.stream.Collectors; - -/** - * @author: Scott - * @date: 2018/2/7 - * @description: shiro 配置类 - */ - -@Slf4j -//@Configuration -public class ShiroConfig { - - @Resource - private LettuceConnectionFactory lettuceConnectionFactory; - @Autowired - private Environment env; - @Resource - private JeecgBaseConfig jeecgBaseConfig; - @Autowired(required = false) - private RedisProperties redisProperties; - - /** - * Filter Chain定义说明 - * - * 1、一个URL可以配置多个Filter,使用逗号分隔 - * 2、当设置多个过滤器时,全部验证通过,才视为通过 - * 3、部分过滤器可指定参数,如perms,roles - */ - @Bean("shiroFilterFactoryBean") - public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { - CustomShiroFilterFactoryBean shiroFilterFactoryBean = new CustomShiroFilterFactoryBean(); - shiroFilterFactoryBean.setSecurityManager(securityManager); - // 拦截器 - Map filterChainDefinitionMap = new LinkedHashMap(); - - //支持yml方式,配置拦截排除 - if(jeecgBaseConfig!=null && jeecgBaseConfig.getShiro()!=null){ - String shiroExcludeUrls = jeecgBaseConfig.getShiro().getExcludeUrls(); - if(oConvertUtils.isNotEmpty(shiroExcludeUrls)){ - String[] permissionUrl = shiroExcludeUrls.split(","); - for(String url : permissionUrl){ - filterChainDefinitionMap.put(url,"anon"); - } - } - } - // 配置不会被拦截的链接 顺序判断 - filterChainDefinitionMap.put("/sys/cas/client/validateLogin", "anon"); //cas验证登录 - filterChainDefinitionMap.put("/sys/randomImage/**", "anon"); //登录验证码接口排除 - filterChainDefinitionMap.put("/sys/checkCaptcha", "anon"); //登录验证码接口排除 - filterChainDefinitionMap.put("/sys/login", "anon"); //登录接口排除 - filterChainDefinitionMap.put("/sys/mLogin", "anon"); //登录接口排除 - filterChainDefinitionMap.put("/sys/logout", "anon"); //登出接口排除 - filterChainDefinitionMap.put("/sys/thirdLogin/**", "anon"); //第三方登录 - filterChainDefinitionMap.put("/sys/getEncryptedString", "anon"); //获取加密串 - filterChainDefinitionMap.put("/sys/sms", "anon");//短信验证码 - filterChainDefinitionMap.put("/sys/phoneLogin", "anon");//手机登录 - filterChainDefinitionMap.put("/sys/user/checkOnlyUser", "anon");//校验用户是否存在 - filterChainDefinitionMap.put("/sys/user/register", "anon");//用户注册 - filterChainDefinitionMap.put("/sys/user/phoneVerification", "anon");//用户忘记密码验证手机号 - filterChainDefinitionMap.put("/sys/user/passwordChange", "anon");//用户更改密码 - filterChainDefinitionMap.put("/auth/2step-code", "anon");//登录验证码 - filterChainDefinitionMap.put("/sys/common/static/**", "anon");//图片预览 &下载文件不限制token - filterChainDefinitionMap.put("/sys/common/pdf/**", "anon");//pdf预览 - filterChainDefinitionMap.put("/generic/**", "anon");//pdf预览需要文件 - - filterChainDefinitionMap.put("/sys/getLoginQrcode/**", "anon"); //登录二维码 - filterChainDefinitionMap.put("/sys/getQrcodeToken/**", "anon"); //监听扫码 - filterChainDefinitionMap.put("/sys/checkAuth", "anon"); //授权接口排除 - - - filterChainDefinitionMap.put("/", "anon"); - filterChainDefinitionMap.put("/doc.html", "anon"); - filterChainDefinitionMap.put("/**/*.js", "anon"); - filterChainDefinitionMap.put("/**/*.css", "anon"); - filterChainDefinitionMap.put("/**/*.html", "anon"); - filterChainDefinitionMap.put("/**/*.svg", "anon"); - filterChainDefinitionMap.put("/**/*.pdf", "anon"); - filterChainDefinitionMap.put("/**/*.jpg", "anon"); - filterChainDefinitionMap.put("/**/*.png", "anon"); - filterChainDefinitionMap.put("/**/*.gif", "anon"); - filterChainDefinitionMap.put("/**/*.ico", "anon"); - filterChainDefinitionMap.put("/**/*.ttf", "anon"); - filterChainDefinitionMap.put("/**/*.woff", "anon"); - filterChainDefinitionMap.put("/**/*.woff2", "anon"); - - filterChainDefinitionMap.put("/druid/**", "anon"); - filterChainDefinitionMap.put("/swagger-ui.html", "anon"); - filterChainDefinitionMap.put("/swagger**/**", "anon"); - filterChainDefinitionMap.put("/webjars/**", "anon"); - filterChainDefinitionMap.put("/v3/**", "anon"); - // 企业微信证书排除 - filterChainDefinitionMap.put("/WW_verify*", "anon"); - - filterChainDefinitionMap.put("/sys/annountCement/show/**", "anon"); - - //积木报表排除 - filterChainDefinitionMap.put("/jmreport/**", "anon"); - filterChainDefinitionMap.put("/**/*.js.map", "anon"); - filterChainDefinitionMap.put("/**/*.css.map", "anon"); - - //拖拽仪表盘设计器排除 - filterChainDefinitionMap.put("/drag/view", "anon"); - filterChainDefinitionMap.put("/drag/page/queryById", "anon"); - filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getAllChartData", "anon"); - filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getTotalData", "anon"); - filterChainDefinitionMap.put("/drag/mock/json/**", "anon"); - //大屏模板例子 - filterChainDefinitionMap.put("/test/bigScreen/**", "anon"); - filterChainDefinitionMap.put("/bigscreen/template1/**", "anon"); - filterChainDefinitionMap.put("/bigscreen/template1/**", "anon"); - //filterChainDefinitionMap.put("/test/jeecgDemo/rabbitMqClientTest/**", "anon"); //MQ测试 - //filterChainDefinitionMap.put("/test/jeecgDemo/html", "anon"); //模板页面 - //filterChainDefinitionMap.put("/test/jeecgDemo/redis/**", "anon"); //redis测试 - - //websocket排除 - filterChainDefinitionMap.put("/websocket/**", "anon");//系统通知和公告 - filterChainDefinitionMap.put("/newsWebsocket/**", "anon");//CMS模块 - filterChainDefinitionMap.put("/vxeSocket/**", "anon");//JVxeTable无痕刷新示例 - - //性能监控——安全隐患泄露TOEKN(durid连接池也有) - //filterChainDefinitionMap.put("/actuator/**", "anon"); - //测试模块排除 - filterChainDefinitionMap.put("/test/seata/**", "anon"); - - // update-begin--author:liusq Date:20230522 for:[issues/4829]访问不存在的url时会提示Token失效,请重新登录呢 - //错误路径排除 - filterChainDefinitionMap.put("/error", "anon"); - // update-end--author:liusq Date:20230522 for:[issues/4829]访问不存在的url时会提示Token失效,请重新登录呢 - - // 添加自己的过滤器并且取名为jwt - Map filterMap = new HashMap(1); - //如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】 - Object cloudServer = env.getProperty(CommonConstant.CLOUD_SERVER_KEY); - filterMap.put("jwt", new JwtFilter(cloudServer==null)); - shiroFilterFactoryBean.setFilters(filterMap); - // - 1.12.0 3.11.0 - 3.2.2 1.4.4 1.4.7 8.5.7 - 1.3.4 + 1.4.0 + 1.16.6 1.6.1 7.4.0 @@ -317,6 +315,12 @@ + + + me.zhyd.oauth + JustAuth + ${justauth.version} + com.squareup.okhttp3 okhttp