带有 JWT 令牌的 Spring Security 在每个请求上加载 spring UserDetails 对象

Posted

技术标签:

【中文标题】带有 JWT 令牌的 Spring Security 在每个请求上加载 spring UserDetails 对象【英文标题】:Spring Security with JWT token is loading spring UserDetails object on every request 【发布时间】:2020-01-21 14:03:44 【问题描述】:

我正在学习使用 JWT 令牌和 Spring Boot 的 Spring Security。我已经正确实施它并且工作正常。但我对 JwtRequestFilter 的工作原理有一个疑问。我已经浏览了几个网站来了解使用 spring boot 的 spring security 并发现了同样的事情。所以让我去主要怀疑。 我在下面添加 JwtRequestFilter 文件。

JwtRequestFilter.java

@Component
public class JwtRequestFilter extends OncePerRequestFilter 

@Autowired
private JwtUserDetailsService jwtUserDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException 

    final String requestTokenHeader = request.getHeader("Authorization");

    String username = null;
    String jwtToken = null;

    // JWT Token is in the form "Bearer token". Remove Bearer word and get
    // only the Token
    if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) 
        jwtToken = requestTokenHeader.substring(7);
        try 
            username = jwtTokenUtil.getUsernameFromToken(jwtToken);
         catch (IllegalArgumentException e) 
            System.out.println("Unable to get JWT Token");
         catch (ExpiredJwtException e) 
            System.out.println("JWT Token has expired");
        
     else 
        logger.warn("JWT Token does not begin with Bearer String");
    
    // Once we get the token validate it.
    if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) 
         // This below line is calling on every request
        UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername(username);
        // if token is valid configure Spring Security to manually set
        // authentication
        if (jwtTokenUtil.validateToken(jwtToken, userDetails)) 
            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
            userDetails, null, userDetails.getAuthorities());
            usernamePasswordAuthenticationToken
            .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            // After setting the Authentication in the context, we specify
            // that the current user is authenticated. So it passes the
            // Spring Security Configurations successfully.
            SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        
    
    filterChain.doFilter(request, response);

为了验证令牌,我们必须提供 spring UserDetails 对象,并且我们从 jwtUserDetailsS​​ervice 获取 spring UserDetails 对象。因此,此过滤器将调用的每个请求都会执行令牌验证,我们必须在每个请求上调用 jwtUserDetailsS​​ervice。 我的疑问在于我的 jwtUserDetailsS​​ervice 我正在添加几个验证并添加用户权限。因此,在 jwtUserDetailsS​​ervice 中重复以下步骤。

    使用数据库中的用户名获取用户。 获取用户角色 从数据库获取用户权限。 为 userDetails 分配权限。

JwtUserDetailsS​​ervice.java

@Service("jwtUserDetailsService")
@Transactional
public class JwtUserDetailsService implements UserDetailsService 

@Autowired
private UserRepository userRepository;

@Autowired
private IUserService service;

@Autowired
private MessageSource messages;

@Autowired
private RoleRepository roleRepository;

@Override
public UserDetails loadUserByUsername(String email)
  throws UsernameNotFoundException 

    User user = userRepository.findByEmail(email);
    if (user == null) 
        return new org.springframework.security.core.userdetails.User(
          " ", " ", true, true, true, true, 
          getAuthorities(Arrays.asList(
            roleRepository.findByName("ROLE_USER"))));
    

    return new org.springframework.security.core.userdetails.User(
      user.getEmail(), user.getPassword(), user.isEnabled(), true, true, 
      true, getAuthorities(user.getRoles()));


private Collection<? extends GrantedAuthority> getAuthorities(
  Collection<Role> roles) 

    return getGrantedAuthorities(getPrivileges(roles));


private List<String> getPrivileges(Collection<Role> roles) 

    List<String> privileges = new ArrayList<>();
    List<Privilege> collection = new ArrayList<>();
    for (Role role : roles) 
        collection.addAll(role.getPrivileges());
    
    for (Privilege item : collection) 
        privileges.add(item.getName());
    
    return privileges;


