如何使用Spring Security实现自定义登录
那么怎么自定义登陆,我这里先实现一个手机号短信的登录方式,其他的登录方式也是大同小异的
定义登录信息
首先要创建一个自定义的Authentication,用于保存用户信息,Security提供了一个Authentication的子类AbstractAuthenticationToken,我们继承这个类
public class PhoneSmsAuthenticationToken extends AbstractAuthenticationToken {
//认证主体,这里用来存手机号
private final Object principal;
//凭证,一般用来存密码,我这里用来存验证码
private Object credentials;
//租户Id
private Integer tenantId;
//初始化的构造方法
public PhoneSmsAuthenticationToken(Object principal, Object credentials ,Integer tenantId ) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.tenantId = tenantId;
this.setAuthenticated(false);
}
//认证完成后调用的构造方法
public PhoneSmsAuthenticationToken(Object principal, Object credentials, Integer tenantId ,Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
this.tenantId = tenantId;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
...
}
主要实现了这几个方法,这里保存手机号和验证码的字段principal
和credentials
是复用了Spring Security的字段,自定义了tenantId
字段,用于区分不同租户的用户
注意这里实现了两个构造方法,区别是super.setAuthenticated
,一个是用户初始化的,一个是用户认证完成后重新填充用的
定义核心处理逻辑
在上一篇文章提到过,Authentication
和AuthenticationProvider
是成对存在的,每个特定的Authentication由特定的AuthenticationProvider处理
主要是实现AuthenticationProvider
接口的两个方法,一个是声明处理的Authentication,一个是处理的核心逻辑
public class PhoneSmsAuthenticationProvider implements AuthenticationProvider {
//校验用户的业务service
private CustomUserDetailsService userDetailsService;
@Override
//具体认证逻辑
public Authentication authenticate(Authentication authentication) {
PhoneSmsAuthenticationToken authenticationToken = (PhoneSmsAuthenticationToken) authentication;
String phone = (String) authenticationToken.getPrincipal();
String code = (String) authenticationToken.getCredentials();
Integer tenantId = authenticationToken.getTenantId();
//业务类认证
CustomLoginUser user = getUserDetailsService().loginByPhoneSms(phone ,code,tenantId);
if (user == null) {
throw new InternalAuthenticationServiceException("手机号错误");
}
//认证完成填充Authentication,这里将user赋值给principal,复用了这个字段
PhoneSmsAuthenticationToken authenticationResult = new PhoneSmsAuthenticationToken(user, null ,tenantId , user.getAuthorities());
//details可以保存额外的信息,是父类的字段,这里保存的是租户信息
authenticationResult.setDetails(user.getTenantInfo());
return authenticationResult;
}
@Override
//指定具体的Authentication
//根据你指定的Authentication来找到具体的Provider
public boolean supports(Class<?> authentication) {
return NamePassAuthenticationToken.class.isAssignableFrom(authentication);
}
public void setUserDetailsService(CustomUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
protected UserDetailsService getUserDetailsService() {
return userDetailsService;
}
}
处理方法的逻辑也比较简单,判断当前的用户信息能否完成校验,能的话就创建并填充到新的Authentication中
将配置注册到容器中
前边创建了Authentication和AuthenticationProvider,接下来就是将这些配置保存到Spring Security中
@Component
public class PhoneSmsAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
//处理业务类
@Autowired
private CustomUserDetailsService customUserDetailsService;
//解码器,这里的密码是验证码,不需要加密
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void configure(HttpSecurity http) {
PhoneSmsAuthenticationProvider provider = new PhoneSmsAuthenticationProvider();
//设置到provider中
provider.setUserDetailsService(customUserDetailsService);
// provider.setPasswordEncoder(passwordEncoder);
//注册配置
http.authenticationProvider(provider);
}
}
然后再将这个配置注册到Spring中,通常都会有一个实现了WebSecurityConfigurerAdapter的配置类
@Autowired
private PhoneSmsAuthenticationProvider phoneSmsAuthenticationProvider;
protected void configure(HttpSecurity http) throws Exception {
http.apply(phoneSmsAuthenticationProvider);
}
定义业务逻辑
Spring Security提供了UserDetailsService
接口,我们实现这个接口,然后定义一个手机号登录的方法,返回UserDetails
public UserDetails loginByPhoneSms(String phone,String code,Integer tenantId) throws UsernameNotFoundException {
//通过接口查询用户
CustomUser user = accountService.loginByPhoneSms(phone,code,tenantId);
.....
return new CustomLoginUser(phone ,AuthorityUtils.NO_AUTHORITIES , user.getTenantInfo());
}
因为我还需要保存租户信息,用Spring Security提供的org.springframework.security.core.userdetails.User
类提供的字段满足不了,所以我自己实现了UserDetails
public class CustomLoginUser implements UserDetails {
....
private TenantInfo tentantInfo;
...
public CustomUser( String phone, Set<GrantedAuthority> authorities, TentantInfo tentantInfo) {
this.phone = phone;
//权限信息,我这里没有用到,传的AuthorityUtils.NO_AUTHORITIES空集合
this.authorities = authorities;
this.tentantInfo = tentantInfo;
}
...
}
定义登录接口
//交给Spring Security验证
@Autowired
private AuthenticationManager authenticationManager;
public Result loginByPhoneSms( String phone,String code,Integer tenantId) throws Exception {
PhoneSmsAuthenticationToken token = PhoneSmsAuthenticationToken(phone, code , tenantId);
try {
//获取认证完成的Authentication
PhoneSmsAuthenticationToken authentication = (PhoneSmsAuthenticationToken)authenticationManager.authenticate(token);
//保存到上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
CustomLoginUser user = (User) authentication.getPrincipal();
return new Result().success(user ,authentication.getDetails());
} catch (AuthenticationException ex) {
// 处理身份验证异常
return new Result().fail("认证失败: " + ex.getMessage());
}
}
几个类的流转关系
自此,已经实现了用手机号验证码的登录方式
那么再来回顾一下,整个链路是怎么流转的,从触发登录请求开始
-
调用登录请求,前端将信息传递进来
-
封装好信息,向Spring Security传递
PhoneSmsAuthenticationToken
-
Spring Security根据
PhoneSmsAuthenticationToken
找到PhoneSmsAuthenticationProvider
-
PhoneSmsAuthenticationProvider
交给CustomUserDetailsService
处理 -
CustomUserDetailsService
验证完毕,返回填充好的CustomLoginUser
-
PhoneSmsAuthenticationProvider
根据CustomLoginUser
,填充并创建一个新的PhoneSmsAuthenticationToken
-
Spring Security将
PhoneSmsAuthenticationToken
保存到上下文 -
登录接口返回
转载自:https://juejin.cn/post/7339526709421244435