- 一直想做一款后台管理系统,看了很多优秀的开源项目但是发现没有合适自己的。于是利用空闲休息时间开始自己写一套后台系统。如此有了若依管理系统,她可以用于所有的Web应用程序,如网站管理后台,网站会员中心,CMS,CRM,OA等等,当然,您也可以对她进行深度定制,以做出更强系统。所有前端后台代码封装过后十分精简易上手,出错概率低。同时支持移动客户端访问。系统会陆续更新一些实用功能。 + 一直想做一款后台管理系统,看了很多优秀的开源项目但是发现没有合适自己的。于是利用空闲休息时间开始自己写一套后台系统。如此有了SAC管理系统,她可以用于所有的Web应用程序,如网站管理后台,网站会员中心,CMS,CRM,OA等等,当然,您也可以对她进行深度定制,以做出更强系统。所有前端后台代码封装过后十分精简易上手,出错概率低。同时支持移动客户端访问。系统会陆续更新一些实用功能。
当前版本: v{{ version }} @@ -120,21 +120,21 @@
QQ群: 满937441 满887144332
满180251782 满104180207 满186866453 满201396349
- 满101456076 满101539465 满264312783 满167385320
- 满104748341 满160110482 满170801498 满108482800
+ 满101456076 满101539465 满264312783 满167385320
+ 满104748341 满160110482 满170801498 满108482800
满101046199 满136919097 143961921
微信:/ *若依/ *SAC
支付宝:/ *若依/ *SAC
@@ -913,7 +913,7 @@密码:{{ scope.row.verifyCode }}
+密码:{{ scope.row.verifyCode }}
- 领取阿里云通用云产品1888优惠券 --
-https://www.aliyun.com/minisite/goods?userCode=brki8iof -
- 领取腾讯云通用云产品2860优惠券 -
-https://cloud.tencent.com/redirect.php?redirect=1025&cps_key=198c8df2ed259157187173bc7f4f32fd&from=console -
- 阿里云服务器折扣区 ->☛☛点我进入☚☚ - 腾讯云服务器秒杀区 ->☛☛点我进入☚☚
-- 云产品通用红包,可叠加官网常规优惠使用。(仅限新用户) -
-
- 一直想做一款后台管理系统,看了很多优秀的开源项目但是发现没有合适自己的。于是利用空闲休息时间开始自己写一套后台系统。如此有了SAC管理系统,她可以用于所有的Web应用程序,如网站管理后台,网站会员中心,CMS,CRM,OA等等,当然,您也可以对她进行深度定制,以做出更强系统。所有前端后台代码封装过后十分精简易上手,出错概率低。同时支持移动客户端访问。系统会陆续更新一些实用功能。 -
-- 当前版本: v{{ version }} -
-
-
-
+ *
+ * @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
+ * 参考: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
+ * {@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