@Slf4j
@Service
@RequiredArgsConstructor
public class GlobalUserDetailsService implements UserDetailsService {
private final UserService userService;
@Override
public UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {
try {
return Optional.ofNullable(userService.getByPhoneIncludeDelete(phone))
.orElseGet(() -> userService.newUser(phone));
} catch (Exception e) {
log.error("加载用户失败={}", e.getMessage(), e);
throw new UsernameNotFoundException("手机号异常", e);
}
}
}
@Component
public class GlobalUserDetailsChecker implements UserDetailsChecker {
@Override
public void check(UserDetails user) {
Assert.isTrue(user.isAccountNonLocked(), () -> new LockedException("账户已锁定"));
Assert.isTrue(user.isEnabled(), () -> new DisabledException("账户已禁用"));
Assert.isTrue(user.isAccountNonExpired(), () -> new AccountExpiredException("账户已过期"));
Assert.isTrue(user.isCredentialsNonExpired(), () -> new CredentialsExpiredException("账户认证已过期"));
}
}
以上只是一个很简略的认证实现
具体实现
如我在[项目结构] 中展示的
我分别基于以上抽象,
实现了, 手机号/短信验证码 鉴权认证逻辑
@Component
public class PhoneSmsAuthenticationFilter extends GlobalAuthenticationFilter {
public static final String SPRING_SECURITY_FORM_PHONE_KEY = "phone";
public static final String SPRING_SECURITY_FORM_SMS_CODE_KEY = "smsCode";
public static final String SPRING_SECURITY_FROM_URI_PATTEN = "/**/user/login/phone";
public static final HttpMethod SPRING_SECURITY_FROM_METHOD = HttpMethod.GET;
public PhoneSmsAuthenticationFilter() {
super(SPRING_SECURITY_FROM_URI_PATTEN, SPRING_SECURITY_FROM_METHOD);
}
@Override
public GlobalAuthenticationToken combinationAuthentication(HttpServletRequest request) {
String phone = StrUtil.nullToEmpty(
StrUtil.cleanBlank(request.getParameter(SPRING_SECURITY_FORM_PHONE_KEY)));
String smsCode = StrUtil.nullToEmpty(
StrUtil.cleanBlank(request.getParameter(SPRING_SECURITY_FORM_SMS_CODE_KEY)));
GlobalWebAuthenticationDetails details = (GlobalWebAuthenticationDetails) authenticationDetailsSource.buildDetails(
request);
details
.setClientType(ClientType.WeChatMiniProgram)
.setLoginType(LoginType.PhoneSms);
return new PhoneSmsAuthenticationToken(phone, smsCode, details);
}
}
@Getter
public class PhoneSmsAuthenticationToken extends GlobalAuthenticationToken {
private final String phone;
private final String smsCode;
public PhoneSmsAuthenticationToken(String phone, String smsCode, Object details) {
super(phone, smsCode, details);
this.phone = phone;
this.smsCode = smsCode;
}
}
@Component
@RequiredArgsConstructor
public class PhoneSmsAuthenticationProvider extends GlobalAuthenticationProvider<PhoneSmsAuthenticationToken> {
private final SmsService smsService;
@Override
public String validate4Username(PhoneSmsAuthenticationToken authentication) {
String phone = authentication.getPhone();
String smsCode = authentication.getSmsCode();
String ip = ((GlobalWebAuthenticationDetails) authentication.getDetails()).getIp();
smsService.verifySmsCode(phone, ip, smsCode);
return phone;
}
}
实现了, 微信小程序手机号快速验证 鉴权认证逻辑
@Component
public class WeChatMiniProgramAuthenticationFilter extends GlobalAuthenticationFilter {
public static final String SPRING_SECURITY_FORM_APP_ID_KEY = "appId";
public static final String SPRING_SECURITY_FORM_PHONE_CODE_KEY = "phoneCode";
public static final String SPRING_SECURITY_FROM_URI_PATTEN = "/user/login/wechat/miniapp";
public static final HttpMethod SPRING_SECURITY_FROM_METHOD = HttpMethod.POST;
public WeChatMiniProgramAuthenticationFilter() {
super(SPRING_SECURITY_FROM_URI_PATTEN, SPRING_SECURITY_FROM_METHOD);
}
@Override
public GlobalAuthenticationToken combinationAuthentication(HttpServletRequest request) throws IOException {
JSONObject paramJson = JSONUtil.parseObj(IoUtil.read(request.getInputStream(), StandardCharsets.UTF_8));
String appId = paramJson.getStr(SPRING_SECURITY_FORM_APP_ID_KEY);
String phoneCode = paramJson.getStr(SPRING_SECURITY_FORM_PHONE_CODE_KEY);
GlobalWebAuthenticationDetails details = (GlobalWebAuthenticationDetails) authenticationDetailsSource.buildDetails(
request);
details
.setClientType(ClientType.WeChatMiniProgram)
.setLoginType(LoginType.WeChatMiniProgram);
return new WeChatMiniProgramAuthenticationToken(appId, phoneCode, details);
}
}
@Getter
public class WeChatMiniProgramAuthenticationToken extends GlobalAuthenticationToken {
private final String appId;
private final String phoneCode;
public WeChatMiniProgramAuthenticationToken(String appId, String phoneCode, Object details) {
super(appId, phoneCode, details);
this.appId = appId;
this.phoneCode = phoneCode;
}
}
@Component
@RequiredArgsConstructor
public class WeChatMiniProgramAuthenticationProvider extends
GlobalAuthenticationProvider<WeChatMiniProgramAuthenticationToken> {
private final WxMiniAppService wxMiniAppService;
@Override
public String validate4Username(WeChatMiniProgramAuthenticationToken authentication) {
if (ApplicationTools.isNotProd()) {
throw new BadCredentialsException("当前环境不支持该登录方式!");
}
String appId = authentication.getAppId();
String phoneCode = authentication.getPhoneCode();
if (!wxMiniAppService.switchover(appId)) {
throw new BadCredentialsException(StrUtil.format("未找到对应微信小城 AppId=[{}]配置,请核实后重试", appId));
}
WxMaPhoneNumberInfo phoneNoInfo;
try {
phoneNoInfo = wxMiniAppService
.getUserService()
.getPhoneNoInfo(phoneCode);
} catch (WxErrorException e) {
throw new BadCredentialsException(e
.getError()
.getErrorMsg());
}
return phoneNoInfo.getPurePhoneNumber();
}
}
前言
正文
项目结构
思路讲解
阅读源码
UsernamePasswordAuthenticationFilter
这是 Spring Security 中非常关键的一个基类, 等同于 Spring 框架原生态的给了你一个实现模板, 以常见的 username/password 的形式, 实现了基本的鉴权入口
这行代码决定了这个 Filter 只用来处理 Http 接口路径 为 /login 的请求
解析参数
构建 Authentication
构建 Detail
鉴权验证
寻找思路
于是乎, 就有了以下设计方案
设计思路
其中有涉及到两个重点实现
比如我的默认实现
具体实现
实现了, 手机号/短信验证码 鉴权认证逻辑
实现了, 微信小程序手机号快速验证 鉴权认证逻辑
实现了, 通过 refreshToken 刷新 Token 鉴权认证逻辑
网络检索
类似直接写一个 Controller, 以常规化的 controller->service->dao(mapper)的方式, 比比皆是
当然也同样检索到类似我上述实现方式的文章, 只是大同小异
疑惑
以上是 Security 的正确打开方式? 还有其他实现思路和方案吗?
我自己的实现方式, 始终给我一种不够优雅, 不够简洁, 甚至于不方便定位的感觉
所以我想请教佬们关于这一点的看法