【问题标题】:Spring Security, JWT (REST) based Auth flowSpring Security,基于 JWT (REST) 的身份验证流程
【发布时间】:2021-01-27 03:01:32
【问题描述】:

我正在尝试在我的SpringBoot 2.2.6 REST 后端实现JWT Token based,我想:

  • 执行FIRST 身份验证UserDetailService检索用户
  • 如果身份验证顺利生成JWT Token
  • 对于以下访问验证令牌无需访问数据库

这是我的身份验证过滤器:

public class JWTAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

  protected JWTAuthenticationFilter(String defaultFilterProcessesUrl) {
    super(defaultFilterProcessesUrl);
  }

  @Override
  public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
    ApplicationUser creds = new ObjectMapper().readValue(request.getInputStream(), ApplicationUser.class);
    
    UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
            creds.getUsername(), creds.getPassword(), Collections.emptyList());
    // retrieve user from my userdetailservice
    return getAuthenticationManager().authenticate(authToken);
  }

  @Override
  protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
        throws IOException, ServletException {
    super.successfulAuthentication(request, response, chain, authResult);
   // if Authentication goes well I generate the token and send it through the header response

    response.addHeader("Bearer ", "token");
  } 
}

这是我的SecurityConfig

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

  private final ApplicationUserDetailService userDetailService;
  private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailService).passwordEncoder(new BCryptPasswordEncoder());
  }

  @Bean
  public JWTAuthenticationFilter authenticationTokenFilterBean() throws Exception {
    JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter("/login");
    jwtAuthenticationFilter.setAuthenticationManager(authenticationManager);
    ...
    return jwtAuthenticationFilter;
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
        .cors().and()
        .csrf().disable()
        .authorizeRequests()
        .anyRequest()
        .authenticated()
        .and()
        .exceptionHandling().authenticationEntryPoint(restAuthenticationEntryPoint)
        .and()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    
    http.addFilterBefore(authenticationTokenFilterBean(), BasicAuthenticationFilter.class);
    
}

这个配置正确吗??放置另一个 OncePerRequestFilter 来解析令牌(如果存在)是个好主意,或者我应该在上面的身份验证过滤器中执行所有操作?

谢谢

------------更新---------- ----

我将尝试放置一个过滤器来检查令牌是否存在,如果存在,则对其进行验证 - 提取用户和角色并将 UsernamePasswordAuthenticationToken 放入上下文中。因此我重写AbstractAuthenticationProcessingFilterdoFilter方法如下:

public class JWTAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

  protected JWTAuthenticationFilter(String defaultFilterProcessesUrl) {
    super(defaultFilterProcessesUrl);
  }

  @Override
  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
    String header = request.getHeader(SecurityConstant.AUTH);
    if (header == null || !header.startsWith(SecurityConstant.TOKEN_PREFIX)) {
        filterChain.doFilter(request, response);
        return;
    }
    
    UsernamePasswordAuthenticationToken authentication = getAuthentication(request);

    SecurityContextHolder.getContext().setAuthentication(authentication);
    filterChain.doFilter(request, response);
}

但是,测试它,如果我调用私有路径(需要身份验证)doFilter 方法会触发两次......

我把那个过滤器放在BasicAuthenticationFilter.class..之前有没有人可以解释我怎么了??

谢谢

