This commit is contained in:
2026-02-24 21:01:54 +08:00
parent 4277ff1869
commit 94bee0be90
7 changed files with 492 additions and 8 deletions

View File

@@ -40,9 +40,9 @@ spring:
druid: druid:
# 主库数据源 # 主库数据源
master: master:
url: jdbc:mysql://13.211.168.80:3306/vetti_service?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 url: jdbc:mysql://ddns.hotake.cn:13306/vetti_service?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: vetti_service username: root
password: Hotake@2025 password: Hamkke@2021
# 从库数据源 # 从库数据源
slave: slave:
# 从数据源开关/默认关闭 # 从数据源开关/默认关闭
@@ -207,7 +207,7 @@ oauth2:
google: google:
client-id: 398978985110-ve0usu381mmdio12ff01iqvv1g087qvi.apps.googleusercontent.com # Google Cloud Console获取 client-id: 398978985110-ve0usu381mmdio12ff01iqvv1g087qvi.apps.googleusercontent.com # Google Cloud Console获取
client-secret: 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 # Google Cloud Console获取 client-secret: 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 # Google Cloud Console获取
redirect-uri: https://vetti.hotake.cn/oauth2/callback/google redirect-uri: https://vetti.com.au/oauth2/callback/google
scope: openid email profile # Google标准scope空格分隔 scope: openid email profile # Google标准scope空格分隔
auth-uri: https://accounts.google.com/o/oauth2/v2/auth # Google授权地址 auth-uri: https://accounts.google.com/o/oauth2/v2/auth # Google授权地址
token-uri: https://oauth2.googleapis.com/token # Google令牌地址 token-uri: https://oauth2.googleapis.com/token # Google令牌地址
@@ -220,7 +220,7 @@ oauth2:
microsoft: microsoft:
client-id: 608cbc4f-3ee2-4f51-a72c-b2e3133fc5b2 # Azure Portal获取 client-id: 608cbc4f-3ee2-4f51-a72c-b2e3133fc5b2 # Azure Portal获取
client-secret: gvw8Q~dwG8Sv7HN3R3W3R7TQtcZyvrh88ZiJubPa # Azure Portal获取 client-secret: gvw8Q~dwG8Sv7HN3R3W3R7TQtcZyvrh88ZiJubPa # Azure Portal获取
redirect-uri: https://vetti.hotake.cn/oauth2/callback/microsoft redirect-uri: https://vetti.com.au/oauth2/callback/microsoft
scope: openid,email,profile,User.Read # 需要额外的User.Read权限才能读取用户信息 scope: openid,email,profile,User.Read # 需要额外的User.Read权限才能读取用户信息
auth-uri: https://login.microsoftonline.com/common/oauth2/v2.0/authorize # 微软授权地址 auth-uri: https://login.microsoftonline.com/common/oauth2/v2.0/authorize # 微软授权地址
token-uri: https://login.microsoftonline.com/common/oauth2/v2.0/token # 微软令牌地址 token-uri: https://login.microsoftonline.com/common/oauth2/v2.0/token # 微软令牌地址
@@ -233,7 +233,7 @@ oauth2:
linkedin: linkedin:
client-id: 86uq3opzshd3bq # LinkedIn Developer Portal获取 client-id: 86uq3opzshd3bq # LinkedIn Developer Portal获取
client-secret: WPL_AP1.mipgyxfgfBoN12Th.1TXeFg== # LinkedIn Developer Portal获取 client-secret: WPL_AP1.mipgyxfgfBoN12Th.1TXeFg== # LinkedIn Developer Portal获取
redirect-uri: https://vetti.hotake.cn/oauth2/callback/linkedin redirect-uri: https://vetti.com.au/oauth2/callback/linkedin
scope: openid profile email # LinkedIn使用OpenID Connectscope顺序和命名略有不同 scope: openid profile email # LinkedIn使用OpenID Connectscope顺序和命名略有不同
auth-uri: https://www.linkedin.com/oauth/v2/authorization # LinkedIn授权地址 auth-uri: https://www.linkedin.com/oauth/v2/authorization # LinkedIn授权地址
token-uri: https://www.linkedin.com/oauth/v2/accessToken # LinkedIn令牌地址 token-uri: https://www.linkedin.com/oauth/v2/accessToken # LinkedIn令牌地址

