sso service

This commit is contained in:
Github_lizhw 2024-10-19 20:20:15 +08:00
parent 3443f5fc4e
commit 1510f110f4
30 changed files with 1299 additions and 0 deletions

View File

@ -0,0 +1,92 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-modules</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-sso-server</artifactId>
<description> sso-server </description>
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.38.0</sa-token.version>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 没有loadbalancer调用会报错 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
<!-- SpringBoot Web依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 视图引擎(在前后端不分离模式下提供视图支持) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<!-- 缓存服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-redis</artifactId>
<version>${revision}</version>
</dependency>
<!-- satoken -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-satoken</artifactId>
<version>${revision}</version>
</dependency>
<!-- 安全模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-security</artifactId>
<version>${revision}</version>
</dependency>
<!-- 租户模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-tenant</artifactId>
<version>${revision}</version>
</dependency>
<!-- Sa-Token 插件整合SSO -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-sso</artifactId>
<version>1.38.0</version>
</dependency>
</dependencies>
</project>

View File

@ -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();
}
}

View File

@ -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端
* <p>如果不需要前后端分离架构下集成SSO可删除此包下所有代码</p>
* @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);
}
}
}

View File

@ -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 = "<h2>SSO 平台首页</h2>";
String client1 = "<p><a href='" + link1 + "' target='_blank'> 进入Client1系统 </a></p>";
String client2 = "<p><a href='" + link2 + "' target='_blank'> 进入Client2系统 </a></p>";
String client3 = "<p><a href='" + link3 + "' target='_blank'> 进入Client3系统 </a></p>";
return title + client1 + client2 + client3;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<String> grantTypeList;
/**
* 授权类型
*/
private String grantType;
/**
* 设备类型
*/
private String deviceType;
/**
* token活跃超时时间
*/
private Long activeTimeout;
/**
* token固定超时时间
*/
private Long timeout;
/**
* 状态0正常 1停用
*/
private String status;
}

View File

@ -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);
}
}

View File

@ -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<SysRoleVo> roles;
/**
* 角色组
*/
private Long[] roleIds;
/**
* 岗位组
*/
private Long[] postIds;
/**
* 数据权限 当前角色ID
*/
private Long roleId;
}

View File

@ -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);
}

View File

@ -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<SysUser>().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;
// }
}

View File

@ -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<SysUser>().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;
// }
}

View File

@ -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());
}
}

View File

@ -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 -- 账号密码登录接口接受参数namepwd
* http://{host}:{port}/sso/checkTicket -- Ticket校验接口isHttp=true时打开接受参数ticket=ticket码ssoLogoutCall=单点注销回调地址 [可选]
* http://{host}:{port}/sso/signout -- 单点注销地址isSlo=true时打开接受参数loginId=账号idsign=参数签名
*/
@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);
}
}

View File