【问题讨论】:

    标签: rest spring-security jwt


    【解决方案1】:

    解决如下:

    调用的第一个过滤器:

    public class JWTAuthorizationFilter extends OncePerRequestFilter {
    
      @Override
      protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String header = request.getHeader(SecurityConstant.AUTH);
        if (header == null || !header.startsWith(SecurityConstant.TOKEN_PREFIX)) {
            filterChain.doFilter(request, response);
            return;
        }
        
        UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
    
        SecurityContextHolder.getContext().setAuthentication(authentication);
        filterChain.doFilter(request, response);
      }
    
      private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        String token = request.getHeader(SecurityConstant.AUTH);
    
        if (token != null) {
          // parse token
            if (user != null) {
                return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
            }
        }
        return null;
      }
    }
    

    如果路径需要身份验证且令牌不存在:

    public class JWTAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    
      private JWTUtil jwtUtil;
    
      protected JWTAuthenticationFilter(String defaultFilterProcessesUrl) {
        super(defaultFilterProcessesUrl);
      }
    
      public void setJwtUtilManager(JWTUtil jwtUtil) throws Exception {
        this.jwtUtil = jwtUtil;
      }
    
      @Override
      public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        ApplicationUser creds = new ObjectMapper().readValue(request.getInputStream(), ApplicationUser.class);
        
        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                creds.getUsername(), creds.getPassword(), Collections.emptyList());
        // authenticate with userdetailservice
        return getAuthenticationManager().authenticate(authToken);
      }
    
      @Override
      protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {
        super.successfulAuthentication(request, response, chain, authResult);
        // generate token with jwtUtil
        response.addHeader("Bearer ", "token");
      } 
    }
    

    安全配置:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .cors().and()
            .csrf().disable()
            .authorizeRequests()
            .antMatchers("/api/public").permitAll()
            .anyRequest()
            .authenticated()
            .and()
            .exceptionHandling().authenticationEntryPoint(restAuthenticationEntryPoint)
            .and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .addFilterAt(authenticationTokenFilterBean(), BasicAuthenticationFilter.class);
        
        http.addFilterBefore(authorizationTokenFilterBean(), BasicAuthenticationFilter.class);
        
    }
    

    我只有一个疑问,我这里不知道如何处理MismatchedInputException: No content to map due to end-of-input

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        ApplicationUser creds = new ObjectMapper().readValue(request.getInputStream(), ApplicationUser.class);
        
        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                creds.getUsername(), creds.getPassword(), Collections.emptyList());
        
        return getAuthenticationManager().authenticate(authToken);
    }
    

    如果我没有在请求中传递usernamepassword 参数并且ObjectMapper 抛出上述错误.. 希望有所帮助。

    反正在这里问真的没用..

    【讨论】:

    • 我建议您创建一个单独的端点来进行身份验证(而不是使用attemptAuthentication)。因为,例如,假设您收到 BadCredentials 错误,甚至是您现在遇到的错误。由于过滤器位于调度程序 servlet 之前,因此返回响应可能会变得很麻烦。即你必须重定向到/error.......最好离开它:)
    【解决方案2】:

    我建议您创建一个单独的端点来对用户进行身份验证,并使用令牌JWTAuthorizationFilter 来过滤传入的请求。推荐这个的原因之一是,例如假设您收到BadCredentials 错误,甚至是您现在遇到的错误。过滤器在DispacherServlet 之前执行,因此当出现错误时,它会在到达之前缩短电路。在这种情况下,spring mvc 会将帖子发送到/error,因此如果您设置了它,它将触发它,否则将发回默认消息。选中此项以处理 /error 端点

    @PostMapping(path = "/error")
    public ResponseEntity<ResponseBase> renderErrorResponse(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
        ResponseBase response;
        Class<ResponseBase> responseClass = ResponseBase.class;
        try {
            Object status = httpRequest.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
            //you have the status, do what ever you like with it
        }catch (Exception e) {
            //hanlde 
        }
        
        return new ResponseEntity<>(response, HttpStatus.YYYY);
    }
    

    但是,对我来说,这似乎有点可疑。所以我不喜欢使用attemptAuthentication()。而是为用户创建一个控制器以便能够获取令牌(例如在登录时)。并且所有调用都会检查一个令牌(当然除了令牌生成器和一些公共端点)。因此,现在您可以确定所有调用都会到达 DispacherServlet,它允许您根据需要自定义异常处理。

    【讨论】:

      猜你喜欢
      • 2016-03-20
      • 2022-01-15
      • 2011-08-16
      • 2016-04-30
      • 2019-02-07
      • 2013-05-13
      • 2017-07-08
      • 2017-07-10
      • 2012-06-20
      相关资源
      最近更新 更多