如何使用 Spring Security 实现具有两级令牌认证的 Spring Boot 微服务?

Posted

技术标签:

【中文标题】如何使用 Spring Security 实现具有两级令牌认证的 Spring Boot 微服务?【英文标题】:How to implement Spring Boot Microservice with two levels of token authentication using Spring Security? 【发布时间】:2021-02-07 05:28:40 【问题描述】:

团队您好,

我正在开发一个 Spring Boot 项目,我想在其中使用 Spring Security + JWT 设置两个级别的令牌身份验证。

我的一些同事已经使用 Dropwizard 框架构建了一个类似的应用程序。

我想在我的 Spring Boot 项目中实现相同的架构。我在这个问题的末尾添加了指向 API 架构的链接。

我可以使用 Spring Boot 设置第一级令牌认证(使用 Spring Security + JWT),但我无法找到设置第二级令牌认证的正确方法。

我尝试搜索相关文章,但找不到。

如果您可以共享代码 sn-p 在 Spring Boot(使用 Spring Security)中实现两个级别的令牌身份验证以更好地理解,将会很有帮助。

谢谢你的期待!

【问题讨论】:

实施任何类型的自定义安全流程都是不好的做法。有许多实施不当的流程导致安全漏洞的例子。您应该遵循任何标准的 oauth2 流程,或者使用 spring 提供的当前现有的已通过社区审查和测试的身份验证方法。 【参考方案1】:

以下是实现您需要的概念证明。第一类是安全配置:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter 

   public static final List<String> WHITE_LIST = asList("/authenticate");
   public static final List<String> TOKEN1_LIST = asList("/get-access-token");
   public static final List<String> TOKEN2_LIST = asList("/add-new-user");

   @Autowired
   private Token1Filter token1Filter;

   @Autowired
   private Token2Filter token2Filter;


   @Override
   protected void configure(HttpSecurity http) throws Exception 
     http.csrf().disable()
            .formLogin().disable()
            .httpBasic().disable()
            // Make sure we use stateless session; session won't be used to store user's state
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            // Handle an authorized attempts
            .exceptionHandling().authenticationEntryPoint((req, rsp, e) -> rsp.sendError(HttpServletResponse.SC_UNAUTHORIZED))
            .and()
            .authorizeRequests()
            // List of services do not require authentication
            .antMatchers(OPTIONS).permitAll()
            .antMatchers(GET, WHITE_LIST.toArray(new String[WHITE_LIST.size()])).permitAll()
            // Any other request must be authenticated
            .anyRequest().authenticated()
            .and()
            .addFilterBefore(token1Filter, UsernamePasswordAuthenticationFilter.class)
            .addFilterBefore(token2Filter, UsernamePasswordAuthenticationFilter.class);
  

如您所见,不同的 Url 包含在不同的属性中,并且与它们相关的每个 List 都在 HttpSecurity 类中配置。用于管理每个子集的过滤器,我的意思是,使用 Jwt 1 和 Jwt 2 保护的 Urls 如下:

@AllArgsConstructor
@Component
public class Token1Filter extends OncePerRequestFilter 

  private static final String TOKEN_PREFIX = "Bearer ";

  @Override
  protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response,
                                 FilterChain filterChain) throws ServletException, IOException 
    getJwt(request)
            .ifPresent(jwt1 -> 
                /**
                 *    Here you can use functionality to check provided Jwt token 1,
                 * adding included data into Spring SecurityContextHolder.
                 */

                // Used for testing purpose
                SecurityContextHolder.getContext().setAuthentication(
                        new UsernamePasswordAuthenticationToken("testUserToken1", null, new ArrayList<>())
                );
            );
    filterChain.doFilter(request, response);
  


  @Override
  protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException 
    return !WebSecurityConfiguration.TOKEN1_LIST.contains(request.getRequestURI());
  


  private Optional<String> getJwt(HttpServletRequest request) 
    return ofNullable(request)
            .map(r -> r.getHeader(AUTHORIZATION))
            .filter(Predicate.not(String::isEmpty))
            .map(t -> t.replace(TOKEN_PREFIX, ""))
            .filter(Predicate.not(String::isEmpty));
  




