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

x8goxv8g  于 2022-11-11  发布在  Spring
关注(0)|答案(4)|浏览(134)

我已经改变了用户在我的后端进行身份验证的方式。从现在开始,我将从Firebase接收JWT令牌,然后在我的Sping Boot 服务器上进行验证。
到目前为止,这工作得很好,但是有一个变化我不太满意,那就是principal-object现在是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令牌时,在我的数据库中创建一个用户记录,而该用户还不存在。
此外,我的许多业务逻辑目前依赖于作为用户实体业务对象的主体。我可以修改这些代码,但这是一项乏味的工作,谁不想回顾一下几行遗留代码呢?

ncgqoxb0

ncgqoxb01#

我做的和朱利安·艾奇卡德有点不同。
在我的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,它将返回我们想要的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);
    }

}
cs7cruho

cs7cruho2#

到目前为止,这一切都很顺利,但有一个变化我不太满意,那就是主体对象现在是org.springframework.security.oauth2.jwt.Jwt,而不是像以前那样是AppUserEntity,即用户模型。
在我的应用程序中,我通过滚动我自己的JwtAuthenticationFilter而不是使用BearerTokenAuthenticationFilter来避免这个问题,然后将我的User实体设置为Authentication对象中的principal。但是,在我的例子中,这几乎是根据JWT声明构造一个User,这可能是一个不好的做法:SonarLint提示使用DTO来降低某人使用已泄露的JWT令牌将任意数据注入到其用户记录中的风险。我不知道这是否是一个大问题-如果您不能信任您的JWT,则会有其他问题,恕我直言。
我不知道这是否是最好的方法,但是我打算在发现一个用户的有效JWT令牌(该用户还不存在)后,在我的数据库中创建一个用户记录。
请记住,应用程序应该以无状态的方式验证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
}

您需要将涉及此用户的更改持久化到数据库中。

s2j5cfk0

s2j5cfk03#

SecurityContextHolder.setContext(context);

不适用于

request.getUserPrincipal();

您可以创建一个自定义类来扩展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()
}
xvw2m8pv

xvw2m8pv4#

根据Josh Cummings的回答在issue #7834 make中配置:

public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
    http...
       .oauth2ResourceServer(oauth2 -> oauth2.jwt(
             jwt -> jwt.jwtAuthenticationConverter(JwtUtil::createJwtUser)))
    ...
    return http.build();
}

和实现工厂方法,例如:

public class JwtUtil {
   public static JwtUser createJwtUser(Jwt jwt) {
      int id = ((Long) jwt.getClaims().get("id")).intValue();
      String rawRoles = (String) jwt.getClaims().get("roles");
      Set<Role> roles = Arrays.stream(rawRoles.split(" "))
        .map(Role::valueOf)
        .collect(Collectors.toSet());
      return new JwtUser(jwt, roles, id);
   }
}

public class JwtUser extends JwtAuthenticationToken {
    public JwtUser(Jwt jwt, Collection<? extends GrantedAuthority> authorities, int id) {
        super(jwt, authorities);
        ....
    }
}

请注意,控制器的方法应该注入JwtUser jwtUser,而不注入任何@AuthenticationPrincipal

相关问题