View File

@@ -86,3 +86,10 @@ HotakeSecurityController10004 = All other sessions terminated
# Logout related # Logout related
HotakeSecurityServiceImpl10011 = User logged out HotakeSecurityServiceImpl10011 = User logged out
# Trusted device related messages
HotakeSecurityServiceImpl10012 = Updated trusted device
HotakeSecurityServiceImpl10013 = Added trusted device
HotakeSecurityServiceImpl10014 = Device does not exist or no permission to operate
HotakeSecurityServiceImpl10015 = Removed trusted device

View File

@@ -83,3 +83,10 @@ HotakeSecurityController10004 = 所有其他会话已终止
# 退出登录相关 # 退出登录相关
HotakeSecurityServiceImpl10011 = 用户退出登录 HotakeSecurityServiceImpl10011 = 用户退出登录
# 可信设备相关消息
HotakeSecurityServiceImpl10012 = 更新可信设备
HotakeSecurityServiceImpl10013 = 添加可信设备
HotakeSecurityServiceImpl10014 = 设备不存在或无权操作
HotakeSecurityServiceImpl10015 = 移除可信设备

View File

@@ -0,0 +1,96 @@
package com.vetti.hotake.mapper;
import com.vetti.hotake.domain.HotakeSecurityTrustedDevices;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 可信设备Mapper接口
*
* @author vetti
* @date 2026-02-02
*/
public interface HotakeSecurityTrustedDevicesMapper
{
/**
* 查询可信设备
*
* @param id 可信设备主键
* @return 可信设备
*/
public HotakeSecurityTrustedDevices selectHotakeSecurityTrustedDevicesById(Long id);
/**
* 根据用户ID和设备ID查询可信设备
*
* @param userId 用户ID
* @param deviceId 设备ID
* @return 可信设备
*/
public HotakeSecurityTrustedDevices selectByUserIdAndDeviceId(@Param("userId") Long userId, @Param("deviceId") String deviceId);
/**
* 查询可信设备列表
*
* @param hotakeSecurityTrustedDevices 可信设备
* @return 可信设备集合
*/
public List<HotakeSecurityTrustedDevices> selectHotakeSecurityTrustedDevicesList(HotakeSecurityTrustedDevices hotakeSecurityTrustedDevices);
/**
* 查询用户的可信设备列表
*
* @param userId 用户ID
* @return 可信设备集合
*/
public List<HotakeSecurityTrustedDevices> selectTrustedDevicesByUserId(Long userId);
/**
* 新增可信设备
*
* @param hotakeSecurityTrustedDevices 可信设备
* @return 结果
*/
public int insertHotakeSecurityTrustedDevices(HotakeSecurityTrustedDevices hotakeSecurityTrustedDevices);
/**
* 修改可信设备
*
* @param hotakeSecurityTrustedDevices 可信设备
* @return 结果
*/
public int updateHotakeSecurityTrustedDevices(HotakeSecurityTrustedDevices hotakeSecurityTrustedDevices);
/**
* 删除可信设备
*
* @param id 可信设备主键
* @return 结果
*/
public int deleteHotakeSecurityTrustedDevicesById(Long id);
/**
* 批量删除可信设备
*
* @param ids 需要删除的数据主键集合
* @return 结果
*/
public int deleteHotakeSecurityTrustedDevicesByIds(Long[] ids);
/**
* 移除用户的可信设备
*
* @param id 设备记录ID
* @param userId 用户ID
* @return 结果
*/
public int removeTrustedDevice(@Param("id") Long id, @Param("userId") Long userId);
/**
* 清理过期的可信设备
*
* @return 结果
*/
public int cleanExpiredTrustedDevices();
}

View File

