使用 Spring 和 Spring 安全性访问拒绝和允许功能

Posted

技术标签:

【中文标题】使用 Spring 和 Spring 安全性访问拒绝和允许功能【英文标题】:Access Deny and Allow Functionality using Spring and Spring security 【发布时间】:2020-02-29 21:12:39 【问题描述】:

目前我正在尝试使用 spring MVC 和带有 spring security 的 spring boot 来实现身份验证示例。在我的示例应用程序中,我要做的是 - 我在一个 URL 的标头中发送一个身份验证令牌。我需要从 URL 中获取此身份验证令牌并进行解码。如果用户名和密码匹配,则只需将控制权转移到端点“api/getStudent/v1”或类似的东西。否则从那里只需要给出否认的回应。

为此,目前我尝试使用 spring security 的身份验证提供程序。但它不适合从请求头中获取令牌。在这里我的困惑是,从 spring security 我必须在这里实现哪种方法?任何人都可以提出标准的实施方式吗?或任何此类实现的文档?

【问题讨论】:

【参考方案1】:

您似乎正在使用 REST API,您可以使用类似于此的 JWT 和自定义过滤器 (https://medium.com/@hantsy/protect-rest-apis-with-spring-security-and-jwt-5fbc90305cc5)

【讨论】:

是的。我浏览了您共享链接的文档。但它使用用户详细信息和身份验证接收不在标题中。在这里,我想要标头中的身份验证令牌,并且需要实现身份验证。你能看看这个吗?如果走错了方向,请纠正我先生?。 嗯,如何使用自定义过滤器(baeldung.com/spring-security-custom-filter),您可以通过doFilter()中的HttpServletRequest httpRequest = (HttpServletRequest) request; Enumeration<String> headerNames = httpRequest.getHeaderNames();读取标题【参考方案2】:

你需要做的就是创建一个自定义的安全过滤器,并在 spring security BasicAuthenticationFilter 之前插入这个过滤器。示例代码 -

public class CustomAuthenticationFilter extends OncePerRequestFilter 

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

        String authHeader = request.getHeaders("Authorization");
        //Decode the authHeader

        //Validate the authHeader with your username & password
        if(invalid) 
            //throw exception and abort processing
        
        filterChain.doFilter(request, response);
    

现在您可以创建 bean 或将其设为 @component,以便 spring 将其拾取并为您创建 bean。

在您的安全配置中,添加以下 -

@Configuration
public class CustomWebSecurityConfig extends WebSecurityConfigurerAdapter 

    @Override
    protected void configure(HttpSecurity http) throws Exception 

        http.addFilterAfter(new CustomAuthenticationFilter(), BasicAuthenticationFilter.class);
    

【讨论】:

如果您正在寻找任何进一步的实施细节,请在此处告诉我。 此方法不允许将此过滤器仅应用于关节端点(例如 api/getStudent/v1)。此外,Spring Security 不会以这种方式处理身份验证,并且不会有与当前线程关联的身份验证对象。方法级别的安全性(@PreAuthorize@PostAuthorize)也不可能。 @EvgeniyKhyst 如果过滤器的顺序在SecurityContextPersistenceFilter 之后,此过滤器从 SecurityContextRepository 获取现有的安全上下文。在用户定义的过滤器中,可以将身份验证对象设置到安全上下文中。这样您对方法级别安全性@PreAuthorize@PostAuthorize 的担忧也可以得到解决。在自定义过滤器中,验证标头后,您可以将身份验证对象设置为SecurityContextHolder.getContext().setAuthentication(authentication);,您的身份验证对象可以在其中授予权限。 @EvgeniyKhyst 我不确定您为什么对答案投了反对票。关于第一点,我们总是可以使用 spring security 的 authorizeRequests() 和 antMatchers() 来定义哪个端点受保护,哪个不受保护。不知道为什么我们需要编写这么多代码(您已经编写过)来定义这个。其次,虽然不会有身份验证对象,但我们可以很容易地创建它并传递它。 IMO,答案缺少if (invalid) /* throw exception */ else SecurityContextHolder.getContext().setAuthentication(authentication); 之类的内容以及如何手动创建此authentication 对象。【参考方案3】:

我在一个 URL 的标头中发送一个身份验证令牌。我需要 从 URL 中获取此身份验证令牌并进行解码。如果用户名和 密码匹配...

通常,使用令牌进行身份验证的目的是摆脱用户名和密码检查。

开箱即用的 Spring Security 支持的基本 HTTP 身份验证假定在 HTTP 标头中传递 base64 编码的用户名和密码:例如Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l(base64 编码Aladdin:OpenSesame)。

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter 

    @Override
    protected void configure(HttpSecurity http) throws Exception 
        http.authorizeRequests()
          .antMatchers("/public").permitAll()
          .anyRequest().authenticated()
          .and()
          .httpBasic();
    

如果您仍需要以其他方式从令牌中提取用户名和密码,请考虑以下示例。

考虑到您有以下 REST 控制器:

@RestController
public class TestRestController 

    @GetMapping("/api/getStudent/v1")
    public String helloWorld() 
        return "Hello, World!";
    

    @GetMapping("/info")
    public String test() 
        return "Test";
    

为了使端点/api/getStudent/v1 受保护和/info 公开,并从HTTP 请求标头中提取主体和凭据,您需要实现自定义AbstractAuthenticationProcessingFilter

public class HeaderUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter 

    public HeaderUsernamePasswordAuthenticationFilter(RequestMatcher requiresAuthenticationRequestMatcher) 
        super(requiresAuthenticationRequestMatcher);
        setAuthenticationSuccessHandler((request, response, authentication) -> 
        );
        setAuthenticationFailureHandler((request, response, exception) ->
                response.sendError(HttpServletResponse.SC_UNAUTHORIZED, exception.getMessage()));
    

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException 
        String token = request.getHeader("token");
        String username = token; //get username from token
        String password = token; //get password from token
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(username, password);
        return getAuthenticationManager().authenticate(authenticationToken);
    

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException 
        super.successfulAuthentication(request, response, chain, authResult);
        chain.doFilter(request, response);
    

此过滤器必须从标头中传递的令牌中提取主体和凭据,并尝试使用 Spring Security 进行身份验证。

接下来,您必须创建此自定义过滤器的实例并配置 Spring Security 以将过滤器添加到安全过滤器链中(.addFilterBefore(authenticationFilter(), UsernamePasswordAuthenticationFilter.class)):

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter 

    @Bean
    public HeaderUsernamePasswordAuthenticationFilter authenticationFilter() throws Exception 
        HeaderUsernamePasswordAuthenticationFilter authenticationFilter =
                new HeaderUsernamePasswordAuthenticationFilter(new AntPathRequestMatcher("/api/**"));
        authenticationFilter.setAuthenticationManager(authenticationManagerBean());
        return authenticationFilter;
    

    @Override
    protected void configure(HttpSecurity http) throws Exception 
        http.sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .csrf().disable()
                .addFilterBefore(
                        authenticationFilter(),
                        UsernamePasswordAuthenticationFilter.class);
    

    //...

让过滤器知道 Spring Security authenticationManagerBean: authenticationFilter.setAuthenticationManager(authenticationManagerBean()); 很重要。

您可以通过传递RequestMatcher 来配置通过身份验证保护哪些端点:例如new AntPathRequestMatcher("/api/**").

为了测试,你可以在内存中创建UserDetailsService并使用用户名test、密码test和权限admin进行测试:

public class WebSecurityConfig extends WebSecurityConfigurerAdapter 

    //...

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

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception 
        auth.inMemoryAuthentication()
            .withUser("test")
            .password(passwordEncoder().encode("test"))
            .authorities("admin");
    

运行应用程序并尝试在没有身份验证的情况下访问公共端点:

curl -i http://localhost:8080/info
HTTP/1.1 200 
Test

没有身份验证的受保护端点:

curl -i http://localhost:8080/api/getStudent/v1 
HTTP/1.1 401

没有无效令牌的受保护端点:

curl -i http://localhost:8080/api/getStudent/v1 -H 'token: not_valid'
HTTP/1.1 401

最后是带有有效令牌的受保护端点:

curl -i http://localhost:8080/api/getStudent/v1 -H 'token: test'
HTTP/1.1 200 
Hello, World!

【讨论】:

我想你忘记了 OP 正在处理没有与每个请求相关联的会话的无状态请求。您对 UsernamePasswordAuthenticationFilter 的实现和对 rest API 的身份验证成功处理程序的实现看起来像是让更简单的事情变得更复杂。对于无状态请求,简单的过滤器(OncePerRequestFilter impl of order before BasicAuthFilter)从标头中读取令牌并将身份验证对象设置为上下文或拒绝访问就足够了。 在问题中提到:“如果用户名和密码匹配”。 Spring Security 的标准机制是DaoAuthenticationProviderUserDetailsServicePasswordEncoder 没有基于token的认证标准鼓励在token中发送密码,实际上BasicAuthentication是一个例外但它是浏览器支持的标准协议,它使用base64Encoder进行编码,但事实是BasicAuthentication属于不同的应用程序用户只能进行身份验证过程的类别。 & 对于基于令牌/无状态的身份验证,它不需要由 UsernamePasswordAuthenticationFilter 完成的所有进程,它在后台执行创建会话,您可能也需要这样做,并且不需要成功处理程序。 谢谢您的回复先生。让我试试这段代码。现在我只是尝试了 Akash 给出的简单代码。那是有效的。让我尝试添加您的代码。其冗长的代码。让我补充一下。【参考方案4】:

您可以尝试以下方法。我在这里使用了 JWT 身份验证。根据您的问题,您可以使用 spring 的 @Preauthorize 注释对端点“api/getStudent/v1”进行预授权。 以下是将用户引导至登录的终点。

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

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

    SecurityContextHolder.getContext().setAuthentication(authentication);

    String jwt = jwtProvider.generateJwtToken(authentication);

    UserPrinciple userPrinciple = (UserPrinciple) authentication.getPrincipal();
    String name = userRepo.findById(userPrinciple.getId()).get().getName();

    return ResponseEntity.ok(new JwtResponse(jwt, userPrinciple.getUsername(),
            userPrinciple.getAuthorities(),name,userPrinciple.getGender()));

下面是 WebSecurityConfig

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
        prePostEnabled = true
)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter 
    @Autowired
    UserDetailsServiceImpl userDetailsService;

    @Autowired
    private JwtAuthEntryPoint unauthorizedHandler;

    @Bean
    public JwtAuthTokenFilter authenticationJwtTokenFilter() 
        return new JwtAuthTokenFilter();
    

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

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

    @Bean
    public AuthorizationRequestRepository<OAuth2AuthorizationRequest> customAuthorizationRequestRepository() 
        return new HttpSessionOAuth2AuthorizationRequestRepository();
    

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



