This commit is contained in:
wol 2025-08-17 22:20:34 +08:00
parent 74064a4dc4
commit 4a0b1499c7
71 changed files with 2728 additions and 332 deletions

View File

@ -24,6 +24,11 @@
<artifactId>wol-module-ai</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.agileboot</groupId>
<artifactId>wol-auth</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
</project>

View File

@ -17,6 +17,7 @@
<module>wol-common-mybatis</module>
<module>wol-common-redis</module>
<module>wol-common-json</module>
<module>wol-common-satoken</module>
</modules>
<packaging>pom</packaging>

View File

@ -47,6 +47,11 @@
<artifactId>wol-common-json</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.agileboot</groupId>
<artifactId>wol-common-satoken</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>

View File

@ -85,6 +85,10 @@
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!--ENC加密-->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>

View File

@ -45,11 +45,13 @@ public interface Constants {
* 通用成功标识
*/
String SUCCESS = "0";
String NORMAL = "0"; // 正常状态
/**
* 通用失败标识
*/
String FAIL = "1";
String DISABLE = "1"; // 异常/停用状态
/**
* 登录成功
@ -80,6 +82,45 @@ public interface Constants {
* 顶级部门id
*/
Long TOP_PARENT_ID = 0L;
/**
* 超级管理员ID
*/
Long SUPER_ADMIN_ID = 1L;
/**
* 超级管理员角色 roleKey
*/
String SUPER_ADMIN_ROLE_KEY = "superadmin";
/**
* 租户管理员角色 roleKey
*/
String TENANT_ADMIN_ROLE_KEY = "admin";
/**
* 租户管理员角色名称
*/
String TENANT_ADMIN_ROLE_NAME = "管理员";
/**
* 默认租户ID
*/
String DEFAULT_TENANT_ID = "000000";
interface Cache {
/**
* 全局 redis key (业务无关的key)
*/
String GLOBAL_REDIS_KEY = "global:";
/**
* 登录账户密码错误次数 redis key
*/
String PWD_ERR_CNT_KEY = "pwd_err_cnt:";
/**
* 验证码 redis key
*/
String CAPTCHA_CODE_KEY = GLOBAL_REDIS_KEY + "captcha_codes:";
}
}

View File

@ -0,0 +1,44 @@
package com.agileboot.common.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 登录类型
*
* @author Lion Li
*/
@Getter
@AllArgsConstructor
public enum LoginType {
/**
* 密码登录
*/
PASSWORD("user.password.retry.limit.exceed", "user.password.retry.limit.count"),
/**
* 短信登录
*/
SMS("sms.code.retry.limit.exceed", "sms.code.retry.limit.count"),
/**
* 邮箱登录
*/
EMAIL("email.code.retry.limit.exceed", "email.code.retry.limit.count"),
/**
* 小程序登录
*/
XCX("", "");
/**
* 登录重试超出限制提示
*/
final String retryLimitExceed;
/**
* 登录重试限制计数提示
*/
final String retryLimitCount;
}

View File

@ -0,0 +1,39 @@
package com.agileboot.common.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
/**
* 用户类型
*
* @author Lion Li
*/
@Getter
@AllArgsConstructor
public enum UserType {
/**
* 后台系统用户
*/
SYS_USER("sys_user"),
/**
* 移动客户端用户
*/
APP_USER("app_user");
/**
* 用户类型标识用于 token权限识别等
*/
private final String userType;
public static UserType getUserType(String str) {
for (UserType value : values()) {
if (StringUtils.contains(str, value.getUserType())) {
return value;
}
}
throw new RuntimeException("'UserType' not found By " + str);
}
}

View File

@ -0,0 +1,36 @@
package com.agileboot.common.core.utils;
import cn.hutool.extra.spring.SpringUtil;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Validator;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.util.Set;
/**
* Validator 校验框架工具
*
* @author Lion Li
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ValidatorUtils {
private static final Validator VALID = SpringUtil.getBean(Validator.class);
/**
* 对给定对象进行参数校验并根据指定的校验组进行校验
*
* @param object 要进行校验的对象
* @param groups 校验组
* @throws ConstraintViolationException 如果校验不通过则抛出参数校验异常
*/
public static <T> void validate(T object, Class<?>... groups) {
Set<ConstraintViolation<T>> validate = VALID.validate(object, groups);
if (!validate.isEmpty()) {
throw new ConstraintViolationException("参数校验异常", validate);
}
}
}

View File

@ -39,6 +39,10 @@
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>com.agileboot</groupId>
<artifactId>wol-common-satoken</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -1,6 +1,8 @@
package com.agileboot.common.mybatis.config;
import com.agileboot.common.core.factory.YmlPropertySourceFactory;
import com.agileboot.common.mybatis.handler.InjectionMetaObjectHandler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
@ -29,4 +31,12 @@ public class MybatisPlusConfiguration {
return interceptor;
}
/**
* 元对象字段填充控制器
*/
@Bean
public MetaObjectHandler metaObjectHandler() {
return new InjectionMetaObjectHandler();
}
}

View File

@ -22,9 +22,6 @@ public class BaseEntity implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 搜索值
*/

View File

@ -0,0 +1,110 @@
package com.agileboot.common.mybatis.handler;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.HttpStatus;
import com.agileboot.common.core.exception.BizException;
import com.agileboot.common.mybatis.core.domain.BaseEntity;
import com.agileboot.common.satoken.pojo.LoginUser;
import com.agileboot.common.satoken.utils.LoginHelper;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.ibatis.reflection.MetaObject;
import java.util.Date;
/**
* MP注入处理器
*
* @author Lion Li
*/
@Slf4j
public class InjectionMetaObjectHandler implements MetaObjectHandler {
/**
* 如果用户不存在默认注入-1代表无用户
*/
private static final Long DEFAULT_USER_ID = -1L;
/**
* 插入填充方法用于在插入数据时自动填充实体对象中的创建时间更新时间创建人更新人等信息
*
* @param metaObject 元对象用于获取原始对象并进行填充
*/
@Override
public void insertFill(MetaObject metaObject) {
try {
if (ObjectUtil.isNotNull(metaObject) && metaObject.getOriginalObject() instanceof BaseEntity baseEntity) {
Date current = ObjectUtils.defaultIfNull(baseEntity.getCreateTime(), new Date());
baseEntity.setCreateTime(current);
baseEntity.setUpdateTime(current);
baseEntity.setDeleted(0);
// 如果创建人为空则填充当前登录用户的信息
if (ObjectUtil.isNull(baseEntity.getCreateBy())) {
LoginUser loginUser = getLoginUser();
if (ObjectUtil.isNotNull(loginUser)) {
Long userId = loginUser.getUserId();
// 填充创建人更新人和创建部门信息
baseEntity.setCreateBy(userId);
baseEntity.setUpdateBy(userId);
} else {
// 填充创建人更新人和创建部门信息
baseEntity.setCreateBy(DEFAULT_USER_ID);
baseEntity.setUpdateBy(DEFAULT_USER_ID);
}
}
} else {
Date date = new Date();
this.strictInsertFill(metaObject, "createTime", Date.class, date);
this.strictInsertFill(metaObject, "updateTime", Date.class, date);
}
} catch (Exception e) {
throw new BizException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);
}
}
/**
* 更新填充方法用于在更新数据时自动填充实体对象中的更新时间和更新人信息
*
* @param metaObject 元对象用于获取原始对象并进行填充
*/
@Override
public void updateFill(MetaObject metaObject) {
try {
if (ObjectUtil.isNotNull(metaObject) && metaObject.getOriginalObject() instanceof BaseEntity baseEntity) {
// 获取当前时间作为更新时间无论原始对象中的更新时间是否为空都填充
Date current = new Date();
baseEntity.setUpdateTime(current);
// 获取当前登录用户的ID并填充更新人信息
Long userId = LoginHelper.getUserId();
if (ObjectUtil.isNotNull(userId)) {
baseEntity.setUpdateBy(userId);
} else {
baseEntity.setUpdateBy(DEFAULT_USER_ID);
}
} else {
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
}
} catch (Exception e) {
throw new BizException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);
}
}
/**
* 获取当前登录用户信息
*
* @return 当前登录用户的信息如果用户未登录则返回 null
*/
private LoginUser getLoginUser() {
LoginUser loginUser;
try {
loginUser = LoginHelper.getLoginUser();
} catch (Exception e) {
return null;
}
return loginUser;
}
}

View File

@ -27,7 +27,10 @@
<groupId>com.baomidou</groupId>
<artifactId>lock4j-redisson-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>

View File

