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<String, String> 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<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);
|
|
ResponseEntity<String> 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<String> entity = new HttpEntity<>(headers);
|
|
ResponseEntity<String> 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;
|
}
|
|
|
}
|