From 3ecff3cec351c98cea7c4808c42b1f74a3707126 Mon Sep 17 00:00:00 2001 From: Grand-cocoa <1075576561@qq.com49111108+grand-cocoa@users.noreply.github.com> Date: Tue, 11 Nov 2025 18:32:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E6=94=AF=E6=8C=81=E9=80=9A?= =?UTF-8?q?=E8=A1=8C=E5=AF=86=E9=92=A5=E5=87=AD=E8=AF=81=E5=90=8D=E7=A7=B0?= =?UTF-8?q?=E5=92=8C=E6=9F=A5=E7=9C=8B=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增凭证名称字段用于注册时标识不同通行密钥 - 添加获取所有用户凭证接口,返回凭证列表及使用信息 - 更新用户凭证实体类增加名称、最后使用时间和创建时间字段 - 实现凭证信息的 VO 转换与接口暴露 - 增加 ByteArray 的序列化与反序列化支持以适配 WebAuthn 数据结构 - 配置通行密钥依赖方允许来源域名白名单 - 修改异常过滤器中未登录响应状态码为 401 --- .../PasskeyAuthorizationController.java | 16 ++++++++-- .../animo/auth/domain/UserCredential.java | 14 ++++++++ .../auth/domain/vo/UserCredentialVO.java | 32 +++++++++++++++++++ .../service/PasskeyAuthorizationService.java | 11 ++++++- .../impl/PasskeyAuthorizationServiceImpl.java | 27 ++++++++++++++-- .../com/kane/animo/config/JsonConfigurer.java | 22 +++++++++++++ .../kane/animo/config/PasskeyConfigurer.java | 6 ++++ .../serializer/ByteArrayDeserializer.java | 27 ++++++++++++++++ .../serializer/ByteArraySerializer.java | 25 +++++++++++++++ .../kane/animo/filter/ExceptionFilter.java | 2 +- 10 files changed, 176 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/kane/animo/auth/domain/vo/UserCredentialVO.java create mode 100644 src/main/java/com/kane/animo/config/JsonConfigurer.java create mode 100644 src/main/java/com/kane/animo/config/serializer/ByteArrayDeserializer.java create mode 100644 src/main/java/com/kane/animo/config/serializer/ByteArraySerializer.java diff --git a/src/main/java/com/kane/animo/auth/controller/PasskeyAuthorizationController.java b/src/main/java/com/kane/animo/auth/controller/PasskeyAuthorizationController.java index c30a93e..c8d7483 100644 --- a/src/main/java/com/kane/animo/auth/controller/PasskeyAuthorizationController.java +++ b/src/main/java/com/kane/animo/auth/controller/PasskeyAuthorizationController.java @@ -1,6 +1,7 @@ package com.kane.animo.auth.controller; import com.fasterxml.jackson.core.JsonProcessingException; +import com.kane.animo.auth.domain.vo.UserCredentialVO; import com.kane.animo.auth.service.PasskeyAuthorizationService; import com.kane.animo.model.R; import com.yubico.webauthn.exception.AssertionFailedException; @@ -10,6 +11,7 @@ import jakarta.servlet.http.HttpServletRequest; import org.springframework.web.bind.annotation.*; import java.io.IOException; +import java.util.List; /** * 通行密钥 @@ -35,10 +37,11 @@ public class PasskeyAuthorizationController { /** * 验证通行密钥 * @param credential 凭证 + * @param name 凭证名称 */ @PostMapping("/registration") - public R verifyPasskeyRegistration(@RequestBody String credential) throws RegistrationFailedException, IOException { - service.finishPasskeyRegistration(credential); + public R verifyPasskeyRegistration(@RequestBody String credential, String name) throws RegistrationFailedException, IOException { + service.finishPasskeyRegistration(credential, name); return R.success(); } @@ -63,4 +66,13 @@ public class PasskeyAuthorizationController { return R.success(s); } + /** + * 获取所有凭证 + * @return 凭证 + */ + @GetMapping("/credentials") + public R> getCredentials() { + return R.success(service.getCredentials()); + } + } diff --git a/src/main/java/com/kane/animo/auth/domain/UserCredential.java b/src/main/java/com/kane/animo/auth/domain/UserCredential.java index 2debb11..bad63a8 100644 --- a/src/main/java/com/kane/animo/auth/domain/UserCredential.java +++ b/src/main/java/com/kane/animo/auth/domain/UserCredential.java @@ -6,6 +6,8 @@ import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.experimental.Accessors; +import java.time.LocalDateTime; + /** * 用户凭证 * @author Kane @@ -24,6 +26,10 @@ public class UserCredential { * 用户ID */ private Long userId; + /** + * 用户凭证名称 + */ + private String name; /** * 用户凭证 */ @@ -36,6 +42,14 @@ public class UserCredential { * 传输方式 */ private String transports; + /** + * 最后使用时间 + */ + private LocalDateTime lastUsed; + /** + * 创建时间 + */ + private LocalDateTime createTime; /** * 删除标识 */ diff --git a/src/main/java/com/kane/animo/auth/domain/vo/UserCredentialVO.java b/src/main/java/com/kane/animo/auth/domain/vo/UserCredentialVO.java new file mode 100644 index 0000000..337bd56 --- /dev/null +++ b/src/main/java/com/kane/animo/auth/domain/vo/UserCredentialVO.java @@ -0,0 +1,32 @@ +package com.kane.animo.auth.domain.vo; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; + +/** + * 用户凭证 + * @author Kane + * @since 2025/11/11 16:33 + */ +@Data +@Accessors(chain = true) +public class UserCredentialVO { + /** + * 凭证ID + */ + private Long id; + /** + * 凭证名称 + */ + private String name; + /** + * 最后使用时间 + */ + private LocalDateTime lastUsed; + /** + * 创建时间 + */ + private LocalDateTime created; +} diff --git a/src/main/java/com/kane/animo/auth/service/PasskeyAuthorizationService.java b/src/main/java/com/kane/animo/auth/service/PasskeyAuthorizationService.java index 26a419a..9118142 100644 --- a/src/main/java/com/kane/animo/auth/service/PasskeyAuthorizationService.java +++ b/src/main/java/com/kane/animo/auth/service/PasskeyAuthorizationService.java @@ -1,10 +1,12 @@ package com.kane.animo.auth.service; import com.fasterxml.jackson.core.JsonProcessingException; +import com.kane.animo.auth.domain.vo.UserCredentialVO; import com.yubico.webauthn.exception.AssertionFailedException; import com.yubico.webauthn.exception.RegistrationFailedException; import java.io.IOException; +import java.util.List; /** * 通行密钥授权服务 @@ -24,8 +26,9 @@ public interface PasskeyAuthorizationService { * 验证通行密钥 * * @param credential 凭证 + * @param name 凭证名称 */ - void finishPasskeyRegistration(String credential) throws IOException, RegistrationFailedException; + void finishPasskeyRegistration(String credential, String name) throws IOException, RegistrationFailedException; /** * 获取通行密钥验证参数 @@ -42,4 +45,10 @@ public interface PasskeyAuthorizationService { * @param credential 凭证 */ String finishPasskeyAssertion(String id, String credential) throws IOException, AssertionFailedException; + + /** + * 获取所有凭证 + * @return 凭证 + */ + List getCredentials(); } diff --git a/src/main/java/com/kane/animo/auth/service/impl/PasskeyAuthorizationServiceImpl.java b/src/main/java/com/kane/animo/auth/service/impl/PasskeyAuthorizationServiceImpl.java index fbd48cb..6b68a18 100644 --- a/src/main/java/com/kane/animo/auth/service/impl/PasskeyAuthorizationServiceImpl.java +++ b/src/main/java/com/kane/animo/auth/service/impl/PasskeyAuthorizationServiceImpl.java @@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWra import com.fasterxml.jackson.core.JsonProcessingException; import com.kane.animo.auth.domain.User; import com.kane.animo.auth.domain.UserCredential; +import com.kane.animo.auth.domain.vo.UserCredentialVO; import com.kane.animo.auth.mapper.UserCredentialMapper; import com.kane.animo.auth.mapper.UserMapper; import com.kane.animo.auth.service.PasskeyAuthorizationService; @@ -20,7 +21,7 @@ import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import java.io.IOException; -import java.util.HashSet; +import java.time.LocalDateTime; import java.util.List; import java.util.TreeSet; @@ -73,9 +74,10 @@ public class PasskeyAuthorizationServiceImpl implements PasskeyAuthorizationServ * 验证通行密钥 * * @param credential 凭证 + * @param name 凭证名称 */ @Override - public void finishPasskeyRegistration(String credential) throws IOException, RegistrationFailedException { + public void finishPasskeyRegistration(String credential, String name) throws IOException, RegistrationFailedException { User user = userMapper.selectById(StpUtil.getLoginIdAsLong()); PublicKeyCredential pkc = PublicKeyCredential.parseRegistrationResponseJson(credential); @@ -89,6 +91,7 @@ public class PasskeyAuthorizationServiceImpl implements PasskeyAuthorizationServ .build()); UserCredential userCredential = new UserCredential() .setUserId(user.getId()) + .setName(name) .setIdentity(JSON.toJSONString(request.getUser())) .setTransports(JSON.toJSONString(result.getKeyId().getTransports().orElse(new TreeSet<>()))) .setCredential(JSON.toJSONString(RegisteredCredential.builder() @@ -145,6 +148,7 @@ public class PasskeyAuthorizationServiceImpl implements PasskeyAuthorizationServ RegisteredCredential build = parsed.toBuilder().signatureCount(result.getSignatureCount()).build(); new LambdaUpdateChainWrapper<>(credentialMapper) .set(UserCredential::getCredential, JSON.toJSONString(build)) + .set(UserCredential::getLastUsed, LocalDateTime.now()) .eq(UserCredential::getId, userCredential.getId()) .update(); } @@ -152,4 +156,23 @@ public class PasskeyAuthorizationServiceImpl implements PasskeyAuthorizationServ StpUtil.login(one.getId()); return result.getUsername(); } + + /** + * 获取所有凭证 + * + * @return 凭证 + */ + @Override + public List getCredentials() { + List list = new LambdaQueryChainWrapper<>(credentialMapper) + .eq(UserCredential::getUserId, StpUtil.getLoginIdAsLong()) + .list(); + return list.stream() + .map(x -> new UserCredentialVO() + .setId(x.getId()) + .setName(x.getName()) + .setCreated(x.getCreateTime()) + .setLastUsed(x.getLastUsed())) + .toList(); + } } diff --git a/src/main/java/com/kane/animo/config/JsonConfigurer.java b/src/main/java/com/kane/animo/config/JsonConfigurer.java new file mode 100644 index 0000000..dc577ec --- /dev/null +++ b/src/main/java/com/kane/animo/config/JsonConfigurer.java @@ -0,0 +1,22 @@ +package com.kane.animo.config; + +import com.alibaba.fastjson2.JSON; +import com.kane.animo.config.serializer.ByteArrayDeserializer; +import com.kane.animo.config.serializer.ByteArraySerializer; +import com.yubico.webauthn.RegisteredCredential; +import com.yubico.webauthn.data.ByteArray; +import jakarta.annotation.PostConstruct; +import org.springframework.context.annotation.Configuration; + +/** + * @author Kane + * @since 2025/11/11 18:21 + */ +@Configuration +public class JsonConfigurer { + @PostConstruct + public void configure() { + JSON.register(ByteArray.class, ByteArraySerializer.INSTANCE); + JSON.register(ByteArray.class, ByteArrayDeserializer.INSTANCE); + } +} diff --git a/src/main/java/com/kane/animo/config/PasskeyConfigurer.java b/src/main/java/com/kane/animo/config/PasskeyConfigurer.java index 0409be5..b2f380f 100644 --- a/src/main/java/com/kane/animo/config/PasskeyConfigurer.java +++ b/src/main/java/com/kane/animo/config/PasskeyConfigurer.java @@ -9,6 +9,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.util.Set; + /** * 通行密钥配置 * @author Kane @@ -31,6 +33,10 @@ public class PasskeyConfigurer { return RelyingParty.builder() .identity(rpIdentity) .credentialRepository(credentialRepository) + .origins(Set.of( + "http://localhost:5173", + "https://animo.alina-dace.info" + )) .build(); } } diff --git a/src/main/java/com/kane/animo/config/serializer/ByteArrayDeserializer.java b/src/main/java/com/kane/animo/config/serializer/ByteArrayDeserializer.java new file mode 100644 index 0000000..a8150a1 --- /dev/null +++ b/src/main/java/com/kane/animo/config/serializer/ByteArrayDeserializer.java @@ -0,0 +1,27 @@ +package com.kane.animo.config.serializer; + +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.reader.ObjectReader; +import com.yubico.webauthn.data.ByteArray; +import com.yubico.webauthn.data.exception.Base64UrlException; + +import java.lang.reflect.Type; + +/** + * @author Kane + * @since 2025/11/11 18:11 + */ +public class ByteArrayDeserializer implements ObjectReader { + + public static final ByteArrayDeserializer INSTANCE = new ByteArrayDeserializer(); + + @Override + public ByteArray readObject(JSONReader jsonReader, Type fieldType, Object fieldName, long features) { + String s = jsonReader.readString(); + try { + return ByteArray.fromBase64Url(s); + } catch (Base64UrlException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/kane/animo/config/serializer/ByteArraySerializer.java b/src/main/java/com/kane/animo/config/serializer/ByteArraySerializer.java new file mode 100644 index 0000000..edf872a --- /dev/null +++ b/src/main/java/com/kane/animo/config/serializer/ByteArraySerializer.java @@ -0,0 +1,25 @@ +package com.kane.animo.config.serializer; + +import com.alibaba.fastjson2.JSONWriter; +import com.alibaba.fastjson2.writer.ObjectWriter; +import com.yubico.webauthn.data.ByteArray; + +import java.lang.reflect.Type; + +/** + * @author Kane + * @since 2025/11/11 18:11 + */ +public class ByteArraySerializer implements ObjectWriter { + + public static final ByteArraySerializer INSTANCE = new ByteArraySerializer(); + + @Override + public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) { + if (object == null){ + jsonWriter.writeNull(); + } + ByteArray array = (ByteArray) object; + jsonWriter.writeString(array.getBase64Url()); + } +} diff --git a/src/main/java/com/kane/animo/filter/ExceptionFilter.java b/src/main/java/com/kane/animo/filter/ExceptionFilter.java index 83c0b86..5fa14a6 100644 --- a/src/main/java/com/kane/animo/filter/ExceptionFilter.java +++ b/src/main/java/com/kane/animo/filter/ExceptionFilter.java @@ -20,6 +20,6 @@ public class ExceptionFilter { @ExceptionHandler(NotLoginException.class) public R handleException(NotLoginException e) { - return R.error("认证失败 - 未登录"); + return R.build(401, "认证失败 - 未登录"); } }