【问题标题】:How to return HTTP 403 after successful authentication, but unsuccessful authorization?认证成功但授权失败后如何返回HTTP 403?
【发布时间】:2016-07-25 11:40:15
【问题描述】:

我有一个也使用 Spring Security 的 Spring Boot 应用程序。我想检查用户是否有权登录应用程序,但必须在身份验证之后。关键是,在登录时,用户选择他们必须连接的项目。一个用户可以被允许连接到一个项目,但不能被允许连接到另一个项目。但是,如果用户输入了无效凭据,即使用户无权登录所选项目,也必须首先显示有关无效凭据的消息。因此,检查项目的权限必须在认证之后。

这是我的 SecurityConfig 类:

package org.aze.accountingprogram.config;

import org.aze.accountingprogram.models.CurrentUser;
import org.aze.accountingprogram.models.PermissionAliasConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.encoding.Md5PasswordEncoder;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

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

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests().antMatchers("/lib/**").permitAll().anyRequest().fullyAuthenticated()
                .and()
                .formLogin().successHandler(successHandler()).loginPage("/login").permitAll()
                .and().exceptionHandling().accessDeniedHandler(accessDeniedHandler())
                .and()
                .logout().logoutUrl("/logout").logoutSuccessUrl("/login").permitAll();

        http.csrf().disable();
    }

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

    private AccessDeniedHandler accessDeniedHandler() {
        return (request, response, e) -> {
            logger.debug("Returning HTTP 403 FORBIDDEN with message: \"{}\"", e.getMessage());
            response.sendError(HttpStatus.FORBIDDEN.value(), e.getMessage());
        };
    }

    private AuthenticationSuccessHandler successHandler() {
        return new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                CurrentUser user = (CurrentUser) authentication.getPrincipal();
                if (!user.hasAccessRight(PermissionAliasConstants.LOGIN)) {
                    throw new AccessDeniedException(String.format("User \"%s\" is not authorized to login \"%s\" project", user.getUsername(), user.getProject().getName()));
                }
            }
        };
    }

}

UserDetailsS​​ervice的实现:

package org.aze.accountingprogram.serviceimpl;

import org.aze.accountingprogram.common.Constants;
import org.aze.accountingprogram.exceptions.DataNotFoundException;
import org.aze.accountingprogram.models.AccessRightsPermission;
import org.aze.accountingprogram.models.CurrentUser;
import org.aze.accountingprogram.models.Project;
import org.aze.accountingprogram.models.User;
import org.aze.accountingprogram.service.AccessRightsService;
import org.aze.accountingprogram.service.ProjectService;
import org.aze.accountingprogram.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Autowired
    private ProjectService projectService;

    @Autowired
    private AccessRightsService accessRightsService;
    
    @Autowired
    private HttpServletRequest request;

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

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user;
        Project project;
        final String projectId = request.getParameter(Constants.SESSION_PROJECT_ID);
        logger.debug("Username: {}, projectId: {}", username, projectId);

        try {
            user = userService.getUserByUsername(username);
        } catch (DataNotFoundException e) {
            throw new UsernameNotFoundException(e.getMessage(), e);
        }

        // Value of projectId is from combo box which is filled from table of projects
        // That is why there is no probability that the project will not be found
        project = projectService.getProjectById(Integer.valueOf(projectId));

        // User can have different rights for different projects
        List<AccessRightsPermission> accessRights = accessRightsService.getAccessRightsByProject(user.getId(), project.getId());
        Set<GrantedAuthority> authorities = new HashSet<>(accessRights.size());
        authorities.addAll(accessRights.stream().map(right -> new SimpleGrantedAuthority(right.getAlias())).collect(Collectors.toList()));

        final CurrentUser currentUser = new CurrentUser(user, project, authorities);

        // If to check LOGIN access right to project here, and user entered right credentials
        // then user will see message about invalid credentials.
        // .and().exceptionHandling().accessDeniedHandler(accessDeniedHandler()) at SecurityConfig is not working in this case
//        if (!currentUser.hasAccessRight(PermissionAliasConstants.LOGIN)) {
//            throw new AccessDeniedException(String.format("User \"%s\" is not authorized to login \"%s\" project", user.getUsername(), project.getName()));
//        }

        logger.info("Logged in user: {}", currentUser);
        return currentUser;
    }
}

