引言
在OAuth集成框架中,数据模型是整个系统的核心。它不仅要承载业务数据,还要处理不同平台间的差异,保证数据的完整性和一致性。JustAuth通过精心设计的数据模型,成功抽象了平台的用户信息,实现了统一的API接口。
本期我们将深入分析JustAuth的数据模型设计,从领域建模原则到具体实现细节,探索如何设计出既灵活又稳定的数据模型。
一、领域建模原则
1.1 充血模型 vs 贫血模型的选择
在DDD(领域驱动设计)中,充血模型和贫血模型是两种不同的建模方式:
贫血模型(Anemic Domain Model):
- 对象只包含数据,不包含业务逻辑
- 业务逻辑在Service层处理
- 优点是简单直观,缺点是对象缺乏行为
充血模型(Rich Domain Model):
- 对象既包含数据,也包含业务逻辑
- 对象具有自我管理的能力
- 优点是对象更加内聚,缺点是复杂度较高
JustAuth选择了贫血模型,这是基于以下考虑:
// JustAuth的AuthUser模型 - 贫血模型设计
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AuthUser implements Serializable {
private String uuid;
private String username;
private String nickname;
// ... 其他属性
// 没有业务方法,只有数据
}
选择贫血模型的原因:
- 跨平台兼容性:不同平台的用户信息差异很大,难以在模型层统一业务逻辑
- 序列化友好:贫血模型更容易进行JSON序列化/反序列化
- 框架定位:JustAuth定位为工具框架,而非业务系统
- 扩展性:让使用者可以根据业务需求扩展模型
1.2 数据传输对象的设计原则
JustAuth的数据模型设计遵循了以下原则:
1.2.1 最小化接口原则
// AuthUser只暴露必要的属性,避免信息泄露
public class AuthUser {
private String uuid; // 业务主键
private String username; // 用户名
private String nickname; // 昵称
// 不暴露敏感信息如密码等
}
1.2.2 向后兼容性原则
// 通过Builder模式支持灵活的对象构建
@Builder
public class AuthUser {
// 新增字段不会破坏现有代码
private boolean snapshotUser; // 新增字段
}
1.2.3 领域完整性保证
// 通过关联关系保证数据的完整性
public class AuthUser {
private AuthToken token; // 关联令牌信息
private JSONObject rawUserInfo; // 保留原始数据
}
二、核心模型分析
2.1 AuthUser - 用户信息聚合
AuthUser是JustAuth的核心模型,它抽象了不同平台的用户信息:
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AuthUser implements Serializable {
/**
* 用户第三方系统的唯一id。在调用方集成该组件时,可以用uuid + source唯一确定一个用户
*/
private String uuid;
/**
* 用户名
*/
private String username;
/**
* 用户昵称
*/
private String nickname;
/**
* 用户头像
*/
private String avatar;
/**
* 用户网址
*/
private String blog;
/**
* 所在公司
*/
private String company;
/**
* 位置
*/
private String location;
/**
* 用户邮箱
*/
private String email;
/**
* 用户备注(各平台中的用户个人介绍)
*/
private String remark;
/**
* 性别
*/
private AuthUserGender gender;
/**
* 用户来源
*/
private String source;
/**
* 用户授权的token信息
*/
private AuthToken token;
/**
* 第三方平台返回的原始用户信息
*/
private JSONObject rawUserInfo;
/**
* 微信公众号 - 网页授权的登录时可用
* 微信针对网页授权登录,增加了一个快照页的逻辑,快照页获取到的微信用户的 uid oid 和头像昵称都是虚拟的信息
*/
private boolean snapshotUser;
}
2.1.1 设计亮点分析
1. 业务主键设计
private String uuid; // 第三方平台的唯一标识
private String source; // 平台来源标识
// 组合键:uuid + source 可以唯一确定一个用户
2. 关联关系处理
private AuthToken token; // 关联令牌对象
private JSONObject rawUserInfo; // 保留原始数据,便于扩展
3. 平台特性支持
private boolean snapshotUser; // 支持微信快照页的特殊逻辑
2.1.2 字段映射策略
不同平台的用户信息字段差异很大,JustAuth采用了灵活的映射策略:
// GitHub平台实现示例
@Override
public AuthUser getUserInfo(AuthToken authToken) {
JSONObject object = JSONObject.parseObject(response);
return AuthUser.builder()
.rawUserInfo(object) // 保留原始数据
.uuid(object.getString("id")) // GitHub的id映射到uuid
.username(object.getString("login")) // GitHub的login映射到username
.avatar(object.getString("avatar_url")) // GitHub的avatar_url映射到avatar
.blog(object.getString("blog")) // GitHub的blog映射到blog
.nickname(object.getString("name")) // GitHub的name映射到nickname
.company(object.getString("company")) // GitHub的company映射到company
.location(object.getString("location")) // GitHub的location映射到location
.email(object.getString("email")) // GitHub的email映射到email
.remark(object.getString("bio")) // GitHub的bio映射到remark
.gender(AuthUserGender.UNKNOWN) // GitHub不提供性别信息
.token(authToken) // 关联令牌信息
.source(source.toString()) // 设置来源标识
.build();
}
2.2 AuthToken - 令牌值对象
AuthToken封装了OAuth流程中的各种令牌信息:
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AuthToken implements Serializable {
// 标准OAuth2.0字段
private String accessToken;
private int expireIn;
private String refreshToken;
private int refreshTokenExpireIn;
private String uid;
private String openId;
private String accessCode;
private String unionId;
// Google特有字段
private String scope;
private String tokenType;
private String idToken;
// 小米特有字段
private String macAlgorithm;
private String macKey;
// 企业微信特有字段
private String code;
private boolean snapshotUser;
// Twitter特有字段
private String oauthToken;
private String oauthTokenSecret;
private String userId;
private String screenName;
private Boolean oauthCallbackConfirmed;
// Apple特有字段
private String username;
// 钉钉特有字段
private String corpId;
}
2.2.1 设计特点
1. 兼容性设计
- 包含标准OAuth2.0字段
- 通过扩展字段支持各平台特有属性
- 使用包装类型避免基本类型的默认值问题
2. 时间处理策略
private int expireIn; // 过期时间(秒)
private int refreshTokenExpireIn; // 刷新令牌过期时间(秒)
// 注意:使用int而不是long,因为OAuth2.0标准中expire_in通常是int类型
3. 平台差异化处理
// 不同平台的令牌结构差异很大,通过扩展字段支持
private String oauthToken; // Twitter OAuth1.0a
private String oauthTokenSecret; // Twitter OAuth1.0a
private String idToken; // Google OpenID Connect
private String macKey; // 小米MAC认证
2.3 AuthCallback - 回调参数封装
AuthCallback封装了OAuth回调时的各种参数:
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AuthCallback implements Serializable {
/**
* 访问AuthorizeUrl后回调时带的参数code
*/
private String code;
/**
* 访问AuthorizeUrl后回调时带的参数auth_code,该参数目前只使用于支付宝登录
*/
private String auth_code;
/**
* 访问AuthorizeUrl后回调时带的参数state,用于和请求AuthorizeUrl前的state比较,防止CSRF攻击
*/
private String state;
/**
* 华为授权登录接受code的参数名
*/
private String authorization_code;
/**
* Twitter回调后返回的oauth_token
*/
private String oauth_token;
/**
* Twitter回调后返回的oauth_verifier
*/
private String oauth_verifier;
/**
* 苹果仅在用户首次授权应用程序时返回此值
*/
private String user;
/**
* 苹果错误信息,仅在用户取消授权时返回此值
*/
private String error;
/**
* 统一的code获取方法
*/
public String getCode() {
return StringUtils.isEmpty(code) ? auth_code : code;
}
}
2.3.1 设计亮点
1. 参数统一化
public String getCode() {
return StringUtils.isEmpty(code) ? auth_code : code;
}
// 支付宝使用auth_code,其他平台使用code,通过此方法统一处理
2. 平台兼容性
- 支持不同平台的回调参数差异
- 通过字段扩展支持新平台的特殊参数
3. 安全性考虑
private String state; // CSRF防护参数
2.4 AuthResponse - 统一响应封装
AuthResponse提供了统一的响应格式:
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AuthResponse<T> implements Serializable {
/**
* 授权响应状态码
*/
private int code;
/**
* 授权响应信息
*/
private String msg;
/**
* 授权响应数据,当且仅当 code = 2000 时返回
*/
private T data;
/**
* 是否请求成功
*/
public boolean ok() {
return this.code == AuthResponseStatus.SUCCESS.getCode();
}
}
2.4.1 设计特点
1. 泛型设计
public class AuthResponse<T> implements Serializable {
private T data; // 支持任意类型的响应数据
}
2. 状态判断方法
public boolean ok() {
return this.code == AuthResponseStatus.SUCCESS.getCode();
}
// 提供便捷的状态判断方法
三、枚举类型设计
3.1 AuthUserGender - 性别枚举
性别信息在不同平台中的表示差异很大,JustAuth通过枚举统一处理:
@Getter
@AllArgsConstructor
public enum AuthUserGender {
MALE("1", "男"),
FEMALE("0", "女"),
UNKNOWN("-1", "未知");
private String code;
private String desc;
/**
* 获取用户的实际性别,常规网站
*/
public static AuthUserGender getRealGender(String originalGender) {
if (null == originalGender || UNKNOWN.getCode().equals(originalGender)) {
return UNKNOWN;
}
String[] males = {"m", "男", "1", "male"};
if (Arrays.asList(males).contains(originalGender.toLowerCase())) {
return MALE;
}
return FEMALE;
}
/**
* 获取微信平台用户的实际性别,0表示未定义,1表示男性,2表示女性
*/
public static AuthUserGender getWechatRealGender(String originalGender) {
if (StringUtils.isEmpty(originalGender) || "0".equals(originalGender)) {
return AuthUserGender.UNKNOWN;
}
return getRealGender(originalGender);
}
}
3.1.1 设计亮点
1. 多平台兼容
- 提供通用的性别解析方法
- 针对特殊平台(如微信)提供专门的解析方法
2. 容错处理
UNKNOWN("-1", "未知") // 提供容错值,处理无法解析的性别信息
3. 静态工具方法
public static AuthUserGender getRealGender(String originalGender)
public static AuthUserGender getWechatRealGender(String originalGender)
// 提供便捷的转换方法
四、JSON序列化策略
4.1 Fastjson的使用技巧
JustAuth使用Fastjson进行JSON序列化,这带来了以下优势:
1. 性能优势
// Fastjson的性能优于Jackson和Gson
JSONObject object = JSONObject.parseObject(response);
String jsonString = JSONObject.toJSONString(authUser);
2. 灵活性
// 支持动态字段访问
JSONObject rawUserInfo = object.getJSONObject("user");
String customField = object.getString("custom_field");
3. 兼容性
// 保留原始数据,便于处理平台特有的字段
private JSONObject rawUserInfo; // 存储完整的原始响应
4.2 序列化兼容性处理
1. 字段映射处理
// 不同平台的字段名差异
"avatar_url" -> "avatar" // GitHub
"profile_image_url" -> "avatar" // Twitter
"headimgurl" -> "avatar" // 微信
2. 数据类型转换
// 处理不同平台的数据类型差异
String id = object.getString("id"); // 字符串ID
Long numericId = object.getLong("id"); // 数字ID
3. 空值处理
// 安全的字段获取
String email = object.getString("email");
if (StringUtils.isEmpty(email)) {
email = null; // 统一空值处理
}
4.3 敏感信息保护
1. 令牌信息保护
// 在日志中脱敏处理
@Override
public String toString() {
return "AuthToken{" +
"accessToken='" + (accessToken != null ? "***" : null) + '\'' +
", refreshToken='" + (refreshToken != null ? "***" : null) + '\'' +
// ... 其他字段
'}';
}
2. 用户信息保护
// 避免在日志中输出敏感信息
private String email; // 邮箱信息需要谨慎处理
private String phone; // 手机号等敏感信息
五、实战案例:设计更完善的用户模型
5.1 现有模型的不足
分析JustAuth的AuthUser模型,我们可以发现一些可以改进的地方:
1. 缺乏验证逻辑
// 当前模型没有验证逻辑
public class AuthUser {
private String email; // 没有邮箱格式验证
private String uuid; // 没有UUID格式验证
}
2. 缺乏业务方法
// 当前模型是纯数据对象
public class AuthUser {
// 没有提供业务方法
// 如:isValid(), getDisplayName(), getAvatarUrl()等
}
3. 扩展性有限
// 难以支持动态字段
public class AuthUser {
// 字段是固定的,难以支持平台特有的字段
}
5.2 改进方案设计
5.2.1 增强的AuthUser模型
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class EnhancedAuthUser implements Serializable {
// 基础字段
private String uuid;
private String username;
private String nickname;
private String avatar;
private String email;
private AuthUserGender gender;
private String source;
private AuthToken token;
// 扩展字段
private Map<String, Object> extendedFields;
private JSONObject rawUserInfo;
// 验证方法
public boolean isValid() {
return StringUtils.isNotEmpty(uuid) && StringUtils.isNotEmpty(source);
}
public boolean hasValidEmail() {
return StringUtils.isNotEmpty(email) && EmailValidator.isValid(email);
}
// 业务方法
public String getDisplayName() {
return StringUtils.isNotEmpty(nickname) ? nickname : username;
}
public String getAvatarUrl() {
if (StringUtils.isEmpty(avatar)) {
return getDefaultAvatarUrl();
}
return avatar;
}
public String getDefaultAvatarUrl() {
return "https://www.gravatar.com/avatar/" +
DigestUtils.md5Hex(StringUtils.defaultString(email, uuid)) +
"?d=identicon";
}
// 扩展字段操作
public void setExtendedField(String key, Object value) {
if (extendedFields == null) {
extendedFields = new HashMap<>();
}
extendedFields.put(key, value);
}
public Object getExtendedField(String key) {
return extendedFields != null ? extendedFields.get(key) : null;
}
// 平台特定方法
public boolean isWechatUser() {
return "wechat".equals(source) || "wechat_open".equals(source);
}
public boolean isGithubUser() {
return "github".equals(source);
}
// 序列化优化
@Override
public String toString() {
return "EnhancedAuthUser{" +
"uuid='" + uuid + '\'' +
", username='" + username + '\'' +
", nickname='" + nickname + '\'' +
", source='" + source + '\'' +
", email='" + (hasValidEmail() ? "***" : email) + '\'' +
", isValid=" + isValid() +
'}';
}
}
5.2.2 验证器设计
public class EmailValidator {
private static final String EMAIL_PATTERN =
"^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";
public static boolean isValid(String email) {
if (StringUtils.isEmpty(email)) {
return false;
}
return email.matches(EMAIL_PATTERN);
}
}
public class UuidValidator {
public static boolean isValid(String uuid) {
if (StringUtils.isEmpty(uuid)) {
return false;
}
// 根据平台特性验证UUID格式
return uuid.matches("^[a-zA-Z0-9_-]+$");
}
}
5.2.3 工厂模式应用
public class AuthUserFactory {
public static EnhancedAuthUser createFromPlatform(String source, JSONObject rawData) {
EnhancedAuthUser user = new EnhancedAuthUser();
user.setSource(source);
user.setRawUserInfo(rawData);
// 根据平台特性设置字段
switch (source.toLowerCase()) {
case "github":
return createGithubUser(rawData, user);
case "wechat":
return createWechatUser(rawData, user);
default:
return createGenericUser(rawData, user);
}
}
private static EnhancedAuthUser createGithubUser(JSONObject data, EnhancedAuthUser user) {
user.setUuid(data.getString("id"));
user.setUsername(data.getString("login"));
user.setNickname(data.getString("name"));
user.setAvatar(data.getString("avatar_url"));
user.setEmail(data.getString("email"));
user.setExtendedField("blog", data.getString("blog"));
user.setExtendedField("company", data.getString("company"));
user.setExtendedField("location", data.getString("location"));
user.setExtendedField("bio", data.getString("bio"));
return user;
}
private static EnhancedAuthUser createWechatUser(JSONObject data, EnhancedAuthUser user) {
user.setUuid(data.getString("openid"));
user.setNickname(data.getString("nickname"));
user.setAvatar(data.getString("headimgurl"));
user.setGender(AuthUserGender.getWechatRealGender(data.getString("sex")));
user.setExtendedField("province", data.getString("province"));
user.setExtendedField("city", data.getString("city"));
user.setExtendedField("country", data.getString("country"));
return user;
}
}
5.3 测试用例设计
@Test
public void testEnhancedAuthUserValidation() {
// 测试有效用户
EnhancedAuthUser validUser = EnhancedAuthUser.builder()
.uuid("12345")
.username("testuser")
.source("github")
.email("test@example.com")
.build();
assertTrue(validUser.isValid());
assertTrue(validUser.hasValidEmail());
// 测试无效用户
EnhancedAuthUser invalidUser = EnhancedAuthUser.builder()
.username("testuser")
.source("github")
.build();
assertFalse(invalidUser.isValid());
}
@Test
public void testPlatformSpecificMethods() {
EnhancedAuthUser wechatUser = EnhancedAuthUser.builder()
.source("wechat")
.build();
assertTrue(wechatUser.isWechatUser());
assertFalse(wechatUser.isGithubUser());
}
@Test
public void testExtendedFields() {
EnhancedAuthUser user = new EnhancedAuthUser();
user.setExtendedField("custom_field", "custom_value");
assertEquals("custom_value", user.getExtendedField("custom_field"));
}
六、模型演进的兼容性问题
6.1 版本兼容性策略
1. 字段添加策略
// 新增字段使用包装类型,避免基本类型的默认值问题
private String newField; // 新增字段,默认为null
private Integer newNumericField; // 数字字段使用包装类型
2. 字段废弃策略
@Deprecated
private String oldField; // 标记废弃字段,但不立即删除
// 提供替代方法
public String getNewField() {
return newField != null ? newField : oldField; // 向后兼容
}
3. 序列化兼容性
// 使用@JsonIgnoreProperties(ignoreUnknown = true)忽略未知字段
@JsonIgnoreProperties(ignoreUnknown = true)
public class AuthUser {
// 新增字段不会影响反序列化
}
6.2 数据迁移策略
1. 渐进式迁移
// 支持新旧字段并存
public class AuthUser {
private String oldField; // 旧字段
private String newField; // 新字段
public String getField() {
return newField != null ? newField : oldField;
}
}
2. 配置驱动迁移
// 通过配置控制字段映射
@Configuration
public class FieldMappingConfig {
@Value("${auth.field.mapping.enabled:false}")
private boolean fieldMappingEnabled;
public String mapField(String oldField, String newField) {
return fieldMappingEnabled ? newField : oldField;
}
}
七、最佳实践总结
7.1 数据模型设计原则
- 单一职责原则:每个模型只负责一个领域概念
- 开闭原则:对扩展开放,对修改关闭
- 依赖倒置原则:依赖抽象而非具体实现
- 接口隔离原则:提供最小化的接口
7.2 跨平台兼容性设计
- 统一抽象:找到不同平台的共同点进行抽象
- 扩展机制:通过扩展字段支持平台特有功能
- 容错处理:提供默认值和容错机制
- 原始数据保留:保留原始响应数据,便于扩展
7.3 性能优化考虑
- 序列化优化:选择合适的序列化库
- 内存管理:及时清理不需要的数据
- 缓存策略:对频繁访问的数据进行缓存
- 延迟加载:按需加载用户详细信息
7.4 安全性考虑
- 敏感信息保护:对令牌等敏感信息进行脱敏
- 输入验证:对用户输入进行严格验证
- 权限控制:根据权限返回不同的用户信息
- 审计日志:记录关键操作的审计日志
八、学习收获
通过本期对JustAuth数据模型设计的深入分析,我们学到了:
- 领域建模的艺术:如何在复杂业务场景中设计合适的数据模型
- 跨平台兼容性:如何抽象不同平台的差异,提供统一的接口
- 扩展性设计:如何设计可扩展的模型,支持未来的需求变化
- 性能与安全:如何在保证性能的同时确保数据安全
- 版本兼容性:如何保证模型演进时的向后兼容性
这些设计原则和实践经验,不仅适用于OAuth集成框架,也可以应用到其他需要处理复杂数据模型的场景中。
九、下期预告
下一期我们将深入分析JustAuth的缓存架构设计,探索状态管理的安全与性能平衡。我们将学习:
- 多层缓存接口设计
- 并发安全设计
- 内存泄漏防护机制
- 分布式缓存集成方案
敬请期待!