@@ -3,6 +3,7 @@ package com.vetti.hotake.service;
import com.vetti.hotake.domain.HotakeSecuritySettings; import com.vetti.hotake.domain.HotakeSecuritySettings;
import com.vetti.hotake.domain.dto.SecurityChangePasswordDto; import com.vetti.hotake.domain.dto.SecurityChangePasswordDto;
import com.vetti.hotake.domain.vo.SecuritySessionVo; import com.vetti.hotake.domain.vo.SecuritySessionVo;
import com.vetti.hotake.domain.vo.SecurityTrustedDeviceVo;
import java.util.List; import java.util.List;
@@ -97,4 +98,37 @@ public interface IHotakeSecurityService
* @return 结果 * @return 结果
*/ */
int updateSessionLogout(String sessionToken); int updateSessionLogout(String sessionToken);
/**
* 获取当前用户的可信设备列表
*
* @return 可信设备列表
*/
List<SecurityTrustedDeviceVo> getTrustedDevices();
/**
* 添加可信设备
*
* @param deviceId 设备唯一标识
* @param deviceName 设备名称
* @return 结果
*/
int addTrustedDevice(String deviceId, String deviceName);
/**
* 移除可信设备
*
* @param id 设备记录ID
* @return 结果
*/
int removeTrustedDevice(Long id);
/**
* 检查设备是否为可信设备
*
* @param userId 用户ID
* @param deviceId 设备唯一标识
* @return 是否可信
*/
boolean isTrustedDevice(Long userId, String deviceId);
} }

View File

