From 8d81b35b252612d0a6d85f2478d9c4de2b43c41f Mon Sep 17 00:00:00 2001 From: MaxKey Date: Sun, 26 Mar 2023 13:51:23 +0800 Subject: [PATCH] =?UTF-8?q?OAuth=202=20=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/server/pom.xml | 5 ++ .../java/io/jpom/controller/IndexControl.java | 7 ++ .../java/io/jpom/controller/LoginControl.java | 78 ++++++++++++++++++ .../jpom/oauth2/AuthOauth2CustomRequest.java | 72 ++++++++++++++++ .../java/io/jpom/oauth2/Oauth2AuthSource.java | 59 +++++++++++++ .../java/io/jpom/system/ServerConfig.java | 21 +++++ .../server/src/main/resources/application.yml | 12 +++ .../server/src/main/resources/logo/oauth2.png | Bin 0 -> 13391 bytes web-vue/src/api/user/user.js | 17 ++++ web-vue/src/pages/login/index.vue | 28 ++++++- 10 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 modules/server/src/main/java/io/jpom/oauth2/AuthOauth2CustomRequest.java create mode 100644 modules/server/src/main/java/io/jpom/oauth2/Oauth2AuthSource.java create mode 100644 modules/server/src/main/resources/logo/oauth2.png diff --git a/modules/server/pom.xml b/modules/server/pom.xml index 8aabb8e36..67d96bde7 100644 --- a/modules/server/pom.xml +++ b/modules/server/pom.xml @@ -129,6 +129,11 @@ storage-module-mysql ${project.version} + + me.zhyd.oauth + JustAuth + 1.16.5 + diff --git a/modules/server/src/main/java/io/jpom/controller/IndexControl.java b/modules/server/src/main/java/io/jpom/controller/IndexControl.java index 07b66dac6..751ff64eb 100644 --- a/modules/server/src/main/java/io/jpom/controller/IndexControl.java +++ b/modules/server/src/main/java/io/jpom/controller/IndexControl.java @@ -205,6 +205,13 @@ public class IndexControl extends BaseServerController { // InputStream inputStream = ResourceUtil.getStream("classpath:/logo/favicon.ico"); // ServletUtil.write(response, inputStream, MediaType.IMAGE_PNG_VALUE); } + + @RequestMapping(value = "oauth2_image", method = RequestMethod.GET, produces = MediaType.IMAGE_PNG_VALUE) + @NotLogin + public void oauth2Image(HttpServletResponse response) throws IOException { + String logoFile = webConfig.getLogoFile(); + this.loadImage(response, logoFile, "classpath:/logo/oauth2.png", "jpg", "png", "gif"); + } private void loadImage(HttpServletResponse response, String imgFile, String defaultResource, String... suffix) throws IOException { if (StrUtil.isNotEmpty(imgFile)) { diff --git a/modules/server/src/main/java/io/jpom/controller/LoginControl.java b/modules/server/src/main/java/io/jpom/controller/LoginControl.java index 37b7f5c77..9657f862b 100644 --- a/modules/server/src/main/java/io/jpom/controller/LoginControl.java +++ b/modules/server/src/main/java/io/jpom/controller/LoginControl.java @@ -46,6 +46,8 @@ import io.jpom.func.user.server.UserLoginLogServer; import io.jpom.model.data.WorkspaceModel; import io.jpom.model.dto.UserLoginDto; import io.jpom.model.user.UserModel; +import io.jpom.oauth2.AuthOauth2CustomRequest; +import io.jpom.oauth2.Oauth2AuthSource; import io.jpom.permission.ClassFeature; import io.jpom.permission.Feature; import io.jpom.service.user.UserBindWorkspaceService; @@ -56,12 +58,18 @@ import io.jpom.util.StringUtil; import org.springframework.http.MediaType; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; +import me.zhyd.oauth.config.AuthConfig; +import me.zhyd.oauth.model.AuthCallback; +import me.zhyd.oauth.model.AuthResponse; +import me.zhyd.oauth.model.AuthUser; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.awt.*; import java.io.IOException; +import java.util.HashMap; import java.util.List; import java.util.concurrent.TimeUnit; @@ -89,6 +97,9 @@ public class LoginControl extends BaseServerController { private final ServerConfig.UserConfig userConfig; private final ServerConfig.WebConfig webConfig; private final UserLoginLogServer userLoginLogServer; + private final ServerConfig.OauthConfig oauthConfig; + private AuthOauth2CustomRequest authOauth2Request; + public LoginControl(UserService userService, UserBindWorkspaceService userBindWorkspaceService, @@ -98,7 +109,16 @@ public class LoginControl extends BaseServerController { this.userBindWorkspaceService = userBindWorkspaceService; this.userConfig = serverConfig.getUser(); this.webConfig = serverConfig.getWeb(); + this.oauthConfig = serverConfig.getOauth2(); this.userLoginLogServer = userLoginLogServer; + + AuthConfig authConfig = AuthConfig.builder() + .clientId(oauthConfig.getClientId()) + .clientSecret(oauthConfig.getClientSecret()) + .redirectUri(oauthConfig.getRedirectUri()) + .build(); + + authOauth2Request = new AuthOauth2CustomRequest( authConfig , new Oauth2AuthSource(oauthConfig)); } /** @@ -154,6 +174,7 @@ public class LoginControl extends BaseServerController { return count > userConfig.getAlwaysIpLoginError(); } + /** * 登录接口 * @@ -228,6 +249,63 @@ public class LoginControl extends BaseServerController { } } } + + /** + * oauth 状态检查 + * @param response + * @return json + * @throws IOException + */ + @RequestMapping(value = "oauth2/state", method = RequestMethod.GET) + @ResponseBody + @NotLogin + public HashMap oauth2LoginState(HttpServletResponse response) throws IOException { + HashMap data = new HashMap(); + data.put("code", 200); + data.put("enabled", oauthConfig.isEnabled()); + return data; + } + + /** + * 跳转到认证中心登录 + * @param request + * @return + */ + @GetMapping(value = "oauth2/login") + @NotLogin + public ModelAndView oauth2Login(HttpServletRequest request){ + return new ModelAndView("redirect:" + authOauth2Request.authorize(null)); + } + + /** + * oauth2 登录并获取token + * @param code + * @param state + * @param request + * @return + */ + @PostMapping(value = "oauth2/login", produces = MediaType.APPLICATION_JSON_VALUE) + @NotLogin + public JsonMessage oauth2Callback( + @RequestParam(value = "code", required = true) String code, + @RequestParam(value = "state", required = false)String state, + HttpServletRequest request){ + AuthCallback authCallback = new AuthCallback(); + authCallback.setCode(code); + authCallback.setState(state); + AuthResponse authResponse = authOauth2Request.login(authCallback); + AuthUser authUser = (AuthUser)authResponse.getData(); + if(authUser != null) { + UserModel userModel = userService.getByKey(authUser.getUsername()); + if (userModel == null) { + return new JsonMessage<>(400, "OAuth 2 登录失败,用户不存在请联系管理员!"); + } + UserLoginDto userLoginDto = this.createToken(userModel); + userLoginLogServer.success(userModel, false, request); + return new JsonMessage<>(200, "OAuth 2 登录成功", userLoginDto); + } + return new JsonMessage<>(400, "OAuth 2 登录失败,请联系管理员!"); + } private UserLoginDto createToken(UserModel userModel) { // 判断工作空间 diff --git a/modules/server/src/main/java/io/jpom/oauth2/AuthOauth2CustomRequest.java b/modules/server/src/main/java/io/jpom/oauth2/AuthOauth2CustomRequest.java new file mode 100644 index 000000000..d9022f930 --- /dev/null +++ b/modules/server/src/main/java/io/jpom/oauth2/AuthOauth2CustomRequest.java @@ -0,0 +1,72 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Code Technology Studio + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package io.jpom.oauth2; + +import com.alibaba.fastjson2.JSONObject; + +import me.zhyd.oauth.config.AuthConfig; +import me.zhyd.oauth.config.AuthSource; +import me.zhyd.oauth.model.AuthCallback; +import me.zhyd.oauth.model.AuthToken; +import me.zhyd.oauth.model.AuthUser; +import me.zhyd.oauth.request.AuthDefaultRequest; + +public class AuthOauth2CustomRequest extends AuthDefaultRequest { + + public AuthOauth2CustomRequest(AuthConfig config, AuthSource source) { + super(config, source); + } + + @Override + protected AuthToken getAccessToken(AuthCallback authCallback) { + String body = doPostAuthorizationCode(authCallback.getCode()); + JSONObject object = JSONObject.parseObject(body); + AuthToken authToken = + AuthToken.builder() + .accessToken(object.getString("access_token")) + .refreshToken(object.getString("refresh_token")) + .idToken(object.getString("id_token")) + .tokenType(object.getString("token_type")) + .scope(object.getString("scope")) + .build(); + return authToken; + } + + @Override + protected AuthUser getUserInfo(AuthToken authToken) { + String body = doGetUserInfo(authToken); + JSONObject object = JSONObject.parseObject(body); + AuthUser authUser = + AuthUser.builder() + .uuid(object.getString("id")) + .username(object.getString("username")) + .nickname(object.getString("name")) + .company(object.getString("organization")) + .email(object.getString("email")) + .token(authToken) + .source(source.toString()) + .build(); + return authUser; + } + +} diff --git a/modules/server/src/main/java/io/jpom/oauth2/Oauth2AuthSource.java b/modules/server/src/main/java/io/jpom/oauth2/Oauth2AuthSource.java new file mode 100644 index 000000000..4f7ba3e23 --- /dev/null +++ b/modules/server/src/main/java/io/jpom/oauth2/Oauth2AuthSource.java @@ -0,0 +1,59 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Code Technology Studio + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package io.jpom.oauth2; +import io.jpom.system.ServerConfig; +import io.jpom.system.ServerConfig.OauthConfig; +import me.zhyd.oauth.config.AuthSource; +import me.zhyd.oauth.request.AuthDefaultRequest; + +public class Oauth2AuthSource implements AuthSource{ + + ServerConfig.OauthConfig oauthConfig; + + public Oauth2AuthSource(OauthConfig oauthConfig) { + super(); + this.oauthConfig = oauthConfig; + } + + @Override + public String authorize() { + return oauthConfig.getAuthorizationUri(); + } + + @Override + public String accessToken() { + return oauthConfig.getAccessTokenUri(); + } + + @Override + public String userInfo() { + return oauthConfig.getUserInfoUri(); + } + + @Override + public Class getTargetClass() { + // TODO Auto-generated method stub + return AuthOauth2CustomRequest.class; + } + +} diff --git a/modules/server/src/main/java/io/jpom/system/ServerConfig.java b/modules/server/src/main/java/io/jpom/system/ServerConfig.java index 6218649c4..4ea2e4fc4 100644 --- a/modules/server/src/main/java/io/jpom/system/ServerConfig.java +++ b/modules/server/src/main/java/io/jpom/system/ServerConfig.java @@ -104,6 +104,15 @@ public class ServerConfig extends BaseExtConfig { }); } + private OauthConfig oauth2; + + public OauthConfig getOauth2() { + return Optional.ofNullable(this.oauth2).orElseGet(() -> { + this.oauth2 = new OauthConfig(); + return this.oauth2; + }); + } + /** * 获取当前登录用户的临时文件存储路径,如果没有登录则抛出异常 * @@ -144,6 +153,18 @@ public class ServerConfig extends BaseExtConfig { return file; } + @Data + public static class OauthConfig{ + private boolean enabled; + private String clientId; + private String clientSecret; + private String authorizationUri; + private String accessTokenUri; + private String userInfoUri; + private String redirectUri; + + } + @Data public static class WebConfig { diff --git a/modules/server/src/main/resources/application.yml b/modules/server/src/main/resources/application.yml index cf9802e2f..8965333fa 100644 --- a/modules/server/src/main/resources/application.yml +++ b/modules/server/src/main/resources/application.yml @@ -98,6 +98,15 @@ jpom: pool-wait-queue: 10 # 日志显示 压缩折叠显示进度比例 范围 1-100 log-reduce-progress-ratio: 5 + oauth2: + enabled: false + clientId: 837136275440926720 + clientSecret: 8453MjUwMzIwMjMyMjQxNDY5MzYSD5 + serverUrl: http://sso.maxkey.top + authorizationUri: ${jpom.oauth2.serverUrl}/sign/authz/oauth/v20/authorize + accessTokenUri: ${jpom.oauth2.serverUrl}/sign/authz/oauth/v20/token + userInfoUri: ${jpom.oauth2.serverUrl}/sign/api/oauth/v20/me + redirectUri: http://localhost:3000/ server: #运行端口号 port: 2122 @@ -120,6 +129,7 @@ server: tomcat: uri-encoding: UTF-8 connection-timeout: 10M + spring: profiles: active: mysql-1 @@ -141,3 +151,5 @@ spring: # 是否允许远程访问(开启此配置有安全风险),默认为 false(当部署到服务器上之后,是否可以通过其他浏览器访问数据库) settings: web-allow-others: false + + \ No newline at end of file diff --git a/modules/server/src/main/resources/logo/oauth2.png b/modules/server/src/main/resources/logo/oauth2.png new file mode 100644 index 0000000000000000000000000000000000000000..41a8d35aa834108cdfb86d5b9d8eef369367c324 GIT binary patch literal 13391 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 l@S6 + + 第三方登录 + + + +