带有 JWT AuthenticationCredentialsNotFoundException 的 Spring 安全配置

Posted

技术标签:

【中文标题】带有 JWT AuthenticationCredentialsNotFoundException 的 Spring 安全配置【英文标题】:Spring security configuration with JWT AuthenticationCredentialsNotFoundException 【发布时间】:2019-08-06 00:15:38 【问题描述】:

当我尝试使用 POSTMAN 调用受保护的 ws 并在登录 Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJBZGVsSVAiLCJpYXQiOjE1NTI2MDM4NTIsImV4cCI6MTU1MjY5MDI1Mn0.OfzkQlhubdLBa9cV7O231M1AB8ya9g5Q1zefhjhPvJIICz45SUunT2xP6r008O-oxUXBQT3RrRha7n6gSo9Jpw 后使用获得的令牌传递授权标头时出现以下错误。

例外:

org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
org.springframework.security.access.intercept.AbstractSecurityInterceptor.credentialsNotFound(AbstractSecurityInterceptor.java:379)
org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:223)
org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:65)
org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688)
com.spring.jwtauthentication.controller.TestRestAPIs$$EnhancerBySpringCGLIB$$11db1a81.adminAccess(<generated>)
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.lang.reflect.Method.invoke(Method.java:498)
org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:209)
org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:136)
org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102)
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:877)
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:783)
org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:991)
org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925)
org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974)
org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:866)
javax.servlet.http.HttpServlet.service(HttpServlet.java:635)
org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851)
javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)

请在下面找到我的实现:

1) 登录 api AuthRestAPIs.java :

@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api/auth")
public class AuthRestAPIs 

    @Autowired
    AuthenticationManager authenticationManager;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    RoleRepository roleRepository;

    @Autowired
    PasswordEncoder encoder;

    @Autowired
    JwtProvider jwtProvider;

    @PostMapping("/signin")
    public ResponseEntity<?> authenticateUser( @RequestBody LoginForm loginRequest) 

        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        loginRequest.getUsername(),
                        loginRequest.getPassword()
                )
        );

        SecurityContextHolder.getContext().setAuthentication(authentication);
        String jwt = jwtProvider.generateJwtToken(authentication);
        return ResponseEntity.ok(new JwtResponse(jwt));
    


测试 api apiTestRestAPIs.java(受保护的资源):

@RestController
public class TestRestAPIs 

    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/api/test//admin")
    public String adminAccess() 
        return ">>> Admin Contents";
    

2) 安全配置类

@Configuration
@EnableWebSecurity
@ComponentScan(basePackages = "com.spring.jwtauthentication", scopedProxy = ScopedProxyMode.INTERFACES)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter 

    @Autowired
    @Qualifier("userDetailsServiceImpl")
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtAuthEntryPoint unauthorizedHandler;


    public JwtAuthTokenFilter authenticationJwtTokenFilter() 
        JwtAuthTokenFilter test = new JwtAuthTokenFilter();
        return test;
    

    @Autowired
    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception 
        authenticationManagerBuilder
                .userDetailsService(this.userDetailsService)
                .passwordEncoder(passwordEncoder());
    


    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception 
        return super.authenticationManagerBean();
    

    @Bean
    public PasswordEncoder passwordEncoder() 
        return new BCryptPasswordEncoder();
    

    @Override
    protected void configure(HttpSecurity http) throws Exception 
        http.cors().and().csrf().disable().
                authorizeRequests()
                .antMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
    


3) JwtAuthTokenFilter.java :

@Component
public class JwtAuthTokenFilter extends OncePerRequestFilter 

    @Autowired
    private JwtProvider tokenProvider;

    @Autowired
    @Qualifier("userDetailsServiceImpl")
    private UserDetailsService userDetailsService;

    private static final Logger logger = LoggerFactory.getLogger(JwtAuthTokenFilter.class);

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

            String jwt = getJwt(request);
            if (jwt!=null && tokenProvider.validateJwtToken(jwt)) 
                String username = tokenProvider.getUserNameFromJwtToken(jwt);

                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                UsernamePasswordAuthenticationToken authentication
                        = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                SecurityContextHolder.getContext().setAuthentication(authentication);
            
         catch (Exception e) 
            logger.error("Can NOT set user authentication -> Message: ", e);
        

        filterChain.doFilter(request, response);
    

    private String getJwt(HttpServletRequest request) 
        String authHeader = request.getHeader("Authorization");

        if (authHeader != null && authHeader.startsWith("Bearer ")) 
            return authHeader.replace("Bearer ","");
        

        return null;
    