@@ -16,12 +16,15 @@ import com.vetti.hotake.domain.HotakeSecurityLoginSessions;
import com.vetti.hotake.domain.HotakeSecurityPasswordHistory; import com.vetti.hotake.domain.HotakeSecurityPasswordHistory;
import com.vetti.hotake.domain.HotakeSecuritySecurityLogs; import com.vetti.hotake.domain.HotakeSecuritySecurityLogs;
import com.vetti.hotake.domain.HotakeSecuritySettings; import com.vetti.hotake.domain.HotakeSecuritySettings;
import com.vetti.hotake.domain.HotakeSecurityTrustedDevices;
import com.vetti.hotake.domain.dto.SecurityChangePasswordDto; import com.vetti.hotake.domain.dto.SecurityChangePasswordDto;
import com.vetti.hotake.domain.vo.SecuritySessionVo; import com.vetti.hotake.domain.vo.SecuritySessionVo;
import com.vetti.hotake.domain.vo.SecurityTrustedDeviceVo;
import com.vetti.hotake.mapper.HotakeSecurityLoginSessionsMapper; import com.vetti.hotake.mapper.HotakeSecurityLoginSessionsMapper;
import com.vetti.hotake.mapper.HotakeSecurityPasswordHistoryMapper; import com.vetti.hotake.mapper.HotakeSecurityPasswordHistoryMapper;
import com.vetti.hotake.mapper.HotakeSecuritySecurityLogsMapper; import com.vetti.hotake.mapper.HotakeSecuritySecurityLogsMapper;
import com.vetti.hotake.mapper.HotakeSecuritySettingsMapper; import com.vetti.hotake.mapper.HotakeSecuritySettingsMapper;
import com.vetti.hotake.mapper.HotakeSecurityTrustedDevicesMapper;
import com.vetti.hotake.service.IHotakeSecurityService; import com.vetti.hotake.service.IHotakeSecurityService;
import com.vetti.system.service.ISysUserService; import com.vetti.system.service.ISysUserService;
import eu.bitwalker.useragentutils.Browser; import eu.bitwalker.useragentutils.Browser;
@@ -60,6 +63,9 @@ public class HotakeSecurityServiceImpl extends BaseServiceImpl implements IHotak
@Autowired @Autowired
private HotakeSecuritySecurityLogsMapper securityLogsMapper; private HotakeSecuritySecurityLogsMapper securityLogsMapper;
@Autowired
private HotakeSecurityTrustedDevicesMapper trustedDevicesMapper;
@Autowired @Autowired
private ISysUserService userService; private ISysUserService userService;
@@ -451,4 +457,202 @@ public class HotakeSecurityServiceImpl extends BaseServiceImpl implements IHotak
} }
return 0; return 0;
} }
/**
* 获取当前用户的可信设备列表
*
* @return 可信设备列表
*/
@Transactional(readOnly = true)
@Override
public List<SecurityTrustedDeviceVo> getTrustedDevices()
{
Long userId = SecurityUtils.getUserId();
// 查询用户的可信设备
List<HotakeSecurityTrustedDevices> devicesList = trustedDevicesMapper.selectTrustedDevicesByUserId(userId);
// 获取当前设备标识可以从请求头或Cookie中获取
String currentDeviceId = getCurrentDeviceId();
// 转换为VO
List<SecurityTrustedDeviceVo> voList = new ArrayList<>();
for (HotakeSecurityTrustedDevices device : devicesList)
{
SecurityTrustedDeviceVo vo = new SecurityTrustedDeviceVo();
vo.setId(device.getId());
vo.setDeviceId(device.getDeviceId());
vo.setDeviceName(device.getDeviceName());
vo.setDeviceType(device.getDeviceType());
vo.setBrowser(device.getBrowser());
vo.setOs(device.getOs());
vo.setIpAddress(device.getIpAddress());
vo.setLocation(device.getLocation());
vo.setTrustExpiresAt(device.getTrustExpiresAt());
vo.setCreateTime(device.getCreateTime());
vo.setIsCurrent(device.getDeviceId().equals(currentDeviceId));
voList.add(vo);
}
return voList;
}
/**
* 添加可信设备
*
* @param deviceId 设备唯一标识
* @param deviceName 设备名称
* @return 结果
*/
@Transactional(rollbackFor = Exception.class)
@Override
public int addTrustedDevice(String deviceId, String deviceName)
{
Long userId = SecurityUtils.getUserId();
// 检查是否已存在
HotakeSecurityTrustedDevices existDevice = trustedDevicesMapper.selectByUserIdAndDeviceId(userId, deviceId);
if (existDevice != null)
{
// 已存在,更新信任状态
existDevice.setIsTrusted(1);
existDevice.setDeviceName(deviceName);
// 设置信任过期时间30天
existDevice.setTrustExpiresAt(new Date(System.currentTimeMillis() + 30L * 24 * 60 * 60 * 1000));
fill(FillTypeEnum.UPDATE.getCode(), existDevice);
recordSecurityLog(userId, "TRUSTED_DEVICE_UPDATE",
MessageUtils.messageCustomize("HotakeSecurityServiceImpl10012") + "" + deviceName, "SUCCESS");
return trustedDevicesMapper.updateHotakeSecurityTrustedDevices(existDevice);
}
// 解析User-Agent
String userAgent = ServletUtils.getRequest().getHeader("User-Agent");
UserAgent ua = UserAgent.parseUserAgentString(userAgent);
Browser browser = ua.getBrowser();
OperatingSystem os = ua.getOperatingSystem();
// 获取IP和位置
String ipAddress = IpUtils.getIpAddr();
String location = AddressUtils.getRealAddressByIP(ipAddress);
// 判断设备类型
String deviceType = "Desktop";
if (os.getDeviceType() != null)
{
deviceType = os.getDeviceType().getName();
}
// 创建新的可信设备记录
HotakeSecurityTrustedDevices device = new HotakeSecurityTrustedDevices();
device.setUserId(userId);
device.setDeviceId(deviceId);
device.setDeviceName(StringUtils.isNotEmpty(deviceName) ? deviceName : browser.getName() + " / " + os.getName());
device.setDeviceType(deviceType);
device.setBrowser(browser.getName());
device.setOs(os.getName());
device.setIpAddress(ipAddress);
device.setLocation(location);
device.setIsTrusted(1);
// 设置信任过期时间30天
device.setTrustExpiresAt(new Date(System.currentTimeMillis() + 30L * 24 * 60 * 60 * 1000));
fill(FillTypeEnum.INSERT.getCode(), device);
// 记录安全日志
recordSecurityLog(userId, "TRUSTED_DEVICE_ADD",
MessageUtils.messageCustomize("HotakeSecurityServiceImpl10013") + "" + device.getDeviceName(), "SUCCESS");
return trustedDevicesMapper.insertHotakeSecurityTrustedDevices(device);
}
/**
* 移除可信设备
*
* @param id 设备记录ID
* @return 结果
*/
@Transactional(rollbackFor = Exception.class)
@Override
public int removeTrustedDevice(Long id)
{
Long userId = SecurityUtils.getUserId();
// 查询设备信息
HotakeSecurityTrustedDevices device = trustedDevicesMapper.selectHotakeSecurityTrustedDevicesById(id);
if (device == null || !device.getUserId().equals(userId))
{
throw new ServiceException(MessageUtils.messageCustomize("HotakeSecurityServiceImpl10014"));
}
// 删除设备
int result = trustedDevicesMapper.removeTrustedDevice(id, userId);
// 记录安全日志
recordSecurityLog(userId, "TRUSTED_DEVICE_REMOVE",
MessageUtils.messageCustomize("HotakeSecurityServiceImpl10015") + "" + device.getDeviceName(), "SUCCESS");
return result;
}
/**
* 检查设备是否为可信设备
*
* @param userId 用户ID
* @param deviceId 设备唯一标识
* @return 是否可信
*/
@Transactional(readOnly = true)
@Override
public boolean isTrustedDevice(Long userId, String deviceId)
{
if (StringUtils.isEmpty(deviceId))
{
return false;
}
HotakeSecurityTrustedDevices device = trustedDevicesMapper.selectByUserIdAndDeviceId(userId, deviceId);
if (device == null || device.getIsTrusted() != 1)
{
return false;
}
// 检查是否过期
if (device.getTrustExpiresAt() != null && device.getTrustExpiresAt().before(new Date()))
{
return false;
}
return true;
}
/**
* 获取当前设备标识
* 可以从请求头或Cookie中获取这里简单实现
*/
private String getCurrentDeviceId()
{
HttpServletRequest request = ServletUtils.getRequest();
// 优先从请求头获取
String deviceId = request.getHeader("X-Device-Id");
if (StringUtils.isEmpty(deviceId))
{
// 从Cookie获取
javax.servlet.http.Cookie[] cookies = request.getCookies();
if (cookies != null)
{
for (javax.servlet.http.Cookie cookie : cookies)
{
if ("device_id".equals(cookie.getName()))
{
deviceId = cookie.getValue();
break;
}
}
}
}
return deviceId;
}
} }

