From 7111ad07d600e3894add632ead24cfee2a161f98 Mon Sep 17 00:00:00 2001 From: Grand-cocoa <1075576561@qq.com49111108+grand-cocoa@users.noreply.github.com> Date: Mon, 10 Nov 2025 19:10:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E5=AE=9E=E7=8E=B0=E9=80=9A?= =?UTF-8?q?=E8=A1=8C=E5=AF=86=E9=92=A5=E9=AA=8C=E8=AF=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加了对通行密钥断言验证的支持- 更新了用户凭证实体类以支持传输方式存储 - 修改了凭证仓库实现,以正确处理认证器传输方式 - 调整了授权服务实现,完善注册与验证流程 - 配置了Sa-Token拦截器,排除通行密钥相关路径 - 引入必要的异常处理和JSON序列化支持 - 增加了对凭证签名计数更新的支持 - 优化了缓存键的管理逻辑 --- .../PasskeyAuthorizationController.java | 9 +-- .../animo/auth/domain/UserCredential.java | 6 ++ .../service/PasskeyAuthorizationService.java | 5 +- .../impl/CredentialRepositoryImpl.java | 7 +- .../impl/PasskeyAuthorizationServiceImpl.java | 67 ++++++++++++++++--- .../kane/animo/config/SaTokenConfigurer.java | 6 +- 6 files changed, 82 insertions(+), 18 deletions(-) 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 29589b0..c30a93e 100644 --- a/src/main/java/com/kane/animo/auth/controller/PasskeyAuthorizationController.java +++ b/src/main/java/com/kane/animo/auth/controller/PasskeyAuthorizationController.java @@ -3,6 +3,7 @@ package com.kane.animo.auth.controller; import com.fasterxml.jackson.core.JsonProcessingException; import com.kane.animo.auth.service.PasskeyAuthorizationService; import com.kane.animo.model.R; +import com.yubico.webauthn.exception.AssertionFailedException; import com.yubico.webauthn.exception.RegistrationFailedException; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; @@ -47,7 +48,7 @@ public class PasskeyAuthorizationController { * @return 验证参数 */ @GetMapping("/assertion/options") - public R getPasskeyAssertionOptions(HttpServletRequest httpServletRequest) { + public R getPasskeyAssertionOptions(HttpServletRequest httpServletRequest) throws JsonProcessingException { return R.success(service.startPasskeyAssertion(httpServletRequest.getSession().getId())); } @@ -57,9 +58,9 @@ public class PasskeyAuthorizationController { * @param credential 凭证 */ @PostMapping("/assertion") - public R verifyPasskeyAssertion(HttpServletRequest httpServletRequest, @RequestBody String credential) { - service.finishPasskeyAssertion(httpServletRequest.getSession().getId(), credential); - return R.success(); + public R verifyPasskeyAssertion(HttpServletRequest httpServletRequest, @RequestBody String credential) throws IOException, AssertionFailedException { + String s = service.finishPasskeyAssertion(httpServletRequest.getSession().getId(), credential); + return R.success(s); } } 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 b75d37f..2debb11 100644 --- a/src/main/java/com/kane/animo/auth/domain/UserCredential.java +++ b/src/main/java/com/kane/animo/auth/domain/UserCredential.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; +import lombok.experimental.Accessors; /** * 用户凭证 @@ -11,6 +12,7 @@ import lombok.Data; * @since 2025/11/7 16:57 */ @Data +@Accessors(chain = true) @TableName("user_credential") public class UserCredential { /** @@ -30,6 +32,10 @@ public class UserCredential { * 身份标识 */ private String identity; + /** + * 传输方式 + */ + private String transports; /** * 删除标识 */ 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 d543e86..26a419a 100644 --- a/src/main/java/com/kane/animo/auth/service/PasskeyAuthorizationService.java +++ b/src/main/java/com/kane/animo/auth/service/PasskeyAuthorizationService.java @@ -1,6 +1,7 @@ package com.kane.animo.auth.service; import com.fasterxml.jackson.core.JsonProcessingException; +import com.yubico.webauthn.exception.AssertionFailedException; import com.yubico.webauthn.exception.RegistrationFailedException; import java.io.IOException; @@ -32,7 +33,7 @@ public interface PasskeyAuthorizationService { * @param id 登录id * @return 验证参数 */ - String startPasskeyAssertion(String id); + String startPasskeyAssertion(String id) throws JsonProcessingException; /** * 验证通行密钥 @@ -40,5 +41,5 @@ public interface PasskeyAuthorizationService { * @param id 登录id * @param credential 凭证 */ - void finishPasskeyAssertion(String id, String credential); + String finishPasskeyAssertion(String id, String credential) throws IOException, AssertionFailedException; } diff --git a/src/main/java/com/kane/animo/auth/service/impl/CredentialRepositoryImpl.java b/src/main/java/com/kane/animo/auth/service/impl/CredentialRepositoryImpl.java index a9dda56..437c81e 100644 --- a/src/main/java/com/kane/animo/auth/service/impl/CredentialRepositoryImpl.java +++ b/src/main/java/com/kane/animo/auth/service/impl/CredentialRepositoryImpl.java @@ -9,12 +9,14 @@ import com.kane.animo.auth.mapper.UserMapper; import com.yubico.webauthn.CredentialRepository; import com.yubico.webauthn.RegisteredCredential; import com.yubico.webauthn.RegistrationResult; +import com.yubico.webauthn.data.AuthenticatorTransport; import com.yubico.webauthn.data.ByteArray; import com.yubico.webauthn.data.PublicKeyCredentialDescriptor; import com.yubico.webauthn.data.UserIdentity; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; +import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; @@ -53,11 +55,12 @@ public class CredentialRepositoryImpl implements CredentialRepository { .eq(UserCredential::getUserId, one.getId()) .list() .stream() - .map(UserCredential::getCredential) +// .map(UserCredential::getCredential) .map(x -> { - RegisteredCredential credential = JSON.parseObject(x, RegisteredCredential.class); + RegisteredCredential credential = JSON.parseObject(x.getCredential(), RegisteredCredential.class); return PublicKeyCredentialDescriptor.builder() .id(credential.getCredentialId()) + .transports(new HashSet<>(JSON.parseArray(x.getTransports(), AuthenticatorTransport.class))) .build(); }).collect(Collectors.toSet()); } 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 53f631f..fbd48cb 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 @@ -1,21 +1,28 @@ package com.kane.animo.auth.service.impl; import cn.dev33.satoken.stp.StpUtil; +import com.alibaba.fastjson2.JSON; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; 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.mapper.UserCredentialMapper; import com.kane.animo.auth.mapper.UserMapper; import com.kane.animo.auth.service.PasskeyAuthorizationService; +import com.kane.animo.exception.ServiceException; import com.kane.animo.util.CacheService; -import com.yubico.webauthn.FinishRegistrationOptions; -import com.yubico.webauthn.RegistrationResult; -import com.yubico.webauthn.RelyingParty; -import com.yubico.webauthn.StartRegistrationOptions; +import com.yubico.webauthn.*; import com.yubico.webauthn.data.*; +import com.yubico.webauthn.exception.AssertionFailedException; import com.yubico.webauthn.exception.RegistrationFailedException; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.TreeSet; /** * 通行密钥授权服务实现 @@ -31,6 +38,9 @@ public class PasskeyAuthorizationServiceImpl implements PasskeyAuthorizationServ @Resource private UserMapper userMapper; + @Resource + private UserCredentialMapper credentialMapper; + @Resource private CacheService cacheService; @@ -77,6 +87,17 @@ public class PasskeyAuthorizationServiceImpl implements PasskeyAuthorizationServ .request(request) .response(pkc) .build()); + UserCredential userCredential = new UserCredential() + .setUserId(user.getId()) + .setIdentity(JSON.toJSONString(request.getUser())) + .setTransports(JSON.toJSONString(result.getKeyId().getTransports().orElse(new TreeSet<>()))) + .setCredential(JSON.toJSONString(RegisteredCredential.builder() + .credentialId(result.getKeyId().getId()) + .userHandle(request.getUser().getId()) + .publicKeyCose(result.getPublicKeyCose()) + .signatureCount(result.getSignatureCount()) + .build())); + credentialMapper.insert(userCredential); cacheService.removeCache(PASSKEY_REGISTRATION_KEY + user.getId()); } @@ -87,8 +108,10 @@ public class PasskeyAuthorizationServiceImpl implements PasskeyAuthorizationServ * @return 验证参数 */ @Override - public String startPasskeyAssertion(String id) { - return ""; + public String startPasskeyAssertion(String id) throws JsonProcessingException { + AssertionRequest request = relyingParty.startAssertion(StartAssertionOptions.builder().build()); + cacheService.setCache(PASSKEY_ASSERTION_KEY + id, request.toJson()); + return request.toCredentialsGetJson(); } /** @@ -98,7 +121,35 @@ public class PasskeyAuthorizationServiceImpl implements PasskeyAuthorizationServ * @param credential 凭证 */ @Override - public void finishPasskeyAssertion(String id, String credential) { - + public String finishPasskeyAssertion(String id, String credential) throws IOException, AssertionFailedException { + String cache = cacheService.getCache(PASSKEY_ASSERTION_KEY + id, String.class); + AssertionRequest request = AssertionRequest.fromJson(cache); + PublicKeyCredential keyCredential = PublicKeyCredential.parseAssertionResponseJson(credential); + AssertionResult result = relyingParty.finishAssertion(FinishAssertionOptions.builder() + .request(request) + .response(keyCredential) + .build()); + cacheService.removeCache(PASSKEY_ASSERTION_KEY + id); + if (!result.isSuccess()){ + throw new ServiceException("验证失败"); + } + User one = new LambdaQueryChainWrapper<>(userMapper) + .eq(User::getUser, result.getUsername()) + .one(); + List list = new LambdaQueryChainWrapper<>(credentialMapper) + .eq(UserCredential::getUserId, one.getId()) + .list(); + for (UserCredential userCredential : list) { + RegisteredCredential parsed = JSON.parseObject(userCredential.getCredential(), RegisteredCredential.class); + if (parsed.getCredentialId().equals(result.getCredential().getCredentialId())){ + RegisteredCredential build = parsed.toBuilder().signatureCount(result.getSignatureCount()).build(); + new LambdaUpdateChainWrapper<>(credentialMapper) + .set(UserCredential::getCredential, JSON.toJSONString(build)) + .eq(UserCredential::getId, userCredential.getId()) + .update(); + } + } + StpUtil.login(one.getId()); + return result.getUsername(); } } diff --git a/src/main/java/com/kane/animo/config/SaTokenConfigurer.java b/src/main/java/com/kane/animo/config/SaTokenConfigurer.java index 7bd315a..6f06ae1 100644 --- a/src/main/java/com/kane/animo/config/SaTokenConfigurer.java +++ b/src/main/java/com/kane/animo/config/SaTokenConfigurer.java @@ -17,7 +17,9 @@ public class SaTokenConfigurer implements WebMvcConfigurer { public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor(handler -> StpUtil.checkLogin())) .addPathPatterns("/**") - .excludePathPatterns("/auth/login"); -// .excludePathPatterns("/auth/register"); + .excludePathPatterns("/auth/login") + .excludePathPatterns("/auth/register") + .excludePathPatterns("/passkey/assertion/options") + .excludePathPatterns("/passkey/assertion"); } }