@ -42,7 +42,7 @@ public class RedissonConfiguration {
@Autowired
private RedissonProperties redissonProperties;
@Bean
// @Bean
public RedissonAutoConfigurationCustomizer redissonCustomizer() {
return config -> {
JavaTimeModule javaTimeModule = new JavaTimeModule();

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.agileboot</groupId>
<artifactId>agileboot-common</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>wol-common-satoken</artifactId>
<properties>
<satoken.version>1.44.0</satoken.version>
</properties>
<dependencies>
<dependency>
<groupId>com.agileboot</groupId>
<artifactId>wol-common-core</artifactId>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>${satoken.version}</version>
</dependency>
<!-- Sa-Token 整合 jwt -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-jwt</artifactId>
<version>${satoken.version}</version>
</dependency>
<!-- Sa-Token 整合 RedisTemplate -->
<!-- <dependency>-->
<!-- <groupId>cn.dev33</groupId>-->
<!-- <artifactId>sa-token-redis-template</artifactId>-->
<!-- <version>${satoken.version}</version>-->
<!-- </dependency>-->
<!-- 提供 Redis 连接池 -->
<!-- <dependency>-->
<!-- <groupId>org.apache.commons</groupId>-->
<!-- <artifactId>commons-pool2</artifactId>-->
<!-- </dependency>-->
</dependencies>
</project>

View File

@ -0,0 +1,52 @@
package com.agileboot.common.satoken.config;
import cn.dev33.satoken.jwt.StpLogicJwtForSimple;
import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpLogic;
import com.agileboot.common.core.factory.YmlPropertySourceFactory;
import com.agileboot.common.satoken.handler.SaTokenExceptionHandler;
import com.agileboot.common.satoken.service.SaPermissionImpl;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
/**
* Sa-Token 配置
*
* @author Lion Li
*/
@AutoConfiguration
@PropertySource(value = "classpath:common-satoken.yml", factory = YmlPropertySourceFactory.class)
public class SaTokenConfiguration {
// Sa-Token 整合 jwt (Simple 简单模式)
@Bean
public StpLogic getStpLogicJwt() {
return new StpLogicJwtForSimple();
}
/**
* 权限接口实现(使用bean注入方便用户替换)
*/
@Bean
public StpInterface stpInterface() {
return new SaPermissionImpl();
}
/**
* 自定义dao层存储
*/
// @Bean
// public SaTokenDao saTokenDao() {
// return new PlusSaTokenDao();
// }
/**
* 异常处理器
*/
@Bean
public SaTokenExceptionHandler saTokenExceptionHandler() {
return new SaTokenExceptionHandler();
}
}

View File

@ -0,0 +1,62 @@
package com.agileboot.common.satoken.config;
import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.same.SaSameUtil;
import cn.dev33.satoken.util.SaResult;
import com.agileboot.common.core.constant.HttpStatus;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 权限安全配置
*
* @author Lion Li
*/
@AutoConfiguration
public class SaTokenMvcConfiguration implements WebMvcConfigurer {
/**
* 注册sa-token的拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册路由拦截器自定义验证规则
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
}
/**
* 校验是否从网关转发
*/
// @Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
.addInclude("/**")
.addExclude("/actuator", "/actuator/**")
.setAuth(obj -> {
if (SaManager.getConfig().getCheckSameToken()) {
SaSameUtil.checkCurrentRequestToken();
}
})
.setError(e -> SaResult.error("认证失败,无法访问系统资源").setCode(HttpStatus.UNAUTHORIZED));
}
/**
* actuator 健康检查接口 做账号密码鉴权
*/
// @Bean
// public SaServletFilter actuatorFilter() {
// String username = SpringUtil.getProperty("spring.cloud.nacos.discovery.metadata.username");
// String password = SpringUtil.getProperty("spring.cloud.nacos.discovery.metadata.userpassword");
// return new SaServletFilter()
// .addInclude("/actuator", "/actuator/**")
// .setAuth(obj -> {
// SaHttpBasicUtil.check(username + ":" + password);
// })
// .setError(e -> SaResult.error(e.getMessage()).setCode(HttpStatus.UNAUTHORIZED));
// }
}

View File

@ -0,0 +1,52 @@
package com.agileboot.common.satoken.handler;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import cn.hutool.http.HttpStatus;
import com.agileboot.common.core.core.R;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* SaToken异常处理器
*
* @author Lion Li
*/
@Slf4j
@RestControllerAdvice
public class SaTokenExceptionHandler {
/**
* 权限码异常
*/
@ExceptionHandler(NotPermissionException.class)
public R<Void> handleNotPermissionException(NotPermissionException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',权限码校验失败'{}'", requestURI, e.getMessage());
return R.fail(HttpStatus.HTTP_FORBIDDEN, "没有访问权限,请联系管理员授权");
}
/**
* 角色权限异常
*/
@ExceptionHandler(NotRoleException.class)
public R<Void> handleNotRoleException(NotRoleException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',角色权限校验失败'{}'", requestURI, e.getMessage());
return R.fail(HttpStatus.HTTP_FORBIDDEN, "没有访问权限,请联系管理员授权");
}
/**
* 认证失败
*/
@ExceptionHandler(NotLoginException.class)
public R<Void> handleNotLoginException(NotLoginException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, e.getMessage());
return R.fail(HttpStatus.HTTP_UNAUTHORIZED, "认证失败,无法访问系统资源");
}
}

View File

@ -0,0 +1,151 @@
package com.agileboot.common.satoken.pojo;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
import java.util.Set;
/**
* 用户信息
*
* @author ruoyi
*/
@Data
@NoArgsConstructor
public class LoginUser implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 租户ID
*/
private String tenantId;
/**
* 用户ID
*/
private Long userId;
/**
* 部门ID
*/
private Long deptId;
/**
* 部门类别编码
*/
private String deptCategory;
/**
* 部门名
*/
private String deptName;
/**
* 用户唯一标识
*/
private String token;
/**
* 用户类型
*/
private String userType;
/**
* 登录时间
*/
private Long loginTime;
/**
* 过期时间
*/
private Long expireTime;
/**
* 登录IP地址
*/
private String ipaddr;
/**
* 登录地点
*/
private String loginLocation;
/**
* 浏览器类型
*/
private String browser;
/**
* 操作系统
*/
private String os;
/**
* 菜单权限
*/
private Set<String> menuPermission;
/**
* 角色权限
*/
private Set<String> rolePermission;
/**
* 用户名
*/
private String username;
/**
* 用户昵称
*/
private String nickname;
/**
* 密码
*/
private String password;
/**
* 角色对象
*/
private List<RoleDTO> roles;
/**
* 岗位对象
*/
private List<PostDTO> posts;
/**
* 数据权限 当前角色ID
*/
private Long roleId;
/**
* 客户端
*/
private String clientKey;
/**
* 设备类型
*/
private String deviceType;
/**
* 获取登录id
*/
public String getLoginId() {
if (userType == null) {
throw new IllegalArgumentException("用户类型不能为空");
}
if (userId == null) {
throw new IllegalArgumentException("用户ID不能为空");
}
return userType + ":" + userId;
}
}

View File

@ -0,0 +1,46 @@
package com.agileboot.common.satoken.pojo;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* 岗位
*
* @author AprilWind
*/
@Data
@NoArgsConstructor
public class PostDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 岗位ID
*/
private Long postId;
/**
* 部门id
*/
private Long deptId;
/**
* 岗位编码
*/
private String postCode;
/**
* 岗位名称
*/
private String postName;
/**
* 岗位类别编码
*/
private String postCategory;
}

View File

@ -0,0 +1,42 @@
package com.agileboot.common.satoken.pojo;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* 角色
*
* @author Lion Li
*/
@Data
@NoArgsConstructor
public class RoleDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 角色ID
*/
private Long roleId;
/**
* 角色名称
*/
private String roleName;
/**
* 角色权限
*/
private String roleKey;
/**
* 数据范围1全部数据权限 2自定数据权限 3本部门数据权限 4本部门及以下数据权限 5仅本人数据权限 6部门及以下或本人数据权限
*/
private String dataScope;
}

View File