View File

@@ -0,0 +1,136 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.vetti.hotake.mapper.HotakeSecurityTrustedDevicesMapper">
<resultMap type="HotakeSecurityTrustedDevices" id="HotakeSecurityTrustedDevicesResult">
<result property="id" column="id" />
<result property="userId" column="user_id" />
<result property="deviceId" column="device_id" />
<result property="deviceName" column="device_name" />
<result property="deviceType" column="device_type" />
<result property="browser" column="browser" />
<result property="os" column="os" />
<result property="ipAddress" column="ip_address" />
<result property="location" column="location" />
<result property="isTrusted" column="is_trusted" />
<result property="trustExpiresAt" column="trust_expires_at"/>
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
</resultMap>
<sql id="selectHotakeSecurityTrustedDevicesVo">
select id, user_id, device_id, device_name, device_type, browser, os, ip_address, location, is_trusted, trust_expires_at, create_by, create_time, update_by, update_time, remark from hotake_security_trusted_devices
</sql>
<select id="selectHotakeSecurityTrustedDevicesList" parameterType="HotakeSecurityTrustedDevices" resultMap="HotakeSecurityTrustedDevicesResult">
<include refid="selectHotakeSecurityTrustedDevicesVo"/>
<where>
<if test="userId != null "> and user_id = #{userId}</if>
<if test="deviceId != null and deviceId != ''"> and device_id = #{deviceId}</if>
<if test="deviceName != null and deviceName != ''"> and device_name like concat('%', #{deviceName}, '%')</if>
<if test="deviceType != null and deviceType != ''"> and device_type = #{deviceType}</if>
<if test="browser != null and browser != ''"> and browser = #{browser}</if>
<if test="os != null and os != ''"> and os = #{os}</if>
<if test="isTrusted != null "> and is_trusted = #{isTrusted}</if>
</where>
order by create_time desc
</select>
<select id="selectHotakeSecurityTrustedDevicesById" parameterType="Long" resultMap="HotakeSecurityTrustedDevicesResult">
<include refid="selectHotakeSecurityTrustedDevicesVo"/>
where id = #{id}
</select>
<select id="selectByUserIdAndDeviceId" resultMap="HotakeSecurityTrustedDevicesResult">
<include refid="selectHotakeSecurityTrustedDevicesVo"/>
where user_id = #{userId} and device_id = #{deviceId}
</select>
<select id="selectTrustedDevicesByUserId" parameterType="Long" resultMap="HotakeSecurityTrustedDevicesResult">
<include refid="selectHotakeSecurityTrustedDevicesVo"/>
where user_id = #{userId} and is_trusted = 1 and (trust_expires_at is null or trust_expires_at > now())
order by create_time desc
</select>
<insert id="insertHotakeSecurityTrustedDevices" parameterType="HotakeSecurityTrustedDevices" useGeneratedKeys="true" keyProperty="id">
insert into hotake_security_trusted_devices
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="userId != null">user_id,</if>
<if test="deviceId != null and deviceId != ''">device_id,</if>
<if test="deviceName != null">device_name,</if>
<if test="deviceType != null">device_type,</if>
<if test="browser != null">browser,</if>
<if test="os != null">os,</if>
<if test="ipAddress != null">ip_address,</if>
<if test="location != null">location,</if>
<if test="isTrusted != null">is_trusted,</if>
<if test="trustExpiresAt != null">trust_expires_at,</if>
<if test="createBy != null">create_by,</if>
<if test="createTime != null">create_time,</if>
<if test="updateBy != null">update_by,</if>
<if test="updateTime != null">update_time,</if>
<if test="remark != null">remark,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="userId != null">#{userId},</if>
<if test="deviceId != null and deviceId != ''">#{deviceId},</if>
<if test="deviceName != null">#{deviceName},</if>
<if test="deviceType != null">#{deviceType},</if>
<if test="browser != null">#{browser},</if>
<if test="os != null">#{os},</if>
<if test="ipAddress != null">#{ipAddress},</if>
<if test="location != null">#{location},</if>
<if test="isTrusted != null">#{isTrusted},</if>
<if test="trustExpiresAt != null">#{trustExpiresAt},</if>
<if test="createBy != null">#{createBy},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateBy != null">#{updateBy},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="remark != null">#{remark},</if>
</trim>
</insert>
<update id="updateHotakeSecurityTrustedDevices" parameterType="HotakeSecurityTrustedDevices">
update hotake_security_trusted_devices
<trim prefix="SET" suffixOverrides=",">
<if test="userId != null">user_id = #{userId},</if>
<if test="deviceId != null and deviceId != ''">device_id = #{deviceId},</if>
<if test="deviceName != null">device_name = #{deviceName},</if>
<if test="deviceType != null">device_type = #{deviceType},</if>
<if test="browser != null">browser = #{browser},</if>
<if test="os != null">os = #{os},</if>
<if test="ipAddress != null">ip_address = #{ipAddress},</if>
<if test="location != null">location = #{location},</if>
<if test="isTrusted != null">is_trusted = #{isTrusted},</if>
<if test="trustExpiresAt != null">trust_expires_at = #{trustExpiresAt},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="remark != null">remark = #{remark},</if>
</trim>
where id = #{id}
</update>
<delete id="deleteHotakeSecurityTrustedDevicesById" parameterType="Long">
delete from hotake_security_trusted_devices where id = #{id}
</delete>
<delete id="deleteHotakeSecurityTrustedDevicesByIds" parameterType="String">
delete from hotake_security_trusted_devices where id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
<delete id="removeTrustedDevice">
delete from hotake_security_trusted_devices where id = #{id} and user_id = #{userId}
</delete>
<delete id="cleanExpiredTrustedDevices">
delete from hotake_security_trusted_devices where trust_expires_at is not null and trust_expires_at &lt; now()
</delete>
</mapper>