Spring Security专栏(Security认证对象,如何认证的)

748 阅读6分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

写在前面

上文我们讲到用户认证体系当中的用户体系,今天我们来聊聊认证体系,本文将带你了解,Security是如何认证的!

多说一句,欢迎大家点击我头像,去查看Security专栏,我们一起学习!!!,设计模式专题已经完结,接下来主要完成Security专栏和并发队列专栏。

认证对象

有了上文聊的用户对象,我们就可以讨论具体的认证过程了,首先来看认证对象 Authentication,如下所示:

public interface Authentication extends Principal, Serializable {
    //安全主体具有的权限
    Collection<? extends GrantedAuthority> getAuthorities();
 
	//证明主体有效性的凭证
    Object getCredentials();
 
    //认证请求的明细信息
    Object getDetails();
 
    //主体的标识信息
    Object getPrincipal();
 
    //认证是否通过
    boolean isAuthenticated();
 
    //设置认证结果
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

认证对象代表认证请求本身,并保存该请求访问应用程序过程中涉及的各个实体的详细信息。

在安全领域,请求访问该应用程序的用户通常被称为主体(Principal),在 JDK 中存在一个同名的接口,而 Authentication 扩展了这个接口。

很显然,Authentication 只代表了认证请求本身,而具体执行认证的过程和逻辑需要由专门的组件来负责,这个组件就是 AuthenticationProvider,定义如下:

public interface AuthenticationProvider {
 
    //执行认证,返回认证结果
    Authentication authenticate(Authentication authentication)
             throws AuthenticationException;
 
    //判断是否支持当前的认证对象
    boolean supports(Class<?> authentication);
}

说到这里,你可能会认为 Spring Security 是直接使用 AuthenticationProvider 接口完成用户认证的,其实不然。如果你翻阅 Spring Security 的源码,会发现它使用了 AuthenticationManager 接口来代理 AuthenticationProvider 提供的认证功能

我们以 InMemoryUserDetailsManager 中的 changePassword 为例,分析用户认证的执行过程

public void changePassword(String oldPassword, String newPassword) {
        Authentication currentUser = SecurityContextHolder.getContext()
                 .getAuthentication();
 
        if (currentUser == null) {
             throw new AccessDeniedException(
                     "Can't change password as no Authentication object found in context "
                             + "for current user.");
        }
 
        String username = currentUser.getName();
 
        if (authenticationManager != null) {
 
             authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
                     username, oldPassword));
        }
        else {
             …
        }
 
        MutableUserDetails user = users.get(username);
 
        if (user == null) {
             throw new IllegalStateException("Current user doesn't exist in database.");
        }
 
        user.setPassword(newPassword);
}

为了展示 代码做了裁剪。

从上面可以看到这里使用了 AuthenticationManager 而不是 AuthenticationProvider 中的 authenticate() 方法来执行认证。同时,我们也注意到这里出现了 UsernamePasswordAuthenticationToken 类,这就是 Authentication 接口的一个具体实现类,用来存储用户认证所需的用户名和密码信息

看到这里的小伙伴心里应该大概有所了解了吧。那么我们如何定制化用户认证方案呢,别慌,往下看

自定义认证方案

通过前面的分析,我们明确了用户信息存储的实现过程实际上是可以定制化的。Security 所做的工作只是把常见的、符合一般业务场景的实现方式嵌入到了框架中。如果有特殊的场景,开发人员完全可以实现自定义的用户信息存储方案。

现在,我们已经知道 UserDetails 接口代表着用户详细信息,而负责对 UserDetails 进行各种操作的则是 UserDetailsService 接口。因此,实现定制化用户认证方案主要就是实现 UserDetails 和 UserDetailsService 这两个接口

扩展 UserDetails 的方法就是直接实现该接口,例如我们可以构建如下所示的 SpringUser 类:

public class SpringUser implements UserDetails {
 
    private static final long serialVersionUID = 1L;
    private Long id;  
    private final String username;
    private final String password;
    private final String phoneNumber;
    //省略 getter/setter
  
    @Override
    public String getUsername() {
        return username;
    }
    
    @Override
    public String getPassword() {
        return password;
    }
 
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
         return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
    }
    
  @Override
  public boolean isAccountNonExpired() {
      return true;
  }
 
  @Override
  public boolean isAccountNonLocked() {
      return true;
  }
 
  @Override
  public boolean isCredentialsNonExpired() {
      return true;
  }
 
  @Override
  public boolean isEnabled() {
      return true;
  }
}

