bihuisong 11 months ago
parent
commit
55efb57758

+ 4 - 4
zhsq_qk-admin/src/main/resources/application-druid.yml

@@ -9,12 +9,12 @@ spring:
 #        url: jdbc:mysql://localhost:3306/zhsq_qk_2.0?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
 #        username: root
 #        password: root
-        url: jdbc:mysql://192.168.10.15:63306/zhsq_qk_2.0?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
-        username: root
-        password: sooka1a2b3c4d%...
-#        url: jdbc:mysql://192.168.1.13:63306/zhsq_qk_2.0?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+#        url: jdbc:mysql://192.168.10.15:63306/zhsq_qk_2.0?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
 #        username: root
 #        password: sooka1a2b3c4d%...
+        url: jdbc:mysql://192.168.1.13:63306/zhsq_qk_2.0?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+        username: root
+        password: sooka1a2b3c4d%...
       # 从库数据源
       slave:
         # 从数据源开关/默认关闭

+ 2 - 2
zhsq_qk-admin/src/main/resources/application.yml

@@ -68,11 +68,11 @@ spring:
   # redis 配置
   redis:
     # 地址
-    host: localhost
+    host: www.sooka.onest.com
     # 端口,默认为6379
     port: 16379
     # 数据库索引
-    database: 0
+    database: 1
     # 密码
     password: sooka123456
     # 连接超时时间

+ 5 - 0
zhsq_qk-common/pom.xml

@@ -126,6 +126,11 @@
             <artifactId>javax.servlet-api</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <version>1.18.30</version>
+        </dependency>
     </dependencies>
 
 </project>

+ 7 - 0
zhsq_qk-common/src/main/java/zhsq_qk/common/core/domain/model/LoginUser.java

@@ -1,6 +1,7 @@
 package zhsq_qk.common.core.domain.model;
 
 import com.alibaba.fastjson2.annotation.JSONField;
+import lombok.Data;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.userdetails.UserDetails;
 import zhsq_qk.common.core.domain.entity.SysUser;
@@ -13,6 +14,7 @@ import java.util.Set;
  *
  * @author  
  */