@ -0,0 +1,30 @@
package com.agileboot.common.satoken.service;
import cn.dev33.satoken.stp.StpInterface;
import java.util.ArrayList;
import java.util.List;
/**
* sa-token 权限管理实现类
*
* @author Lion Li
*/
public class SaPermissionImpl implements StpInterface {
/**
* 获取菜单权限列表
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
return new ArrayList<>();
}
/**
* 获取角色权限列表
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
return new ArrayList<>();
}
}

View File

@ -0,0 +1,216 @@
package com.agileboot.common.satoken.utils;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import com.agileboot.common.core.constant.Constants;
import com.agileboot.common.core.enums.UserType;
import com.agileboot.common.satoken.pojo.LoginUser;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import java.util.Set;
/**
* 登录鉴权助手
* <p>
* user_type 用户类型 同一个用户表 可以有多种用户类型 例如 pc,app
* deivce 设备类型 同一个用户类型 可以有 多种设备类型 例如 web,ios
* 可以组成 用户类型与设备类型多对多的 权限灵活控制
* <p>
* 多用户体系 针对 多种用户类型 但权限控制不一致
* 可以组成 多用户类型表与多设备类型 分别控制权限
*
* @author Lion Li
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class LoginHelper {
public static final String LOGIN_USER_KEY = "loginUser";
public static final String TENANT_KEY = "tenantId";
public static final String USER_KEY = "userId";
public static final String USER_NAME_KEY = "userName";
public static final String DEPT_KEY = "deptId";
public static final String DEPT_NAME_KEY = "deptName";
public static final String DEPT_CATEGORY_KEY = "deptCategory";
public static final String CLIENT_KEY = "clientid";
/**
* 登录系统 基于 设备类型
* 针对相同用户体系不同设备
*
* @param loginUser 登录用户信息
* @param model 配置参数
*/
public static void login(LoginUser loginUser, SaLoginParameter model) {
model = ObjectUtil.defaultIfNull(model, new SaLoginParameter());
StpUtil.login(loginUser.getLoginId(),
model.setExtra(TENANT_KEY, loginUser.getTenantId())
.setExtra(USER_KEY, loginUser.getUserId())
.setExtra(USER_NAME_KEY, loginUser.getUsername())
.setExtra(DEPT_KEY, loginUser.getDeptId())
.setExtra(DEPT_NAME_KEY, loginUser.getDeptName())
.setExtra(DEPT_CATEGORY_KEY, loginUser.getDeptCategory())
);
StpUtil.getTokenSession().set(LOGIN_USER_KEY, loginUser);
}
/**
* 获取用户(多级缓存)
*/
@SuppressWarnings("unchecked cast")
public static <T extends LoginUser> T getLoginUser() {
SaSession session = StpUtil.getTokenSession();
if (ObjectUtil.isNull(session)) {
return null;
}
return (T) session.get(LOGIN_USER_KEY);
}
/**
* 获取用户基于token
*/
@SuppressWarnings("unchecked cast")
public static <T extends LoginUser> T getLoginUser(String token) {
SaSession session = StpUtil.getTokenSessionByToken(token);
if (ObjectUtil.isNull(session)) {
return null;
}
return (T) session.get(LOGIN_USER_KEY);
}
/**
* 获取用户id
*/
public static Long getUserId() {
return Convert.toLong(getExtra(USER_KEY));
}
/**
* 获取用户id
*/
public static String getUserIdStr() {
return Convert.toStr(getExtra(USER_KEY));
}
/**
* 获取用户账户
*/
public static String getUsername() {
return Convert.toStr(getExtra(USER_NAME_KEY));
}
/**
* 获取租户ID
*/
public static String getTenantId() {
return Convert.toStr(getExtra(TENANT_KEY));
}
/**
* 获取部门ID
*/
public static Long getDeptId() {
return Convert.toLong(getExtra(DEPT_KEY));
}
/**
* 获取部门名
*/
public static String getDeptName() {
return Convert.toStr(getExtra(DEPT_NAME_KEY));
}
/**
* 获取部门类别编码
*/
public static String getDeptCategory() {
return Convert.toStr(getExtra(DEPT_CATEGORY_KEY));
}
/**
* 获取当前 Token 的扩展信息
*
* @param key 键值
* @return 对应的扩展数据
*/
private static Object getExtra(String key) {
try {
return StpUtil.getExtra(key);
} catch (Exception e) {
return null;
}
}
/**
* 获取用户类型
*/
public static UserType getUserType() {
String loginType = StpUtil.getLoginIdAsString();
return UserType.getUserType(loginType);
}
/**
* 是否为超级管理员
*
* @param userId 用户ID
* @return 结果
*/
public static boolean isSuperAdmin(Long userId) {
return Constants.SUPER_ADMIN_ID.equals(userId);
}
/**
* 是否为超级管理员
*
* @return 结果
*/
public static boolean isSuperAdmin() {
return isSuperAdmin(getUserId());
}
/**
* 是否为租户管理员
*
* @param rolePermission 角色权限标识组
* @return 结果
*/
public static boolean isTenantAdmin(Set<String> rolePermission) {
if (CollectionUtils.isEmpty(rolePermission)) {
return false;
}
return rolePermission.contains(Constants.TENANT_ADMIN_ROLE_KEY);
}
/**
* 是否为租户管理员
*
* @return 结果
*/
public static boolean isTenantAdmin() {
LoginUser loginUser = getLoginUser();
if (loginUser == null) {
return false;
}
return Convert.toBool(isTenantAdmin(loginUser.getRolePermission()));
}
/**
* 检查当前用户是否已登录
*
* @return 结果
*/
public static boolean isLogin() {
try {
StpUtil.checkLogin();
return true;
} catch (Exception e) {
return false;
}
}
}

View File

@ -0,0 +1,2 @@
com.agileboot.common.satoken.config.SaTokenConfiguration
com.agileboot.common.satoken.config.SaTokenMvcConfiguration

View File

@ -0,0 +1,13 @@
# 内置配置 不允许修改 如需修改请在 nacos 上写相同配置覆盖
# Sa-Token配置
sa-token:
# 允许动态设置 token 有效期
dynamic-active-timeout: true
# 允许从 请求参数 读取 token
is-read-body: true
# 允许从 header 读取 token
is-read-header: true
# 关闭 cookie 鉴权 从根源杜绝 csrf 漏洞风险
is-read-cookie: false
# token前缀
token-prefix: "Bearer"

View File

@ -5,6 +5,7 @@ import com.agileboot.common.web.handler.GlobalExceptionHandler;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
@ -14,10 +15,12 @@ import java.util.Date;
/**
* 通用配置
* 注解EnableAspectJAutoProxy 表示通过aop框架暴露该代理对象,AopContext能够访问
*
* @author Lion Li
*/
@AutoConfiguration
@EnableAspectJAutoProxy(exposeProxy = true)
public class ResourcesConfig implements WebMvcConfigurer {
@Override

View File

@ -1,7 +1,9 @@
package com.agileboot.system.config.pojo.entity;
import com.agileboot.common.mybatis.core.domain.BaseEntity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
@ -27,6 +29,9 @@ public class SysConfig extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID)
private Long id;
@ApiModelProperty("配置名称")
@TableField("config_name")
private String configName;

View File

@ -1,105 +0,0 @@
package com.agileboot.system.login.controller;
import com.agileboot.common.core.core.R;
import com.agileboot.common.core.exception.BizException;
import com.agileboot.common.core.exception.error.ErrorCode;
import com.agileboot.system.config.pojo.dto.ConfigDTO;
import com.agileboot.system.login.pojo.vo.CaptchaVO;
import com.agileboot.system.login.service.LoginService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 登录相关接口
*
* @author valarchie
*/
@RestController
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;
private final MenuAService menuService;
private final UserService userService;
/**
* 获取系统的内置配置
*
* @return 配置信息
*/
@GetMapping("/getConfig")
public R<ConfigDTO> getConfig() {
return R.ok(loginService.getConfig());
}
/**
* 生成验证码
*/
@GetMapping("/captchaImage")
public R<CaptchaVO> getCaptchaImg() {
return R.ok(loginService.generateCaptchaImg());
}
/**
* 登录方法
*
* @param loginCommand 登录信息
* @return 结果
*/
@PostMapping("/login")
public R<TokenDTO> login(@RequestBody LoginCommand loginCommand) {
// 生成令牌
String token = loginService.login(loginCommand);
SystemLoginUser loginUser = AuthenticationUtils.getSystemLoginUser();
CurrentLoginUserDTO currentUserDTO = userService.getLoginUserInfo(loginUser);
return R.ok(new TokenDTO(token, currentUserDTO));
}
/**
* 获取当前登录用户信息
*
* @return 用户信息
*/
@GetMapping("/getLoginUserInfo")
public R<CurrentLoginUserDTO> getLoginUserInfo() {
SystemLoginUser loginUser = AuthenticationUtils.getSystemLoginUser();
CurrentLoginUserDTO currentUserDTO = userService.getLoginUserInfo(loginUser);
return R.ok(currentUserDTO);
}
/**
* 获取用户对应的菜单路由 用于动态生成路由
* TODO 如果要在前端开启路由缓存的话 需要在ServerConfig.json 设置CachingAsyncRoutes=true 避免一直重复请求路由接口
*
* @return 路由信息
*/
@GetMapping("/getRouters")
public R<List<RouterDTO>> getRouters() {
SystemLoginUser loginUser = AuthenticationUtils.getSystemLoginUser();
List<RouterDTO> routerTree = menuService.getRouterTree(loginUser);
return R.ok(routerTree);
}
/**
* 注册接口
*
* @param command
* @return
*/
@PostMapping("/register")
public R<Void> register(@RequestBody AddUserCommand command) {
throw new BizException(ErrorCode.Business.COMMON_UNSUPPORTED_OPERATION);
}
}