4) JwtProvider.java

@Component
public class JwtProvider 

    private static final Logger logger = LoggerFactory.getLogger(JwtProvider.class);


    private String jwtSecret = "jwtOlfaSecretKey";

    private int jwtExpiration =86400;

    public String generateJwtToken(Authentication authentication) 

        UserPrinciple userPrincipal = (UserPrinciple) authentication.getPrincipal();

        return Jwts.builder()
                        .setSubject((userPrincipal.getUsername()))
                        .setIssuedAt(new Date())
                        .setExpiration(new Date((new Date()).getTime() + jwtExpiration*1000))
                        .signWith(SignatureAlgorithm.HS512, jwtSecret)
                        .compact();
    

    public boolean validateJwtToken(String authToken) 
        try 
            Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
            return true;
         catch (SignatureException e) 
            logger.error("Invalid JWT signature -> Message:  ", e);
         catch (MalformedJwtException e) 
            logger.error("Invalid JWT token -> Message: ", e);
         catch (ExpiredJwtException e) 
            logger.error("Expired JWT token -> Message: ", e);
         catch (UnsupportedJwtException e) 
            logger.error("Unsupported JWT token -> Message: ", e);
         catch (IllegalArgumentException e) 
            logger.error("JWT claims string is empty -> Message: ", e);
        

        return false;
    

    public String getUserNameFromJwtToken(String token) 
        return Jwts.parser()
                            .setSigningKey(jwtSecret)
                            .parseClaimsJws(token)
                            .getBody().getSubject();
    

5) JwtAuthEntryPoint.java :

@Component
public class JwtAuthEntryPoint implements AuthenticationEntryPoint 

    private static final Logger logger = LoggerFactory.getLogger(JwtAuthEntryPoint.class);

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException e) 
                                 throws IOException, ServletException 

        logger.error("Unauthorized error. Message - ", e.getMessage());
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error -> Unauthorized");
    

6) web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

    <display-name>myApp</display-name>

    <servlet>
        <servlet-name>myApp</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>myApp</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/cfg.xml</param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

</web-app>

7) pom.xml:

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.0.8.RELEASE</version>
        </dependency> 
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-core</artifactId>
            <version>5.1.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
            <version>5.1.3.RELEASE</version>
        </dependency>

【问题讨论】:

首先您的 JwtAuthTokenFilter 被标记为 @Component 但您不使用它,而是创建它的一个新实例。用它!自动连线!还要确保服务器实际上正在接收令牌。确保角色(userdetails.getAuthorities() 不为空)。第二件事,JWT 的目的是无状态。 UserDetails userDetails = userDetailsS​​ervice.loadUserByUsername(username) 一段代码使它不是无状态的。如果您想要/需要权限,请使用您的 jwtprovider 类将其作为声明传递到 JWT 令牌本身中。你的代码的其他部分没问题。希望对你有帮助 【参考方案1】:

使用@PreAuthorize("hasAuthority('ADMIN')") 代替@PreAuthorize("hasRole('ADMIN')"),这适用于您的情况。

【讨论】:

以上是关于带有 JWT AuthenticationCredentialsNotFoundException 的 Spring 安全配置的主要内容,如果未能解决你的问题,请参考以下文章

带有 JWT 响应的登录请求

带有 JWT 的 AngularJS 或 SPA - 到期和刷新

如何在 Angular 中从服务器获取带有 jwt 令牌的响应标头

带有 jQ​​uery Ajax 的 JWT 令牌

使用带有 C# 的公钥验证 JWT

带有 Angular 8 和 Springboot 2 的 JWT:JWT 字符串必须恰好包含 2 个句点字符