当前位置: 首页 > 知识库问答 >
问题:

Spring Security JWT REST API返回401

应子真
2023-03-14

我使用Spring Boot 2和Spring Security有一个相对简单的设置,我使用JWT基本上让用户登录。

完整的项目在这里:http://github.com/mikeycoxon/spring-boot-2-security-jwt

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

我有一个AuthNFilter:

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中设置用户

为此,我设置了一个WebSecurity配置适配器,以便覆盖spring security的默认设置:

@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\u URL=/api/user,是一个POSTLOGIN\u URL=spring自己的/LOGINendpoint

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

@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

我不确定/loginendpoint来自哪里,但我非常确定它不应该像现在这样进行身份验证,否则任何人如何登录?

我认为我对Spring安全缺乏理解是她的错,有人能看出我做错了什么吗?

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

共有1个答案

郝玄天
2023-03-14

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

@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();
}

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

>

  • 您已经嘲笑了UserRepository,但没有嘲笑该方法,因此findByUsername将始终返回null。我已经删除了它,以便对hsql使用真正的存储库。

    用户始终处于锁定状态。

    @Override
    public boolean isAccountNonLocked() {
        return false; //changed it to true
    } 
    

    更改这些后,测试运行没有错误。

  •  类似资料:
    • 问题内容: 例如我有一个功能: 我怎样才能返回AJAX后得到的? 问题答案: 因为请求是异步的,所以您无法返回ajax请求的结果(而同步ajax请求是一个 糟糕的 主意)。 最好的选择是将自己的回调传递给f1 然后,您将像这样致电:

    • 问题内容: 我在使用Ajax时遇到问题。 问题是,在获得ajax响应之前,它会返回cnt。因此它总是返回NULL。 有没有办法使正确的返回响应值? 谢谢! 问题答案: 由于AJAX请求是异步的,因此您的cnt变量将在请求返回并调用成功处理程序之前返回。 我建议重构您的代码以解决此问题。 一种方法是从AJAX请求的成功处理程序中调用调用了GetGrantAmazonItemCnt()的任何函数,此方

    • 我想在下面返回JSON。 {“名字”:“杰基”} 新来的春靴在这里。1天大。有没有合适的方法可以做到这一点?

    • 问题内容: 我创建了一个自定义错误类型来包装错误,以便更轻松地在Golang中进行调试。当有打印错误时它可以工作,但是现在引起了恐慌。 演示版 当我调用一个函数时,它不会返回错误,我仍然应该能够包装该错误。 预期的行为是,如果错误为nil,则应该简单地忽略它,不幸的是,它会做相反的事情。 我希望它能打印出来。而是即使错误为nil也会打印。 问题答案: 正在将err变量与nil进行比较,但实际上它是

    • 问题内容: 有人可以向我解释为什么返回类型 只是返回类型 我不明白为什么地图会映射到一个以上的值。TIA。 问题答案: 它返回具有 相同 名称的控件的所有参数值。 例如: 要么 任何选中/选择的值都将以以下形式出现: 对于表中的多个选择它也很有用: 与…结合

    • 问题内容: 我有一类这样的方法: 我如何在另一个类中调用此方法? 问题答案: 1. 如果要从中调用该方法的类位于同一包中,则创建该类的实例并调用该方法。 2. 使用 3. 最好有个赞等等。 例如:

    • 这三个函数的返回类型提示有什么不同吗? 他们都应该有< code>- 提问的动机是这个问题,这个很好的答案,以及我正在学习类型提示的事实。

    • 我使用类型TreeMap定义了一个集合 我想返回与包含给定字符串值的列表配对的字符串(TreeMap键)。例如,我有一个字符串“bob”存储在列表中的一对中,我想返回与“bob”所在的对列表相关联的Treemap的键(字符串)。我将如何执行此操作?