View File

@ -1,15 +0,0 @@
package com.agileboot.system.login.pojo.vo;
import lombok.Data;
/**
* @author valarchie
*/
@Data
public class CaptchaVO {
private Boolean isCaptchaOn;
private String captchaCodeKey;
private String captchaCodeImg;
}

View File

@ -1,205 +0,0 @@
package com.agileboot.system.login.service;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.img.ImgUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.extra.servlet.ServletUtil;
import com.agileboot.common.core.config.AgileBootConfig;
import com.agileboot.common.core.enums.common.LoginStatusEnum;
import com.agileboot.common.core.exception.BizException;
import com.agileboot.common.core.exception.error.ErrorCode;
import com.agileboot.common.core.utils.ServletHolderUtil;
import com.agileboot.system.config.pojo.dto.ConfigDTO;
import com.agileboot.system.enums.ConfigKeyEnum;
import com.agileboot.system.login.pojo.vo.CaptchaVO;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.FastByteArrayOutputStream;
import java.awt.image.BufferedImage;
/**
* 登录校验方法
*
* @author ruoyi
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class LoginService {
private final TokenService tokenService;
private final RedisCacheService redisCache;
private final GuavaCacheService guavaCache;
private final AuthenticationManager authenticationManager;
@Resource(name = "captchaProducer")
private Producer captchaProducer;
@Resource(name = "captchaProducerMath")
private Producer captchaProducerMath;
/**
* 登录验证
*
* @param loginCommand 登录参数
* @return 结果
*/
public String login(LoginCommand loginCommand) {
// 验证码开关
if (isCaptchaOn()) {
validateCaptcha(loginCommand.getUsername(), loginCommand.getCaptchaCode(), loginCommand.getCaptchaCodeKey());
}
// 用户验证
Authentication authentication;
String decryptPassword = decryptPassword(loginCommand.getPassword());
try {
// 该方法会去调用UserDetailsServiceImpl#loadUserByUsername 校验用户名和密码 认证鉴权
authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
loginCommand.getUsername(), decryptPassword));
} catch (BadCredentialsException e) {
ThreadPoolManager.execute(AsyncTaskFactory.loginInfoTask(loginCommand.getUsername(), LoginStatusEnum.LOGIN_FAIL,
MessageUtils.message("Business.LOGIN_WRONG_USER_PASSWORD")));
throw new ApiException(e, ErrorCode.Business.LOGIN_WRONG_USER_PASSWORD);
} catch (AuthenticationException e) {
ThreadPoolManager.execute(AsyncTaskFactory.loginInfoTask(loginCommand.getUsername(), LoginStatusEnum.LOGIN_FAIL, e.getMessage()));
throw new ApiException(e, ErrorCode.Business.LOGIN_ERROR, e.getMessage());
} catch (Exception e) {
ThreadPoolManager.execute(AsyncTaskFactory.loginInfoTask(loginCommand.getUsername(), LoginStatusEnum.LOGIN_FAIL, e.getMessage()));
throw new ApiException(e, Business.LOGIN_ERROR, e.getMessage());
}
// 把当前登录用户 放入上下文中
SecurityContextHolder.getContext().setAuthentication(authentication);
// 这里获取的loginUser是UserDetailsServiceImpl#loadUserByUsername方法返回的LoginUser
SystemLoginUser loginUser = (SystemLoginUser) authentication.getPrincipal();
recordLoginInfo(loginUser);
// 生成token
return tokenService.createTokenAndPutUserInCache(loginUser);
}
/**
* 获取验证码 data
*
* @return {@link ConfigDTO}
*/
public ConfigDTO getConfig() {
ConfigDTO configDTO = new ConfigDTO();
boolean isCaptchaOn = isCaptchaOn();
configDTO.setIsCaptchaOn(isCaptchaOn);
configDTO.setDictionary(MapCache.dictionaryCache());
return configDTO;
}
/**
* 获取验证码 data
*
* @return 验证码
*/
public CaptchaVO generateCaptchaImg() {
CaptchaVO captchaVO = new CaptchaVO();
boolean isCaptchaOn = isCaptchaOn();
captchaVO.setIsCaptchaOn(isCaptchaOn);
if (isCaptchaOn) {
String expression;
String answer = null;
BufferedImage image = null;
// 生成验证码
String captchaType = AgileBootConfig.getCaptchaType();
if (Captcha.MATH_TYPE.equals(captchaType)) {
String capText = captchaProducerMath.createText();
String[] expressionAndAnswer = capText.split("@");
expression = expressionAndAnswer[0];
answer = expressionAndAnswer[1];
image = captchaProducerMath.createImage(expression);
}
if (Captcha.CHAR_TYPE.equals(captchaType)) {
expression = answer = captchaProducer.createText();
image = captchaProducer.createImage(expression);
}
if (image == null) {
throw new BizException(ErrorCode.Internal.LOGIN_CAPTCHA_GENERATE_FAIL);
}
// 保存验证码信息
String imgKey = IdUtil.simpleUUID();
redisCache.captchaCache.set(imgKey, answer);
// 转换流信息写出
FastByteArrayOutputStream os = new FastByteArrayOutputStream();
ImgUtil.writeJpg(image, os);
captchaVO.setCaptchaCodeKey(imgKey);
captchaVO.setCaptchaCodeImg(Base64.encode(os.toByteArray()));
}
return captchaVO;
}
/**
* 校验验证码
*
* @param username 用户名
* @param captchaCode 验证码
* @param captchaCodeKey 验证码对应的缓存key
*/
public void validateCaptcha(String username, String captchaCode, String captchaCodeKey) {
String captcha = redisCache.captchaCache.getObjectById(captchaCodeKey);
redisCache.captchaCache.delete(captchaCodeKey);
if (captcha == null) {
ThreadPoolManager.execute(AsyncTaskFactory.loginInfoTask(username, LoginStatusEnum.LOGIN_FAIL,
ErrorCode.Business.LOGIN_CAPTCHA_CODE_EXPIRE.message()));
throw new BizException(ErrorCode.Business.LOGIN_CAPTCHA_CODE_EXPIRE);
}
if (!captchaCode.equalsIgnoreCase(captcha)) {
ThreadPoolManager.execute(AsyncTaskFactory.loginInfoTask(username, LoginStatusEnum.LOGIN_FAIL,
ErrorCode.Business.LOGIN_CAPTCHA_CODE_WRONG.message()));
throw new BizException(ErrorCode.Business.LOGIN_CAPTCHA_CODE_WRONG);
}
}
/**
* 记录登录信息
* @param loginUser 登录用户
*/
public void recordLoginInfo(SystemLoginUser loginUser) {
ThreadPoolManager.execute(AsyncTaskFactory.loginInfoTask(loginUser.getUsername(), LoginStatusEnum.LOGIN_SUCCESS,
LoginStatusEnum.LOGIN_SUCCESS.getDesc()));
SysUserEntity entity = redisCache.userCache.getObjectById(loginUser.getUserId());
entity.setLoginIp(ServletUtil.getClientIP(ServletHolderUtil.getRequest()));
entity.setLoginDate(DateUtil.date());
entity.updateById();
}
public String decryptPassword(String originalPassword) {
byte[] decryptBytes = SecureUtil.rsa(AgileBootConfig.getRsaPrivateKey(), null)
.decrypt(Base64.decode(originalPassword), KeyType.PrivateKey);
return StrUtil.str(decryptBytes, CharsetUtil.CHARSET_UTF_8);
}
private boolean isCaptchaOn() {
return Convert.toBool(guavaCache.configCache.get(ConfigKeyEnum.CAPTCHA.getValue()));
}
}

View File

@ -12,6 +12,7 @@
<modules>
<module>wol-auth</module>
<module>agileboot-system-base</module>
<module>wol-gateway</module>
</modules>
</project>

View File

