first commit
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
package org.dromara;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
|
||||
|
||||
/**
|
||||
* 启动程序
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class PoJieApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication application = new SpringApplication(PoJieApplication.class);
|
||||
application.setApplicationStartup(new BufferingApplicationStartup(2048));
|
||||
application.run(args);
|
||||
System.out.println("(♥◠‿◠)ノ゙ PoJie启动成功 ლ(´ڡ`ლ)゙");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.dromara;
|
||||
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
|
||||
|
||||
/**
|
||||
* web容器中进行部署
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
public class PoJieServletInitializer extends SpringBootServletInitializer {
|
||||
|
||||
@Override
|
||||
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
|
||||
return application.sources(PoJieApplication.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
package org.dromara.app.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import cn.hutool.core.codec.Base64;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.zhyd.oauth.model.AuthResponse;
|
||||
import me.zhyd.oauth.model.AuthUser;
|
||||
import me.zhyd.oauth.request.AuthRequest;
|
||||
import me.zhyd.oauth.utils.AuthStateUtils;
|
||||
import org.dromara.app.domain.vo.AppConfigVo;
|
||||
import org.dromara.app.service.AppLoginService;
|
||||
import org.dromara.app.service.IAppAuthStrategy;
|
||||
import org.dromara.common.core.constant.SystemConstants;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.domain.model.LoginBody;
|
||||
import org.dromara.common.core.domain.model.RegisterBody;
|
||||
import org.dromara.common.core.domain.model.SocialLoginBody;
|
||||
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.json.utils.JsonUtils;
|
||||
import org.dromara.common.ratelimiter.annotation.RateLimiter;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
|
||||
import org.dromara.common.social.config.properties.SocialProperties;
|
||||
import org.dromara.common.social.utils.SocialUtils;
|
||||
import org.dromara.member.domain.bo.AppUserBo;
|
||||
import org.dromara.member.service.AppSocialAuthService;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.system.service.SysClientService;
|
||||
import org.dromara.system.service.SysConfigService;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* App端认证控制器
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@SaIgnore
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/app/auth")
|
||||
public class AppAuthController {
|
||||
|
||||
private final SocialProperties socialProperties;
|
||||
private final AppLoginService appLoginService;
|
||||
private final org.dromara.app.service.AppLoginRiskService appLoginRiskService;
|
||||
private final SysClientService clientService;
|
||||
private final SysConfigService configService;
|
||||
private final AppSocialAuthService appSocialAuthService;
|
||||
|
||||
/**
|
||||
* App端登录方法
|
||||
*
|
||||
* @param body 登录信息
|
||||
* @return 结果
|
||||
*/
|
||||
// TODO: App端暂时不加密,先完善用户端的加密方法在加密
|
||||
// @ApiEncrypt
|
||||
@PostMapping("/login")
|
||||
public R<LoginVo> login(@RequestBody String body) {
|
||||
LoginBody loginBody = JsonUtils.parseObject(body, LoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
// 授权类型和客户端id
|
||||
String clientId = loginBody.getClientId();
|
||||
String grantType = loginBody.getGrantType();
|
||||
SysClientVo client = clientService.queryByClientId(clientId);
|
||||
// 查询不到 client 或 client 内不包含 grantType
|
||||
if (ObjectUtil.isNull(client) || !StringUtils.contains(client.getGrantType(), grantType)) {
|
||||
log.info("客户端id: {} 认证类型:{} 异常!.", clientId, grantType);
|
||||
return R.fail(MessageUtils.message("auth.grant.type.error"));
|
||||
} else if (!SystemConstants.NORMAL.equals(client.getStatus())) {
|
||||
return R.fail(MessageUtils.message("auth.grant.type.blocked"));
|
||||
}
|
||||
// App端登录
|
||||
LoginVo loginVo = IAppAuthStrategy.authenticate(body, client, grantType);
|
||||
|
||||
Long userId = LoginHelper.getUserId();
|
||||
// 记录登录信息
|
||||
appLoginRiskService.checkRisk(userId, LoginHelper.getLoginUser().getIpaddr());
|
||||
appLoginService.recordLoginInfo(userId, LoginHelper.getLoginUser().getIpaddr());
|
||||
|
||||
return R.ok(loginVo);
|
||||
}
|
||||
|
||||
/**
|
||||
* App端退出登录
|
||||
*
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
public R<Void> logout() {
|
||||
appLoginService.logout();
|
||||
return R.ok("退出成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* App端用户注册
|
||||
*
|
||||
* @param user 用户信息
|
||||
* @return 结果
|
||||
*/
|
||||
// TODO: App端暂时不加密,先完善用户端的加密方法在加密
|
||||
// @ApiEncrypt
|
||||
@RateLimiter(key = "register", time = 60, count = 1)
|
||||
@PostMapping("/register")
|
||||
public R<Void> register(@Validated @RequestBody RegisterBody user) {
|
||||
if (!configService.selectRegisterEnabled()) {
|
||||
return R.fail("当前系统没有开启注册功能!");
|
||||
}
|
||||
AppUserBo bo = new AppUserBo();
|
||||
bo.setUserName(user.getUsername());
|
||||
bo.setPassword(user.getPassword());
|
||||
bo.setNickName(user.getUsername());
|
||||
|
||||
// 校验是否已存在
|
||||
if (!appLoginService.checkUserNameUnique(bo.getUserName())) {
|
||||
return R.fail("注册失败,账号已存在");
|
||||
}
|
||||
|
||||
// 执行注册
|
||||
return appLoginService.register(bo) ? R.ok() : R.fail("注册失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取App端配置信息
|
||||
*
|
||||
* @return 配置信息
|
||||
*/
|
||||
@GetMapping("/config")
|
||||
public R<AppConfigVo> getConfig() {
|
||||
AppConfigVo config = new AppConfigVo();
|
||||
config.setRegisterEnabled(configService.selectRegisterEnabled());
|
||||
// 可以添加更多App端特有的配置
|
||||
return R.ok(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录二维码与票据
|
||||
*/
|
||||
@GetMapping("/qrcode")
|
||||
public R<java.util.Map<String,String>> getQrCode() {
|
||||
String ticket = cn.hutool.core.util.IdUtil.fastSimpleUUID();
|
||||
RedisUtils.setCacheObject("APP_LOGIN_QR:" + ticket, "PENDING", java.time.Duration.ofMinutes(5));
|
||||
String url = "/wx/mp/qrcode/url?ticket=" + ticket;
|
||||
java.util.Map<String,String> res = new java.util.HashMap<>();
|
||||
res.put("ticket", ticket);
|
||||
res.put("url", url);
|
||||
return R.ok(res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮询二维码登录状态
|
||||
*/
|
||||
@GetMapping("/qrcode/login")
|
||||
public R<String> pollQrLogin(@RequestParam String ticket) {
|
||||
String key = "APP_LOGIN_QR:" + ticket;
|
||||
String status = RedisUtils.getCacheObject(key);
|
||||
if (status == null) {
|
||||
return R.ok("EXPIRED");
|
||||
}
|
||||
return R.ok(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定第三方账号
|
||||
*
|
||||
* @param body 第三方账号信息
|
||||
* @return 结果
|
||||
*/
|
||||
// TODO: App端暂时不加密,先完善前段的加密方法在加密
|
||||
// @ApiEncrypt
|
||||
@PostMapping("/bind/social")
|
||||
public R<Void> bindSocial(@RequestBody String body) {
|
||||
SocialLoginBody socialLoginBody = JsonUtils.parseObject(body, SocialLoginBody.class);
|
||||
ValidatorUtils.validate(socialLoginBody);
|
||||
|
||||
Long userId = LoginHelper.getUserId();
|
||||
if (userId == null) {
|
||||
return R.fail("请先登录");
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取第三方登录信息
|
||||
AuthResponse<AuthUser> response = SocialUtils.loginAuth(
|
||||
socialLoginBody.getSource(), socialLoginBody.getSocialCode(),
|
||||
socialLoginBody.getSocialState(), socialProperties);
|
||||
AuthUser authUserData = response.getData();
|
||||
// 判断授权响应是否成功
|
||||
if (!response.ok()) {
|
||||
return R.fail(response.getMsg());
|
||||
}
|
||||
// 执行绑定逻辑
|
||||
appSocialAuthService.socialRegister(authUserData);
|
||||
log.info("用户 {} 绑定第三方账号成功,平台: {}", userId, socialLoginBody.getSource());
|
||||
return R.ok("绑定成功");
|
||||
} catch (Exception e) {
|
||||
log.error("绑定第三方账号失败", e);
|
||||
return R.fail("绑定失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑第三方账号
|
||||
*
|
||||
* @param platform 第三方平台
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/unbind/social")
|
||||
public R<Void> unbindSocial(@RequestParam String platform) {
|
||||
Long userId = LoginHelper.getUserId();
|
||||
if (userId == null) {
|
||||
return R.fail("请先登录");
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(platform)) {
|
||||
return R.fail("平台参数不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 查找并删除绑定关系
|
||||
boolean success = appSocialAuthService.deleteByUserIdAndPlatform(userId, platform);
|
||||
if (success) {
|
||||
log.info("用户 {} 解绑第三方账号成功,平台: {}", userId, platform);
|
||||
return R.ok("解绑成功");
|
||||
} else {
|
||||
log.warn("用户 {} 尝试解绑不存在的第三方账号,平台: {}", userId, platform);
|
||||
return R.fail("未找到绑定关系");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("用户 {} 解绑第三方账号失败,平台: {}", userId, platform, e);
|
||||
return R.fail("解绑失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取App端第三方登录跳转URL
|
||||
*
|
||||
* @param source 登录来源
|
||||
* @param domain 域名
|
||||
* @return 结果
|
||||
*/
|
||||
@GetMapping("/binding/{source}")
|
||||
public R<String> authBinding(@PathVariable("source") String source, @RequestParam String domain) {
|
||||
SocialLoginConfigProperties obj = socialProperties.getType().get(source);
|
||||
if (ObjectUtil.isNull(obj)) {
|
||||
return R.fail(source + "平台账号暂不支持");
|
||||
}
|
||||
AuthRequest authRequest = SocialUtils.getAuthRequest(source, socialProperties);
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("domain", domain);
|
||||
map.put("state", AuthStateUtils.createState());
|
||||
String authorizeUrl = authRequest.authorize(Base64.encode(JsonUtils.toJsonString(map), StandardCharsets.UTF_8));
|
||||
return R.ok("操作成功", authorizeUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* App端社交账号绑定回调(需要token)
|
||||
*
|
||||
* @param loginBody 请求体
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/social/binding")
|
||||
public R<Void> socialBinding(@RequestBody SocialLoginBody loginBody) {
|
||||
// 校验token
|
||||
Long userId = LoginHelper.getUserId();
|
||||
if (userId == null) {
|
||||
return R.fail("请先登录");
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取第三方登录信息
|
||||
AuthResponse<AuthUser> response = SocialUtils.loginAuth(
|
||||
loginBody.getSource(), loginBody.getSocialCode(),
|
||||
loginBody.getSocialState(), socialProperties);
|
||||
AuthUser authUserData = response.getData();
|
||||
// 判断授权响应是否成功
|
||||
if (!response.ok()) {
|
||||
return R.fail(response.getMsg());
|
||||
}
|
||||
appSocialAuthService.socialRegister(authUserData);
|
||||
return R.ok("绑定成功");
|
||||
} catch (Exception e) {
|
||||
log.error("社交账号绑定失败", e);
|
||||
return R.fail("绑定失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户绑定的第三方账号列表
|
||||
*
|
||||
* @return 绑定列表
|
||||
*/
|
||||
@GetMapping("/social/list")
|
||||
public R<?> getSocialList() {
|
||||
Long userId = LoginHelper.getUserId();
|
||||
if (userId == null) {
|
||||
return R.fail("请先登录");
|
||||
}
|
||||
|
||||
try {
|
||||
var socialList = appSocialAuthService.queryByUserId(userId);
|
||||
return R.ok(socialList);
|
||||
} catch (Exception e) {
|
||||
log.error("获取第三方账号绑定列表失败", e);
|
||||
return R.fail("获取失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.dromara.app.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.app.session.AppSession;
|
||||
import org.dromara.app.session.AppSessionManager;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/app/session")
|
||||
public class AppSessionController {
|
||||
|
||||
private final AppSessionManager sessionManager;
|
||||
|
||||
@GetMapping("/active")
|
||||
@SaCheckLogin
|
||||
public R<List<AppSession>> active() {
|
||||
Long userId = LoginHelper.getUserId();
|
||||
return R.ok(sessionManager.list(userId));
|
||||
}
|
||||
|
||||
@PostMapping("/terminate")
|
||||
@SaCheckLogin
|
||||
public R<Boolean> terminate(@RequestParam String sessionId) {
|
||||
Long userId = LoginHelper.getUserId();
|
||||
return R.ok(sessionManager.terminate(userId, sessionId));
|
||||
}
|
||||
|
||||
@PostMapping("/heartbeat")
|
||||
@SaCheckLogin
|
||||
public R<Boolean> heartbeat(@RequestParam String sessionId) {
|
||||
Long userId = LoginHelper.getUserId();
|
||||
return R.ok(sessionManager.heartbeat(userId, sessionId, Duration.ofDays(30)));
|
||||
}
|
||||
|
||||
@GetMapping("/sse")
|
||||
@SaCheckLogin
|
||||
public SseEmitter sse() {
|
||||
Long userId = LoginHelper.getUserId();
|
||||
SseEmitter emitter = new SseEmitter(Duration.ofHours(1).toMillis());
|
||||
String ch = "APP:SESS_EVT:" + userId;
|
||||
org.dromara.common.redis.utils.RedisUtils.subscribe(ch, String.class, msg -> {
|
||||
try {
|
||||
emitter.send(SseEmitter.event().name("session").data(msg));
|
||||
} catch (IOException e) {
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
});
|
||||
return emitter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package org.dromara.app.domain;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.dromara.common.core.domain.model.LoginUser;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* App登录用户身份权限
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@NoArgsConstructor
|
||||
public class AppLoginUser extends LoginUser {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
private Long deptId;
|
||||
|
||||
/**
|
||||
* 部门名
|
||||
*/
|
||||
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 Boolean infoComplete;
|
||||
|
||||
/**
|
||||
* 获取登录id
|
||||
*/
|
||||
@Override
|
||||
public String getLoginId() {
|
||||
if (userType == null) {
|
||||
throw new IllegalArgumentException("用户类型不能为空");
|
||||
}
|
||||
if (userId == null) {
|
||||
throw new IllegalArgumentException("用户ID不能为空");
|
||||
}
|
||||
return userType + ":" + userId;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.dromara.app.domain.vo;
|
||||
|
||||
public class AppConfigVo {
|
||||
private boolean registerEnabled;
|
||||
|
||||
public boolean isRegisterEnabled() {
|
||||
return registerEnabled;
|
||||
}
|
||||
|
||||
public void setRegisterEnabled(boolean registerEnabled) {
|
||||
this.registerEnabled = registerEnabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package org.dromara.app.listener;
|
||||
|
||||
import cn.dev33.satoken.listener.SaTokenListener;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.http.useragent.UserAgent;
|
||||
import cn.hutool.http.useragent.UserAgentUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.app.service.AppLoginService;
|
||||
import org.dromara.common.core.constant.CacheConstants;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.domain.dto.UserOnlineDTO;
|
||||
import org.dromara.common.core.utils.MessageUtils;
|
||||
import org.dromara.common.core.utils.ServletUtils;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.core.utils.ip.AddressUtils;
|
||||
import org.dromara.common.log.event.LogininforEvent;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* App用户行为 侦听器的实现
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
@Slf4j
|
||||
public class AppUserActionListener implements SaTokenListener {
|
||||
|
||||
private final AppLoginService appLoginService;
|
||||
|
||||
/**
|
||||
* 每次登录时触发
|
||||
*/
|
||||
@Override
|
||||
public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) {
|
||||
UserAgent userAgent = UserAgentUtil.parse(ServletUtils.getRequest().getHeader("User-Agent"));
|
||||
String ip = ServletUtils.getClientIP();
|
||||
UserOnlineDTO dto = new UserOnlineDTO();
|
||||
dto.setIpaddr(ip);
|
||||
dto.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
|
||||
dto.setBrowser(userAgent.getBrowser().getName());
|
||||
dto.setOs(userAgent.getOs().getName());
|
||||
dto.setLoginTime(System.currentTimeMillis());
|
||||
dto.setTokenId(tokenValue);
|
||||
String username = (String) loginParameter.getExtra(LoginHelper.USER_NAME_KEY);
|
||||
dto.setUserName(username);
|
||||
dto.setClientKey((String) loginParameter.getExtra(LoginHelper.CLIENT_KEY));
|
||||
dto.setDeviceType(loginParameter.getDeviceType());
|
||||
dto.setDeptName((String) loginParameter.getExtra(LoginHelper.DEPT_NAME_KEY));
|
||||
if (loginParameter.getTimeout() == -1) {
|
||||
RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto);
|
||||
} else {
|
||||
RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto, Duration.ofSeconds(loginParameter.getTimeout()));
|
||||
}
|
||||
// 记录登录日志
|
||||
LogininforEvent logininforEvent = new LogininforEvent();
|
||||
logininforEvent.setUsername(username);
|
||||
logininforEvent.setStatus(Constants.LOGIN_SUCCESS);
|
||||
logininforEvent.setMessage(MessageUtils.message("user.login.success"));
|
||||
logininforEvent.setRequest(ServletUtils.getRequest());
|
||||
SpringUtils.context().publishEvent(logininforEvent);
|
||||
// 更新App用户登录信息
|
||||
appLoginService.recordLoginInfo((Long) loginParameter.getExtra(LoginHelper.USER_KEY), ip);
|
||||
log.info("app user doLogin, userId:{}, token:{}", loginId, tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次注销时触发
|
||||
*/
|
||||
@Override
|
||||
public void doLogout(String loginType, Object loginId, String tokenValue) {
|
||||
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
|
||||
log.info("app user doLogout, userId:{}, token:{}", loginId, tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次被踢下线时触发
|
||||
*/
|
||||
@Override
|
||||
public void doKickout(String loginType, Object loginId, String tokenValue) {
|
||||
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
|
||||
log.info("app user doKickout, userId:{}, token:{}", loginId, tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次被顶下线时触发
|
||||
*/
|
||||
@Override
|
||||
public void doReplaced(String loginType, Object loginId, String tokenValue) {
|
||||
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
|
||||
log.info("app user doReplaced, userId:{}, token:{}", loginId, tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次被封禁时触发
|
||||
*/
|
||||
@Override
|
||||
public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) {
|
||||
log.info("App用户被禁用: loginType={}, loginId={}, service={}, level={}, disableTime={}",
|
||||
loginType, loginId, service, level, disableTime);
|
||||
|
||||
// 删除在线用户信息
|
||||
String tokenValue = StpUtil.getTokenValueByLoginId(loginId);
|
||||
if (StringUtils.isNotBlank(tokenValue)) {
|
||||
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次被解封时触发
|
||||
*/
|
||||
@Override
|
||||
public void doUntieDisable(String loginType, Object loginId, String service) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次打开二级认证时触发
|
||||
*/
|
||||
@Override
|
||||
public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次关闭二级认证时触发
|
||||
*/
|
||||
@Override
|
||||
public void doCloseSafe(String loginType, String tokenValue, String service) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次创建Session时触发
|
||||
*/
|
||||
@Override
|
||||
public void doCreateSession(String id) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次注销Session时触发
|
||||
*/
|
||||
@Override
|
||||
public void doLogoutSession(String id) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次Token续期时触发
|
||||
*/
|
||||
@Override
|
||||
public void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.dromara.app.service;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.member.domain.vo.AppUserVo;
|
||||
import org.dromara.common.core.utils.ip.AddressUtils;
|
||||
import org.dromara.common.mail.utils.MailUtils;
|
||||
import org.dromara.member.service.AppUserService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* App登录异地风险检测服务
|
||||
* 比对用户上次登录IP归属地与本次登录IP归属地,若发生变化则进行日志与邮件告警。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AppLoginRiskService {
|
||||
|
||||
private final AppUserService appUserService;
|
||||
|
||||
/**
|
||||
* 执行异地登录风险检测
|
||||
* @param userId 当前登录用户ID
|
||||
* @param currentIp 本次登录IP
|
||||
*/
|
||||
public void checkRisk(Long userId, String currentIp) {
|
||||
AppUserVo user = appUserService.queryById(userId);
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
String lastIp = StrUtil.blankToDefault(user.getLoginIp(), "");
|
||||
String lastRegion = AddressUtils.getRealAddressByIP(lastIp);
|
||||
String currentRegion = AddressUtils.getRealAddressByIP(currentIp);
|
||||
boolean validLast = !AddressUtils.UNKNOWN_IP.equals(lastRegion) && !AddressUtils.UNKNOWN_ADDRESS.equals(lastRegion);
|
||||
boolean validCurrent = !AddressUtils.UNKNOWN_IP.equals(currentRegion) && !AddressUtils.UNKNOWN_ADDRESS.equals(currentRegion);
|
||||
boolean isLocal = AddressUtils.LOCAL_ADDRESS.equals(lastRegion) || AddressUtils.LOCAL_ADDRESS.equals(currentRegion);
|
||||
if (validLast && validCurrent && !isLocal && !StrUtil.equals(lastRegion, currentRegion)) {
|
||||
log.warn("App用户 {} 异地登录风险, 上次:{}({}), 本次:{}({})", userId, lastRegion, lastIp, currentRegion, currentIp);
|
||||
String email = user.getEmail();
|
||||
if (StrUtil.isNotBlank(email)) {
|
||||
String subject = "异地登录告警";
|
||||
String content = "您的账号检测到异地登录风险\n上次登录: " + lastRegion + " (" + lastIp + ")\n本次登录: " + currentRegion + " (" + currentIp + ")";
|
||||
try {
|
||||
MailUtils.sendText(email, subject, content);
|
||||
} catch (Exception e) {
|
||||
log.error("发送异地登录告警邮件失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package org.dromara.app.service;
|
||||
|
||||
import cn.dev33.satoken.exception.NotLoginException;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.app.domain.AppLoginUser;
|
||||
import org.dromara.member.domain.AppUser;
|
||||
import org.dromara.member.domain.bo.AppUserBo;
|
||||
import org.dromara.common.core.constant.CacheConstants;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.enums.LoginType;
|
||||
import org.dromara.common.core.exception.user.UserException;
|
||||
import org.dromara.common.core.utils.MessageUtils;
|
||||
import org.dromara.common.core.utils.ServletUtils;
|
||||
import org.dromara.common.log.event.LogininforEvent;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.member.service.AppUserService;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* App登录校验方法
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AppLoginService {
|
||||
|
||||
private final AppUserService appUserService;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
@Value("${user.password.maxRetryCount}")
|
||||
private Integer maxRetryCount;
|
||||
@Value("${user.password.lockTime}")
|
||||
private Integer lockTime;
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
public void logout() {
|
||||
try {
|
||||
AppLoginUser loginUser = LoginHelper.getLoginUser();
|
||||
if (ObjectUtil.isNotNull(loginUser)) {
|
||||
StpUtil.logout();
|
||||
recordLogininfor(loginUser.getUsername(), Constants.LOGOUT, MessageUtils.message("user.logout.success"));
|
||||
}
|
||||
} catch (NotLoginException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册
|
||||
*/
|
||||
public Boolean register(AppUserBo bo) {
|
||||
return appUserService.register(bo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验用户名唯一
|
||||
*/
|
||||
public boolean checkUserNameUnique(String userName) {
|
||||
return appUserService.checkUserNameUnique(userName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录登录信息
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param status 状态
|
||||
* @param message 消息内容
|
||||
*/
|
||||
public void recordLogininfor(String username, String status, String message) {
|
||||
LogininforEvent logininforEvent = new LogininforEvent();
|
||||
logininforEvent.setUsername(username);
|
||||
logininforEvent.setStatus(status);
|
||||
logininforEvent.setMessage(message);
|
||||
logininforEvent.setRequest(ServletUtils.getRequest());
|
||||
eventPublisher.publishEvent(logininforEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建登录用户
|
||||
*/
|
||||
public AppLoginUser buildLoginUser(AppUser appUser) {
|
||||
AppLoginUser loginUser = new AppLoginUser();
|
||||
loginUser.setUserId(appUser.getUserId());
|
||||
loginUser.setUsername(appUser.getUserName());
|
||||
loginUser.setUserType("app");
|
||||
loginUser.setInfoComplete(StringUtils.isNotBlank(appUser.getPhone()));
|
||||
// 可以根据需要添加更多字段
|
||||
return loginUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录登录信息
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param ip IP地址
|
||||
*/
|
||||
public void recordLoginInfo(Long userId, String ip) {
|
||||
AppUser appUser = new AppUser();
|
||||
appUser.setUserId(userId);
|
||||
appUser.setLoginIp(ip);
|
||||
appUser.setLoginDate(LocalDateTime.now());
|
||||
appUserService.updateById(appUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录校验
|
||||
*/
|
||||
public void checkLogin(LoginType loginType, String username, Supplier<Boolean> supplier) {
|
||||
String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username;
|
||||
String loginFail = Constants.LOGIN_FAIL;
|
||||
|
||||
// 获取用户登录错误次数,默认为0 (可自定义限制策略 例如: key + username + ip)
|
||||
int errorNumber = ObjectUtil.defaultIfNull(RedisUtils.getCacheObject(errorKey), 0);
|
||||
// 锁定时间内登录 则踢出
|
||||
if (errorNumber >= maxRetryCount) {
|
||||
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
|
||||
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
|
||||
}
|
||||
|
||||
if (supplier.get()) {
|
||||
// 是否第一次
|
||||
errorNumber++;
|
||||
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
|
||||
// 达到规定错误次数 则锁定登录
|
||||
if (errorNumber >= maxRetryCount) {
|
||||
RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
|
||||
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
|
||||
} else {
|
||||
RedisUtils.setCacheObject(errorKey, errorNumber);
|
||||
throw new UserException(loginType.getRetryLimitCount(), errorNumber);
|
||||
}
|
||||
}
|
||||
|
||||
// 登录成功 清空错误次数
|
||||
RedisUtils.deleteObject(errorKey);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.dromara.app.service;
|
||||
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
|
||||
/**
|
||||
* App认证策略
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
public interface IAppAuthStrategy {
|
||||
|
||||
String BASE_NAME = "AppAuthStrategy";
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*
|
||||
* @param body 登录对象
|
||||
* @param client 授权管理视图对象
|
||||
* @param grantType 授权类型
|
||||
* @return 登录验证信息
|
||||
*/
|
||||
static LoginVo authenticate(String body, SysClientVo client, String grantType) {
|
||||
// 授权类型和客户端id
|
||||
String beanName = grantType + BASE_NAME;
|
||||
if (!SpringUtils.containsBean(beanName)) {
|
||||
throw new ServiceException("授权类型不正确!");
|
||||
}
|
||||
IAppAuthStrategy instance = SpringUtils.getBean(beanName);
|
||||
return instance.login(body, client);
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*
|
||||
* @param body 登录对象
|
||||
* @param client 授权管理视图对象
|
||||
* @return 登录验证信息
|
||||
*/
|
||||
LoginVo login(String body, SysClientVo client);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package org.dromara.app.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.app.domain.AppLoginUser;
|
||||
import org.dromara.member.domain.vo.AppUserVo;
|
||||
import org.dromara.app.service.AppLoginService;
|
||||
import org.dromara.member.service.AppUserService;
|
||||
import org.dromara.app.service.IAppAuthStrategy;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.constant.GlobalConstants;
|
||||
import org.dromara.common.core.domain.model.EmailLoginBody;
|
||||
import org.dromara.common.core.enums.LoginType;
|
||||
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.json.utils.JsonUtils;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* App邮箱认证策略
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("app_email" + IAppAuthStrategy.BASE_NAME)
|
||||
@RequiredArgsConstructor
|
||||
public class AppEmailAuthStrategy implements IAppAuthStrategy {
|
||||
|
||||
private final AppLoginService appLoginService;
|
||||
private final AppUserService appUserService;
|
||||
|
||||
@Override
|
||||
public LoginVo login(String body, SysClientVo client) {
|
||||
EmailLoginBody loginBody = JsonUtils.parseObject(body, EmailLoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
String email = loginBody.getEmail();
|
||||
String emailCode = loginBody.getEmailCode();
|
||||
|
||||
AppUserVo user = loadUserByEmail(email);
|
||||
appLoginService.checkLogin(LoginType.EMAIL, user.getUserName(), () -> !validateEmailCode(email, emailCode));
|
||||
// 构建App登录用户
|
||||
AppLoginUser loginUser = new AppLoginUser();
|
||||
loginUser.setUserId(user.getUserId());
|
||||
loginUser.setUsername(user.getUserName());
|
||||
loginUser.setUserType("app");
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验邮箱验证码
|
||||
*/
|
||||
private boolean validateEmailCode(String email, String emailCode) {
|
||||
String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + email);
|
||||
if (StringUtils.isBlank(code)) {
|
||||
appLoginService.recordLogininfor(email, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
|
||||
throw new CaptchaExpireException();
|
||||
}
|
||||
return code.equals(emailCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过邮箱加载用户信息
|
||||
*/
|
||||
private AppUserVo loadUserByEmail(String email) {
|
||||
AppUserVo appUser = appUserService.selectUserByEmail(email);
|
||||
if (ObjectUtil.isNull(appUser)) {
|
||||
log.info("登录用户:{} 不存在.", email);
|
||||
throw new UserException("user.not.exists", email);
|
||||
} else if ("1".equals(appUser.getStatus())) {
|
||||
log.info("登录用户:{} 已被停用.", email);
|
||||
throw new UserException("user.blocked", email);
|
||||
}
|
||||
// 将 AppUser 转换为 AppUserVo
|
||||
AppUserVo user = new AppUserVo();
|
||||
BeanUtil.copyProperties(appUser, user);
|
||||
return user;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package org.dromara.app.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.crypto.digest.BCrypt;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.app.domain.AppLoginUser;
|
||||
import org.dromara.member.domain.AppUser;
|
||||
import org.dromara.member.domain.vo.AppUserVo;
|
||||
import org.dromara.app.service.AppLoginService;
|
||||
import org.dromara.member.service.AppUserService;
|
||||
import org.dromara.app.service.IAppAuthStrategy;
|
||||
import org.dromara.app.session.AppSession;
|
||||
import org.dromara.app.session.AppSessionManager;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.constant.GlobalConstants;
|
||||
import org.dromara.common.core.domain.model.PasswordLoginBody;
|
||||
import org.dromara.common.core.enums.LoginType;
|
||||
import org.dromara.common.core.exception.user.CaptchaException;
|
||||
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.ServletUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.core.utils.ValidatorUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.common.web.config.properties.CaptchaProperties;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* App密码认证策略
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("app_password" + IAppAuthStrategy.BASE_NAME)
|
||||
@RequiredArgsConstructor
|
||||
public class AppPasswordAuthStrategy implements IAppAuthStrategy {
|
||||
|
||||
private final CaptchaProperties captchaProperties;
|
||||
private final AppLoginService appLoginService;
|
||||
private final AppUserService appUserService;
|
||||
private final AppSessionManager appSessionManager;
|
||||
|
||||
@Override
|
||||
public LoginVo login(String body, SysClientVo client) {
|
||||
PasswordLoginBody loginBody = JsonUtils.parseObject(body, PasswordLoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
String username = loginBody.getUsername();
|
||||
String password = loginBody.getPassword();
|
||||
String code = loginBody.getCode();
|
||||
String uuid = loginBody.getUuid();
|
||||
|
||||
boolean captchaEnabled = captchaProperties.getEnable();
|
||||
// 验证码开关
|
||||
// if (captchaEnabled) {
|
||||
// validateCaptcha(username, code, uuid);
|
||||
// }
|
||||
|
||||
AppUser appUser = loadUserByUsername(username);
|
||||
appLoginService.checkLogin(LoginType.PASSWORD, username, () -> !BCrypt.checkpw(password, appUser.getPassword()));
|
||||
|
||||
// 创建App登录用户
|
||||
AppLoginUser loginUser = buildAppLoginUser(appUser);
|
||||
|
||||
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());
|
||||
|
||||
// 注册会话
|
||||
try {
|
||||
String ua = ServletUtils.getRequest().getHeader("User-Agent");
|
||||
String deviceId = ServletUtils.getRequest().getHeader("X-Device-Id");
|
||||
String ip = LoginHelper.getLoginUser().getIpaddr();
|
||||
AppSession s = new AppSession();
|
||||
s.setSessionId(StpUtil.getTokenValue());
|
||||
s.setUserId(loginUser.getUserId());
|
||||
s.setDeviceType(client.getDeviceType());
|
||||
s.setDeviceId(deviceId != null ? deviceId : UUID.randomUUID().toString());
|
||||
s.setUserAgent(ua);
|
||||
s.setIp(ip);
|
||||
s.setLocation("");
|
||||
long now = Instant.now().toEpochMilli();
|
||||
s.setLoginAt(now);
|
||||
Long ttlSec = StpUtil.getTokenTimeout();
|
||||
s.setExpireAt(now + (ttlSec != null ? ttlSec * 1000 : 24 * 3600 * 1000));
|
||||
s.setActive(true);
|
||||
s.setToken(StpUtil.getTokenValue());
|
||||
appSessionManager.register(s, Duration.ofSeconds(ttlSec != null ? ttlSec : 86400));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return loginVo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验验证码
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param code 验证码
|
||||
* @param uuid 唯一标识
|
||||
*/
|
||||
private void validateCaptcha(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) {
|
||||
appLoginService.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
|
||||
throw new CaptchaExpireException();
|
||||
}
|
||||
if (!code.equalsIgnoreCase(captcha)) {
|
||||
appLoginService.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
|
||||
throw new CaptchaException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户名加载App用户
|
||||
*/
|
||||
private AppUser loadUserByUsername(String username) {
|
||||
AppUserVo user = appUserService.selectUserByUserName(username);
|
||||
if (ObjectUtil.isNull(user)) {
|
||||
log.info("登录用户:{} 不存在.", username);
|
||||
throw new UserException("user.not.exists", username);
|
||||
} else if ("1".equals(user.getStatus())) {
|
||||
log.info("登录用户:{} 已被停用.", username);
|
||||
throw new UserException("user.blocked", username);
|
||||
}
|
||||
return JsonUtils.parseObject(JsonUtils.toJsonString(user), AppUser.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建App登录用户
|
||||
*/
|
||||
private AppLoginUser buildAppLoginUser(AppUser appUser) {
|
||||
AppLoginUser loginUser = new AppLoginUser();
|
||||
loginUser.setUserId(appUser.getUserId());
|
||||
loginUser.setUsername(appUser.getUserName());
|
||||
loginUser.setUserType("app");
|
||||
// 设置其他必要字段
|
||||
return loginUser;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package org.dromara.app.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.app.domain.AppLoginUser;
|
||||
import org.dromara.member.domain.vo.AppUserVo;
|
||||
import org.dromara.app.service.AppLoginService;
|
||||
import org.dromara.member.service.AppUserService;
|
||||
import org.dromara.app.service.IAppAuthStrategy;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.constant.GlobalConstants;
|
||||
import org.dromara.common.core.domain.model.SmsLoginBody;
|
||||
import org.dromara.common.core.enums.LoginType;
|
||||
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.json.utils.JsonUtils;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* App短信认证策略
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("app_sms" + IAppAuthStrategy.BASE_NAME)
|
||||
@RequiredArgsConstructor
|
||||
public class AppSmsAuthStrategy implements IAppAuthStrategy {
|
||||
|
||||
private final AppLoginService appLoginService;
|
||||
private final AppUserService appUserService;
|
||||
|
||||
@Override
|
||||
public LoginVo login(String body, SysClientVo client) {
|
||||
SmsLoginBody loginBody = JsonUtils.parseObject(body, SmsLoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
String phonenumber = loginBody.getPhonenumber();
|
||||
String smsCode = loginBody.getSmsCode();
|
||||
|
||||
AppUserVo user = loadUserByPhonenumber(phonenumber);
|
||||
appLoginService.checkLogin(LoginType.SMS, user.getUserName(), () -> !validateSmsCode(phonenumber, smsCode));
|
||||
// 构建App登录用户
|
||||
AppLoginUser loginUser = new AppLoginUser();
|
||||
loginUser.setUserId(user.getUserId());
|
||||
loginUser.setUsername(user.getUserName());
|
||||
loginUser.setUserType("app");
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验短信验证码
|
||||
*/
|
||||
private boolean validateSmsCode(String phonenumber, String smsCode) {
|
||||
String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + phonenumber);
|
||||
if (StringUtils.isBlank(code)) {
|
||||
appLoginService.recordLogininfor(phonenumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
|
||||
throw new CaptchaExpireException();
|
||||
}
|
||||
return code.equals(smsCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过手机号加载用户信息
|
||||
*/
|
||||
private AppUserVo loadUserByPhonenumber(String phonenumber) {
|
||||
AppUserVo appUser = appUserService.selectUserByPhone(phonenumber);
|
||||
if (ObjectUtil.isNull(appUser)) {
|
||||
log.info("登录用户:{} 不存在.", phonenumber);
|
||||
throw new UserException("user.not.exists", phonenumber);
|
||||
} else if ("1".equals(appUser.getStatus())) {
|
||||
log.info("登录用户:{} 已被停用.", phonenumber);
|
||||
throw new UserException("user.blocked", phonenumber);
|
||||
}
|
||||
// 将 AppUser 转换为 AppUserVo
|
||||
AppUserVo user = new AppUserVo();
|
||||
BeanUtil.copyProperties(appUser, user);
|
||||
if (ObjectUtil.isNull(user)) {
|
||||
log.info("登录用户:{} 不存在.", phonenumber);
|
||||
throw new UserException("user.not.exists", phonenumber);
|
||||
} else if ("1".equals(user.getStatus())) {
|
||||
log.info("登录用户:{} 已被停用.", phonenumber);
|
||||
throw new UserException("user.blocked", phonenumber);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package org.dromara.app.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.zhyd.oauth.model.AuthCallback;
|
||||
import me.zhyd.oauth.model.AuthResponse;
|
||||
import me.zhyd.oauth.model.AuthUser;
|
||||
import me.zhyd.oauth.request.AuthRequest;
|
||||
import org.dromara.app.domain.AppLoginUser;
|
||||
import org.dromara.member.domain.AppSocialAuth;
|
||||
import org.dromara.member.domain.vo.AppUserVo;
|
||||
import org.dromara.app.service.AppLoginService;
|
||||
import org.dromara.member.service.AppSocialAuthService;
|
||||
import org.dromara.member.service.AppUserService;
|
||||
import org.dromara.app.service.IAppAuthStrategy;
|
||||
import org.dromara.common.core.domain.model.SocialLoginBody;
|
||||
import org.dromara.common.core.enums.LoginType;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.ValidatorUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.common.social.config.properties.SocialProperties;
|
||||
import org.dromara.common.social.utils.SocialUtils;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* App第三方登录认证策略
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("app_social" + IAppAuthStrategy.BASE_NAME)
|
||||
@RequiredArgsConstructor
|
||||
public class AppSocialAuthStrategy implements IAppAuthStrategy {
|
||||
|
||||
private final SocialProperties socialProperties;
|
||||
private final AppUserService appUserService;
|
||||
private final AppLoginService appLoginService;
|
||||
private final AppSocialAuthService appSocialAuthService;
|
||||
|
||||
@Override
|
||||
public LoginVo login(String body, SysClientVo client) {
|
||||
SocialLoginBody loginBody = JsonUtils.parseObject(body, SocialLoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
String source = loginBody.getSource();
|
||||
String socialCode = loginBody.getSocialCode();
|
||||
String socialState = loginBody.getSocialState();
|
||||
// 获取第三方登录信息
|
||||
AuthRequest authRequest = SocialUtils.getAuthRequest(source, socialProperties);
|
||||
AuthCallback callback = AuthCallback.builder()
|
||||
.code(socialCode)
|
||||
.state(socialState)
|
||||
.build();
|
||||
AuthResponse<AuthUser> response = authRequest.login(callback);
|
||||
if (!response.ok()) {
|
||||
throw new ServiceException(response.getMsg());
|
||||
}
|
||||
AuthUser authUserData = response.getData();
|
||||
|
||||
AppUserVo user = loadUserBySocial(authUserData.getSource(), authUserData.getUuid());
|
||||
assert user != null;
|
||||
appLoginService.checkLogin(LoginType.PASSWORD, user.getUserName(), () -> ObjectUtil.isNull(user));
|
||||
// 构建App登录用户
|
||||
AppLoginUser loginUser = new AppLoginUser();
|
||||
loginUser.setUserId(user.getUserId());
|
||||
loginUser.setUsername(user.getUserName());
|
||||
loginUser.setUserType("app");
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过第三方信息查找用户
|
||||
*/
|
||||
private AppUserVo loadUserBySocial(String source, String openId) {
|
||||
// 根据第三方平台信息查找绑定的App用户
|
||||
log.info("App端第三方登录查找用户,source: {}, openId: {}", source, openId);
|
||||
|
||||
// 首先通过openId查找
|
||||
AppSocialAuth socialAuth = appSocialAuthService.queryByPlatformAndOpenId(source, openId);
|
||||
|
||||
if (socialAuth != null && socialAuth.getUserId() != null) {
|
||||
// 找到绑定的用户,返回用户信息
|
||||
AppUserVo user = appUserService.queryById(socialAuth.getUserId());
|
||||
if (user != null) {
|
||||
log.info("通过openId找到绑定用户,userId: {}, userName: {}", user.getUserId(), user.getUserName());
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
log.warn("未找到与第三方账号绑定的App用户,source: {}, openId: {}", source, openId);
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package org.dromara.app.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.zhyd.oauth.model.AuthCallback;
|
||||
import me.zhyd.oauth.model.AuthResponse;
|
||||
import me.zhyd.oauth.model.AuthToken;
|
||||
import me.zhyd.oauth.model.AuthUser;
|
||||
import me.zhyd.oauth.request.AuthRequest;
|
||||
import org.dromara.app.domain.AppLoginUser;
|
||||
import org.dromara.member.domain.vo.AppUserVo;
|
||||
import org.dromara.member.service.AppSocialAuthService;
|
||||
import org.dromara.app.service.IAppAuthStrategy;
|
||||
import org.dromara.common.core.domain.model.XcxLoginBody;
|
||||
import org.dromara.common.core.exception.user.UserException;
|
||||
import org.dromara.common.core.utils.ValidatorUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.common.social.config.properties.SocialProperties;
|
||||
import org.dromara.common.social.utils.SocialUtils;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* App端微信小程序认证策略
|
||||
* 支持微信小程序登录,通过微信小程序授权码获取用户信息并完成登录
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("app_wechat_miniprogram" + IAppAuthStrategy.BASE_NAME)
|
||||
@RequiredArgsConstructor
|
||||
public class AppWechatMiniprogramAuthStrategy implements IAppAuthStrategy {
|
||||
|
||||
private final SocialProperties socialProperties;
|
||||
private final AppSocialAuthService appSocialAuthService;
|
||||
|
||||
@Override
|
||||
public LoginVo login(String body, SysClientVo client) {
|
||||
XcxLoginBody loginBody = JsonUtils.parseObject(body, XcxLoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
|
||||
// xcxCode 为小程序调用 wx.login 授权后获取
|
||||
String xcxCode = loginBody.getCode();
|
||||
// 多个小程序识别使用
|
||||
String appid = loginBody.getAppid();
|
||||
|
||||
// 校验 appid + appsecret + xcxCode 调用登录凭证校验接口 获取 session_key 与 openid
|
||||
// 使用SocialUtils获取微信小程序的AuthRequest
|
||||
AuthRequest authRequest = SocialUtils.getAuthRequest("wechat_miniprogram", socialProperties);
|
||||
if (authRequest == null) {
|
||||
throw new UserException("未配置微信小程序登录");
|
||||
}
|
||||
AuthCallback authCallback = new AuthCallback();
|
||||
authCallback.setCode(xcxCode);
|
||||
AuthResponse<AuthUser> response = authRequest.login(authCallback);
|
||||
|
||||
String openid = null;
|
||||
String unionId = null;
|
||||
if (response.ok()) {
|
||||
AuthToken token = response.getData().getToken();
|
||||
openid = token.getOpenId();
|
||||
// 微信小程序只有关联到微信开放平台下之后才能获取到 unionId,因此unionId不一定能返回。
|
||||
unionId = token.getUnionId();
|
||||
} else {
|
||||
throw new UserException("第三方登录失败,原因:" + response.getMsg());
|
||||
}
|
||||
|
||||
AppUserVo user = loadUserByWxId("wechat_miniprogram", openid, unionId);
|
||||
if (ObjectUtil.isNull(user)) {
|
||||
throw new UserException("登录失败,请先绑定用户账号");
|
||||
}
|
||||
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||
AppLoginUser loginUser = new AppLoginUser();
|
||||
loginUser.setUserId(user.getUserId());
|
||||
loginUser.setUsername(user.getUserName());
|
||||
loginUser.setNickname(user.getNickName());
|
||||
loginUser.setUserType("app");
|
||||
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());
|
||||
loginVo.setOpenid(openid);
|
||||
return loginVo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过微信标识加载用户信息,优先 unionId,回退 openId
|
||||
*/
|
||||
private AppUserVo loadUserByWxId(String platform, String openid, String unionId) {
|
||||
if (ObjectUtil.isNotEmpty(unionId)) {
|
||||
AppUserVo byUnion = appSocialAuthService.selectUserByPlatformAndUnionId(platform, unionId);
|
||||
if (ObjectUtil.isNotNull(byUnion)) {
|
||||
return byUnion;
|
||||
}
|
||||
}
|
||||
return appSocialAuthService.selectUserByPlatformAndOpenId(platform, openid);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package org.dromara.app.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.zhyd.oauth.model.AuthCallback;
|
||||
import me.zhyd.oauth.model.AuthResponse;
|
||||
import me.zhyd.oauth.model.AuthUser;
|
||||
import me.zhyd.oauth.request.AuthRequest;
|
||||
import org.dromara.app.domain.AppLoginUser;
|
||||
import org.dromara.member.domain.vo.AppUserVo;
|
||||
import org.dromara.member.service.AppSocialAuthService;
|
||||
import org.dromara.app.service.IAppAuthStrategy;
|
||||
import org.dromara.common.core.domain.model.SocialLoginBody;
|
||||
import org.dromara.common.core.exception.user.UserException;
|
||||
import org.dromara.common.core.utils.ValidatorUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.common.social.config.properties.SocialProperties;
|
||||
import org.dromara.common.social.utils.SocialUtils;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* App端微信公众号认证策略
|
||||
* 处理微信公众号环境下的OAuth2登录逻辑
|
||||
* 依赖 justauth.type.wechat_mp 配置项
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("app_wechat_mp" + IAppAuthStrategy.BASE_NAME)
|
||||
@RequiredArgsConstructor
|
||||
public class AppWechatMpAuthStrategy implements IAppAuthStrategy {
|
||||
|
||||
private final SocialProperties socialProperties;
|
||||
private final AppSocialAuthService appSocialAuthService;
|
||||
|
||||
/**
|
||||
* 微信公众号登录核心逻辑
|
||||
*
|
||||
* @param body 登录请求体,包含 socialCode 和 socialState
|
||||
* @param client 客户端配置
|
||||
* @return 登录结果 LoginVo
|
||||
*/
|
||||
@Override
|
||||
public LoginVo login(String body, SysClientVo client) {
|
||||
SocialLoginBody loginBody = JsonUtils.parseObject(body, SocialLoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
|
||||
String code = loginBody.getSocialCode();
|
||||
String state = loginBody.getSocialState();
|
||||
|
||||
// 使用SocialUtils获取微信公众号的AuthRequest
|
||||
// 对应 application.yml 中 justauth.type.wechat_mp 的配置
|
||||
AuthRequest authRequest = SocialUtils.getAuthRequest("wechat_mp", socialProperties);
|
||||
if (authRequest == null) {
|
||||
throw new UserException("未配置微信公众号登录");
|
||||
}
|
||||
|
||||
// 构造回调参数
|
||||
AuthCallback authCallback = new AuthCallback();
|
||||
authCallback.setCode(code);
|
||||
authCallback.setState(state);
|
||||
// 执行第三方登录请求,换取 Access Token 和 User Info
|
||||
AuthResponse<AuthUser> response = authRequest.login(authCallback);
|
||||
|
||||
String openid;
|
||||
String unionId = null;
|
||||
if (response.ok()) {
|
||||
AuthUser authUser = response.getData();
|
||||
openid = authUser.getUuid();
|
||||
if (authUser.getToken() != null) {
|
||||
unionId = authUser.getToken().getUnionId();
|
||||
}
|
||||
} else {
|
||||
throw new UserException("第三方登录失败,原因:" + response.getMsg());
|
||||
}
|
||||
|
||||
// 根据 OpenId 或 UnionId 查找系统用户
|
||||
AppUserVo user = loadUserByWxId("wechat_mp", openid, unionId);
|
||||
if (ObjectUtil.isNull(user)) {
|
||||
throw new UserException("登录失败,请先绑定用户账号");
|
||||
}
|
||||
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||
AppLoginUser loginUser = new AppLoginUser();
|
||||
loginUser.setUserId(user.getUserId());
|
||||
loginUser.setUsername(user.getUserName());
|
||||
loginUser.setNickname(user.getNickName());
|
||||
loginUser.setUserType("app");
|
||||
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());
|
||||
loginVo.setOpenid(openid);
|
||||
return loginVo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过微信标识加载用户信息,优先使用 unionId(跨应用统一标识),缺失时回退使用 openId
|
||||
* @param platform 平台标识(wechat_mp / wechat_open / wechat_miniprogram)
|
||||
* @param openid 微信 openId
|
||||
* @param unionId 微信 unionId(可能为空)
|
||||
*/
|
||||
private AppUserVo loadUserByWxId(String platform, String openid, String unionId) {
|
||||
if (ObjectUtil.isNotEmpty(unionId)) {
|
||||
AppUserVo byUnion = appSocialAuthService.selectUserByPlatformAndUnionId(platform, unionId);
|
||||
if (ObjectUtil.isNotNull(byUnion)) {
|
||||
return byUnion;
|
||||
}
|
||||
}
|
||||
return appSocialAuthService.selectUserByPlatformAndOpenId(platform, openid);
|
||||
}
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
package org.dromara.app.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.zhyd.oauth.model.AuthCallback;
|
||||
import me.zhyd.oauth.model.AuthResponse;
|
||||
import me.zhyd.oauth.model.AuthUser;
|
||||
import me.zhyd.oauth.request.AuthRequest;
|
||||
import org.dromara.app.domain.AppLoginUser;
|
||||
import org.dromara.member.domain.vo.AppUserVo;
|
||||
import org.dromara.member.service.AppSocialAuthService;
|
||||
import org.dromara.app.service.IAppAuthStrategy;
|
||||
import org.dromara.common.core.domain.model.SocialLoginBody;
|
||||
import org.dromara.common.core.exception.user.UserException;
|
||||
import org.dromara.common.core.utils.ValidatorUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.common.social.config.properties.SocialProperties;
|
||||
import org.dromara.common.social.utils.SocialUtils;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* App端微信开放平台认证策略
|
||||
* 处理微信开放平台(APP端/网站应用)环境下的OAuth2登录逻辑
|
||||
* 依赖 justauth.type.wechat_open 配置项
|
||||
* 通常用于APP原生微信登录
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("app_wechat_open" + IAppAuthStrategy.BASE_NAME)
|
||||
@RequiredArgsConstructor
|
||||
public class AppWechatOpenAuthStrategy implements IAppAuthStrategy {
|
||||
|
||||
private final SocialProperties socialProperties;
|
||||
private final AppSocialAuthService appSocialAuthService;
|
||||
|
||||
/**
|
||||
* 微信开放平台登录核心逻辑
|
||||
*
|
||||
* @param body 登录请求体,包含 socialCode 和 socialState
|
||||
* @param client 客户端配置
|
||||
* @return 登录结果 LoginVo
|
||||
*/
|
||||
@Override
|
||||
public LoginVo login(String body, SysClientVo client) {
|
||||
SocialLoginBody loginBody = JsonUtils.parseObject(body, SocialLoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
|
||||
String code = loginBody.getSocialCode();
|
||||
String state = loginBody.getSocialState();
|
||||
|
||||
// 使用SocialUtils获取微信开放平台的AuthRequest
|
||||
// 对应 application.yml 中 justauth.type.wechat_open 的配置
|
||||
AuthRequest authRequest = SocialUtils.getAuthRequest("wechat_open", socialProperties);
|
||||
if (authRequest == null) {
|
||||
throw new UserException("未配置微信开放平台登录");
|
||||
}
|
||||
|
||||
// 构造回调参数
|
||||
AuthCallback authCallback = new AuthCallback();
|
||||
authCallback.setCode(code);
|
||||
authCallback.setState(state);
|
||||
// 执行第三方登录请求,换取 Access Token 和 User Info
|
||||
AuthResponse<AuthUser> response = authRequest.login(authCallback);
|
||||
|
||||
String openid;
|
||||
String unionId = null;
|
||||
if (response.ok()) {
|
||||
AuthUser authUser = response.getData();
|
||||
openid = authUser.getUuid();
|
||||
if (authUser.getToken() != null) {
|
||||
unionId = authUser.getToken().getUnionId();
|
||||
}
|
||||
} else {
|
||||
throw new UserException("第三方登录失败,原因:" + response.getMsg());
|
||||
}
|
||||
|
||||
// 根据 OpenId 或 UnionId 查找系统用户
|
||||
AppUserVo user = loadUserByWxId("wechat_open", openid, unionId);
|
||||
if (ObjectUtil.isNull(user)) {
|
||||
throw new UserException("登录失败,请先绑定用户账号");
|
||||
}
|
||||
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||
AppLoginUser loginUser = new AppLoginUser();
|
||||
loginUser.setUserId(user.getUserId());
|
||||
loginUser.setUsername(user.getUserName());
|
||||
loginUser.setNickname(user.getNickName());
|
||||
loginUser.setUserType("app");
|
||||
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());
|
||||
loginVo.setOpenid(openid);
|
||||
return loginVo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过微信标识加载用户信息,优先 unionId,回退 openId
|
||||
*/
|
||||
private AppUserVo loadUserByWxId(String platform, String openid, String unionId) {
|
||||
if (ObjectUtil.isNotEmpty(unionId)) {
|
||||
AppUserVo byUnion = appSocialAuthService.selectUserByPlatformAndUnionId(platform, unionId);
|
||||
if (ObjectUtil.isNotNull(byUnion)) {
|
||||
return byUnion;
|
||||
}
|
||||
}
|
||||
return appSocialAuthService.selectUserByPlatformAndOpenId(platform, openid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.dromara.app.session;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AppSession {
|
||||
private String sessionId;
|
||||
private Long userId;
|
||||
private String deviceType;
|
||||
private String deviceId;
|
||||
private String userAgent;
|
||||
private String ip;
|
||||
private String location;
|
||||
private long loginAt;
|
||||
private long expireAt;
|
||||
private boolean active;
|
||||
private String token;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package org.dromara.app.session;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AppSessionManager {
|
||||
private static final String KEY_INDEX_FMT = "APP:SESS_INDEX:%s";
|
||||
private static final String KEY_SESS_FMT = "APP:SESS:%s:%s";
|
||||
private static final String CH_EVT_FMT = "APP:SESS_EVT:%s";
|
||||
|
||||
public void register(AppSession s, Duration ttl) {
|
||||
String indexKey = indexKey(s.getUserId());
|
||||
String sessKey = sessKey(s.getUserId(), s.getSessionId());
|
||||
RedisUtils.addCacheSet(indexKey, sessKey);
|
||||
RedisUtils.setCacheObject(sessKey, JsonUtils.toJsonString(s), ttl);
|
||||
publishEvent(s.getUserId(), "REGISTER", s.getSessionId());
|
||||
}
|
||||
|
||||
public List<AppSession> list(Long userId) {
|
||||
String indexKey = indexKey(userId);
|
||||
Set<String> keys = RedisUtils.getCacheSet(indexKey);
|
||||
List<AppSession> out = new ArrayList<>();
|
||||
if (keys != null) {
|
||||
for (String k : keys) {
|
||||
String val = RedisUtils.getCacheObject(k);
|
||||
if (val != null) {
|
||||
out.add(JsonUtils.parseObject(val, AppSession.class));
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public boolean terminate(Long userId, String sessionId) {
|
||||
String indexKey = indexKey(userId);
|
||||
String sessKey = sessKey(userId, sessionId);
|
||||
RedisUtils.deleteObject(sessKey);
|
||||
try {
|
||||
org.redisson.api.RSet<String> set = org.dromara.common.redis.utils.RedisUtils.getClient().getSet(indexKey);
|
||||
set.remove(sessKey);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
publishEvent(userId, "TERMINATE", sessionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean heartbeat(Long userId, String sessionId, Duration ttlExtend) {
|
||||
String sessKey = sessKey(userId, sessionId);
|
||||
String val = RedisUtils.getCacheObject(sessKey);
|
||||
if (val == null) return false;
|
||||
AppSession s = JsonUtils.parseObject(val, AppSession.class);
|
||||
RedisUtils.setCacheObject(sessKey, JsonUtils.toJsonString(s), ttlExtend);
|
||||
publishEvent(userId, "HEARTBEAT", sessionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void publishEvent(Long userId, String type, String sessionId) {
|
||||
String ch = String.format(CH_EVT_FMT, userId);
|
||||
String payload = type + ":" + sessionId;
|
||||
RedisUtils.publish(ch, payload);
|
||||
}
|
||||
|
||||
private String indexKey(Long userId) {
|
||||
return String.format(KEY_INDEX_FMT, userId);
|
||||
}
|
||||
private String sessKey(Long userId, String sessionId) {
|
||||
return String.format(KEY_SESS_FMT, userId, sessionId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package org.dromara.web.config;
|
||||
|
||||
import cn.dev33.satoken.exception.NotLoginException;
|
||||
import org.dromara.app.domain.AppLoginUser;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.domain.model.LoginUser;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.core.utils.ServletUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* App用户信息补全过滤器
|
||||
*
|
||||
* @author Pair-Programmer
|
||||
*/
|
||||
public class AppInfoCompletionFilter implements Filter {
|
||||
|
||||
@Value("#{'${app.auth.whitelist:/app/user/profile,/app/user/bind,/app/auth/logout}'.split(',')}")
|
||||
private List<String> whitelist;
|
||||
|
||||
private final AntPathMatcher pathMatcher = new AntPathMatcher();
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
||||
String uri = httpRequest.getRequestURI();
|
||||
|
||||
// 仅处理App端请求
|
||||
if (StringUtils.startsWith(uri, "/app")) {
|
||||
try {
|
||||
// 尝试获取登录用户,若未登录会抛出异常或返回null
|
||||
LoginUser loginUser = LoginHelper.getLoginUser();
|
||||
|
||||
// 仅针对App登录用户进行检查
|
||||
if (loginUser instanceof AppLoginUser appUser) {
|
||||
// 如果信息不完善 (infoComplete 为 false 或 null)
|
||||
if (Boolean.FALSE.equals(appUser.getInfoComplete())) {
|
||||
// 检查白名单
|
||||
boolean isAllowed = false;
|
||||
if (whitelist != null) {
|
||||
for (String pattern : whitelist) {
|
||||
if (pathMatcher.match(pattern.trim(), uri)) {
|
||||
isAllowed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAllowed) {
|
||||
ServletUtils.renderString((HttpServletResponse) response,
|
||||
JsonUtils.toJsonString(R.fail(403, "用户信息未补全,请先完善信息")));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 未登录或其他异常(如Token无效),放行交由后续认证过滤器或拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.dromara.web.config;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.core.utils.ServletUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
/**
|
||||
* 风险控制拦截器
|
||||
*
|
||||
* @author Pair-Programmer
|
||||
*/
|
||||
public class RiskInterceptor implements HandlerInterceptor {
|
||||
|
||||
public static final String RISK_LOCKED_KEY = "RISK_LOCKED";
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
// 仅处理登录用户
|
||||
if (!StpUtil.isLogin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查 Session 是否有风险锁
|
||||
Object isLocked = StpUtil.getSession().get(RISK_LOCKED_KEY);
|
||||
if (ObjectUtil.isNotNull(isLocked) && (Boolean) isLocked) {
|
||||
String uri = request.getRequestURI();
|
||||
|
||||
// 白名单:改密接口、登出接口、获取个人信息(用于页面展示)
|
||||
// 注意:具体路径需根据实际 Controller 调整
|
||||
if (StringUtils.equalsAnyIgnoreCase(uri,
|
||||
"/system/user/profile/updatePwd",
|
||||
"/auth/logout",
|
||||
"/system/user/profile"
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 拦截并返回 403
|
||||
ServletUtils.renderString(response,
|
||||
JsonUtils.toJsonString(R.fail(403, "检测到异常登录,请强制修改密码后继续操作")));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.dromara.web.config;
|
||||
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Web MVC 扩展配置
|
||||
*
|
||||
* @author Pair-Programmer
|
||||
*/
|
||||
@Configuration
|
||||
public class WebMvcPlusConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 注册风险控制拦截器
|
||||
registry.addInterceptor(new RiskInterceptor())
|
||||
.addPathPatterns("/system/**", "/monitor/**", "/tool/**");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AppInfoCompletionFilter appInfoCompletionFilter() {
|
||||
return new AppInfoCompletionFilter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FilterRegistrationBean<AppInfoCompletionFilter> appInfoCompletionFilterRegistration(AppInfoCompletionFilter filter) {
|
||||
FilterRegistrationBean<AppInfoCompletionFilter> registration = new FilterRegistrationBean<>(filter);
|
||||
registration.addUrlPatterns("/app/*");
|
||||
registration.setName("appInfoCompletionFilter");
|
||||
// 顺序需在 Sa-Token 认证过滤器之后,但在业务逻辑之前
|
||||
// Sa-Token 拦截器在 Interceptor 层,Filter 在 Servlet 层。
|
||||
// 但 LoginHelper 需要 Token,Token 在 Header 里,随时可取。
|
||||
// 关键是 Filter 需要在请求处理前执行。
|
||||
registration.setOrder(100);
|
||||
return registration;
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package org.dromara.web.config.properties;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 风险控制配置
|
||||
*
|
||||
* @author Pair-Programmer
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "sys.risk")
|
||||
public class RiskControlProperties {
|
||||
|
||||
/**
|
||||
* 是否开启风险检测
|
||||
*/
|
||||
private Boolean enabled = true;
|
||||
|
||||
/**
|
||||
* 风险阈值 (0-100)
|
||||
*/
|
||||
private Integer threshold = 60;
|
||||
|
||||
/**
|
||||
* 异地登录权重
|
||||
*/
|
||||
private Integer geoWeight = 50;
|
||||
|
||||
/**
|
||||
* 新设备权重
|
||||
*/
|
||||
private Integer deviceWeight = 30;
|
||||
|
||||
/**
|
||||
* 非工作时间权重 (00:00-06:00)
|
||||
*/
|
||||
private Integer timeWeight = 10;
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package org.dromara.web.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.codec.Base64;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.zhyd.oauth.model.AuthResponse;
|
||||
import me.zhyd.oauth.model.AuthUser;
|
||||
import me.zhyd.oauth.request.AuthRequest;
|
||||
import me.zhyd.oauth.utils.AuthStateUtils;
|
||||
import org.dromara.common.core.constant.SystemConstants;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.domain.model.LoginBody;
|
||||
import org.dromara.common.core.domain.model.RegisterBody;
|
||||
import org.dromara.common.core.domain.model.SocialLoginBody;
|
||||
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.encrypt.annotation.ApiEncrypt;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
|
||||
import org.dromara.common.social.config.properties.SocialProperties;
|
||||
import org.dromara.common.social.utils.SocialUtils;
|
||||
import org.dromara.common.sse.dto.SseMessageDto;
|
||||
import org.dromara.common.sse.utils.SseMessageUtils;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.system.service.SysClientService;
|
||||
import org.dromara.system.service.SysConfigService;
|
||||
import org.dromara.system.service.SysSocialService;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.dromara.web.service.IAuthStrategy;
|
||||
import org.dromara.web.service.SysLoginService;
|
||||
import org.dromara.web.service.SysRegisterService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 认证
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@SaIgnore
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/auth")
|
||||
public class AuthController {
|
||||
|
||||
private final SocialProperties socialProperties;
|
||||
private final SysLoginService loginService;
|
||||
private final SysRegisterService registerService;
|
||||
private final SysConfigService configService;
|
||||
private final SysSocialService socialUserService;
|
||||
private final SysClientService clientService;
|
||||
private final ScheduledExecutorService scheduledExecutorService;
|
||||
|
||||
|
||||
/**
|
||||
* 登录方法
|
||||
*
|
||||
* @param body 登录信息
|
||||
* @return 结果
|
||||
*/
|
||||
@ApiEncrypt
|
||||
@PostMapping("/login")
|
||||
public R<LoginVo> login(@RequestBody String body) {
|
||||
LoginBody loginBody = JsonUtils.parseObject(body, LoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
// 授权类型和客户端id
|
||||
String clientId = loginBody.getClientId();
|
||||
String grantType = loginBody.getGrantType();
|
||||
SysClientVo client = clientService.queryByClientId(clientId);
|
||||
// 查询不到 client 或 client 内不包含 grantType
|
||||
if (ObjectUtil.isNull(client) || !StringUtils.contains(client.getGrantType(), grantType)) {
|
||||
log.info("客户端id: {} 认证类型:{} 异常!.", clientId, grantType);
|
||||
return R.fail(MessageUtils.message("auth.grant.type.error"));
|
||||
} else if (!SystemConstants.NORMAL.equals(client.getStatus())) {
|
||||
return R.fail(MessageUtils.message("auth.grant.type.blocked"));
|
||||
}
|
||||
// 登录
|
||||
LoginVo loginVo = IAuthStrategy.login(body, client, grantType);
|
||||
|
||||
Long userId = LoginHelper.getUserId();
|
||||
scheduledExecutorService.schedule(() -> {
|
||||
SseMessageDto dto = new SseMessageDto();
|
||||
dto.setMessage("欢迎登录RuoYi-Vue-Plus后台管理系统");
|
||||
dto.setUserIds(List.of(userId));
|
||||
SseMessageUtils.publishMessage(dto);
|
||||
}, 5, TimeUnit.SECONDS);
|
||||
return R.ok(loginVo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取跳转URL
|
||||
*
|
||||
* @param source 登录来源
|
||||
* @return 结果
|
||||
*/
|
||||
@GetMapping("/binding/{source}")
|
||||
public R<String> authBinding(@PathVariable("source") String source, @RequestParam String domain) {
|
||||
SocialLoginConfigProperties obj = socialProperties.getType().get(source);
|
||||
if (ObjectUtil.isNull(obj)) {
|
||||
return R.fail(source + "平台账号暂不支持");
|
||||
}
|
||||
AuthRequest authRequest = SocialUtils.getAuthRequest(source, socialProperties);
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("domain", domain);
|
||||
map.put("state", AuthStateUtils.createState());
|
||||
String authorizeUrl = authRequest.authorize(Base64.encode(JsonUtils.toJsonString(map), StandardCharsets.UTF_8));
|
||||
return R.ok("操作成功", authorizeUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 前端回调绑定授权(需要token)
|
||||
*
|
||||
* @param loginBody 请求体
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/social/callback")
|
||||
public R<Void> socialCallback(@RequestBody SocialLoginBody loginBody) {
|
||||
// 校验token
|
||||
StpUtil.checkLogin();
|
||||
// 获取第三方登录信息
|
||||
AuthResponse<AuthUser> response = SocialUtils.loginAuth(
|
||||
loginBody.getSource(), loginBody.getSocialCode(),
|
||||
loginBody.getSocialState(), socialProperties);
|
||||
AuthUser authUserData = response.getData();
|
||||
// 判断授权响应是否成功
|
||||
if (!response.ok()) {
|
||||
return R.fail(response.getMsg());
|
||||
}
|
||||
loginService.socialRegister(authUserData);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 取消授权(需要token)
|
||||
*
|
||||
* @param socialId socialId
|
||||
*/
|
||||
@DeleteMapping(value = "/unlock/{socialId}")
|
||||
public R<Void> unlockSocial(@PathVariable Long socialId) {
|
||||
// 校验token
|
||||
StpUtil.checkLogin();
|
||||
Boolean rows = socialUserService.deleteWithValidById(socialId);
|
||||
return rows ? R.ok() : R.fail("取消授权失败");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
public R<Void> logout() {
|
||||
loginService.logout();
|
||||
return R.ok("退出成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
@ApiEncrypt
|
||||
@PostMapping("/register")
|
||||
public R<Void> register(@Validated @RequestBody RegisterBody user) {
|
||||
if (!configService.selectRegisterEnabled()) {
|
||||
return R.fail("当前系统没有开启注册功能!");
|
||||
}
|
||||
registerService.register(user);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录页面租户下拉框(兼容多租户前端)
|
||||
*/
|
||||
@GetMapping("/tenant/list")
|
||||
public R<TenantListVo> tenantList() {
|
||||
return R.ok(TenantListVo.DISABLED_TENANT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 租户下拉列表
|
||||
*
|
||||
* @param tenantEnabled 租户开关
|
||||
*/
|
||||
public record TenantListVo(boolean tenantEnabled) {
|
||||
|
||||
/**
|
||||
* 禁用租户,租户开关响应false(即关闭),让前端关闭租户功能
|
||||
*/
|
||||
public static final TenantListVo DISABLED_TENANT = TenantListVo.of(false);
|
||||
|
||||
public static TenantListVo of(boolean tenantEnabled) {
|
||||
return new TenantListVo(tenantEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package org.dromara.web.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import cn.hutool.captcha.AbstractCaptcha;
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
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.R;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.core.utils.reflect.ReflectUtils;
|
||||
import org.dromara.common.mail.config.properties.MailProperties;
|
||||
import org.dromara.common.mail.utils.MailUtils;
|
||||
import org.dromara.common.ratelimiter.annotation.RateLimiter;
|
||||
import org.dromara.common.ratelimiter.enums.LimitType;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.web.config.properties.CaptchaProperties;
|
||||
import org.dromara.common.web.enums.CaptchaType;
|
||||
import org.dromara.sms4j.api.SmsBlend;
|
||||
import org.dromara.sms4j.api.entity.SmsResponse;
|
||||
import org.dromara.sms4j.core.factory.SmsFactory;
|
||||
import org.dromara.web.domain.vo.CaptchaVo;
|
||||
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;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
/**
|
||||
* 验证码操作处理
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@SaIgnore
|
||||
@Slf4j
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
public class CaptchaController {
|
||||
|
||||
private final CaptchaProperties captchaProperties;
|
||||
private final MailProperties mailProperties;
|
||||
|
||||
/**
|
||||
* 短信验证码
|
||||
*
|
||||
* @param phonenumber 用户手机号
|
||||
*/
|
||||
@RateLimiter(key = "#phonenumber", time = 60, count = 1)
|
||||
@GetMapping("/resource/sms/code")
|
||||
public R<Void> smsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phonenumber) {
|
||||
String key = GlobalConstants.CAPTCHA_CODE_KEY + phonenumber;
|
||||
String code = RandomUtil.randomNumbers(4);
|
||||
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
||||
// 验证码模板id 自行处理 (查数据库或写死均可)
|
||||
String templateId = "";
|
||||
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
|
||||
map.put("code", code);
|
||||
SmsBlend smsBlend = SmsFactory.getSmsBlend("config1");
|
||||
SmsResponse smsResponse = smsBlend.sendMessage(phonenumber, templateId, map);
|
||||
if (!smsResponse.isSuccess()) {
|
||||
log.error("验证码短信发送异常 => {}", smsResponse);
|
||||
return R.fail(smsResponse.getData().toString());
|
||||
}
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮箱验证码
|
||||
*
|
||||
* @param email 邮箱
|
||||
*/
|
||||
@GetMapping("/resource/email/code")
|
||||
public R<Void> emailCode(@NotBlank(message = "{user.email.not.blank}") String email) {
|
||||
if (!mailProperties.getEnabled()) {
|
||||
return R.fail("当前系统没有开启邮箱功能!");
|
||||
}
|
||||
SpringUtils.getAopProxy(this).emailCodeImpl(email);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮箱验证码
|
||||
* 独立方法避免验证码关闭之后仍然走限流
|
||||
*/
|
||||
@RateLimiter(key = "#email", time = 60, count = 1)
|
||||
public void emailCodeImpl(String email) {
|
||||
String key = GlobalConstants.CAPTCHA_CODE_KEY + email;
|
||||
String code = RandomUtil.randomNumbers(4);
|
||||
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
||||
try {
|
||||
MailUtils.sendText(email, "登录验证码", "您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。");
|
||||
} catch (Exception e) {
|
||||
log.error("验证码短信发送异常 => {}", e.getMessage());
|
||||
throw new ServiceException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
*/
|
||||
@GetMapping("/auth/code")
|
||||
public R<CaptchaVo> getCode() {
|
||||
boolean captchaEnabled = captchaProperties.getEnable();
|
||||
if (!captchaEnabled) {
|
||||
CaptchaVo captchaVo = new CaptchaVo();
|
||||
captchaVo.setCaptchaEnabled(false);
|
||||
return R.ok(captchaVo);
|
||||
}
|
||||
return R.ok(SpringUtils.getAopProxy(this).getCodeImpl());
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
* 独立方法避免验证码关闭之后仍然走限流
|
||||
*/
|
||||
@RateLimiter(time = 60, count = 10, limitType = LimitType.IP)
|
||||
public CaptchaVo getCodeImpl() {
|
||||
// 保存验证码信息
|
||||
String uuid = IdUtil.simpleUUID();
|
||||
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid;
|
||||
// 生成验证码
|
||||
CaptchaType captchaType = captchaProperties.getType();
|
||||
boolean isMath = CaptchaType.MATH == captchaType;
|
||||
Integer length = isMath ? captchaProperties.getNumberLength() : captchaProperties.getCharLength();
|
||||
CodeGenerator codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), length);
|
||||
AbstractCaptcha captcha = SpringUtils.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));
|
||||
CaptchaVo captchaVo = new CaptchaVo();
|
||||
captchaVo.setUuid(uuid);
|
||||
captchaVo.setImg(captcha.getImageBase64());
|
||||
return captchaVo;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.dromara.web.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 首页
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@SaIgnore
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
public class IndexController {
|
||||
|
||||
/**
|
||||
* 访问首页,提示语
|
||||
*/
|
||||
@GetMapping("/")
|
||||
public String index() {
|
||||
return StringUtils.format("欢迎使用{}后台管理框架,请通过前端地址访问。", SpringUtils.getApplicationName());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.dromara.web.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 验证码信息
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
public class CaptchaVo {
|
||||
|
||||
/**
|
||||
* 是否开启验证码
|
||||
*/
|
||||
private Boolean captchaEnabled = true;
|
||||
|
||||
/**
|
||||
* 验证码唯一标识
|
||||
*/
|
||||
private String uuid;
|
||||
|
||||
/**
|
||||
* 验证码图片
|
||||
*/
|
||||
private String img;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package org.dromara.web.domain.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 登录验证信息
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@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;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package org.dromara.web.listener;
|
||||
|
||||
import cn.dev33.satoken.listener.SaTokenListener;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.http.useragent.UserAgent;
|
||||
import cn.hutool.http.useragent.UserAgentUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.constant.CacheConstants;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.domain.dto.UserOnlineDTO;
|
||||
import org.dromara.common.core.utils.MessageUtils;
|
||||
import org.dromara.common.core.utils.ServletUtils;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.core.utils.ip.AddressUtils;
|
||||
import org.dromara.common.log.event.LogininforEvent;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.web.config.properties.RiskControlProperties;
|
||||
import org.dromara.web.service.RiskControlService;
|
||||
import org.dromara.web.service.SysLoginService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* 用户行为 侦听器的实现
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
@Slf4j
|
||||
public class UserActionListener implements SaTokenListener {
|
||||
|
||||
private final SysLoginService loginService;
|
||||
private final RiskControlService riskControlService;
|
||||
private final RiskControlProperties riskProperties;
|
||||
|
||||
/**
|
||||
* 每次登录时触发
|
||||
*/
|
||||
@Override
|
||||
public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) {
|
||||
UserAgent userAgent = UserAgentUtil.parse(ServletUtils.getRequest().getHeader("User-Agent"));
|
||||
String ip = ServletUtils.getClientIP();
|
||||
UserOnlineDTO dto = new UserOnlineDTO();
|
||||
dto.setIpaddr(ip);
|
||||
dto.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
|
||||
dto.setBrowser(userAgent.getBrowser().getName());
|
||||
dto.setOs(userAgent.getOs().getName());
|
||||
dto.setLoginTime(System.currentTimeMillis());
|
||||
dto.setTokenId(tokenValue);
|
||||
String username = (String) loginParameter.getExtra(LoginHelper.USER_NAME_KEY);
|
||||
dto.setUserName(username);
|
||||
dto.setClientKey((String) loginParameter.getExtra(LoginHelper.CLIENT_KEY));
|
||||
dto.setDeviceType(loginParameter.getDeviceType());
|
||||
dto.setDeptName((String) loginParameter.getExtra(LoginHelper.DEPT_NAME_KEY));
|
||||
if (loginParameter.getTimeout() == -1) {
|
||||
RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto);
|
||||
} else {
|
||||
RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto, Duration.ofSeconds(loginParameter.getTimeout()));
|
||||
}
|
||||
|
||||
// 风险检测 (仅针对 sys 用户)
|
||||
// 假设 "login" 是 sys 用户的 loginType (SaToken 默认配置)
|
||||
// if ("login".equals(loginType)) {
|
||||
// int score = riskControlService.assessRisk(username, ip, ServletUtils.getRequest().getHeader("User-Agent"));
|
||||
// if (score > riskProperties.getThreshold()) {
|
||||
// log.warn("用户 {} 登录风险过高 (score={}),已锁定会话", username, score);
|
||||
// StpUtil.getSession().set(RiskInterceptor.RISK_LOCKED_KEY, true);
|
||||
// // 可以在此触发邮件/短信告警
|
||||
// }
|
||||
// }
|
||||
|
||||
// 记录登录日志
|
||||
LogininforEvent logininforEvent = new LogininforEvent();
|
||||
logininforEvent.setUsername(username);
|
||||
logininforEvent.setStatus(Constants.LOGIN_SUCCESS);
|
||||
logininforEvent.setMessage(MessageUtils.message("user.login.success"));
|
||||
logininforEvent.setRequest(ServletUtils.getRequest());
|
||||
SpringUtils.context().publishEvent(logininforEvent);
|
||||
// 更新登录信息
|
||||
loginService.recordLoginInfo((Long) loginParameter.getExtra(LoginHelper.USER_KEY), ip);
|
||||
log.info("user doLogin, userId:{}, token:{}", loginId, tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次注销时触发
|
||||
*/
|
||||
@Override
|
||||
public void doLogout(String loginType, Object loginId, String tokenValue) {
|
||||
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
|
||||
log.info("user doLogout, userId:{}, token:{}", loginId, tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次被踢下线时触发
|
||||
*/
|
||||
@Override
|
||||
public void doKickout(String loginType, Object loginId, String tokenValue) {
|
||||
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
|
||||
log.info("user doKickout, userId:{}, token:{}", loginId, tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次被顶下线时触发
|
||||
*/
|
||||
@Override
|
||||
public void doReplaced(String loginType, Object loginId, String tokenValue) {
|
||||
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
|
||||
log.info("user doReplaced, userId:{}, token:{}", loginId, tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次被封禁时触发
|
||||
*/
|
||||
@Override
|
||||
public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) {
|
||||
log.info("用户被禁用: loginType={}, loginId={}, service={}, level={}, disableTime={}",
|
||||
loginType, loginId, service, level, disableTime);
|
||||
|
||||
// 删除在线用户信息
|
||||
String tokenValue = StpUtil.getTokenValueByLoginId(loginId);
|
||||
if (StringUtils.isNotBlank(tokenValue)) {
|
||||
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次被解封时触发
|
||||
*/
|
||||
@Override
|
||||
public void doUntieDisable(String loginType, Object loginId, String service) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次打开二级认证时触发
|
||||
*/
|
||||
@Override
|
||||
public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次创建Session时触发
|
||||
*/
|
||||
@Override
|
||||
public void doCloseSafe(String loginType, String tokenValue, String service) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次创建Session时触发
|
||||
*/
|
||||
@Override
|
||||
public void doCreateSession(String id) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次注销Session时触发
|
||||
*/
|
||||
@Override
|
||||
public void doLogoutSession(String id) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次Token续期时触发
|
||||
*/
|
||||
@Override
|
||||
public void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.dromara.web.service;
|
||||
|
||||
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
|
||||
/**
|
||||
* 授权策略
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
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 (!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);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package org.dromara.web.service;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.useragent.UserAgent;
|
||||
import cn.hutool.http.useragent.UserAgentUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.core.utils.ip.AddressUtils;
|
||||
import org.dromara.system.domain.bo.SysLogininforBo;
|
||||
import org.dromara.system.domain.vo.SysLogininforVo;
|
||||
import org.dromara.system.service.SysLogininforService;
|
||||
import org.dromara.web.config.properties.RiskControlProperties;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 风险控制服务
|
||||
*
|
||||
* @author Pair-Programmer
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RiskControlService {
|
||||
|
||||
private final SysLogininforService logininforService;
|
||||
private final RiskControlProperties properties;
|
||||
|
||||
/**
|
||||
* 评估登录风险
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param currentIp 当前IP
|
||||
* @param uaStr User-Agent字符串
|
||||
* @return 风险分数 (0-100)
|
||||
*/
|
||||
public int assessRisk(String username, String currentIp, String uaStr) {
|
||||
if (!properties.getEnabled()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int score = 0;
|
||||
String currentAddress = AddressUtils.getRealAddressByIP(currentIp);
|
||||
UserAgent currentUserAgent = UserAgentUtil.parse(uaStr);
|
||||
|
||||
// 1. 获取最近成功的登录记录 (取最近10条)
|
||||
SysLogininforBo query = new SysLogininforBo();
|
||||
query.setUserName(username);
|
||||
query.setStatus(Constants.SUCCESS);
|
||||
// 这里假设 selectLogininforList 是按时间倒序的,且我们可以通过PageHelper或流截取
|
||||
// 由于是 Service 调用,我们先获取列表然后手动截取,或者依赖 Mapper 的分页(这里简化处理)
|
||||
List<SysLogininforVo> historyList = logininforService.selectLogininforList(query);
|
||||
|
||||
// 如果是首次登录或无历史记录,视为低风险
|
||||
if (CollUtil.isEmpty(historyList)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 截取最近 10 条作为基线
|
||||
List<SysLogininforVo> baseline = historyList.stream().limit(10).collect(Collectors.toList());
|
||||
|
||||
// 2. 检查异地登录 (省份是否变化)
|
||||
boolean isGeoAnomaly = true;
|
||||
Set<String> validLocations = baseline.stream()
|
||||
.map(SysLogininforVo::getLoginLocation)
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.map(this::extractProvince)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
String currentProvince = extractProvince(currentAddress);
|
||||
if (validLocations.contains(currentProvince)) {
|
||||
isGeoAnomaly = false;
|
||||
} else {
|
||||
// 特殊处理:如果历史记录都是“未知”或“内网IP”,且当前是公网IP,可能误报,暂且认为是异地
|
||||
log.info("风险检测-异地登录: 用户={}, 历史={}, 当前={}", username, validLocations, currentProvince);
|
||||
}
|
||||
|
||||
if (isGeoAnomaly) {
|
||||
score += properties.getGeoWeight();
|
||||
}
|
||||
|
||||
// 3. 检查新设备 (Browser + OS)
|
||||
boolean isNewDevice = true;
|
||||
Set<String> knownDevices = baseline.stream()
|
||||
.map(log -> log.getBrowser() + " on " + log.getOs())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
String currentDevice = currentUserAgent.getBrowser().getName() + " on " + currentUserAgent.getOs().getName();
|
||||
if (knownDevices.contains(currentDevice)) {
|
||||
isNewDevice = false;
|
||||
} else {
|
||||
log.info("风险检测-新设备: 用户={}, 历史={}, 当前={}", username, knownDevices, currentDevice);
|
||||
}
|
||||
|
||||
if (isNewDevice) {
|
||||
score += properties.getDeviceWeight();
|
||||
}
|
||||
|
||||
// 4. 检查非工作时间 (00:00 - 06:00)
|
||||
int hour = DateUtil.hour(new Date(), true);
|
||||
if (hour >= 0 && hour < 6) {
|
||||
score += properties.getTimeWeight();
|
||||
}
|
||||
|
||||
log.info("风险检测完成: 用户={}, 总分={}, 详情(Geo={}, Device={}, Time={})",
|
||||
username, score, isGeoAnomaly, isNewDevice, (hour >= 0 && hour < 6));
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取省份 (简单截取)
|
||||
*/
|
||||
private String extractProvince(String location) {
|
||||
if (StrUtil.isBlank(location)) {
|
||||
return "Unknown";
|
||||
}
|
||||
// 假设 location 格式为 "XX省 XX市" 或 "XX省"
|
||||
// 简单处理:取前2-3个字符,或者根据空格分割
|
||||
if (location.contains("省")) {
|
||||
return location.substring(0, location.indexOf("省") + 1);
|
||||
}
|
||||
// 直辖市
|
||||
if (location.startsWith("北京") || location.startsWith("上海") || location.startsWith("天津") || location.startsWith("重庆")) {
|
||||
return location.substring(0, 2);
|
||||
}
|
||||
return location; // 无法识别则返回原值
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package org.dromara.web.service;
|
||||
|
||||
import cn.dev33.satoken.exception.NotLoginException;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.lang.Opt;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.lock.annotation.Lock4j;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.zhyd.oauth.model.AuthUser;
|
||||
import org.dromara.common.core.constant.CacheConstants;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.domain.dto.PostDTO;
|
||||
import org.dromara.common.core.domain.dto.RoleDTO;
|
||||
import org.dromara.common.core.domain.model.LoginUser;
|
||||
import org.dromara.common.core.enums.LoginType;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.exception.user.UserException;
|
||||
import org.dromara.common.core.utils.MessageUtils;
|
||||
import org.dromara.common.core.utils.ServletUtils;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.log.event.LogininforEvent;
|
||||
import org.dromara.common.mybatis.helper.DataPermissionHelper;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.system.domain.bo.SysSocialBo;
|
||||
import org.dromara.system.domain.vo.*;
|
||||
import org.dromara.system.service.*;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* 登录校验方法
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SysLoginService {
|
||||
|
||||
@Value("${user.password.maxRetryCount}")
|
||||
private Integer maxRetryCount;
|
||||
|
||||
@Value("${user.password.lockTime}")
|
||||
private Integer lockTime;
|
||||
|
||||
private final SysPermissionService permissionService;
|
||||
private final SysSocialService sysSocialService;
|
||||
private final SysRoleService roleService;
|
||||
private final SysDeptService deptService;
|
||||
private final SysPostService postService;
|
||||
private final SysUserService userService;
|
||||
|
||||
|
||||
/**
|
||||
* 绑定第三方用户
|
||||
*
|
||||
* @param authUserData 授权响应实体
|
||||
*/
|
||||
@Lock4j
|
||||
public void socialRegister(AuthUser authUserData) {
|
||||
String authId = authUserData.getSource() + authUserData.getUuid();
|
||||
// 第三方用户信息
|
||||
SysSocialBo bo = BeanUtil.toBean(authUserData, SysSocialBo.class);
|
||||
BeanUtil.copyProperties(authUserData.getToken(), bo);
|
||||
Long userId = LoginHelper.getUserId();
|
||||
bo.setUserId(userId);
|
||||
bo.setAuthId(authId);
|
||||
bo.setOpenId(authUserData.getUuid());
|
||||
bo.setUserName(authUserData.getUsername());
|
||||
bo.setNickName(authUserData.getNickname());
|
||||
List<SysSocialVo> checkList = sysSocialService.selectByAuthId(authId);
|
||||
if (CollUtil.isNotEmpty(checkList)) {
|
||||
throw new ServiceException("此三方账号已经被绑定!");
|
||||
}
|
||||
// 查询是否已经绑定用户
|
||||
SysSocialBo params = new SysSocialBo();
|
||||
params.setUserId(userId);
|
||||
params.setSource(bo.getSource());
|
||||
List<SysSocialVo> list = sysSocialService.queryList(params);
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
// 没有绑定用户, 新增用户信息
|
||||
sysSocialService.insertByBo(bo);
|
||||
} else {
|
||||
// 更新用户信息
|
||||
bo.setId(list.get(0).getId());
|
||||
sysSocialService.updateByBo(bo);
|
||||
// 如果要绑定的平台账号已经被绑定过了 是否抛异常自行决断
|
||||
// throw new ServiceException("此平台账号已经被绑定!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
public void logout() {
|
||||
try {
|
||||
LoginUser loginUser = LoginHelper.getLoginUser();
|
||||
if (ObjectUtil.isNull(loginUser)) {
|
||||
return;
|
||||
}
|
||||
recordLogininfor(loginUser.getUsername(), Constants.LOGOUT, MessageUtils.message("user.logout.success"));
|
||||
} catch (NotLoginException ignored) {
|
||||
} finally {
|
||||
try {
|
||||
StpUtil.logout();
|
||||
} catch (NotLoginException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录登录信息
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param status 状态
|
||||
* @param message 消息内容
|
||||
*/
|
||||
public void recordLogininfor(String username, String status, String message) {
|
||||
LogininforEvent logininforEvent = new LogininforEvent();
|
||||
logininforEvent.setUsername(username);
|
||||
logininforEvent.setStatus(status);
|
||||
logininforEvent.setMessage(message);
|
||||
logininforEvent.setRequest(ServletUtils.getRequest());
|
||||
SpringUtils.context().publishEvent(logininforEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建登录用户
|
||||
*/
|
||||
public LoginUser buildLoginUser(SysUserVo user) {
|
||||
LoginUser loginUser = new LoginUser();
|
||||
Long userId = user.getUserId();
|
||||
loginUser.setUserId(userId);
|
||||
loginUser.setDeptId(user.getDeptId());
|
||||
loginUser.setUsername(user.getUserName());
|
||||
loginUser.setNickname(user.getNickName());
|
||||
loginUser.setUserType(user.getUserType());
|
||||
loginUser.setMenuPermission(permissionService.getMenuPermission(userId));
|
||||
loginUser.setRolePermission(permissionService.getRolePermission(userId));
|
||||
if (ObjectUtil.isNotNull(user.getDeptId())) {
|
||||
Opt<SysDeptVo> deptOpt = Opt.of(user.getDeptId()).map(deptService::selectDeptById);
|
||||
loginUser.setDeptName(deptOpt.map(SysDeptVo::getDeptName).orElse(StringUtils.EMPTY));
|
||||
loginUser.setDeptCategory(deptOpt.map(SysDeptVo::getDeptCategory).orElse(StringUtils.EMPTY));
|
||||
}
|
||||
List<SysRoleVo> roles = roleService.selectRolesByUserId(userId);
|
||||
List<SysPostVo> posts = postService.selectPostsByUserId(userId);
|
||||
loginUser.setRoles(BeanUtil.copyToList(roles, RoleDTO.class));
|
||||
loginUser.setPosts(BeanUtil.copyToList(posts, PostDTO.class));
|
||||
return loginUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录登录信息
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
public void recordLoginInfo(Long userId, String ip) {
|
||||
DataPermissionHelper.ignore(() -> userService.updateLoginInfo(userId, ip));
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录校验
|
||||
*/
|
||||
public void checkLogin(LoginType loginType, String username, Supplier<Boolean> supplier) {
|
||||
String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username;
|
||||
String loginFail = Constants.LOGIN_FAIL;
|
||||
|
||||
// 获取用户登录错误次数,默认为0 (可自定义限制策略 例如: key + username + ip)
|
||||
int errorNumber = ObjectUtil.defaultIfNull(RedisUtils.getCacheObject(errorKey), 0);
|
||||
// 锁定时间内登录 则踢出
|
||||
if (errorNumber >= maxRetryCount) {
|
||||
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
|
||||
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
|
||||
}
|
||||
|
||||
if (supplier.get()) {
|
||||
// 错误次数递增
|
||||
errorNumber++;
|
||||
RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
|
||||
// 达到规定错误次数 则锁定登录
|
||||
if (errorNumber >= maxRetryCount) {
|
||||
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
|
||||
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
|
||||
} else {
|
||||
// 未达到规定错误次数
|
||||
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
|
||||
throw new UserException(loginType.getRetryLimitCount(), errorNumber);
|
||||
}
|
||||
}
|
||||
|
||||
// 登录成功 清空错误次数
|
||||
RedisUtils.deleteObject(errorKey);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package org.dromara.web.service;
|
||||
|
||||
import cn.hutool.crypto.digest.BCrypt;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.constant.GlobalConstants;
|
||||
import org.dromara.common.core.domain.model.RegisterBody;
|
||||
import org.dromara.common.core.enums.UserType;
|
||||
import org.dromara.common.core.exception.user.CaptchaException;
|
||||
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.ServletUtils;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.log.event.LogininforEvent;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.web.config.properties.CaptchaProperties;
|
||||
import org.dromara.system.domain.SysUser;
|
||||
import org.dromara.system.domain.bo.SysUserBo;
|
||||
import org.dromara.system.mapper.SysUserMapper;
|
||||
import org.dromara.system.service.SysUserService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 注册校验方法
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class SysRegisterService {
|
||||
|
||||
private final SysUserService userService;
|
||||
private final SysUserMapper userMapper;
|
||||
private final CaptchaProperties captchaProperties;
|
||||
|
||||
/**
|
||||
* 注册
|
||||
*/
|
||||
public void register(RegisterBody registerBody) {
|
||||
String username = registerBody.getUsername();
|
||||
String password = registerBody.getPassword();
|
||||
// 校验用户类型是否存在
|
||||
String userType = UserType.getUserType(registerBody.getUserType()).getUserType();
|
||||
|
||||
boolean captchaEnabled = captchaProperties.getEnable();
|
||||
// 验证码开关
|
||||
if (captchaEnabled) {
|
||||
validateCaptcha(username, registerBody.getCode(), registerBody.getUuid());
|
||||
}
|
||||
SysUserBo sysUser = new SysUserBo();
|
||||
sysUser.setUserName(username);
|
||||
sysUser.setNickName(username);
|
||||
sysUser.setPassword(BCrypt.hashpw(password));
|
||||
sysUser.setUserType(userType);
|
||||
|
||||
boolean exist = !userService.checkUserNameUnique(sysUser);
|
||||
if (exist) {
|
||||
throw new UserException("user.register.save.error", username);
|
||||
}
|
||||
boolean regFlag = userService.registerUser(sysUser);
|
||||
if (!regFlag) {
|
||||
throw new UserException("user.register.error");
|
||||
}
|
||||
recordLogininfor(username, Constants.REGISTER, MessageUtils.message("user.register.success"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验验证码
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param code 验证码
|
||||
* @param uuid 唯一标识
|
||||
*/
|
||||
public void validateCaptcha(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) {
|
||||
recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
|
||||
throw new CaptchaExpireException();
|
||||
}
|
||||
if (!code.equalsIgnoreCase(captcha)) {
|
||||
recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
|
||||
throw new CaptchaException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录登录信息
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param status 状态
|
||||
* @param message 消息内容
|
||||
* @return
|
||||
*/
|
||||
private void recordLogininfor(String username, String status, String message) {
|
||||
LogininforEvent logininforEvent = new LogininforEvent();
|
||||
logininforEvent.setUsername(username);
|
||||
logininforEvent.setStatus(status);
|
||||
logininforEvent.setMessage(message);
|
||||
logininforEvent.setRequest(ServletUtils.getRequest());
|
||||
SpringUtils.context().publishEvent(logininforEvent);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package org.dromara.web.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
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.constant.SystemConstants;
|
||||
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.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.json.utils.JsonUtils;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.system.domain.vo.SysUserVo;
|
||||
import org.dromara.system.service.SysUserService;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.dromara.web.service.IAuthStrategy;
|
||||
import org.dromara.web.service.SysLoginService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 邮件认证策略
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("email" + IAuthStrategy.BASE_NAME)
|
||||
@RequiredArgsConstructor
|
||||
public class EmailAuthStrategy implements IAuthStrategy {
|
||||
|
||||
private final SysLoginService loginService;
|
||||
private final SysUserService userService;
|
||||
|
||||
@Override
|
||||
public LoginVo login(String body, SysClientVo client) {
|
||||
EmailLoginBody loginBody = JsonUtils.parseObject(body, EmailLoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
String email = loginBody.getEmail();
|
||||
String emailCode = loginBody.getEmailCode();
|
||||
|
||||
SysUserVo user = loadUserByEmail(email);
|
||||
loginService.checkLogin(LoginType.EMAIL, user.getUserName(), () -> !validateEmailCode(email, emailCode));
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||
LoginUser loginUser = loginService.buildLoginUser(user);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验邮箱验证码
|
||||
*/
|
||||
private boolean validateEmailCode(String email, String emailCode) {
|
||||
String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + email);
|
||||
if (StringUtils.isBlank(code)) {
|
||||
loginService.recordLogininfor(email, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
|
||||
throw new CaptchaExpireException();
|
||||
}
|
||||
return code.equals(emailCode);
|
||||
}
|
||||
|
||||
private SysUserVo loadUserByEmail(String email) {
|
||||
SysUserVo user = userService.selectUserByEmail(email);
|
||||
if (ObjectUtil.isNull(user)) {
|
||||
log.info("登录用户:{} 不存在.", email);
|
||||
throw new UserException("user.not.exists", email);
|
||||
} else if (SystemConstants.DISABLE.equals(user.getStatus())) {
|
||||
log.info("登录用户:{} 已被停用.", email);
|
||||
throw new UserException("user.blocked", email);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package org.dromara.web.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.crypto.digest.BCrypt;
|
||||
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.constant.SystemConstants;
|
||||
import org.dromara.common.core.domain.model.LoginUser;
|
||||
import org.dromara.common.core.domain.model.PasswordLoginBody;
|
||||
import org.dromara.common.core.enums.LoginType;
|
||||
import org.dromara.common.core.exception.user.CaptchaException;
|
||||
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.json.utils.JsonUtils;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.common.web.config.properties.CaptchaProperties;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.system.domain.vo.SysUserVo;
|
||||
import org.dromara.system.service.SysUserService;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.dromara.web.service.IAuthStrategy;
|
||||
import org.dromara.web.service.SysLoginService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 密码认证策略
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("password" + IAuthStrategy.BASE_NAME)
|
||||
@RequiredArgsConstructor
|
||||
public class PasswordAuthStrategy implements IAuthStrategy {
|
||||
|
||||
private final CaptchaProperties captchaProperties;
|
||||
private final SysLoginService loginService;
|
||||
private final SysUserService userService;
|
||||
|
||||
@Override
|
||||
public LoginVo login(String body, SysClientVo client) {
|
||||
PasswordLoginBody loginBody = JsonUtils.parseObject(body, PasswordLoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
String username = loginBody.getUsername();
|
||||
String password = loginBody.getPassword();
|
||||
String code = loginBody.getCode();
|
||||
String uuid = loginBody.getUuid();
|
||||
|
||||
boolean captchaEnabled = captchaProperties.getEnable();
|
||||
// 验证码开关
|
||||
if (captchaEnabled) {
|
||||
validateCaptcha(username, code, uuid);
|
||||
}
|
||||
|
||||
SysUserVo user = loadUserByUsername(username);
|
||||
loginService.checkLogin(LoginType.PASSWORD, username, () -> !BCrypt.checkpw(password, user.getPassword()));
|
||||
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser
|
||||
LoginUser loginUser = loginService.buildLoginUser(user);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验验证码
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param code 验证码
|
||||
* @param uuid 唯一标识
|
||||
*/
|
||||
private void validateCaptcha(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(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
|
||||
throw new CaptchaExpireException();
|
||||
}
|
||||
if (!code.equalsIgnoreCase(captcha)) {
|
||||
loginService.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
|
||||
throw new CaptchaException();
|
||||
}
|
||||
}
|
||||
|
||||
private SysUserVo loadUserByUsername(String username) {
|
||||
SysUserVo user = userService.selectUserByUserName(username);
|
||||
if (ObjectUtil.isNull(user)) {
|
||||
log.info("登录用户:{} 不存在.", username);
|
||||
throw new UserException("user.not.exists", username);
|
||||
} else if (SystemConstants.DISABLE.equals(user.getStatus())) {
|
||||
log.info("登录用户:{} 已被停用.", username);
|
||||
throw new UserException("user.blocked", username);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package org.dromara.web.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.constant.GlobalConstants;
|
||||
import org.dromara.common.core.constant.SystemConstants;
|
||||
import org.dromara.common.core.domain.model.LoginUser;
|
||||
import org.dromara.common.core.domain.model.SmsLoginBody;
|
||||
import org.dromara.common.core.enums.LoginType;
|
||||
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.json.utils.JsonUtils;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.system.domain.vo.SysUserVo;
|
||||
import org.dromara.system.service.SysUserService;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.dromara.web.service.IAuthStrategy;
|
||||
import org.dromara.web.service.SysLoginService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 短信认证策略
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("sms" + IAuthStrategy.BASE_NAME)
|
||||
public class SmsAuthStrategy implements IAuthStrategy {
|
||||
|
||||
@Autowired
|
||||
private SysLoginService loginService;
|
||||
@Autowired
|
||||
private SysUserService userService;
|
||||
|
||||
@Override
|
||||
public LoginVo login(String body, SysClientVo client) {
|
||||
SmsLoginBody loginBody = JsonUtils.parseObject(body, SmsLoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
String phonenumber = loginBody.getPhonenumber();
|
||||
String smsCode = loginBody.getSmsCode();
|
||||
|
||||
SysUserVo user = loadUserByPhonenumber(phonenumber);
|
||||
loginService.checkLogin(LoginType.SMS, user.getUserName(), () -> !validateSmsCode(phonenumber, smsCode));
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||
LoginUser loginUser = loginService.buildLoginUser(user);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验短信验证码
|
||||
*/
|
||||
private boolean validateSmsCode(String phonenumber, String smsCode) {
|
||||
String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + phonenumber);
|
||||
if (StringUtils.isBlank(code)) {
|
||||
loginService.recordLogininfor(phonenumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
|
||||
throw new CaptchaExpireException();
|
||||
}
|
||||
return code.equals(smsCode);
|
||||
}
|
||||
|
||||
private SysUserVo loadUserByPhonenumber(String phonenumber) {
|
||||
SysUserVo user = userService.selectUserByPhonenumber(phonenumber);
|
||||
if (ObjectUtil.isNull(user)) {
|
||||
log.info("登录用户:{} 不存在.", phonenumber);
|
||||
throw new UserException("user.not.exists", phonenumber);
|
||||
} else if (SystemConstants.DISABLE.equals(user.getStatus())) {
|
||||
log.info("登录用户:{} 已被停用.", phonenumber);
|
||||
throw new UserException("user.blocked", phonenumber);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package org.dromara.web.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.zhyd.oauth.model.AuthResponse;
|
||||
import me.zhyd.oauth.model.AuthUser;
|
||||
import org.dromara.common.core.constant.SystemConstants;
|
||||
import org.dromara.common.core.domain.model.LoginUser;
|
||||
import org.dromara.common.core.domain.model.SocialLoginBody;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.exception.user.UserException;
|
||||
import org.dromara.common.core.utils.ValidatorUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.common.social.config.properties.SocialProperties;
|
||||
import org.dromara.common.social.utils.SocialUtils;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.system.domain.vo.SysSocialVo;
|
||||
import org.dromara.system.domain.vo.SysUserVo;
|
||||
import org.dromara.system.service.SysUserService;
|
||||
import org.dromara.system.service.SysSocialService;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.dromara.web.service.IAuthStrategy;
|
||||
import org.dromara.web.service.SysLoginService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 第三方授权策略
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("social" + IAuthStrategy.BASE_NAME)
|
||||
@RequiredArgsConstructor
|
||||
public class SocialAuthStrategy implements IAuthStrategy {
|
||||
|
||||
private final SocialProperties socialProperties;
|
||||
private final SysSocialService sysSocialService;
|
||||
private final SysUserService userService;
|
||||
private final SysLoginService loginService;
|
||||
|
||||
/**
|
||||
* 登录-第三方授权登录
|
||||
*
|
||||
* @param body 登录信息
|
||||
* @param client 客户端信息
|
||||
*/
|
||||
@Override
|
||||
public LoginVo login(String body, SysClientVo client) {
|
||||
SocialLoginBody loginBody = JsonUtils.parseObject(body, SocialLoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
AuthResponse<AuthUser> response = SocialUtils.loginAuth(
|
||||
loginBody.getSource(), loginBody.getSocialCode(),
|
||||
loginBody.getSocialState(), socialProperties);
|
||||
if (!response.ok()) {
|
||||
throw new ServiceException(response.getMsg());
|
||||
}
|
||||
AuthUser authUserData = response.getData();
|
||||
|
||||
List<SysSocialVo> list = sysSocialService.selectByAuthId(authUserData.getSource() + authUserData.getUuid());
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
throw new ServiceException("你还没有绑定第三方账号,绑定后才可以登录!");
|
||||
}
|
||||
SysSocialVo social = list.get(0);
|
||||
|
||||
SysUserVo user = loadUser(social.getUserId());
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||
LoginUser loginUser = loginService.buildLoginUser(user);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private SysUserVo loadUser(Long userId) {
|
||||
SysUserVo user = userService.selectUserById(userId);
|
||||
if (ObjectUtil.isNull(user)) {
|
||||
log.info("登录用户:{} 不存在.", "");
|
||||
throw new UserException("user.not.exists", "");
|
||||
} else if (SystemConstants.DISABLE.equals(user.getStatus())) {
|
||||
log.info("登录用户:{} 已被停用.", "");
|
||||
throw new UserException("user.blocked", "");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
}
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,274 @@
|
||||
# AI模块配置
|
||||
ruoyi:
|
||||
ai:
|
||||
# 是否启用AI模块
|
||||
enabled: true
|
||||
|
||||
# 默认配置
|
||||
default:
|
||||
# 默认超时时间(秒)
|
||||
timeout: 30
|
||||
# 默认最大重试次数
|
||||
max-retries: 3
|
||||
# 默认最大并发数
|
||||
max-concurrency: 10
|
||||
# 默认权重
|
||||
weight: 1
|
||||
# 默认优先级
|
||||
priority: 0
|
||||
|
||||
# 缓存配置
|
||||
cache:
|
||||
# 是否启用缓存
|
||||
enabled: true
|
||||
# 缓存过期时间(分钟)
|
||||
expire-minutes: 60
|
||||
# 缓存键前缀
|
||||
key-prefix: "ai:chat:cache:"
|
||||
|
||||
# 健康检查配置
|
||||
health:
|
||||
# 健康检查间隔(秒)
|
||||
check-interval: 60
|
||||
# 健康检查超时时间(秒)
|
||||
check-timeout: 10
|
||||
# 缓存键前缀
|
||||
cache-prefix: "ai:health:"
|
||||
|
||||
# 流式响应配置
|
||||
stream:
|
||||
# 是否启用流式响应
|
||||
enabled: true
|
||||
# 流式响应缓冲区大小
|
||||
buffer-size: 1024
|
||||
# 流式响应超时时间(秒)
|
||||
timeout: 60
|
||||
|
||||
# 负载均衡配置
|
||||
load-balance:
|
||||
# 负载均衡策略(ROUND_ROBIN, WEIGHTED_ROUND_ROBIN, RANDOM, LEAST_CONNECTIONS)
|
||||
strategy: WEIGHTED_ROUND_ROBIN
|
||||
# 是否启用健康检查
|
||||
health-check: true
|
||||
|
||||
# 熔断器配置
|
||||
circuit-breaker:
|
||||
# 是否启用熔断器
|
||||
enabled: true
|
||||
# 失败阈值
|
||||
failure-threshold: 5
|
||||
# 恢复时间(秒)
|
||||
recovery-timeout: 30
|
||||
# 半开状态最大请求数
|
||||
half-open-max-calls: 3
|
||||
|
||||
# 限流配置
|
||||
rate-limit:
|
||||
# 是否启用限流
|
||||
enabled: true
|
||||
# 每秒最大请求数
|
||||
max-requests-per-second: 100
|
||||
# 突发请求数
|
||||
burst-capacity: 200
|
||||
|
||||
# 监控配置
|
||||
monitoring:
|
||||
# 是否启用监控
|
||||
enabled: true
|
||||
# 统计数据保留天数
|
||||
stats-retention-days: 30
|
||||
# 是否记录详细日志
|
||||
detailed-logging: false
|
||||
|
||||
# 故障转移配置
|
||||
failover:
|
||||
enabled: true
|
||||
check-interval-seconds: 30
|
||||
failure-threshold: 3
|
||||
recovery-threshold: 2
|
||||
|
||||
# 知识库配置
|
||||
knowledge:
|
||||
enabled: true
|
||||
default-search-count: 5
|
||||
similarity-threshold: 0.7
|
||||
max-context-length: 4000
|
||||
reranker-enabled: false
|
||||
embedding:
|
||||
base-url:
|
||||
default-model: bge-large-zh
|
||||
vector-store:
|
||||
type: db
|
||||
qdrant:
|
||||
base-url: http://localhost:6333
|
||||
api-key:
|
||||
collection: knowledge_vectors
|
||||
distance: Cosine
|
||||
weaviate:
|
||||
base-url: http://localhost:8080
|
||||
api-key:
|
||||
class-name: KnowledgeVector
|
||||
chunk:
|
||||
size: 1000
|
||||
overlap: 100
|
||||
|
||||
# 外部AI配置
|
||||
external-ai:
|
||||
# OpenAI配置
|
||||
openai:
|
||||
enabled: false
|
||||
api-key: ${OPENAI_API_KEY:}
|
||||
base-url: https://api.openai.com
|
||||
default-model: gpt-3.5-turbo
|
||||
timeout: 60s
|
||||
|
||||
# Azure配置
|
||||
azure:
|
||||
enabled: false
|
||||
api-key: ${AZURE_API_KEY:}
|
||||
endpoint: ${AZURE_ENDPOINT:}
|
||||
deployment-name: ${AZURE_DEPLOYMENT_NAME:}
|
||||
api-version: 2023-12-01-preview
|
||||
|
||||
# ChatGLM配置
|
||||
chatglm:
|
||||
enabled: false
|
||||
api-key: ${CHATGLM_API_KEY:}
|
||||
base-url: https://open.bigmodel.cn
|
||||
default-model: glm-4
|
||||
|
||||
# 通义千问配置
|
||||
qwen:
|
||||
enabled: false
|
||||
api-key: ${QWEN_API_KEY:}
|
||||
base-url: https://dashscope.aliyuncs.com
|
||||
default-model: qwen-turbo
|
||||
|
||||
# 智谱AI配置
|
||||
zhipu:
|
||||
enabled: false
|
||||
api-key: ${ZHIPU_API_KEY:}
|
||||
base-url: https://open.bigmodel.cn
|
||||
default-model: glm-4
|
||||
|
||||
# AI平台配置
|
||||
ai-platform:
|
||||
# FastGPT配置
|
||||
fastgpt:
|
||||
enabled: false
|
||||
base-url: ${FASTGPT_BASE_URL:}
|
||||
api-key: ${FASTGPT_API_KEY:}
|
||||
default-app-id: ${FASTGPT_DEFAULT_APP_ID:}
|
||||
|
||||
# Coze配置
|
||||
coze:
|
||||
enabled: false
|
||||
base-url: https://www.coze.com
|
||||
api-key: ${COZE_API_KEY:}
|
||||
default-bot-id: ${COZE_DEFAULT_BOT_ID:}
|
||||
|
||||
# DIFY配置
|
||||
dify:
|
||||
enabled: false
|
||||
base-url: ${DIFY_BASE_URL:}
|
||||
api-key: ${DIFY_API_KEY:}
|
||||
default-app-id: ${DIFY_DEFAULT_APP_ID:}
|
||||
|
||||
# Provider特定配置
|
||||
providers:
|
||||
# OpenAI配置
|
||||
openai:
|
||||
# API基础URL
|
||||
base-url: "https://api.openai.com/v1"
|
||||
# 默认模型
|
||||
default-model: "gpt-3.5-turbo"
|
||||
# 支持的模型列表
|
||||
supported-models:
|
||||
- "gpt-3.5-turbo"
|
||||
- "gpt-4"
|
||||
- "gpt-4-turbo"
|
||||
|
||||
# Azure OpenAI配置
|
||||
azure-openai:
|
||||
# API版本
|
||||
api-version: "2023-12-01-preview"
|
||||
# 默认模型
|
||||
default-model: "gpt-35-turbo"
|
||||
# 支持的模型列表
|
||||
supported-models:
|
||||
- "gpt-35-turbo"
|
||||
- "gpt-4"
|
||||
|
||||
# ChatGLM配置
|
||||
chatglm:
|
||||
# API基础URL
|
||||
base-url: "https://open.bigmodel.cn/api/paas/v4"
|
||||
# 默认模型
|
||||
default-model: "glm-4"
|
||||
# 支持的模型列表
|
||||
supported-models:
|
||||
- "glm-4"
|
||||
- "glm-3-turbo"
|
||||
|
||||
# 通义千问配置
|
||||
qwen:
|
||||
# API基础URL
|
||||
base-url: "https://dashscope.aliyuncs.com/api/v1"
|
||||
# 默认模型
|
||||
default-model: "qwen-turbo"
|
||||
# 支持的模型列表
|
||||
supported-models:
|
||||
- "qwen-turbo"
|
||||
- "qwen-plus"
|
||||
- "qwen-max"
|
||||
|
||||
# 智谱AI配置
|
||||
zhipu:
|
||||
# API基础URL
|
||||
base-url: "https://open.bigmodel.cn/api/paas/v4"
|
||||
# 默认模型
|
||||
default-model: "glm-4"
|
||||
# 支持的模型列表
|
||||
supported-models:
|
||||
- "glm-4"
|
||||
- "glm-3-turbo"
|
||||
|
||||
# FastGPT配置
|
||||
fastgpt:
|
||||
# 默认模型
|
||||
default-model: "fastgpt"
|
||||
# 支持的模型列表
|
||||
supported-models:
|
||||
- "fastgpt"
|
||||
|
||||
# Coze配置
|
||||
coze:
|
||||
# API基础URL
|
||||
base-url: "https://www.coze.cn/api"
|
||||
# 默认模型
|
||||
default-model: "coze"
|
||||
# 支持的模型列表
|
||||
supported-models:
|
||||
- "coze"
|
||||
|
||||
# Dify配置
|
||||
dify:
|
||||
# 默认模型
|
||||
default-model: "dify"
|
||||
# 支持的模型列表
|
||||
supported-models:
|
||||
- "dify"
|
||||
|
||||
# 知识库配置
|
||||
knowledge:
|
||||
# 是否启用
|
||||
enabled: true
|
||||
# 默认模型
|
||||
default-model: "knowledge"
|
||||
# 支持的模型列表
|
||||
supported-models:
|
||||
- "knowledge"
|
||||
# 相似度阈值
|
||||
similarity-threshold: 0.7
|
||||
# 最大返回结果数
|
||||
max-results: 10
|
||||
@@ -0,0 +1,337 @@
|
||||
--- # 监控中心配置
|
||||
spring.boot.admin.client:
|
||||
# 增加客户端开关
|
||||
enabled: false
|
||||
url: http://localhost:9090/admin
|
||||
instance:
|
||||
service-host-type: IP
|
||||
metadata:
|
||||
username: ${spring.boot.admin.client.username}
|
||||
userpassword: ${spring.boot.admin.client.password}
|
||||
username: @monitor.username@
|
||||
password: @monitor.password@
|
||||
|
||||
--- # snail-job 配置
|
||||
snail-job:
|
||||
enabled: false
|
||||
# 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务
|
||||
group: "ruoyi_group"
|
||||
# SnailJob 接入验证令牌 详见 script/sql/ry_job.sql `sj_group_config` 表
|
||||
token: "SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT"
|
||||
server:
|
||||
host: 127.0.0.1
|
||||
port: 17888
|
||||
# 命名空间UUID 详见 script/sql/ry_job.sql `sj_namespace`表`unique_id`字段
|
||||
namespace: ${spring.profiles.active}
|
||||
# 随主应用端口漂移
|
||||
port: 2${server.port}
|
||||
# 客户端ip指定
|
||||
host:
|
||||
# RPC类型: netty, grpc
|
||||
rpc-type: grpc
|
||||
|
||||
--- # 数据源配置
|
||||
spring:
|
||||
datasource:
|
||||
type: com.zaxxer.hikari.HikariDataSource
|
||||
# 动态数据源文档 https://www.kancloud.cn/tracy5546/dynamic-datasource/content
|
||||
dynamic:
|
||||
# 性能分析插件(有性能损耗 不建议生产环境使用)
|
||||
p6spy: true
|
||||
# 设置默认的数据源或者数据源组,默认值即为 master
|
||||
primary: master
|
||||
# 严格模式 匹配不到数据源则报错
|
||||
strict: true
|
||||
datasource:
|
||||
# 主库数据源
|
||||
master:
|
||||
type: ${spring.datasource.type}
|
||||
driverClassName: org.postgresql.Driver
|
||||
url: jdbc:postgresql://localhost:5432/postgres?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
|
||||
username: postgres
|
||||
password: 123456
|
||||
# # 从库数据源
|
||||
# slave:
|
||||
# lazy: true
|
||||
# type: ${spring.datasource.type}
|
||||
# driverClassName: com.mysql.cj.jdbc.Driver
|
||||
# url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
# username:
|
||||
# password:
|
||||
# oracle:
|
||||
# type: ${spring.datasource.type}
|
||||
# driverClassName: oracle.jdbc.OracleDriver
|
||||
# url: jdbc:oracle:thin:@//localhost:1521/XE
|
||||
# username: ROOT
|
||||
# password: root
|
||||
# postgres:
|
||||
# type: ${spring.datasource.type}
|
||||
# driverClassName: org.postgresql.Driver
|
||||
# url: jdbc:postgresql://localhost:5432/postgres?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
|
||||
# username: root
|
||||
# password: root
|
||||
# sqlserver:
|
||||
# type: ${spring.datasource.type}
|
||||
# driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
|
||||
# url: jdbc:sqlserver://localhost:1433;DatabaseName=tempdb;SelectMethod=cursor;encrypt=false;rewriteBatchedStatements=true
|
||||
# username: SA
|
||||
# password: root
|
||||
hikari:
|
||||
# 最大连接池数量
|
||||
maxPoolSize: 20
|
||||
# 最小空闲线程数量
|
||||
minIdle: 10
|
||||
# 配置获取连接等待超时的时间
|
||||
connectionTimeout: 30000
|
||||
# 校验超时时间
|
||||
validationTimeout: 5000
|
||||
# 空闲连接存活最大时间,默认10分钟
|
||||
idleTimeout: 600000
|
||||
# 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认30分钟
|
||||
maxLifetime: 1800000
|
||||
# 多久检查一次连接的活性
|
||||
keepaliveTime: 30000
|
||||
|
||||
--- # redis 单机配置(单机与集群只能开启一个另一个需要注释掉)
|
||||
spring.data:
|
||||
redis:
|
||||
# 地址
|
||||
host: localhost
|
||||
# 端口,默认为6379
|
||||
port: 6379
|
||||
# 数据库索引
|
||||
database: 0
|
||||
# redis 密码必须配置
|
||||
password: 123456
|
||||
# 连接超时时间
|
||||
timeout: 10s
|
||||
# 是否开启ssl连接
|
||||
ssl.enabled: false
|
||||
|
||||
# redisson 配置
|
||||
redisson:
|
||||
# redis key前缀
|
||||
keyPrefix:
|
||||
# 线程池数量
|
||||
threads: 4
|
||||
# Netty线程池数量
|
||||
nettyThreads: 8
|
||||
# 单节点配置
|
||||
singleServerConfig:
|
||||
# 客户端名称 不能用中文
|
||||
clientName: RuoYi-Vue-Plus
|
||||
# 最小空闲连接数
|
||||
connectionMinimumIdleSize: 8
|
||||
# 连接池大小
|
||||
connectionPoolSize: 32
|
||||
# 连接空闲超时,单位:毫秒
|
||||
idleConnectionTimeout: 10000
|
||||
# 命令等待超时,单位:毫秒
|
||||
timeout: 3000
|
||||
# 发布和订阅连接池大小
|
||||
subscriptionConnectionPoolSize: 50
|
||||
|
||||
--- # mail 邮件发送
|
||||
mail:
|
||||
enabled: false
|
||||
host: smtp.163.com
|
||||
port: 465
|
||||
# 是否需要用户名密码验证
|
||||
auth: true
|
||||
# 发送方,遵循RFC-822标准
|
||||
from: xxx@163.com
|
||||
# 用户名(注意:如果使用foxmail邮箱,此处user为qq号)
|
||||
user: xxx@163.com
|
||||
# 密码(注意,某些邮箱需要为SMTP服务单独设置密码,详情查看相关帮助)
|
||||
pass: xxxxxxxxxx
|
||||
# 使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。
|
||||
starttlsEnable: true
|
||||
# 使用SSL安全连接
|
||||
sslEnable: true
|
||||
# SMTP超时时长,单位毫秒,缺省值不超时
|
||||
timeout: 0
|
||||
# Socket连接超时值,单位毫秒,缺省值不超时
|
||||
connectionTimeout: 0
|
||||
|
||||
--- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
|
||||
# https://sms4j.com/doc3/ 差异配置文档地址 支持单厂商多配置,可以配置多个同时使用
|
||||
sms:
|
||||
# 配置源类型用于标定配置来源(interface,yaml)
|
||||
config-type: yaml
|
||||
# 用于标定yml中的配置是否开启短信拦截,接口配置不受此限制
|
||||
restricted: true
|
||||
# 短信拦截限制单手机号每分钟最大发送,只对开启了拦截的配置有效
|
||||
minute-max: 1
|
||||
# 短信拦截限制单手机号每日最大发送量,只对开启了拦截的配置有效
|
||||
account-max: 30
|
||||
# 以下配置来自于 org.dromara.sms4j.provider.config.BaseConfig类中
|
||||
blends:
|
||||
# 唯一ID 用于发送短信寻找具体配置 随便定义别用中文即可
|
||||
# 可以同时存在两个相同厂商 例如: ali1 ali2 两个不同的阿里短信账号
|
||||
config1:
|
||||
# 框架定义的厂商名称标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
|
||||
supplier: alibaba
|
||||
# 有些称为accessKey有些称之为apiKey,也有称为sdkKey或者appId。
|
||||
access-key-id: 您的accessKey
|
||||
# 称为accessSecret有些称之为apiSecret
|
||||
access-key-secret: 您的accessKeySecret
|
||||
signature: 您的短信签名
|
||||
sdk-app-id: 您的sdkAppId
|
||||
config2:
|
||||
# 厂商标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
|
||||
supplier: tencent
|
||||
access-key-id: 您的accessKey
|
||||
access-key-secret: 您的accessKeySecret
|
||||
signature: 您的短信签名
|
||||
sdk-app-id: 您的sdkAppId
|
||||
|
||||
|
||||
--- # 三方授权
|
||||
justauth:
|
||||
# 前端外网访问地址
|
||||
address: http://localhost:80
|
||||
type:
|
||||
maxkey:
|
||||
# maxkey 服务器地址
|
||||
# 注意 如下均配置均不需要修改 maxkey 已经内置好了数据
|
||||
server-url: http://sso.maxkey.top
|
||||
client-id: 876892492581044224
|
||||
client-secret: x1Y5MTMwNzIwMjMxNTM4NDc3Mzche8
|
||||
redirect-uri: ${justauth.address}/social-callback?source=maxkey
|
||||
topiam:
|
||||
# topiam 服务器地址
|
||||
server-url: http://127.0.0.1:1898/api/v1/authorize/y0q************spq***********8ol
|
||||
client-id: 449c4*********937************759
|
||||
client-secret: ac7***********1e0************28d
|
||||
redirect-uri: ${justauth.address}/social-callback?source=topiam
|
||||
scopes: [ openid, email, phone, profile ]
|
||||
qq:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=qq
|
||||
union-id: false
|
||||
weibo:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=weibo
|
||||
gitee:
|
||||
client-id: 9ceb2ba3150369c0278f69f7eab35ba41dd011a568368c65bb132125a4d95821
|
||||
client-secret: 9801fe69875da3b56f0773ae6737d416b6888f3f4af04d6bca5b63055728859c
|
||||
redirect-uri: ${justauth.address}/social-callback?source=gitee
|
||||
dingtalk:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=dingtalk
|
||||
baidu:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=baidu
|
||||
csdn:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=csdn
|
||||
coding:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=coding
|
||||
coding-group-name: xx
|
||||
oschina:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=oschina
|
||||
alipay_wallet:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=alipay_wallet
|
||||
alipay-public-key: MIIB**************DAQAB
|
||||
wechat_open:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=wechat_open
|
||||
wechat_mp:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=wechat_mp
|
||||
wechat_miniprogram:
|
||||
client-id: wx329e1cc66e69f88a
|
||||
client-secret: 181c3b076969a5cb935fb88f5fe6347b
|
||||
redirect-uri: ${justauth.address}/social-callback?source=wechat_miniprogram
|
||||
wechat_enterprise:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=wechat_enterprise
|
||||
agent-id: 1000002
|
||||
gitlab:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=gitlab
|
||||
gitea:
|
||||
# 前端改动 https://gitee.com/JavaLionLi/plus-ui/pulls/204
|
||||
# gitea 服务器地址
|
||||
server-url: https://demo.gitea.com
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=gitea
|
||||
|
||||
|
||||
--- # 微信平台
|
||||
wx:
|
||||
miniapp:
|
||||
configs:
|
||||
# 测试号
|
||||
- appid: wxb1e1ffda7cb636e9
|
||||
secret: 80bae6f0fd391a95a89f5ad867bd6991
|
||||
token: mvlcmI66IVHDLZP8ZFdCKy6okr2thHgr
|
||||
aesKey: #微信小程序消息服务器配置的EncodingAESKey
|
||||
msgDataFormat: JSON
|
||||
# 测试号
|
||||
- appid: wxb1e1ffda7cb636e1
|
||||
secret: 083ae010dae63a0e2efa851245581802
|
||||
token: mvlcmI66IVHDLZP8ZFdCKy6okr2thHgr
|
||||
aesKey: Jv70essZrhV4cguBvUh4nwu3bjVTQcRJeAUzqBBbdqz
|
||||
msgDataFormat: JSON
|
||||
mp:
|
||||
useRedis: false
|
||||
redisConfig:
|
||||
host:
|
||||
port:
|
||||
database:
|
||||
password:
|
||||
timeout:
|
||||
configs:
|
||||
# 测试号
|
||||
- appId: wxb1e1ffda7cb636e9
|
||||
secret: aab69b83d10226bd8608ec4cf6c6ec0c
|
||||
token: mvlcmI66IVHDLZP8ZFdCKy6okr2thHgr
|
||||
aesKey:
|
||||
oauth2RedirectUrl: http://192.168.0.3:8080/pages/home/main
|
||||
qrConnectRedirectUrl: http://192.168.0.3:8080/pages/home/main
|
||||
# 测试号
|
||||
- appId: wxb1e1ffda7cb636e1
|
||||
secret: aab69b83d10226bd8608ec4cf6c6ec0c
|
||||
token: mvlcmI66IVHDLZP8ZFdCKy6okr2thHgr
|
||||
aesKey: WCnqUoRueykM2E3oDsFAacXBUjioC10EfxwO2TMhYhT
|
||||
oauth2RedirectUrl: http://192.168.0.22:9090/#/pages/index
|
||||
qrConnectRedirectUrl: http://192.168.0.22:8080/#/pages/index
|
||||
|
||||
pay:
|
||||
alipay:
|
||||
appId: 2021003164606727
|
||||
mchId: 2088541440500474
|
||||
serverUrl: https://openapi.alipay.com/gateway.do
|
||||
alipayPublicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmDCp3S+3WiWZsZPrTZU40wgnb2LlGPxlUnPrlGGZipKtSx+qNrquh0gfWFFXx4PxArkJN2bh39eeUHlNb+WUTQJLq8eMa7jLw8WRpfi5e0O0zzwv8XGGqc3Rt97RAQqOTXTPUJ0hVMtr6DLX/0aE9p3kTh4+BK9pGo/o9HK/NDG5zKP6DDuAfAOpY48ktFmhZOHd799Ki8p7w2CRLxAmuicC8RaQsR7jv/AJLEIwF7WIAmJmviDDhjuHOyUaqoHEC14EHmVw68IBK5xcFct7QvRjJoi703zc7yXlzClRdkjw06PC8SQ267TRg0z1jYRPyDBzDRhjvv1rZPijtwC7JQIDAQAB
|
||||
privateKey: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCYMKndL7daJZmxk+tNlTjTCCdvYuUY/GVSc+uUYZmKkq1LH6o2uq6HSB9YUVfHg/ECuQk3ZuHf155QeU1v5ZRNAkurx4xruMvDxZGl+Ll7Q7TPPC/xcYapzdG33tEBCo5NdM9QnSFUy2voMtf/RoT2neROHj4Er2kaj+j0cr80MbnMo/oMO4B8A6ljjyS0WaFk4d3v30qLynvDYJEvECa6JwLxFpCxHuO/8AksQjAXtYgCYma+IMOGO4c7JRqqgcQLXgQeZXDrwgErnFwVy3tC9GMmiLvTfNzvJeXMKVF2SPDTo8LxJDbrtNGDTPWNhE/IMHMNGGO+/Wtk+KO3ALslAgMBAAECggEAUBx9rgLqHzffuYxFtqcwLFYEfCuwsQBbTZXbklX1/u3K7tqSPRDzwh6C8XiQHmQjv+0rHtC3YZ8cpPvVeWt+LzNgJeJ61lGGYV1kl9tft7UiPlxOWGgMHOJM3N8bYdLuqXMtlh/AZeRWvvnUnXm/kBn5De35c3nqd6L9W3/zikM5y9MuMQR23iSaNtoeBVGLkX/i9otf12+UrWv/loLJpqLCw8C5MT+zbXZgFZG/zBHjk6oUoiYB9NLUj0C+V9w6/ROGXoBAfBYEbxVgiQKIlSySdozYOlzt3Mp0FSS/RevjNoVHj4eDiOQZDxGkpzEg8LRSwhUn254/echQSyuimQKBgQDafIXftqpt9ts27S/q4GSHMB1qN9QiKJ6iZAAel98bEe42Zvum0nEUxxGQ2tADBtz+lnHBi4VvSrid9O1fsmih0FtXMZcG6hMBY49G1fXpbhGI6kSZBxKPeDgiLb0rgmvAdCBtgUx6ML50lW0dVwEXbobQ2LKVqOE/LNlnPU5QnwKBgQCyUh0q8Lqk4cXDg/qnYHbtv43CBOend/frBsSxrYKkf6CiV2kbKJFeBSjBOD8YtGad1Pm2EhloAotJUAh2nqybV4CQkBZUEYQDtM2b5yJF8xCn5lbf+S7rmij1zP0dyOtLdKiOMwtRdBFJJthw1gt6eFhpD+c5CvpqSTD00yHJuwKBgQCb98iZqwx+83oJ+8f5I7afyvk0miYVPGoCAuES9deOu34R1/JNZGzVKEah4ZIclwmrtDoAsFjQ2cZw/Cd36SRIXzTVSdFGXlKy6x+csaCawrhBxPqzQxk80dVAkOY56SCCgmOjyGmP0Lwk/YanKzTcRUp4TDkwHR6uupV33YvKgQKBgDqNKdSirLZdB8G2AUSaMRLJtfNCBwp/IuGCHG226lG3Mnh7uSBYxrqXeRVQsa8b9SHX/5JgCQWU6EVPSSgh2806AxX0qdA63B0XbffGAgPz1sE6qcXrHRPxT4e+IlJ4WYIyMPJYIlxBfzeE0MbkEWrKP0VoGmUpjKX/mFqbRNnnAoGAf3YEakCzCyd6PylB+gWsajoQfiZOaOkwReX8/5P7atVmpNSNPOaehvtQE8Bd8lu/9Bh3LYfpb2+giVVsdujqPDShez+q6hSMyrW1TSw8we7iY1J0J0r9RwxAdokdLH2J8JX6LuWP7NY9VHDNXbERcDVjat15gukJmuK85KqMFSc=
|
||||
notifyUrl: http://bdhdm3.natappfree.cc/no-auth/ali/notify/trade
|
||||
returnUrl: xxxxpay/notify
|
||||
signType: RSA2
|
||||
charset: UTF-8
|
||||
wechat:
|
||||
appMp1Id: wxb7250f9a052e9015
|
||||
appMp2Id: wx11fde1504adee855
|
||||
mchId: 1635571318
|
||||
mchSerialNo: 3177DDC954F864ADE851086DB3E24A7AB932A711
|
||||
privateKey: -----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDG0SuHJE8dXLNk
|
||||
-----END PRIVATE KEY-----
|
||||
apiV3Key: geneface27bdaf31d54447d8b7b7c7d7
|
||||
notifyUrl: https://ppm-stage.geneface.com/gf-api
|
||||
@@ -0,0 +1,276 @@
|
||||
--- # 临时文件存储位置 避免临时文件被系统清理报错
|
||||
spring.servlet.multipart.location: /ruoyi/server/temp
|
||||
|
||||
--- # 监控中心配置
|
||||
spring.boot.admin.client:
|
||||
# 增加客户端开关
|
||||
enabled: true
|
||||
url: http://localhost:9090/admin
|
||||
instance:
|
||||
service-host-type: IP
|
||||
metadata:
|
||||
username: ${spring.boot.admin.client.username}
|
||||
userpassword: ${spring.boot.admin.client.password}
|
||||
username: @monitor.username@
|
||||
password: @monitor.password@
|
||||
|
||||
--- # snail-job 配置
|
||||
snail-job:
|
||||
enabled: true
|
||||
# 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务
|
||||
group: "ruoyi_group"
|
||||
# SnailJob 接入验证令牌 详见 script/sql/ry_job.sql `sj_group_config`表
|
||||
token: "SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT"
|
||||
server:
|
||||
host: 127.0.0.1
|
||||
port: 17888
|
||||
# 命名空间UUID 详见 script/sql/ry_job.sql `sj_namespace`表`unique_id`字段
|
||||
namespace: ${spring.profiles.active}
|
||||
# 随主应用端口漂移
|
||||
port: 2${server.port}
|
||||
# 客户端ip指定
|
||||
host:
|
||||
# RPC类型: netty, grpc
|
||||
rpc-type: grpc
|
||||
|
||||
--- # 数据源配置
|
||||
spring:
|
||||
datasource:
|
||||
type: com.zaxxer.hikari.HikariDataSource
|
||||
# 动态数据源文档 https://www.kancloud.cn/tracy5546/dynamic-datasource/content
|
||||
dynamic:
|
||||
# 性能分析插件(有性能损耗 不建议生产环境使用)
|
||||
p6spy: false
|
||||
# 设置默认的数据源或者数据源组,默认值即为 master
|
||||
primary: master
|
||||
# 严格模式 匹配不到数据源则报错
|
||||
strict: true
|
||||
datasource:
|
||||
# 主库数据源
|
||||
master:
|
||||
type: ${spring.datasource.type}
|
||||
driverClassName: org.postgresql.Driver
|
||||
url: jdbc:postgresql://localhost:5432/postgres?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
|
||||
username: postgres
|
||||
password: 123456
|
||||
# # 从库数据源
|
||||
# slave:
|
||||
# lazy: true
|
||||
# type: ${spring.datasource.type}
|
||||
# driverClassName: com.mysql.cj.jdbc.Driver
|
||||
# url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
# username:
|
||||
# password:
|
||||
# oracle:
|
||||
# type: ${spring.datasource.type}
|
||||
# driverClassName: oracle.jdbc.OracleDriver
|
||||
# url: jdbc:oracle:thin:@//localhost:1521/XE
|
||||
# username: ROOT
|
||||
# password: root
|
||||
# postgres:
|
||||
# type: ${spring.datasource.type}
|
||||
# driverClassName: org.postgresql.Driver
|
||||
# url: jdbc:postgresql://localhost:5432/postgres?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
|
||||
# username: root
|
||||
# password: root
|
||||
# sqlserver:
|
||||
# type: ${spring.datasource.type}
|
||||
# driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
|
||||
# url: jdbc:sqlserver://localhost:1433;DatabaseName=tempdb;SelectMethod=cursor;encrypt=false;rewriteBatchedStatements=true
|
||||
# username: SA
|
||||
# password: root
|
||||
hikari:
|
||||
# 最大连接池数量
|
||||
maxPoolSize: 20
|
||||
# 最小空闲线程数量
|
||||
minIdle: 10
|
||||
# 配置获取连接等待超时的时间
|
||||
connectionTimeout: 30000
|
||||
# 校验超时时间
|
||||
validationTimeout: 5000
|
||||
# 空闲连接存活最大时间,默认10分钟
|
||||
idleTimeout: 600000
|
||||
# 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认30分钟
|
||||
maxLifetime: 1800000
|
||||
# 多久检查一次连接的活性
|
||||
keepaliveTime: 30000
|
||||
|
||||
--- # redis 单机配置(单机与集群只能开启一个另一个需要注释掉)
|
||||
spring.data:
|
||||
redis:
|
||||
# 地址
|
||||
host: localhost
|
||||
# 端口,默认为6379
|
||||
port: 6379
|
||||
# 数据库索引
|
||||
database: 0
|
||||
# redis 密码必须配置
|
||||
password: ruoyi123
|
||||
# 连接超时时间
|
||||
timeout: 10s
|
||||
# 是否开启ssl
|
||||
ssl.enabled: false
|
||||
|
||||
# redisson 配置
|
||||
redisson:
|
||||
# redis key前缀
|
||||
keyPrefix:
|
||||
# 线程池数量
|
||||
threads: 16
|
||||
# Netty线程池数量
|
||||
nettyThreads: 32
|
||||
# 单节点配置
|
||||
singleServerConfig:
|
||||
# 客户端名称 不能用中文
|
||||
clientName: RuoYi-Vue-Plus
|
||||
# 最小空闲连接数
|
||||
connectionMinimumIdleSize: 32
|
||||
# 连接池大小
|
||||
connectionPoolSize: 64
|
||||
# 连接空闲超时,单位:毫秒
|
||||
idleConnectionTimeout: 10000
|
||||
# 命令等待超时,单位:毫秒
|
||||
timeout: 3000
|
||||
# 发布和订阅连接池大小
|
||||
subscriptionConnectionPoolSize: 50
|
||||
|
||||
--- # mail 邮件发送
|
||||
mail:
|
||||
enabled: false
|
||||
host: smtp.163.com
|
||||
port: 465
|
||||
# 是否需要用户名密码验证
|
||||
auth: true
|
||||
# 发送方,遵循RFC-822标准
|
||||
from: xxx@163.com
|
||||
# 用户名(注意:如果使用foxmail邮箱,此处user为qq号)
|
||||
user: xxx@163.com
|
||||
# 密码(注意,某些邮箱需要为SMTP服务单独设置密码,详情查看相关帮助)
|
||||
pass: xxxxxxxxxx
|
||||
# 使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。
|
||||
starttlsEnable: true
|
||||
# 使用SSL安全连接
|
||||
sslEnable: true
|
||||
# SMTP超时时长,单位毫秒,缺省值不超时
|
||||
timeout: 0
|
||||
# Socket连接超时值,单位毫秒,缺省值不超时
|
||||
connectionTimeout: 0
|
||||
|
||||
--- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
|
||||
# https://sms4j.com/doc3/ 差异配置文档地址 支持单厂商多配置,可以配置多个同时使用
|
||||
sms:
|
||||
# 配置源类型用于标定配置来源(interface,yaml)
|
||||
config-type: yaml
|
||||
# 用于标定yml中的配置是否开启短信拦截,接口配置不受此限制
|
||||
restricted: true
|
||||
# 短信拦截限制单手机号每分钟最大发送,只对开启了拦截的配置有效
|
||||
minute-max: 1
|
||||
# 短信拦截限制单手机号每日最大发送量,只对开启了拦截的配置有效
|
||||
account-max: 30
|
||||
# 以下配置来自于 org.dromara.sms4j.provider.config.BaseConfig类中
|
||||
blends:
|
||||
# 唯一ID 用于发送短信寻找具体配置 随便定义别用中文即可
|
||||
# 可以同时存在两个相同厂商 例如: ali1 ali2 两个不同的阿里短信账号
|
||||
config1:
|
||||
# 框架定义的厂商名称标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
|
||||
supplier: alibaba
|
||||
# 有些称为accessKey有些称之为apiKey,也有称为sdkKey或者appId。
|
||||
access-key-id: 您的accessKey
|
||||
# 称为accessSecret有些称之为apiSecret
|
||||
access-key-secret: 您的accessKeySecret
|
||||
signature: 您的短信签名
|
||||
sdk-app-id: 您的sdkAppId
|
||||
config2:
|
||||
# 厂商标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
|
||||
supplier: tencent
|
||||
access-key-id: 您的accessKey
|
||||
access-key-secret: 您的accessKeySecret
|
||||
signature: 您的短信签名
|
||||
sdk-app-id: 您的sdkAppId
|
||||
|
||||
# 三方授权
|
||||
justauth:
|
||||
# 前端外网访问地址
|
||||
address: http://localhost:80
|
||||
type:
|
||||
maxkey:
|
||||
# maxkey 服务器地址
|
||||
# 注意 如下均配置均不需要修改 maxkey 已经内置好了数据
|
||||
server-url: http://sso.maxkey.top
|
||||
client-id: 876892492581044224
|
||||
client-secret: x1Y5MTMwNzIwMjMxNTM4NDc3Mzche8
|
||||
redirect-uri: ${justauth.address}/social-callback?source=maxkey
|
||||
topiam:
|
||||
# topiam 服务器地址
|
||||
server-url: http://127.0.0.1:1989/api/v1/authorize/y0q************spq***********8ol
|
||||
client-id: 449c4*********937************759
|
||||
client-secret: ac7***********1e0************28d
|
||||
redirect-uri: ${justauth.address}/social-callback?source=topiam
|
||||
scopes: [ openid, email, phone, profile ]
|
||||
qq:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=qq
|
||||
union-id: false
|
||||
weibo:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=weibo
|
||||
gitee:
|
||||
client-id: 91436b7940090d09c72c7daf85b959cfd5f215d67eea73acbf61b6b590751a98
|
||||
client-secret: 02c6fcfd70342980cd8dd2f2c06c1a350645d76c754d7a264c4e125f9ba915ac
|
||||
redirect-uri: ${justauth.address}/social-callback?source=gitee
|
||||
dingtalk:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=dingtalk
|
||||
baidu:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=baidu
|
||||
csdn:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=csdn
|
||||
coding:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=coding
|
||||
coding-group-name: xx
|
||||
oschina:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=oschina
|
||||
alipay_wallet:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=alipay_wallet
|
||||
alipay-public-key: MIIB**************DAQAB
|
||||
wechat_open:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=wechat_open
|
||||
wechat_mp:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=wechat_mp
|
||||
wechat_miniprogram:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=wechat_miniprogram
|
||||
wechat_enterprise:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=wechat_enterprise
|
||||
agent-id: 1000002
|
||||
gitlab:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=gitlab
|
||||
gitea:
|
||||
# 前端改动 https://gitee.com/JavaLionLi/plus-ui/pulls/204
|
||||
# gitea 服务器地址
|
||||
server-url: https://demo.gitea.com
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=gitea
|
||||
@@ -0,0 +1,274 @@
|
||||
# 开发环境配置
|
||||
server:
|
||||
# 服务器的HTTP端口,默认为8080
|
||||
port: 8080
|
||||
servlet:
|
||||
# 应用的访问路径
|
||||
context-path: /
|
||||
# undertow 配置
|
||||
undertow:
|
||||
# HTTP post内容的最大大小。当值为-1时,默认值为大小是无限的
|
||||
max-http-post-size: -1
|
||||
# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
|
||||
# 每块buffer的空间大小,越小的空间被利用越充分
|
||||
buffer-size: 512
|
||||
# 是否分配的直接内存
|
||||
direct-buffers: true
|
||||
threads:
|
||||
# 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程
|
||||
io: 8
|
||||
# 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载
|
||||
worker: 256
|
||||
|
||||
captcha:
|
||||
# 是否启用验证码校验
|
||||
enable: true
|
||||
# 验证码类型 math 数组计算 char 字符验证
|
||||
type: MATH
|
||||
# line 线段干扰 circle 圆圈干扰 shear 扭曲干扰
|
||||
category: CIRCLE
|
||||
# 数字验证码位数
|
||||
numberLength: 1
|
||||
# 字符验证码长度
|
||||
charLength: 4
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
org.dromara: @logging.level@
|
||||
org.springframework: warn
|
||||
org.mybatis.spring.mapper: error
|
||||
org.apache.fury: warn
|
||||
config: classpath:logback-plus.xml
|
||||
|
||||
# 用户配置
|
||||
user:
|
||||
password:
|
||||
# 密码最大错误次数
|
||||
maxRetryCount: 5
|
||||
# 密码锁定时间(默认10分钟)
|
||||
lockTime: 10
|
||||
auth:
|
||||
# 验证码配置
|
||||
captcha:
|
||||
# App端验证码开关
|
||||
enabled: false
|
||||
# 验证码类型 math 数组计算 char 字符验证
|
||||
type: math
|
||||
# 白名单配置(多个路径用逗号分隔)
|
||||
whitelist: /app/user/profile,/app/user/bind,/app/auth/logout
|
||||
|
||||
# Spring配置
|
||||
spring:
|
||||
application:
|
||||
name: RuoYi-Vue-Plus
|
||||
threads:
|
||||
# 开启虚拟线程 仅jdk21可用
|
||||
virtual:
|
||||
enabled: false
|
||||
# 资源信息
|
||||
messages:
|
||||
# 国际化资源文件路径
|
||||
basename: i18n/messages
|
||||
profiles:
|
||||
active: @profiles.active@
|
||||
# 文件上传
|
||||
servlet:
|
||||
multipart:
|
||||
# 单个文件大小
|
||||
max-file-size: 10MB
|
||||
# 设置总上传的文件大小
|
||||
max-request-size: 20MB
|
||||
mvc:
|
||||
# 设置静态资源路径 防止所有请求都去查静态资源
|
||||
static-path-pattern: /static/**
|
||||
format:
|
||||
date-time: yyyy-MM-dd HH:mm:ss
|
||||
jackson:
|
||||
# 日期格式化
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
serialization:
|
||||
# 格式化输出
|
||||
indent_output: false
|
||||
# 忽略无法转换的对象
|
||||
fail_on_empty_beans: false
|
||||
deserialization:
|
||||
# 允许对象忽略json中不存在的属性
|
||||
fail_on_unknown_properties: false
|
||||
|
||||
# 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
|
||||
|
||||
# security配置
|
||||
security:
|
||||
# 排除路径
|
||||
excludes:
|
||||
- /*.html
|
||||
- /**/*.html
|
||||
- /**/*.css
|
||||
- /**/*.js
|
||||
- /favicon.ico
|
||||
- /error
|
||||
- /*/api-docs
|
||||
- /*/api-docs/**
|
||||
- /warm-flow-ui/config
|
||||
- /chat/**
|
||||
|
||||
# MyBatisPlus配置
|
||||
# https://baomidou.com/config/
|
||||
mybatis-plus:
|
||||
# 自定义配置 是否全局开启逻辑删除 关闭后 所有逻辑删除功能将失效
|
||||
enableLogicDelete: true
|
||||
# 多包名使用 例如 org.dromara.**.mapper,org.xxx.**.mapper
|
||||
mapperPackage: org.dromara.**.mapper
|
||||
# 对应的 XML 文件位置
|
||||
mapperLocations: classpath*:mapper/**/*Mapper.xml
|
||||
# 实体扫描,多个package用逗号或者分号分隔
|
||||
typeAliasesPackage: org.dromara.**.domain
|
||||
global-config:
|
||||
dbConfig:
|
||||
# 主键类型
|
||||
# AUTO 自增 NONE 空 INPUT 用户输入 ASSIGN_ID 雪花 ASSIGN_UUID 唯一 UUID
|
||||
# 如需改为自增 需要将数据库表全部设置为自增
|
||||
idType: ASSIGN_ID
|
||||
|
||||
# 数据加密
|
||||
mybatis-encryptor:
|
||||
# 是否开启加密
|
||||
enable: false
|
||||
# 默认加密算法
|
||||
algorithm: BASE64
|
||||
# 编码方式 BASE64/HEX。默认BASE64
|
||||
encode: BASE64
|
||||
# 安全秘钥 对称算法的秘钥 如:AES,SM4
|
||||
password:
|
||||
# 公私钥 非对称算法的公私钥 如:SM2,RSA
|
||||
publicKey:
|
||||
privateKey:
|
||||
|
||||
# api接口加密
|
||||
api-decrypt:
|
||||
# 是否开启全局接口加密
|
||||
enabled: true
|
||||
# AES 加密头标识
|
||||
headerFlag: encrypt-key
|
||||
# 响应加密公钥 非对称算法的公私钥 如:SM2,RSA 使用者请自行更换
|
||||
# 对应前端解密私钥 MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=
|
||||
publicKey: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJnNwrj4hi/y3CCJu868ghCG5dUj8wZK++RNlTLcXoMmdZWEQ/u02RgD5LyLAXGjLOjbMtC+/J9qofpSGTKSx/MCAwEAAQ==
|
||||
# 请求解密私钥 非对称算法的公私钥 如:SM2,RSA 使用者请自行更换
|
||||
# 对应前端加密公钥 MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==
|
||||
privateKey: MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKNPuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gAkM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWowcSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99EcvDQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthhYhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3UP8iWi1Qw0Y=
|
||||
|
||||
springdoc:
|
||||
api-docs:
|
||||
# 是否开启接口文档
|
||||
enabled: true
|
||||
info:
|
||||
# 标题
|
||||
title: '标题:RuoYi-Vue-Plus管理系统_接口文档'
|
||||
# 描述
|
||||
description: '描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...'
|
||||
# 版本
|
||||
version: '版本号: ${project.version}'
|
||||
# 作者信息
|
||||
contact:
|
||||
name: Lion Li
|
||||
email: crazylionli@163.com
|
||||
url: https://gitee.com/dromara/RuoYi-Vue-Plus
|
||||
components:
|
||||
# 鉴权方式配置
|
||||
security-schemes:
|
||||
apiKey:
|
||||
type: APIKEY
|
||||
in: HEADER
|
||||
name: ${sa-token.token-name}
|
||||
#这里定义了两个分组,可定义多个,也可以不定义
|
||||
group-configs:
|
||||
- group: 1.演示模块
|
||||
packages-to-scan: org.dromara.demo
|
||||
- group: 2.通用模块
|
||||
packages-to-scan: org.dromara.web
|
||||
- group: 3.系统模块
|
||||
packages-to-scan: org.dromara.system
|
||||
- group: 4.代码生成模块
|
||||
packages-to-scan: org.dromara.generator
|
||||
- group: 5.工作流模块
|
||||
packages-to-scan: org.dromara.workflow
|
||||
- group: 6.App端模块
|
||||
packages-to-scan: org.dromara.app
|
||||
|
||||
# 防止XSS攻击
|
||||
xss:
|
||||
# 过滤开关
|
||||
enabled: true
|
||||
# 排除链接(多个用逗号分隔)
|
||||
excludeUrls:
|
||||
- /system/notice
|
||||
|
||||
# 全局线程池相关配置
|
||||
# 如使用JDK21请直接使用虚拟线程 不要开启此配置
|
||||
thread-pool:
|
||||
# 是否开启线程池
|
||||
enabled: false
|
||||
# 队列最大长度
|
||||
queueCapacity: 128
|
||||
# 线程池维护线程所允许的空闲时间
|
||||
keepAliveSeconds: 300
|
||||
|
||||
--- # 分布式锁 lock4j 全局配置
|
||||
lock4j:
|
||||
# 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
acquire-timeout: 3000
|
||||
# 分布式锁的超时时间,默认为 30 秒
|
||||
expire: 30000
|
||||
|
||||
--- # Actuator 监控端点的配置项
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: '*'
|
||||
endpoint:
|
||||
health:
|
||||
show-details: ALWAYS
|
||||
logfile:
|
||||
external-file: ./logs/sys-console.log
|
||||
|
||||
--- # 默认/推荐使用sse推送
|
||||
sse:
|
||||
enabled: true
|
||||
path: /resource/sse
|
||||
|
||||
--- # websocket
|
||||
websocket:
|
||||
# 如果关闭 需要和前端开关一起关闭
|
||||
enabled: false
|
||||
# 路径
|
||||
path: /resource/websocket
|
||||
# 设置访问源地址
|
||||
allowedOrigins: '*'
|
||||
|
||||
--- # warm-flow工作流配置
|
||||
warm-flow:
|
||||
# 是否开启工作流,默认true
|
||||
enabled: false
|
||||
# 是否开启设计器ui
|
||||
ui: true
|
||||
# 默认Authorization,如果有多个token,用逗号分隔
|
||||
token-name: ${sa-token.token-name},clientid
|
||||
# 流程状态对应的三元色
|
||||
chart-status-color:
|
||||
## 未办理
|
||||
- 62,62,62
|
||||
## 待办理
|
||||
- 255,205,23
|
||||
## 已办理
|
||||
- 157,255,0
|
||||
@@ -0,0 +1,8 @@
|
||||
Application Version: ${revision}
|
||||
Spring Boot Version: ${spring-boot.version}
|
||||
__________ _____.___.__ ____ ____ __________.__
|
||||
\______ \__ __ ____\__ | |__| \ \ / /_ __ ____ \______ \ | __ __ ______
|
||||
| _/ | \/ _ \/ | | | ______ \ Y / | \_/ __ \ ______ | ___/ | | | \/ ___/
|
||||
| | \ | ( <_> )____ | | /_____/ \ /| | /\ ___/ /_____/ | | | |_| | /\___ \
|
||||
|____|_ /____/ \____// ______|__| \___/ |____/ \___ > |____| |____/____//____ >
|
||||
\/ \/ \/ \/
|
||||
@@ -0,0 +1,56 @@
|
||||
#错误消息
|
||||
not.null=* 必须填写
|
||||
user.jcaptcha.error=验证码错误
|
||||
user.jcaptcha.expire=验证码已失效
|
||||
user.not.exists=对不起, 您的账号:{0} 不存在.
|
||||
user.password.not.match=用户不存在/密码错误
|
||||
user.password.retry.limit.count=密码输入错误{0}次
|
||||
user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟
|
||||
user.password.delete=对不起,您的账号:{0} 已被删除
|
||||
user.blocked=对不起,您的账号:{0} 已禁用,请联系管理员
|
||||
role.blocked=角色已封禁,请联系管理员
|
||||
user.logout.success=退出成功
|
||||
length.not.valid=长度必须在{min}到{max}个字符之间
|
||||
user.username.not.blank=用户名不能为空
|
||||
user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头
|
||||
user.username.length.valid=账户长度必须在{min}到{max}个字符之间
|
||||
user.password.not.blank=用户密码不能为空
|
||||
user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间
|
||||
user.password.not.valid=* 5-50个字符
|
||||
user.email.not.valid=邮箱格式错误
|
||||
user.email.not.blank=邮箱不能为空
|
||||
user.phonenumber.not.blank=用户手机号不能为空
|
||||
user.mobile.phone.number.not.valid=手机号格式错误
|
||||
user.login.success=登录成功
|
||||
user.register.success=注册成功
|
||||
user.register.save.error=保存用户 {0} 失败,注册账号已存在
|
||||
user.register.error=注册失败,请联系系统管理人员
|
||||
user.notfound=请重新登录
|
||||
user.forcelogout=管理员强制退出,请重新登录
|
||||
user.unknown.error=未知错误,请重新登录
|
||||
auth.grant.type.error=认证权限类型错误
|
||||
auth.grant.type.blocked=认证权限类型已禁用
|
||||
auth.grant.type.not.blank=认证权限类型不能为空
|
||||
auth.clientid.not.blank=认证客户端id不能为空
|
||||
##文件上传消息
|
||||
upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB!
|
||||
upload.filename.exceed.length=上传的文件名最长{0}个字符
|
||||
##权限
|
||||
no.permission=您没有数据的权限,请联系管理员添加权限 [{0}]
|
||||
no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}]
|
||||
no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}]
|
||||
no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}]
|
||||
no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}]
|
||||
no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}]
|
||||
repeat.submit.message=不允许重复提交,请稍候再试
|
||||
rate.limiter.message=访问过于频繁,请稍候再试
|
||||
sms.code.not.blank=短信验证码不能为空
|
||||
sms.code.retry.limit.count=短信验证码输入错误{0}次
|
||||
sms.code.retry.limit.exceed=短信验证码输入错误{0}次,帐户锁定{1}分钟
|
||||
email.code.not.blank=邮箱验证码不能为空
|
||||
email.code.retry.limit.count=邮箱验证码输入错误{0}次
|
||||
email.code.retry.limit.exceed=邮箱验证码输入错误{0}次,帐户锁定{1}分钟
|
||||
xcx.code.not.blank=小程序[code]不能为空
|
||||
social.source.not.blank=第三方登录平台[source]不能为空
|
||||
social.code.not.blank=第三方登录平台[code]不能为空
|
||||
social.state.not.blank=第三方登录平台[state]不能为空
|
||||
@@ -0,0 +1,56 @@
|
||||
#错误消息
|
||||
not.null=* Required fill in
|
||||
user.jcaptcha.error=Captcha error
|
||||
user.jcaptcha.expire=Captcha invalid
|
||||
user.not.exists=Sorry, your account: {0} does not exist
|
||||
user.password.not.match=User does not exist/Password error
|
||||
user.password.retry.limit.count=Password input error {0} times
|
||||
user.password.retry.limit.exceed=Password input error {0} times, account locked for {1} minutes
|
||||
user.password.delete=Sorry, your account:{0} has been deleted
|
||||
user.blocked=Sorry, your account: {0} has been disabled. Please contact the administrator
|
||||
role.blocked=Role disabled,please contact administrators
|
||||
user.logout.success=Exit successful
|
||||
length.not.valid=The length must be between {min} and {max} characters
|
||||
user.username.not.blank=Username cannot be blank
|
||||
user.username.not.valid=* 2 to 20 chinese characters, letters, numbers or underscores, and must start with a non number
|
||||
user.username.length.valid=Account length must be between {min} and {max} characters
|
||||
user.password.not.blank=Password cannot be empty
|
||||
user.password.length.valid=Password length must be between {min} and {max} characters
|
||||
user.password.not.valid=* 5-50 characters
|
||||
user.email.not.valid=Mailbox format error
|
||||
user.email.not.blank=Mailbox cannot be blank
|
||||
user.phonenumber.not.blank=Phone number cannot be blank
|
||||
user.mobile.phone.number.not.valid=Phone number format error
|
||||
user.login.success=Login successful
|
||||
user.register.success=Register successful
|
||||
user.register.save.error=Failed to save user {0}, The registered account already exists
|
||||
user.register.error=Register failed, please contact system administrator
|
||||
user.notfound=Please login again
|
||||
user.forcelogout=The administrator is forced to exit,please login again
|
||||
user.unknown.error=Unknown error, please login again
|
||||
auth.grant.type.error=Auth grant type error
|
||||
auth.grant.type.blocked=Auth grant type disabled
|
||||
auth.grant.type.not.blank=Auth grant type cannot be blank
|
||||
auth.clientid.not.blank=Auth clientid cannot be blank
|
||||
##文件上传消息
|
||||
upload.exceed.maxSize=The uploaded file size exceeds the limit file size!<br/>the maximum allowed file size is:{0}MB!
|
||||
upload.filename.exceed.length=The maximum length of uploaded file name is {0} characters
|
||||
##权限
|
||||
no.permission=You do not have permission to the data,please contact your administrator to add permissions [{0}]
|
||||
no.create.permission=You do not have permission to create data,please contact your administrator to add permissions [{0}]
|
||||
no.update.permission=You do not have permission to modify data,please contact your administrator to add permissions [{0}]
|
||||
no.delete.permission=You do not have permission to delete data,please contact your administrator to add permissions [{0}]
|
||||
no.export.permission=You do not have permission to export data,please contact your administrator to add permissions [{0}]
|
||||
no.view.permission=You do not have permission to view data,please contact your administrator to add permissions [{0}]
|
||||
repeat.submit.message=Repeat submit is not allowed, please try again later
|
||||
rate.limiter.message=Visit too frequently, please try again later
|
||||
sms.code.not.blank=Sms code cannot be blank
|
||||
sms.code.retry.limit.count=Sms code input error {0} times
|
||||
sms.code.retry.limit.exceed=Sms code input error {0} times, account locked for {1} minutes
|
||||
email.code.not.blank=Email code cannot be blank
|
||||
email.code.retry.limit.count=Email code input error {0} times
|
||||
email.code.retry.limit.exceed=Email code input error {0} times, account locked for {1} minutes
|
||||
xcx.code.not.blank=Mini program [code] cannot be blank
|
||||
social.source.not.blank=Social login platform [source] cannot be blank
|
||||
social.code.not.blank=Social login platform [code] cannot be blank
|
||||
social.state.not.blank=Social login platform [state] cannot be blank
|
||||
@@ -0,0 +1,56 @@
|
||||
#错误消息
|
||||
not.null=* 必须填写
|
||||
user.jcaptcha.error=验证码错误
|
||||
user.jcaptcha.expire=验证码已失效
|
||||
user.not.exists=对不起, 您的账号:{0} 不存在.
|
||||
user.password.not.match=用户不存在/密码错误
|
||||
user.password.retry.limit.count=密码输入错误{0}次
|
||||
user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟
|
||||
user.password.delete=对不起,您的账号:{0} 已被删除
|
||||
user.blocked=对不起,您的账号:{0} 已禁用,请联系管理员
|
||||
role.blocked=角色已封禁,请联系管理员
|
||||
user.logout.success=退出成功
|
||||
length.not.valid=长度必须在{min}到{max}个字符之间
|
||||
user.username.not.blank=用户名不能为空
|
||||
user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头
|
||||
user.username.length.valid=账户长度必须在{min}到{max}个字符之间
|
||||
user.password.not.blank=用户密码不能为空
|
||||
user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间
|
||||
user.password.not.valid=* 5-50个字符
|
||||
user.email.not.valid=邮箱格式错误
|
||||
user.email.not.blank=邮箱不能为空
|
||||
user.phonenumber.not.blank=用户手机号不能为空
|
||||
user.mobile.phone.number.not.valid=手机号格式错误
|
||||
user.login.success=登录成功
|
||||
user.register.success=注册成功
|
||||
user.register.save.error=保存用户 {0} 失败,注册账号已存在
|
||||
user.register.error=注册失败,请联系系统管理人员
|
||||
user.notfound=请重新登录
|
||||
user.forcelogout=管理员强制退出,请重新登录
|
||||
user.unknown.error=未知错误,请重新登录
|
||||
auth.grant.type.error=认证权限类型错误
|
||||
auth.grant.type.blocked=认证权限类型已禁用
|
||||
auth.grant.type.not.blank=认证权限类型不能为空
|
||||
auth.clientid.not.blank=认证客户端id不能为空
|
||||
##文件上传消息
|
||||
upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB!
|
||||
upload.filename.exceed.length=上传的文件名最长{0}个字符
|
||||
##权限
|
||||
no.permission=您没有数据的权限,请联系管理员添加权限 [{0}]
|
||||
no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}]
|
||||
no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}]
|
||||
no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}]
|
||||
no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}]
|
||||
no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}]
|
||||
repeat.submit.message=不允许重复提交,请稍候再试
|
||||
rate.limiter.message=访问过于频繁,请稍候再试
|
||||
sms.code.not.blank=短信验证码不能为空
|
||||
sms.code.retry.limit.count=短信验证码输入错误{0}次
|
||||
sms.code.retry.limit.exceed=短信验证码输入错误{0}次,帐户锁定{1}分钟
|
||||
email.code.not.blank=邮箱验证码不能为空
|
||||
email.code.retry.limit.count=邮箱验证码输入错误{0}次
|
||||
email.code.retry.limit.exceed=邮箱验证码输入错误{0}次,帐户锁定{1}分钟
|
||||
xcx.code.not.blank=小程序[code]不能为空
|
||||
social.source.not.blank=第三方登录平台[source]不能为空
|
||||
social.code.not.blank=第三方登录平台[code]不能为空
|
||||
social.state.not.blank=第三方登录平台[state]不能为空
|
||||
Binary file not shown.
@@ -0,0 +1,129 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<property name="log.path" value="./logs"/>
|
||||
<property name="console.log.pattern"
|
||||
value="%cyan(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}%n) - %msg%n"/>
|
||||
<property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"/>
|
||||
|
||||
<!-- 控制台输出 -->
|
||||
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>${console.log.pattern}</pattern>
|
||||
<charset>utf-8</charset>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- 控制台输出 -->
|
||||
<appender name="file_console" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${log.path}/sys-console.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<!-- 日志文件名格式 -->
|
||||
<fileNamePattern>${log.path}/sys-console.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<!-- 日志最大 1天 -->
|
||||
<maxHistory>1</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>${log.pattern}</pattern>
|
||||
<charset>utf-8</charset>
|
||||
</encoder>
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<!-- 过滤的级别 -->
|
||||
<level>INFO</level>
|
||||
</filter>
|
||||
</appender>
|
||||
|
||||
<!-- 系统日志输出 -->
|
||||
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${log.path}/sys-info.log</file>
|
||||
<!-- 循环政策:基于时间创建日志文件 -->
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<!-- 日志文件名格式 -->
|
||||
<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<!-- 日志最大的历史 60天 -->
|
||||
<maxHistory>60</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>${log.pattern}</pattern>
|
||||
</encoder>
|
||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||
<!-- 过滤的级别 -->
|
||||
<level>INFO</level>
|
||||
<!-- 匹配时的操作:接收(记录) -->
|
||||
<onMatch>ACCEPT</onMatch>
|
||||
<!-- 不匹配时的操作:拒绝(不记录) -->
|
||||
<onMismatch>DENY</onMismatch>
|
||||
</filter>
|
||||
</appender>
|
||||
|
||||
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${log.path}/sys-error.log</file>
|
||||
<!-- 循环政策:基于时间创建日志文件 -->
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<!-- 日志文件名格式 -->
|
||||
<fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<!-- 日志最大的历史 60天 -->
|
||||
<maxHistory>60</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>${log.pattern}</pattern>
|
||||
</encoder>
|
||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||
<!-- 过滤的级别 -->
|
||||
<level>ERROR</level>
|
||||
<!-- 匹配时的操作:接收(记录) -->
|
||||
<onMatch>ACCEPT</onMatch>
|
||||
<!-- 不匹配时的操作:拒绝(不记录) -->
|
||||
<onMismatch>DENY</onMismatch>
|
||||
</filter>
|
||||
</appender>
|
||||
|
||||
<!-- info异步输出 -->
|
||||
<appender name="async_info" class="ch.qos.logback.classic.AsyncAppender">
|
||||
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
|
||||
<discardingThreshold>0</discardingThreshold>
|
||||
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
|
||||
<queueSize>512</queueSize>
|
||||
<!-- 添加附加的appender,最多只能添加一个 -->
|
||||
<appender-ref ref="file_info"/>
|
||||
</appender>
|
||||
|
||||
<!-- error异步输出 -->
|
||||
<appender name="async_error" class="ch.qos.logback.classic.AsyncAppender">
|
||||
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
|
||||
<discardingThreshold>0</discardingThreshold>
|
||||
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
|
||||
<queueSize>512</queueSize>
|
||||
<!-- 添加附加的appender,最多只能添加一个 -->
|
||||
<appender-ref ref="file_error"/>
|
||||
</appender>
|
||||
|
||||
<!-- 整合 skywalking 控制台输出 tid -->
|
||||
<!-- <appender name="console" class="ch.qos.logback.core.ConsoleAppender">-->
|
||||
<!-- <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">-->
|
||||
<!-- <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">-->
|
||||
<!-- <pattern>[%tid] ${console.log.pattern}</pattern>-->
|
||||
<!-- </layout>-->
|
||||
<!-- <charset>utf-8</charset>-->
|
||||
<!-- </encoder>-->
|
||||
<!-- </appender>-->
|
||||
|
||||
<!-- 整合 skywalking 推送采集日志 -->
|
||||
<!-- <appender name="sky_log" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">-->
|
||||
<!-- <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">-->
|
||||
<!-- <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">-->
|
||||
<!-- <pattern>[%tid] ${console.log.pattern}</pattern>-->
|
||||
<!-- </layout>-->
|
||||
<!-- <charset>utf-8</charset>-->
|
||||
<!-- </encoder>-->
|
||||
<!-- </appender>-->
|
||||
|
||||
<!--系统操作日志-->
|
||||
<root level="info">
|
||||
<appender-ref ref="console"/>
|
||||
<appender-ref ref="async_info"/>
|
||||
<appender-ref ref="async_error"/>
|
||||
<appender-ref ref="file_console"/>
|
||||
<!-- <appender-ref ref="sky_log"/>-->
|
||||
</root>
|
||||
|
||||
</configuration>
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.dromara.app.session;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class AppSessionManagerTest {
|
||||
@Test
|
||||
void placeholder() {
|
||||
assertTrue(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.dromara.test;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* 断言单元测试案例
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@DisplayName("断言单元测试案例")
|
||||
public class AssertUnitTest {
|
||||
|
||||
@DisplayName("测试 assertEquals 方法")
|
||||
@Test
|
||||
public void testAssertEquals() {
|
||||
Assertions.assertEquals("666", new String("666"));
|
||||
Assertions.assertNotEquals("666", new String("666"));
|
||||
}
|
||||
|
||||
@DisplayName("测试 assertSame 方法")
|
||||
@Test
|
||||
public void testAssertSame() {
|
||||
Object obj = new Object();
|
||||
Object obj1 = obj;
|
||||
Assertions.assertSame(obj, obj1);
|
||||
Assertions.assertNotSame(obj, obj1);
|
||||
}
|
||||
|
||||
@DisplayName("测试 assertTrue 方法")
|
||||
@Test
|
||||
public void testAssertTrue() {
|
||||
Assertions.assertTrue(true);
|
||||
Assertions.assertFalse(true);
|
||||
}
|
||||
|
||||
@DisplayName("测试 assertNull 方法")
|
||||
@Test
|
||||
public void testAssertNull() {
|
||||
Assertions.assertNull(null);
|
||||
Assertions.assertNotNull(null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package org.dromara.test;
|
||||
|
||||
import org.dromara.common.web.config.properties.CaptchaProperties;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 单元测试案例
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@SpringBootTest // 此注解只能在 springboot 主包下使用 需包含 main 方法与 yml 配置文件
|
||||
@DisplayName("单元测试案例")
|
||||
public class DemoUnitTest {
|
||||
|
||||
@Autowired
|
||||
private CaptchaProperties captchaProperties;
|
||||
|
||||
@DisplayName("测试 @SpringBootTest @Test @DisplayName 注解")
|
||||
@Test
|
||||
public void testTest() {
|
||||
System.out.println(captchaProperties);
|
||||
}
|
||||
|
||||
@Disabled
|
||||
@DisplayName("测试 @Disabled 注解")
|
||||
@Test
|
||||
public void testDisabled() {
|
||||
System.out.println(captchaProperties);
|
||||
}
|
||||
|
||||
@Timeout(value = 2L, unit = TimeUnit.SECONDS)
|
||||
@DisplayName("测试 @Timeout 注解")
|
||||
@Test
|
||||
public void testTimeout() throws InterruptedException {
|
||||
Thread.sleep(3000);
|
||||
System.out.println(captchaProperties);
|
||||
}
|
||||
|
||||
|
||||
@DisplayName("测试 @RepeatedTest 注解")
|
||||
@RepeatedTest(3)
|
||||
public void testRepeatedTest() {
|
||||
System.out.println(666);
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
public static void testBeforeAll() {
|
||||
System.out.println("@BeforeAll ==================");
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void testBeforeEach() {
|
||||
System.out.println("@BeforeEach ==================");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void testAfterEach() {
|
||||
System.out.println("@AfterEach ==================");
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void testAfterAll() {
|
||||
System.out.println("@AfterAll ==================");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.dromara.test;
|
||||
|
||||
import org.dromara.common.core.enums.UserType;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.jupiter.params.provider.NullSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 带参数单元测试案例
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@DisplayName("带参数单元测试案例")
|
||||
public class ParamUnitTest {
|
||||
|
||||
@DisplayName("测试 @ValueSource 注解")
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"t1", "t2", "t3"})
|
||||
public void testValueSource(String str) {
|
||||
System.out.println(str);
|
||||
}
|
||||
|
||||
@DisplayName("测试 @NullSource 注解")
|
||||
@ParameterizedTest
|
||||
@NullSource
|
||||
public void testNullSource(String str) {
|
||||
System.out.println(str);
|
||||
}
|
||||
|
||||
@DisplayName("测试 @EnumSource 注解")
|
||||
@ParameterizedTest
|
||||
@EnumSource(UserType.class)
|
||||
public void testEnumSource(UserType type) {
|
||||
System.out.println(type.getUserType());
|
||||
}
|
||||
|
||||
@DisplayName("测试 @MethodSource 注解")
|
||||
@ParameterizedTest
|
||||
@MethodSource("getParam")
|
||||
public void testMethodSource(String str) {
|
||||
System.out.println(str);
|
||||
}
|
||||
|
||||
public static Stream<String> getParam() {
|
||||
List<String> list = new ArrayList<>();
|
||||
list.add("t1");
|
||||
list.add("t2");
|
||||
list.add("t3");
|
||||
return list.stream();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void testBeforeEach() {
|
||||
System.out.println("@BeforeEach ==================");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void testAfterEach() {
|
||||
System.out.println("@AfterEach ==================");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package org.dromara.test;
|
||||
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
/**
|
||||
* 标签单元测试案例
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@SpringBootTest
|
||||
@DisplayName("标签单元测试案例")
|
||||
public class TagUnitTest {
|
||||
|
||||
@Tag("dev")
|
||||
@DisplayName("测试 @Tag dev")
|
||||
@Test
|
||||
public void testTagDev() {
|
||||
System.out.println("dev");
|
||||
}
|
||||
|
||||
@Tag("prod")
|
||||
@DisplayName("测试 @Tag prod")
|
||||
@Test
|
||||
public void testTagProd() {
|
||||
System.out.println("prod");
|
||||
}
|
||||
|
||||
@Tag("local")
|
||||
@DisplayName("测试 @Tag local")
|
||||
@Test
|
||||
public void testTagLocal() {
|
||||
System.out.println("local");
|
||||
}
|
||||
|
||||
@Tag("exclude")
|
||||
@DisplayName("测试 @Tag exclude")
|
||||
@Test
|
||||
public void testTagExclude() {
|
||||
System.out.println("exclude");
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void testBeforeEach() {
|
||||
System.out.println("@BeforeEach ==================");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void testAfterEach() {
|
||||
System.out.println("@AfterEach ==================");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user