下面的JWTProvider类包含了生成JWT令牌的方法。(注意:我已经设置了每个用户的email作为用户名,你可以根据自己的意愿去做)

@Component
public class JwtProvider 

    @Autowired
    UserRepository userRepo;

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

    public String generateJwtToken(Authentication authentication) 

        UserPrinciple userPrincipal = (UserPrinciple) authentication.getPrincipal();
        String name = userRepo.findById(userPrincipal.getId()).get().getName();

        return Jwts.builder()
                        .setSubject((userPrincipal.getUsername())) //getUsername returns the email
                        .claim("id",userPrincipal.getId() )
                        .claim("name",name)
                        .setIssuedAt(new Date())
                        .setExpiration(new Date((new Date()).getTime() + EXPIRATION_TIME))
                        .signWith(SignatureAlgorithm.HS512, SECRET)
                        .compact();
    

    public String generateJwtToken(UserPrinciple userPrincipal) 

        String name = userRepo.findById(userPrincipal.getId()).get().getName();

        return Jwts.builder()
                .setSubject((userPrincipal.getUsername())) //getUsername returns the email
                .claim("id",userPrincipal.getId() )
                .claim("name",name)
                .setIssuedAt(new Date())
                .setExpiration(new Date((new Date()).getTime() + EXPIRATION_TIME))
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .compact();
    

    public boolean validateJwtToken(String authToken) 
        try 
            Jwts.parser().setSigningKey(SECRET).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(SECRET)
                            .parseClaimsJws(token)
                            .getBody().getSubject();
    