@ -16,6 +16,18 @@
<groupId>com.agileboot</groupId>
<artifactId>wol-common-web</artifactId>
</dependency>
<dependency>
<groupId>com.agileboot</groupId>
<artifactId>wol-common-mybatis</artifactId>
</dependency>
<dependency>
<groupId>com.agileboot</groupId>
<artifactId>wol-common-satoken</artifactId>
</dependency>
<dependency>
<groupId>com.agileboot</groupId>
<artifactId>wol-common-redis</artifactId>
</dependency>
</dependencies>
<build>

View File

@ -0,0 +1,88 @@
package com.agileboot.auth.captcha;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.core.math.Calculator;
import cn.hutool.core.util.CharUtil;
import cn.hutool.core.util.RandomUtil;
import org.apache.commons.lang3.StringUtils;
import java.io.Serial;
/**
* 无符号计算生成器
*
* @author Lion Li
*/
public class UnsignedMathGenerator implements CodeGenerator {
@Serial
private static final long serialVersionUID = -5514819971774091076L;
private static final String OPERATORS = "+-*";
/**
* 参与计算数字最大长度
*/
private final int numberLength;
/**
* 构造
*/
public UnsignedMathGenerator() {
this(2);
}
/**
* 构造
*
* @param numberLength 参与计算最大数字位数
*/
public UnsignedMathGenerator(int numberLength) {
this.numberLength = numberLength;
}
@Override
public String generate() {
final int limit = getLimit();
int a = RandomUtil.randomInt(limit);
int b = RandomUtil.randomInt(limit);
String max = Integer.toString(Math.max(a, b));
String min = Integer.toString(Math.min(a, b));
max = StringUtils.rightPad(max, this.numberLength, CharUtil.SPACE);
min = StringUtils.rightPad(min, this.numberLength, CharUtil.SPACE);
return max + RandomUtil.randomChar(OPERATORS) + min + '=';
}
@Override
public boolean verify(String code, String userInputCode) {
int result;
try {
result = Integer.parseInt(userInputCode);
} catch (NumberFormatException e) {
// 用户输入非数字
return false;
}
final int calculateResult = (int) Calculator.conversion(code);
return result == calculateResult;
}
/**
* 获取验证码长度
*
* @return 验证码长度
*/
public int getLength() {
return this.numberLength * 2 + 2;
}
/**
* 根据长度获取参与计算数字最大值
*
* @return 最大值
*/
private int getLimit() {
return Integer.parseInt("1" + StringUtils.repeat('0', this.numberLength));
}
}

View File

@ -0,0 +1,62 @@
package com.agileboot.auth.config;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.CircleCaptcha;
import cn.hutool.captcha.LineCaptcha;
import cn.hutool.captcha.ShearCaptcha;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import java.awt.*;
/**
* 验证码配置
*
* @author Lion Li
*/
@Configuration
public class CaptchaConfig {
private static final int WIDTH = 160;
private static final int HEIGHT = 60;
private static final Color BACKGROUND = Color.LIGHT_GRAY;
private static final Font FONT = new Font("Arial", Font.BOLD, 48);
/**
* 圆圈干扰验证码
*/
@Lazy
@Bean
public CircleCaptcha circleCaptcha() {
CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(WIDTH, HEIGHT);
captcha.setBackground(BACKGROUND);
captcha.setFont(FONT);
return captcha;
}
/**
* 线段干扰的验证码
*/
@Lazy
@Bean
public LineCaptcha lineCaptcha() {
LineCaptcha captcha = CaptchaUtil.createLineCaptcha(WIDTH, HEIGHT);
captcha.setBackground(BACKGROUND);
captcha.setFont(FONT);
return captcha;
}
/**
* 扭曲干扰验证码
*/
@Lazy
@Bean
public ShearCaptcha shearCaptcha() {
ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(WIDTH, HEIGHT);
captcha.setBackground(BACKGROUND);
captcha.setFont(FONT);
return captcha;
}
}

View File

@ -0,0 +1,83 @@
package com.agileboot.auth.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.hutool.core.util.ObjectUtil;
import com.agileboot.auth.pojo.dto.LoginBody;
import com.agileboot.auth.pojo.dto.RegisterBody;
import com.agileboot.auth.pojo.vo.LoginVO;
import com.agileboot.auth.pojo.vo.SysClientVO;
import com.agileboot.auth.service.IAuthStrategy;
import com.agileboot.auth.service.ISysClientService;
import com.agileboot.auth.service.SysLoginService;
import com.agileboot.common.core.constant.Constants;
import com.agileboot.common.core.core.R;
import com.agileboot.common.core.utils.ValidatorUtils;
import com.alibaba.fastjson2.JSONObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 认证
*
* @author Lion Li
*/
@Slf4j
@SaIgnore
@RequiredArgsConstructor
@RestController
@RequestMapping("")
public class AuthController {
private final SysLoginService loginService;
private final ISysClientService sysClientService;
/**
* 登录方法
*
* @param body 登录信息
* @return 结果
*/
@PostMapping("/login")
public R<?> login(@RequestBody String body) {
LoginBody loginBody = JSONObject.parseObject(body, LoginBody.class);
ValidatorUtils.validate(loginBody);
String clientId = loginBody.getClientId();
String grantType = loginBody.getGrantType();
SysClientVO clientVo = sysClientService.queryByClientId(clientId);
if (ObjectUtil.isNull(clientVo) || !StringUtils.contains(clientVo.getGrantType(), grantType)) {
log.info("客户端id: {} 认证类型:{} 异常!.", clientId, grantType);
return R.fail("auth.grant.type.error");
} else if (!Constants.NORMAL.equals(clientVo.getStatus())) {
return R.fail("auth.grant.type.blocked");
}
// 登录
LoginVO loginVo = IAuthStrategy.login(body, clientVo, grantType);
return R.ok(loginVo);
}
/**
* 退出登录
*/
@PostMapping("/logout")
public R<Void> logout() {
loginService.logout();
return R.ok("退出成功");
}
/**
* 用户注册
*/
@PostMapping("/register")
public R<Void> register(@Validated @RequestBody RegisterBody user) {
loginService.register(user);
return R.ok();
}
}

View File

