ldap登录
This commit is contained in:
白书科技
2023-09-01 03:48:39 +00:00
parent 069c3e4cc9
commit d5e410cb1f
30 changed files with 1292 additions and 89 deletions

View File

@@ -0,0 +1,143 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.api.bus;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import xyz.playedu.api.event.UserLoginEvent;
import xyz.playedu.common.domain.LdapUser;
import xyz.playedu.common.domain.User;
import xyz.playedu.common.exception.ServiceException;
import xyz.playedu.common.service.*;
import xyz.playedu.common.util.HelperUtil;
import xyz.playedu.common.util.IpUtil;
import xyz.playedu.common.util.RequestUtil;
import xyz.playedu.common.util.StringUtil;
import xyz.playedu.common.util.ldap.LdapTransformUser;
import java.util.HashMap;
@Component
@Slf4j
public class LoginBus {
@Autowired private FrontendAuthService authService;
@Autowired private DepartmentService departmentService;
@Autowired private LdapUserService ldapUserService;
@Autowired private UserService userService;
@Autowired private AppConfigService appConfigService;
@Autowired private ApplicationContext ctx;
public HashMap<String, Object> tokenByUser(User user) {
String token = authService.loginUsingId(user.getId(), RequestUtil.url());
HashMap<String, Object> data = new HashMap<>();
data.put("token", token);
ctx.publishEvent(
new UserLoginEvent(
this,
user.getId(),
user.getEmail(),
token,
IpUtil.getIpAddress(),
RequestUtil.ua()));
return data;
}
@Transactional
public HashMap<String, Object> tokenByLdapTransformUser(LdapTransformUser ldapTransformUser)
throws ServiceException {
// LDAP用户的名字
String ldapUserName = ldapTransformUser.getCn();
// 将LDAP用户所属的部门同步到本地
Integer depId = departmentService.createWithChainList(ldapTransformUser.getOu());
Integer[] depIds = depId == 0 ? null : new Integer[] {depId};
// LDAP用户在本地的缓存记录
LdapUser ldapUser = ldapUserService.findByUUID(ldapTransformUser.getId());
User user;
// 计算将LDAP用户关联到本地users表的email字段值
String localUserEmail = ldapTransformUser.getUid();
if (StringUtil.isNotEmpty(ldapTransformUser.getEmail())) {
localUserEmail = ldapTransformUser.getEmail();
}
if (ldapUser == null) {
// 检测localUserEmail是否存在
if (userService.find(localUserEmail) != null) {
throw new ServiceException(String.format("已有其它账号在使用:%s", localUserEmail));
}
// LDAP用户数据缓存到本地
ldapUser = ldapUserService.store(ldapTransformUser);
// 创建本地user
user =
userService.createWithDepIds(
localUserEmail,
ldapUserName,
appConfigService.defaultAvatar(),
HelperUtil.randomString(20),
"",
depIds);
// 将LDAP缓存数据与本地user关联
ldapUserService.updateUserId(ldapUser.getId(), user.getId());
} else {
user = userService.find(ldapUser.getUserId());
// 账号修改[账号有可能是email也有可能是uid]
if (!localUserEmail.equals(user.getEmail())) {
// 检测localUserEmail是否存在
if (userService.find(localUserEmail) != null) {
throw new ServiceException(String.format("已有其它账号在使用:%s", localUserEmail));
}
userService.updateEmail(user.getId(), localUserEmail);
}
// ldap-email的变化
if (!ldapUser.getEmail().equals(ldapTransformUser.getEmail())) {
ldapUserService.updateEmail(ldapUser.getId(), ldapTransformUser.getEmail());
}
// ldap-uid的变化
if (!ldapUser.getUid().equals(ldapTransformUser.getUid())) {
ldapUserService.updateUid(ldapUser.getId(), ldapTransformUser.getUid());
}
// 名字同步修改
if (!ldapUserName.equals(ldapUser.getCn())) {
userService.updateName(user.getId(), ldapUserName);
ldapUserService.updateCN(ldapUser.getId(), ldapUserName);
}
// 部门修改同步
String newOU = String.join(",", ldapTransformUser.getOu());
if (!newOU.equals(ldapUser.getOu())) {
userService.updateDepId(user.getId(), depIds);
ldapUserService.updateOU(ldapUser.getId(), newOU);
}
}
return tokenByUser(user);
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.api.cache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import xyz.playedu.common.config.PlayEduConfig;
import xyz.playedu.common.exception.ServiceException;
import xyz.playedu.common.service.RateLimiterService;
import xyz.playedu.common.util.RedisUtil;
@Component
public class LoginLimitCache {
@Autowired private RateLimiterService rateLimiterService;
@Autowired private PlayEduConfig playEduConfig;
public void check(String email) throws ServiceException {
String limitKey = cacheKey(email);
Long reqCount = rateLimiterService.current(limitKey, 600L);
if (reqCount >= 10 && !playEduConfig.getTesting()) {
Long exp = RedisUtil.ttlWithoutPrefix(limitKey);
String msg = String.format("您的账号已被锁定,请%s后重试", exp > 60 ? exp / 60 + "分钟" : exp + "");
throw new ServiceException(msg);
}
}
public void destroy(String email) {
RedisUtil.del(cacheKey(email));
}
private String cacheKey(String email) {
return "login-limit:" + email;
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.api.cache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import xyz.playedu.common.util.RedisDistributedLock;
import java.util.concurrent.TimeUnit;
@Component
public class LoginLockCache {
@Autowired private RedisDistributedLock redisDistributedLock;
public boolean apply(String username) {
String key = cacheKey(username);
return redisDistributedLock.tryLock(key, 10L, TimeUnit.SECONDS);
}
public void release(String username) {
redisDistributedLock.releaseLock(cacheKey(username));
}
private String cacheKey(String username) {
return "login-lock:" + username;
}
}

View File

@@ -35,6 +35,7 @@ import xyz.playedu.common.context.BCtx;
import xyz.playedu.common.domain.Department;
import xyz.playedu.common.domain.User;
import xyz.playedu.common.exception.NotFoundException;
import xyz.playedu.common.service.AppConfigService;
import xyz.playedu.common.service.DepartmentService;
import xyz.playedu.common.service.UserService;
import xyz.playedu.common.types.JsonResponse;
@@ -66,6 +67,8 @@ public class DepartmentController {
@Autowired private ApplicationContext ctx;
@Autowired private AppConfigService appConfigService;
@GetMapping("/index")
@Log(title = "部门-列表", businessType = BusinessTypeConstant.GET)
public JsonResponse index() {
@@ -98,6 +101,9 @@ public class DepartmentController {
@Log(title = "部门-新建", businessType = BusinessTypeConstant.INSERT)
public JsonResponse store(@RequestBody @Validated DepartmentRequest req)
throws NotFoundException {
if (appConfigService.enabledLdapLogin()) {
return JsonResponse.error("已启用LDAP服务禁止添加部门");
}
departmentService.create(req.getName(), req.getParentId(), req.getSort());
return JsonResponse.success();
}
@@ -115,6 +121,9 @@ public class DepartmentController {
@Log(title = "部门-编辑", businessType = BusinessTypeConstant.UPDATE)
public JsonResponse update(@PathVariable Integer id, @RequestBody DepartmentRequest req)
throws NotFoundException {
if (appConfigService.enabledLdapLogin()) {
return JsonResponse.error("已启用LDAP服务禁止添加部门");
}
Department department = departmentService.findOrFail(id);
departmentService.update(department, req.getName(), req.getParentId(), req.getSort());
return JsonResponse.success();
@@ -124,6 +133,9 @@ public class DepartmentController {
@GetMapping("/{id}/destroy")
@Log(title = "部门-批量删除", businessType = BusinessTypeConstant.DELETE)
public JsonResponse preDestroy(@PathVariable Integer id) {
if (appConfigService.enabledLdapLogin()) {
return JsonResponse.error("已启用LDAP服务禁止添加部门");
}
List<Integer> courseIds = courseDepartmentService.getCourseIdsByDepId(id);
List<Integer> userIds = departmentService.getUserIdsByDepId(id);
@@ -165,6 +177,9 @@ public class DepartmentController {
@DeleteMapping("/{id}")
@Log(title = "部门-删除", businessType = BusinessTypeConstant.DELETE)
public JsonResponse destroy(@PathVariable Integer id) throws NotFoundException {
if (appConfigService.enabledLdapLogin()) {
return JsonResponse.error("已启用LDAP服务禁止添加部门");
}
Department department = departmentService.findOrFail(id);
departmentService.destroy(department.getId());
ctx.publishEvent(new DepartmentDestroyEvent(this, BCtx.getId(), department.getId()));
@@ -184,6 +199,9 @@ public class DepartmentController {
@Log(title = "部门-更新父级", businessType = BusinessTypeConstant.UPDATE)
public JsonResponse updateParent(@RequestBody @Validated DepartmentParentRequest req)
throws NotFoundException {
if (appConfigService.enabledLdapLogin()) {
return JsonResponse.error("已启用LDAP服务禁止添加部门");
}
departmentService.changeParent(req.getId(), req.getParentId(), req.getIds());
return JsonResponse.success();
}

View File

@@ -28,7 +28,6 @@ import xyz.playedu.api.request.frontend.CourseHourRecordRequest;
import xyz.playedu.common.context.FCtx;
import xyz.playedu.common.types.JsonResponse;
import xyz.playedu.common.util.RedisDistributedLock;
import xyz.playedu.course.bus.UserBus;
import xyz.playedu.course.caches.CourseCache;
import xyz.playedu.course.caches.UserCanSeeCourseCache;
import xyz.playedu.course.caches.UserLastLearnTimeCache;
@@ -61,8 +60,6 @@ public class HourController {
@Autowired private UserCourseHourRecordService userCourseHourRecordService;
@Autowired private UserBus userBus;
// ------- CACHE ----------
@Autowired private UserCanSeeCourseCache userCanSeeCourseCache;
@Autowired private CourseCache courseCache;

View File

@@ -15,6 +15,9 @@
*/
package xyz.playedu.api.controller.frontend;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.validation.annotation.Validated;
@@ -23,26 +26,29 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.playedu.api.event.UserLoginEvent;
import xyz.playedu.api.bus.LoginBus;
import xyz.playedu.api.cache.LoginLimitCache;
import xyz.playedu.api.cache.LoginLockCache;
import xyz.playedu.api.event.UserLogoutEvent;
import xyz.playedu.api.request.frontend.LoginLdapRequest;
import xyz.playedu.api.request.frontend.LoginPasswordRequest;
import xyz.playedu.common.config.PlayEduConfig;
import xyz.playedu.common.constant.ConfigConstant;
import xyz.playedu.common.context.FCtx;
import xyz.playedu.common.domain.User;
import xyz.playedu.common.exception.LimitException;
import xyz.playedu.common.service.FrontendAuthService;
import xyz.playedu.common.service.RateLimiterService;
import xyz.playedu.common.service.UserService;
import xyz.playedu.common.exception.ServiceException;
import xyz.playedu.common.service.*;
import xyz.playedu.common.types.JsonResponse;
import xyz.playedu.common.util.HelperUtil;
import xyz.playedu.common.util.IpUtil;
import xyz.playedu.common.util.RedisUtil;
import xyz.playedu.common.util.RequestUtil;
import xyz.playedu.common.util.*;
import xyz.playedu.common.util.ldap.LdapTransformUser;
import xyz.playedu.common.util.ldap.LdapUtil;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/auth/login")
@Slf4j
public class LoginController {
@Autowired private UserService userService;
@@ -51,13 +57,27 @@ public class LoginController {
@Autowired private ApplicationContext ctx;
@Autowired private RateLimiterService rateLimiterService;
@Autowired private AppConfigService appConfigService;
@Autowired private PlayEduConfig playEduConfig;
@Autowired private LdapUserService ldapUserService;
@Autowired private DepartmentService departmentService;
@Autowired private LoginBus loginBus;
@Autowired private LoginLimitCache loginLimitCache;
@Autowired private LoginLockCache loginLockCache;
@PostMapping("/password")
public JsonResponse password(@RequestBody @Validated LoginPasswordRequest req)
@SneakyThrows
public JsonResponse password(
@RequestBody @Validated LoginPasswordRequest req, LoginBus loginBus)
throws LimitException {
if (appConfigService.enabledLdapLogin()) {
return JsonResponse.error("请使用LDAP登录");
}
String email = req.getEmail();
User user = userService.find(email);
@@ -65,39 +85,74 @@ public class LoginController {
return JsonResponse.error("邮箱或密码错误");
}
String limitKey = "login-limit:" + req.getEmail();
Long reqCount = rateLimiterService.current(limitKey, 600L);
if (reqCount >= 10 && !playEduConfig.getTesting()) {
Long exp = RedisUtil.ttlWithoutPrefix(limitKey);
return JsonResponse.error(
String.format("您的账号已被锁定,请%s后重试", exp > 60 ? exp / 60 + "分钟" : exp + ""));
}
loginLimitCache.check(email);
if (!HelperUtil.MD5(req.getPassword() + user.getSalt()).equals(user.getPassword())) {
return JsonResponse.error("邮箱或密码错误");
}
RedisUtil.del(limitKey);
if (user.getIsLock() == 1) {
return JsonResponse.error("当前学员已锁定无法登录");
}
String token = authService.loginUsingId(user.getId(), RequestUtil.url());
loginLimitCache.destroy(email);
HashMap<String, Object> data = new HashMap<>();
data.put("token", token);
return JsonResponse.data(loginBus.tokenByUser(user));
}
ctx.publishEvent(
new UserLoginEvent(
this,
user.getId(),
user.getEmail(),
token,
IpUtil.getIpAddress(),
RequestUtil.ua()));
@PostMapping("/ldap")
@SneakyThrows
public JsonResponse ldap(@RequestBody @Validated LoginLdapRequest req) {
String username = req.getUsername();
return JsonResponse.data(data);
// 系统配置
Map<String, String> config = appConfigService.keyValues();
String url = config.get(ConfigConstant.LDAP_URL);
String adminUser = config.get(ConfigConstant.LDAP_ADMIN_USER);
String adminPass = config.get(ConfigConstant.LDAP_ADMIN_PASS);
String baseDN = config.get(ConfigConstant.LDAP_BASE_DN);
if (url.isEmpty() || adminUser.isEmpty() || adminPass.isEmpty() || baseDN.isEmpty()) {
return JsonResponse.error("LDAP服务未配置");
}
String mail = null;
String uid = null;
if (StringUtil.contains(username, "@")) {
mail = username;
} else {
uid = username;
}
// 限流控制
loginLimitCache.check(username);
// 锁控制-防止并发登录重复写入数据
if (!loginLockCache.apply(username)) {
return JsonResponse.error("请稍候再试");
}
try {
LdapTransformUser ldapTransformUser =
LdapUtil.loginByMailOrUid(
url, adminUser, adminPass, baseDN, mail, uid, req.getPassword());
if (ldapTransformUser == null) {
return JsonResponse.error("登录失败.请检查账号和密码");
}
HashMap<String, Object> data = loginBus.tokenByLdapTransformUser(ldapTransformUser);
// 删除限流控制
loginLimitCache.destroy(username);
return JsonResponse.data(data);
} catch (ServiceException e) {
return JsonResponse.error(e.getMessage());
} catch (Exception e) {
log.error("LDAP登录失败", e);
return JsonResponse.error("系统错误");
} finally {
loginLockCache.release(username);
}
}
@PostMapping("/logout")

View File

@@ -53,6 +53,8 @@ public class SystemController {
data.put("player-bullet-secret-opacity", configs.get("player.bullet_secret_opacity"));
data.put("player-disabled-drag", configs.get("player.disabled_drag"));
data.put("ldap-enabled", configs.get(ConfigConstant.LDAP_ENABLED));
return JsonResponse.data(data);
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.api.request.frontend;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class LoginLdapRequest {
@NotBlank(message = "请输入账户名")
private String username;
@NotBlank(message = "请输入密码")
private String password;
}