From 666575770a67560cf303bad041a4697ac24acc80 Mon Sep 17 00:00:00 2001 From: akun <957746831@qq.com> Date: Fri, 12 Apr 2024 12:54:25 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=B8=89=E6=96=B9=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E6=A8=A1=E5=9D=97=EF=BC=8C=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=8D=8E=E4=B8=BA=E8=B4=A6=E5=8F=B7=E5=85=8D=E5=AF=86=E7=99=BB?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 10 +- sf-admin/pom.xml | 6 + .../controller/system/SysLoginController.java | 5 +- .../sf/framework/config/SecurityConfig.java | 2 +- .../web/service/SysLoginService.java | 32 +++- .../web/service/SysPasswordService.java | 3 + sf-oauth/pom.xml | 32 ++++ .../java/com/sf/oauth/config/AuthConfig.java | 41 ++++ .../java/com/sf/oauth/config/AuthSource.java | 76 ++++++++ .../oauth/controller/RestAuthController.java | 143 ++++++++++++++ .../com/sf/oauth/domain/AuthCallback.java | 42 +++++ .../java/com/sf/oauth/domain/AuthToken.java | 42 +++++ .../java/com/sf/oauth/domain/AuthUser.java | 79 ++++++++ .../com/sf/oauth/enums/AuthDefaultSource.java | 47 +++++ .../com/sf/oauth/enums/AuthPlatformInfo.java | 51 +++++ .../com/sf/oauth/enums/AuthUserGender.java | 45 +++++ .../sf/oauth/enums/scope/AuthHuaweiScope.java | 44 +++++ .../com/sf/oauth/enums/scope/AuthScope.java | 23 +++ .../com/sf/oauth/exception/AuthException.java | 46 +++++ .../sf/oauth/request/AuthDefaultRequest.java | 177 ++++++++++++++++++ .../sf/oauth/request/AuthHuaweiRequest.java | 168 +++++++++++++++++ .../com/sf/oauth/request/AuthRequest.java | 76 ++++++++ .../com/sf/oauth/service/IAuthService.java | 15 ++ .../oauth/service/impl/IAuthServiceImpl.java | 62 ++++++ .../java/com/sf/oauth/utils/AuthChecker.java | 79 ++++++++ .../com/sf/oauth/utils/AuthScopeUtils.java | 44 +++++ .../java/com/sf/oauth/utils/AuthUtils.java | 127 +++++++++++++ 27 files changed, 1507 insertions(+), 10 deletions(-) create mode 100644 sf-oauth/pom.xml create mode 100644 sf-oauth/src/main/java/com/sf/oauth/config/AuthConfig.java create mode 100644 sf-oauth/src/main/java/com/sf/oauth/config/AuthSource.java create mode 100644 sf-oauth/src/main/java/com/sf/oauth/controller/RestAuthController.java create mode 100644 sf-oauth/src/main/java/com/sf/oauth/domain/AuthCallback.java create mode 100644 sf-oauth/src/main/java/com/sf/oauth/domain/AuthToken.java create mode 100644 sf-oauth/src/main/java/com/sf/oauth/domain/AuthUser.java create mode 100644 sf-oauth/src/main/java/com/sf/oauth/enums/AuthDefaultSource.java create mode 100644 sf-oauth/src/main/java/com/sf/oauth/enums/AuthPlatformInfo.java create mode 100644 sf-oauth/src/main/java/com/sf/oauth/enums/AuthUserGender.java create mode 100644 sf-oauth/src/main/java/com/sf/oauth/enums/scope/AuthHuaweiScope.java create mode 100644 sf-oauth/src/main/java/com/sf/oauth/enums/scope/AuthScope.java create mode 100644 sf-oauth/src/main/java/com/sf/oauth/exception/AuthException.java create mode 100644 sf-oauth/src/main/java/com/sf/oauth/request/AuthDefaultRequest.java create mode 100644 sf-oauth/src/main/java/com/sf/oauth/request/AuthHuaweiRequest.java create mode 100644 sf-oauth/src/main/java/com/sf/oauth/request/AuthRequest.java create mode 100644 sf-oauth/src/main/java/com/sf/oauth/service/IAuthService.java create mode 100644 sf-oauth/src/main/java/com/sf/oauth/service/impl/IAuthServiceImpl.java create mode 100644 sf-oauth/src/main/java/com/sf/oauth/utils/AuthChecker.java create mode 100644 sf-oauth/src/main/java/com/sf/oauth/utils/AuthScopeUtils.java create mode 100644 sf-oauth/src/main/java/com/sf/oauth/utils/AuthUtils.java diff --git a/pom.xml b/pom.xml index 9649529..cb31587 100644 --- a/pom.xml +++ b/pom.xml @@ -161,6 +161,13 @@ ${sf.version} + + + com.smarterFramework + sf-oauth + ${sf.version} + + com.smarterFramework @@ -200,7 +207,8 @@ sf-file sf-common sf-apijson - + sf-oauth + pom diff --git a/sf-admin/pom.xml b/sf-admin/pom.xml index 781eeae..276a779 100644 --- a/sf-admin/pom.xml +++ b/sf-admin/pom.xml @@ -60,6 +60,12 @@ com.smarterFramework sf-file + + + com.smarterFramework + sf-oauth + + com.smarterFramework sf-order diff --git a/sf-admin/src/main/java/com/sf/web/controller/system/SysLoginController.java b/sf-admin/src/main/java/com/sf/web/controller/system/SysLoginController.java index cc05d23..2bfa6b5 100644 --- a/sf-admin/src/main/java/com/sf/web/controller/system/SysLoginController.java +++ b/sf-admin/src/main/java/com/sf/web/controller/system/SysLoginController.java @@ -48,9 +48,10 @@ public class SysLoginController public AjaxResult login(@RequestBody LoginBody loginBody, HttpSession session) { AjaxResult ajax = AjaxResult.success(); + loginService.validateCaptcha(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode()); // 生成令牌 - String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(), - loginBody.getUuid(), session); + String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), + session); ajax.put(Constants.TOKEN, token); return ajax; } diff --git a/sf-framework/src/main/java/com/sf/framework/config/SecurityConfig.java b/sf-framework/src/main/java/com/sf/framework/config/SecurityConfig.java index 3585a87..801c068 100644 --- a/sf-framework/src/main/java/com/sf/framework/config/SecurityConfig.java +++ b/sf-framework/src/main/java/com/sf/framework/config/SecurityConfig.java @@ -112,7 +112,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter // 过滤请求 .authorizeRequests() // 对于登录login 注册register 验证码captchaImage 允许匿名访问 - .antMatchers("/login", "/register", "/captchaImage").permitAll() + .antMatchers("/login", "/register", "/captchaImage","/oauth/**").permitAll() // 静态资源,可匿名访问 .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll() diff --git a/sf-framework/src/main/java/com/sf/framework/web/service/SysLoginService.java b/sf-framework/src/main/java/com/sf/framework/web/service/SysLoginService.java index 9054eb0..737f482 100644 --- a/sf-framework/src/main/java/com/sf/framework/web/service/SysLoginService.java +++ b/sf-framework/src/main/java/com/sf/framework/web/service/SysLoginService.java @@ -1,6 +1,7 @@ package com.sf.framework.web.service; import javax.annotation.Resource; +import javax.security.auth.message.AuthException; import javax.servlet.http.HttpSession; import org.springframework.beans.factory.annotation.Autowired; @@ -32,6 +33,7 @@ import com.sf.framework.manager.factory.AsyncFactory; import com.sf.framework.security.context.AuthenticationContextHolder; import com.sf.system.service.ISysConfigService; import com.sf.system.service.ISysUserService; +import org.springframework.util.Assert; /** * 登录校验方法 @@ -56,19 +58,18 @@ public class SysLoginService @Autowired private ISysConfigService configService; + @Autowired + private SysPermissionService permissionService; + /** * 登录验证 - * + * * @param username 用户名 * @param password 密码 - * @param code 验证码 - * @param uuid 唯一标识 * @return 结果 */ - public String login(String username, String password, String code, String uuid, HttpSession session) + public String login(String username, String password, HttpSession session) { - // 验证码校验 - validateCaptcha(username, code, uuid); // 登录前置校验 loginPreCheck(username, password); // 用户验证 @@ -105,6 +106,25 @@ public class SysLoginService return tokenService.createToken(loginUser); } + /** + * 无密码登录 + * @param userName + * @return + * @author zoukun + */ + public String noPwdLogin(String userName, HttpSession session){ + SysUser user = userService.selectUserByUserName(userName); + Assert.notNull(user,MessageUtils.message("user.not.exists")); + LoginUser loginUser = new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user)); + // 记录登陆信息 + AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGIN_SUCCESS, + MessageUtils.message("user.login.success"))); + recordLoginInfo(loginUser.getUserId()); + ApijsonUtils.buildFormSession(session, String.valueOf(loginUser.getUserId())); + return tokenService.createToken(loginUser); + } + + /** * 校验验证码 * diff --git a/sf-framework/src/main/java/com/sf/framework/web/service/SysPasswordService.java b/sf-framework/src/main/java/com/sf/framework/web/service/SysPasswordService.java index 2c48b60..ab9cf6e 100644 --- a/sf-framework/src/main/java/com/sf/framework/web/service/SysPasswordService.java +++ b/sf-framework/src/main/java/com/sf/framework/web/service/SysPasswordService.java @@ -1,6 +1,8 @@ package com.sf.framework.web.service; import java.util.concurrent.TimeUnit; + +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; @@ -23,6 +25,7 @@ import com.sf.framework.security.context.AuthenticationContextHolder; * * @author ztzh */ +@Slf4j @Component public class SysPasswordService { diff --git a/sf-oauth/pom.xml b/sf-oauth/pom.xml new file mode 100644 index 0000000..aa73cb3 --- /dev/null +++ b/sf-oauth/pom.xml @@ -0,0 +1,32 @@ + + + + smarterFramework + com.smarterFramework + 1.0.0 + + 4.0.0 + + sf-oauth + + + oauth三方登录认证模块 + + + + + + + com.smarterFramework + sf-common + + + com.smarterFramework + sf-framework + + + + + \ No newline at end of file diff --git a/sf-oauth/src/main/java/com/sf/oauth/config/AuthConfig.java b/sf-oauth/src/main/java/com/sf/oauth/config/AuthConfig.java new file mode 100644 index 0000000..3a30f11 --- /dev/null +++ b/sf-oauth/src/main/java/com/sf/oauth/config/AuthConfig.java @@ -0,0 +1,41 @@ +package com.sf.oauth.config; + +import lombok.*; + +import java.util.List; + +/** + * 配置类 + * + * @author zoukun + */ +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AuthConfig { + + /** + * 客户端id:对应各平台的appKey + */ + private String clientId; + + /** + * 客户端Secret:对应各平台的appSecret + */ + private String clientSecret; + + /** + * 登录成功后的回调地址 + */ + private String redirectUri; + + /** + * 支持自定义授权平台的 scope 内容 + */ + private List 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 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 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; + } +} From e9c9a6ea589919fec9de256bf29060823258bfd8 Mon Sep 17 00:00:00 2001 From: akun <957746831@qq.com> Date: Fri, 12 Apr 2024 12:58:26 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 6 ++++++ sf-admin/pom.xml | 2 -- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index cb31587..a82b5d6 100644 --- a/pom.xml +++ b/pom.xml @@ -168,6 +168,12 @@ ${sf.version} + + com.smarterFramework + sf-order + ${sf.version} + + com.smarterFramework diff --git a/sf-admin/pom.xml b/sf-admin/pom.xml index 276a779..067964b 100644 --- a/sf-admin/pom.xml +++ b/sf-admin/pom.xml @@ -69,8 +69,6 @@ com.smarterFramework sf-order - 1.0.0 - compile