@ -0,0 +1,89 @@
package com.agileboot.auth.controller;
import cn.hutool.captcha.AbstractCaptcha;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.agileboot.auth.enums.CaptchaType;
import com.agileboot.auth.pojo.vo.CaptchaVO;
import com.agileboot.auth.properties.CaptchaProperties;
import com.agileboot.common.core.constant.Constants;
import com.agileboot.common.core.core.R;
import com.agileboot.common.redis.utils.RedisUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.aop.framework.AopContext;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Duration;
/**
* 验证码操作处理
*
* @author Lion Li
*/
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
public class CaptchaController {
private final CaptchaProperties captchaProperties;
// private final RedisTemplate<String, String> redisTemplate;
/**
* 生成验证码
*/
@GetMapping("/code")
public R<CaptchaVO> getCode() {
CaptchaVO captchaVo = new CaptchaVO();
boolean captchaEnabled = captchaProperties.getEnabled();
if (!captchaEnabled) {
captchaVo.setCaptchaEnabled(false);
return R.ok(captchaVo);
}
return R.ok(this.getCodeImpl());
}
/**
* 生成验证码
* 独立方法避免验证码关闭之后仍然走限流
*/
// @RateLimiter(time = 60, count = 10, limitType = LimitType.IP)
public CaptchaVO getCodeImpl() {
// 保存验证码信息
String uuid = IdUtil.simpleUUID();
String verifyKey = Constants.Cache.CAPTCHA_CODE_KEY + uuid;
// 生成验证码
CaptchaType captchaType = captchaProperties.getType();
boolean isMath = CaptchaType.MATH == captchaType;
Integer length = isMath ? captchaProperties.getNumberLength() : captchaProperties.getCharLength();
CodeGenerator codeGenerator = ReflectUtil.newInstance(captchaType.getClazz(), length);
AbstractCaptcha captcha = SpringUtil.getBean(captchaProperties.getCategory().getClazz());
captcha.setGenerator(codeGenerator);
captcha.createCode();
// 如果是数学验证码使用SpEL表达式处理验证码结果
String code = captcha.getCode();
if (isMath) {
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(StringUtils.remove(code, "="));
code = exp.getValue(String.class);
}
RedisUtils.setCacheObject(verifyKey, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
// redisTemplate.opsForValue().set(verifyKey, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
CaptchaVO captchaVo = new CaptchaVO();
captchaVo.setUuid(uuid);
captchaVo.setImg(captcha.getImageBase64());
captchaVo.setCode(code);
return captchaVo;
}
}

View File

@ -0,0 +1,35 @@
package com.agileboot.auth.enums;
import cn.hutool.captcha.AbstractCaptcha;
import cn.hutool.captcha.CircleCaptcha;
import cn.hutool.captcha.LineCaptcha;
import cn.hutool.captcha.ShearCaptcha;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 验证码类别
*
* @author Lion Li
*/
@Getter
@AllArgsConstructor
public enum CaptchaCategory {
/**
* 线段干扰
*/
LINE(LineCaptcha.class),
/**
* 圆圈干扰
*/
CIRCLE(CircleCaptcha.class),
/**
* 扭曲干扰
*/
SHEAR(ShearCaptcha.class);
private final Class<? extends AbstractCaptcha> clazz;
}

View File

@ -0,0 +1,29 @@
package com.agileboot.auth.enums;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.captcha.generator.RandomGenerator;
import com.agileboot.auth.captcha.UnsignedMathGenerator;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 验证码类型
*
* @author Lion Li
*/
@Getter
@AllArgsConstructor
public enum CaptchaType {
/**
* 数字
*/
MATH(UnsignedMathGenerator.class),
/**
* 字符
*/
CHAR(RandomGenerator.class);
private final Class<? extends CodeGenerator> clazz;
}

View File

@ -0,0 +1,8 @@
package com.agileboot.auth.mapper;
import com.agileboot.auth.pojo.entity.SysClient;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface SysClientMapper extends BaseMapper<SysClient> {
}

View File

@ -0,0 +1,8 @@
package com.agileboot.auth.mapper;
import com.agileboot.auth.pojo.entity.SysUser;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface SysUserMapper extends BaseMapper<SysUser> {
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.agileboot.auth.pojo.entity.SysClient">
</mapper>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.agileboot.auth.pojo.entity.SysUser">
</mapper>

View File

@ -0,0 +1,48 @@
package com.agileboot.auth.pojo.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 用户登录对象
*
* @author Lion Li
*/
@Data
public class LoginBody implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 客户端id
*/
@NotBlank(message = "Auth clientid cannot be blank")
private String clientId;
/**
* 授权类型
*/
@NotBlank(message = "Auth grant type cannot be blank")
private String grantType;
/**
* 租户ID
*/
private String tenantId;
/**
* 验证码
*/
private String code;
/**
* 唯一标识
*/
private String uuid;
}

View File

@ -0,0 +1,37 @@
package com.agileboot.auth.pojo.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.validator.constraints.Length;
/**
* 用户注册对象
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class RegisterBody extends LoginBody {
/**
* 用户名
*/
@NotBlank(message = "{user.username.not.blank}")
@Length(min = 2, max = 30, message = "{user.username.length.valid}")
private String username;
/**
* 用户密码
*/
@NotBlank(message = "{user.password.not.blank}")
@Length(min = 5, max = 30, message = "{user.password.length.valid}")
// @Pattern(regexp = RegexConstants.PASSWORD, message = "{user.password.format.valid}")
private String password;
/**
* 用户类型 sys_user app_user
*/
private String userType;
}

View File

@ -0,0 +1,70 @@
package com.agileboot.auth.pojo.entity;
import com.agileboot.common.mybatis.core.domain.BaseEntity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
/**
* 授权管理对象 sys_client
*
* @author Michelle.Chung
* @date 2023-05-15
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_client")
public class SysClient extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 客户端id
*/
private String clientId;
/**
* 客户端key
*/
private String clientKey;
/**
* 客户端秘钥
*/
private String clientSecret;
/**
* 授权类型
*/
private String grantType;
/**
* 设备类型
*/
private String deviceType;
/**
* token活跃超时时间
*/
private Long activeTimeout;
/**
* token固定超时时间
*/
private Long timeout;
/**
* 状态0正常 1停用
*/
private String status;
}

View File

@ -0,0 +1,112 @@
package com.agileboot.auth.pojo.entity;
import com.agileboot.common.mybatis.core.domain.BaseEntity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@TableName("sys_user")
public class SysUser extends BaseEntity {
/**
* 用户ID
*/
@TableId(value = "user_id")
private Long userId;
/**
* 租户编号
*/
private String tenantId;
/**
* 部门ID
*/
private Long deptId;
/**
* 用户账号
*/
private String username;
/**
* 用户昵称
*/
private String nickname;
/**
* 用户类型sys_user系统用户
*/
private String userType;
/**
* 用户邮箱
*/
private String email;
/**
* 手机号码
*/
private String phoneNumber;
/**
* 用户性别
*/
private String sex;
/**
* 用户头像
*/
private String avatar;
/**
* 密码
*/
@TableField(
insertStrategy = FieldStrategy.NOT_EMPTY,
updateStrategy = FieldStrategy.NOT_EMPTY,
whereStrategy = FieldStrategy.NOT_EMPTY
)
private String password;
/**
* 帐号状态0正常 1停用
*/
private String status;
/**
* 最后登录IP
*/
private String loginIp;
/**
* 最后登录时间
*/
private Date loginDate;
/**
* 备注
*/
private String remark;
/**
* 职位id
*/
private Long postId;
/**
* 角色id
*/
private Long roleId;
/**
* 超级管理员标志1是0否
*/
private Integer isAdmin;
}

View File

@ -0,0 +1,31 @@
package com.agileboot.auth.pojo.form;
import com.agileboot.auth.pojo.dto.LoginBody;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 邮件登录对象
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class EmailLoginBody extends LoginBody {
/**
* 邮箱
*/
@NotBlank(message = "{user.email.not.blank}")
@Email(message = "{user.email.not.valid}")
private String email;
/**
* 邮箱code
*/
@NotBlank(message = "{email.code.not.blank}")
private String emailCode;
}

View File

@ -0,0 +1,28 @@
package com.agileboot.auth.pojo.form;
import com.agileboot.auth.pojo.dto.LoginBody;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 三方登录对象
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class MiniappLoginBody extends LoginBody {
/**
* 小程序id(多个小程序时使用)
*/
private String appid;
/**
* 小程序code
*/
@NotBlank(message = "{xcx.code.not.blank}")
private String xcxCode;
}

View File

@ -0,0 +1,32 @@
package com.agileboot.auth.pojo.form;
import com.agileboot.auth.pojo.dto.LoginBody;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.validator.constraints.Length;
/**
* 密码登录对象
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class PasswordLoginBody extends LoginBody {
/**
* 用户名
*/
@NotBlank(message = "Username cannot be blank")
@Length(min = 2, max = 30, message = "user.username.length.valid")
private String username;
/**
* 用户密码
*/
@NotBlank(message = "Password cannot be empty")
@Length(min = 5, max = 30, message = "user.password.length.valid")
private String password;
}

View File

@ -0,0 +1,38 @@
package com.agileboot.auth.pojo.form;
import com.agileboot.auth.pojo.dto.LoginBody;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.validator.constraints.Length;
/**
* 用户注册对象
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class RegisterBody extends LoginBody {
/**
* 用户名
*/
@NotBlank(message = "{user.username.not.blank}")
@Length(min = 2, max = 30, message = "{user.username.length.valid}")
private String username;
/**
* 用户密码
*/
@NotBlank(message = "{user.password.not.blank}")
@Length(min = 5, max = 30, message = "{user.password.length.valid}")
// @Pattern(regexp = RegexConstants.PASSWORD, message = "{user.password.format.valid}")
private String password;
/**
* 用户类型
*/
private String userType;
}

View File

@ -0,0 +1,29 @@
package com.agileboot.auth.pojo.form;
import com.agileboot.auth.pojo.dto.LoginBody;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 短信登录对象
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class SmsLoginBody extends LoginBody {
/**
* 手机号
*/
@NotBlank(message = "{user.phonenumber.not.blank}")
private String phonenumber;
/**
* 短信code
*/
@NotBlank(message = "{sms.code.not.blank}")
private String smsCode;
}

View File

@ -0,0 +1,35 @@
package com.agileboot.auth.pojo.form;
import com.agileboot.auth.pojo.dto.LoginBody;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 三方登录对象
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class SocialLoginBody extends LoginBody {
/**
* 第三方登录平台
*/
@NotBlank(message = "{social.source.not.blank}")
private String source;
/**
* 第三方登录code
*/
@NotBlank(message = "{social.code.not.blank}")
private String socialCode;
/**
* 第三方登录socialState
*/
@NotBlank(message = "{social.state.not.blank}")
private String socialState;
}

View File

@ -0,0 +1,26 @@
package com.agileboot.auth.pojo.vo;
import lombok.Data;
/**
* 验证码信息
*
* @author Michelle.Chung
*/
@Data
public class CaptchaVO {
/**
* 是否开启验证码
*/
private Boolean captchaEnabled = true;
private String uuid;
/**
* 验证码图片
*/
private String img;
private String code;
}

