feat(auth): 实现通行密钥验证功能
- 添加了对通行密钥断言验证的支持- 更新了用户凭证实体类以支持传输方式存储 - 修改了凭证仓库实现,以正确处理认证器传输方式 - 调整了授权服务实现,完善注册与验证流程 - 配置了Sa-Token拦截器,排除通行密钥相关路径 - 引入必要的异常处理和JSON序列化支持 - 增加了对凭证签名计数更新的支持 - 优化了缓存键的管理逻辑
This commit is contained in:
parent
ecfeb0e9f5
commit
7111ad07d6
@ -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<String> getPasskeyAssertionOptions(HttpServletRequest httpServletRequest) {
|
||||
public R<String> 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<Void> verifyPasskeyAssertion(HttpServletRequest httpServletRequest, @RequestBody String credential) {
|
||||
service.finishPasskeyAssertion(httpServletRequest.getSession().getId(), credential);
|
||||
return R.success();
|
||||
public R<String> verifyPasskeyAssertion(HttpServletRequest httpServletRequest, @RequestBody String credential) throws IOException, AssertionFailedException {
|
||||
String s = service.finishPasskeyAssertion(httpServletRequest.getSession().getId(), credential);
|
||||
return R.success(s);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
/**
|
||||
* 删除标识
|
||||
*/
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> 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<UserCredential> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user