@AllArgsConstructor
@Component
public class Token2Filter extends OncePerRequestFilter 

  private static final String TOKEN_PREFIX = "Bearer ";

  @Override
  protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response,
                                 FilterChain filterChain) throws ServletException, IOException 
    getJwt(request)
            .ifPresent(jwt2 -> 
                /**
                 *    Here you can use functionality to check provided Jwt token 2,
                 * adding included data into Spring SecurityContextHolder.
                 */

                // Used for testing purpose
                SecurityContextHolder.getContext().setAuthentication(
                        new UsernamePasswordAuthenticationToken("testUserToken2", null,
                                asList(new SimpleGrantedAuthority("ADMIN")))
                );
            );
    filterChain.doFilter(request, response);
  


  @Override
  protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException 
    return !WebSecurityConfiguration.TOKEN2_LIST.contains(request.getRequestURI());
  


  private Optional<String> getJwt(HttpServletRequest request) 
    return ofNullable(request)
            .map(r -> r.getHeader(AUTHORIZATION))
            .filter(Predicate.not(String::isEmpty))
            .map(t -> t.replace(TOKEN_PREFIX, ""))
            .filter(Predicate.not(String::isEmpty));
  

每个都包含shouldNotFilter 方法的实现,以了解它是否必须管理当前请求。并包含对您需要添加功能以提取和验证提供的 Jwt 令牌的位置的注释。

最后,只有一个虚拟控制器来测试每个用例:

@AllArgsConstructor
@RestController
public class TestController 

  @GetMapping("/authenticate")
  public ResponseEntity<String> authenticate(@RequestParam String username, @RequestParam String password) 
    return new ResponseEntity("[authenticate] Testing purpose", OK);
  

  @GetMapping("/get-access-token")
  public ResponseEntity<String> getAccessToken() 
    return new ResponseEntity("[get-access-token] Testing purpose", OK);
  

  @GetMapping("/add-new-user")
  @PreAuthorize("hasAuthority('ADMIN')")
  public ResponseEntity<String> addNewUser() 
    return new ResponseEntity("[add-new-user] Testing purpose", OK);
  

可以添加一些改进,但这是一个很好的初始点,可以按预期工作。

【讨论】:

如果您提供的令牌不以Bearer 为前缀,但其他任何内容都将通过您的小验证。实施自定义安全过滤器是不好的做法,并且有很多实施不良的自定义弹簧安全过滤器的例子。 另一方面,有几件事情需要考虑:1. Bearer 前缀是一个标准,所以在这里它是有意义的。 2.由于“白名单”中未包含的Url已配置为authenticated(),您的确认anything else it will pass your little validation不正确。 3. 在示例中,SecurityContextHolder 中包含的数据仅用作测试目的,如果您阅读上述评论,我会向他解释如何管理每个用例。 4. 如果您需要自定义安全方法但 Spring 本身不提供,则解决方案不能是 don't do it 不应该用概念证明来回答不好的做法。应该回答这是不好的做法。如果您需要自定义安全性,那么首先有人做错了什么。有一些标准是应该遵循的,我们应该教导这些标准。任何其他都是错误的答案。我们不应该回答有不良做法的人。 创建自定义过滤器是框架提供的一个工具,也是一种广泛使用的技术。您仍然坚持称它为bad practice,好的,请随时与 Spring 团队讨论它。我打电话给proof of concept,因为我不知道如何处理他的Jwt令牌的细节,所以我不能“填补空白”。出于这个原因,我将 cmets 包含在应该管理这些 cmets 的代码中,并将所需的内容添加到 Spring Authorization 内容中。 @dotore 提供的解决方案完美运行。考虑到最佳实践,这是一个已经在客户现有项目中实现的架构,所以我别无选择,只能遵循相同的架构。但是,我已经按照 Thomas Andolf 分享的文章中的建议进行了必要的更改,以使用 JJWT 库进行令牌解析。感谢医生和 Thomas Andolf 的帮助。

以上是关于如何使用 Spring Security 实现具有两级令牌认证的 Spring Boot 微服务?的主要内容,如果未能解决你的问题,请参考以下文章

具有Spring Security的多个用户

如何在 Spring Security 中的子域之间共享会话

如何使用 Spring Security 处理基于用户权限的授权?

具有 Spring Security 和 Java Config 的自定义身份验证提供程序

具有特定表角色的spring security UserDetailsS​​ervice

我们如何使用带有 Spring 5.0 的最新 spring-security-oauth2 jar 来实现授权服务器?