代码之家  ›  专栏  ›  技术社区  ›  Michael Coxon

Spring Security JWT REST API返回401

  •  0
  • Michael Coxon  · 技术社区  · 7 年前

    我有一个相对简单的设置,使用SpringBoot2,SpringSecurity,我使用JWT基本上保持用户登录。

    完整的项目如下: http://github.com/mikeycoxon/spring-boot-2-security-jwt

    我有两个过滤器,一个是做身份验证,另一个是授权。

    我有一个授权过滤器:

    public class AuthNFilter extends UsernamePasswordAuthenticationFilter {
        private AuthenticationManager authenticationManager;
    
        public AuthNFilter(AuthenticationManager authenticationManager) {
            this.authenticationManager = authenticationManager;
        }
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest req,
                                                    HttpServletResponse res) throws AuthenticationException {
            try {
                User creds = new ObjectMapper()
                        .readValue(req.getInputStream(), User.class);
    
                return authenticationManager.authenticate(
                        new UsernamePasswordAuthenticationToken(
                                creds.getUsername(),
                                creds.getPassword(),
                                creds.getRoles())
                );
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    
        @Override
        protected void successfulAuthentication(HttpServletRequest req,
                                                HttpServletResponse res,
                                                FilterChain chain,
                                                Authentication auth) throws IOException, ServletException {
    
            String token = Jwts.builder()
                    .setSubject(((User) auth.getPrincipal()).getUsername())
                    .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                    .signWith(SignatureAlgorithm.HS512, SECRET.getBytes())
                    .compact();
            res.addHeader(HEADER_STRING, TOKEN_PREFIX + token);
        }
    }
    

    这将根据数据存储验证用户,并使用令牌向响应添加自定义头。

    和AuthzFilter:

    public class AuthZFilter  extends BasicAuthenticationFilter {
    
        public AuthZFilter(AuthenticationManager authManager) {
            super(authManager);
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest req,
                                        HttpServletResponse res,
                                        FilterChain chain) throws IOException, ServletException {
            String header = req.getHeader(HEADER_STRING);
    
            if (header == null || !header.startsWith(TOKEN_PREFIX)) {
                chain.doFilter(req, res);
                return;
            }
    
            UsernamePasswordAuthenticationToken authentication = getAuthentication(req);
    
            SecurityContextHolder.getContext().setAuthentication(authentication);
            chain.doFilter(req, res);
        }
    
        private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
            String token = request.getHeader(HEADER_STRING);
            if (token != null) {
                // parse the token.
                String user = Jwts.parser()
                        .setSigningKey(SECRET.getBytes())
                        .parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
                        .getBody()
                        .getSubject();
    
                if (user != null) {
                    return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
                }
                return null;
            }
            return null;
        }
    }
    

    它替换了basicauthenticationfilter,以便我们可以读取JWT并在SecurityContext中设置用户。

    为此,我设置了一个WebSecurityConfigureAdapter,以便我们可以覆盖Spring安全性的默认值:

    @EnableWebSecurity
    public class WebSecurity extends WebSecurityConfigurerAdapter {
        private UserDetailsServiceImpl userDetailsService;
        private BCryptPasswordEncoder bCryptPasswordEncoder;
    
        public WebSecurity(UserDetailsServiceImpl userDetailsServiceImpl, BCryptPasswordEncoder bCryptPasswordEncoder) {
            this.userDetailsService = userDetailsServiceImpl;
            this.bCryptPasswordEncoder = bCryptPasswordEncoder;
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.cors().and().csrf().disable().authorizeRequests()
                    .antMatchers(SIGN_UP_URL).permitAll()
                    .antMatchers(LOGIN_URL).permitAll()
                    .anyRequest().authenticated()
                    .and()
                    .addFilter(new AuthNFilter(authenticationManager()))
                    .addFilter(new AuthZFilter(authenticationManager()))
                    // this disables session creation on Spring Security
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    
        @Override
        public void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
        }
    
        @Bean
        CorsConfigurationSource corsConfigurationSource() {
            final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
            return source;
        }
    }
    

    SIGNUP_URL =/api/用户,是一个帖子 LOGIN_URL =Spring自己的/登录端点

    基本上,问题出现在测试中:

    @RunWith(SpringRunner.class)
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    @ActiveProfiles("embedded")
    @AutoConfigureMockMvc
    public class AccessControllerFunctionalTest {
    
        @Autowired
        private WebApplicationContext context;
    
        @Autowired
        private MockMvc mvc;
    
        @MockBean
        private UserRepository userRepository;
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Before
        public void setup() {
            mvc = MockMvcBuilders
                    .webAppContextSetup(context)
                    .apply(springSecurity())
                    .build();
        }
    
        @Test
        public void doSignup() throws Exception {
            String requestString = "{\"username\": \"mike@gmail.com\",\"password\": \"password\"}";
            mvc.perform(post("/api/user").contentType(APPLICATION_JSON)
                    .content(requestString))
                    .andDo(print()).andExpect(status().isOk());
        }
    
        @Test
        public void doLoginFailsWithUserNotExists() throws Exception {
            String requestString = "{\"username\": \"mike@gmail.com\",\"password\": \"password\"}";
            mvc.perform(post("/login").contentType(APPLICATION_JSON)
                    .content(requestString))
                    .andDo(print())
                    .andExpect(status().isUnauthorized());
        }
    
        @Test
        public void doLoginSuccessWithUserExists() throws Exception {
            String requestString = "{\"username\": \"rmjcoxon@gmail.com\",\"password\": \"password\"}";
            mvc.perform(post("/login").contentType(APPLICATION_JSON)
                    .content(requestString))
                    .andDo(print())
                    .andExpect(status().isOk())
                    .andExpect(header().exists(HEADER_STRING));
        }
    
    }
    

    前两个测试通过,第三个测试失败,这是出乎意料的。它总是返回:

    MockHttpServletRequest:
          HTTP Method = POST
          Request URI = /login
           Parameters = {}
              Headers = {Content-Type=[application/json]}
                 Body = <no character encoding set>
        Session Attrs = {}
    
    Handler:
                 Type = null
    
    Async:
        Async started = false
         Async result = null
    
    Resolved Exception:
                 Type = null
    
    ModelAndView:
            View name = null
                 View = null
                Model = null
    
    FlashMap:
           Attributes = null
    
    MockHttpServletResponse:
               Status = 401
        Error message = Unauthorized
              Headers = {X-Content-Type-Options=[nosniff], X-XSS-Protection=[1; mode=block], Cache-Control=[no-cache, no-store, max-age=0, must-revalidate], Pragma=[no-cache], Expires=[0], X-Frame-Options=[DENY]}
         Content type = null
                 Body = 
        Forwarded URL = null
       Redirected URL = null
              Cookies = []
    2018-05-27 19:56:24.868  INFO 8949 --- [    Test worker] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet ''
    2018-05-27 19:56:24.868  INFO 8949 --- [    Test worker] o.s.t.web.servlet.TestDispatcherServlet  : FrameworkServlet '': initialization started
    2018-05-27 19:56:24.872  INFO 8949 --- [    Test worker] o.s.t.web.servlet.TestDispatcherServlet  : FrameworkServlet '': initialization completed in 4 ms
    
    MockHttpServletRequest:
          HTTP Method = POST
          Request URI = /login
           Parameters = {}
              Headers = {Content-Type=[application/json]}
                 Body = <no character encoding set>
        Session Attrs = {}
    
    Handler:
                 Type = null
    
    Async:
        Async started = false
         Async result = null
    
    Resolved Exception:
                 Type = null
    
    ModelAndView:
            View name = null
                 View = null
                Model = null
    
    FlashMap:
           Attributes = null
    
    MockHttpServletResponse:
               Status = 401
        Error message = Unauthorized
              Headers = {X-Content-Type-Options=[nosniff], X-XSS-Protection=[1; mode=block], Cache-Control=[no-cache, no-store, max-age=0, must-revalidate], Pragma=[no-cache], Expires=[0], X-Frame-Options=[DENY]}
         Content type = null
                 Body = 
        Forwarded URL = null
       Redirected URL = null
              Cookies = []
    
    Status expected:<200> but was:<401>
    Expected :200
    Actual   :401
    

    我不确定/login端点来自何处,但我很确定它不应该像现在一样经过身份验证,否则,如何登录?

    我想我对春天安全的理解不足是她的错,有人能看出我做错了什么吗?

    我以前在不同的设置上问过类似的问题-答案几乎没有问题,所以我再次尝试。

    1 回复  |  直到 7 年前
        1
  •  3
  •   Tom    7 年前

    默认情况下,Spring会生成一个基本的表单登录。您需要在Web安全中禁用它,如下所示:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable().authorizeRequests()
                .antMatchers(SIGN_UP_URL).permitAll()
                .antMatchers(LOGIN_URL).permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilter(new AuthNFilter(authenticationManager()))
                .addFilter(new AuthZFilter(authenticationManager()))
                // this disables session creation on Spring Security
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().formLogin().disable();
    }
    

    编辑: 经过一些调试,我发现了错误。

    1. 你嘲笑了 UserRepository 但不是方法,所以 findByUsername 将始终返回空值。我把它移到 对HSQL使用真正的存储库。

    2. 用户始终处于锁定状态。

      @Override
      public boolean isAccountNonLocked() {
          return false; //changed it to true
      } 
      
    3. 密码编码器只支持来自bcyrpt的$2A$版本,而不支持$2Y$版本。

    更改这些之后,测试将毫无错误地运行。