From 31662c3a11d3c2dae29ff161e37c3cabc075d0d6 Mon Sep 17 00:00:00 2001 From: sh Date: Sun, 16 Nov 2025 11:19:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=BB=8F=E5=8A=9E=E6=AE=B5?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E9=97=A8=E6=88=B7=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ruoyi/cms/util/oauth/OauthClient.java | 234 +++++++++++------- .../entity/tymh/authority/QxUserRole.java | 24 ++ .../entity/tymh/nwToken/NwTokenResult.java | 5 +- .../web/service/OauthLoginService.java | 74 +++++- .../web/service/UserDetailsServiceImpl.java | 4 +- 5 files changed, 230 insertions(+), 111 deletions(-) create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/tymh/authority/QxUserRole.java 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 index 9251d8e..682740f 100644 --- 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 @@ -1,7 +1,10 @@ package com.ruoyi.cms.util.oauth; import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.TypeReference; +import com.ruoyi.common.core.domain.entity.tymh.authority.QxUserRole; 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; @@ -11,6 +14,7 @@ import org.springframework.stereotype.Component; import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeoutException; import org.slf4j.Logger; @@ -24,53 +28,38 @@ 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}") @@ -80,7 +69,6 @@ public class OauthClient { /** * 获取经办段token - * @return */ public NwTokenResult nwGetToken(String code) throws IOException, TimeoutException { if (StringUtils.isEmpty(code)) { @@ -119,7 +107,6 @@ public class OauthClient { /** * 获取经办段用户id - * @return */ public NwUserInfoResult nwGetUserInfo(String accessToken) throws IOException, TimeoutException { Map params = new HashMap<>(2); @@ -134,120 +121,177 @@ public class OauthClient { ); } + /** + * 根据用户id查询用户角色列表 + * @param userid + * @return + * @throws IOException + * @throws TimeoutException + */ + public List getUserRoleList(Long userid) throws IOException, TimeoutException { + Map params = new HashMap<>(2); + params.put("appid", appid); + params.put("userid", String.valueOf(userid)); + + return executePostRequestResultList( + tyQueryUserRoleListUrl, + params, + QxUserRole.class, + "根据用户id获取用户角色列表" + ); + } + /** * 公共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(); - } + String responseJson = HttpUtils.doPostJson( + url, + params, + connectTimeout, + readTimeout, + writeTimeout + ); return validateResponse(responseJson, operationName, typeReference); } /** - * 公共响应校验工具方法(网关响应 + 业务 errflag 校验) + * 公共POST请求执行方法处理List + */ + private List executePostRequestResultList(String url, Map params, + Class clazz, String operationName) + throws IOException, TimeoutException { + String responseJson = HttpUtils.doPostJson( + url, + params, + connectTimeout, + readTimeout, + writeTimeout + ); + return parseMapOrDataArray(responseJson, operationName, clazz); + } + + /** + * 简化后的响应校验方法(已移除网关返回码判断) */ private T validateResponse(String responseJson, String operationName, TypeReference> typeReference) { - // 1. 校验响应是否为空 + // 1. 校验响应空值 if (StringUtils.isEmpty(responseJson)) { - String errorMsg = operationName + "失败:接口返回空响应"; + String errorMsg = operationName + "失败:接口返回空"; log.error(errorMsg); - throw new BusinessException("EMPTY_RESPONSE", errorMsg); + throw new RuntimeException(errorMsg); } - log.debug("{}接口返回原始数据: {}", operationName, responseJson); + log.debug("{}返回数据: {}", operationName, responseJson); - // 2. 解析网关响应(外层 appcode 校验) + // 2. 解析响应并校验基础格式 Response gatewayResponse = JSON.parseObject(responseJson, typeReference); if (gatewayResponse == null) { - String errorMsg = operationName + "失败:网关响应格式异常(无法解析)"; + 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); + throw new RuntimeException(errorMsg); } + // 3. 校验业务数据(移除了网关appcode的判断逻辑) T data = gatewayResponse.getData(); if (data == null) { - String errorMsg = operationName + "失败:响应data字段为空"; + String errorMsg = operationName + "失败:业务数据为空"; log.error("{} | 原始响应: {}", errorMsg, responseJson); - throw new BusinessException("DATA_EMPTY", errorMsg); + throw new RuntimeException(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; } + /** + * 解析List + * @param jsonStr + * @param clazz + * @return + */ + public static List parseMapOrDataArray(String jsonStr, String operationName, Class clazz) { + if (StringUtils.isEmpty(jsonStr)) { + String errorMsg = operationName + "失败:接口返回空"; + log.error(errorMsg); + throw new RuntimeException(errorMsg); + } + JSONObject jsonObject; + try { + jsonObject = JSON.parseObject(jsonStr); + } catch (Exception e) { + String errorMsg = operationName + "失败:响应格式错误(非合法 JSON)"; + log.error("{} | 原始响应: {}", errorMsg, jsonStr, e); + throw new RuntimeException(errorMsg, e); + } + + if (jsonObject == null) { + String errorMsg = operationName + "失败:响应格式错误(解析后为 null)"; + log.error("{} | 原始响应: {}", errorMsg, jsonStr); + throw new RuntimeException(errorMsg); + } + + JSONArray targetArray = getJSONArraySafe(jsonObject, "map"); + if (targetArray == null || targetArray.isEmpty()) { + targetArray = getJSONArraySafe(jsonObject, "data"); + } + + if (targetArray == null || targetArray.isEmpty()) { + String errorMsg = operationName + "失败:业务数据为空(map/data 字段无有效数组)"; + log.error("{} | 原始响应: {}", errorMsg, jsonStr); + throw new RuntimeException(errorMsg); + } + + List dataList; + try { + dataList = JSON.parseArray(targetArray.toJSONString(), clazz); + } catch (Exception e) { + String errorMsg = operationName + "失败:数据解析失败(数组元素与目标类不匹配)"; + log.error("{} | 原始响应: {} | 目标类: {}", errorMsg, jsonStr, clazz.getName(), e); + throw new RuntimeException(errorMsg, e); + } + + if (dataList == null || dataList.isEmpty()) { + String errorMsg = operationName + "失败:解析后业务数据为空"; + log.error("{} | 原始响应: {}", errorMsg, jsonStr); + throw new RuntimeException(errorMsg); + } + + return dataList; + } + + /** + * 避免空值和类型转换异常 + * @param jsonObject + * @param key + * @return + */ + private static JSONArray getJSONArraySafe(JSONObject jsonObject, String key) { + if (jsonObject == null || StringUtils.isEmpty(key)) { + return null; + } + if (!jsonObject.containsKey(key)) { + return null; + } + Object value = jsonObject.get(key); + return (value instanceof JSONArray) ? (JSONArray) value : null; + } + + /** + * 静态返回解析 + * @param + */ 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 void setAppcode(String appcode) { this.appcode = appcode; } + public void setMsg(String msg) { this.msg = msg; } + public void setData(T data) { this.data = 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/authority/QxUserRole.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/tymh/authority/QxUserRole.java new file mode 100644 index 0000000..ca14ee7 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/tymh/authority/QxUserRole.java @@ -0,0 +1,24 @@ +package com.ruoyi.common.core.domain.entity.tymh.authority; + +import lombok.Data; + +/** + * 用户角色 + */ +@Data +public class QxUserRole { + private Long roleId; + private String roleName; + private String roleType; + private String appId; + private String roleLevel; + private String aae100; + private String aae011; + private String postType; + private String agf001; + private String aae036; + private String agf003; + private String agf002; + private String agf004; + private String folderId; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/tymh/nwToken/NwTokenResult.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/tymh/nwToken/NwTokenResult.java index d9606d5..28a485c 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/tymh/nwToken/NwTokenResult.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/tymh/nwToken/NwTokenResult.java @@ -1,6 +1,6 @@ package com.ruoyi.common.core.domain.entity.tymh.nwToken; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.alibaba.fastjson2.annotation.JSONField; import io.swagger.annotations.ApiModelProperty; import lombok.Data; @@ -13,10 +13,11 @@ public class NwTokenResult { @ApiModelProperty("错误文本") private String errtext; - @JsonProperty("Access-Token") + @JSONField(name = "access_token") @ApiModelProperty("访问令牌") private String accessToken; + @JSONField(name = "expires_in") @ApiModelProperty("access_token接口调用凭证超") private Long expiresIn; } 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 index a367d65..f05fb0c 100644 --- 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 @@ -3,6 +3,7 @@ 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.authority.QxUserRole; 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; @@ -19,15 +20,18 @@ 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.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Service; import java.io.IOException; import java.util.Date; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; @Service public class OauthLoginService { @@ -42,7 +46,7 @@ public class OauthLoginService { @Autowired private ISysUserService sysUserService; @Autowired - private AuthenticationManager authenticationManager; + private UserDetailsService userDetailsService; // Redis缓存:门户UserID → 若依本地用户名(避免重复匹配数据库) private static final String REDIS_KEY_PORTAL_USER_MAPPING = "portal:user:mapping:"; // 门户 Token 存储前缀(Redis 键:门户 userId → 门户 Token 信息) @@ -92,6 +96,12 @@ public class OauthLoginService { String cacheKey = REDIS_KEY_PORTAL_USER_MAPPING + portalUserId; String localUsername = redisCache.getCacheObject(cacheKey); if (StringUtils.isNotBlank(localUsername)) { + try { + //更新用户信息 + //updateUserInfo(portalUser); + }catch (Exception e){ + e.printStackTrace(); + } return localUsername; } @@ -134,9 +144,20 @@ public class OauthLoginService { newUser.setUserId(portalUserId); newUser.setPassword(SecurityUtils.encryptPassword("123456")); newUser.setDelFlag("0"); - - // 调用若依原生方法新增用户(自动处理角色关联,需提前配置默认角色) - sysUserService.insertUser(newUser); + try { + //查询权限,保存权限 + List userRoleList=oauthClient.getUserRoleList(portalUserId); + if(userRoleList!=null&&userRoleList.size()>0){ + Long[] longs=userRoleList.stream().mapToLong(QxUserRole::getRoleId).boxed().toArray(Long[]::new);; + newUser.setRoleIds(longs); + }else { + throw new Exception("未查询到用户角色,请授权后访问!"); + } + // 调用若依原生方法新增用户(自动处理角色关联,需提前配置默认角色) + sysUserService.insertUser(newUser); + }catch (Exception e){ + e.printStackTrace(); + } return newUser; } @@ -147,15 +168,16 @@ public class OauthLoginService { private Authentication authenticateLocalUser(String localUsername) { Authentication authentication = null; try { - // 构建认证令牌:用户名+空密码(因为门户已验证身份,本地仅需加载用户信息) - UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(localUsername, ""); - AuthenticationContextHolder.setContext(authenticationToken); + UserDetails userDetails = userDetailsService.loadUserByUsername(localUsername); - // 触发认证:会调用 UserDetailsServiceImpl.loadUserByUsername(localUsername) - // 该方法会加载用户权限、角色,返回 LoginUser - authentication = authenticationManager.authenticate(authenticationToken); + authentication = new UsernamePasswordAuthenticationToken( + userDetails, + null, // 密码为 null,彻底绕过 Spring Security 的密码校验 + userDetails.getAuthorities() + ); + + AuthenticationContextHolder.setContext(authentication); } catch (Exception e) { - // 捕获认证异常(如用户被禁用、权限加载失败等) throw new ServiceException("本地用户认证失败:" + e.getMessage()); } finally { AuthenticationContextHolder.clearContext(); @@ -178,6 +200,34 @@ public class OauthLoginService { redisCache.setCacheObject(redisKey, tokenCache, safeLongToInt(tokenResult.getExpiresIn()), TimeUnit.SECONDS); } + /** + * 修改用户信息 + * @param portalUser + */ + private void updateUserInfo(NwUserInfoResult portalUser){ + SysUser sysUser=new SysUser(); + Long portalUserId=parsePortalUserId(portalUser.getUserid()); + //查询用户角色 + try { + List userRoleList=oauthClient.getUserRoleList(portalUserId); + if(userRoleList!=null&&userRoleList.size()>0){ + Long[] longs=userRoleList.stream().mapToLong(QxUserRole::getRoleId).boxed().toArray(Long[]::new);; + sysUser.setRoleIds(longs); + }else { + throw new Exception("未查询到用户角色,请授权后访问!"); + } + }catch (Exception e){ + e.printStackTrace(); + } + + String localUsername = "portal_" + portalUserId; + sysUser.setUserName(localUsername); + sysUser.setNickName(portalUser.getName()); + sysUser.setIdCard(portalUser.getIdcardno()); + sysUser.setUserId(portalUserId); + sysUserService.updateUser(sysUser); + } + /** * 记录登录信息(复用若依原生逻辑,直接复制过来) */ diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java index 5dcdf90..117072c 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java @@ -53,8 +53,8 @@ public class UserDetailsServiceImpl implements UserDetailsService log.info("登录用户:{} 已被停用.", username); throw new ServiceException(MessageUtils.message("user.blocked")); } - - passwordService.validate(user); + ////单点跳过验证密码阶段 + //passwordService.validate(user); return createLoginUser(user); }