package com.ruoyi.web.controller.sso; import com.alibaba.fastjson.JSON; import com.ruoyi.common.core.domain.entity.SysUser; import com.ruoyi.common.core.domain.model.LoginUser; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.framework.web.service.TokenService; import com.ruoyi.system.service.ISysUserService; import com.smartor.domain.SSOTokenResponse; import com.smartor.domain.SSOUserInfo; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URLEncoder; /** * SSO单点登录控制器 */ @RestController @RequestMapping("/sso") @Slf4j public class SSOController { @Value("${sso.client_id}") private String clientId; @Value("${sso.client_secret}") private String clientSecret; @Value("${sso.internal.authorize_url}") private String internalAuthorizeUrl; @Value("${sso.internal.token_url}") private String internalTokenUrl; @Value("${sso.internal.userinfo_url}") private String internalUserinfoUrl; @Value("${sso.internal.redirect_uri}") private String internalRedirectUri; @Value("${sso.external.authorize_url}") private String externalAuthorizeUrl; @Value("${sso.external.token_url}") private String externalTokenUrl; @Value("${sso.external.userinfo_url}") private String externalUserinfoUrl; @Value("${sso.external.redirect_uri}") private String externalRedirectUri; @Value("${sso.state}") private String state; @Value("${sso.scope}") private String scope; @Autowired private ISysUserService userService; @Autowired private TokenService tokenService; private final RestTemplate restTemplate; public SSOController() { // 配置RestTemplate超时 SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(10000); // 连接超时10秒 factory.setReadTimeout(30000); // 读取超时30秒 this.restTemplate = new RestTemplate(factory); } /** * SSO登录入口 - 信通院会调用这个地址 * 访问路径:http://域名:8095/sso/login */ @GetMapping("") public void ssoLogin(HttpServletResponse response, HttpServletRequest request) throws IOException { log.info("收到SSO登录请求,开始重定向到授权服务器"); // 获取客户端IP String clientIp = getClientIp(request); boolean isInternal = isInternalNetwork(clientIp); // 构建授权URL String authUrl = buildAuthorizationUrl(isInternal); log.info("重定向到授权URL: {}", authUrl); response.sendRedirect(authUrl); } /** * SSO回调处理 */ @GetMapping("/callback") public void ssoCallback(@RequestParam(required = false) String code, @RequestParam(required = false) String state, @RequestParam(required = false) String error, HttpServletResponse response, HttpServletRequest request) throws IOException { log.info("收到SSO回调,code: {}, state: {}, error: {}", code, state, error); if (error != null) { log.error("SSO授权失败: {}", error); try { response.sendRedirect("/login?sso_error=" + URLEncoder.encode(error, "UTF-8")); } catch (Exception e) { log.error("重定向失败", e); response.sendRedirect("/login?sso_error=unknown_error"); } return; } if (code == null || !this.state.equals(state)) { log.error("SSO回调参数错误,code: {}, state: {}", code, state); response.sendRedirect("/login?sso_error=invalid_callback"); return; } try { // 获取客户端IP String clientIp = getClientIp(request); boolean isInternal = isInternalNetwork(clientIp); // 1. 用code换取access_token SSOTokenResponse tokenResponse = getAccessToken(code, isInternal); log.info("获取到access_token: {}", tokenResponse.getAccess_token()); // 2. 用access_token获取用户信息 SSOUserInfo userInfo = getUserInfo(tokenResponse.getAccess_token(), isInternal); log.info("获取到用户信息: {}", userInfo); // 3. 根据用户信息创建本地会话 String token = createLocalSession(userInfo); // 4. 重定向到前端首页,携带token String frontendUrl = "/#/index?token=" + token; response.sendRedirect(frontendUrl); } catch (RuntimeException e) { log.error("SSO业务处理失败: {}", e.getMessage(), e); try { response.sendRedirect("/login?sso_error=" + URLEncoder.encode(e.getMessage(), "UTF-8")); } catch (Exception ex) { log.error("重定向失败", ex); response.sendRedirect("/login?sso_error=system_error"); } } catch (Exception e) { log.error("SSO登录处理失败", e); response.sendRedirect("/login?sso_error=login_failed"); } } /** * 构建授权URL */ private String buildAuthorizationUrl(boolean isInternal) { try { String redirectUri = getRedirectUri(isInternal); return getAuthorizeUrl(isInternal) + "?" + "client_id=" + clientId + "&redirect_uri=" + URLEncoder.encode(redirectUri, "UTF-8") + "&response_type=code" + "&state=" + state + "&scope=" + URLEncoder.encode(scope, "UTF-8"); } catch (Exception e) { log.error("构建授权URL失败", e); throw new RuntimeException("构建授权URL失败", e); } } /** * 获取访问令牌 */ private SSOTokenResponse getAccessToken(String code, boolean isInternal) throws Exception { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); MultiValueMap params = new LinkedMultiValueMap<>(); params.add("client_id", clientId); params.add("client_secret", clientSecret); params.add("code", code); params.add("grant_type", "authorization_code"); params.add("redirect_uri", getRedirectUri(isInternal)); HttpEntity> request = new HttpEntity<>(params, headers); ResponseEntity response = restTemplate.exchange( getTokenUrl(isInternal), HttpMethod.POST, request, String.class); log.info("Token响应: {}", response.getBody()); if (response.getBody() == null || response.getBody().trim().isEmpty()) { throw new RuntimeException("Token响应为空"); } SSOTokenResponse tokenResponse = JSON.parseObject(response.getBody(), SSOTokenResponse.class); if (tokenResponse == null || StringUtils.isEmpty(tokenResponse.getAccess_token())) { throw new RuntimeException("获取access_token失败"); } return tokenResponse; } /** * 获取用户信息 */ private SSOUserInfo getUserInfo(String accessToken, boolean isInternal) throws Exception { HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + accessToken); HttpEntity entity = new HttpEntity<>(headers); ResponseEntity response = restTemplate.exchange( getUserinfoUrl(isInternal), HttpMethod.GET, entity, String.class); log.info("用户信息响应: {}", response.getBody()); if (response.getBody() == null || response.getBody().trim().isEmpty()) { throw new RuntimeException("用户信息响应为空"); } SSOUserInfo userInfo = JSON.parseObject(response.getBody(), SSOUserInfo.class); if (userInfo == null || StringUtils.isEmpty(userInfo.getName())) { throw new RuntimeException("获取用户信息失败或用户名为空"); } return userInfo; } /** * 创建本地会话 */ private String createLocalSession(SSOUserInfo userInfo) { // 根据SSO用户信息查找本地用户(根据工号匹配) SysUser localUser = findLocalUserByName(userInfo.getName()); if (localUser == null) { throw new RuntimeException("用户不存在或未开通系统权限:" + userInfo.getName()); } // 创建登录用户对象 LoginUser loginUser = new LoginUser(localUser.getUserId(), localUser.getDeptId(), localUser, null); // 生成token return tokenService.createToken(loginUser); } /** * 根据工号查找本地用户 */ private SysUser findLocalUserByName(String workNumber) { if (StringUtils.isEmpty(workNumber)) { log.error("工号为空,无法查找用户"); return null; } try { SysUser user = userService.selectUserByUserName(workNumber); if (user != null) { log.info("找到用户: {} - {}", workNumber, user.getNickName()); } else { log.warn("未找到用户: {}", workNumber); } return user; } catch (Exception e) { log.error("查询用户失败: {}", workNumber, e); return null; } } /** * 根据客户端IP判断是否为内网 */ private boolean isInternalNetwork(String clientIp) { if (clientIp == null || clientIp.isEmpty()) { return false; } // 判断是否为内网网段 10.10.13.* return clientIp.startsWith("10.10.13."); } /** * 获取客户端真实IP */ private String getClientIp(HttpServletRequest request) { String ip = request.getHeader("X-Forwarded-For"); if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_CLIENT_IP"); } if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } // 如果有多个IP,取第一个 if (ip != null && ip.contains(",")) { ip = ip.split(",")[0].trim(); } log.info("客户端IP: {}", ip); return ip; } /** * 根据网络环境获取授权URL */ private String getAuthorizeUrl(boolean isInternal) { return isInternal ? internalAuthorizeUrl : externalAuthorizeUrl; } /** * 根据网络环境获取Token URL */ private String getTokenUrl(boolean isInternal) { return isInternal ? internalTokenUrl : externalTokenUrl; } /** * 根据网络环境获取用户信息URL */ private String getUserinfoUrl(boolean isInternal) { return isInternal ? internalUserinfoUrl : externalUserinfoUrl; } /** * 根据网络环境获取回调URI */ private String getRedirectUri(boolean isInternal) { return isInternal ? internalRedirectUri : externalRedirectUri; } }