显然,这里使用了一种最简单的方法来满足 UserDetails 中各个接口的实现需求。一旦我们构建了这样一个 SpringUser 类,就可以创建对应的表结构存储类中定义的字段

我们还可以扩展 UserDetailsService


@Service
public class SpringUserDetailsService 
        implements UserDetailsService {
	 
  @Autowired
  private SpringUserRepository repository;
 
  @Override
  public UserDetails loadUserByUsername(String username)
      throws UsernameNotFoundException {
 
    SpringUser user = repository.findByUsername(username);
    if (user != null) {
      return user;
    }
    throw new UsernameNotFoundException(
                    "SpringUser '" + username + "' not found");
  }
}

我们知道 UserDetailsService 接口只有一个 loadUserByUsername 方法需要实现。因此,我们基于 SpringUserRepository 的 findByUsername 方法,根据用户名从数据库中查询数据。

我们还可以扩展 AuthenticationProvider

扩展 AuthenticationProvider 的过程就是提供一个自定义的 AuthenticationProvider 实现类。这里我们以最常见的用户名密码认证为例,梳理自定义认证过程所需要实现的步骤,如下所示

image.png

画的不太好,但是很简洁,应该能帮助到大家理解记忆。

首先我们需要通过 UserDetailsService 获取一个 UserDetails 对象,然后根据该对象中的密码与认证请求中的密码进行匹配,如果一致则认证成功,反之抛出一个 BadCredentialsException 异常。示例代码如下所示:

@Component
public class SpringAuthenticationProvider implements AuthenticationProvider {
 
    @Autowired
    private UserDetailsService userDetailsService;
 
    @Autowired
    private PasswordEncoder passwordEncoder;
 
    @Override
    public Authentication authenticate(Authentication authentication) {
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();
 
        UserDetails user = userDetailsService.loadUserByUsername(username);
        if (passwordEncoder.matches(password, user.getPassword())) {
            return new UsernamePasswordAuthenticationToken(username, password, u.getAuthorities());
        } else {
            throw new BadCredentialsException("The username or password is wrong!");
        }
    }
 
    @Override
    public boolean supports(Class<?> authenticationType) {
        return authenticationType.equals(UsernamePasswordAuthenticationToken.class);
    }
}

这里我们同样使用了 UsernamePasswordAuthenticationToken 来传递用户名和密码,并使用一个 PasswordEncoder 对象校验密码。

最后 我们整合一下这套定制化配置

整合定制化配置

我们创建一个 SpringSecurityConfig 类,该类继承了 WebSecurityConfigurerAdapter 配置类。这次,我们将使用自定义的 SpringUserDetailsService 来完成用户信息的存储和查询,需要对原有配置策略做一些调整。调整之后的完整 SpringSecurityConfig 类如下所示:

@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Autowired
    private UserDetailsService springUserDetailsService;
 
    @Autowired
    private AuthenticationProvider springAuthenticationProvider;
 
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
 
     auth.userDetailsService(springUserDetailsService)
	.authenticationProvider(springAuthenticationProvider);
	}
}

大家可能在自己项目中见到最多是上面这个类。

  • 这里我们注入了 SpringUserDetailsService 和 SpringAuthenticationProvider,并将其添加到 AuthenticationManagerBuilder 中

  • 这样 AuthenticationManagerBuilder 将基于这个自定义的 SpringUserDetailsService 完成 UserDetails 的创建和管理

  • 最后基于自定义的 SpringAuthenticationProvider 完成用户认证。

OK 今天关于认证的一些东西就聊到这里,基本上就这些。懂得这些就行了。我觉得也没有必要深入了。

总结

本文主要讲通过扩展 UserDetailsService 和 AuthenticationProvider 接口的方式来实现定制化的用户认证方案。希望大家下去可以敲下代码,再理解下,下一期我们接着聊。 加油,一起学习!

弦外之音

感谢你的阅读,如果你感觉学到了东西,麻烦您点赞,关注。也欢迎有问题我们下面评论交流

加油! 我们下期再见!

给大家分享几个我前面写的几篇骚操作

聊聊不一样的策略模式(值得收藏)

copy对象,这个操作有点骚!