feat(auth): 支持通行密钥凭证名称和查看功能
- 新增凭证名称字段用于注册时标识不同通行密钥 - 添加获取所有用户凭证接口,返回凭证列表及使用信息 - 更新用户凭证实体类增加名称、最后使用时间和创建时间字段 - 实现凭证信息的 VO 转换与接口暴露 - 增加 ByteArray 的序列化与反序列化支持以适配 WebAuthn 数据结构 - 配置通行密钥依赖方允许来源域名白名单 - 修改异常过滤器中未登录响应状态码为 401
This commit is contained in:
parent
7111ad07d6
commit
3ecff3cec3
@ -1,6 +1,7 @@
|
|||||||
package com.kane.animo.auth.controller;
|
package com.kane.animo.auth.controller;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
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.auth.service.PasskeyAuthorizationService;
|
||||||
import com.kane.animo.model.R;
|
import com.kane.animo.model.R;
|
||||||
import com.yubico.webauthn.exception.AssertionFailedException;
|
import com.yubico.webauthn.exception.AssertionFailedException;
|
||||||
@ -10,6 +11,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通行密钥
|
* 通行密钥
|
||||||
@ -35,10 +37,11 @@ public class PasskeyAuthorizationController {
|
|||||||
/**
|
/**
|
||||||
* 验证通行密钥
|
* 验证通行密钥
|
||||||
* @param credential 凭证
|
* @param credential 凭证
|
||||||
|
* @param name 凭证名称
|
||||||
*/
|
*/
|
||||||
@PostMapping("/registration")
|
@PostMapping("/registration")
|
||||||
public R<Void> verifyPasskeyRegistration(@RequestBody String credential) throws RegistrationFailedException, IOException {
|
public R<Void> verifyPasskeyRegistration(@RequestBody String credential, String name) throws RegistrationFailedException, IOException {
|
||||||
service.finishPasskeyRegistration(credential);
|
service.finishPasskeyRegistration(credential, name);
|
||||||
return R.success();
|
return R.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,4 +66,13 @@ public class PasskeyAuthorizationController {
|
|||||||
return R.success(s);
|
return R.success(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有凭证
|
||||||
|
* @return 凭证
|
||||||
|
*/
|
||||||
|
@GetMapping("/credentials")
|
||||||
|
public R<List<UserCredentialVO>> getCredentials() {
|
||||||
|
return R.success(service.getCredentials());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import com.baomidou.mybatisplus.annotation.TableName;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户凭证
|
* 用户凭证
|
||||||
* @author Kane
|
* @author Kane
|
||||||
@ -24,6 +26,10 @@ public class UserCredential {
|
|||||||
* 用户ID
|
* 用户ID
|
||||||
*/
|
*/
|
||||||
private Long userId;
|
private Long userId;
|
||||||
|
/**
|
||||||
|
* 用户凭证名称
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
/**
|
/**
|
||||||
* 用户凭证
|
* 用户凭证
|
||||||
*/
|
*/
|
||||||
@ -36,6 +42,14 @@ public class UserCredential {
|
|||||||
* 传输方式
|
* 传输方式
|
||||||
*/
|
*/
|
||||||
private String transports;
|
private String transports;
|
||||||
|
/**
|
||||||
|
* 最后使用时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime lastUsed;
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime createTime;
|
||||||
/**
|
/**
|
||||||
* 删除标识
|
* 删除标识
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
@ -1,10 +1,12 @@
|
|||||||
package com.kane.animo.auth.service;
|
package com.kane.animo.auth.service;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
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.AssertionFailedException;
|
||||||
import com.yubico.webauthn.exception.RegistrationFailedException;
|
import com.yubico.webauthn.exception.RegistrationFailedException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通行密钥授权服务
|
* 通行密钥授权服务
|
||||||
@ -24,8 +26,9 @@ public interface PasskeyAuthorizationService {
|
|||||||
* 验证通行密钥
|
* 验证通行密钥
|
||||||
*
|
*
|
||||||
* @param credential 凭证
|
* @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 凭证
|
* @param credential 凭证
|
||||||
*/
|
*/
|
||||||
String finishPasskeyAssertion(String id, String credential) throws IOException, AssertionFailedException;
|
String finishPasskeyAssertion(String id, String credential) throws IOException, AssertionFailedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有凭证
|
||||||
|
* @return 凭证
|
||||||
|
*/
|
||||||
|
List<UserCredentialVO> getCredentials();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWra
|
|||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.kane.animo.auth.domain.User;
|
import com.kane.animo.auth.domain.User;
|
||||||
import com.kane.animo.auth.domain.UserCredential;
|
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.UserCredentialMapper;
|
||||||
import com.kane.animo.auth.mapper.UserMapper;
|
import com.kane.animo.auth.mapper.UserMapper;
|
||||||
import com.kane.animo.auth.service.PasskeyAuthorizationService;
|
import com.kane.animo.auth.service.PasskeyAuthorizationService;
|
||||||
@ -20,7 +21,7 @@ import jakarta.annotation.Resource;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.HashSet;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
|
||||||
@ -73,9 +74,10 @@ public class PasskeyAuthorizationServiceImpl implements PasskeyAuthorizationServ
|
|||||||
* 验证通行密钥
|
* 验证通行密钥
|
||||||
*
|
*
|
||||||
* @param credential 凭证
|
* @param credential 凭证
|
||||||
|
* @param name 凭证名称
|
||||||
*/
|
*/
|
||||||
@Override
|
@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());
|
User user = userMapper.selectById(StpUtil.getLoginIdAsLong());
|
||||||
PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> pkc =
|
PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> pkc =
|
||||||
PublicKeyCredential.parseRegistrationResponseJson(credential);
|
PublicKeyCredential.parseRegistrationResponseJson(credential);
|
||||||
@ -89,6 +91,7 @@ public class PasskeyAuthorizationServiceImpl implements PasskeyAuthorizationServ
|
|||||||
.build());
|
.build());
|
||||||
UserCredential userCredential = new UserCredential()
|
UserCredential userCredential = new UserCredential()
|
||||||
.setUserId(user.getId())
|
.setUserId(user.getId())
|
||||||
|
.setName(name)
|
||||||
.setIdentity(JSON.toJSONString(request.getUser()))
|
.setIdentity(JSON.toJSONString(request.getUser()))
|
||||||
.setTransports(JSON.toJSONString(result.getKeyId().getTransports().orElse(new TreeSet<>())))
|
.setTransports(JSON.toJSONString(result.getKeyId().getTransports().orElse(new TreeSet<>())))
|
||||||
.setCredential(JSON.toJSONString(RegisteredCredential.builder()
|
.setCredential(JSON.toJSONString(RegisteredCredential.builder()
|
||||||
@ -145,6 +148,7 @@ public class PasskeyAuthorizationServiceImpl implements PasskeyAuthorizationServ
|
|||||||
RegisteredCredential build = parsed.toBuilder().signatureCount(result.getSignatureCount()).build();
|
RegisteredCredential build = parsed.toBuilder().signatureCount(result.getSignatureCount()).build();
|
||||||
new LambdaUpdateChainWrapper<>(credentialMapper)
|
new LambdaUpdateChainWrapper<>(credentialMapper)
|
||||||
.set(UserCredential::getCredential, JSON.toJSONString(build))
|
.set(UserCredential::getCredential, JSON.toJSONString(build))
|
||||||
|
.set(UserCredential::getLastUsed, LocalDateTime.now())
|
||||||
.eq(UserCredential::getId, userCredential.getId())
|
.eq(UserCredential::getId, userCredential.getId())
|
||||||
.update();
|
.update();
|
||||||
}
|
}
|
||||||
@ -152,4 +156,23 @@ public class PasskeyAuthorizationServiceImpl implements PasskeyAuthorizationServ
|
|||||||
StpUtil.login(one.getId());
|
StpUtil.login(one.getId());
|
||||||
return result.getUsername();
|
return result.getUsername();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有凭证
|
||||||
|
*
|
||||||
|
* @return 凭证
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<UserCredentialVO> getCredentials() {
|
||||||
|
List<UserCredential> 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
src/main/java/com/kane/animo/config/JsonConfigurer.java
Normal file
22
src/main/java/com/kane/animo/config/JsonConfigurer.java
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,6 +9,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
|
|||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通行密钥配置
|
* 通行密钥配置
|
||||||
* @author Kane
|
* @author Kane
|
||||||
@ -31,6 +33,10 @@ public class PasskeyConfigurer {
|
|||||||
return RelyingParty.builder()
|
return RelyingParty.builder()
|
||||||
.identity(rpIdentity)
|
.identity(rpIdentity)
|
||||||
.credentialRepository(credentialRepository)
|
.credentialRepository(credentialRepository)
|
||||||
|
.origins(Set.of(
|
||||||
|
"http://localhost:5173",
|
||||||
|
"https://animo.alina-dace.info"
|
||||||
|
))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<ByteArray> {
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<ByteArray> {
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -20,6 +20,6 @@ public class ExceptionFilter {
|
|||||||
|
|
||||||
@ExceptionHandler(NotLoginException.class)
|
@ExceptionHandler(NotLoginException.class)
|
||||||
public R<String> handleException(NotLoginException e) {
|
public R<String> handleException(NotLoginException e) {
|
||||||
return R.error("认证失败 - 未登录");
|
return R.build(401, "认证失败 - 未登录");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user