likes
comments
collection
share

如何使用Spring Security实现自定义登录

作者站长头像
站长
· 阅读数 3

那么怎么自定义登陆,我这里先实现一个手机号短信的登录方式,其他的登录方式也是大同小异的

定义登录信息

首先要创建一个自定义的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;
    }
    ...

}

主要实现了这几个方法,这里保存手机号和验证码的字段principalcredentials是复用了Spring Security的字段,自定义了tenantId字段,用于区分不同租户的用户

注意这里实现了两个构造方法,区别是super.setAuthenticated,一个是用户初始化的,一个是用户认证完成后重新填充用的

定义核心处理逻辑

在上一篇文章提到过,AuthenticationAuthenticationProvider是成对存在的,每个特定的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实现自定义登录

那么再来回顾一下,整个链路是怎么流转的,从触发登录请求开始

  1. 调用登录请求,前端将信息传递进来

  2. 封装好信息,向Spring Security传递PhoneSmsAuthenticationToken

  3. Spring Security根据PhoneSmsAuthenticationToken找到PhoneSmsAuthenticationProvider

  4. PhoneSmsAuthenticationProvider交给CustomUserDetailsService处理

  5. CustomUserDetailsService验证完毕,返回填充好的CustomLoginUser

  6. PhoneSmsAuthenticationProvider根据CustomLoginUser,填充并创建一个新的PhoneSmsAuthenticationToken

  7. Spring Security将PhoneSmsAuthenticationToken保存到上下文

  8. 登录接口返回

转载自:https://juejin.cn/post/7339526709421244435
评论
请登录