liusheng
2025-08-09 44d70e42817bfb518f29240d396ee3f53297e9fc
SSO代码提交
已添加3个文件
已修改7个文件
525 ■■■■■ 文件已修改
ruoyi-admin/src/main/java/com/ruoyi/web/controller/sso/SSOController.java 362 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application-druid.yml 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application-hn.yml 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application-ls.yml 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application-xh.yml 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application.yml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/logback.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/domain/SSOTokenResponse.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/domain/SSOUserInfo.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/sso/SSOController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,362 @@
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;
    }
}
ruoyi-admin/src/main/resources/application-druid.yml
@@ -149,6 +149,26 @@
dingAppid: dingn8iip5ubj7clrrsv
dingAppSecret: qlEK8D3oOVwGPOTiBQIBYTqQVlAfy9S_qQizEQFjJdSScwemWFryg4gbneu-NqWD
# æ²³å—SSO配置(丽水用不到)
sso:
  enabled: true
  client_id: "mbglxt"  # ä¿¡é€šé™¢æä¾›
  client_secret: "mbglxt"  # ä¿¡é€šé™¢æä¾›
  # å†…网配置
  internal:
    authorize_url: "http://10.10.13.112:37727/connect/authorize"
    token_url: "http://10.10.13.112:37727/connect/token"
    userinfo_url: "http://10.10.13.112:37727/connect/userinfo"
    redirect_uri: "http://10.10.13.142:8096/sso/callback"
  # å¤–网配置
  external:
    authorize_url: "http://172.20.111.142:37727/connect/authorize"
    token_url: "http://172.20.111.142:37727/connect/token"
    userinfo_url: "http://172.20.111.142:37727/connect/userinfo"
    redirect_uri: "http://172.20.111.142:8096/sso/callback"
  state: "smartor"  # ä½ ä»¬ç³»ç»Ÿæ ‡è¯†
  scope: "openid roles profile"
# websocket超时时间
server:
websocket:
ruoyi-admin/src/main/resources/application-hn.yml
@@ -120,6 +120,27 @@
    default-page: 1
    default-size: 10
# SSO配置
sso:
  enabled: true
  client_id: "mbglxt"  # ä¿¡é€šé™¢æä¾›
  client_secret: "mbglxt"  # ä¿¡é€šé™¢æä¾›
  # å†…网配置
  internal:
    authorize_url: "http://10.10.13.112:37727/connect/authorize"
    token_url: "http://10.10.13.112:37727/connect/token"
    userinfo_url: "http://10.10.13.112:37727/connect/userinfo"
    redirect_uri: "http://10.10.13.142:8096/sso/callback"
  # å¤–网配置
  external:
    authorize_url: "http://172.20.111.142:37727/connect/authorize"
    token_url: "http://172.20.111.142:37727/connect/token"
    userinfo_url: "http://172.20.111.142:37727/connect/userinfo"
    redirect_uri: "http://172.20.111.142:8096/sso/callback"
  state: "smartor"  # ä½ ä»¬ç³»ç»Ÿæ ‡è¯†
  scope: "openid roles profile"
#钉钉的密钥
dingAppid: dingn8iip5ubj7clrrsv
dingAppSecret: qlEK8D3oOVwGPOTiBQIBYTqQVlAfy9S_qQizEQFjJdSScwemWFryg4gbneu-NqWD
ruoyi-admin/src/main/resources/application-ls.yml
@@ -124,6 +124,26 @@
    default-page: 1
    default-size: 10
# æ²³å—SSO配置(丽水用不到)
sso:
  enabled: true
  client_id: "mbglxt"  # ä¿¡é€šé™¢æä¾›
  client_secret: "mbglxt"  # ä¿¡é€šé™¢æä¾›
  # å†…网配置
  internal:
    authorize_url: "http://10.10.13.112:37727/connect/authorize"
    token_url: "http://10.10.13.112:37727/connect/token"
    userinfo_url: "http://10.10.13.112:37727/connect/userinfo"
    redirect_uri: "http://10.10.13.142:8096/sso/callback"
  # å¤–网配置
  external:
    authorize_url: "http://172.20.111.142:37727/connect/authorize"
    token_url: "http://172.20.111.142:37727/connect/token"
    userinfo_url: "http://172.20.111.142:37727/connect/userinfo"
    redirect_uri: "http://172.20.111.142:8096/sso/callback"
  state: "smartor"  # ä½ ä»¬ç³»ç»Ÿæ ‡è¯†
  scope: "openid roles profile"
#钉钉的密钥
dingAppid: dingn8iip5ubj7clrrsv
dingAppSecret: qlEK8D3oOVwGPOTiBQIBYTqQVlAfy9S_qQizEQFjJdSScwemWFryg4gbneu-NqWD
ruoyi-admin/src/main/resources/application-xh.yml
@@ -117,6 +117,26 @@
  supportMethodsArguments: true
  params: count=countSql
# æ²³å—SSO配置(新华用不到)
sso:
  enabled: true
  client_id: "mbglxt"  # ä¿¡é€šé™¢æä¾›
  client_secret: "mbglxt"  # ä¿¡é€šé™¢æä¾›
  # å†…网配置
  internal:
    authorize_url: "http://10.10.13.112:37727/connect/authorize"
    token_url: "http://10.10.13.112:37727/connect/token"
    userinfo_url: "http://10.10.13.112:37727/connect/userinfo"
    redirect_uri: "http://10.10.13.142:8096/sso/callback"
  # å¤–网配置
  external:
    authorize_url: "http://172.20.111.142:37727/connect/authorize"
    token_url: "http://172.20.111.142:37727/connect/token"
    userinfo_url: "http://172.20.111.142:37727/connect/userinfo"
    redirect_uri: "http://172.20.111.142:8096/sso/callback"
  state: "smartor"  # ä½ ä»¬ç³»ç»Ÿæ ‡è¯†
  scope: "openid roles profile"
