将 JWT 身份验证主体转换为 Spring 中更可用的内容

Boo*_*aka 7 spring spring-security spring-boot

我有一个 Spring Boot 微服务,用于验证 JWT(由不同服务颁发)进行身份验证。它运行良好,我可以像这样访问控制器中的 JWT 详细信息:

// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'

// MyController.java
@RestController
@RequestMapping("/")
public class MyController {
    @GetMapping()
    public String someControllerMethod(@AuthenticationPrincipal Jwt jwt) {
        int userId = Integer.parseInt(jwt.getClaim("userid"));
        ...
    }
}
Run Code Online (Sandbox Code Playgroud)

效果很好。我可以从 JWT 中提取我需要的内容,然后使用正确的用户 ID 等继续与我的数据库对话。

然而,我发现必须使用 Jwt 类型在每个控制器中获取这些值有点乏味。有没有办法可以注入不同的类型作为@AuthenticationPrincipal?

例如,我自己的类已经从 JWT 中提取了所需的内容,并公开了类似.getUserId()返回 int 的内容?这也让我可以集中解析声明或抛出异常(如果它们不符合预期)的逻辑。

更新

经过更多谷歌洞穴探险后,似乎我有两个选择

选项1:@ControllerAdvice和@ModelAttribute

正如这个答案中所解释的。我可以做类似的事情:

import com.whatever.CustomPrincipal; // a basic "data" class with some properties, getters, setters and constructor
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;

@ControllerAdvice
public class SecurityControllerAdvice {

    @ModelAttribute
    public CustomPrincipal customPrincipal(Authentication auth) throws Exception {
        CustomPrincipal customPrincipal;
        if (auth != null && auth.getPrincipal() instanceof Jwt) {
            Jwt jwt = (Jwt) auth.getPrincipal();
            String sessionId = jwt.getClaimAsString("sessionid");
            int userId = Integer.parseInt(jwt.getClaimAsString("userid"));
            customPrincipal = new CustomPrincipal(userId, sessionId);
        } else {
            // log an error and throw an exception?
        }
        return customPrincipal;
    }
}
Run Code Online (Sandbox Code Playgroud)

进而

import com.whatever.CustomPrincipal;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RestController;

@RestController
@ControllerAdvice
public class HelloWorldController {
    @GetMapping("/controlleradvice")
    public String index(@ModelAttribute CustomPrincipal cp) {
        log.info(cp.getUserId());
        return "whatever";
    }
}

Run Code Online (Sandbox Code Playgroud)

这看起来相当简洁、整洁。1 个带有 @ControllerAdvice 的新课程,鲍勃是你的叔叔!

选项2:使用 jwtAuthenticationConverter()

这个答案显示了另一种方法,使用“转换器”,它似乎将默认主体从 JWT 转换为包含 JWT (.getCredentials()) 以及自定义对象的自定义对象(扩展 AbstractAuthenticationToken)像 CustomPrincipal (或者 User 类之类的)。

@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .cors().disable()
            .csrf().disable()
            .authorizeHttpRequests((authorize) -> authorize
                    .anyRequest().authenticated()
            )
            .oauth2ResourceServer().jwt(customizer -> customizer.jwtAuthenticationConverter((new MyPrincipalJwtConvertor())));
        return http.build();
    }
}


import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.jwt.Jwt;

public class MyPrincipalJwtConvertor implements Converter<Jwt, MyAuthenticationToken> {
    @Override
    public MyAuthenticationToken convert(Jwt jwt) {
        var principal = new MyPrincipal(Integer.parseInt(jwt.getClaimAsString("userid")), jwt.getClaimAsString("sessionid"));
        return new MyAuthenticationToken(jwt, principal);
    }
}


@RestController
public class HelloWorldController {
    @GetMapping("/converter")
    public String converter(@AuthenticationPrincipal MyPrincipal myPrincipal) {
        log.info("/converter triggered");
        log.info("" + myPrincipal.getUserId());
        return "woo";
    }
}


import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class MyPrincipal {
    private int userId;
    private String sessionId;
}
Run Code Online (Sandbox Code Playgroud)

选项 1 看起来简单得多。

但选项 2 很好,因为我有运行过滤器来执行额外的验证(例如验证 JWT 中的会话 ID)。当该过滤器运行时,当它调用 SecurityContext.getContext().getAuthentication().getPrincipal() 时,它将获取 MyPrincipal 对象,而不必调用 Jwt.getClaimAsString() 并对其进行强制转换等。

我想我是在问,这两种方法有我没有考虑过的优点和缺点吗?他们中的一个人是否可能以一种不应该的方式破坏/虐待某些东西?

或者是很相似,我应该选择我喜欢的那个?

小智 0

只是想添加与“选项 1”类似的内容。如果您在生成 jwt 之前使用用户名/密码格式登录用户,则可以将身份验证主体声明为您的 UserEntity 并从拦截器分配值。

\n

控制器方法将是这样的:

\n
//With this method the preAuthorize becomes redundant but I still like including it.\n\n@PreAuthorize("isAuthenticated()")\n    @PutMapping("/updatePassword")\n    public ResponseEntity<?> updatePassword(@AuthenticationPrincipal UserEntity loggedUser,\n            @RequestBody PasswordUpdateRequest passwordRequest) {\n        return userEntityService.updatePassword(loggedUser, passwordRequest);\n    }\n
Run Code Online (Sandbox Code Playgroud)\n

例如,在您从 jwt 中声明用户 ID 后,您实际上会从 UserDetailsS​​ervice 中的方法加载用户,如下所示:

\n
UserEntity user = (UserEntity) userDetailsService.loadUserById(userId);\n
Run Code Online (Sandbox Code Playgroud)\n

然后声明一个 UsernamePasswordAuthenticationToken 将用于在上下文中设置,如下所示:

\n
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user,\n                        user.getRoles(), user.getAuthorities());\n                authentication.setDetails(new WebAuthenticationDetails(request));\n\n                SecurityContextHolder.getContext().setAuthentication(authentication);\n
Run Code Online (Sandbox Code Playgroud)\n

要实现此拦截器,请确保使用 addFilterBefore,以便我们可以在 UsernamePasswordAuthenticationFilter 之前实现此拦截器。

\n

如果您使用SecurityFilterChain,请执行以下操作:

\n
@Bean\n    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {\n       http.addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class);\n        return http.build();\n    }\n
Run Code Online (Sandbox Code Playgroud)\n

如果使用旧的配置方法,那么它是相同的,但不要\xc2\xb4t 写“return http.build();” 部分。

\n

在这两个中,您还可以使用 antmatchers 来限制哪种用户可以通过方法 .hasRole() 和 .hasAnyRole() 使用方法。

\n

我还建议设置一个自定义的 AuthenticationEntryPoint 来进行异常处理,并从注释为 bean 的 CorsConfigurationSource 启用 cors(在安全过滤器链中声明 http.cors(),springboot 将自动选择此 cors 配置)。

\n

我喜欢这样做的原因是,从生成令牌的微服务到仅验证它的其他微服务,代码仅发生很小的变化,这也使得使用我的其余端点的人们可以使用以下方式创建服务:如果他们需要登录用户的数据,则只需最少的努力,因为他们只需要在控制器中声明 @AuthenticationPrincipal UserEntity 用户。

\n

这是我第一次回答,所以如果我以错误的方式回答或者只是没有正确解释自己,请告诉我。

\n