diff --git a/src/main/java/com/chinaweal/aiccs/config/UnifiedAuthConfig.java b/src/main/java/com/chinaweal/aiccs/config/UnifiedAuthConfig.java new file mode 100644 index 0000000..2f996b5 --- /dev/null +++ b/src/main/java/com/chinaweal/aiccs/config/UnifiedAuthConfig.java @@ -0,0 +1,97 @@ +package com.chinaweal.aiccs.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * 统一认证平台OAuth2配置 + * + * @author lroyia + * @since 2025/03/02 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "oauth2.unified-auth") +public class UnifiedAuthConfig { + + /** + * 认证地址 + */ + private String authUrl; + + /** + * 客户端ID + */ + private String clientId; + + /** + * 客户端秘钥 + */ + private String clientSecret; + + /** + * 回调地址 + */ + private String redirectUri; + + /** + * 授权码接口路径 + */ + private String authorizePath = "/authcenter/getOauth2Authorize"; + + /** + * 获取令牌接口路径 + */ + private String tokenPath = "/authcenter/getOauth2Token"; + + /** + * 获取用户信息接口路径 + */ + private String userinfoPath = "/authcenter/getOauth2UserInfo"; + + /** + * 验证令牌有效性接口路径 + */ + private String checkTokenPath = "/authcenter/checkTAValid"; + + /** + * 单点登出接口路径 + */ + private String logoutPath = "/authcenter/userLogout"; + + /** + * 获取完整授权码接口URL + */ + public String getAuthorizeUrl() { + return authUrl + authorizePath; + } + + /** + * 获取完整令牌接口URL + */ + public String getTokenUrl() { + return authUrl + tokenPath; + } + + /** + * 获取完整用户信息接口URL + */ + public String getUserinfoUrl() { + return authUrl + userinfoPath; + } + + /** + * 获取完整验证令牌接口URL + */ + public String getCheckTokenUrl() { + return authUrl + checkTokenPath; + } + + /** + * 获取完整单点登出接口URL + */ + public String getLogoutUrl() { + return authUrl + logoutPath; + } +} \ No newline at end of file diff --git a/src/main/java/com/chinaweal/aiccs/org/controller/OAuth2Controller.java b/src/main/java/com/chinaweal/aiccs/org/controller/OAuth2Controller.java index 15b6b67..b63222e 100644 --- a/src/main/java/com/chinaweal/aiccs/org/controller/OAuth2Controller.java +++ b/src/main/java/com/chinaweal/aiccs/org/controller/OAuth2Controller.java @@ -14,6 +14,7 @@ import com.chinaweal.aiccs.org.entity.dto.*; import com.chinaweal.aiccs.org.service.IOauthAccessTokenService; import com.chinaweal.aiccs.org.service.IOauthAuthorizationCodeService; import com.chinaweal.aiccs.org.service.IOauthClientService; +import com.chinaweal.aiccs.org.service.IUnifiedAuthService; import com.chinaweal.aicorg.model.AICUser; import com.chinaweal.aicorg.services.OrgUM; import com.chinaweal.youfool.framework.springboot.rest.RestResult; @@ -28,6 +29,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; +import java.io.UnsupportedEncodingException; import java.net.URLEncoder; /** @@ -52,6 +54,8 @@ public class OAuth2Controller extends BaseController { private IOauthAccessTokenService oauthTokenService; @Autowired private OrgUM orgUM; + @Autowired + private IUnifiedAuthService unifiedAuthService; /** * OAuth授权端点 @@ -534,4 +538,142 @@ public class OAuth2Controller extends BaseController { jsonObject.put("expired", System.currentTimeMillis() + 1000 * 60 * 5);// 增加有效期 return RestResult.ok(SM4Utils.encrypt(jsonObject.toJSONString(), sm4Key)); } + + // ==================== 统一认证平台相关接口 ==================== + + /** + * 跳转到统一认证平台登录页面 + * + * @param state 状态参数,用于防止CSRF攻击 + * @return 登录URL + */ + @ApiOperation("跳转到统一认证平台登录页面") + @GetMapping("/unified/login") + public ResponseEntity unifiedLogin( + @RequestParam(value = "state", required = false) String state) { + try { + String loginUrl = unifiedAuthService.buildLoginUrl(state); + log.info("跳转到统一认证平台登录页面: {}", loginUrl); + return ResponseEntity.status(HttpStatus.FOUND) + .location(java.net.URI.create(loginUrl)) + .build(); + } catch (Exception e) { + log.error("跳转到统一认证平台登录页面失败", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("跳转失败: " + e.getMessage()); + } + } + + /** + * 统一认证平台OAuth2回调接口 + * + * @param code 授权码 + * @param state 状态参数 + * @return 登录结果 + */ + @ApiOperation("统一认证平台OAuth2回调接口") + @GetMapping("/callback") + public ResponseEntity callback( + @RequestParam(value = "code", required = false) String code, + @RequestParam(value = "state", required = false) String state, + @RequestParam(value = "error", required = false) String error, + @RequestParam(value = "error_description", required = false) String errorDescription) { + + try { + // 检查是否有错误 + if (StringUtils.isNotBlank(error)) { + log.error("统一认证平台返回错误: {} - {}", error, errorDescription); + // 重定向到错误页面或首页 + return ResponseEntity.status(HttpStatus.FOUND) + .location(java.net.URI.create("/integration/#/login?error=" + URLEncoder.encode(error + ":" + errorDescription, "UTF-8"))) + .build(); + } + + // 检查授权码 + if (StringUtils.isBlank(code)) { + log.error("未收到授权码"); + return ResponseEntity.status(HttpStatus.FOUND) + .location(java.net.URI.create("/integration/#/login?error=no_code")) + .build(); + } + + log.info("收到统一认证平台回调,code: {}, state: {}", code, state); + + // 使用授权码获取访问令牌 + UnifiedAuthDTO.TokenResponse tokenResponse = unifiedAuthService.getAccessToken(code); + if (StringUtils.isNotBlank(tokenResponse.getError())) { + log.error("获取访问令牌失败: {} - {}", tokenResponse.getError(), tokenResponse.getError_description()); + return ResponseEntity.status(HttpStatus.FOUND) + .location(java.net.URI.create("/integration/#/login?error=" + URLEncoder.encode(tokenResponse.getError_description(), "UTF-8"))) + .build(); + } + + // 使用访问令牌获取用户信息 + UnifiedAuthDTO.UserInfoResponse userInfo = unifiedAuthService.getUserInfo(tokenResponse.getAccess_token()); + if (StringUtils.isNotBlank(userInfo.getError())) { + log.error("获取用户信息失败: {} - {}", userInfo.getError(), userInfo.getError_description()); + return ResponseEntity.status(HttpStatus.FOUND) + .location(java.net.URI.create("/integration/#/login?error=" + URLEncoder.encode(userInfo.getError_description(), "UTF-8"))) + .build(); + } + + // 获取到用户信息,进行本地登录处理 + log.info("获取到用户信息: {}", JSON.toJSONString(userInfo)); + + // 这里需要根据业务需求,使用用户信息创建本地会话 + // 例如:调用本地登录逻辑,设置session等 + // 这里简化处理,实际需要根据系统现有的登录机制实现 + // TODO:实际登录逻辑 + + // 跳转到首页 + return ResponseEntity.status(HttpStatus.FOUND) + .location(java.net.URI.create("/integration/#/")) + .build(); + + } catch (Exception e) { + log.error("统一认证平台回调处理失败", e); + try { + return ResponseEntity.status(HttpStatus.FOUND) + .location(java.net.URI.create("/integration/#/login?error=" + URLEncoder.encode("系统错误: " + e.getMessage(), "UTF-8"))) + .build(); + } catch (UnsupportedEncodingException ex) { + throw new RuntimeException(ex); + } + } + } + + /** + * 统一认证平台单点登出 + * + * @param request HTTP请求 + * @return 登出结果 + */ + @ApiOperation("统一认证平台单点登出") + @GetMapping("/unified/logout") + public ResponseEntity unifiedLogout(HttpServletRequest request) { + try { + // 获取当前用户的访问令牌(需要从session或其他存储中获取) + // 这里简化处理,实际需要根据系统现有的会话管理机制实现 + String accessToken = request.getParameter("access_token"); + + if (StringUtils.isBlank(accessToken)) { + return ResponseEntity.ok(RestResult.error(ResultCode.PARAM_IS_BLANK, "访问令牌不能为空")); + } + + // 调用统一认证平台登出接口 + UnifiedAuthDTO.LogoutResponse response = unifiedAuthService.logout(accessToken); + + if ("0".equals(response.getCode())) { + // 清除本地会话 + // 这里需要根据系统现有的会话管理机制实现 + return ResponseEntity.ok(RestResult.ok("登出成功")); + } else { + return ResponseEntity.ok(RestResult.error(ResultCode.BUSINESS_LOGIC_ERROR, response.getMessage())); + } + + } catch (Exception e) { + log.error("统一认证平台单点登出失败", e); + return ResponseEntity.ok(RestResult.error(ResultCode.BUSINESS_LOGIC_ERROR, "登出失败: " + e.getMessage())); + } + } } diff --git a/src/main/java/com/chinaweal/aiccs/org/entity/dto/UnifiedAuthDTO.java b/src/main/java/com/chinaweal/aiccs/org/entity/dto/UnifiedAuthDTO.java new file mode 100644 index 0000000..53a88fa --- /dev/null +++ b/src/main/java/com/chinaweal/aiccs/org/entity/dto/UnifiedAuthDTO.java @@ -0,0 +1,92 @@ +package com.chinaweal.aiccs.org.entity.dto; + +import lombok.Data; + +/** + * 统一认证平台相关DTO + * + * @author lroyia + * @since 2025/03/02 + */ +@Data +public class UnifiedAuthDTO { + + /** + * 授权码 + */ + private String code; + + /** + * 访问令牌 + */ + private String accessToken; + + /** + * 令牌有效期(秒) + */ + private Integer expiresIn; + + /** + * 状态参数 + */ + private String state; + + /** + * 错误码 + */ + private String error; + + /** + * 错误描述 + */ + private String errorDescription; + + /** + * 用户ID + */ + private String userId; + + /** + * 用户名 + */ + private String userName; + + /** + * 姓名 + */ + private String name; + + /** + * 登录返回对象 + */ + @Data + public static class TokenResponse { + private String access_token; + private Integer expires_in; + private String error; + private String error_description; + } + + /** + * 用户信息返回对象 + */ + @Data + public static class UserInfoResponse { + private String userId; + private String userName; + private String name; + private String code; + private String message; + private String error; + private String error_description; + } + + /** + * 登出返回对象 + */ + @Data + public static class LogoutResponse { + private String code; + private String message; + } +} \ No newline at end of file diff --git a/src/main/java/com/chinaweal/aiccs/org/service/IUnifiedAuthService.java b/src/main/java/com/chinaweal/aiccs/org/service/IUnifiedAuthService.java new file mode 100644 index 0000000..5997916 --- /dev/null +++ b/src/main/java/com/chinaweal/aiccs/org/service/IUnifiedAuthService.java @@ -0,0 +1,52 @@ +package com.chinaweal.aiccs.org.service; + +import com.chinaweal.aiccs.org.entity.dto.UnifiedAuthDTO; + +/** + * 统一认证平台OAuth2服务接口 + * + * @author lroyia + * @since 2025/03/02 + */ +public interface IUnifiedAuthService { + + /** + * 生成统一认证平台登录URL + * + * @param state 状态参数,用于防止CSRF攻击 + * @return 登录URL + */ + String buildLoginUrl(String state); + + /** + * 通过授权码获取访问令牌 + * + * @param code 授权码 + * @return 令牌响应 + */ + UnifiedAuthDTO.TokenResponse getAccessToken(String code); + + /** + * 通过访问令牌获取用户信息 + * + * @param accessToken 访问令牌 + * @return 用户信息 + */ + UnifiedAuthDTO.UserInfoResponse getUserInfo(String accessToken); + + /** + * 单点登出 + * + * @param accessToken 访问令牌 + * @return 登出结果 + */ + UnifiedAuthDTO.LogoutResponse logout(String accessToken); + + /** + * 验证令牌有效性 + * + * @param accessToken 访问令牌 + * @return 是否有效 + */ + boolean checkToken(String accessToken); +} \ No newline at end of file diff --git a/src/main/java/com/chinaweal/aiccs/org/service/impl/UnifiedAuthServiceImpl.java b/src/main/java/com/chinaweal/aiccs/org/service/impl/UnifiedAuthServiceImpl.java new file mode 100644 index 0000000..2133d15 --- /dev/null +++ b/src/main/java/com/chinaweal/aiccs/org/service/impl/UnifiedAuthServiceImpl.java @@ -0,0 +1,207 @@ +package com.chinaweal.aiccs.org.service.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.chinaweal.aiccs.common.util.StringUtils; +import com.chinaweal.aiccs.config.UnifiedAuthConfig; +import com.chinaweal.aiccs.org.entity.dto.UnifiedAuthDTO; +import com.chinaweal.aiccs.org.service.IUnifiedAuthService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.util.UUID; + +/** + * 统一认证平台OAuth2服务实现 + * + * @author lroyia + * @since 2025/03/02 + */ +@Slf4j +@Service +public class UnifiedAuthServiceImpl implements IUnifiedAuthService { + + @Autowired + private UnifiedAuthConfig unifiedAuthConfig; + + @Autowired + private RestTemplate restTemplate; + + @Override + public String buildLoginUrl(String state) { + if (StringUtils.isBlank(state)) { + state = UUID.randomUUID().toString().replace("-", ""); + } + + StringBuilder url = new StringBuilder(unifiedAuthConfig.getAuthorizeUrl()); + url.append("?response_type=code"); + url.append("&client_id=").append(unifiedAuthConfig.getClientId()); + url.append("&redirect_uri=").append(unifiedAuthConfig.getRedirectUri()); + url.append("&state=").append(state); + + log.info("构建统一认证平台登录URL: {}", url.toString()); + return url.toString(); + } + + @Override + public UnifiedAuthDTO.TokenResponse getAccessToken(String code) { + try { + // 构建请求参数 + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", "authorization_code"); + params.add("client_id", unifiedAuthConfig.getClientId()); + params.add("client_secret", unifiedAuthConfig.getClientSecret()); + params.add("code", code); + params.add("redirect_uri", unifiedAuthConfig.getRedirectUri()); + + // 设置请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + HttpEntity> requestEntity = new HttpEntity<>(params, headers); + + // 发送POST请求 + ResponseEntity responseEntity = restTemplate.postForEntity( + unifiedAuthConfig.getTokenUrl(), + requestEntity, + String.class + ); + + String responseBody = responseEntity.getBody(); + log.info("获取访问令牌响应: {}", responseBody); + + // 解析响应 + UnifiedAuthDTO.TokenResponse response = JSON.parseObject(responseBody, UnifiedAuthDTO.TokenResponse.class); + + if (StringUtils.isNotBlank(response.getError())) { + log.error("获取访问令牌失败: {} - {}", response.getError(), response.getError_description()); + } + + return response; + } catch (Exception e) { + log.error("获取访问令牌异常", e); + UnifiedAuthDTO.TokenResponse errorResponse = new UnifiedAuthDTO.TokenResponse(); + errorResponse.setError("server_error"); + errorResponse.setError_description("服务器内部错误: " + e.getMessage()); + return errorResponse; + } + } + + @Override + public UnifiedAuthDTO.UserInfoResponse getUserInfo(String accessToken) { + try { + // 构建请求URL + String url = String.format("%s?access_token=%s&client_id=%s", + unifiedAuthConfig.getUserinfoUrl(), + accessToken, + unifiedAuthConfig.getClientId()); + + // 发送GET请求 + ResponseEntity responseEntity = restTemplate.getForEntity(url, String.class); + + String responseBody = responseEntity.getBody(); + log.info("获取用户信息响应: {}", responseBody); + + // 解析响应 + UnifiedAuthDTO.UserInfoResponse response = JSON.parseObject(responseBody, UnifiedAuthDTO.UserInfoResponse.class); + + // 统一处理返回数据 + if (StringUtils.isNotBlank(response.getError())) { + log.error("获取用户信息失败: {} - {}", response.getError(), response.getError_description()); + } else if (StringUtils.isNotBlank(response.getCode()) && !"0".equals(response.getCode())) { + log.error("获取用户信息失败: {} - {}", response.getCode(), response.getMessage()); + response.setError(response.getCode()); + response.setError_description(response.getMessage()); + } + + return response; + } catch (Exception e) { + log.error("获取用户信息异常", e); + UnifiedAuthDTO.UserInfoResponse errorResponse = new UnifiedAuthDTO.UserInfoResponse(); + errorResponse.setError("server_error"); + errorResponse.setError_description("服务器内部错误: " + e.getMessage()); + return errorResponse; + } + } + + @Override + public UnifiedAuthDTO.LogoutResponse logout(String accessToken) { + try { + // 构建请求参数 + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("access_token", accessToken); + params.add("client_id", unifiedAuthConfig.getClientId()); + + // 设置请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + HttpEntity> requestEntity = new HttpEntity<>(params, headers); + + // 发送POST请求 + ResponseEntity responseEntity = restTemplate.postForEntity( + unifiedAuthConfig.getLogoutUrl(), + requestEntity, + String.class + ); + + String responseBody = responseEntity.getBody(); + log.info("单点登出响应: {}", responseBody); + + // 解析响应 + UnifiedAuthDTO.LogoutResponse response = JSON.parseObject(responseBody, UnifiedAuthDTO.LogoutResponse.class); + + return response; + } catch (Exception e) { + log.error("单点登出异常", e); + UnifiedAuthDTO.LogoutResponse errorResponse = new UnifiedAuthDTO.LogoutResponse(); + errorResponse.setCode("error"); + errorResponse.setMessage("服务器内部错误: " + e.getMessage()); + return errorResponse; + } + } + + @Override + public boolean checkToken(String accessToken) { + try { + // 构建请求参数 + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("access_token", accessToken); + params.add("client_id", unifiedAuthConfig.getClientId()); + + // 设置请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity> requestEntity = new HttpEntity<>(params, headers); + + // 发送POST请求 + ResponseEntity responseEntity = restTemplate.postForEntity( + unifiedAuthConfig.getCheckTokenUrl(), + requestEntity, + String.class + ); + + String responseBody = responseEntity.getBody(); + log.info("验证令牌响应: {}", responseBody); + + if (StringUtils.isBlank(responseBody)) { + return false; + } + + // 解析响应JSON + JSONObject jsonObject = JSON.parseObject(responseBody); + String code = jsonObject.getString("code"); + return "0".equals(code); + + } catch (Exception e) { + log.error("验证令牌异常", e); + return false; + } + } +} \ No newline at end of file