From 1510f110f45879fc745e73795eb17bf8eb813971 Mon Sep 17 00:00:00 2001
From: Github_lizhw <171332584@qq.com>
Date: Sat, 19 Oct 2024 20:20:15 +0800
Subject: [PATCH] sso service
---
ruoyi-modules/ruoyi-sso-server/pom.xml | 92 ++++++++++++
.../java/com/pj/SaSsoServerApplication.java | 21 +++
.../src/main/java/com/pj/h5/H5Controller.java | 44 ++++++
.../main/java/com/pj/home/HomeController.java | 36 +++++
.../src/main/java/com/pj/model/RoleDTO.java | 40 ++++++
.../main/java/com/pj/model/vo/CaptchaVo.java | 25 ++++
.../main/java/com/pj/model/vo/LoginVo.java | 54 ++++++++
.../java/com/pj/model/vo/SysClientVo.java | 74 ++++++++++
.../main/java/com/pj/model/vo/SysRoleVo.java | 80 +++++++++++
.../main/java/com/pj/model/vo/SysUserVo.java | 131 ++++++++++++++++++
.../java/com/pj/service/IAuthStrategy.java | 45 ++++++
.../pj/service/impl/EmailAuthStrategy.java | 121 ++++++++++++++++
.../pj/service/impl/PasswordAuthStrategy.java | 117 ++++++++++++++++
.../com/pj/sso/GlobalExceptionHandler.java | 23 +++
.../java/com/pj/sso/SsoServerController.java | 108 +++++++++++++++
.../src/main/resources/application.yml | 77 ++++++++++
.../src/main/resources/static/favicon.ico | Bin 0 -> 4286 bytes
.../resources/static/sa-res/jquery.min.js | 2 +
.../resources/static/sa-res/layer/layer.js | 2 +
.../static/sa-res/layer/mobile/layer.js | 2 +
.../static/sa-res/layer/mobile/need/layer.css | 1 +
.../sa-res/layer/theme/default/icon-ext.png | Bin 0 -> 5911 bytes
.../sa-res/layer/theme/default/icon.png | Bin 0 -> 11493 bytes
.../sa-res/layer/theme/default/layer.css | 1 +
.../sa-res/layer/theme/default/loading-0.gif | Bin 0 -> 5793 bytes
.../sa-res/layer/theme/default/loading-1.gif | Bin 0 -> 701 bytes
.../sa-res/layer/theme/default/loading-2.gif | Bin 0 -> 1787 bytes
.../main/resources/static/sa-res/login.css | 59 ++++++++
.../src/main/resources/static/sa-res/login.js | 99 +++++++++++++
.../main/resources/templates/sa-login.html | 45 ++++++
30 files changed, 1299 insertions(+)
create mode 100644 ruoyi-modules/ruoyi-sso-server/pom.xml
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/SaSsoServerApplication.java
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/h5/H5Controller.java
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/home/HomeController.java
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/model/RoleDTO.java
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/model/vo/CaptchaVo.java
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/model/vo/LoginVo.java
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/model/vo/SysClientVo.java
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/model/vo/SysRoleVo.java
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/model/vo/SysUserVo.java
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/service/IAuthStrategy.java
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/service/impl/EmailAuthStrategy.java
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/service/impl/PasswordAuthStrategy.java
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/sso/GlobalExceptionHandler.java
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/sso/SsoServerController.java
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/resources/application.yml
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/resources/static/favicon.ico
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/resources/static/sa-res/jquery.min.js
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/resources/static/sa-res/layer/layer.js
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/resources/static/sa-res/layer/mobile/layer.js
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/resources/static/sa-res/layer/mobile/need/layer.css
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/resources/static/sa-res/layer/theme/default/icon-ext.png
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/resources/static/sa-res/layer/theme/default/icon.png
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/resources/static/sa-res/layer/theme/default/layer.css
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/resources/static/sa-res/layer/theme/default/loading-0.gif
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/resources/static/sa-res/layer/theme/default/loading-1.gif
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/resources/static/sa-res/layer/theme/default/loading-2.gif
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/resources/static/sa-res/login.css
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/resources/static/sa-res/login.js
create mode 100644 ruoyi-modules/ruoyi-sso-server/src/main/resources/templates/sa-login.html
diff --git a/ruoyi-modules/ruoyi-sso-server/pom.xml b/ruoyi-modules/ruoyi-sso-server/pom.xml
new file mode 100644
index 000000000..0577cc336
--- /dev/null
+++ b/ruoyi-modules/ruoyi-sso-server/pom.xml
@@ -0,0 +1,92 @@
+
+
+ org.dromara
+ ruoyi-modules
+ ${revision}
+
+ 4.0.0
+ ruoyi-sso-server
+
+ sso-server
+
+
+
+ 1.38.0
+
+
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-discovery
+
+
+ org.springframework.cloud
+ spring-cloud-starter-openfeign
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-loadbalancer
+
+
+
+ io.github.openfeign
+ feign-okhttp
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+
+
+ org.dromara
+ ruoyi-common-core
+
+
+
+ org.dromara
+ ruoyi-common-redis
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-satoken
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-security
+ ${revision}
+
+
+
+
+ org.dromara
+ ruoyi-common-tenant
+ ${revision}
+
+
+
+
+ cn.dev33
+ sa-token-sso
+ 1.38.0
+
+
+
+
+
\ No newline at end of file
diff --git a/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/SaSsoServerApplication.java b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/SaSsoServerApplication.java
new file mode 100644
index 000000000..f8559239d
--- /dev/null
+++ b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/SaSsoServerApplication.java
@@ -0,0 +1,21 @@
+package com.pj;
+
+import cn.dev33.satoken.sso.SaSsoManager;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class SaSsoServerApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SaSsoServerApplication.class, args);
+
+ System.out.println();
+ System.out.println("---------------------- Sa-Token SSO 统一认证中心启动成功 ----------------------");
+ System.out.println("配置信息:" + SaSsoManager.getServerConfig());
+ System.out.println("统一认证登录地址:http://sa-sso-server.com:9000/sso/auth");
+ System.out.println("测试前需要根据官网文档修改hosts文件,测试账号密码:sa / 123456");
+ System.out.println();
+ }
+
+}
\ No newline at end of file
diff --git a/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/h5/H5Controller.java b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/h5/H5Controller.java
new file mode 100644
index 000000000..4c54e8fd0
--- /dev/null
+++ b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/h5/H5Controller.java
@@ -0,0 +1,44 @@
+package com.pj.h5;
+
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import cn.dev33.satoken.sso.util.SaSsoConsts;
+import cn.dev33.satoken.sso.template.SaSsoUtil;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.dev33.satoken.util.SaFoxUtil;
+import cn.dev33.satoken.util.SaResult;
+
+/**
+ * 前后台分离架构下集成SSO所需的代码 (SSO-Server端)
+ *
(注:如果不需要前后端分离架构下集成SSO,可删除此包下所有代码)
+ * @author click33
+ *
+ */
+@RestController
+public class H5Controller {
+
+ /**
+ * 获取 redirectUrl
+ */
+ @RequestMapping("/sso/getRedirectUrl")
+ public SaResult getRedirectUrl(String redirect, String mode, String client) {
+ // 未登录情况下,返回 code=401
+ if(!StpUtil.isLogin()) {
+ return SaResult.code(401);
+ }
+ // 已登录情况下,构建 redirectUrl
+ redirect = SaFoxUtil.decoderUrl(redirect);
+ if(SaSsoConsts.MODE_SIMPLE.equals(mode)) {
+ // 模式一
+ SaSsoUtil.checkRedirectUrl(redirect);
+ return SaResult.data(redirect);
+ } else {
+ // 模式二或模式三
+ String redirectUrl = SaSsoUtil.buildRedirectUrl(StpUtil.getLoginId(), client, redirect);
+ return SaResult.data(redirectUrl);
+ }
+ }
+
+}
diff --git a/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/home/HomeController.java b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/home/HomeController.java
new file mode 100644
index 000000000..1be7c1d23
--- /dev/null
+++ b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/home/HomeController.java
@@ -0,0 +1,36 @@
+package com.pj.home;
+
+import cn.dev33.satoken.context.SaHolder;
+import cn.dev33.satoken.stp.StpUtil;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * SSO 平台中心模式示例,跳连接进入子系统
+ */
+@RestController
+public class HomeController {
+
+ // 平台化首页
+ @RequestMapping("/home")
+ public Object index() {
+ // 如果未登录,则先去登录
+ if(!StpUtil.isLogin()) {
+ return SaHolder.getResponse().redirect("/sso/auth");
+ }
+
+ // 拼接各个子系统的地址,格式形如:/sso/auth?redirect=${子系统首页}/sso/login?back=${子系统首页}
+ String link1 = "/sso/auth?redirect=http://sa-sso-client1.com:9003/sso/login?back=http://sa-sso-client1.com:9003/";
+ String link2 = "/sso/auth?redirect=http://sa-sso-client2.com:9003/sso/login?back=http://sa-sso-client2.com:9003/";
+ String link3 = "/sso/auth?redirect=http://sa-sso-client3.com:9003/sso/login?back=http://sa-sso-client3.com:9003/";
+
+ // 组织网页结构返回到前端
+ String title = "SSO 平台首页
";
+ String client1 = " 进入Client1系统
";
+ String client2 = " 进入Client2系统
";
+ String client3 = " 进入Client3系统
";
+
+ return title + client1 + client2 + client3;
+ }
+
+}
diff --git a/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/model/RoleDTO.java b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/model/RoleDTO.java
new file mode 100644
index 000000000..06ff8a1c2
--- /dev/null
+++ b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/model/RoleDTO.java
@@ -0,0 +1,40 @@
+package com.pj.model;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 角色
+ *
+ * @author Lion Li
+ */
+
+@Data
+@NoArgsConstructor
+public class RoleDTO implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 角色ID
+ */
+ private Long roleId;
+
+ /**
+ * 角色名称
+ */
+ private String roleName;
+
+ /**
+ * 角色权限
+ */
+ private String roleKey;
+
+ /**
+ * 数据范围(1:所有数据权限;2:自定义数据权限;3:本部门数据权限;4:本部门及以下数据权限;5:仅本人数据权限)
+ */
+ private String dataScope;
+
+}
diff --git a/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/model/vo/CaptchaVo.java b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/model/vo/CaptchaVo.java
new file mode 100644
index 000000000..5c37211a7
--- /dev/null
+++ b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/model/vo/CaptchaVo.java
@@ -0,0 +1,25 @@
+package com.pj.model.vo;
+
+import lombok.Data;
+
+/**
+ * 验证码信息
+ *
+ * @author Michelle.Chung
+ */
+@Data
+public class CaptchaVo {
+
+ /**
+ * 是否开启验证码
+ */
+ private Boolean captchaEnabled = true;
+
+ private String uuid;
+
+ /**
+ * 验证码图片
+ */
+ private String img;
+
+}
diff --git a/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/model/vo/LoginVo.java b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/model/vo/LoginVo.java
new file mode 100644
index 000000000..a6eb5179a
--- /dev/null
+++ b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/model/vo/LoginVo.java
@@ -0,0 +1,54 @@
+package com.pj.model.vo;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * 登录验证信息
+ *
+ * @author Michelle.Chung
+ */
+@Data
+public class LoginVo {
+
+ /**
+ * 授权令牌
+ */
+ @JsonProperty("access_token")
+ private String accessToken;
+
+ /**
+ * 刷新令牌
+ */
+ @JsonProperty("refresh_token")
+ private String refreshToken;
+
+ /**
+ * 授权令牌 access_token 的有效期
+ */
+ @JsonProperty("expire_in")
+ private Long expireIn;
+
+ /**
+ * 刷新令牌 refresh_token 的有效期
+ */
+ @JsonProperty("refresh_expire_in")
+ private Long refreshExpireIn;
+
+ /**
+ * 应用id
+ */
+ @JsonProperty("client_id")
+ private String clientId;
+
+ /**
+ * 令牌权限
+ */
+ private String scope;
+
+ /**
+ * 用户 openid
+ */
+ private String openid;
+
+}
diff --git a/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/model/vo/SysClientVo.java b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/model/vo/SysClientVo.java
new file mode 100644
index 000000000..8e9d3e830
--- /dev/null
+++ b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/model/vo/SysClientVo.java
@@ -0,0 +1,74 @@
+package com.pj.model.vo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+
+/**
+ * 授权管理视图对象 sys_client
+ *
+ * @author Michelle.Chung
+ * @date 2023-05-15
+ */
+@Data
+public class SysClientVo implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * id
+ */
+ private Long id;
+
+ /**
+ * 客户端id
+ */
+ private String clientId;
+
+ /**
+ * 客户端key
+ */
+ private String clientKey;
+
+ /**
+ * 客户端秘钥
+ */
+ private String clientSecret;
+
+ /**
+ * 授权类型
+ */
+ private List grantTypeList;
+
+ /**
+ * 授权类型
+ */
+ private String grantType;
+
+ /**
+ * 设备类型
+ */
+ private String deviceType;
+
+ /**
+ * token活跃超时时间
+ */
+ private Long activeTimeout;
+
+ /**
+ * token固定超时时间
+ */
+ private Long timeout;
+
+ /**
+ * 状态(0正常 1停用)
+ */
+ private String status;
+
+
+}
diff --git a/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/model/vo/SysRoleVo.java b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/model/vo/SysRoleVo.java
new file mode 100644
index 000000000..4d7058a44
--- /dev/null
+++ b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/model/vo/SysRoleVo.java
@@ -0,0 +1,80 @@
+package com.pj.model.vo;
+
+import lombok.Data;
+import org.dromara.common.core.constant.UserConstants;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 角色信息视图对象 sys_role
+ *
+ * @author Michelle.Chung
+ */
+@Data
+public class SysRoleVo implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 角色ID
+ */
+ private Long roleId;
+
+ /**
+ * 角色名称
+ */
+ private String roleName;
+
+ /**
+ * 角色权限字符串
+ */
+ private String roleKey;
+
+ /**
+ * 显示顺序
+ */
+ private Integer roleSort;
+
+ /**
+ * 数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)
+ */
+ private String dataScope;
+
+ /**
+ * 菜单树选择项是否关联显示
+ */
+ private Boolean menuCheckStrictly;
+
+ /**
+ * 部门树选择项是否关联显示
+ */
+ private Boolean deptCheckStrictly;
+
+ /**
+ * 角色状态(0正常 1停用)
+ */
+ private String status;
+
+ /**
+ * 备注
+ */
+ private String remark;
+
+ /**
+ * 创建时间
+ */
+ private Date createTime;
+
+ /**
+ * 用户是否存在此角色标识 默认不存在
+ */
+ private boolean flag = false;
+
+ public boolean isSuperAdmin() {
+ return UserConstants.SUPER_ADMIN_ID.equals(this.roleId);
+ }
+
+}
diff --git a/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/model/vo/SysUserVo.java b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/model/vo/SysUserVo.java
new file mode 100644
index 000000000..f07884bfd
--- /dev/null
+++ b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/model/vo/SysUserVo.java
@@ -0,0 +1,131 @@
+package com.pj.model.vo;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+
+/**
+ * 用户信息视图对象 sys_user
+ *
+ * @author Michelle.Chung
+ */
+@Data
+public class SysUserVo implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 用户ID
+ */
+ private Long userId;
+
+ /**
+ * 租户ID
+ */
+ private String tenantId;
+
+ /**
+ * 部门ID
+ */
+ private Long deptId;
+
+ /**
+ * 用户账号
+ */
+ private String userName;
+
+ /**
+ * 用户昵称
+ */
+ private String nickName;
+
+ /**
+ * 用户类型(sys_user系统用户)
+ */
+ private String userType;
+
+ /**
+ * 用户邮箱
+ */
+ private String email;
+
+ /**
+ * 手机号码
+ */
+ private String phonenumber;
+
+ /**
+ * 用户性别(0男 1女 2未知)
+ */
+ private String sex;
+
+ /**
+ * 头像地址
+ */
+ private Long avatar;
+
+ /**
+ * 密码
+ */
+ @JsonIgnore
+ @JsonProperty
+ private String password;
+
+ /**
+ * 帐号状态(0正常 1停用)
+ */
+ private String status;
+
+ /**
+ * 最后登录IP
+ */
+ private String loginIp;
+
+ /**
+ * 最后登录时间
+ */
+ private Date loginDate;
+
+ /**
+ * 备注
+ */
+ private String remark;
+
+ /**
+ * 创建时间
+ */
+ private Date createTime;
+
+ /**
+ * 部门名
+ */
+ private String deptName;
+
+ /**
+ * 角色对象
+ */
+ private List roles;
+
+ /**
+ * 角色组
+ */
+ private Long[] roleIds;
+
+ /**
+ * 岗位组
+ */
+ private Long[] postIds;
+
+ /**
+ * 数据权限 当前角色ID
+ */
+ private Long roleId;
+
+}
diff --git a/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/service/IAuthStrategy.java b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/service/IAuthStrategy.java
new file mode 100644
index 000000000..39d762a90
--- /dev/null
+++ b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/service/IAuthStrategy.java
@@ -0,0 +1,45 @@
+package com.pj.service;
+
+
+import com.pj.model.vo.LoginVo;
+import com.pj.model.vo.SysClientVo;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.SpringUtils;
+
+/**
+ * 授权策略
+ *
+ * @author Michelle.Chung
+ */
+public interface IAuthStrategy {
+
+ String BASE_NAME = "AuthStrategy";
+
+ /**
+ * 登录
+ *
+ * @param body 登录对象
+ * @param client 授权管理视图对象
+ * @param grantType 授权类型
+ * @return 登录验证信息
+ */
+ static LoginVo login(String body, SysClientVo client, String grantType) {
+ // 授权类型和客户端id
+ String beanName = grantType + IAuthStrategy.BASE_NAME;
+ if (!SpringUtils.containsBean(beanName)) {
+ throw new ServiceException("授权类型不正确!");
+ }
+ IAuthStrategy instance = SpringUtils.getBean(beanName);
+ return instance.login(body, client);
+ }
+
+ /**
+ * 登录
+ *
+ * @param body 登录对象
+ * @param client 授权管理视图对象
+ * @return 登录验证信息
+ */
+ LoginVo login(String body, SysClientVo client);
+
+}
diff --git a/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/service/impl/EmailAuthStrategy.java b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/service/impl/EmailAuthStrategy.java
new file mode 100644
index 000000000..ed572ac83
--- /dev/null
+++ b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/service/impl/EmailAuthStrategy.java
@@ -0,0 +1,121 @@
+package com.pj.service.impl;
+
+import cn.dev33.satoken.stp.SaLoginModel;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.lang.Opt;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONUtil;
+import com.pj.model.vo.LoginVo;
+import com.pj.model.vo.SysClientVo;
+import com.pj.model.vo.SysRoleVo;
+import com.pj.model.vo.SysUserVo;
+import com.pj.service.IAuthStrategy;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.core.domain.dto.RoleDTO;
+import org.dromara.common.core.domain.model.EmailLoginBody;
+import org.dromara.common.core.domain.model.LoginUser;
+import org.dromara.common.core.enums.LoginType;
+import org.dromara.common.core.enums.UserStatus;
+import org.dromara.common.core.exception.user.CaptchaExpireException;
+import org.dromara.common.core.exception.user.UserException;
+import org.dromara.common.core.utils.MessageUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.ValidatorUtils;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 邮件认证策略
+ *
+ * @author Michelle.Chung
+ */
+@Slf4j
+@Service("email" + IAuthStrategy.BASE_NAME)
+@RequiredArgsConstructor
+public class EmailAuthStrategy implements IAuthStrategy {
+
+ //后续改为远程的
+// private final SysLoginService loginService;
+// private final SysUserMapper userMapper;
+
+ @Override
+ public LoginVo login(String body, SysClientVo client) {
+ EmailLoginBody loginBody = JSONUtil.toBean(body, EmailLoginBody.class);
+ ValidatorUtils.validate(loginBody);
+ String tenantId = loginBody.getTenantId();
+ String email = loginBody.getEmail();
+ String emailCode = loginBody.getEmailCode();
+ LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
+// SysUserVo user = loadUserByEmail(email);
+// loginService.checkLogin(LoginType.EMAIL, tenantId, user.getUserName(), () -> !validateEmailCode(tenantId, email, emailCode));
+ // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
+// return loginService.buildLoginUser(user);
+ return buildLoginUser();
+ });
+ loginUser.setClientKey(client.getClientKey());
+ loginUser.setDeviceType(client.getDeviceType());
+ SaLoginModel model = new SaLoginModel();
+ model.setDevice(client.getDeviceType());
+ // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
+ // 例如: 后台用户30分钟过期 app用户1天过期
+ model.setTimeout(client.getTimeout());
+ model.setActiveTimeout(client.getActiveTimeout());
+ model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
+ // 生成token
+ LoginHelper.login(loginUser, model);
+
+ LoginVo loginVo = new LoginVo();
+ loginVo.setAccessToken(StpUtil.getTokenValue());
+ loginVo.setExpireIn(StpUtil.getTokenTimeout());
+ loginVo.setClientId(client.getClientId());
+ return loginVo;
+ }
+
+
+ /**
+ * 构建登录用户
+ */
+ private LoginUser buildLoginUser() {
+ LoginUser loginUser = new LoginUser();
+ loginUser.setTenantId("1");
+ loginUser.setUserId(1L);
+ loginUser.setDeptId(2L);
+ loginUser.setUsername("测试用户登录名");
+ loginUser.setNickname("测试用户名称");
+ loginUser.setUserType("用户类型1");
+ return loginUser;
+ }
+
+// /**
+// * 校验邮箱验证码
+// */
+// private boolean validateEmailCode(String tenantId, String email, String emailCode) {
+// String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + email);
+// if (StringUtils.isBlank(code)) {
+// loginService.recordLogininfor(tenantId, email, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
+// throw new CaptchaExpireException();
+// }
+// return code.equals(emailCode);
+// }
+//
+// private SysUserVo loadUserByEmail(String email) {
+// SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper().eq(SysUser::getEmail, email));
+// if (ObjectUtil.isNull(user)) {
+// log.info("登录用户:{} 不存在.", email);
+// throw new UserException("user.not.exists", email);
+// } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+// log.info("登录用户:{} 已被停用.", email);
+// throw new UserException("user.blocked", email);
+// }
+// return user;
+// }
+
+}
diff --git a/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/service/impl/PasswordAuthStrategy.java b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/service/impl/PasswordAuthStrategy.java
new file mode 100644
index 000000000..6b5280426
--- /dev/null
+++ b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/service/impl/PasswordAuthStrategy.java
@@ -0,0 +1,117 @@
+package com.pj.service.impl;
+
+import cn.dev33.satoken.stp.SaLoginModel;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.json.JSONUtil;
+import com.pj.model.vo.LoginVo;
+import com.pj.model.vo.SysClientVo;
+import com.pj.service.IAuthStrategy;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.domain.model.LoginUser;
+import org.dromara.common.core.domain.model.PasswordLoginBody;
+import org.dromara.common.core.utils.ValidatorUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.springframework.stereotype.Service;
+
+/**
+ * 密码认证策略
+ *
+ * @author Michelle.Chung
+ */
+@Slf4j
+@Service("password" + IAuthStrategy.BASE_NAME)
+@RequiredArgsConstructor
+public class PasswordAuthStrategy implements IAuthStrategy {
+
+// private final CaptchaProperties captchaProperties;
+// private final SysLoginService loginService;
+// private final SysUserMapper userMapper;
+
+ @Override
+ public LoginVo login(String body, SysClientVo client) {
+ PasswordLoginBody loginBody = JSONUtil.toBean(body, PasswordLoginBody.class);
+ ValidatorUtils.validate(loginBody);
+ String tenantId = loginBody.getTenantId();
+ String username = loginBody.getUsername();
+ String password = loginBody.getPassword();
+ String code = loginBody.getCode();
+ String uuid = loginBody.getUuid();
+
+// boolean captchaEnabled = captchaProperties.getEnable();
+// // 验证码开关
+// if (captchaEnabled) {
+// validateCaptcha(tenantId, username, code, uuid);
+// }
+ LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
+// SysUserVo user = loadUserByUsername(username);
+// loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword()));
+// // 此处可根据登录用户的数据不同 自行创建 loginUser
+// return loginService.buildLoginUser(user);
+ return buildLoginUser();
+ });
+ loginUser.setClientKey(client.getClientKey());
+ loginUser.setDeviceType(client.getDeviceType());
+ SaLoginModel model = new SaLoginModel();
+ model.setDevice(client.getDeviceType());
+ // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
+ // 例如: 后台用户30分钟过期 app用户1天过期
+ model.setTimeout(client.getTimeout());
+ model.setActiveTimeout(client.getActiveTimeout());
+ model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
+ // 生成token
+ LoginHelper.login(loginUser, model);
+
+ LoginVo loginVo = new LoginVo();
+ loginVo.setAccessToken(StpUtil.getTokenValue());
+ loginVo.setExpireIn(StpUtil.getTokenTimeout());
+ loginVo.setClientId(client.getClientId());
+ return loginVo;
+ }
+
+ private LoginUser buildLoginUser() {
+ LoginUser loginUser = new LoginUser();
+ loginUser.setTenantId("1");
+ loginUser.setUserId(1L);
+ loginUser.setDeptId(2L);
+ loginUser.setUsername("测试用户登录名");
+ loginUser.setNickname("测试用户名称");
+ loginUser.setUserType("用户类型1");
+ return loginUser;
+ }
+
+ /**
+ * 校验验证码
+ *
+ * @param username 用户名
+ * @param code 验证码
+ * @param uuid 唯一标识
+ */
+// private void validateCaptcha(String tenantId, String username, String code, String uuid) {
+// String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.blankToDefault(uuid, "");
+// String captcha = RedisUtils.getCacheObject(verifyKey);
+// RedisUtils.deleteObject(verifyKey);
+// if (captcha == null) {
+// loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
+// throw new CaptchaExpireException();
+// }
+// if (!code.equalsIgnoreCase(captcha)) {
+// loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
+// throw new CaptchaException();
+// }
+// }
+//
+// private SysUserVo loadUserByUsername(String username) {
+// SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper().eq(SysUser::getUserName, username));
+// if (ObjectUtil.isNull(user)) {
+// log.info("登录用户:{} 不存在.", username);
+// throw new UserException("user.not.exists", username);
+// } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+// log.info("登录用户:{} 已被停用.", username);
+// throw new UserException("user.blocked", username);
+// }
+// return user;
+// }
+
+}
diff --git a/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/sso/GlobalExceptionHandler.java b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/sso/GlobalExceptionHandler.java
new file mode 100644
index 000000000..12e3ae848
--- /dev/null
+++ b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/sso/GlobalExceptionHandler.java
@@ -0,0 +1,23 @@
+package com.pj.sso;
+
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import cn.dev33.satoken.util.SaResult;
+
+/**
+ * 全局异常处理
+ * @author click33
+ *
+ */
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ // 全局异常拦截
+ @ExceptionHandler
+ public SaResult handlerException(Exception e) {
+ e.printStackTrace();
+ return SaResult.error(e.getMessage());
+ }
+
+}
diff --git a/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/sso/SsoServerController.java b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/sso/SsoServerController.java
new file mode 100644
index 000000000..9c4c1773d
--- /dev/null
+++ b/ruoyi-modules/ruoyi-sso-server/src/main/java/com/pj/sso/SsoServerController.java
@@ -0,0 +1,108 @@
+package com.pj.sso;
+
+import cn.dev33.satoken.context.SaHolder;
+import cn.dev33.satoken.context.model.SaRequest;
+import cn.dev33.satoken.sign.SaSignUtil;
+import cn.dev33.satoken.sso.config.SaSsoServerConfig;
+import cn.dev33.satoken.sso.processor.SaSsoServerProcessor;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.dev33.satoken.util.SaResult;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.pj.model.vo.LoginVo;
+import com.pj.model.vo.SysClientVo;
+import com.pj.service.IAuthStrategy;
+import org.dromara.common.core.domain.model.PasswordLoginBody;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.ModelAndView;
+
+/**
+ * Sa-Token-SSO Server端 Controller
+ * @author click33
+ *
+ */
+@RestController
+public class SsoServerController {
+
+ /**
+ * SSO-Server端:处理所有SSO相关请求
+ * http://{host}:{port}/sso/auth -- 单点登录授权地址,接受参数:redirect=授权重定向地址
+ * http://{host}:{port}/sso/doLogin -- 账号密码登录接口,接受参数:name、pwd
+ * http://{host}:{port}/sso/checkTicket -- Ticket校验接口(isHttp=true时打开),接受参数:ticket=ticket码、ssoLogoutCall=单点注销回调地址 [可选]
+ * http://{host}:{port}/sso/signout -- 单点注销地址(isSlo=true时打开),接受参数:loginId=账号id、sign=参数签名
+ */
+ @RequestMapping("/sso/*")
+ public Object ssoRequest() {
+ // 如果登录时没有提供redirect参数,则进入平台中心首页 /home,而不是重定向到 client 端
+ SaRequest req = SaHolder.getRequest();
+ if(req.isPath("/sso/auth") && !req.hasParam("redirect") && StpUtil.isLogin()) {
+ return SaHolder.getResponse().redirect("/home");
+ }
+
+ if(req.isPath("/sso/isLogin")) {
+ return SaResult.data(StpUtil.isLogin());
+ }
+
+ return SaSsoServerProcessor.instance.dister();
+ }
+
+ // 配置SSO相关参数
+ @Autowired
+ private void configSso(SaSsoServerConfig ssoServer) {
+
+ // 配置:未登录时返回的View
+ ssoServer.notLoginView = () -> {
+ return new ModelAndView("sa-login.html");
+ };
+
+ // 配置:登录处理函数
+ ssoServer.doLoginHandle = (name, pwd) -> {
+ // 此处仅做模拟登录,真实环境应该查询数据进行登录
+ if("sa".equals(name) && "123456".equals(pwd)) {
+ StpUtil.login(10001);
+ PasswordLoginBody body = new PasswordLoginBody();
+ body.setUsername("sa");
+ body.setPassword("123456");
+ body.setClientId("ClientId");
+ body.setGrantType("password");
+ body.setTenantId("1");
+ body.setCode("code1");
+ body.setUuid("qweqw");
+
+
+
+ SysClientVo client = new SysClientVo();
+ client.setId(1L);
+ client.setTimeout(10000L);
+ client.setActiveTimeout(1000000L);
+
+ LoginVo loginVo = IAuthStrategy.login(JSONUtil.toJsonStr(body), client, body.getGrantType());
+
+ return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue());
+ }
+ return SaResult.error("登录失败!");
+ };
+
+ }
+
+ // 示例:获取数据接口(用于在模式三下,为 client 端开放拉取数据的接口)
+ @RequestMapping("/sso/getData")
+ public SaResult getData(String apiType, String loginId) {
+ System.out.println("---------------- 获取数据 ----------------");
+ System.out.println("apiType=" + apiType);
+ System.out.println("loginId=" + loginId);
+
+ // 校验签名:只有拥有正确秘钥发起的请求才能通过校验
+ SaSignUtil.checkRequest(SaHolder.getRequest());
+
+ // 自定义返回结果(模拟)
+ return SaResult.ok()
+ .set("id", loginId)
+ .set("name", "LinXiaoYu")
+ .set("sex", "女")
+ .set("age", 18);
+ }
+
+}
diff --git a/ruoyi-modules/ruoyi-sso-server/src/main/resources/application.yml b/ruoyi-modules/ruoyi-sso-server/src/main/resources/application.yml
new file mode 100644
index 000000000..6dd14e176
--- /dev/null
+++ b/ruoyi-modules/ruoyi-sso-server/src/main/resources/application.yml
@@ -0,0 +1,77 @@
+# 端口
+server:
+ port: 9002
+
+spring:
+ cloud:
+ nacos:
+ discovery:
+ server-addr: http://127.0.0.1:8848
+ namespace: c5c00044-93e6-4e66-8060-3f39aa7ab82b
+ group: ${spring.profiles.active}
+ openfeign:
+ okhttp:
+ enabled: true
+ httpclient:
+ connection-timeout: 5000
+ # Redis配置 (SSO模式一和模式二使用Redis来同步会话)
+ data:
+ redis:
+ # Redis数据库索引(默认为0)
+ database: 0
+ # Redis服务器地址
+ host: 127.0.0.1
+ # Redis服务器连接端口
+ port: 6379
+ # Redis服务器连接密码(默认为空)
+ password: Lzw8800580/
+ # 连接超时时间
+ timeout: 10s
+ lettuce:
+ pool:
+ # 连接池最大连接数
+ max-active: 200
+ # 连接池最大阻塞等待时间(使用负值表示没有限制)
+ max-wait: -1ms
+ # 连接池中的最大空闲连接
+ max-idle: 10
+ # 连接池中的最小空闲连接
+ min-idle: 0
+
+# Sa-Token配置
+sa-token:
+ # token名称 (同时也是cookie名称)
+ token-name: Authorization
+ # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
+ is-concurrent: true
+ # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
+ is-share: false
+ # jwt秘钥
+ jwt-secret-key: abcdefghijklmnopqrstuvwxyz
+
+ # ------- SSO-模式二相关配置
+ sso-server:
+ # Ticket有效期 (单位: 秒),默认五分钟
+ ticket-timeout: 300
+ # 所有允许的授权回调地址
+ allow-url: "*"
+
+# ruoyi-security配置
+security:
+ # 排除路径
+ excludes:
+ # 静态资源
+ - /*.html
+ - /**/*.html
+ - /**/*.css
+ - /**/*.js
+ # 公共路径
+ - /favicon.ico
+ - /error
+ - /sso/auth
+ - /sso/doLogin
+ # swagger 文档配置
+ - /*/api-docs
+ - /*/api-docs/**
+
+
\ No newline at end of file
diff --git a/ruoyi-modules/ruoyi-sso-server/src/main/resources/static/favicon.ico b/ruoyi-modules/ruoyi-sso-server/src/main/resources/static/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..f145375eb5669c953b91907f9fb369b872f38d64
GIT binary patch
literal 4286
zcmc(i+lx(69LLuomnUZ^70od!W){rSPa#JUr761H>~q$!_JtY5bWKbiV*
z1N`=+`#1QmhrwV?<+Y%;q&UC+7QSPl30_3TJ;rY-B-s)g9)YhFx_^n^Oqc_&BVv@d
z2(@e>4)-CX>DEbeF2v|R#cwghS^>ujxO#p$R{s-uS|P2iBH$zVy7pfB^PtxA@5g5V
zq&3Ct1FPp>wN`8C-^OPGWVP`G-UMG;_c8k0vkE(LTO#yd1$;Dj3!FSzi(2FFz#jwm
z`9u2JZ{xCxI&ottKY+CQ_lR-NXeVW=qTV%kA+5f~WO^#4u=6ykb=LX%?*0pnzx+3Z
zQ+C(teK(@}E?ihg^)JYxuQ^cvLpF)ZQ*Z`U4%rkcllEWi`Bx^#e9${C2g6}CTnGD1
z*C?+9jfZ>&K{H$i>*HUo{w#b#^W*w!e$K&h=zwqFwyXRVuD~%k3rTZo$tYh2%6eQ9};9fY3thT0B+eCuJ_8yi92PX2Sh?P`-x
zJx%L)8C(E;*S&{}upF|ial$;ym5do!X17)t$q&X%+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp($),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+$),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ne=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(m.childNodes),m.childNodes),t[m.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&((e?e.ownerDocument||e:m)!==C&&T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!A[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&U.test(t)){(s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=k),o=(l=h(t)).length;while(o--)l[o]="#"+s+" "+xe(l[o]);c=l.join(","),f=ee.test(t)&&ye(e.parentNode)||e}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){A(t,!0)}finally{s===k&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[k]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:m;return r!==C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),m!==C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=k,!C.getElementsByName||!C.getElementsByName(k).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+k+"-]").length||v.push("~="),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+k+"+*").length||v.push(".#.+[+~]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",$)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument===m&&y(m,e)?-1:t===C||t.ownerDocument===m&&y(m,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===C?-1:t===C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]===m?-1:s[r]===m?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if((e.ownerDocument||e)!==C&&T(e),d.matchesSelector&&E&&!A[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){A(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=p[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&p(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?k.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?k.grep(e,function(e){return e===n!==r}):"string"!=typeof n?k.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(k.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:L.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof k?t[0]:t,k.merge(this,k.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),D.test(r[1])&&k.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(k):k.makeArray(e,this)}).prototype=k.fn,q=k(E);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}k.fn.extend({has:function(e){var t=k(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?k.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;nx",y.noCloneChecked=!!me.cloneNode(!0).lastChild.defaultValue;var Te=/^key/,Ce=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ee=/^([^.]*)(?:\.(.+)|)/;function ke(){return!0}function Se(){return!1}function Ne(e,t){return e===function(){try{return E.activeElement}catch(e){}}()==("focus"===t)}function Ae(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Ae(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Se;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return k().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=k.guid++)),e.each(function(){k.event.add(this,t,i,r,n)})}function De(e,i,o){o?(Q.set(e,i,!1),k.event.add(e,i,{namespace:!1,handler:function(e){var t,n,r=Q.get(this,i);if(1&e.isTrigger&&this[i]){if(r.length)(k.event.special[i]||{}).delegateType&&e.stopPropagation();else if(r=s.call(arguments),Q.set(this,i,r),t=o(this,i),this[i](),r!==(n=Q.get(this,i))||t?Q.set(this,i,!1):n={},r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n.value}else r.length&&(Q.set(this,i,{value:k.event.trigger(k.extend(r[0],k.Event.prototype),r.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Q.get(e,i)&&k.event.add(e,i,ke)}k.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.get(t);if(v){n.handler&&(n=(o=n).handler,i=o.selector),i&&k.find.matchesSelector(ie,i),n.guid||(n.guid=k.guid++),(u=v.events)||(u=v.events={}),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof k&&k.event.triggered!==e.type?k.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(R)||[""]).length;while(l--)d=g=(s=Ee.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=k.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=k.event.special[d]||{},c=k.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&k.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),k.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.hasData(e)&&Q.get(e);if(v&&(u=v.events)){l=(t=(t||"").match(R)||[""]).length;while(l--)if(d=g=(s=Ee.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d){f=k.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||k.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)k.event.remove(e,d+t[l],n,r,!0);k.isEmptyObject(u)&&Q.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=k.event.fix(e),u=new Array(arguments.length),l=(Q.get(this,"events")||{})[s.type]||[],c=k.event.special[s.type]||{};for(u[0]=s,t=1;t\x20\t\r\n\f]*)[^>]*)\/>/gi,qe=/
+
+
+
+