View File

@ -0,0 +1,54 @@
package com.agileboot.auth.pojo.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,64 @@
package com.agileboot.auth.pojo.vo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
@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,44 @@
package com.agileboot.auth.properties;
import com.agileboot.auth.enums.CaptchaCategory;
import com.agileboot.auth.enums.CaptchaType;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 验证码配置
*
* @author ruoyi
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "security.captcha")
public class CaptchaProperties {
/**
* 验证码类型
*/
private CaptchaType type;
/**
* 验证码类别
*/
private CaptchaCategory category;
/**
* 数字验证码位数
*/
private Integer numberLength;
/**
* 字符验证码长度
*/
private Integer charLength;
/**
* 验证码开关
*/
private Boolean enabled;
}

View File

@ -0,0 +1,27 @@
package com.agileboot.auth.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 用户密码配置
*
* @author Lion Li
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "user.password")
public class UserPasswordProperties {
/**
* 密码最大错误次数
*/
private Integer maxRetryCount;
/**
* 密码锁定时间默认10分钟
*/
private Integer lockTime;
}

View File

@ -0,0 +1,46 @@
package com.agileboot.auth.service;
import cn.hutool.extra.spring.SpringUtil;
import com.agileboot.auth.pojo.vo.LoginVO;
import com.agileboot.auth.pojo.vo.SysClientVO;
import com.agileboot.common.core.exception.ServiceException;
/**
* 授权策略
*
* @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 + BASE_NAME;
if (!SpringUtil.getBeanFactory().containsBean(beanName)) {
throw new ServiceException("授权类型不正确!");
}
IAuthStrategy instance = SpringUtil.getBean(beanName);
return instance.login(body, client);
}
/**
* 登录
*
* @param body 登录对象
* @param client 授权管理视图对象
* @return 登录验证信息
*/
LoginVO login(String body, SysClientVO client);
}

View File

@ -0,0 +1,7 @@
package com.agileboot.auth.service;
import com.agileboot.auth.pojo.vo.SysClientVO;
public interface ISysClientService {
SysClientVO queryByClientId(String clientId);
}

View File

@ -0,0 +1,10 @@
package com.agileboot.auth.service;
import com.agileboot.auth.pojo.entity.SysUser;
import com.agileboot.common.satoken.pojo.LoginUser;
public interface ISysUserService {
LoginUser getUserInfo(String username);
boolean registerUserInfo(SysUser sysUser);
}

View File

@ -0,0 +1,145 @@
package com.agileboot.auth.service;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.digest.BCrypt;
import com.agileboot.auth.pojo.dto.RegisterBody;
import com.agileboot.auth.pojo.entity.SysUser;
import com.agileboot.auth.properties.CaptchaProperties;
import com.agileboot.auth.properties.UserPasswordProperties;
import com.agileboot.common.core.constant.Constants;
import com.agileboot.common.core.enums.LoginType;
import com.agileboot.common.core.enums.UserType;
import com.agileboot.common.core.exception.BizException;
import com.agileboot.common.redis.utils.RedisUtils;
import com.agileboot.common.satoken.pojo.LoginUser;
import com.agileboot.common.satoken.utils.LoginHelper;
import com.alibaba.fastjson2.JSONObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.function.Supplier;
/**
* 登录校验方法
*
* @author Lion Li
*/
@RequiredArgsConstructor
@Slf4j
@Service
public class SysLoginService {
private final UserPasswordProperties userPasswordProperties;
private final CaptchaProperties captchaProperties;
private final ISysUserService sysUserService;
// private final RedisTemplate<String, String> redisTemplate;
/**
* 登录校验
*/
public void checkLogin(LoginType loginType, String tenantId, String username, Supplier<Boolean> supplier) {
String errorKey = Constants.Cache.PWD_ERR_CNT_KEY + username;
String loginFail = Constants.LOGIN_FAIL;
Integer maxRetryCount = userPasswordProperties.getMaxRetryCount();
Integer lockTime = userPasswordProperties.getLockTime();
// 获取用户登录错误次数默认为0 (可自定义限制策略 例如: key + username + ip)
int errorNumber = ObjectUtil.defaultIfNull(RedisUtils.getCacheObject(errorKey), 0);
// 锁定时间内登录 则踢出
if (errorNumber >= maxRetryCount) {
throw new BizException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
}
if (supplier.get()) {
// 错误次数递增
errorNumber++;
RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
// 达到规定错误次数 则锁定登录
if (errorNumber >= maxRetryCount) {
throw new BizException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
} else {
// 未达到规定错误次数
throw new BizException(loginType.getRetryLimitCount(), errorNumber);
}
}
// 登录成功 清空错误次数
RedisUtils.deleteObject(errorKey);
}
/**
* 退出登录
*/
public void logout() {
try {
LoginUser loginUser = LoginHelper.getLoginUser();
if (ObjectUtil.isNull(loginUser)) {
return;
}
log.info(JSONObject.toJSONString(loginUser));
} catch (NotLoginException ignored) {
} finally {
try {
StpUtil.logout();
} catch (NotLoginException ignored) {
}
}
}
/**
* 注册
*/
public void register(RegisterBody registerBody) {
String tenantId = registerBody.getTenantId();
String username = registerBody.getUsername();
String password = registerBody.getPassword();
// 校验用户类型是否存在
String userType = UserType.getUserType(registerBody.getUserType()).getUserType();
// 验证码开关
if (captchaProperties.getEnabled()) {
validateCaptcha(tenantId, username, registerBody.getCode(), registerBody.getUuid());
}
// 注册用户信息
SysUser sysUser = new SysUser();
sysUser.setTenantId(tenantId);
sysUser.setUsername(username);
sysUser.setNickname(username);
sysUser.setPassword(BCrypt.hashpw(password));
sysUser.setUserType(userType);
boolean regFlag = sysUserService.registerUserInfo(sysUser);
if (!regFlag) {
throw new BizException("user.register.error");
}
}
/**
* 校验验证码
*
* @param username 用户名
* @param code 验证码
* @param uuid 唯一标识
*/
public void validateCaptcha(String tenantId, String username, String code, String uuid) {
String verifyKey = Constants.Cache.CAPTCHA_CODE_KEY + StringUtils.defaultIfBlank(uuid, "");
String captcha = RedisUtils.getCacheObject(verifyKey);
// String captcha = redisTemplate.opsForValue().get(verifyKey);
RedisUtils.deleteObject(verifyKey);
if (captcha == null) {
throw new BizException("user.captcha.expire");
}
if (!code.equalsIgnoreCase(captcha)) {
throw new BizException("user.captcha.error");
}
}
}

View File

@ -0,0 +1,19 @@
package com.agileboot.auth.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.agileboot.auth.mapper.SysClientMapper;
import com.agileboot.auth.pojo.entity.SysClient;
import com.agileboot.auth.pojo.vo.SysClientVO;
import com.agileboot.auth.service.ISysClientService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
@Service
public class SysClientServiceImpl extends ServiceImpl<SysClientMapper, SysClient> implements ISysClientService {
@Override
public SysClientVO queryByClientId(String clientId) {
SysClient client = super.baseMapper.selectOne(new LambdaQueryWrapper<SysClient>().eq(SysClient::getClientId, clientId));
return BeanUtil.copyProperties(client, SysClientVO.class);
}
}

View File

@ -0,0 +1,50 @@
package com.agileboot.auth.service.impl;
import cn.hutool.core.util.ObjectUtil;
import com.agileboot.auth.mapper.SysUserMapper;
import com.agileboot.auth.pojo.entity.SysUser;
import com.agileboot.auth.service.ISysUserService;
import com.agileboot.common.core.constant.Constants;
import com.agileboot.common.core.exception.BizException;
import com.agileboot.common.satoken.pojo.LoginUser;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService {
@Override
public LoginUser getUserInfo(String username) {
SysUser sysUser = super.baseMapper.selectOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, username));
if (ObjectUtil.isNull(sysUser)) {
throw new BizException("user.not.exists", username);
}
if (Constants.DISABLE.equals(sysUser.getStatus())) {
throw new BizException("user.blocked", username);
}
LoginUser loginUser = new LoginUser();
loginUser.setUserId(sysUser.getUserId());
loginUser.setDeptId(sysUser.getDeptId());
loginUser.setPassword(sysUser.getPassword());
loginUser.setUserType(sysUser.getUserType());
loginUser.setUsername(sysUser.getUsername());
loginUser.setNickname(sysUser.getNickname());
loginUser.setPassword(sysUser.getPassword());
return loginUser;
}
@Override
public boolean registerUserInfo(SysUser sysUser) {
// if (!("true".equals(configService.selectConfigByKey("sys.account.registerUser")))) {
// throw new BizException("当前系统没有开启注册功能");
// }
if (super.baseMapper.exists(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, sysUser.getUsername()))) {
throw new BizException("user.register.save.error", sysUser.getUsername());
}
sysUser.setCreateBy(0L);
sysUser.setUpdateBy(0L);
return baseMapper.insert(sysUser) > 0;
}
}

