如何在身份验证期间或之后设置自定义主体对象?

How to set a custom principal object during or after authentication?

我已经更改了用户在我的后端进行身份验证的方式。从现在开始,我将从 Firebase 接收 JWT 令牌,然后在我的 Spring 启动服务器上进行验证。

到目前为止一切正常,但有一个变化我不太满意,主体对象现在是 org.springframework.security.oauth2.jwt.Jwt 而不是 AppUserEntity,用户模型, 和以前一样。

// Note: "authentication" is a JwtAuthenticationToken

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Jwt jwt = (Jwt) authentication.getPrincipal();

所以,经过一些阅读和调试后,我发现 BearerTokenAuthenticationFilter 基本上像这样设置 Authentication 对象:

// BearerTokenAuthenticationFilter.java

AuthenticationManager authenticationManager = this.authenticationManagerResolver.resolve(request);

// Note: authenticationResult is our JwtAuthenticationToken
Authentication authenticationResult = authenticationManager.authenticate(authenticationRequest);  

SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authenticationResult);
SecurityContextHolder.setContext(context);

正如我们所见,另一方面,它来自 authenticationManager,它是 org.springframework.security.authentication.ProviderManager 等等。兔子洞变深了。

我没有找到任何可以让我以某种方式替换 Authentication 的东西。

那么计划是什么?

由于 Firebase 现在负责用户身份验证,因此可以在我的后端不知道的情况下创建用户。我不知道这是否是最好的方法,但我打算在发现尚不存在的用户的有效 JWT 令牌后,在我的数据库中简单地创建一个用户记录。

此外,我的很多业务逻辑目前都依赖于作为用户实体业务对象的主体。我可以更改此代码,但这是一项乏味的工作,谁不想回顾几行遗留代码?

This is working fine so far but there's one change which I am not too happy about and it's that the principal-object is now a org.springframework.security.oauth2.jwt.Jwt and not a AppUserEntity, the user-model, like before.

在我的应用程序中,我通过滚动我自己的 JwtAuthenticationFilter 而不是使用 BearerTokenAuthenticationFilter 来规避此问题,然后将我的 User 实体设置为 principal Authentication 对象。但是,在我的例子中,这几乎从 JWT 声明中构造了一个 User,这可能是一种不好的做法:SonarLint 提示使用 DTO 来减轻有人使用受损的 JWT 令牌将任意数据注入其用户记录的风险.我不知道这是否是一个大问题 - 如果您不信任您的 JWT,那么您还有其他问题,恕我直言。

I don't know if this is the best way to do it but I intend to simply create a user record in my database once I discover a valid JWT-token of a user which does not exist yet.

请记住,您的应用程序应以无状态方式验证 JWT,仅通过验证其签名。您不应该在每次验证它们时都访问数据库。因此,如果您使用像

这样的方法调用来创建用户记录会更好
void foo(@AuthenticationPrincipal final Jwt jwt) {
  // only invoke next line if reading JWT claims is not enough
  final User user = userService.findOrCreateByJwt(jwt);

  // TODO method logic
}

一旦您需要保留对涉及该用户的数据库的更改。

我做的和 Julian Echkard 有点不同。

在我的 WebSecurityConfigurerAdapter 中,我设置 Customizer 如下:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.oauth2ResourceServer()
            .jwt(new JwtResourceServerCustomizer(this.customAuthenticationProvider));
}

customAuthenticationProvider 是一个 JwtResourceServerCustomizer,我是这样实现的:

public class JwtResourceServerCustomizer implements Customizer<OAuth2ResourceServerConfigurer<HttpSecurity>.JwtConfigurer> {

    private final JwtAuthenticationProvider customAuthenticationProvider;

    public JwtResourceServerCustomizer(JwtAuthenticationProvider customAuthenticationProvider) {
        this.customAuthenticationProvider = customAuthenticationProvider;
    }

    @Override
    public void customize(OAuth2ResourceServerConfigurer<HttpSecurity>.JwtConfigurer jwtConfigurer) {
        String key = UUID.randomUUID().toString();
        AnonymousAuthenticationProvider anonymousAuthenticationProvider = new AnonymousAuthenticationProvider(key);
        ProviderManager providerManager = new ProviderManager(this.customAuthenticationProvider, anonymousAuthenticationProvider);
        jwtConfigurer.authenticationManager(providerManager);
    }
}

我正在这样配置 NimbusJwtDecoder

@Component
public class JwtConfig {

    @Bean
    public JwtDecoder jwtDecoder() {
        String jwkUri = "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com";
        return NimbusJwtDecoder.withJwkSetUri(jwkUri)
                .build();
    }

}

最后,我们需要一个自定义 AuthenticationProvider,它将 return 我们想要的 Authentication 对象:

@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {

    private final JwtDecoder jwtDecoder;

    @Autowired
    public JwtAuthenticationProvider(JwtDecoder jwtDecoder) {
        this.jwtDecoder = jwtDecoder;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        BearerTokenAuthenticationToken token = (BearerTokenAuthenticationToken) authentication;

        Jwt jwt;
        try {
            jwt = this.jwtDecoder.decode(token.getToken());
        } catch (JwtValidationException ex) {
            return null;
        }

        List<GrantedAuthority> authorities = new ArrayList<>();

        if (jwt.hasClaim("roles")) {
            List<String> rolesClaim = jwt.getClaim("roles");
            List<RoleEntity.RoleType> collect = rolesClaim
                    .stream()
                    .map(RoleEntity.RoleType::valueOf)
                    .collect(Collectors.toList());

            for (RoleEntity.RoleType role : collect) {
                authorities.add(new SimpleGrantedAuthority(role.toString()));
            }
        }

        return new JwtAuthenticationToken(jwt, authorities);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(BearerTokenAuthenticationToken.class);
    }

}

SecurityContextHolder.setContext(context);

不适用于

request.getUserPrincipal();

您可以创建自定义 class 扩展 HttpServletRequestWrapper

import java.security.Principal;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class UserPrincipalHttpServletRequest extends HttpServletRequestWrapper {

  private final Principal principal;

  public UserPrincipalHttpServletRequest(HttpServletRequest request, Principal principal) {
    super(request);
    this.principal = principal;
  }

  @Override
  public Principal getUserPrincipal() {
    return principal;
  }
}

然后在您的过滤器中执行如下操作:

protected void doFilterInternal(HttpServletRequest request){
    
    . . . 

    // create user details, roles are required
    Set<GrantedAuthority> authorities = new HashSet<>();
    authorities.add(new SimpleGrantedAuthority("SOME ROLE"));
    UserDetails userDetails = new User("SOME USERNAME", "SOME PASSWORD", authorities);

    // Create an authentication token
    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

    // follow the filter chain, using the new wrapped UserPrincipalHtppServletRequest
    chain.doFilter(new UserPrincipalHttpServletRequest(request, usernamePasswordAuthenticationToken), response);
    // all filters coming up, will be able to run request.getUserPrincipal()
}