+@Data
 public class LoginUser implements UserDetails {
     private static final long serialVersionUID = 1L;
 
@@ -22,6 +24,11 @@ public class LoginUser implements UserDetails {
     private Long userId;
 
     /**
+     * 用户名
+     */
+    private String username;
+
+    /**
      * 部门ID
      */
     private Long deptId;

+ 11 - 2
zhsq_qk-framework/pom.xml

@@ -52,13 +52,22 @@
             <groupId>com.github.oshi</groupId>
             <artifactId>oshi-core</artifactId>
         </dependency>
-
+        <!-- Http 请求工具 -->
+        <dependency>
+            <groupId>com.dtflys.forest</groupId>
+            <artifactId>forest-spring-boot-starter</artifactId>
+            <version>1.5.26</version>
+        </dependency>
         <!-- 系统模块-->
         <dependency>
             <groupId>zhsq_qk</groupId>
             <artifactId>zhsq_qk-system</artifactId>
         </dependency>
-
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <version>1.18.30</version>
+        </dependency>
     </dependencies>
 
 </project>

+ 1 - 1
zhsq_qk-framework/src/main/java/zhsq_qk/framework/config/SecurityConfig.java

@@ -108,7 +108,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
                 // 过滤请求
                 .authorizeRequests()
                 // 对于登录login 注册register 验证码captchaImage 允许匿名访问
-                .antMatchers("/login", "/register", "/captchaImage").permitAll()
+                .antMatchers("/login", "/register", "/captchaImage","/sso/**").permitAll()
                 // 静态资源,可匿名访问
                 .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
                 .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()

+ 124 - 0
zhsq_qk-framework/src/main/java/zhsq_qk/framework/sso/SsoLoginController.java

@@ -0,0 +1,124 @@
+package zhsq_qk.framework.sso;
+
+import org.apache.commons.lang3.ObjectUtils;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import zhsq_qk.common.core.domain.AjaxResult;
+import zhsq_qk.common.core.domain.entity.SysUser;
+import zhsq_qk.framework.sso.utli.AjaxJson;
+import zhsq_qk.framework.sso.utli.MyHttpSessionHolder;
+import zhsq_qk.framework.web.service.SysLoginService;
+import zhsq_qk.framework.web.service.TokenService;
+import zhsq_qk.system.service.impl.SysUserServiceImpl;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController
+public class SsoLoginController {
+
+    @Resource
+    private SysUserServiceImpl sysUserServiceImpl;
+
+    @Resource
+    private SysLoginService sysLoginService;
+
+    @Resource
+    private TokenService tokenService;
+
+    @Resource
+    private SsoRequestUtil ssoRequestUtil;
+
+
+    // 返回SSO认证中心登录地址 (前后台分离环境下专用)
+    @RequestMapping("/sso/getSsoAuthUrl")
+    public AjaxJson getSsoAuthUrl(String clientLoginUrl) {
+        String serverAuthUrl = ssoRequestUtil.buildServerAuthUrl(clientLoginUrl);
+        return AjaxJson.getSuccessData(serverAuthUrl);
+    }
+
+    // 根据ticket进行登录(前后台分离环境下专用)
+    @RequestMapping("/sso/doLoginByTicket")
+    public AjaxResult doLoginByTicket(String ticket, HttpServletRequest request) {
+        Map map = new HashMap();
+        HttpSession session = MyHttpSessionHolder.getSession(request);
+        Object loginId = ssoRequestUtil.checkTicket(ticket, request, "/sso/doLoginByTicket");
+        if(loginId != null) {
+            session.setAttribute("userId", loginId);
+            session.setAttribute("user", ssoRequestUtil.getUserInfo(loginId));
+            /**此处处理一体化用户登录业务**/
+            SysUser user = sysUserServiceImpl.selectUserById(Long.parseLong(loginId.toString()));//此处根据sso端用户唯一标识来换取客户端用户与唯一标识,不局限于用户ID,只要能和sso端的loginId对应即可
+            if(ObjectUtils.isNotEmpty(user)){
+                String token = sysLoginService.ssoLogin(user.getUserName());
+                map.put("access_token", token);
+                map.put("expires_in", 30);
+                map.put("userinfo",user);
+                map.put("session_id", session.getId());
+            }
+            return AjaxResult.success(map);
+        }
+        return AjaxResult.error("无效ticket:" + ticket);
+    }
+
+    // getInfo接口 (只有登录后才可以调用此接口)
+    @RequestMapping("/getCurrInfo")
+    public Object getCurrInfo(HttpServletRequest request) {
+        // 如果没有登录,就返回特定信息
+        HttpSession session = MyHttpSessionHolder.getSession(request);
+        if(session.getAttribute("userId") == null) {
+            return AjaxJson.getNotLogin();
+        }
+        // 从Session中获取user对象
+        SysUser user = (SysUser) session.getAttribute("user");
+        return AjaxJson.getSuccessData(user);
+    }
+
+
+    // SSO-Client端:单点注销地址
+    @RequestMapping("/sso/logout")
+    public Object ssoLogout(HttpServletRequest request, HttpServletResponse response) throws IOException {
+
+        // 如果未登录,则无需注销
+        HttpSession session = MyHttpSessionHolder.getSession(request);
+        if(session.getAttribute("userId") == null) {
+            return AjaxResult.success(ssoRequestUtil.getLogoutRedirectUrl());
+        }
+
+        // 调用 sso-server 认证中心单点注销API
+        Object loginId = session.getAttribute("userId");  // 账号id
+        String timestamp = String.valueOf(System.currentTimeMillis());	// 时间戳
+        String nonce = ssoRequestUtil.getRandomString(20);		// 随机字符串
+        String sign = ssoRequestUtil.getSign(loginId, timestamp, nonce, ssoRequestUtil.secretkey);	// 参数签名
+
+        String url = ssoRequestUtil.getSloUrl() +
+                "?loginId=" + loginId +
+                "&timestamp=" + timestamp +
+                "&nonce=" + nonce +
+                "&sign=" + sign;
+        AjaxJson result = ssoRequestUtil.request(url);
+
+        // 校验响应状态码,200 代表成功
+        if(result.getCode() == 200) {
+
+            // 极端场景下,sso-server 中心的单点注销可能并不会通知到此 client 端,所以这里需要再补一刀
+            session.removeAttribute("userId");
+            // 如果指定了 back 地址,则重定向,否则返回 JSON 信息
+            return AjaxResult.success("注销成功",ssoRequestUtil.getLogoutRedirectUrl());
+//            if(SsoRequestUtil.isNotEmpty(back)) {
+//                response.sendRedirect(back);
+//                return null;
+//            } else {
+//                return AjaxJson.getSuccess();
+//            }
+
+        } else {
+            // 将 sso-server 回应的消息作为异常抛出
+            throw new RuntimeException(result.getMsg());
+        }
+    }
+}

+ 302 - 0
zhsq_qk-framework/src/main/java/zhsq_qk/framework/sso/SsoRequestUtil.java

@@ -0,0 +1,302 @@
+package zhsq_qk.framework.sso;
+
+import com.dtflys.forest.Forest;
+import org.springframework.stereotype.Service;
+import zhsq_qk.framework.sso.domain.SysLoginUser;
+import zhsq_qk.framework.sso.utli.AjaxJson;
+import zhsq_qk.framework.sso.utli.JsonUtil;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.security.MessageDigest;
+import java.util.Map;
+import java.util.Random;
+
+/**
+ * 封装一些 sso 共用方法
+ *
+ * @author kong
+ * @since: 2022-4-30
+ */
+@Service
+public class SsoRequestUtil {
+
+
+    public String host = "http://192.168.10.12:3000";
+
+    /**
+     * SSO-Server端 统一认证地址
+     */
+    public String authUrl;
+
+    public String getAuthUrl() {
+        return authUrl = host + "/sso/auth";
+    }
+    /**
+     * 使用 Http 请求校验ticket
+     */
+//	public static boolean isHttp = true;
+
+    /**
+     * SSO-Server端 ticket校验地址
+     */
+    public String checkTicketUrl;
+
+    public String getCheckTicketUrl() {
+        return checkTicketUrl = host + "/sso/checkTicket";
+    }
+
+    /**
+     * 打开单点注销功能
+     */
+    public boolean isSlo = true;
+
+    /**
+     * 单点注销地址
+     */
+    public String sloUrl;
+
+    public String getSloUrl() {
+        return sloUrl = host + "/sso/signout";
+    }
+    /**
+     * 接口调用秘钥
+     */
+    public String secretkey = "YQfyZtAmDbYHTBaHPSx3GZeX7x2ip7ik";
+
+    /**
+     * SSO-Server端 注销后跳转地址
+     */
+    public String logoutRedirectUrl;
+
+    public String getLogoutRedirectUrl() {
+        return logoutRedirectUrl = host + "/home";
+    }
+    /**
+     * SSO-Server端 查询userinfo地址
+     */
+    public String userinfoUrl;
+    public String getUserinfoUrl() {
+        return userinfoUrl = host + "/sso/userinfo";
+    }
+    /**
+     * 当前 client 的标识,可为 null
+     */
+    public String client = "";
+
+
+    // -------------------------- 封装方法
+
+    /**
+     * 拼接 sso 授权地址
+     * @return
+     */
+    public String buildServerAuthUrl(String clientLoginUrl) {
+        String serverAuthUrl = getAuthUrl();
+        if(isNotEmpty(client)) {
+            serverAuthUrl = joinParam(serverAuthUrl, "client=" + client);
+        }
+        serverAuthUrl = joinParam(serverAuthUrl, "redirect=" + clientLoginUrl);
+        return serverAuthUrl;
+    }
+
+    /**
+     * 校验 ticket,返回 userId
+     * @param ticket ticket 码
+     * @param request 请求对象
+     * @param currPath 当前接口的访问path
+     * @return
+     */
+    public Object checkTicket(String ticket, HttpServletRequest request, String currPath) {
+        // 校验 ticket 的地址
+        String checkUrl = getCheckTicketUrl() + "?ticket=" + ticket;
+
+        // 如果锁定 client 的标识
+        if(isNotEmpty(client)) {
+            checkUrl += "&client=" + client;
+        }
+        // 如果打开了单点注销
+        if(isSlo) {
+            String ssoLogoutCall = request.getRequestURL().toString().replace(currPath, "/sso/logoutCall");
+            checkUrl += "&ssoLogoutCall=" + ssoLogoutCall;
+        }
+        // 发起请求
+        AjaxJson result = request(checkUrl);
+
+        // 200 代表校验成功
+        if(result.getCode() == 200 && SsoRequestUtil.isEmpty(result.getData()) == false) {
+            // 登录上
+            Object loginId = result.getData();
+            return loginId;
+        } else {
+            // 将 sso-server 回应的消息作为异常抛出
+            throw new RuntimeException(result.getMsg());
+        }
+    }
+
+    /**
+     * 获取指定用户id的详细资料  (调用此接口的前提是 sso-server 端开放了 /sso/userinfo 路由)
+     */
+    public SysLoginUser getUserInfo(Object loginId) {
+
+        // 组织 url 参数
+        String timestamp = String.valueOf(System.currentTimeMillis());	// 时间戳
+        String nonce = getRandomString(20);		// 随机字符串
+        String sign = getSign(loginId, timestamp, nonce, secretkey);	// 参数签名
+
+        // 请求
+        String url = getUserinfoUrl() +
+                "?loginId=" + loginId +
+                "&timestamp=" + timestamp +
+                "&nonce=" + nonce +
+                "&sign=" + sign;
+        AjaxJson result = request(url);
+
+        // 如果返回值的 code 不是200,代表请求失败
+        if(result.getCode() == null || result.getCode() != 200) {
+            throw new RuntimeException(result.getMsg());
+        }
+
+        // 解析出 user
+        SysLoginUser user = JsonUtil.parseObjectToModel(result.getData(), SysLoginUser.class);
+        return user;
+    }
+
+
+    // -------------------------- 工具方法
+
+    /**
+     * 发出请求,并返回 SaResult 结果
+     * @param url 请求地址
+     * @return 返回的结果
+     */
+    public static AjaxJson request(String url) {
+        System.out.println("------ 发起请求:" + url);
+        Map<String, Object> map = Forest.post(url).executeAsMap();
+        return new AjaxJson(map);
+    }
+
+    /**
+     * 根据参数计算签名
+     * @param loginId 账号id
+     * @param timestamp 当前时间戳,13位
+     * @param nonce 随机字符串
+     * @param secretkey 账号id
+     * @return 签名
+     */
+    public static String getSign(Object loginId, String timestamp, String nonce, String secretkey) {
+        return md5("loginId=" + loginId + "&nonce=" + nonce + "&timestamp=" + timestamp + "&key=" + secretkey);
+    }
+
+    /**
+     * 指定元素是否为null或者空字符串
+     * @param str 指定元素
+     * @return 是否为null或者空字符串
+     */
+    public static boolean isEmpty(Object str) {
+        return str == null || "".equals(str);
+    }
+
+    /**
+     * 指定元素是否不为null或者空字符串
+     * @param str 指定元素
+     * @return 是否为null或者空字符串
+     */
+    public static boolean isNotEmpty(Object str) {
+        return !isEmpty(str);
+    }
+
+    /**
+     * md5加密
+     * @param str 指定字符串
+     * @return 加密后的字符串
+     */
+    public static String md5(String str) {
+        str = (str == null ? "" : str);
+        char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+        try {
+            byte[] btInput = str.getBytes();
+            MessageDigest mdInst = MessageDigest.getInstance("MD5");
+            mdInst.update(btInput);
+            byte[] md = mdInst.digest();
+            int j = md.length;
+            char[] strA = new char[j * 2];
+            int k = 0;
+            for (byte byte0 : md) {
+                strA[k++] = hexDigits[byte0 >>> 4 & 0xf];
+                strA[k++] = hexDigits[byte0 & 0xf];
+            }
+            return new String(strA);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 生成指定长度的随机字符串
+     *
+     * @param length 字符串的长度
+     * @return 一个随机字符串
+     */
+    public static String getRandomString(int length) {
+        String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+        Random random = new Random();
+        StringBuffer sb = new StringBuffer();
+        for (int i = 0; i < length; i++) {
+            int number = random.nextInt(62);
+            sb.append(str.charAt(number));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * URL编码
+     * @param url see note
+     * @return see note
+     */
+    public static String encodeUrl(String url) {
+        try {
+            return URLEncoder.encode(url, "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 在url上拼接上kv参数并返回
+     * @param url url
+     * @param parameStr 参数, 例如 id=1001
+     * @return 拼接后的url字符串
+     */
+    public static String joinParam(String url, String parameStr) {
+        // 如果参数为空, 直接返回
+        if(parameStr == null || parameStr.length() == 0) {
+            return url;
+        }
+        if(url == null) {
+            url = "";
+        }
+        int index = url.lastIndexOf('?');
+        // ? 不存在
+        if(index == -1) {
+            return url + '?' + parameStr;
+        }
+        // ? 是最后一位
+        if(index == url.length() - 1) {
+            return url + parameStr;
+        }
+        // ? 是其中一位
+        if(index > -1 && index < url.length() - 1) {
+            String separatorChar = "&";
+            // 如果最后一位是 不是&, 且 parameStr 第一位不是 &, 就增送一个 &
+            if(url.lastIndexOf(separatorChar) != url.length() - 1 && parameStr.indexOf(separatorChar) != 0) {
+                return url + separatorChar + parameStr;
+            } else {
+                return url + parameStr;
+            }
+        }
+        // 正常情况下, 代码不可能执行到此
+        return url;
+    }
+}

+ 131 - 0
zhsq_qk-framework/src/main/java/zhsq_qk/framework/sso/domain/SysLoginUser.java

@@ -0,0 +1,131 @@
+package zhsq_qk.framework.sso.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import lombok.Data;
+import lombok.experimental.Accessors;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * Model: sys_user -- 用户表
+ * @author shengzhang
+ */
+@Data
+@Accessors(chain = true)
+public class SysLoginUser implements Serializable {
+
+    // ---------- 模块常量 ----------
+    /**
+     * 序列化版本id
+     */
+    private static final long serialVersionUID = 1L;
+    /**
+     * 此模块对应的表名
+     */
+    public static final String TABLE_NAME = "sys_user";
+    /**
+     * 此模块对应的权限码
+     */
+    public static final String PERMISSION_CODE = "sys-user";
+
+
+    // ---------- 表中字段 ----------
+    /**
+     * id号
+     */
+    private Long id;
+
+    /**
+     * 用户昵称
+     */
+    private String username;
+
+    /**
+     * 账号密码
+     */
+    private String password;
+
+    /**
+     * 用户头像
+     */
+    private String avatar;
+
+    /**
+     * 个人介绍(签名)
+     */
+    private String intro;
+
+    /**
+     * 用户年龄
+     */
+    private Integer age;
+
+    /**
+     * 用户性别 (1=男,2=女,3=未知)
+     */
+    private Integer sex;
+
+    /**
+     * 手机号
+     */
+    private String phone;
+
+    /**
+     * 用户邮箱
+     */
+    private String email;
+
+    /**
+     * 账号状态(1=正常,2=禁用)
+     */
+    private Integer status;
+
+    /**
+     * 创建时间
+     */
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+08:00")
+    private Date createTime;
+
+    /**
+     * 上次登陆时间
+     */
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+08:00")
+    private Date loginTime;
+
+    /**
+     * 上次登陆IP
+     */
+    private String loginIp;
+
+    /**
+     * 登陆次数
+     */
+    private Integer loginCount;
+
+    /**
+     * 是否删除(1=否,2=是)
+     */
+    private Integer isDel;
+
+
+    /** 防止密码被传递到前台  */
+    public String getPassword(){
+        return "********";
+    }
+    /** 获取真实密码   */
+    @JsonIgnore()
+    public String getPassword2(){
+        return this.password;
+    }
+
+
+
+
+
+
+}

+ 230 - 0
zhsq_qk-framework/src/main/java/zhsq_qk/framework/sso/utli/AjaxJson.java

@@ -0,0 +1,230 @@
+package zhsq_qk.framework.sso.utli;
+
+import java.io.Serializable;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * ajax请求返回Json格式数据的封装 <br>
+ * 所有预留字段:<br>
+ * code=状态码 <br>
+ * msg=描述信息 <br>
+ * data=携带对象 <br>
+ * pageNo=当前页 <br>
+ * pageSize=页大小 <br>
+ * startIndex=起始索引 <br>
+ * dataCount=数据总数 <br>
+ * pageCount=分页总数 <br>
+ * <p> 返回范例:</p>
+ *  <pre>
+ {
+ "code": 200,    // 成功时=200, 失败时=500  msg=失败原因
+ "msg": "ok",
+ "data": {}
+ }
+ </pre>
+ */
+public class AjaxJson extends LinkedHashMap<String, Object> implements Serializable {
+
+    private static final long serialVersionUID = 1L;	// 序列化版本号
+
+    public static final int CODE_SUCCESS = 200;			// 成功状态码
+    public static final int CODE_ERROR = 500;			// 错误状态码
+    public static final int CODE_WARNING = 501;			// 警告状态码
+    public static final int CODE_NOT_JUR = 403;			// 无权限状态码
+    public static final int CODE_NOT_LOGIN = 401;		// 未登录状态码
+    public static final int CODE_INVALID_REQUEST = 400;	// 无效请求状态码
+
+
+
+    // ============================  写值取值  ==================================
+
+    /** 给code赋值,连缀风格 */
+    public AjaxJson setCode(int code) {
+        this.put("code", code);
+        return this;
+    }
+    /** 返回code */
+    public Integer getCode() {
+        return (Integer)this.get("code");
+    }
+
+    /** 给msg赋值,连缀风格 */
+    public AjaxJson setMsg(String msg) {
+        this.put("msg", msg);
+        return this;
+    }
+    /** 获取msg */
+    public String getMsg() {
+        return (String)this.get("msg");
+    }
+
+    /** 给data赋值,连缀风格 */
+    public AjaxJson setData(Object data) {
+        this.put("data", data);
+        return this;
+    }
+    /** 获取data */
+    public Object getData() {
+        return this.get("data");
+    }
+    /** 将data还原为指定类型并返回 */
+    @SuppressWarnings("unchecked")
+    public <T> T getData(Class<T> cs) {
+        return (T) this.getData();
+    }
+
+    /** 给dataCount(数据总数)赋值,连缀风格 */
+    public AjaxJson setDataCount(Long dataCount) {
+        this.put("dataCount", dataCount);
+        // 如果提供了数据总数,则尝试计算page信息
+        if(dataCount != null && dataCount >= 0) {
+            // 如果:已有page信息
+            if(get("pageNo") != null) {
+                this.initPageInfo();
+            }
+//			// 或者:是JavaWeb环境
+//			else if(SoMap.isJavaWeb()) {
+//				SoMap so = SoMap.getRequestSoMap();
+//				this.setPageNoAndSize(so.getKeyPageNo(), so.getKeyPageSize());
+//				this.initPageInfo();
+//			}
+        }
+        return this;
+    }
+    /** 获取dataCount(数据总数) */
+    public Long getDataCount() {
+        return (Long)this.get("dataCount");
+    }
+
+    /** 设置pageNo 和 pageSize,并计算出startIndex于pageCount */
+    public AjaxJson setPageNoAndSize(long pageNo, long pageSize) {
+        this.put("pageNo", pageNo);
+        this.put("pageSize", pageSize);
+        return this;
+    }
+
+    /** 根据 pageSize dataCount,计算startIndex 与 pageCount */
+    public AjaxJson initPageInfo() {
+        long pageNo = (long)this.get("pageNo");
+        long pageSize = (long)this.get("pageSize");
+        long dataCount = (long)this.get("dataCount");
+        this.set("startIndex", (pageNo - 1) * pageSize);
+        long pc = dataCount / pageSize;
+        this.set("pageCount", (dataCount % pageSize == 0 ?  pc : pc + 1));
+        return this;
+    }
+
+
+    /** 写入一个值 自定义key, 连缀风格 */
+    public AjaxJson set(String key, Object data) {
+        this.put(key, data);
+        return this;
+    }
+
+    /** 写入一个Map, 连缀风格 */
+    public AjaxJson setMap(Map<String, ?> map) {
+        for (String key : map.keySet()) {
+            this.put(key, map.get(key));
+        }
+        return this;
+    }
+
+
+    // ============================  构建  ==================================
+
+    public AjaxJson(int code, String msg, Object data, Long dataCount) {
+        this.setCode(code);
+        this.setMsg(msg);
+        this.setData(data);
+        if(dataCount != null) {
+            this.setDataCount(dataCount);
+        }
+    }
+
+    public AjaxJson(Map<String, Object> map) {
+        for (String key: map.keySet()) {
+            this.set(key, map.get(key));
+        }
+    }
+
+    /** 返回成功 */
+    public static AjaxJson getSuccess() {
+        return new AjaxJson(CODE_SUCCESS, "ok", null, null);
+    }
+    public static AjaxJson getSuccess(String msg) {
+        return new AjaxJson(CODE_SUCCESS, msg, null, null);
+    }
+    public static AjaxJson getSuccess(String msg, Object data) {
+        return new AjaxJson(CODE_SUCCESS, msg, data, null);
+    }
+    public static AjaxJson getSuccessData(Object data) {
+        return new AjaxJson(CODE_SUCCESS, "ok", data, null);
+    }
+
+
+    /** 返回失败 */
+    public static AjaxJson getError() {
+        return new AjaxJson(CODE_ERROR, "error", null, null);
+    }
+    public static AjaxJson getError(String msg) {
+        return new AjaxJson(CODE_ERROR, msg, null, null);
+    }
+
+    /** 返回警告  */
+    public static AjaxJson getWarning() {
+        return new AjaxJson(CODE_ERROR, "warning", null, null);
+    }
+    public static AjaxJson getWarning(String msg) {
+        return new AjaxJson(CODE_WARNING, msg, null, null);
+    }
+
+    /** 返回未登录  */
+    public static AjaxJson getNotLogin() {
+        return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null);
+    }
+
+    /** 返回没有权限的  */
+    public static AjaxJson getNotJur(String msg) {
+        return new AjaxJson(CODE_NOT_JUR, msg, null, null);
+    }
+
+    /** 返回一个自定义状态码的  */
+    public static AjaxJson get(int code, String msg){
+        return new AjaxJson(code, msg, null, null);
+    }
+
+    /** 返回分页和数据的  */
+    public static AjaxJson getPageData(Long dataCount, Object data){
+        return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount);
+    }
+
+    /** 返回, 根据受影响行数的(大于0=ok,小于0=error)  */
+    public static AjaxJson getByLine(int line){
+        if(line > 0){
+            return getSuccess("ok", line);
+        }
+        return getError("error").setData(line);
+    }
+
+    /** 返回,根据布尔值来确定最终结果的  (true=ok,false=error)  */
+    public static AjaxJson getByBoolean(boolean b){
+        return b ? getSuccess("ok") : getError("error");
+    }
+
+
+
+
+
+
+
+//  // 历史版本遗留代码
+//	public int code; 	// 状态码
+//	public String msg; 	// 描述信息
+//	public Object data; // 携带对象
+//	public Long dataCount;	// 数据总数,用于分页
+
+
+
+
+}

+ 63 - 0
zhsq_qk-framework/src/main/java/zhsq_qk/framework/sso/utli/JsonUtil.java

@@ -0,0 +1,63 @@
+package zhsq_qk.framework.sso.utli;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.util.Map;
+
+/**
+ *  JSON 转换器
+ *
+ * @author kong
+ * @since: 2022-11-24
+ */
+public class JsonUtil {
+
+    /**
+     * 底层 Mapper 对象
+     */
+    public static ObjectMapper objectMapper = new ObjectMapper();
+
+    /**
+     * 将任意对象转换为 json 字符串
+     *
+     * @param obj 对象
+     * @return 转换后的 json 字符串
+     */
+    public static String toJsonString(Object obj) {
+        try {
+            return objectMapper.writeValueAsString(obj);
+        } catch (JsonProcessingException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 将 json 字符串解析为 Map
+     */
+    public static Map<String, Object> parseJsonToMap(String jsonStr) {
+        try {
+            @SuppressWarnings("unchecked")
+            Map<String, Object> map = objectMapper.readValue(jsonStr, Map.class);
+            return map;
+        } catch (JsonProcessingException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 将任意类型转化为指定类型
+     */
+    public static <T>T parseObjectToModel(Object obj, Class<T> cs) {
+        if(obj == null) {
+            return null;
+        }
+        String jsonStr = toJsonString(obj);
+        try {
+            return objectMapper.readValue(jsonStr, cs);
+        } catch (JsonProcessingException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+}

+ 66 - 0
zhsq_qk-framework/src/main/java/zhsq_qk/framework/sso/utli/MyHttpSessionHolder.java

@@ -0,0 +1,66 @@
+package zhsq_qk.framework.sso.utli;
+
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpSessionEvent;
+import javax.servlet.http.HttpSessionListener;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 记录所有已创建的 HttpSession 对象
+ *
+ * <b> 此种方式有性能问题,仅做demo示例,真实项目中请更换为其它方案记录用户会话数据 </b>
+ *
+ * @author kong
+ * @since: 2022-4-30
+ */
+@Component
+public class MyHttpSessionHolder implements HttpSessionListener {
+
+    public static Map<String, HttpSession> sessionMap = new LinkedHashMap<String, HttpSession>();
+
+    public void sessionCreated(HttpSessionEvent httpSessionEvent) {
+        HttpSession session = httpSessionEvent.getSession();
+        sessionMap.put(session.getId(), session);
+    }
+
+    public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
+        HttpSession session = httpSessionEvent.getSession();
+        sessionMap.remove(session.getId());
+    }
+
+    /**
+     * 获取指定请求的 HttpSession (前后台分离模式下必须使用此方法获取 HttpSession )
+     * @param request
+     */
+    public static HttpSession getSession(HttpServletRequest request) {
+        // 先使用默认的尝试一下
+        HttpSession session = request.getSession(false);
+        if(session != null) {
+            return session;
+        }
+
+        // 获取前端提交的 token
+        String token = request.getHeader("JSESSIONID");
+        if(token == null) {
+            token = request.getHeader("satoken");
+            if(token == null) {
+                token = request.getParameter("satoken");
+            }
+        }
+
+        // 获取
+        HttpSession session2 = sessionMap.get(token);
+        if(session2 != null) {
+            return session2;
+        }
+
+        return request.getSession();
+    }
+
+
+}
+

+ 22 - 0
zhsq_qk-framework/src/main/java/zhsq_qk/framework/web/service/SysLoginService.java

@@ -21,6 +21,7 @@ import zhsq_qk.common.utils.ip.IpUtils;
 import zhsq_qk.framework.manager.AsyncManager;
 import zhsq_qk.framework.manager.factory.AsyncFactory;
 import zhsq_qk.framework.security.context.AuthenticationContextHolder;
+import zhsq_qk.system.mapper.SysUserMapper;
 import zhsq_qk.system.service.ISysConfigService;
 import zhsq_qk.system.service.ISysUserService;
 
@@ -48,6 +49,9 @@ public class SysLoginService {
     @Autowired
     private ISysConfigService configService;
 
+    @Autowired
+    private SysUserMapper sysUserMapper;
+
     /**
      * 登录验证
      *
@@ -87,6 +91,24 @@ public class SysLoginService {
         return tokenService.createToken(loginUser);
     }
 
+
+    /**
+     * 登录验证
+     *
+     * @param username 用户名
+     * @return 结果
+     */
+    public String ssoLogin(String username) {
+        SysUser sysUser = sysUserMapper.selectUserByUserName(username);
+        LoginUser loginUser = new LoginUser();
+        loginUser.setUserId(sysUser.getUserId());
+        loginUser.setDeptId(sysUser.getDeptId());
+        loginUser.setUser(sysUser);
+        // 生成token
+        return tokenService.createToken(loginUser);
+    }
+
+
     /**
      * 校验验证码
      *

+ 1 - 1
zhsq_qk-ui/package.json

@@ -37,7 +37,7 @@
   },
   "dependencies": {
     "@riophae/vue-treeselect": "0.4.0",
-    "axios": "0.24.0",
+    "axios": "^1.1.3",
     "cesium": "^1.111.0",
     "clipboard": "2.0.8",
     "core-js": "3.25.3",

+ 39 - 0
zhsq_qk-ui/src/api/sso/method-util.js

@@ -0,0 +1,39 @@
+import axios from 'axios'
+
+// sso-client 的后端服务地址
+export const baseUrl = "http://192.168.1.11:3042";
+// export const baseUrl = process.env.VUE_APP_BASE_API;
+
+
+// 封装一下 Ajax 方法
+export const ajax = function(path, data, successFn) {
+  axios({
+    url: baseUrl + path,
+    method: 'post',
+    data: data,
+    headers: {
+      "Content-Type": "application/x-www-form-urlencoded",
+      "satoken": localStorage.getItem("satoken")
+    }
+  }).
+  then(function (response) { // 成功时执行
+    const res = response.data;
+    successFn(res);
+  }).
+  catch(function (error) {
+    alert("异常:" + JSON.stringify(error));
+    throw error;
+  })
+}
+
+// 从url中查询到指定名称的参数值
+export const getParam = function(name, defaultValue){
+  var query = window.location.search.substring(1);
+  var vars = query.split("&");
+  for (var i=0;i<vars.length;i++) {
+    var pair = vars[i].split("=");
+    if(pair[0] == name){return pair[1];}
+  }
+  return(defaultValue == undefined ? null : defaultValue);
+}
+

+ 10 - 1
zhsq_qk-ui/src/layout/components/Navbar.vue

@@ -57,6 +57,7 @@ import SizeSelect from '@/components/SizeSelect'
 import Search from '@/components/HeaderSearch'
 import RuoYiGit from '@/components/RuoYi/Git'
 import RuoYiDoc from '@/components/RuoYi/Doc'
+import {ajax, getParam} from '@/api/sso/method-util';
 
 export default {
   components: {
@@ -103,7 +104,15 @@ export default {
         type: 'warning'
       }).then(() => {
         this.$store.dispatch('LogOut').then(() => {
-          location.href = '/index';
+          // location.href = '/index';
+          ajax('/sso/logout', {}, function(res) {
+            // 根据返回的状态码执行不同动作
+            if(res.code === 200) {
+              location.href = res.data;
+            } else {
+              alert(res.msg);
+            }
+          });
         })
       }).catch(() => {
       });

+ 3 - 2
zhsq_qk-ui/src/permission.js

@@ -8,7 +8,7 @@ import {isRelogin} from '@/utils/request'
 
 NProgress.configure({showSpinner: false})
 
-const whiteList = ['/login', '/register']
+const whiteList = ['/login', '/register','/sso-login']
 
 router.beforeEach((to, from, next) => {
   NProgress.start()
@@ -47,7 +47,8 @@ router.beforeEach((to, from, next) => {
       // 在免登录白名单,直接进入
       next()
     } else {
-      next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) // 否则全部重定向到登录页
+      // next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) // 否则全部重定向到登录页
+      next(`/sso-login`) // 否则全部重定向到单点登录的登录页,而不是再进入原来的登录页
       NProgress.done()
     }
   }

+ 6 - 0
zhsq_qk-ui/src/router/index.js

@@ -49,6 +49,12 @@ export const constantRoutes = [
   //     icon: ['eye-open'] // 添加图标
   //   }
   // },
+  // SSO-登录用
+  {
+    name: 'sso-login',
+    path: '/sso-login',
+    component: () => import('@/views/sso/sso-login'),
+  },
   {
     path: '/qkmap',
     component: () => import('@/views/supermap/qkmap'),

+ 7 - 0
zhsq_qk-ui/src/utils/auth.js

@@ -2,6 +2,9 @@ import Cookies from 'js-cookie'
 
 const TokenKey = 'Admin-Token'
 
+
+const ExpiresInKey = 'Admin-Expires-In'
+
 export function getToken() {
   return Cookies.get(TokenKey)
 }
@@ -13,3 +16,7 @@ export function setToken(token) {
 export function removeToken() {
   return Cookies.remove(TokenKey)
 }
+
+export function setExpiresIn(time) {
+  return Cookies.set(ExpiresInKey, time)
+}

+ 53 - 0
zhsq_qk-ui/src/views/sso/sso-login.vue

@@ -0,0 +1,53 @@
+<!-- Client端-登录页 -->
+<template>
+  <div></div>
+</template>
+
+<script>
+import {getToken, setToken, setExpiresIn, removeToken} from '@/utils/auth';
+import {ajax, getParam} from '../../api/sso/method-util';
+import router from '@/router';
+
+
+export default {
+  name: 'App',
+  data() {
+    return {
+      back: getParam('back') || router.currentRoute.query.back,
+      ticket: getParam('ticket') || router.currentRoute.query.ticket
+    }
+  },
+  // 页面加载后触发
+  created() {
+    console.log('获取 back 参数:', this.back)
+    console.log('获取 ticket 参数:', this.ticket)
+    if (this.ticket) {
+      this.doLoginByTicket(this.ticket);
+    } else {
+      this.goSsoAuthUrl();
+    }
+  },
+  methods: {
+    // 重定向至认证中心
+    goSsoAuthUrl() {
+      ajax('/sso/getSsoAuthUrl', {clientLoginUrl: location.href}, function (res) {
+        console.log('/sso/getSsoAuthUrl 返回数据', res);
+        location.href = res.data;
+      })
+    },
+    // 根据ticket值登录
+    doLoginByTicket(ticket) {
+      ajax('/sso/doLoginByTicket', {ticket: ticket}, function (res) {
+        if (res.code === 200) {
+          localStorage.setItem('satoken', res.data.session_id);
+          setToken(res.data.access_token);
+          setExpiresIn(res.data.expires_in);
+          location.href = decodeURIComponent(this.back);
+        } else {
+          alert(res.msg);
+        }
+      }.bind(this))
+    }
+  }
+}
+</script>

+ 1 - 1
zhsq_qk-ui/vue.config.js

@@ -9,7 +9,7 @@ const CompressionPlugin = require('compression-webpack-plugin')
 
 const name = process.env.VUE_APP_TITLE || '汽开区城市运行一网统管平台' // 网页标题
 
-const port = process.env.port || process.env.npm_config_port || 11001 // 端口
+const port = process.env.port || process.env.npm_config_port || 18001 // 端口
 
 // vue.config.js 配置说明
 //官方vue.config.js 参考文档 https://cli.vuejs.org/zh/config/#css-loaderoptions