View File

@ -0,0 +1,72 @@
package com.agileboot.auth.service.strategy;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.hutool.crypto.digest.BCrypt;
import com.agileboot.auth.pojo.form.PasswordLoginBody;
import com.agileboot.auth.pojo.vo.LoginVO;
import com.agileboot.auth.pojo.vo.SysClientVO;
import com.agileboot.auth.properties.CaptchaProperties;
import com.agileboot.auth.service.IAuthStrategy;
import com.agileboot.auth.service.ISysUserService;
import com.agileboot.auth.service.SysLoginService;
import com.agileboot.common.core.enums.LoginType;
import com.agileboot.common.core.utils.ValidatorUtils;
import com.agileboot.common.satoken.pojo.LoginUser;
import com.agileboot.common.satoken.utils.LoginHelper;
import com.alibaba.fastjson2.JSONObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 密码认证策略
*
* @author Michelle.Chung
*/
@Slf4j
@Service("password" + IAuthStrategy.BASE_NAME)
@RequiredArgsConstructor
public class PasswordAuthStrategy implements IAuthStrategy {
private final ISysUserService userService;
private final SysLoginService loginService;
private final CaptchaProperties captchaProperties;
@Override
public LoginVO login(String body, SysClientVO client) {
PasswordLoginBody loginBody = JSONObject.parseObject(body, PasswordLoginBody.class);
ValidatorUtils.validate(loginBody);
String username = loginBody.getUsername();
String password = loginBody.getPassword();
String code = loginBody.getCode();
String uuid = loginBody.getUuid();
// 验证码开关
if (captchaProperties.getEnabled()) {
loginService.validateCaptcha(null, null, code, uuid);
}
LoginUser loginUser = userService.getUserInfo(username);
loginService.checkLogin(LoginType.PASSWORD, null, loginUser.getUsername(), () -> !BCrypt.checkpw(password, loginUser.getPassword()));
loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType());
SaLoginParameter model = new SaLoginParameter();
model.setDeviceType(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;
}
}

View File

@ -0,0 +1,129 @@
# 数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
webStatFilter:
enabled: true
statViewServlet:
enabled: true
# 设置白名单,不填则允许所有访问
allow:
url-pattern: /druid/*
# 控制台管理用户名和密码
login-username: agileboot
login-password: 123456
filter:
stat:
enabled: true
# 慢SQL记录
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true
dynamic:
primary: master
strict: false
druid:
# 初始连接数
initialSize: 5
# 最小连接池数量
minIdle: 10
# 最大连接池数量
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
datasource:
master:
url: jdbc:mysql://mysql2.sqlpub.com:3307/agileboot?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&sslMode=REQUIRED
username: ENC(s4kjpEsplGGLeV3YRNvJpJhDSOAO0tEf)
password: ENC(hg/hxmducWsI8u83/eXgAi8yHBDFbB5z0xzwNtBejPc=)
data:
redis:
database: 1
host: 47.94.178.59
port: 6379
password: 'jjt@1234'
jasypt:
encryptor:
password: ${JASYPT_ENCRYPTOR_PASSWORD:}
user:
password:
# 密码最大错误次数
maxRetryCount: 5
# 密码锁定时间默认10分钟
lockTime: 10
# 安全配置
security:
# 验证码
captcha:
# 是否开启验证码
enabled: false
# 验证码类型 math 数组计算 char 字符验证
type: CHAR
# line 线段干扰 circle 圆圈干扰 shear 扭曲干扰
category: CIRCLE
# 数字验证码位数
numberLength: 0
# 字符验证码长度
charLength: 5
# redisson 配置
#redisson:
# # redis key前缀
# keyPrefix:
# # 线程池数量
# threads: 4
# # Netty线程池数量
# nettyThreads: 8
# # 单节点配置
# singleServerConfig:
# # 客户端名称
# clientName: ${spring.application.name}
# # 最小空闲连接数
# connectionMinimumIdleSize: 8
# # 连接池大小
# connectionPoolSize: 32
# # 连接空闲超时,单位:毫秒
# idleConnectionTimeout: 10000
# # 命令等待超时,单位:毫秒
# timeout: 3000
# # 发布和订阅连接池大小
# subscriptionConnectionPoolSize: 50
# 分布式锁 lock4j 全局配置
#lock4j:
# # 获取分布式锁超时时间,默认为 3000 毫秒
# acquire-timeout: 3000
# # 分布式锁的超时时间,默认为 30 秒
# expire: 30000
# Sa-Token配置
sa-token:
# token名称 (同时也是cookie名称)
token-name: Authorization
# 开启内网服务调用鉴权(不允许越过gateway访问内网服务 保障服务安全)
check-same-token: true
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
is-share: false
# jwt秘钥
jwt-secret-key: abcdefghijklmnopqrstuvwxyz

View File

@ -4,4 +4,6 @@ server:
context-path: /auth
spring:
application:
name: wol-auth
name: wol-auth
profiles:
active: dev

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.agileboot</groupId>
<artifactId>agileboot-system</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>wol-gateway</artifactId>
<dependencies>
<dependency>
<groupId>com.agileboot</groupId>
<artifactId>wol-common-satoken</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,20 @@
package com.agileboot.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
/**
* 网关启动程序
*
* @author ruoyi
*/
@SpringBootApplication
public class WolGatewayApplication {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(WolGatewayApplication.class);
application.setApplicationStartup(new BufferingApplicationStartup(2048));
application.run(args);
System.out.println("(♥◠‿◠)ノ゙ 网关启动成功 ლ(´ڡ`ლ)゙ ");
}
}

View File

@ -0,0 +1,63 @@
package com.agileboot.gateway.filter;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.servlet.model.SaRequestForServlet;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import com.agileboot.common.core.constant.HttpStatus;
import com.agileboot.common.satoken.utils.LoginHelper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.reactive.ServerHttpRequest;
/**
* [Sa-Token 权限认证] 拦截器
*
* @author Lion Li
*/
@Configuration
public class AuthFilter {
/**
* 注册 Sa-Token 全局过滤器
*/
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
// 拦截地址
.addInclude("/**")
.addExclude("/favicon.ico", "/actuator", "/actuator/**", "/resource/sse")
// 鉴权方法每次访问进入
.setAuth(obj -> {
// 登录校验 -- 拦截所有路由
SaRouter.match("/**")
// .notMatch()
.check(r -> {
SaRequestForServlet req = (SaRequestForServlet) obj;
ServerHttpRequest request = (ServerHttpRequest) req.getSource();
// 检查是否登录 是否有token
StpUtil.checkLogin();
// 检查 header param 里的 clientid token 里的是否一致
String headerCid = request.getHeaders().getFirst(LoginHelper.CLIENT_KEY);
String paramCid = request.getQueryParams().getFirst(LoginHelper.CLIENT_KEY);
String clientId = StpUtil.getExtra(LoginHelper.CLIENT_KEY).toString();
if (!StringUtils.equalsAny(clientId, headerCid, paramCid)) {
// token 无效
throw NotLoginException.newInstance(StpUtil.getLoginType(),
"-100", "客户端ID与Token不匹配",
StpUtil.getTokenValue());
}
});
}).setError(e -> {
if (e instanceof NotLoginException) {
return SaResult.error(e.getMessage()).setCode(HttpStatus.UNAUTHORIZED);
}
return SaResult.error("认证失败,无法访问系统资源").setCode(HttpStatus.UNAUTHORIZED);
});
}
}

View File

@ -0,0 +1,9 @@
# Tomcat
server:
port: 8080
servlet:
context-path: /
spring:
application:
name: wol-gateway

View File

@ -43,6 +43,7 @@
<redisson.version>3.50.0</redisson.version>
<lock4j.version>2.2.7</lock4j.version>
<guava.version>31.0.1-jre</guava.version>
<fastjson2.version>2.0.58</fastjson2.version>
<!-- 插件版本 -->
@ -265,7 +266,11 @@
<artifactId>therapi-runtime-javadoc</artifactId>
<version>${therapi-javadoc.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
</dependencies>
</dependencyManagement>