@ -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/**

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,2 @@
/*! layer mobile-v2.0.0 Web弹层组件 MIT License http://layer.layui.com/mobile By 贤心 */
;!function(e){"use strict";var t=document,n="querySelectorAll",i="getElementsByClassName",a=function(e){return t[n](e)},s={type:0,shade:!0,shadeClose:!0,fixed:!0,anim:"scale"},l={extend:function(e){var t=JSON.parse(JSON.stringify(s));for(var n in e)t[n]=e[n];return t},timer:{},end:{}};l.touch=function(e,t){e.addEventListener("click",function(e){t.call(this,e)},!1)};var r=0,o=["layui-m-layer"],c=function(e){var t=this;t.config=l.extend(e),t.view()};c.prototype.view=function(){var e=this,n=e.config,s=t.createElement("div");e.id=s.id=o[0]+r,s.setAttribute("class",o[0]+" "+o[0]+(n.type||0)),s.setAttribute("index",r);var l=function(){var e="object"==typeof n.title;return n.title?'<h3 style="'+(e?n.title[1]:"")+'">'+(e?n.title[0]:n.title)+"</h3>":""}(),c=function(){"string"==typeof n.btn&&(n.btn=[n.btn]);var e,t=(n.btn||[]).length;return 0!==t&&n.btn?(e='<span yes type="1">'+n.btn[0]+"</span>",2===t&&(e='<span no type="0">'+n.btn[1]+"</span>"+e),'<div class="layui-m-layerbtn">'+e+"</div>"):""}();if(n.fixed||(n.top=n.hasOwnProperty("top")?n.top:100,n.style=n.style||"",n.style+=" top:"+(t.body.scrollTop+n.top)+"px"),2===n.type&&(n.content='<i></i><i class="layui-m-layerload"></i><i></i><p>'+(n.content||"")+"</p>"),n.skin&&(n.anim="up"),"msg"===n.skin&&(n.shade=!1),s.innerHTML=(n.shade?"<div "+("string"==typeof n.shade?'style="'+n.shade+'"':"")+' class="layui-m-layershade"></div>':"")+'<div class="layui-m-layermain" '+(n.fixed?"":'style="position:static;"')+'><div class="layui-m-layersection"><div class="layui-m-layerchild '+(n.skin?"layui-m-layer-"+n.skin+" ":"")+(n.className?n.className:"")+" "+(n.anim?"layui-m-anim-"+n.anim:"")+'" '+(n.style?'style="'+n.style+'"':"")+">"+l+'<div class="layui-m-layercont">'+n.content+"</div>"+c+"</div></div></div>",!n.type||2===n.type){var d=t[i](o[0]+n.type),y=d.length;y>=1&&layer.close(d[0].getAttribute("index"))}document.body.appendChild(s);var u=e.elem=a("#"+e.id)[0];n.success&&n.success(u),e.index=r++,e.action(n,u)},c.prototype.action=function(e,t){var n=this;e.time&&(l.timer[n.index]=setTimeout(function(){layer.close(n.index)},1e3*e.time));var a=function(){var t=this.getAttribute("type");0==t?(e.no&&e.no(),layer.close(n.index)):e.yes?e.yes(n.index):layer.close(n.index)};if(e.btn)for(var s=t[i]("layui-m-layerbtn")[0].children,r=s.length,o=0;o<r;o++)l.touch(s[o],a);if(e.shade&&e.shadeClose){var c=t[i]("layui-m-layershade")[0];l.touch(c,function(){layer.close(n.index,e.end)})}e.end&&(l.end[n.index]=e.end)},e.layer={v:"2.0",index:r,open:function(e){var t=new c(e||{});return t.index},close:function(e){var n=a("#"+o[0]+e)[0];n&&(n.innerHTML="",t.body.removeChild(n),clearTimeout(l.timer[e]),delete l.timer[e],"function"==typeof l.end[e]&&l.end[e](),delete l.end[e])},closeAll:function(){for(var e=t[i](o[0]),n=0,a=e.length;n<a;n++)layer.close(0|e[0].getAttribute("index"))}},"function"==typeof define?define(function(){return layer}):function(){var e=document.scripts,n=e[e.length-1],i=n.src,a=i.substring(0,i.lastIndexOf("/")+1);n.getAttribute("merge")||document.head.appendChild(function(){var e=t.createElement("link");return e.href=a+"need/layer.css?2.0",e.type="text/css",e.rel="styleSheet",e.id="layermcss",e}())}()}(window);

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,59 @@
*{margin: 0; padding: 0;}
body{font-family: Helvetica Neue,Helvetica,PingFang SC,Tahoma,Arial,sans-serif;}
::-webkit-input-placeholder{color: #ccc;}
/* 视图盒子 */
.view-box{position: relative; width: 100vw; height: 100vh; overflow: hidden;}
/* 背景 EAEFF3 */
.bg-1{height: 50%; background: linear-gradient(to bottom right, #0466c5, #3496F5);}
.bg-2{height: 50%; background-color: #EAEFF3;}
/* 渐变背景 */
/*.bg-1{
background-size: 500%;
background-image: linear-gradient(125deg,#0466c5,#3496F5,#0466c5,#3496F5,#0466c5,#2496F5);
animation: bganimation 30s infinite;
}
@keyframes bganimation{
0%{background-position: 0% 50%;}
50%{background-position: 100% 50%;}
100%{background-position: 0% 50%;}
} */
/* 背景 */
.bg-1{background: #101C34;}
.bg-2{background: #101C34;}
/* .bg-1{height: 100%; background-image: url(./login-bg.png); background-size: 100% 100%;} */
/* 内容盒子 */
.content-box{position: absolute; width: 100vw; height: 100vh; top: 0px;}
/* 登录盒子 */
/* .login-box{width: 400px; height: 400px; position: absolute; left: calc(50% - 200px); top: calc(50% - 200px); max-width: 90%; } */
.login-box{width: 400px; margin: auto; max-width: 90%; height: 100%;}
.login-box{display: flex; align-items: center; text-align: center;}
/* 表单 */
.from-box{flex: 1; padding: 20px 50px; background-color: #FFF;}
.from-box{border-radius: 1px; box-shadow: 1px 1px 20px #666;}
.from-title{margin-top: 20px; margin-bottom: 30px; text-align: center;}
/* 输入框 */
.from-item{border: 0px #000 solid; margin-bottom: 15px;}
.s-input{width: 100%; line-height: 32px; height: 32px; text-indent: 1em; outline: 0; border: 1px #ccc solid; border-radius: 3px; transition: all 0.2s;}
.s-input{font-size: 12px;}
.s-input:focus{border-color: #409eff}
/* 登录按钮 */
.s-btn{ text-indent: 0; cursor: pointer; background-color: #409EFF; border-color: #409EFF; color: #FFF;}
.s-btn:hover{background-color: #50aEFF;}
/* 重置按钮 */
.reset-box{text-align: left; font-size: 12px;}
.reset-box a{text-decoration: none;}
.reset-box a:hover{text-decoration: underline;}
/* loading框样式 */
.ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);}
.ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;}
.ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; }

View File

@ -0,0 +1,99 @@
// sa
var sa = {};
sa.setToken = function (token) {
localStorage.setItem("Authorization", token)
};
sa.getToken = function () {
return "Bearer " + localStorage.getItem("Authorization")
};
$.ajaxSettings.beforeSend = function (xhr, request) {
xhr.setRequestHeader("Authorization", sa.getToken());
}
// 打开loading
sa.loading = function (msg) {
layer.closeAll(); // 开始前先把所有弹窗关了
return layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load'});
};
// 隐藏loading
sa.hideLoading = function () {
layer.closeAll();
};
$.ajax({
url: "sso/isLogin",
type: "get",
success: function (res) {
debugger
console.log('返回数据:', res);
sa.hideLoading();
if (res.code === 200 && res.data) {
layer.msg('登录成功', {anim: 0, icon: 6});
} else {
layer.msg("检验未登录!", {anim: 6, icon: 2});
}
},
error: function (xhr, type, errorThrown) {
sa.hideLoading();
if (xhr.status == 0) {
return layer.alert('无法连接到服务器,请检查网络');
}
return layer.alert("异常:" + JSON.stringify(xhr));
}
});
// ----------------------------------- 登录事件 -----------------------------------
$('.login-btn').click(function () {
sa.loading("正在登录...");
// 开始登录
setTimeout(function () {
$.ajax({
url: "sso/doLogin",
type: "post",
data: {
name: $('[name=name]').val(),
pwd: $('[name=pwd]').val()
},
dataType: 'json',
success: function (res) {
console.log('返回数据:', res);
sa.hideLoading();
if (res.code === 200) {
layer.msg('登录成功', {anim: 0, icon: 6});
sa.setToken(res.data);
setTimeout(function () {
location.reload();
}, 800)
} else {
layer.msg(res.msg, {anim: 6, icon: 2});
}
},
error: function (xhr, type, errorThrown) {
sa.hideLoading();
if (xhr.status == 0) {
return layer.alert('无法连接到服务器,请检查网络');
}
return layer.alert("异常:" + JSON.stringify(xhr));
}
});
}, 400);
});
// 绑定回车事件
$('[name=name],[name=pwd]').bind('keypress', function (event) {
if (event.keyCode == "13") {
$('.login-btn').click();
}
});
// 输入框获取焦点
$("[name=name]").focus();
// 打印信息
var str = "This page is provided by Sa-Token, Please refer to: " + "https://sa-token.cc/";
console.log(str);

View File

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<title>Sa-SSO-Server 认证中心-登录</title>
<meta charset="utf-8">
<base th:href="@{/}" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="stylesheet" href="./sa-res/login.css">
</head>
<body>
<div class="view-box">
<div class="bg-1"></div>
<div class="bg-2"></div>
<div class="content-box">
<div class="login-box">
<div class="from-box">
<h2 class="from-title">Sa-SSO-Server 认证中心</h2>
<div class="from-item">
<input class="s-input" name="name" placeholder="请输入账号" />
</div>
<div class="from-item">
<input class="s-input" name="pwd" type="password" placeholder="请输入密码" />
</div>
<div class="from-item">
<button class="s-input s-btn login-btn">登录</button>
</div>
<div class="from-item reset-box">
<a href="javascript: location.reload();" >刷新</a>
</div>
</div>
</div>
</div>
<!-- 底部 版权 -->
<div style="position: absolute; bottom: 40px; width: 100%; text-align: center; color: #666;">
This page is provided by Sa-Token-SSO
</div>
</div>
<!-- scripts -->
<script src="./sa-res/jquery.min.js"></script>
<script src="./sa-res/layer/layer.js"></script>
<script src="./sa-res/login.js"></script>
</body>
</html>