和登录控制器

package org.aze.accountingprogram.controllers;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

import java.util.Optional;

@Controller
public class LoginController {

    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public ModelAndView getLoginPage(@RequestParam Optional<String> error) {
        return new ModelAndView("login", "error", error);
    }

}

successHandler() 有效,如果用户无权登录项目,应用程序会抛出 AccessDeniedException。但是accessDeniedHandler() 不起作用,也没有发送 HTTP 403。相反,我收到了 HTTP 500。

如何返回 HTTP 403 响应和异常消息(例如“用户“tural”无权登录“AZB”项目”)并在LoginController 中处理它(使用@ResponseStatus(HttpStatus.FORBIDDEN)@ExceptionHandler) ?

【问题讨论】:

  • 不要在成功处理程序中检查权限。而是编写一个安全表达式来验证用户是否可以访问该项目。您的用户服务不应该依赖于请求/会话变量。它应该只获得具有其权限的用户。你应该写一个安全表达式来限制他访问他不允许访问的东西。您基本上是在使用 Spring Securiyt 而不是使用它。

标签: java spring spring-security


【解决方案1】:

不确定这是否是您要查找的内容,但您可以在 Controller 的登录 POST 方法中注入 HttpServletResponse

因此,如果您的服务通知您授权不正确,您可以例如这样做

response.setStatus(403);

@RequestMapping(value = "/login", method = RequestMethod.POST)
public ModelAndView loginAction(@RequestParam String login, @RequestParam String pw, HttpServletResponse response) {
   doStuff(login,pw);
   if(service.isNotHappy(login,pw)){
      response.setStatus(403);
   }else{
      // happy flow
   }
   ...
}

// 更新:

自从

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException 

方法是org.springframework.security.core.userdetails.UserDetailsService 接口的实现,实际上你不能像在@RequestMapping 注释控制器中那样注入HttpServletResponse

但是,这是另一个(希望是正确的!)解决方案:

1)在loadUserByUsername(String x)方法的实现中,如果用户没有访问权限,抛出一个CustomYourException

2) 在class level 创建一个用@ControllerAdvice 注释的新ExceptionHandlingController 类(并确保它像您的控制器一样被扫描),其中包含这样的方法:

@ControllerAdvice
public class ExceptionHandlingController {

   @ExceptionHandler({CustomYourException.class})
   public ModelAndView handleCustomExceptionError(HttpServletRequest request, HttpServletResponse response, CustomYourException cyee) {
       // this method will be triggered upon CustomYourException only
       // you can manipulate the response code here, 
       // return a custom view, use a special logger
       // or do whatever else you want here :)
   }
}

如您所见,将根据您在 @ExceptionHandler 注释中定义的特定异常调用此处理程序 - 非常有用! 从那里,您可以操作 HttpServletResponse 并设置所需的响应代码,和/或将用户发送到特定视图。

希望这能解决您的问题:)

【讨论】:

  • 我没有 @RequestMapping(value = "/login", method = RequestMethod.POST) 的处理程序。 Spring Security 处理对 /login 本身的 POST 请求。我从这里配置了类似于示例的身份验证:spring.io/guides/gs/securing-web
  • 用新的解决方案更新了答案,请查看:)
  • 我在写这个问题之前尝试了这个解决方案。 loadUserByUsername 在检查密码之前运行。因此,在这种情况下,如果用户无权访问项目并输入了无效的用户名或密码,则应用程序会说,用户无权访问项目,但应该说,用户输入了无效的凭据。这意味着,授权将出现在身份验证之前。
  • 当两个用户的用户名相似但权限不同的项目时,这可能会导致问题。如果一个用户犯了错误,不小心输入了另一个类似的用户名(数据库中存在),该用户名没有项目的权限,应用程序会混淆他说他没有项目的权限,而不是说他有输入了错误的密码。
【解决方案2】:

也许你应该在response.sendError()

之后返回;

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-11-05
    • 2018-10-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-23
    • 2015-02-02
    • 1970-01-01
    • 2019-06-05
    相关资源
    最近更新 更多