From 4716127fc2ae70baa57ab41851a73414ca4ac3cf Mon Sep 17 00:00:00 2001 From: sh Date: Sat, 15 Nov 2025 14:04:05 +0800 Subject: [PATCH] =?UTF-8?q?1.=E4=BF=AE=E6=94=B91101(=E6=B1=82=E8=81=8C?= =?UTF-8?q?=E8=80=85)=E3=80=811102(=E6=8B=9B=E8=81=98=E8=80=85)=E3=80=8111?= =?UTF-8?q?03(=E7=BD=91=E6=A0=BC=E5=91=98)=E3=80=811104(=E5=86=85=E9=83=A8?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E8=80=85)=202.=E6=B7=BB=E5=8A=A0=E9=97=A8?= =?UTF-8?q?=E6=88=B7=E8=AE=A4=E8=AF=81=E7=9A=84=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/system/SysLoginController.java | 17 ++ .../controller/system/SysUserController.java | 2 +- .../src/main/resources/application.yml | 36 ++- .../java/com/ruoyi/cms/util/StringUtil.java | 9 +- .../com/ruoyi/cms/util/oauth/HttpUtils.java | 151 +++++++++++ .../com/ruoyi/cms/util/oauth/OauthClient.java | 253 ++++++++++++++++++ .../tymh/nwToken/PortalTokenCacheDTO.java | 12 + .../framework/config/SecurityConfig.java | 2 +- .../web/service/OauthLoginService.java | 209 +++++++++++++++ 9 files changed, 680 insertions(+), 11 deletions(-) create mode 100644 ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/oauth/HttpUtils.java create mode 100644 ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/oauth/OauthClient.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/tymh/nwToken/PortalTokenCacheDTO.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/OauthLoginService.java diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java index f3dbaf6..6abdc7c 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java @@ -5,6 +5,7 @@ import java.util.Set; import com.ruoyi.common.core.domain.model.RegisterBody; import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.web.service.OauthLoginService; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @@ -34,6 +35,8 @@ public class SysLoginController @Autowired private SysPermissionService permissionService; + @Autowired + private OauthLoginService oauthLoginService; /** * 登录方法 @@ -166,4 +169,18 @@ public class SysLoginController String token=loginService.registerAppUser(registerBody); return AjaxResult.success().put("token",token); } + + /** + * 获取统一门户token + */ + @GetMapping("/getTjmhToken") + public AjaxResult getTjmhToken(@RequestParam("code") String code){ + if(StringUtils.isBlank(code)){ + return AjaxResult.error("参数code为空,请传递code参数"); + } + String token = oauthLoginService.oauthLogin(code); + return AjaxResult.success() + .put("token", token) + .put("msg", "登录成功"); + } } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java index c267953..7eda781 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java @@ -283,7 +283,7 @@ public class SysUserController extends BaseController } sysUser.setPhonenumber(getUsername()); Long[] postIds = {1L}; - Long[] roleIds = {100L}; + Long[] roleIds = {1102L}; sysUser.setPostIds(postIds); sysUser.setRoleIds(roleIds); userService.insertUser(sysUser); diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 4cdfa88..9acc283 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -147,10 +147,38 @@ wx: #统一门户认证 oauth: - appid: aa - clientsecretkey: bb - getToken: http://ip:80/serviceAPI/getToken - getUserInfo: http://ip:80/serviceAPI/getUserInfo + #客户端的ID + appid: 251112100000000015 + #授权码 + clientsecretkey: 2a44cb8d21dcf4b0777881ca11ea0d83ebea94bbe1ab1f405508db0873cdcc99 + #内网 + usptnw: + #获取访问令牌 + nwGatewayGetTokenUrl: http://10.98.80.146/uspt/serviceAPI/getToken + #获取用户信息 + nwGatewayGetUserInfoUrl: http://10.98.80.146/uspt/serviceAPI/getUserInfo + #外网 + usptww: + #门户注册 + wwRegisterPostUrl: http://ip:80/whiteListServiceAPI/doWebRegister + #门户登录 + wwTokenPostUrl: http://ip:80/whiteListServiceAPI/doWebLogon + #查询个人信息 + wwQueryWebPersonalInfoPostUrl: http://ip:80/serviceAPI/queryWebPersonalInfo + #查询单位信息 + wwQueryWebEnterpriseInfoPostUrl: http://ip:80/serviceAPI/queryWebEnterpriseInfo + #用户新增接口 + tyAddUserUrl: http://10.98.80.146/qxgl_backend/security/add_user + #获取当前用户有权系统列表 + tyQueryUserSysListUrl: http://10.98.80.146/qxgl_backend/security/get_effective_app_list + #获取当前用户有权角色列表 + tyQueryUserRoleListUrl: http://10.98.80.146/qxgl_backend/security/get_role_by_userid + #获取角色功能权限信息 + tyQueryRoleInfoUrl: http://10.98.80.146/qxgl_backend/security/get_path_by_role + #获取用户详细信息 + tyQueryUserInfo: http://10.98.80.146/qxgl_backend/security/get_user_by_userid + #获取机构详细信息 + tyQueryUnitInfo: http://10.98.80.146/qxgl_backend/security/get_organization_by_organizationid connect-timeout: 10 read-timeout: 30 write-timeout: 30 \ No newline at end of file diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/StringUtil.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/StringUtil.java index aec57d0..16d990a 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/StringUtil.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/StringUtil.java @@ -1,7 +1,5 @@ package com.ruoyi.cms.util; -import com.ruoyi.cms.domain.query.ESJobSearch; - import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -10,7 +8,8 @@ import java.util.stream.Collectors; public class StringUtil { - public static final Long COMPANY_ADMIN_ROLE_KEY = 100L; + /*1101(求职者)、1102(招聘者)、1103(网格员)、1104(内部工作者)*/ + public static final Long COMPANY_ADMIN_ROLE_KEY = 1002L; /** * 企业用户 @@ -20,11 +19,11 @@ public class StringUtil { /** * pc端-求职者 */ - public static final String SYS_QZZ = "2"; + public static final String SYS_QZZ = "1101"; /** * pc端-企业 */ - public static final String SYS_QY = "100"; + public static final String SYS_QY = "1102"; public static Boolean isEmptyOrNull(String s){ if(Objects.isNull(s)){return true;} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/oauth/HttpUtils.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/oauth/HttpUtils.java new file mode 100644 index 0000000..db1d65f --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/oauth/HttpUtils.java @@ -0,0 +1,151 @@ +package com.ruoyi.cms.util.oauth; + +import com.alibaba.fastjson2.JSON; +import okhttp3.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.CollectionUtils; + +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.net.URLEncoder; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * 简化版 HTTP 客户端工具类(仅保留核心POST功能,保留超时配置) + */ +public class HttpUtils { + private static final Logger log = LoggerFactory.getLogger(HttpUtils.class); + + // 保留默认超时配置 + private static final int DEFAULT_CONNECT_TIMEOUT = 10; + private static final int DEFAULT_READ_TIMEOUT = 30; + private static final int DEFAULT_WRITE_TIMEOUT = 30; + + // 媒体类型常量 + public static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8"); + public static final MediaType FORM_MEDIA_TYPE = MediaType.parse("application/x-www-form-urlencoded; charset=utf-8"); + + // OkHttp 客户端(单例) + private static final OkHttpClient OK_HTTP_CLIENT; + + static { + OkHttpClient.Builder builder = new OkHttpClient.Builder() + .connectTimeout(DEFAULT_CONNECT_TIMEOUT, TimeUnit.SECONDS) + .readTimeout(DEFAULT_READ_TIMEOUT, TimeUnit.SECONDS) + .writeTimeout(DEFAULT_WRITE_TIMEOUT, TimeUnit.SECONDS); + + OK_HTTP_CLIENT = builder.build(); + } + + /** + * POST JSON请求(默认超时) + */ + public static String doPostJson(String url, Map params) throws TimeoutException, IOException { + return doPostJson(url, params, DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT, DEFAULT_WRITE_TIMEOUT); + } + + /** + * POST JSON请求(自定义超时) + */ + public static String doPostJson(String url, Map params, int connectTimeout, int readTimeout, int writeTimeout) + throws TimeoutException, IOException { + String jsonParams = CollectionUtils.isEmpty(params) ? "{}" : JSON.toJSONString(params); + return doPost(url, jsonParams, JSON_MEDIA_TYPE, null, connectTimeout, readTimeout, writeTimeout); + } + + /** + * POST 表单请求(默认超时) + */ + public static String doPostForm(String url, Map params) throws TimeoutException, IOException { + return doPostForm(url, params, null, DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT, DEFAULT_WRITE_TIMEOUT); + } + + /** + * POST 表单请求(自定义超时) + */ + public static String doPostForm(String url, Map params, Map headers, + int connectTimeout, int readTimeout, int writeTimeout) + throws TimeoutException, IOException { + FormBody.Builder formBuilder = new FormBody.Builder(); + if (!CollectionUtils.isEmpty(params)) { + params.forEach((key, value) -> { + if (value != null) { + formBuilder.add(encodeParam(key), value.toString()); + } + }); + } + RequestBody requestBody = formBuilder.build(); + + Request.Builder requestBuilder = new Request.Builder() + .url(url) + .post(requestBody) + .header("Content-Type", FORM_MEDIA_TYPE.toString()); + + addHeaders(requestBuilder, headers); + return executeRequest(requestBuilder.build(), connectTimeout, readTimeout, writeTimeout); + } + + /** + * 通用POST方法 + */ + private static String doPost(String url, String content, MediaType mediaType, Map headers, + int connectTimeout, int readTimeout, int writeTimeout) + throws TimeoutException, IOException { + if (mediaType == null) { + mediaType = JSON_MEDIA_TYPE; + } + RequestBody requestBody = RequestBody.create(mediaType,content); + + Request.Builder requestBuilder = new Request.Builder() + .url(url) + .post(requestBody) + .header("Content-Type", mediaType.toString()); + + addHeaders(requestBuilder, headers); + return executeRequest(requestBuilder.build(), connectTimeout, readTimeout, writeTimeout); + } + + /** + * 执行请求核心方法 + */ + private static String executeRequest(Request request, int connectTimeout, int readTimeout, int writeTimeout) + throws TimeoutException, IOException { + // 保留超时配置覆盖能力 + OkHttpClient tempClient = OK_HTTP_CLIENT.newBuilder() + .connectTimeout(connectTimeout, TimeUnit.SECONDS) + .readTimeout(readTimeout, TimeUnit.SECONDS) + .writeTimeout(writeTimeout, TimeUnit.SECONDS) + .build(); + + try (Response response = tempClient.newCall(request).execute()) { + return response.body() != null ? response.body().string() : ""; + } catch (SocketTimeoutException e) { + throw new TimeoutException(String.format("HTTP 请求超时 | URL: %s | 超时配置: 连接%d秒, 读取%d秒, 写入%d秒", + request.url(), connectTimeout, readTimeout, writeTimeout)); + } + } + + /** + * 参数编码 + */ + private static String encodeParam(String param) { + try { + return URLEncoder.encode(param, "UTF-8"); + } catch (Exception e) { + log.warn("参数编码失败 | param: {}", param, e); + return param; + } + } + + /** + * 添加请求头 + */ + private static void addHeaders(Request.Builder requestBuilder, Map headers) { + if (!CollectionUtils.isEmpty(headers)) { + headers.forEach(requestBuilder::header); + } + } +} \ No newline at end of file diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/oauth/OauthClient.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/oauth/OauthClient.java new file mode 100644 index 0000000..9251d8e --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/oauth/OauthClient.java @@ -0,0 +1,253 @@ +package com.ruoyi.cms.util.oauth; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.TypeReference; +import com.ruoyi.common.core.domain.entity.tymh.nwToken.NwTokenResult; +import com.ruoyi.common.core.domain.entity.tymh.nwToken.NwUserInfoResult; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.crypto.CryptoUtil; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeoutException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * OAuth2.0 认证工具类(合并相似方法) + */ +@Component +public class OauthClient { + + private static final Logger log = LoggerFactory.getLogger(OauthClient.class); + /*====================== 内网 ======================*/ + // 网关请求-内网获取token + @Value("${oauth.usptnw.nwGatewayGetTokenUrl}") + private String nwGatewayGetTokenUrl; + // 网关请求-内网获取用户信息 + @Value("${oauth.usptnw.nwGatewayGetUserInfoUrl}") + private String nwGatewayGetUserInfoUrl; + + /*====================== 外网 ======================*/ + //post-外网注册地址 + @Value("${oauth.usptww.wwRegisterPostUrl}") + private String wwRegisterPostUrl; + // post-外网获取token + @Value("${oauth.usptww.wwTokenPostUrl}") + private String wwTokenPostUrl; + //post-外网获取用户信息 + @Value("${oauth.usptww.wwQueryWebPersonalInfoPostUrl}") + private String wwQueryWebPersonalInfoPostUrl; + //post-外网获取单位信息 + @Value("${oauth.usptww.wwQueryWebEnterpriseInfoPostUrl}") + private String wwQueryWebEnterpriseInfoPostUrl; + + /*====================== 统一门户 ======================*/ + //用户新增接口 + @Value("${oauth.tyAddUserUrl}") + private String tyAddUserUrl; + //获取当前用户有权系统列表 + @Value("${oauth.tyQueryUserSysListUrl}") + private String tyQueryUserSysListUrl; + //获取当前用户有权角色列表 + @Value("${oauth.tyQueryUserRoleListUrl}") + private String tyQueryUserRoleListUrl; + //获取角色功能权限信息 + @Value("${oauth.tyQueryRoleInfoUrl}") + private String tyQueryRoleInfoUrl; + //获取用户详细信息 + @Value("${oauth.tyQueryUserInfo}") + private String tyQueryUserInfo; + //获取机构详细信息 + @Value("${oauth.tyQueryUnitInfo}") + private String tyQueryUnitInfo; + //客户端id + @Value("${oauth.appid}") + private String appid; + //授权码 + @Value("${oauth.clientsecretkey}") + private String clientsecretkey; + // 超时配置 + @Value("${oauth.connect-timeout:10}") + private int connectTimeout; + @Value("${oauth.read-timeout:30}") + private int readTimeout; + @Value("${oauth.write-timeout:30}") + private int writeTimeout; + + /** + * 获取经办段token + * @return + */ + public NwTokenResult nwGetToken(String code) throws IOException, TimeoutException { + if (StringUtils.isEmpty(code)) { + log.error("获取Token失败:授权码code不能为空"); + throw new IllegalArgumentException("授权码code不能为空"); + } + + String decryptedCode; + try { + decryptedCode = CryptoUtil.sm2Decrypt(clientsecretkey, code); + if (StringUtils.isEmpty(decryptedCode)) { + throw new RuntimeException("授权码解密后为空"); + } + } catch (Exception e) { + log.error("授权码解密失败 | 原始code: {}", code, e); + throw new RuntimeException("授权码解密失败:" + e.getMessage(), e); + } + + Map params = new HashMap<>(2); + params.put("appid", appid); + params.put("code", decryptedCode); + + NwTokenResult result = executePostRequest( + nwGatewayGetTokenUrl, + params, + new TypeReference>() {}, + "获取Token" + ); + + if (StringUtils.isEmpty(result.getAccessToken())) { + throw new RuntimeException("Token获取失败:返回的accessToken为空"); + } + + return result; + } + + /** + * 获取经办段用户id + * @return + */ + public NwUserInfoResult nwGetUserInfo(String accessToken) throws IOException, TimeoutException { + Map params = new HashMap<>(2); + params.put("appid", appid); + params.put("access_token", accessToken); + + return executePostRequest( + nwGatewayGetUserInfoUrl, + params, + new TypeReference>() {}, + "获取用户信息" + ); + } + + /** + * 公共POST请求执行方法 + * @param url 请求URL + * @param params 请求参数 + * @param typeReference 响应类型引用 + * @param operationName 操作名称(用于日志和异常信息) + * @return 响应数据对象 + */ + private T executePostRequest(String url, Map params, + TypeReference> typeReference, String operationName) + throws IOException, TimeoutException { + String responseJson = null; + try { + responseJson = HttpUtils.doPostJson( + url, + params, + connectTimeout, + readTimeout, + writeTimeout + ); + }catch (Exception e){ + e.printStackTrace(); + } + return validateResponse(responseJson, operationName, typeReference); + } + + /** + * 公共响应校验工具方法(网关响应 + 业务 errflag 校验) + */ + private T validateResponse(String responseJson, String operationName, TypeReference> typeReference) { + // 1. 校验响应是否为空 + if (StringUtils.isEmpty(responseJson)) { + String errorMsg = operationName + "失败:接口返回空响应"; + log.error(errorMsg); + throw new BusinessException("EMPTY_RESPONSE", errorMsg); + } + log.debug("{}接口返回原始数据: {}", operationName, responseJson); + + // 2. 解析网关响应(外层 appcode 校验) + Response gatewayResponse = JSON.parseObject(responseJson, typeReference); + if (gatewayResponse == null) { + String errorMsg = operationName + "失败:网关响应格式异常(无法解析)"; + log.error("{} | 原始响应: {}", errorMsg, responseJson); + throw new BusinessException("GATEWAY_PARSE_ERROR", errorMsg); + } + + if (!"0".equals(gatewayResponse.getAppcode())) { + String errorMsg = String.format("%s失败:网关响应失败 | appcode: %s, msg: %s", + operationName, gatewayResponse.getAppcode(), gatewayResponse.getMsg()); + log.error("{} | 原始响应: {}", errorMsg, responseJson); + throw new BusinessException(gatewayResponse.getAppcode(), errorMsg); + } + + T data = gatewayResponse.getData(); + if (data == null) { + String errorMsg = operationName + "失败:响应data字段为空"; + log.error("{} | 原始响应: {}", errorMsg, responseJson); + throw new BusinessException("DATA_EMPTY", errorMsg); + } + + if (data instanceof ErrFlagResponse) { + ErrFlagResponse errFlagResponse = (ErrFlagResponse) data; + if (!errFlagResponse.isSuccess()) { + String errorMsg = String.format("%s失败:%s(错误标识:%s)", + operationName, errFlagResponse.getErrtext(), errFlagResponse.getErrflag()); + log.error("{} | 原始响应: {}", errorMsg, responseJson); + throw new BusinessException(errFlagResponse.getErrflag(), errorMsg); + } + } + + // 6. 所有校验通过,返回业务数据 + return data; + } + + public static class Response { + private String appcode; + private String code; + private String msg; + private T data; + private Object map; + + // Getter(仅保留需要的字段,setter可选) + public String getAppcode() { return appcode; } + public String getMsg() { return msg; } + public T getData() { return data; } + } + + public class BusinessException extends RuntimeException { + private String code; // 错误码(可关联门户错误码) + + public BusinessException(String message) { + super(message); + this.code = "BUSINESS_ERROR"; + } + + public BusinessException(String code, String message) { + super(message); + this.code = code; + } + + // Getter + public String getCode() { + return code; + } + } + + public interface ErrFlagResponse { + String getErrflag(); + String getErrtext(); + + // 默认方法:判断是否成功(0=成功,1=失败) + default boolean isSuccess() { + return "0".equals(getErrflag()); + } + } + +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/tymh/nwToken/PortalTokenCacheDTO.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/tymh/nwToken/PortalTokenCacheDTO.java new file mode 100644 index 0000000..59119b4 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/tymh/nwToken/PortalTokenCacheDTO.java @@ -0,0 +1,12 @@ +package com.ruoyi.common.core.domain.entity.tymh.nwToken; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +public class PortalTokenCacheDTO { + @ApiModelProperty("门户 accessToken") + private String accessToken; + @ApiModelProperty("过期时间戳(毫秒)") + private Long expireTimestamp; +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java index e266007..47e4a3c 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java @@ -111,7 +111,7 @@ public class SecurityConfig .authorizeHttpRequests((requests) -> { permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll()); // 对于登录login 注册register 验证码captchaImage 允许匿名访问 - requests.antMatchers("/login", "/register", "/captchaImage","/app/login","/websocket/**","/speech-recognition","/speech-synthesis","/cms/company/listPage","/cms/appUser/noTmlist").permitAll() + requests.antMatchers("/login", "/register", "/captchaImage","/app/login","/websocket/**","/speech-recognition","/speech-synthesis","/cms/company/listPage","/cms/appUser/noTmlist","/getTjmhToken").permitAll() // 静态资源,可匿名访问 .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() // 移动端公用查询,可匿名访问 diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/OauthLoginService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/OauthLoginService.java new file mode 100644 index 0000000..a367d65 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/OauthLoginService.java @@ -0,0 +1,209 @@ +package com.ruoyi.framework.web.service; + +import com.ruoyi.cms.util.oauth.OauthClient; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.entity.tymh.nwToken.NwTokenResult; +import com.ruoyi.common.core.domain.entity.tymh.nwToken.NwUserInfoResult; +import com.ruoyi.common.core.domain.entity.tymh.nwToken.PortalTokenCacheDTO; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.MessageUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.framework.manager.factory.AsyncFactory; +import com.ruoyi.framework.security.context.AuthenticationContextHolder; +import com.ruoyi.system.service.ISysUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.Date; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +@Service +public class OauthLoginService { + + @Autowired + private OauthClient oauthClient; + + @Autowired + private TokenService tokenService; + @Autowired + private RedisCache redisCache; + @Autowired + private ISysUserService sysUserService; + @Autowired + private AuthenticationManager authenticationManager; + // Redis缓存:门户UserID → 若依本地用户名(避免重复匹配数据库) + private static final String REDIS_KEY_PORTAL_USER_MAPPING = "portal:user:mapping:"; + // 门户 Token 存储前缀(Redis 键:门户 userId → 门户 Token 信息) + private static final String REDIS_KEY_PORTAL_TOKEN = "portal:token:"; + + /** + * OAuth 登录流程:通过授权码获取系统令牌 + * @param code 前端传入的 OAuth 授权码 + * @return 系统内部令牌(供前端后续使用) + */ + public String oauthLogin(String code) { + try { + //获取门户token + NwTokenResult nwTokenResult = oauthClient.nwGetToken(code); + if (!"0".equals(nwTokenResult.getErrflag())) { + throw new ServiceException("获取门户 Token 失败:" + nwTokenResult.getErrtext()); + } + String accessToken = nwTokenResult.getAccessToken(); + Long expiresIn = nwTokenResult.getExpiresIn(); + if (StringUtils.isEmpty(accessToken) || expiresIn == null || expiresIn <= 0) { + throw new ServiceException("门户 Token 无效或有效期异常"); + } + //获取门户userInfo + NwUserInfoResult portalUser = oauthClient.nwGetUserInfo(accessToken); + //匹配/创建本地用户 + String localUsername = getOrCreateLocalUser(portalUser); + //执行原来的登录流程 + Authentication authentication = authenticateLocalUser(localUsername); + AsyncManager.me().execute(AsyncFactory.recordLogininfor(localUsername, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"))); + LoginUser loginUser = (LoginUser) authentication.getPrincipal(); + + storePortalToken(loginUser.getUserId(), nwTokenResult); + recordLoginInfo(loginUser.getUserId()); + return tokenService.createToken(loginUser); + } catch (IOException | TimeoutException e) { + throw new ServiceException("OAuth 登录失败:" + e.getMessage()); + } + } + + /** + * 匹配/创建本地用户,返回若依本地用户名 + */ + private String getOrCreateLocalUser(NwUserInfoResult portalUser) { + Long portalUserId = parsePortalUserId(portalUser.getUserid()); + + // 先从Redis查询缓存的本地用户名 + String cacheKey = REDIS_KEY_PORTAL_USER_MAPPING + portalUserId; + String localUsername = redisCache.getCacheObject(cacheKey); + if (StringUtils.isNotBlank(localUsername)) { + return localUsername; + } + + // 缓存未命中,查询本地用户 + SysUser localUser = sysUserService.selectUserById(portalUserId); + + if (localUser == null) { + // 本地无用户,自动创建 + localUser = createLocalUser(portalUser, portalUserId); + // 缓存门户UserID与本地用户名的映射(有效期1天,可调整) + redisCache.setCacheObject(cacheKey, localUser.getUserName(), 1, TimeUnit.DAYS); + return localUser.getUserName(); + } + + // 缓存映射关系(更新有效期) + redisCache.setCacheObject(cacheKey, localUser.getUserName(), 1, TimeUnit.DAYS); + return localUser.getUserName(); + } + + /** + * 门户UserID字符串转Long + */ + private Long parsePortalUserId(String portalUserIdStr) { + try { + return Long.parseLong(portalUserIdStr); + } catch (NumberFormatException e) { + throw new ServiceException("门户用户ID格式错误:" + portalUserIdStr); + } + } + + /** + * 自动创建本地用户 + */ + private SysUser createLocalUser(NwUserInfoResult portalUser, Long portalUserId) { + SysUser newUser = new SysUser(); + String localUsername = "portal_" + portalUserId; + newUser.setUserName(localUsername); + newUser.setNickName(portalUser.getName()); + newUser.setIdCard(portalUser.getIdcardno()); + newUser.setUserId(portalUserId); + newUser.setPassword(SecurityUtils.encryptPassword("123456")); + newUser.setDelFlag("0"); + + // 调用若依原生方法新增用户(自动处理角色关联,需提前配置默认角色) + sysUserService.insertUser(newUser); + return newUser; + } + + /** + * 复用若依认证机制,触发 UserDetailsService 加载 LoginUser + * (关键:用本地用户名构建认证令牌,无需密码,因为门户已完成身份校验) + */ + private Authentication authenticateLocalUser(String localUsername) { + Authentication authentication = null; + try { + // 构建认证令牌:用户名+空密码(因为门户已验证身份,本地仅需加载用户信息) + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(localUsername, ""); + AuthenticationContextHolder.setContext(authenticationToken); + + // 触发认证:会调用 UserDetailsServiceImpl.loadUserByUsername(localUsername) + // 该方法会加载用户权限、角色,返回 LoginUser + authentication = authenticationManager.authenticate(authenticationToken); + } catch (Exception e) { + // 捕获认证异常(如用户被禁用、权限加载失败等) + throw new ServiceException("本地用户认证失败:" + e.getMessage()); + } finally { + AuthenticationContextHolder.clearContext(); + } + return authentication; + } + + /** + * 存储门户 Token 到 Redis(结构化存储,含过期时间) + */ + private void storePortalToken(Long portalUserId, NwTokenResult tokenResult) { + // 构建 Redis 键(门户 userId 作为唯一标识) + String redisKey = REDIS_KEY_PORTAL_TOKEN + portalUserId; + + // 封装 Token 信息(含 accessToken、过期时间戳) + PortalTokenCacheDTO tokenCache = new PortalTokenCacheDTO(); + tokenCache.setAccessToken(tokenResult.getAccessToken()); + tokenCache.setExpireTimestamp(System.currentTimeMillis() + tokenResult.getExpiresIn() * 1000); + + redisCache.setCacheObject(redisKey, tokenCache, safeLongToInt(tokenResult.getExpiresIn()), TimeUnit.SECONDS); + } + + /** + * 记录登录信息(复用若依原生逻辑,直接复制过来) + */ + private void recordLoginInfo(Long userId) { + SysUser sysUser = new SysUser(); + sysUser.setUserId(userId); + sysUser.setLoginIp(IpUtils.getIpAddr(ServletUtils.getRequest())); + sysUser.setLoginDate(new Date()); + sysUserService.updateUserProfile(sysUser); + } + + /** + * Long 转 int + */ + private int safeLongToInt(Long value) { + if (value == null) { + throw new ServiceException("门户 Token 有效期不能为空"); + } + // 校验是否超过 int 最大值(实际场景几乎不会触发) + if (value > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } + // 校验是否为负数(无效有效期) + if (value < 0) { + throw new ServiceException("门户 Token 有效期不能为负数"); + } + return value.intValue(); + } +} \ No newline at end of file