Spring Security: 认证架构(流程)

developerworks

clipboard.png

接收HTTP请求

Spring security 定义了一个过滤器链, 当认证请求到达这个链时, 该请求将会穿过这个链条用于认证和授权. 这个链上的可以定义1..N个过滤器, 过滤器的用途是获取请求中的认证信息, 根据认证方式进行路由, 把认证信息传递给对应的认证处理程序进行处理. 下面的示例图显示了Spring security中常用的认证过滤器.

clipboard.png

不同的过滤器处理不同的认证信息. 例如

  • HTTP Basic 认证请通过过滤器链, 到达 BasicAuthenticationFilter
  • HTTP Digest 认证被 DigestAuthenticationFilter 识别,拦截并处理.
  • 表单登录认证被 UsernamePasswordAuthenticationFilter 识别,拦截并处理.
  • X.509 认证被 X509AuthenticationFilter 识别,拦截并处理.

基于用户凭证创建 AuthenticationToken

下图显示了多种类型的 AuthenticationToken, 基于不同额认证方式, 过滤器会创建不同类型的 AuthenticationToken
clipboard.png

这里我们以最常用表单登录为例子, 用户在登录表单中输入用户名和密码, 并点击确定, 浏览器提交POST请求到服务器, 穿过过滤器链, 被 UsernamePasswordAuthenticationFilter 识别, UsernamePasswordAuthenticationFilter 提取请求中的用户名和密码来创建 UsernamePasswordAuthenticationToken 对象.

把组装好的 AuthenticationToken 传递给 AuthenticationManagager

组装好的 UsernamePasswordAuthenticationToken 对象被传递给 AuthenticationManagager authenticate 方法进行认证决策.

public interface AuthenticationManager
{
  Authentication authenticate(Authentication authentication)throws AuthenticationException;
}
AuthenticationManager 只是一个接口, 实际的实现是 ProviderManager

ProviderManager 有一个配置好的认证提供者列表(AuthenticationProvider), ProviderManager 会把收到的 UsernamePasswordAuthenticationToken 对象传递给列表中的每一个 AuthenticationProvider 进行认证.

进行认证处理

AuthenticationProvider 接口的定义如下:

public interface AuthenticationProvider {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
    
    boolean supports(Class<?> authentication);
}

框架提供了一部分现有的实现类:

  • CasAuthenticationProvider
  • JaasAuthenticationProvider
  • DaoAuthenticationProvider
  • OpenIDAuthenticationProvider
  • RememberMeAuthenticationProvider
  • LdapAuthenticationProvider
上面我们说了, ProviderManager 会把收到的 UsernamePasswordAuthenticationToken 对象传递给列表中的每一个 AuthenticationProvider 进行认证.
那到底 UsernamePasswordAuthenticationToken 会被哪一个接收和处理呢?
注意观察 AuthenticationProvider 接口的 supports 方法!

UserDetailsService

部分认证提供者会使用 UserDetailsService 去获取用户信息. UserDetailsService 获取的对象是一个 UserDetails. 框架中自带一个 User 实现, 但是一般我们需要对 UserDetails 进行定制, 内置的 User 太过简单实际项目无法满足需要.

下面是一个基于JPA的 UserDetailsService 实现:

package com.example.demowebfluxsecurity.authentication;

import com.example.demowebfluxsecurity.entity.Role;
import com.example.demowebfluxsecurity.entity.User;
import com.example.demowebfluxsecurity.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@Service
public class JpaReactiveUserDetailsService implements ReactiveUserDetailsService {
    private UserRepository userRepository;

    @Autowired
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    /**
     * @param s 用户名
     * @return Mono<UserDetails>
     */
    @Override
    public Mono<UserDetails> findByUsername(String s) {
        // 从用户Repository中获取一个User Jpa实体对象
        Optional<User> optionalUser = userRepository.findByUsername(s);
        if (!optionalUser.isPresent()) {
            return Mono.empty();
        }
        User user = optionalUser.get();
        
        // 填充权限
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (Role role : user.getRoles()) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        
        // 返回 UserDetails
        return Mono.just(new org.springframework.security.core.userdetails.User(
            user.getUsername(), user.getPassword(), authorities
        ));
    }
}

用户 DAO

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    User findByEmail(String email);

    @Override
    void delete(User user);

    Optional<User> findByUsername(String username);

}

认证结果处理

如果认证成功(用户名,密码完全正确), AuthenticationProvider 将会返回一个完全有效的 Authentication 对象(UsernamePasswordAuthenticationToken). 否则抛出 AuthenticationException 异常.

完全有效的 Authentication 对象定义如下:

  • authenticated属性为 true
  • 已授权的权限列表(GrantedAuthority列表)
  • 用户凭证(仅用户名)

如果抛出异常, 将会被对应的 AuthenticationEntryPoint 处理.

可参考 UsernamePasswordAuthenticationToken 类以及抽象父类 AbstractAuthenticationToken

认证完成

认证完成后, AuthenticationManager 将会返回该认证对象(UsernamePasswordAuthenticationToken)返回给过滤器

存储认证对象

SecurityContextHolder.getContext().setAuthentication(authentication);

相关的过滤器获得一个认证对象后, 把它存储在安全上下文中(SecurityContext) 用于后续的授权判断(比如查询,修改等操作).

授权由 Authorization Filters (授权过滤器) 进行处理.

认证的过程回顾

clipboard.png

阅读 4.6k

Erlang/Elixir/Java/Javascript实践
本专栏是一个主要研究Erlang/Elixir语言的专栏. 附带其他相关的, 和不相关的东西, 目的是记录自己的学习...
1.6k 声望
257 粉丝
0 条评论
你知道吗?

1.6k 声望
257 粉丝
宣传栏