Spring Security - 获取登录页面时,安全性尝试进行身份验证并返回 401 错误

Posted

技术标签:

【中文标题】Spring Security - 获取登录页面时,安全性尝试进行身份验证并返回 401 错误【英文标题】:Spring Security - when get login page, security try to authenticate and return 401 error 【发布时间】:2020-10-11 16:48:04 【问题描述】:

我正在开发具有微服务架构的 Spring Boot 应用程序。我正在使用 JWT 身份验证。

1-http://localhost:8762/auth "username":"admin", "password":"12345"(POST 请求)

2-http://localhost:8762/auth/loginPage(获取页面请求)

当我尝试第一个请求时,身份验证运行良好,我得到登录信息和 jwt 令牌。 但是当我尝试获取登录页面的第二个请求时,spring 正在尝试进行身份验证并返回 401 错误。

如何忽略登录页面的身份验证。

我有 zull 项目作为网关,身份验证项目作为 auth。

if(header == null || !header.startsWith(jwtConfig.getPrefix())) 
            chain.doFilter(request, response);          // If not valid, go to the next filter.
            return;
        

我认为在这一点上,我必须覆盖过滤器。但是我不知道我是怎么写过滤器的。

这是我的身份验证代码。

auth 项目 -> WebSecurityConfigurerAdapter

@EnableWebSecurity
public class SecurityCredentialsConfig extends WebSecurityConfigurerAdapter 

    @Autowired
    private JwtConfig jwtConfig;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception 
        http
            .csrf().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()
            // Add a filter to validate user credentials and add token in the response header
            
            // What's the authenticationManager()? 
            // An object provided by WebSecurityConfigurerAdapter, used to authenticate the user passing user's credentials
            // The filter needs this auth manager to authenticate the user.
            .addFilter(new JwtUsernameAndPasswordAuthenticationFilter(authenticationManager(), jwtConfig()))    
            .authorizeRequests()
            // allow all POST requests 
            .antMatchers("/auth/**").permitAll()
            .antMatchers("/user/register").permitAll()
            // any other requests must be authenticated
            .anyRequest().authenticated()
              .and()
              .formLogin()
              .loginPage("/auth/loginPage");
    
    
    // Spring has UserDetailsService interface, which can be overriden to provide our implementation for fetching user from database (or any other source).
    // The UserDetailsService object is used by the auth manager to load the user from database.
    // In addition, we need to define the password encoder also. So, auth manager can compare and verify passwords.
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception 
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    
    
    @Bean
    public JwtConfig jwtConfig() 
        return new JwtConfig();
    

auth -> 用户名密码验证过滤器