#钉钉的密钥
dingAppid: dingn8iip5ubj7clrrsv
dingAppSecret: qlEK8D3oOVwGPOTiBQIBYTqQVlAfy9S_qQizEQFjJdSScwemWFryg4gbneu-NqWD
ruoyi-admin/src/main/resources/application.yml
@@ -74,7 +74,7 @@
    # å›½é™…化资源文件路径
    basename: i18n/messages
  profiles:
    active: druid
    active: ls
  # æ–‡ä»¶ä¸Šä¼ 
  servlet:
    multipart:
ruoyi-admin/src/main/resources/logback.xml
@@ -3,7 +3,7 @@
    <!-- æ—¥å¿—存放路径 8095-->
    <property name="log.path" value="D:/health/logs"/>
    <!-- æ—¥å¿—存放路径 8096-->
    <!--    <property name="log.path" value="D:/lihu/logs"/>-->
<!--        <property name="log.path" value="D:/lihu/logs"/>-->
    <!-- ä¸½æ°´æ—¥å¿—存放路径 -->
    <!--    <property name="log.path" value="/home/software/smartor-logs" />-->
    <!-- æ—¥å¿—输出格式 -->
ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
@@ -108,7 +108,7 @@
                // è¿‡æ»¤è¯·æ±‚
                .authorizeRequests()
                // å¯¹äºŽç™»å½•login æ³¨å†Œregister éªŒè¯ç captchaImage å…è®¸åŒ¿åè®¿é—®
                .antMatchers("/login", "/register", "/captchaImage", "/qrcode/generateStaticHtml", "/qrcode/getQRcode", "/qrcode/getFormDate", "/chat", "/system/file/admin/uploadFile", "/smartor/dingtalk/sendNotification", "/patient/read/patientInfo", "/socket", "/API_ESB_Service", "/API_ESB_Service/Run", "/magic/web/**", "/smartor/serviceSubtask/phoneCallBack", "/smartor/serviceSubtask/taskPull", "/smartor/serviceSubtask/phoneCallBackYQ", "/smartor/robot/callstatus", "/smartor/robot/aidialog", "/smartor/robot/cdrinfo", "/getToken", "/smartor/subtaskAnswer/getQuestionCache", "/smartor/subtaskAnswer/saveQuestionCache", "/smartor/servicetask/getScriptInfoByCondition", "/smartor/subtaskAnswer/saveQuestionAnswer", "/smartor/import/download", "/smartor/serviceSubtask/recordAccept", "/smartor/outPath/getInfoByParam", "/smartor/serviceExternal/addDeptInfo", "/smartor/serviceExternal/**").permitAll()
                .antMatchers("/login", "/register", "/captchaImage", "/qrcode/generateStaticHtml", "/qrcode/getQRcode", "/qrcode/getFormDate", "/chat", "/system/file/admin/uploadFile", "/smartor/dingtalk/sendNotification", "/patient/read/patientInfo", "/socket", "/API_ESB_Service", "/API_ESB_Service/Run", "/magic/web/**", "/smartor/serviceSubtask/phoneCallBack", "/smartor/serviceSubtask/taskPull", "/smartor/serviceSubtask/phoneCallBackYQ", "/smartor/robot/callstatus", "/smartor/robot/aidialog", "/smartor/robot/cdrinfo", "/getToken", "/smartor/subtaskAnswer/getQuestionCache", "/smartor/subtaskAnswer/saveQuestionCache", "/smartor/servicetask/getScriptInfoByCondition", "/smartor/subtaskAnswer/saveQuestionAnswer", "/smartor/import/download", "/smartor/serviceSubtask/recordAccept", "/smartor/outPath/getInfoByParam", "/smartor/serviceExternal/addDeptInfo", "/smartor/serviceExternal/**", "/sso/**").permitAll()
                // é™æ€èµ„源,可匿名访问
                .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll().antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
                // é™¤ä¸Šé¢å¤–的所有请求全部需要鉴权认证
smartor/src/main/java/com/smartor/domain/SSOTokenResponse.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
package com.smartor.domain;
public class SSOTokenResponse {
    private String id_token;
    private String access_token;
    private Integer expires_in;
    private String token_type;
    private String scope;
    // getter和setter方法
    public String getId_token() {
        return id_token;
    }
    public void setId_token(String id_token) {
        this.id_token = id_token;
    }
    public String getAccess_token() {
        return access_token;
    }
    public void setAccess_token(String access_token) {
        this.access_token = access_token;
    }
    public Integer getExpires_in() {
        return expires_in;
    }
    public void setExpires_in(Integer expires_in) {
        this.expires_in = expires_in;
    }
    public String getToken_type() {
        return token_type;
    }
    public void setToken_type(String token_type) {
        this.token_type = token_type;
    }
    public String getScope() {
        return scope;
    }
    public void setScope(String scope) {
        this.scope = scope;
    }
}
smartor/src/main/java/com/smartor/domain/SSOUserInfo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
package com.smartor.domain;
public class SSOUserInfo {
    private String sub;  // ç”¨æˆ·ID
    private String name; // ç”¨æˆ·å/工号
    private String nickname; // æ˜¾ç¤ºå
    // getter和setter方法
    public String getSub() { return sub; }
    public void setSub(String sub) { this.sub = sub; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getNickname() { return nickname; }
    public void setNickname(String nickname) { this.nickname = nickname; }
    @Override
    public String toString() {
        return "SSOUserInfo{" +
                "sub='" + sub + '\'' +
                ", name='" + name + '\'' +
                ", nickname='" + nickname + '\'' +
                '}';
    }
}