scopes;
+
+ private boolean ignoreCheckState;
+
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/config/AuthSource.java b/sf-oauth/src/main/java/com/sf/oauth/config/AuthSource.java
new file mode 100644
index 0000000..15e6412
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/config/AuthSource.java
@@ -0,0 +1,76 @@
+package com.sf.oauth.config;
+
+import com.sf.oauth.exception.AuthException;
+import com.sf.oauth.request.AuthDefaultRequest;
+
+/**
+ * OAuth平台的API地址的统一接口,提供以下方法:
+ * 1) {@link AuthSource#authorize()}: 获取授权url. 必须实现
+ * 2) {@link AuthSource#accessToken()}: 获取accessToken的url. 必须实现
+ * 3) {@link AuthSource#userInfo()}: 获取用户信息的url. 必须实现
+ * 4) {@link AuthSource#revoke()}: 获取取消授权的url. 非必须实现接口(部分平台不支持)
+ * 5) {@link AuthSource#refresh()}: 获取刷新授权的url. 非必须实现接口(部分平台不支持)
+ *
+ *
+ * @author zoukun
+ */
+public interface AuthSource {
+
+ /**
+ * 授权的api
+ *
+ * @return url
+ */
+ String authorize();
+
+ /**
+ * 获取accessToken的api
+ *
+ * @return url
+ */
+ String accessToken();
+
+ /**
+ * 获取用户信息的api
+ *
+ * @return url
+ */
+ String userInfo();
+
+ /**
+ * 取消授权的api
+ *
+ * @return url
+ */
+ default String revoke() {
+ throw new AuthException("UNSUPPORTED");
+ }
+
+ /**
+ * 刷新授权的api
+ *
+ * @return url
+ */
+ default String refresh() {
+ throw new AuthException("UNSUPPORTED");
+ }
+
+ /**
+ * 获取Source的字符串名字
+ *
+ * @return name
+ */
+ default String getName() {
+ if (this instanceof Enum) {
+ return String.valueOf(this);
+ }
+ return this.getClass().getSimpleName();
+ }
+
+ /**
+ * 平台对应的 AuthRequest 实现类,必须继承自 {@link AuthDefaultRequest}
+ *
+ * @return class
+ */
+ Class extends AuthDefaultRequest> getTargetClass();
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/controller/RestAuthController.java b/sf-oauth/src/main/java/com/sf/oauth/controller/RestAuthController.java
new file mode 100644
index 0000000..036574c
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/controller/RestAuthController.java
@@ -0,0 +1,143 @@
+package com.sf.oauth.controller;
+
+import cn.hutool.core.lang.UUID;
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.fastjson2.JSONObject;
+import com.sf.common.constant.Constants;
+import com.sf.common.constant.HttpStatus;
+import com.sf.common.core.domain.AjaxResult;
+import com.sf.oauth.config.AuthConfig;
+import com.sf.oauth.domain.AuthCallback;
+import com.sf.oauth.domain.AuthUser;
+import com.sf.oauth.enums.scope.AuthHuaweiScope;
+import com.sf.oauth.exception.AuthException;
+import com.sf.oauth.request.AuthHuaweiRequest;
+import com.sf.oauth.request.AuthRequest;
+import com.sf.oauth.service.IAuthService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.io.IOException;
+import java.util.Arrays;
+
+import static org.apache.commons.lang3.StringUtils.EMPTY;
+
+/**
+ * @author zoukun
+ */
+@Slf4j
+@RestController
+@RequestMapping("/oauth")
+public class RestAuthController {
+
+ @Autowired
+ private IAuthService authService;
+
+ @GetMapping("/authorize/{source}")
+ public void authorize(@PathVariable("source") String source, HttpServletResponse response) throws IOException {
+ log.info("进入render:" + source);
+ AuthRequest authRequest = getAuthRequest(source, EMPTY, EMPTY);
+ String authorizeUrl = authRequest.authorize(UUID.fastUUID().toString());
+ log.info(authorizeUrl);
+ response.sendRedirect(authorizeUrl);
+ }
+
+ /**
+ * oauth平台中配置的授权回调地址
+ * 如在创建华为授权应用时的回调地址为:http://127.0.0.1:8080/oauth/callback/huawei
+ */
+ @RequestMapping("/callback/{source}")
+ public AjaxResult callback(@PathVariable("source") String source, @RequestBody AuthCallback callback, HttpSession session) {
+ log.info("进入callback:" + source + " callback params:" + JSONObject.toJSONString(callback));
+ AuthRequest authRequest = getAuthRequest(source, callback.getClientId(), callback.getClientSecret());
+ AuthUser authUser = authRequest.callback(callback);
+ log.info(JSONObject.toJSONString(authUser));
+ AjaxResult ajax = AjaxResult.success();
+ String token = authService.authLogin(authUser,session);
+ ajax.put(Constants.TOKEN, token);
+ return ajax;
+ }
+
+ /*
+ @RequestMapping("/revoke/{source}/{userId}")
+ @ResponseBody
+ public AjaxResult revokeAuth(@PathVariable("source") String source, @PathVariable("userId") String userId) throws IOException {
+ AuthRequest authRequest = getAuthRequest(source.toLowerCase());
+
+ AuthUser user = userService.getByUuid(uuid);
+ if (null == user) {
+ return Response.error("用户不存在");
+ }
+ AjaxResult response = null;
+ try {
+ response = authRequest.revoke(user.getToken());
+ if (response.ok()) {
+ userService.remove(user.getUuid());
+ return Response.success("用户 [" + user.getUsername() + "] 的 授权状态 已收回!");
+ }
+ return Response.error("用户 [" + user.getUsername() + "] 的 授权状态 收回失败!" + response.getMsg());
+ } catch (AuthException e) {
+ return Response.error(e.getErrorMsg());
+ }
+ }
+
+ @RequestMapping("/refresh/{source}/{userId}")
+ @ResponseBody
+ public Object refreshAuth(@PathVariable("source") String source, @PathVariable("userId") String userId) {
+ AuthRequest authRequest = getAuthRequest(source.toLowerCase());
+
+ AuthUser user = userService.getByUuid(userId);
+ if (null == user) {
+ return Response.error("用户不存在");
+ }
+ AjaxResult response = null;
+ try {
+ response = authRequest.refresh(user.getToken());
+ if (response.ok()) {
+ user.setToken(response.getData());
+ userService.save(user);
+ return Response.success("用户 [" + user.getUsername() + "] 的 access token 已刷新!新的 callback: " + response.getData().getAccessToken());
+ }
+ return Response.error("用户 [" + user.getUsername() + "] 的 access token 刷新失败!" + response.getMsg());
+ } catch (AuthException e) {
+ return Response.error(e.getErrorMsg());
+ }
+ }*/
+
+ /**
+ * 根据具体的授权来源,获取授权请求工具类
+ *
+ * @param source
+ * @return
+ */
+ private AuthRequest getAuthRequest(String source, String clientId, String clientSecret) {
+ AuthRequest authRequest = null;
+ switch (source.toLowerCase()) {
+ case "huawei":
+ authRequest = new AuthHuaweiRequest(AuthConfig.builder()
+ .clientId(StrUtil.isBlank(clientId) ? "110693217" : clientId)
+ .clientSecret(StrUtil.isBlank(clientSecret) ? "1410c01bc71c7ba587175ae79e500137c70945acc1416a38127cf98a09a6f8ba" : clientSecret)
+ .redirectUri("")
+ .ignoreCheckState(true)
+ .scopes(Arrays.asList(
+ AuthHuaweiScope.BASE_PROFILE.getScope(),
+ AuthHuaweiScope.MOBILE_NUMBER.getScope(),
+ AuthHuaweiScope.ACCOUNTLIST.getScope(),
+ AuthHuaweiScope.SCOPE_DRIVE_FILE.getScope(),
+ AuthHuaweiScope.SCOPE_DRIVE_APPDATA.getScope()
+ ))
+ .build());
+ break;
+ default:
+ break;
+ }
+ if (null == authRequest) {
+ throw new AuthException(HttpStatus.BAD_REQUEST, "未获取到有效的Auth配置");
+ }
+ return authRequest;
+ }
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/domain/AuthCallback.java b/sf-oauth/src/main/java/com/sf/oauth/domain/AuthCallback.java
new file mode 100644
index 0000000..403dfb8
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/domain/AuthCallback.java
@@ -0,0 +1,42 @@
+package com.sf.oauth.domain;
+
+import lombok.*;
+
+import java.io.Serializable;
+
+/**
+ * 授权回调时的参数类
+ *
+ * @author zk
+ */
+@Getter
+@Setter
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class AuthCallback implements Serializable {
+
+ /**
+ * 访问AuthorizeUrl后回调时带的参数code
+ */
+ private String code;
+
+ /**
+ * 访问AuthorizeUrl后回调时带的参数state,用于和请求AuthorizeUrl前的state比较,防止CSRF攻击
+ */
+ private String state;
+
+ /**
+ * 华为授权登录
+ *
+ * 客户端id:对应各平台的appKey
+ */
+ private String clientId;
+
+ /**
+ * 客户端Secret:对应各平台的appSecret
+ */
+ private String clientSecret;
+
+
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/domain/AuthToken.java b/sf-oauth/src/main/java/com/sf/oauth/domain/AuthToken.java
new file mode 100644
index 0000000..902bc1c
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/domain/AuthToken.java
@@ -0,0 +1,42 @@
+package com.sf.oauth.domain;
+
+import lombok.*;
+
+import java.io.Serializable;
+
+/**
+ * 授权所需的token
+ *
+ * @author zoukun
+ */
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AuthToken implements Serializable {
+ private String accessToken;
+ private int expireIn;
+ private String refreshToken;
+ private int refreshTokenExpireIn;
+ private String uid;
+ private String openId;
+ private String accessCode;
+ private String unionId;
+
+ /**
+ * 华为返回 生成的Access Token中包含的scope。
+ */
+ private String scope;
+
+ /**
+ * 华为返回 固定返回Bearer,标识返回Access Token的类型
+ */
+ private String tokenType;
+
+ /**
+ * 华为返回 返回JWT格式数据,包含用户基本帐号、用户邮箱等信息。
+ * 参照https://developer.huawei.com/consumer/cn/doc/HMSCore-References/account-verify-id-token_hms_reference-0000001050050577#section3142132691914
+ */
+ private String idToken;
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/domain/AuthUser.java b/sf-oauth/src/main/java/com/sf/oauth/domain/AuthUser.java
new file mode 100644
index 0000000..1201e16
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/domain/AuthUser.java
@@ -0,0 +1,79 @@
+package com.sf.oauth.domain;
+
+
+import com.alibaba.fastjson2.JSONObject;
+import com.sf.oauth.enums.AuthUserGender;
+import lombok.*;
+
+import java.io.Serializable;
+
+/**
+ * 授权成功后的用户信息,根据授权平台的不同,获取的数据完整性也不同
+ *
+ * @author zoukun
+ */
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AuthUser implements Serializable {
+ /**
+ * 用户第三方系统的唯一id。
+ */
+ private String uuid;
+ /**
+ * 用户名
+ */
+ private String username;
+ /**
+ * 用户昵称
+ */
+ private String nickname;
+ /**
+ * 用户头像
+ */
+ private String avatar;
+ /**
+ * 用户网址
+ */
+ private String blog;
+ /**
+ * 所在公司
+ */
+ private String company;
+ /**
+ * 位置
+ */
+ private String location;
+ /**
+ * 用户邮箱
+ */
+ private String email;
+ /**
+ * 用户手机号
+ */
+ private String mobileNumber;
+ /**
+ * 用户备注(各平台中的用户个人介绍)
+ */
+ private String remark;
+ /**
+ * 性别
+ */
+ private AuthUserGender gender;
+ /**
+ * 用户来源
+ */
+ private String source;
+ /**
+ * 用户授权的token信息
+ */
+ private AuthToken token;
+ /**
+ * 第三方平台返回的原始用户信息
+ */
+ private JSONObject rawUserInfo;
+
+
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/enums/AuthDefaultSource.java b/sf-oauth/src/main/java/com/sf/oauth/enums/AuthDefaultSource.java
new file mode 100644
index 0000000..789a4a0
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/enums/AuthDefaultSource.java
@@ -0,0 +1,47 @@
+package com.sf.oauth.enums;
+
+import com.sf.oauth.config.AuthSource;
+import com.sf.oauth.request.AuthDefaultRequest;
+import com.sf.oauth.request.AuthHuaweiRequest;
+
+/**
+ * 内置的各api需要的url, 用枚举类分平台类型管理
+ *
+ * @author zoukun
+ */
+public enum AuthDefaultSource implements AuthSource {
+
+
+ /**
+ * 华为oauth2/v3
+ */
+ HUAWEI {
+ @Override
+ public String authorize() {
+ return "https://oauth-login.cloud.huawei.com/oauth2/v3/authorize";
+ }
+
+ @Override
+ public String accessToken() {
+ return "https://oauth-login.cloud.huawei.com/oauth2/v3/token";
+ }
+
+ @Override
+ public String userInfo() {
+ return "https://account.cloud.huawei.com/rest.php";
+ }
+
+ @Override
+ public String refresh() {
+ return "https://oauth-login.cloud.huawei.com/oauth2/v3/token";
+ }
+
+ @Override
+ public Class extends AuthDefaultRequest> getTargetClass() {
+ return AuthHuaweiRequest.class;
+ }
+ },
+
+
+
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/enums/AuthPlatformInfo.java b/sf-oauth/src/main/java/com/sf/oauth/enums/AuthPlatformInfo.java
new file mode 100644
index 0000000..ae1aa39
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/enums/AuthPlatformInfo.java
@@ -0,0 +1,51 @@
+package com.sf.oauth.enums;
+
+import java.util.*;
+
+/**
+ * @author zoukun
+ */
+public enum AuthPlatformInfo {
+
+ /**
+ * 平台
+ */
+ HUAWEI("华为", "huawei", "https://developer.huawei.com/consumer/cn/doc/HMSCore-Guides/open-platform-oauth-0000001053629189"),
+ ;
+
+ /**
+ * 平台名
+ */
+ private final String name;
+ /**
+ * 平台编码
+ */
+ private final String code;
+ /**
+ * 官网api文档
+ */
+ private final String apiDoc;
+
+ AuthPlatformInfo(String name, String code, String apiDoc) {
+ this.name = name;
+ this.code = code;
+ this.apiDoc = apiDoc;
+ }
+
+ public static List getPlatformInfos() {
+ return Arrays.asList(AuthPlatformInfo.values());
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getCode() {
+ return code;
+ }
+
+ public String getApiDoc() {
+ return apiDoc;
+ }
+
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/enums/AuthUserGender.java b/sf-oauth/src/main/java/com/sf/oauth/enums/AuthUserGender.java
new file mode 100644
index 0000000..7a76bf3
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/enums/AuthUserGender.java
@@ -0,0 +1,45 @@
+package com.sf.oauth.enums;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 用户性别
+ *
+ * @author zoukun
+ */
+@Getter
+@AllArgsConstructor
+public enum AuthUserGender {
+ /**
+ * MALE/FAMALE为正常值,通过{@link AuthUserGender#getRealGender(String)}方法获取真实的性别
+ * UNKNOWN为容错值,部分平台不会返回用户性别,为了方便统一,使用UNKNOWN标记所有未知或不可测的用户性别信息
+ */
+ MALE("0", "男"),
+ FEMALE("1", "女"),
+ UNKNOWN("2", "未知");
+
+ private String code;
+ private String desc;
+
+ /**
+ * 获取用户的实际性别,常规网站
+ *
+ * @param originalGender 用户第三方标注的原始性别
+ * @return 用户性别
+ */
+ public static AuthUserGender getRealGender(String originalGender) {
+ if (null == originalGender || UNKNOWN.getCode().equals(originalGender)) {
+ return UNKNOWN;
+ }
+ String[] males = {"m", "男", "0", "male"};
+ if (Arrays.asList(males).contains(originalGender.toLowerCase())) {
+ return MALE;
+ }
+ return FEMALE;
+ }
+
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/enums/scope/AuthHuaweiScope.java b/sf-oauth/src/main/java/com/sf/oauth/enums/scope/AuthHuaweiScope.java
new file mode 100644
index 0000000..4d61bb0
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/enums/scope/AuthHuaweiScope.java
@@ -0,0 +1,44 @@
+package com.sf.oauth.enums.scope;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 华为平台 OAuth 授权范围
+ *
+ * @author zoukun
+ */
+@Getter
+@AllArgsConstructor
+public enum AuthHuaweiScope implements AuthScope {
+
+ /**
+ * {@code scope} 含义,以{@code description} 为准
+ */
+ BASE_PROFILE("https://www.huawei.com/auth/account/base.profile", "获取用户的基本信息", true),
+ MOBILE_NUMBER("https://www.huawei.com/auth/account/mobile.number", "获取用户的手机号", false),
+ ACCOUNTLIST("https://www.huawei.com/auth/account/accountlist", "获取用户的账单列表", false),
+
+ /**
+ * 以下两个 scope 不需要经过华为评估和验证
+ */
+ SCOPE_DRIVE_FILE("https://www.huawei.com/auth/drive.file", "只允许访问由应用程序创建或打开的文件", false),
+ SCOPE_DRIVE_APPDATA("https://www.huawei.com/auth/drive.appdata", "只允许访问由应用程序创建或打开的文件", false),
+ /**
+ * 以下四个 scope 使用前需要向drivekit@huawei.com提交申请
+ *
+ * 参考:https://developer.huawei.com/consumer/cn/doc/development/HMSCore-Guides-V5/server-dev-0000001050039664-V5#ZH-CN_TOPIC_0000001050039664__section1618418855716
+ */
+ SCOPE_DRIVE("https://www.huawei.com/auth/drive", "只允许访问由应用程序创建或打开的文件", false),
+ SCOPE_DRIVE_READONLY("https://www.huawei.com/auth/drive.readonly", "只允许访问由应用程序创建或打开的文件", false),
+ SCOPE_DRIVE_METADATA("https://www.huawei.com/auth/drive.metadata", "只允许访问由应用程序创建或打开的文件", false),
+ SCOPE_DRIVE_METADATA_READONLY("https://www.huawei.com/auth/drive.metadata.readonly", "只允许访问由应用程序创建或打开的文件", false),
+
+
+ ;
+
+ private final String scope;
+ private final String description;
+ private final boolean isDefault;
+
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/enums/scope/AuthScope.java b/sf-oauth/src/main/java/com/sf/oauth/enums/scope/AuthScope.java
new file mode 100644
index 0000000..52ef1d4
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/enums/scope/AuthScope.java
@@ -0,0 +1,23 @@
+package com.sf.oauth.enums.scope;
+
+/**
+ * 各个平台 scope 类的统一接口
+ *
+ * @author zoukun
+ */
+public interface AuthScope {
+
+ /**
+ * 获取字符串 {@code scope},对应为各平台实际使用的 {@code scope}
+ *
+ * @return String
+ */
+ String getScope();
+
+ /**
+ * 判断当前 {@code scope} 是否为各平台默认启用的
+ *
+ * @return boolean
+ */
+ boolean isDefault();
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/exception/AuthException.java b/sf-oauth/src/main/java/com/sf/oauth/exception/AuthException.java
new file mode 100644
index 0000000..b0f6959
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/exception/AuthException.java
@@ -0,0 +1,46 @@
+package com.sf.oauth.exception;
+
+
+import com.sf.oauth.config.AuthSource;
+
+/**
+ * 授权异常
+ *
+ * @author zoukun
+ */
+public class AuthException extends RuntimeException {
+
+ private int errorCode;
+ private String errorMsg;
+
+ public AuthException(String errorMsg) {
+ super(errorMsg);
+ this.errorMsg = errorMsg;
+ }
+
+ public AuthException(int errorCode, String errorMsg) {
+ super(errorMsg);
+ this.errorCode = errorCode;
+ this.errorMsg = errorMsg;
+ }
+
+ public AuthException(int errorCode, String errorMsg, AuthSource source) {
+ this(errorCode, String.format("%s [%s]", errorMsg, source.getName()));
+ }
+
+ public AuthException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public AuthException(Throwable cause) {
+ super(cause);
+ }
+
+ public int getErrorCode() {
+ return errorCode;
+ }
+
+ public String getErrorMsg() {
+ return errorMsg;
+ }
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/request/AuthDefaultRequest.java b/sf-oauth/src/main/java/com/sf/oauth/request/AuthDefaultRequest.java
new file mode 100644
index 0000000..6a9f991
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/request/AuthDefaultRequest.java
@@ -0,0 +1,177 @@
+package com.sf.oauth.request;
+
+import cn.hutool.core.lang.UUID;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.core.util.URLUtil;
+import cn.hutool.http.HttpUtil;
+import com.sf.common.constant.HttpStatus;
+import com.sf.oauth.config.AuthConfig;
+import com.sf.oauth.config.AuthSource;
+import com.sf.oauth.exception.AuthException;
+import com.sf.oauth.domain.AuthCallback;
+import com.sf.oauth.domain.AuthToken;
+import com.sf.oauth.domain.AuthUser;
+import com.sf.oauth.utils.AuthChecker;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 默认的request处理类
+ *
+ * @author zoukun
+ */
+@Slf4j
+public abstract class AuthDefaultRequest implements AuthRequest {
+ protected AuthConfig config;
+ protected AuthSource source;
+
+ public AuthDefaultRequest(AuthConfig config, AuthSource source) {
+ this.config = config;
+ this.source = source;
+ if (!AuthChecker.isSupportedAuth(config, source)) {
+ throw new AuthException(HttpStatus.ERROR,"Parameter incomplete", source);
+ }
+ // 校验配置合法性
+ // AuthChecker.checkConfig(config, source);
+ }
+
+ /**
+ * 获取access token
+ *
+ * @param authCallback 授权成功后的回调参数
+ * @return token
+ * @see AuthDefaultRequest#authorize()
+ * @see AuthDefaultRequest#authorize(String)
+ */
+ protected abstract AuthToken getAccessToken(AuthCallback authCallback);
+
+ /**
+ * 使用token换取用户信息
+ *
+ * @param authToken token信息
+ * @return 用户信息
+ * @see AuthDefaultRequest#getAccessToken(AuthCallback)
+ */
+ protected abstract AuthUser getUserInfo(AuthToken authToken);
+
+ /**
+ * 统一的登录入口。当通过{@link AuthDefaultRequest#authorize(String)}授权成功后,会跳转到调用方的相关回调方法中
+ * 方法的入参可以使用{@code AuthCallback},{@code AuthCallback}类中封装好了OAuth2授权回调所需要的参数
+ *
+ * @param authCallback 用于接收回调参数的实体
+ * @return AuthResponse
+ */
+ @Override
+ public AuthUser callback(AuthCallback authCallback) {
+ try {
+ checkCode(authCallback);
+ if (!config.isIgnoreCheckState()) {
+ AuthChecker.checkState(authCallback.getState(), source);
+ }
+
+ AuthToken authToken = this.getAccessToken(authCallback);
+ return this.getUserInfo(authToken);
+ } catch (Exception e) {
+ log.error("Failed to callback with oauth authorization", e);
+ throw new AuthException(HttpStatus.UNAUTHORIZED,"Failed to callback with oauth authorization");
+ }
+ }
+
+ protected void checkCode(AuthCallback authCallback) {
+ AuthChecker.checkCode(source, authCallback);
+ }
+
+
+ /**
+ * 返回授权url,可自行跳转页面
+ *
+ * 不建议使用该方式获取授权地址,不带{@code state}的授权地址,容易受到csrf攻击。
+ * 建议使用{@link AuthDefaultRequest#authorize(String)}方法生成授权地址,在回调方法中对{@code state}进行校验
+ *
+ * @return 返回授权地址
+ * @see AuthDefaultRequest#authorize(String)
+ */
+ @Override
+ public String authorize() {
+ return this.authorize(null);
+ }
+
+ /**
+ * 返回带{@code state}参数的授权url,授权回调时会带上这个{@code state}
+ *
+ * @param state state 验证授权流程的参数,可以防止csrf
+ * @return 返回授权地址
+ */
+ @Override
+ public String authorize(String state) {
+ Map form = new HashMap<>(8);
+ form.put("client_id", config.getClientId());
+ form.put("client_secret", config.getClientSecret());
+ form.put("redirect_uri", config.getRedirectUri());
+ form.put("state", getRealState(state));
+ form.put("response_type","code");
+ return HttpUtil.get(source.authorize(), form);
+ }
+
+ /**
+ * 返回获取accessToken的url
+ *
+ * @param code 授权码
+ * @return 返回获取accessToken的url
+ */
+ protected String accessTokenUrl(String code) {
+ Map form = new HashMap<>(8);
+ form.put("code", code);
+ form.put("client_id", config.getClientId());
+ form.put("client_secret", config.getClientSecret());
+ form.put("grant_type", "authorization_code");
+ form.put("redirect_uri", config.getRedirectUri());
+ return HttpUtil.get(source.accessToken(),form);
+ }
+
+
+
+
+ /**
+ * 获取state,如果为空, 则默认取当前日期的时间戳
+ *
+ * @param state 原始的state
+ * @return 返回不为null的state
+ */
+ protected String getRealState(String state) {
+ if (StrUtil.isEmpty(state)) {
+ state = UUID.fastUUID().toString();
+ }
+ // todo 需要缓存state
+ return state;
+ }
+
+
+ /**
+ * 获取以 {@code separator}分割过后的 scope 信息
+ *
+ * @param separator 多个 {@code scope} 间的分隔符
+ * @param encode 是否 encode 编码
+ * @param defaultScopes 默认的 scope, 当客户端没有配置 {@code scopes} 时启用
+ * @return String
+ */
+ protected String getScopes(String separator, boolean encode, List defaultScopes) {
+ List scopes = config.getScopes();
+ if (null == scopes || scopes.isEmpty()) {
+ if (null == defaultScopes || defaultScopes.isEmpty()) {
+ return "";
+ }
+ scopes = defaultScopes;
+ }
+ if (null == separator) {
+ // 默认为空格
+ separator = " ";
+ }
+ String scopeStr = String.join(separator, scopes);
+ return encode ? URLUtil.encode(scopeStr) : scopeStr;
+ }
+
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/request/AuthHuaweiRequest.java b/sf-oauth/src/main/java/com/sf/oauth/request/AuthHuaweiRequest.java
new file mode 100644
index 0000000..065caf7
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/request/AuthHuaweiRequest.java
@@ -0,0 +1,168 @@
+package com.sf.oauth.request;
+
+
+import cn.hutool.http.HttpUtil;
+import com.alibaba.fastjson2.JSONObject;
+import com.sf.common.constant.HttpStatus;
+import com.sf.common.core.domain.AjaxResult;
+import com.sf.oauth.config.AuthConfig;
+import com.sf.oauth.enums.AuthDefaultSource;
+import com.sf.oauth.enums.AuthUserGender;
+import com.sf.oauth.enums.scope.AuthHuaweiScope;
+import com.sf.oauth.exception.AuthException;
+import com.sf.oauth.domain.AuthCallback;
+import com.sf.oauth.domain.AuthToken;
+import com.sf.oauth.domain.AuthUser;
+import com.sf.oauth.utils.AuthScopeUtils;
+import io.jsonwebtoken.Jwts;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * 华为授权登录
+ *
+ * @author zkun
+ */
+@Slf4j
+public class AuthHuaweiRequest extends AuthDefaultRequest {
+
+ public AuthHuaweiRequest(AuthConfig config) {
+ super(config, AuthDefaultSource.HUAWEI);
+ }
+
+
+ /**
+ * 获取access token
+ *
+ * @param authCallback 授权成功后的回调参数
+ * @return token
+ * @see AuthDefaultRequest#authorize()
+ * @see AuthDefaultRequest#authorize(String)
+ */
+ @Override
+ protected AuthToken getAccessToken(AuthCallback authCallback) {
+ Map form = new HashMap<>(8);
+ form.put("grant_type", "authorization_code");
+ form.put("code", authCallback.getCode());
+ form.put("client_id", config.getClientId());
+ form.put("client_secret", config.getClientSecret());
+ form.put("redirect_uri", config.getRedirectUri());
+ String response = HttpUtil.post(source.accessToken(), form);
+ return getAuthToken(response);
+ }
+
+ /**
+ * 使用token换取用户信息
+ *
+ * @param authToken token信息
+ * @return 用户信息
+ * @see AuthDefaultRequest#getAccessToken(AuthCallback)
+ */
+ @Override
+ protected AuthUser getUserInfo(AuthToken authToken) {
+ Map form = new HashMap<>(8);
+ form.put("nsp_ts", System.currentTimeMillis() + "");
+ form.put("access_token", authToken.getAccessToken());
+ form.put("nsp_svc", "GOpen.User.getInfo");
+ form.put("getNickName", "1");
+ String response = HttpUtil.post(source.userInfo(), form);
+ log.info(response);
+
+ JSONObject object = JSONObject.parseObject(response);
+ this.checkResponse(object);
+ AuthUserGender gender = getRealGender(object);
+ return AuthUser.builder()
+ .rawUserInfo(object)
+ .uuid(object.getString("unionID"))
+ .username(object.getString("displayName"))
+ .nickname(object.getString("displayName"))
+ .gender(gender)
+ .avatar(object.getString("headPictureURL"))
+ .mobileNumber(object.getString("mobileNumber"))
+ .email(object.getString("email"))
+ .token(authToken)
+ .source(source.toString())
+ .build();
+ }
+
+ /**
+ * 刷新access token (续期)
+ *
+ * @param authToken 登录成功后返回的Token信息
+ * @return AuthResponse
+ */
+ @Override
+ public AjaxResult refresh(AuthToken authToken) {
+ Map form = new HashMap<>(8);
+ form.put("client_id", config.getClientId());
+ form.put("client_secret", config.getClientSecret());
+ form.put("refresh_token", authToken.getRefreshToken());
+ form.put("grant_type", "refresh_token");
+ String response = HttpUtil.post(source.refresh(), form);
+ return AjaxResult.success(getAuthToken(response));
+ }
+
+ private AuthToken getAuthToken(String response) {
+ JSONObject object = JSONObject.parseObject(response);
+
+ this.checkResponse(object);
+
+ return AuthToken.builder()
+ .accessToken(object.getString("access_token"))
+ .expireIn(object.getIntValue("expires_in"))
+ .refreshToken(object.getString("refresh_token"))
+ .scope(object.getString("scope"))
+ .tokenType(object.getString("token_type"))
+ .idToken(object.getString("id_token"))
+ .build();
+ }
+
+ /**
+ * 返回带{@code state}参数的授权url,授权回调时会带上这个{@code state}
+ *
+ * @param state state 验证授权流程的参数,可以防止csrf
+ * @return 返回授权地址
+ */
+ @Override
+ public String authorize(String state) {
+ Map form = new HashMap<>(8);
+ form.put("client_id", config.getClientId());
+ form.put("client_secret", config.getClientSecret());
+ form.put("redirect_uri", config.getRedirectUri());
+ form.put("state", getRealState(state));
+ form.put("response_type", "code");
+ form.put("access_type", "offline");
+ form.put("scope", this.getScopes(" ", true, AuthScopeUtils.getDefaultScopes(AuthHuaweiScope.values())));
+ return HttpUtil.get(source.authorize(), form);
+ }
+
+
+ /**
+ * 获取用户的实际性别。华为系统中,用户的性别:1表示女,0表示男
+ *
+ * @param object obj
+ * @return AuthUserGender
+ */
+ private AuthUserGender getRealGender(JSONObject object) {
+ int genderCodeInt = object.getIntValue("gender");
+ String genderCode = genderCodeInt == 0 ? "0" : (genderCodeInt == 1) ? "1" : genderCodeInt + "";
+ return AuthUserGender.getRealGender(genderCode);
+ }
+
+ /**
+ * 校验响应结果
+ *
+ * @param object 接口返回的结果
+ */
+ private void checkResponse(JSONObject object) {
+ if (object.containsKey("NSP_STATUS")) {
+ throw new AuthException(object.getString("error"));
+ }
+ if (object.containsKey("error")) {
+ throw new AuthException(object.getString("sub_error") + ":" + object.getString("error_description"));
+ }
+ }
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/request/AuthRequest.java b/sf-oauth/src/main/java/com/sf/oauth/request/AuthRequest.java
new file mode 100644
index 0000000..5691774
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/request/AuthRequest.java
@@ -0,0 +1,76 @@
+package com.sf.oauth.request;
+
+
+import com.sf.common.constant.HttpStatus;
+import com.sf.common.core.domain.AjaxResult;
+import com.sf.oauth.domain.AuthUser;
+import com.sf.oauth.exception.AuthException;
+import com.sf.oauth.domain.AuthCallback;
+import com.sf.oauth.domain.AuthToken;
+
+/**
+ * {@code Request}公共接口,所有平台的{@code Request}都需要实现该接口
+ *
+ * {@link AuthRequest#authorize()}
+ * {@link AuthRequest#authorize(String)}
+ * {@link AuthRequest#callback(AuthCallback)}
+ * {@link AuthRequest#revoke(AuthToken)}
+ * {@link AuthRequest#refresh(AuthToken)}
+ *
+ * @author zoukun
+ */
+public interface AuthRequest {
+
+ /**
+ * 返回授权url,可自行跳转页面
+ *
+ * 不建议使用该方式获取授权地址,不带{@code state}的授权地址,容易受到csrf攻击。
+ * 建议使用{@link AuthDefaultRequest#authorize(String)}方法生成授权地址,在回调方法中对{@code state}进行校验
+ *
+ * @return 返回授权地址
+ */
+ @Deprecated
+ default String authorize() {
+ throw new AuthException(HttpStatus.UNAUTHORIZED,"Not Implemented");
+ }
+
+ /**
+ * 返回带{@code state}参数的授权url,授权回调时会带上这个{@code state}
+ *
+ * @param state state 验证授权流程的参数,可以防止csrf
+ * @return 返回授权地址
+ */
+ default String authorize(String state) {
+ throw new AuthException(HttpStatus.UNAUTHORIZED,"Not Implemented");
+ }
+
+ /**
+ * 第三方登录
+ *
+ * @param authCallback 用于接收回调参数的实体
+ * @return 返回登录成功后的用户信息
+ */
+ default AuthUser callback(AuthCallback authCallback) {
+ throw new AuthException(HttpStatus.UNAUTHORIZED,"Not Implemented");
+ }
+
+ /**
+ * 撤销授权
+ *
+ * @param authToken 登录成功后返回的Token信息
+ * @return AjaxResult
+ */
+ default AjaxResult revoke(AuthToken authToken) {
+ throw new AuthException(HttpStatus.UNAUTHORIZED,"Not Implemented");
+ }
+
+ /**
+ * 刷新access token (续期)
+ *
+ * @param authToken 登录成功后返回的Token信息
+ * @return AjaxResult
+ */
+ default AjaxResult refresh(AuthToken authToken) {
+ throw new AuthException(HttpStatus.UNAUTHORIZED,"Not Implemented");
+ }
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/service/IAuthService.java b/sf-oauth/src/main/java/com/sf/oauth/service/IAuthService.java
new file mode 100644
index 0000000..d86faa2
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/service/IAuthService.java
@@ -0,0 +1,15 @@
+package com.sf.oauth.service;
+
+import com.sf.oauth.domain.AuthUser;
+
+import javax.servlet.http.HttpSession;
+
+/**
+ * 功能描述:
+ *
+ * @author a_kun
+ * @date 2024/4/12 10:21
+ */
+public interface IAuthService {
+ String authLogin(AuthUser authUser, HttpSession session);
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/service/impl/IAuthServiceImpl.java b/sf-oauth/src/main/java/com/sf/oauth/service/impl/IAuthServiceImpl.java
new file mode 100644
index 0000000..82d2cc0
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/service/impl/IAuthServiceImpl.java
@@ -0,0 +1,62 @@
+package com.sf.oauth.service.impl;
+
+import com.sf.common.core.domain.AjaxResult;
+import com.sf.common.core.domain.entity.SysUser;
+import com.sf.common.enums.UserStatus;
+import com.sf.common.utils.SecurityUtils;
+import com.sf.common.utils.ip.IpUtils;
+import com.sf.framework.web.service.SysLoginService;
+import com.sf.oauth.domain.AuthUser;
+import com.sf.oauth.exception.AuthException;
+import com.sf.oauth.service.IAuthService;
+import com.sf.system.service.ISysUserService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.servlet.http.HttpSession;
+import java.util.Date;
+
+/**
+ * 功能描述:
+ *
+ * @author a_kun
+ * @date 2024/4/12 10:21
+ */
+@Slf4j
+@Service
+public class IAuthServiceImpl implements IAuthService {
+
+ @Autowired
+ private SysLoginService loginService;
+
+ @Autowired
+ private ISysUserService userService;
+
+
+ @Override
+ public String authLogin(AuthUser authUser, HttpSession session) {
+ // TODO 应该要登录后绑定一个平台账号,没有平台账号要创建,保存用户的三方来源,及用户的三方唯一标识和绑定的用户
+ if (authUser != null) {
+ SysUser sysUser = new SysUser();
+ sysUser.setUserName(authUser.getUsername());
+ if (userService.checkUserNameUnique(sysUser)) {
+ // 注册用户后登录
+ sysUser.setNickName(authUser.getNickname());
+ sysUser.setEmail(authUser.getEmail());
+ sysUser.setPhonenumber(authUser.getMobileNumber());
+ sysUser.setSex(authUser.getGender().getCode());
+ sysUser.setAvatar(authUser.getAvatar());
+ sysUser.setPassword(SecurityUtils.encryptPassword("ztzh@sac123"));
+ sysUser.setStatus(UserStatus.OK.getCode());
+ sysUser.setDelFlag("0");
+ sysUser.setLoginIp(IpUtils.getHostIp());
+ sysUser.setLoginDate(new Date());
+ userService.insertUser(sysUser);
+ }
+ // 生成令牌
+ return loginService.noPwdLogin(authUser.getUsername(), session);
+ }
+ throw new AuthException("login error");
+ }
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/utils/AuthChecker.java b/sf-oauth/src/main/java/com/sf/oauth/utils/AuthChecker.java
new file mode 100644
index 0000000..9fe8961
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/utils/AuthChecker.java
@@ -0,0 +1,79 @@
+package com.sf.oauth.utils;
+
+
+import cn.hutool.core.util.StrUtil;
+import com.sf.common.constant.HttpStatus;
+import com.sf.oauth.config.AuthConfig;
+import com.sf.oauth.enums.AuthDefaultSource;
+import com.sf.oauth.config.AuthSource;
+import com.sf.oauth.exception.AuthException;
+import com.sf.oauth.domain.AuthCallback;
+
+/**
+ * 授权配置类的校验器
+ *
+ * @author ZK
+ */
+public class AuthChecker {
+
+ /**
+ * 是否支持第三方登录
+ *
+ * @param config config
+ * @param source source
+ * @return true or false
+ */
+ public static boolean isSupportedAuth(AuthConfig config, AuthSource source) {
+
+ return StrUtil.isNotEmpty(config.getClientId())
+ && StrUtil.isNotEmpty(config.getClientSecret())
+ && null != source;
+ }
+
+ /**
+ * 检查配置合法性。针对部分平台, 对redirect uri有特定要求。一般来说redirect uri都是http://,
+ * 而对于部分平台, redirect uri 必须是https的链接
+ *
+ * @param config config
+ * @param source source
+ */
+ public static void checkConfig(AuthConfig config, AuthSource source) {
+ String redirectUri = config.getRedirectUri();
+ if (StrUtil.isEmpty(redirectUri)) {
+ throw new AuthException(HttpStatus.BAD_REQUEST, "Illegal redirect uri", source);
+ }
+ if (!AuthUtils.isHttpProtocol(redirectUri) && !AuthUtils.isHttpsProtocol(redirectUri)) {
+ throw new AuthException(HttpStatus.BAD_REQUEST, "Illegal redirect uri", source);
+ }
+ }
+
+ /**
+ * 校验回调传回的code
+ *
+ * {@code v1.10.0}版本中改为传入{@code source}和{@code callback},对于不同平台使用不同参数接受code的情况统一做处理
+ *
+ * @param source 当前授权平台
+ * @param callback 从第三方授权回调回来时传入的参数集合
+ */
+ public static void checkCode(AuthSource source, AuthCallback callback) {
+ if (StrUtil.isEmpty(callback.getCode())) {
+ throw new AuthException(HttpStatus.UNAUTHORIZED,"Illegal code", source);
+ }
+ }
+
+ /**
+ * 校验回调传回的{@code state},为空或者不存在
+ *
+ * {@code state}不存在的情况只有两种:
+ * 1. {@code state}已使用,被正常清除
+ * 2. {@code state}为前端伪造,本身就不存在
+ *
+ * @param state {@code state}一定不为空
+ * @param source {@code source}当前授权平台
+ */
+ public static void checkState(String state, AuthSource source) {
+ if (StrUtil.isEmpty(state)) {
+ throw new AuthException(HttpStatus.UNAUTHORIZED,"Illegal state", source);
+ }
+ }
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/utils/AuthScopeUtils.java b/sf-oauth/src/main/java/com/sf/oauth/utils/AuthScopeUtils.java
new file mode 100644
index 0000000..9350882
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/utils/AuthScopeUtils.java
@@ -0,0 +1,44 @@
+package com.sf.oauth.utils;
+
+import com.sf.oauth.enums.scope.AuthScope;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Scope 工具类,提供对 scope 类的统一操作
+ *
+ * @author zoukun
+ */
+public class AuthScopeUtils {
+
+ /**
+ * 获取 {@link com.sf.oauth.enums.scope.AuthScope} 数组中所有的被标记为 {@code default} 的 scope
+ *
+ * @param scopes scopes
+ * @return List
+ */
+ public static List getDefaultScopes(AuthScope[] scopes) {
+ if (null == scopes || scopes.length == 0) {
+ return null;
+ }
+ return Arrays.stream(scopes)
+ .filter((AuthScope::isDefault))
+ .map(AuthScope::getScope)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 从 {@link com.sf.oauth.enums.scope.AuthScope} 数组中获取实际的 scope 字符串
+ *
+ * @param scopes 可变参数,支持传任意 {@link com.sf.oauth.enums.scope.AuthScope}
+ * @return List
+ */
+ public static List getScopes(AuthScope... scopes) {
+ if (null == scopes || scopes.length == 0) {
+ return null;
+ }
+ return Arrays.stream(scopes).map(AuthScope::getScope).collect(Collectors.toList());
+ }
+}
diff --git a/sf-oauth/src/main/java/com/sf/oauth/utils/AuthUtils.java b/sf-oauth/src/main/java/com/sf/oauth/utils/AuthUtils.java
new file mode 100644
index 0000000..3d3e22c
--- /dev/null
+++ b/sf-oauth/src/main/java/com/sf/oauth/utils/AuthUtils.java
@@ -0,0 +1,127 @@
+
+package com.sf.oauth.utils;
+
+import cn.hutool.core.util.StrUtil;
+import com.sf.oauth.exception.AuthException;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class AuthUtils {
+ private static final Charset DEFAULT_ENCODING;
+ private static final String HMAC_SHA1 = "HmacSHA1";
+ private static final String HMAC_SHA_256 = "HmacSHA256";
+
+ public AuthUtils() {
+ }
+
+ public static String urlEncode(String value) {
+ if (value == null) {
+ return "";
+ } else {
+ try {
+ String encoded = URLEncoder.encode(value, DEFAULT_ENCODING.displayName());
+ return encoded.replace("+", "%20").replace("*", "%2A").replace("~", "%7E").replace("/", "%2F");
+ } catch (UnsupportedEncodingException var2) {
+ throw new AuthException("Failed To Encode Uri", var2);
+ }
+ }
+ }
+
+ public static String urlDecode(String value) {
+ if (value == null) {
+ return "";
+ } else {
+ try {
+ return URLDecoder.decode(value, DEFAULT_ENCODING.displayName());
+ } catch (UnsupportedEncodingException var2) {
+ throw new AuthException("Failed To Decode Uri", var2);
+ }
+ }
+ }
+
+
+ public static String parseMapToString(Map params, boolean encode) {
+ if (null != params && !params.isEmpty()) {
+ List paramList = new ArrayList();
+ params.forEach((k, v) -> {
+ if (null == v) {
+ paramList.add(k + "=");
+ } else {
+ paramList.add(k + "=" + (encode ? urlEncode(v) : v));
+ }
+
+ });
+ return String.join("&", paramList);
+ } else {
+ return "";
+ }
+ }
+
+ public static boolean isHttpProtocol(String url) {
+ if (StrUtil.isEmpty(url)) {
+ return false;
+ } else {
+ return url.startsWith("http://") || url.startsWith("http%3A%2F%2F");
+ }
+ }
+
+ public static boolean isHttpsProtocol(String url) {
+ if (StrUtil.isEmpty(url)) {
+ return false;
+ } else {
+ return url.startsWith("https://") || url.startsWith("https%3A%2F%2F");
+ }
+ }
+
+ public static boolean isLocalHost(String url) {
+ return StrUtil.isEmpty(url) || url.contains("127.0.0.1") || url.contains("localhost");
+ }
+
+ public static boolean isHttpsProtocolOrLocalHost(String url) {
+ if (StrUtil.isEmpty(url)) {
+ return false;
+ } else {
+ return isHttpsProtocol(url) || isLocalHost(url);
+ }
+ }
+
+
+ public static String getTimestamp() {
+ return String.valueOf(System.currentTimeMillis() / 1000L);
+ }
+
+
+ public static String md5(String str) {
+ MessageDigest md = null;
+ StringBuilder buffer = null;
+
+ try {
+ md = MessageDigest.getInstance("MD5");
+ md.update(str.getBytes(StandardCharsets.UTF_8));
+ byte[] byteData = md.digest();
+ buffer = new StringBuilder();
+ byte[] var4 = byteData;
+ int var5 = byteData.length;
+
+ for(int var6 = 0; var6 < var5; ++var6) {
+ byte byteDatum = var4[var6];
+ buffer.append(Integer.toString((byteDatum & 255) + 256, 16).substring(1));
+ }
+ } catch (Exception var8) {
+ }
+
+ return null == buffer ? "" : buffer.toString();
+ }
+
+ static {
+ DEFAULT_ENCODING = StandardCharsets.UTF_8;
+ }
+}