public class JwtUsernameAndPasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter 

    
        private AuthenticationManager authManager;
        
        private final JwtConfig jwtConfig;
        
        public JwtUsernameAndPasswordAuthenticationFilter(AuthenticationManager authManager, JwtConfig jwtConfig) 
            this.authManager = authManager;
            this.jwtConfig = jwtConfig;
            
            // By default, UsernamePasswordAuthenticationFilter listens to "/login" path. 
            // In our case, we use "/auth". So, we need to override the defaults.
            //this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(jwtConfig.getUri(), "POST"));

            this.setRequiresAuthenticationRequestMatcher(new OrRequestMatcher(                                                                                     
                    new AntPathRequestMatcher("/auth/**")
                    , new AntPathRequestMatcher("/user/register")
            ));
        
        
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
                throws AuthenticationException 
            
            try 
                
                // 1. Get credentials from request
                UserDTO creds = new ObjectMapper().readValue(request.getInputStream(), UserDTO.class);
                
                // 2. Create auth object (contains credentials) which will be used by auth manager
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                        creds.getUsername(), creds.getPassword(), Collections.emptyList());
                
                // 3. Authentication manager authenticate the user, and use UserDetialsServiceImpl::loadUserByUsername() method to load the user.
                return authManager.authenticate(authToken);
                
             catch (IOException e) 
                throw new RuntimeException(e);
            
        
        
        // Upon successful authentication, generate a token.
        // The 'auth' passed to successfulAuthentication() is the current authenticated user.
        @Override
        protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                Authentication auth) throws IOException, ServletException 
            Long now = System.currentTimeMillis();
            String token = Jwts.builder()
                .setSubject(auth.getName()) 
                // Convert to list of strings. 
                // This is important because it affects the way we get them back in the Gateway.
                .claim("authorities", auth.getAuthorities().stream()
                    .map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
                .setIssuedAt(new Date(now))
                .setExpiration(new Date(now + jwtConfig.getExpiration() * 1000))  // in milliseconds
                .signWith(SignatureAlgorithm.HS512, jwtConfig.getSecret().getBytes())
                .compact();
            
            // Add token to header
            response.addHeader(jwtConfig.getHeader(), jwtConfig.getPrefix() + token);
        

控制器

@GetMapping("/auth/loginPage")
    public String loginPage() 
        return "login";
    

【问题讨论】:

【参考方案1】:

通过这样做:

    this.setRequiresAuthenticationRequestMatcher(new OrRequestMatcher(                                                                                     
            new AntPathRequestMatcher("/auth/**")
            , new AntPathRequestMatcher("/user/register")
    ));

过滤器将对/auth/** 的任何请求进行身份验证(因此是 /auth/loginPage),并且由于您将身份验证入口点设置为仅返回 401 状态,因此您将遇到该问题。

评论一下:

.and()
    // handle an authorized attempts 
    .exceptionHandling().authenticationEntryPoint((req, rsp, e) -> rsp.sendError(HttpServletResponse.SC_UNAUTHORIZED))  

它应该将您重定向到登录页面。

PS:根据您的配置,如果我未通过身份验证并尝试访问/auth/loginPage,我将被重定向到/auth/LoginPage,,一旦我输入凭据,我将成功通过身份验证并再次重定向到同一页面/auth/loginPage

【讨论】:

我评论了这些行并收到此错误:错误:超过 maxRedirects。可能陷入重定向循环desktop-1cm5oaa.home:8763/auth/loginPage。 @SemihKoyu 我认为这是不必要的: this.setRequiresAuthenticationRequestMatcher(new OrRequestMatcher( new AntPathRequestMatcher("/auth/**") , new AntPathRequestMatcher("/user/register") ));因为您已经将其设置为 jwtConfig.getUir()【参考方案2】:

如何忽略登录页面的身份验证。

OncePerRequestFilter 有一个方法 shouldNotFilter 可以覆盖。

例如:

@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException 
    return new AntPathMatcher().match("/auth/loginPage", request.getServletPath());

【讨论】:

【参考方案3】:

我认为你的问题在JwtUsernameAndPasswordAuthenticationFilter

您也已将这一点注释掉。您正在POSTGET 上触发此过滤器。您只想为 POST 触发它。

当前方法

            this.setRequiresAuthenticationRequestMatcher(new OrRequestMatcher(                                                                                     
                    new AntPathRequestMatcher("/auth/**")
                    , new AntPathRequestMatcher("/user/register")
            ));

更新

            this.setRequiresAuthenticationRequestMatcher(new OrRequestMatcher(                                                                                     
                    new AntPathRequestMatcher("/auth/**", "POST")
                    , new AntPathRequestMatcher("/user/register", "POST")
            ));

【讨论】:

以上是关于Spring Security - 获取登录页面时,安全性尝试进行身份验证并返回 401 错误的主要内容,如果未能解决你的问题,请参考以下文章

Spring Security:如何获取自动渲染的登录页面代码?

Keycloak 登录页面未显示,我改为获取 Spring Security 登录

如何在 Spring Security 中通过 jdbc 身份验证使用自定义登录页面

使用 Spring Security 时移动和桌面的不同登录页面

如何使初始登陆页面不是spring security的登录表单?

Spring security自定义登录页面不允许我进入[关闭]