以下是在 WebSecurityConfig 类中启动的 JWTAuthTokenFilter 类。这是它从 rquest 解码令牌并检查令牌是否有效的地方

public class JwtAuthTokenFilter extends OncePerRequestFilter 

    @Autowired
    private JwtProvider tokenProvider;

    @Autowired
    private UserDetailsServiceImpl 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 email = tokenProvider.getUserNameFromJwtToken(jwt);//returns the email instead of username

                UserDetails userDetails = userDetailsService.loadUserByUsername(email);
                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;
    


以下是 JWTAuthEntryPoint 。检查 WebSecurityConfig 类以了解此类的使用

@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");
    

以下是我为约束创建的类

public class SecurityConstraints 
    public static final String SECRET = "********";//add any secret you want
    public static final long EXPIRATION_TIME = 864_000_000L;

【讨论】:

请务必要求澄清 你试过了吗?

以上是关于使用 Spring 和 Spring 安全性访问拒绝和允许功能的主要内容,如果未能解决你的问题,请参考以下文章

Spring security 和 spring data :安全访问不属于当前用户的数据

使用 MySQL 和 JPA 提供 403 访问被拒绝的 Spring 自定义安全性

会话创建和会话销毁事件中的 Spring 安全访问用户详细信息

Spring 安全性不允许使用简单的匹配器和 permitAll 进行访问

使用spring-boot对rest服务进行访问控制

Spring安全中的Oauth2客户端