目录
- 一、需求
- 二、初始代码 - 上来就怼
- 三、重构1 - 单一职责(方法级)
- 四、重构2 - 单一职责(类级、策略模式)、简单工厂
- 五、重构3 - 依赖注入(避免重复创建对象)
- 六、重构4 - 使用Map替代if...else、享元模式
- 七、重构5 - 满足开闭原则、迪米特原则
- 7.1 扩展 - 责任链变种
- 八、重构6 - DRY、模板方法
一、需求
接入AuthServer框架,预留UniLoginUserDetailsService接口,负责用户登录认证逻辑,
RBAC支持多种用户登录认证,如账号密码、LDAP用户,且后续支持扩展其他的登录认证方式如手机验证码等。
二、初始代码 - 上来就怼
public class RbacUserDetailsDispatchServiceImpl implements UniLoginUserDetailsService {
/**
* 身份类型参数名称identityType
*/
private final String IDENTIY_TYPE_PARAMETER = ColumnUtils.getFieldName(UserAuth::getIdentityType);
@Override
public UniLoginUserDetails loadUserByAuthParams(Map<String, String> loginParams) throws UsernameNotFoundException {
//获取参数中的身份类型
String identityType = loginParams.get(IDENTITY_TYPE_PARAMETER);
//处理系统用户账密码登录
if (IdentityTypeEnum.SYSTEM.equalCode(identityType)) {
//具体处理逻辑......
}
//处理LDAP登录
else if (IdentityTypeEnum.LDAP.equalCode(identityType)) {
//具体处理逻辑......
}
//身份类型不存在
else {
throw I18nOAuth2AuthenticationException.ofRespCodeWithI18nMsg(SystemMessage.COMMON_LOGIN_FAILED);
}
}
@Override
public void authenticateUser(Map<String, String> loginParams, UniLoginUserDetails uniLoginUserDetails) throws AuthenticationException {
//获取参数中的身份类型
String identityType = loginParams.get(IDENTITY_TYPE_PARAMETER);
//认证系统用户
if (IdentityTypeEnum.SYSTEM.equalCode(identityType)) {
//具体处理逻辑......
}
//认证LDAP
else if (IdentityTypeEnum.LDAP.equalCode(identityType)) {
//具体处理逻辑......
}
}
}
问题:
- 违背单一职责原则
- if…else分支过多
- 后续扩展新的认证方式需修改该类,违背开闭原则
三、重构1 - 单一职责(方法级)
public class RbacUserDetailsDispatchServiceImplTemp implements UniLoginUserDetailsService {
/**
* 身份类型参数名称identityType
*/
private final String IDENTITY_TYPE_PARAMETER = ColumnUtils.getFieldName(UserAuth::getIdentityType);
@Override
public UniLoginUserDetails loadUserByAuthParams(Map<String, String> loginParams) throws UsernameNotFoundException {
//获取参数中的身份类型
String identityType = loginParams.get(IDENTITY_TYPE_PARAMETER);
//处理系统用户账密码登录
if (IdentityTypeEnum.SYSTEM.equalCode(identityType)) {
return this.loadSystemUserByAuthParams(loginParams);
}
//处理LDAP登录
else if (IdentityTypeEnum.LDAP.equalCode(identityType)) {
return this.loadLdapUserByAuthParams(loginParams);
}
//身份类型不存在
else {
throw I18nOAuth2AuthenticationException.ofRespCodeWithI18nMsg(SystemMessage.COMMON_LOGIN_FAILED);
}
}
@Override
public void authenticateUser(Map<String, String> loginParams, UniLoginUserDetails uniLoginUserDetails) throws AuthenticationException {
//获取参数中的身份类型
String identityType = loginParams.get(IDENTITY_TYPE_PARAMETER);
//认证系统用户
if (IdentityTypeEnum.SYSTEM.equalCode(identityType)) {
this.authenticateSystemUser(loginParams, uniLoginUserDetails);
}
//认证LDAP
else if (IdentityTypeEnum.LDAP.equalCode(identityType)) {
this.authenticateLdapUser(loginParams, uniLoginUserDetails);
}
}
private UniLoginUserDetails loadSystemUserByAuthParams(Map<String, String> loginParams) {
//具体处理逻辑......
}
private UniLoginUserDetails loadLdapUserByAuthParams(Map<String, String> loginParams) {
//具体处理逻辑......
}
private void authenticateSystemUser(Map<String, String> loginParams, UniLoginUserDetails uniLoginUserDetails) {
//具体处理逻辑......
}
private void authenticateLdapUser(Map<String, String> loginParams, UniLoginUserDetails uniLoginUserDetails) {
//具体处理逻辑......
}
}
改进:
- 通过方法完成职责拆分
问题:
- 单类中代码量过多
- 单类中职责过多,负责多种认证方式,后续维护起来比较困难
四、重构2 - 单一职责(类级、策略模式)、简单工厂
public class RbacUserDetailsDispatchServiceImplTemp implements UniLoginUserDetailsService {
/**
* 身份类型参数名称identityType
*/
private final String IDENTITY_TYPE_PARAMETER = ColumnUtils.getFieldName(UserAuth::getIdentityType);
@Override
public UniLoginUserDetails loadUserByAuthParams(Map<String, String> loginParams) throws UsernameNotFoundException {
//获取参数中的身份类型
String identityType = loginParams.get(IDENTITY_TYPE_PARAMETER);
//具体用户认证服务实现
UniLoginUserDetailsService uniLoginUserDetailsService = null;
//处理系统用户账密码登录
if (IdentityTypeEnum.SYSTEM.equalCode(identityType)) {
uniLoginUserDetailsService = new SystemUserDetailsServiceImpl();
}
//处理LDAP登录
else if (IdentityTypeEnum.LDAP.equalCode(identityType)) {
uniLoginUserDetailsService = new LdapUserDetailsServiceImpl();
}
//身份类型不存在
else {
throw I18nOAuth2AuthenticationException.ofRespCodeWithI18nMsg(SystemMessage.COMMON_LOGIN_FAILED);
}
//调用具体用户认证服务实现
return uniLoginUserDetailsService.loadUserByAuthParams(loginParams);
}
@Override
public void authenticateUser(Map<String, String> loginParams, UniLoginUserDetails uniLoginUserDetails) throws AuthenticationException {
//获取参数中的身份类型
String identityType = loginParams.get(IDENTITY_TYPE_PARAMETER);
//具体用户认证服务实现
UniLoginUserDetailsService uniLoginUserDetailsService = null;
//处理系统用户账密码登录
if (IdentityTypeEnum.SYSTEM.equalCode(identityType)) {
uniLoginUserDetailsService = new SystemUserDetailsServiceImpl();
}
//处理LDAP登录
else if (IdentityTypeEnum.LDAP.equalCode(identityType)) {
uniLoginUserDetailsService = new LdapUserDetailsServiceImpl();
}
//调用具体用户认证服务实现
uniLoginUserDetailsService.authenticateUser(loginParams, uniLoginUserDetails);
}
}
改进:
- 通过类拆分职责(策略模式),提高可维护性
问题:
- 重复创建认证实现类,增加内存消耗
五、重构3 - 依赖注入(避免重复创建对象)
public class RbacUserDetailsDispatchServiceImplTemp implements UniLoginUserDetailsService {
/**
* 身份类型参数名称identityType
*/
private final String IDENTITY_TYPE_PARAMETER = ColumnUtils.getFieldName(UserAuth::getIdentityType);
@Resource
private SystemUserDetailsServiceImpl systemUserDetailsServiceImpl;
@Resource
private LdapUserDetailsServiceImpl ldapUserDetailsServiceImpl;
@Override
public UniLoginUserDetails loadUserByAuthParams(Map<String, String> loginParams) throws UsernameNotFoundException {
//获取参数中的身份类型
String identityType = loginParams.get(IDENTITY_TYPE_PARAMETER);
//处理系统用户账密码登录
if (IdentityTypeEnum.SYSTEM.equalCode(identityType)) {
return this.systemUserDetailsServiceImpl.loadUserByAuthParams(loginParams);
}
//处理LDAP登录
else if (IdentityTypeEnum.LDAP.equalCode(identityType)) {
return this.ldapUserDetailsServiceImpl.loadUserByAuthParams(loginParams);
}
//身份类型不存在
else {
throw I18nOAuth2AuthenticationException.ofRespCodeWithI18nMsg(SystemMessage.COMMON_LOGIN_FAILED);
}
}
@Override
public void authenticateUser(Map<String, String> loginParams, UniLoginUserDetails uniLoginUserDetails) throws AuthenticationException {
//获取参数中的身份类型
String identityType = loginParams.get(IDENTITY_TYPE_PARAMETER);
//处理系统用户账密码登录
if (IdentityTypeEnum.SYSTEM.equalCode(identityType)) {
this.systemUserDetailsServiceImpl.authenticateUser(loginParams, uniLoginUserDetails);
}
//处理LDAP登录
else if (IdentityTypeEnum.LDAP.equalCode(identityType)) {
this.ldapUserDetailsServiceImpl.authenticateUserInner(loginParams, uniLoginUserDetails);
}
}
}
改进:
- 通过依赖注入避免重复创建对象,节省内存消耗
问题:
- 大量的if…else逻辑
六、重构4 - 使用Map替代if…else、享元模式
public class RbacUserDetailsDispatchServiceImplTemp implements UniLoginUserDetailsService {
/**
* 身份类型参数名称identityType
*/
private final String IDENTITY_TYPE_PARAMETER = ColumnUtils.getFieldName(UserAuth::getIdentityType);
@Resource
private SystemUserDetailsServiceImpl systemUserDetailsServiceImpl;
@Resource
private LdapUserDetailsServiceImpl ldapUserDetailsServiceImpl;
/**
* Map(身份类型 -> 对应类型的登录认证实现)
*/
private Map<String, UniLoginUserDetailsService> authType2ServiceImplMap = new HashMap<>(4);
//核心流程不用修改,若扩展新认证方式仅需修改此处init方法
@PostConstruct
private void init() {
//初始Map(身份类型 -> 对应类型的登录认证实现)
this.authType2ServiceImplMap.put(IdentityTypeEnum.SYSTEM.getCode(), this.systemUserDetailsServiceImpl);
this.authType2ServiceImplMap.put(IdentityTypeEnum.LDAP.getCode(), this.ldapUserDetailsServiceImpl);
}
@Override
public UniLoginUserDetails loadUserByAuthParams(Map<String, String> loginParams) throws UsernameNotFoundException {
//获取参数中的身份类型
String identityType = loginParams.get(IDENTITY_TYPE_PARAMETER);
//验证身份类型是否存在
if (!StringUtils.hasText(identityType) || !this.authType2ServiceImplMap.containsKey(identityType)) {
throw I18nOAuth2AuthenticationException.ofRespCodeWithI18nMsg(SystemMessage.COMMON_LOGIN_FAILED);
}
//调用对应身份类型的用户认证实现 - 根据参数加载用户信息
return this.authType2ServiceImplMap.get(identityType).loadUserByAuthParams(loginParams);
}
@Override
public void authenticateUser(Map<String, String> loginParams, UniLoginUserDetails uniLoginUserDetails) throws AuthenticationException {
//获取参数中的身份类型
String identityType = loginParams.get(IDENTITY_TYPE_PARAMETER);
//调用对应身份类型的用户认证实现 - 认证用户信息
this.authType2ServiceImplMap.get(identityType).authenticateUser(loginParams, uniLoginUserDetails);
}
}
改进:
- 使用Map替代if…else分支判断逻辑,代码更整洁(享元模式)
- 扩展新的认证方式仅需修改init方法即可,核心流程处理代码无需调整
问题:
- 扩展新的认证方仍需修改init方法
- 扩展新的认证方仍需添加新实现类的依赖注入
- 违背开闭原则
七、重构5 - 满足开闭原则、迪米特原则
public class RbacUserDetailsDispatchServiceImpl implements UniLoginUserDetailsService {
/**
* 身份类型参数名称identityType
*/
private final String IDENTITY_TYPE_PARAMETER = ColumnUtils.getFieldName(UserAuth::getIdentityType);
/**
* 不同身份类型的登录认证实现列表(即会自动注入BaseUserDetailService的多个实现,根据BaseUserDetailService.getIdentityType()区分身份类型)
*/
@Resource
private List<BaseUserDetailService> rbacUserDetailsServices;
/**
* Map(身份类型 -> 对应类型的登录认证实现)
*/
private Map<String, UniLoginUserDetailsService> authType2ServiceImplMap = new HashMap<>(4);
@PostConstruct
private void init() {
//初始Map(身份类型 -> 对应类型的登录认证实现)
this.rbacUserDetailsServices.stream().forEach(rbacUserDetailsService -> {
this.authType2ServiceImplMap.put(rbacUserDetailsService.getIdentityType(), rbacUserDetailsService);
});
}
@Override
public UniLoginUserDetails loadUserByAuthParams(Map<String, String> loginParams) throws UsernameNotFoundException {
//获取参数中的身份类型
String identityType = loginParams.get(IDENTITY_TYPE_PARAMETER);
//验证身份类型是否存在
if (!StringUtils.hasText(identityType) || !this.authType2ServiceImplMap.containsKey(identityType)) {
throw I18nOAuth2AuthenticationException.ofRespCodeWithI18nMsg(SystemMessage.COMMON_LOGIN_FAILED);
}
//调用对应身份类型的用户认证实现 - 根据参数加载用户信息
return this.authType2ServiceImplMap.get(identityType).loadUserByAuthParams(loginParams);
}
@Override
public void authenticateUser(Map<String, String> loginParams, UniLoginUserDetails uniLoginUserDetails) throws AuthenticationException {
//获取参数中的身份类型
String identityType = loginParams.get(IDENTITY_TYPE_PARAMETER);
//调用对应身份类型的用户认证实现 - 认证用户信息
this.authType2ServiceImplMap.get(identityType).authenticateUser(loginParams, uniLoginUserDetails);
}
}
改进:
- 使用Spring支持集合注入的特性,避免单独依赖每个实现类
- 将认证类型移到具体认证实现类中确定,无需由上层预先知道(认证类型为String,避免使用枚举导致后续未知认证类型无法扩展)、迪米特原则(最小知道原则)
- 扩展新的认证方式,仅需添加一个新的基于BaseUserDetailService的实现类,无需修改原RbacUserDetailsDispatchServiceImpl中的任何代码,满足开闭原则
7.1 扩展 - 责任链变种
public class RbacUserDetailsDispatchServiceImplTemp implements UniLoginUserDetailsService {
/**
* 身份类型参数名称identityType
*/
private final String IDENTITY_TYPE_PARAMETER = ColumnUtils.getFieldName(UserAuth::getIdentityType);
@Resource
private List<BaseUserDetailService> rbacUserDetailsServices;
@Override
public UniLoginUserDetails loadUserByAuthParams(Map<String, String> loginParams) throws UsernameNotFoundException {
//获取参数中的身份类型
String identityType = loginParams.get(IDENTITY_TYPE_PARAMETER);
//调用对应身份类型的用户认证实现 - 根据参数加载用户信息
return this.rbacUserDetailsServices.stream()
.filter(rbacUserDetailsService -> rbacUserDetailsService.getIdentityType().equals(identityType))
.map(rbacUserDetailsService -> rbacUserDetailsService.loadUserByAuthParams(loginParams))
.findAny()
.orElseThrow(() -> {
//身份类型不存在
throw I18nOAuth2AuthenticationException.ofRespCodeWithI18nMsg(SystemMessage.COMMON_LOGIN_FAILED);
});
}
@Override
public void authenticateUser(Map<String, String> loginParams, UniLoginUserDetails uniLoginUserDetails) throws AuthenticationException {
//获取参数中的身份类型
String identityType = loginParams.get(IDENTITY_TYPE_PARAMETER);
//调用对应身份类型的用户认证实现 - 认证用户信息
this.rbacUserDetailsServices.stream()
.filter(rbacUserDetailsService -> rbacUserDetailsService.getIdentityType().equals(identityType))
.findAny()
.ifPresent(rbacUserDetailsService -> rbacUserDetailsService.authenticateUser(loginParams, uniLoginUserDetails));
}
}
八、重构6 - DRY、模板方法
随着业务的发展,用户认证需支持:
- 用户连续登录失败则锁定用户
- 锁定用户不允许继续登录
- 用户登录成功后清空登录失败计数
- 记录认证成功、失败审计事件
且这些业务规则适用于所有的认证类型,为了避免在每个认证实现类中都添加上述逻辑,则使用了模板方法模式。
public abstract class BaseUserDetailService implements UniLoginUserDetailsService {
//......
@Override
public UniLoginUserDetails loadUserByAuthParams(Map<String, String> authParams) throws UsernameNotFoundException {
//登录认证参数非空验证
this.validateAuthFormsNotEmpty(authParams);
//用户ID = userName:idType
String userWithTypeId = this.buildUserLockId(authParams);
//验证用户锁定状态,若用户已锁定则拒绝登录
if (this.loginLockService.isUserLocked(userWithTypeId)) {
//拒绝登录并给出提示信息
throw this.buildUserLockedException(userWithTypeId, false);
}
//调用内部实现加载用户逻辑
return this.loadUserByAuthParamsInner(authParams);
}
@Override
public void authenticateUser(Map<String, String> authParams, UniLoginUserDetails uniLoginUserDetails) throws AuthenticationException {
//用户ID = userName:idType
String userWithTypeId = this.buildUserLockId(authParams);
try {
//验证验证码是否匹配
this.validateCaptchaParams(authParams);
//调用内部实现认证用户
this.authenticateUserInner(authParams, uniLoginUserDetails);
//登录成功则清除连续登录失败计数
this.loginLockService.clearUserLoginErrorCount(userWithTypeId);
//审计: 登录成功
this.recordAuthenticateSuccessAudit(uniLoginUserDetails);
} catch (AuthenticationException authEx) {
//审计: 登录失败
this.recordAuthenticateExceptionAudit(authParams, uniLoginUserDetails, authEx);
//记录连续登录失败次数,若触发用户锁定则锁定用户、拒绝登录
if (this.loginLockService.incrUserLoginErrorCountAndReturnLockable(userWithTypeId)) {
//锁定用户
this.loginLockService.lockUserAndClearUserLoginErrorCount(userWithTypeId);
//拒绝登录并给出提示信息
throw this.buildUserLockedException(userWithTypeId, true);
}
//抛出原异常(兼容原错误提示)
throw authEx;
}
}
//其他方法实现,省略......
/**
* 获取身份类型(具体用户服务支持的身份类型)
*
* @return 身份类型
* @see IdentityTypeEnum
*/
public abstract String getIdentityType();
/**
* 根据认证form参数查询用户信息 - 内部实现
*
* @param authParams form参数
* @return 用户信息
* @throws UsernameNotFoundException
*/
public abstract UniLoginUserDetails loadUserByAuthParamsInner(Map<String, String> authParams) throws UsernameNotFoundException;
/**
* 认证用户(即form参数和用户信息中的凭证是否匹配等) - 内部实现
*
* @param authParams form参数
* @param uniLoginUserDetails 用户信息
* @throws AuthenticationException
*/
public abstract void authenticateUserInner(Map<String, String> authParams, UniLoginUserDetails uniLoginUserDetails) throws AuthenticationException;
}