private List<GrantedAuthority> getGrantedAuthorities(List<String> privileges) 
    List<GrantedAuthority> authorities = new ArrayList<>();
    for (String privilege : privileges) 
        authorities.add(new SimpleGrantedAuthority(privilege));
    
    return authorities;


因此,对于每个请求,这些查询都会执行。有没有更好的方法来做到这一点?因为一旦我在 Spring UserDetails 对象中添加用户权限,为什么我们需要对每个请求再次执行此操作。或者那些只有请求范围。我曾在 spring mvc 上工作过,一旦我们在 spring UserDetails 对象中添加权限,它就会一直存在,直到我点击注销意味着它将存在于 spring 安全上下文中,直到我们将其删除。春季靴子会一样吗?如果我在 Spring UserDetails 对象中添加一次角色和权限详细信息,为什么我们需要再次添加它?

【问题讨论】:

spring security 5 中已经有了 jwt 过滤器,为什么还要实现自己的过滤器 @ThomasAndolf,对不起,我刚刚开始学习这个。所以我不知道 spring security 5 提供了这样的功能。您能否提供任何链接,以便我可以通过它来理解概念。谢谢。 太多人用谷歌搜索和阅读旧教程。始终先查看他们自己的文档。其实真的很好docs.spring.io/spring-security/site/docs/current/reference/… @ThomasAndolf,谢谢,我会调查的。 @ThomasAndolf 至少他们在谷歌搜索:-)。我正在查看文档,但在列表中没有看到 JWT 过滤器(我见过有人扩展了 UsernamePasswordAuthenticationFilter 或 OncePerFilter)。我在 OAuth 下看到了一个令牌过滤器,但他没有在上面实现 OAuth。 dev.to/d_tomov/… 【参考方案1】:

一旦用户登录,他的身份验证就建立了,所以你不需要再次进行db调用,在每个请求用户登录后,应该只检查身份验证期间在令牌中设置的角色,你需要验证令牌在每个请求中都没有被篡改 而不是通过从数据库调用加载用户详细信息来创建用户详细信息

UserDetails userDetails = this.jwtUserDetailsS​​ervice.loadUserByUsername(username);

您还可以在 JWT 声明中对用户的用户名和角色进行编码 并通过解析来自 JWT 的声明来创建 UserDetails 对象。

【讨论】:

【参考方案2】:

所以这个过滤器将调用的每个请求然后令牌验证将 执行,我们必须在每个请求上调用 jwtUserDetailsS​​ervice。

这是不正确的,因为您有一个条件if (SecurityContextHolder.getContext().getAuthentication() == null)

因此,第一次验证令牌时,您查询您的用户详细信息服务,获取所有授权并将它们设置为安全上下文(您已经在这样做:SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);)。

此外,使用 JWT 身份验证,您通常甚至不需要访问任何用户详细信息服务,因为理想情况下,所有授权都应包含在令牌本身中。所以你唯一需要做的就是验证令牌的签名。

【讨论】:

我同意你的观点,在每次请求时都不应该调用 jwtUserDetailsS​​ervice。在这里,我的前端是有角度的,所以每次我收到请求 SecurityContextHolder.getContext().getAuthentication() 总是为空。从角度来看,我在标题中传递令牌。我需要传递任何其他参数吗?据我所知,过滤器将在 Spring Security 之前调用,因此我们无法从 SecurityContextHolder 获取用户。因为我们将如何识别来自 spring 安全上下文持有者的用户身份验证对象。如果我错了,请告诉我。 您可能错误地注册了过滤器。您可以添加SecurityConfig 代码(您实际将此过滤器添加到过滤器链的位置)吗?

以上是关于带有 JWT 令牌的 Spring Security 在每个请求上加载 spring UserDetails 对象的主要内容,如果未能解决你的问题,请参考以下文章

使用带有令牌的 spring 安全插件的一个好习惯

带有加密 JWT 访问令牌的 Spring Boot OAuth2

带有刷新令牌工作流问题的 Java Spring JWT

使用带有spring security的keycloak JWT令牌时如何修复403

带有 JWT auth 和 csrf 令牌的 Spring Boot STATELESS 应用程序

Spring OAuth/JWT 从访问令牌中获取额外信息