1、移除登录地址位置检查;2、登录登出增加地理位置入库;3、组织机构支持省市县持久化和查询;4、permission-info增加返回ip、地域、组织有效期;

This commit is contained in:
gaoqr
2024-11-05 14:38:35 +08:00
parent 90eb6b8d6d
commit 5ababb60c1
16 changed files with 156 additions and 67 deletions
@@ -22,6 +22,11 @@ public class IPQueryProperties {
@Value("${chenfeng.encrypt.publicKey:}")
private String publicKey;
/**
* 开关
*/
private boolean enable;
/**
* 接口地址
*/
@@ -32,6 +37,14 @@ public class IPQueryProperties {
*/
private String appCode;
public boolean isEnable() {
return enable;
}
public void setEnable(boolean enable) {
this.enable = enable;
}
public String getAppCode() {
return appCode;
}
@@ -9,6 +9,14 @@ import com.cf.imes.framework.ip.core.service.dto.IPQueryDataRespDTO;
* @since 2024/10/30 10:54
*/
public interface IPQueryService {
/**
* 服务是否开启
*
* @return
*/
boolean serviceEnable();
/**
* 查询ip归属地
*
@@ -15,6 +15,11 @@ public class IPQueryDataRespDTO implements Serializable {
private static final long serialVersionUID = 3403763102169461000L;
/**
* ip
*/
private String ip;
/**
* 国家
*/
@@ -27,6 +27,12 @@ public class IPQueryServiceImpl implements IPQueryService {
@Resource
private IPQueryProperties ipQueryProperties;
@Override
public boolean serviceEnable() {
return ipQueryProperties.isEnable();
}
@Override
public IPQueryDataRespDTO querySource(String ip) {
ErrorCode queryError = ErrorCodeConstants.IP_QUERY_ERROR;
@@ -48,17 +54,17 @@ public class IPQueryServiceImpl implements IPQueryService {
String respBody = execute.body();
IPQueryRespDTO ipQueryRespDTO = JSON.parseObject(execute.body(), IPQueryRespDTO.class);
if (HttpStatus.HTTP_OK == ipQueryRespDTO.getRet()) {
return ipQueryRespDTO.getData();
IPQueryDataRespDTO data = ipQueryRespDTO.getData();
data.setIp(ip);
return data;
} else {
ServiceException serviceException = ServiceExceptionUtil.exception(queryError);
log.error(serviceException.getMessage() + ",状态【{}】,异常:{}", ipQueryRespDTO.getRet(), respBody);
throw serviceException;
return null;
}
} catch (ServiceException se) {
throw se;
} catch (Exception e) {
log.error(queryError.getMsg(), e);
throw new ServiceException(queryError);
return null;
}
}
}
@@ -40,4 +40,7 @@ public class LoginLogCreateReqDTO {
@Schema(description = "浏览器 UserAgent", requiredMode = Schema.RequiredMode.REQUIRED, example = "Mozilla/5.0")
private String userAgent;
@Schema(description = "所属地区")
@Size(max = 200, message = "所属地区长度不能超过200个字符")
private String region;
}
@@ -8,6 +8,8 @@ import com.cf.imes.framework.common.exception.ServiceException;
import com.cf.imes.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.cf.imes.framework.common.pojo.CommonResult;
import com.cf.imes.framework.common.validation.Mobile;
import com.cf.imes.framework.ip.core.service.IPQueryService;
import com.cf.imes.framework.ip.core.service.dto.IPQueryDataRespDTO;
import com.cf.imes.framework.operatelog.core.annotations.OperateLog;
import com.cf.imes.framework.security.config.SecurityProperties;
import com.cf.imes.framework.security.core.LoginUser;
@@ -18,11 +20,13 @@ import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginSmsCheckReqVO
import com.cf.imes.module.system.controller.admin.auth.vo.AuthPermissionInfoRespVO;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthSocialLoginReqVO;
import com.cf.imes.module.system.convert.auth.AuthConvert;
import com.cf.imes.module.system.dal.dataobject.organ.OrganizationDO;
import com.cf.imes.module.system.dal.dataobject.permission.MenuDO;
import com.cf.imes.module.system.dal.dataobject.permission.RoleDO;
import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO;
import com.cf.imes.module.system.enums.logger.LoginLogTypeEnum;
import com.cf.imes.module.system.service.auth.AdminAuthService;
import com.cf.imes.module.system.service.organ.OrganService;
import com.cf.imes.module.system.service.permission.MenuService;
import com.cf.imes.module.system.service.permission.PermissionService;
import com.cf.imes.module.system.service.permission.RoleService;
@@ -39,6 +43,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.annotations.Param;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -81,12 +86,23 @@ public class AuthController {
@Resource
private SmsCodeService smsCodeService;
@Resource
private OrganService organService;
@Resource
private IPQueryService ipQueryService;
/**
* ip服务开关,默认为 false不开启
*/
@Value("${chenfeng.ipquery.enable:false}")
private boolean ipQueryEnable;
@PostMapping("/login")
@PermitAll
@Operation(summary = "使用账号密码登录")
@OperateLog(enable = false) // 避免 Post 请求被记录操作日志
public CommonResult<AuthLoginRespVO> login(@Valid @RequestBody AuthLoginReqVO reqVO, HttpServletRequest request) {
reqVO.setIp(getClientIP(request));
return success(authService.login(reqVO));
}
@@ -132,7 +148,7 @@ public class AuthController {
@GetMapping("/get-permission-info")
@Operation(summary = "获取登录用户的权限信息")
public CommonResult<AuthPermissionInfoRespVO> getPermissionInfo() {
public CommonResult<AuthPermissionInfoRespVO> getPermissionInfo(HttpServletRequest request) {
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
if (ObjectUtil.isNull(loginUser)) {
throw new ServiceException(GlobalErrorCodeConstants.UNAUTHORIZED);
@@ -143,11 +159,18 @@ public class AuthController {
if (user == null) {
return null;
}
// 获取机构的有效期
OrganizationDO organizationDO = organService.validOrgan(user.getOrganId());
IPQueryDataRespDTO ipQueryDataRespDTO = null;
if (ipQueryService.serviceEnable()) {
// 从ip获取地域信息
ipQueryDataRespDTO = ipQueryService.querySource(getClientIP(request));
}
// 1.2 获得角色列表
Set<Long> roleIds = permissionService.getUserRoleIdListByUserId(userId);
if (CollUtil.isEmpty(roleIds)) {
return success(AuthConvert.INSTANCE.convert(user, Collections.emptyList(), Collections.emptyList()));
return success(AuthConvert.INSTANCE.convert(user, organizationDO, ipQueryDataRespDTO, Collections.emptyList(), Collections.emptyList()));
}
List<RoleDO> roles = roleService.getRoleList1(roleIds);
roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())); // 移除禁用的角色
@@ -158,7 +181,7 @@ public class AuthController {
menuList.removeIf(menu -> !CommonStatusEnum.ENABLE.getStatus().equals(menu.getStatus())); // 移除禁用的菜单
// 2. 拼接结果返回
return success(AuthConvert.INSTANCE.convert(user, roles, menuList));
return success(AuthConvert.INSTANCE.convert(user, organizationDO, ipQueryDataRespDTO, roles, menuList));
}
// ========== 短信登录相关 ==========
@@ -51,9 +51,6 @@ public class AuthLoginReqVO {
@Schema(description = "state", requiredMode = Schema.RequiredMode.REQUIRED, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62")
private String socialState;
@Schema(hidden = true)
private String ip;
/**
* 开启验证码的 Group
*/
@@ -6,6 +6,7 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
@@ -49,6 +50,15 @@ public class AuthPermissionInfoRespVO {
@Schema(description = "是否需要设置密码")
private boolean needSetPwd;
@Schema(description = "ip地址")
private String ip;
@Schema(description = "所属区域")
private String region;
@Schema(description = "有效期限")
private LocalDateTime expireTime;
}
@Schema(description = "管理后台 - 登录用户的菜单信息 Response VO")
@@ -54,4 +54,8 @@ public class LoginLogRespVO {
@ExcelProperty("登录时间")
private LocalDateTime createTime;
@Schema(description = "所属地区")
@ExcelProperty("所属地区")
private String region;
}
@@ -40,10 +40,25 @@ public class OrganPageReqVO extends PageParam {
/**
* 拼音首字母
*/
@Schema(hidden = true)
private String pyFirstChar;
/**
* 全拼
*/
@Schema(hidden = true)
private String pyAll;
@Schema(description = "")
@Size(max = 64, message = "省名称长度不能超过64个字符")
private String province;
@Schema(description = "")
@Size(max = 64, message = "市名称长度不能超过64个字符")
private String city;
@Schema(description = "")
@Size(max = 64, message = "区名称长度不能超过64个字符")
private String county;
}
@@ -65,4 +65,28 @@ public class OrganRespVO {
@ExcelProperty("备注")
private String remark;
@Schema(description = "")
@ExcelProperty("")
private String province;
@Schema(description = "")
@ExcelProperty("")
private String city;
@Schema(description = "")
@ExcelProperty("")
private String county;
@Schema(description = "地区编码")
@ExcelProperty("地区编码")
private String areaCode;
@Schema(description = "经度")
@ExcelProperty("经度")
private String longitude;
@Schema(description = "纬度")
@ExcelProperty("纬度")
private String latitude;
}
@@ -1,6 +1,8 @@
package com.cf.imes.module.system.convert.auth;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.cf.imes.framework.ip.core.service.dto.IPQueryDataRespDTO;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import com.cf.imes.module.system.api.social.dto.SocialUserBindReqDTO;
@@ -10,6 +12,7 @@ import com.cf.imes.module.system.controller.admin.auth.vo.AuthSmsLoginReqVO;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthSmsSendReqVO;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthSocialLoginReqVO;
import com.cf.imes.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
import com.cf.imes.module.system.dal.dataobject.organ.OrganizationDO;
import com.cf.imes.module.system.dal.dataobject.permission.MenuDO;
import com.cf.imes.module.system.dal.dataobject.permission.RoleDO;
import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO;
@@ -31,10 +34,14 @@ public interface AuthConvert {
AuthLoginRespVO convert(OAuth2AccessTokenDO bean);
default AuthPermissionInfoRespVO convert(AdminUserDO user, List<RoleDO> roleList, List<MenuDO> menuList) {
default AuthPermissionInfoRespVO convert(AdminUserDO user, OrganizationDO organizationDO, IPQueryDataRespDTO ipQueryDataRespDTO, List<RoleDO> roleList, List<MenuDO> menuList) {
boolean ipQuerySuccess = ObjectUtil.isNotNull(ipQueryDataRespDTO);
return AuthPermissionInfoRespVO.builder()
.user(AuthPermissionInfoRespVO.UserVO.builder()
.id(user.getId()).nickname(user.getNickname()).avatar(user.getAvatar()).organId(user.getOrganId()).needSetPwd(StringUtils.isEmpty(user.getPassword())).build())
.id(user.getId()).nickname(user.getNickname()).avatar(user.getAvatar()).organId(user.getOrganId()).needSetPwd(StringUtils.isEmpty(user.getPassword()))
.expireTime(organizationDO.getExpireTime()).ip(ipQuerySuccess ? ipQueryDataRespDTO.getIp() : "")
.region(ipQuerySuccess ? ipQueryDataRespDTO.getProv() + "/" + ipQueryDataRespDTO.getCity() + "/" + ipQueryDataRespDTO.getArea() : "")
.build())
.roles(convertSet(roleList, RoleDO::getCode))
// 权限标识信息
.permissions(convertSet(menuList, MenuDO::getPermission))
@@ -69,4 +69,8 @@ public class LoginLogDO extends BaseDO {
*/
private String userAgent;
/**
* 所属地区
*/
private String region;
}
@@ -31,6 +31,9 @@ public interface OrganMapper extends BaseMapperX<OrganizationDO> {
.likeIfPresent(OrganizationDO::getContactMobile, reqVO.getContactMobile())
.eqIfPresent(OrganizationDO::getStatus, reqVO.getStatus())
.eqIfPresent(OrganizationDO::getName, reqVO.getExactName())
.eqIfPresent(OrganizationDO::getProvince, reqVO.getProvince())
.eqIfPresent(OrganizationDO::getCity, reqVO.getCity())
.eqIfPresent(OrganizationDO::getCounty, reqVO.getCounty())
.and(CharSequenceUtil.isNotBlank(reqVO.getName()), wrapper ->{
wrapper.or(Boolean.TRUE).like(OrganizationDO::getName, reqVO.getName());
wrapper.or(CharSequenceUtil.isNotBlank(reqVO.getPyAll())).like(OrganizationDO::getPinyinFull, reqVO.getPyAll());
@@ -6,13 +6,11 @@ import com.cf.imes.framework.common.enums.UserTypeEnum;
import com.cf.imes.framework.common.exception.util.ServiceExceptionUtil;
import com.cf.imes.framework.common.util.monitor.TracerUtils;
import com.cf.imes.framework.common.util.servlet.ServletUtils;
import com.cf.imes.framework.common.util.string.StrUtils;
import com.cf.imes.framework.common.util.validation.ValidationUtils;
import com.cf.imes.framework.ip.core.service.IPQueryService;
import com.cf.imes.framework.ip.core.service.dto.IPQueryDataRespDTO;
import com.cf.imes.module.system.api.logger.dto.LoginLogCreateReqDTO;
import com.cf.imes.module.system.api.sms.SmsCodeApi;
import com.cf.imes.module.system.api.social.dto.SocialUserBindReqDTO;
import com.cf.imes.module.system.api.social.dto.SocialUserRespDTO;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginReqVO;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginRespVO;
@@ -95,12 +93,6 @@ public class AdminAuthServiceImpl implements AdminAuthService {
@Value("${chenfeng.captcha.enable:true}")
private Boolean captchaEnable;
/**
* 登录ip地址位置校验,默认为 false不开启
*/
@Value("${chenfeng.ipquery.enable:false}")
private boolean ipQueryEnable;
@Override
public AdminUserDO authenticate(AuthLoginReqVO reqVO) {
String username = reqVO.getUsername();
@@ -114,11 +106,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.AUTH_LOGIN_BAD_CREDENTIALS);
}
// 校验机构有效性
OrganizationDO organizationDO = organService.validOrgan(user.getOrganId());
// 校验机构地理位置
if (ipQueryEnable && !checkGeo(organizationDO, ServletUtils.getClientIP())) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.AUTH_LOGIN_GEO_UNCORRECT);
}
organService.validOrgan(user.getOrganId());
// 校验密码
if (!userService.isPasswordMatch(password, user.getPassword())) {
createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
@@ -132,38 +120,6 @@ public class AdminAuthServiceImpl implements AdminAuthService {
return user;
}
/**
* 校验地理位置
* 省市正确或地区代码对上了就校验通过
*
* @return
*/
private boolean checkGeo(OrganizationDO organizationDO, String ip) {
// 检查组织的地理信息
if (ObjectUtil.isAllEmpty(organizationDO.getProvince(), organizationDO.getCity(), organizationDO.getAreaCode())) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.AUTH_LOGIN_ORG_GEO_EMPTY);
}
IPQueryDataRespDTO ipQueryDataRespDTO = ipQueryService.querySource(ip);
// 省名称和市名称包含了ip定位服务返回的省和市即位置正确
boolean provinceAndCityMatch = false;
if (StrUtils.contains(organizationDO.getProvince(), ipQueryDataRespDTO.getProv()) && StrUtils.contains(organizationDO.getCity(), ipQueryDataRespDTO.getCity())) {
provinceAndCityMatch = true;
}
// 行政区编码等于ip定位服务返回的编码即位置正确
boolean areaCodeMatch = false;
if (ObjectUtil.equal(organizationDO.getAreaCode(), ipQueryDataRespDTO.getPostCode())) {
areaCodeMatch = true;
}
if (provinceAndCityMatch || areaCodeMatch) {
return true;
}
return false;
}
@Override
public AuthLoginRespVO login(AuthLoginReqVO reqVO) {
@@ -173,11 +129,6 @@ public class AdminAuthServiceImpl implements AdminAuthService {
// 使用账号密码,进行登录
AdminUserDO user = authenticate(reqVO);
// 如果 socialType 非空,说明需要绑定社交用户
if (reqVO.getSocialType() != null) {
socialUserService.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState()));
}
Long organId = user.getOrganId();
OrganizationDO organ = organService.getOrgan(organId);
String dataSourceCode = organ.getDataSourceCode();
@@ -252,6 +203,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
private void createLoginLog(Long userId, String username,
LoginLogTypeEnum logTypeEnum, LoginResultEnum loginResult) {
String clientIP = getClientIP();
// 插入登录日志
LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO();
reqDTO.setLogType(logTypeEnum.getType());
@@ -260,12 +212,17 @@ public class AdminAuthServiceImpl implements AdminAuthService {
reqDTO.setUserType(getUserType().getValue());
reqDTO.setUsername(username);
reqDTO.setUserAgent(ServletUtils.getUserAgent());
reqDTO.setUserIp(ServletUtils.getClientIP());
reqDTO.setUserIp(clientIP);
reqDTO.setResult(loginResult.getResult());
IPQueryDataRespDTO ipQueryDataRespDTO = ipQueryService.querySource(clientIP);
// 设置登录时通过ip云服务查询到的地域信息
if (ipQueryService.serviceEnable() && ObjectUtil.isNotNull(ipQueryDataRespDTO)) {
reqDTO.setRegion(StringUtils.join(ipQueryDataRespDTO.getProv(), ipQueryDataRespDTO.getCity(), ipQueryDataRespDTO.getArea()));
}
loginLogService.createLoginLog(reqDTO);
// 更新最后登录时间
if (userId != null && Objects.equals(LoginResultEnum.SUCCESS.getResult(), loginResult.getResult())) {
userService.updateUserLogin(userId, ServletUtils.getClientIP());
userService.updateUserLogin(userId, clientIP);
}
}
@@ -360,6 +317,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
}
private void createLogoutLog(Long userId, Integer userType, Integer logType) {
String clientIP = getClientIP();
LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO();
reqDTO.setLogType(logType);
reqDTO.setTraceId(TracerUtils.getTraceId());
@@ -370,8 +328,13 @@ public class AdminAuthServiceImpl implements AdminAuthService {
} else {
reqDTO.setUsername(memberService.getMemberUserMobile(userId));
}
IPQueryDataRespDTO ipQueryDataRespDTO = ipQueryService.querySource(clientIP);
// 设置登录时通过ip云服务查询到的地域信息
if (ipQueryService.serviceEnable() && ObjectUtil.isNotNull(ipQueryDataRespDTO)) {
reqDTO.setRegion(StringUtils.join(ipQueryDataRespDTO.getProv(), ipQueryDataRespDTO.getCity(), ipQueryDataRespDTO.getArea()));
}
reqDTO.setUserAgent(ServletUtils.getUserAgent());
reqDTO.setUserIp(ServletUtils.getClientIP());
reqDTO.setUserIp(clientIP);
reqDTO.setResult(LoginResultEnum.SUCCESS.getResult());
loginLogService.createLoginLog(reqDTO);
}
@@ -213,4 +213,8 @@ chenfeng:
encrypt:
enable: false
publicKey: cfimes
ipquery:
enable: true
apiUrl: https://ipquery.market.alicloudapi.com/query
appCode: hBeRhmOnCR8f/XiD8zJ3lDaBSjbBA5ZZA2OEGswEOYQOK/hXVaT8E+AUOnEBcgFiw0R+39BvQ6TOVe2k
debug: false