Member의 Interceptor에는 로그인과 권한만 일치한다면, 모든 요청을 통과시켰습니다. 
하지만 안드로이드나, web페이지에서 요청을 할 수 있는 버튼만 제공하지 않는다면 안전할까요??


-> 정답은 아닙니다. 서버의 IP와 URI, 같은 권한을 가진 로그인 사용자가 접근한다면 다른 사용자의 DB를 이용할 수 있게 됩니다. 이러한 상황을 막기 위해 TeamInterceptor에는 검증하는 로직을 추가합니다.  MemberInterceptor에도 마찬가지로 추가해야 합니다. 여러분이 실습할 수 있는 시간을 드리기 위해 추가하지는 않겠습니다.

 

1. TeamController, TeamControllerAdvice

@RestController
@RequiredArgsConstructor
@RequestMapping("/team")
public class TeamController {

    private final TeamService teamService;
    @MethodLog
    @GetMapping("/")
    public RestTeamDto findByName(@RequestParam(name =" teamName") String teamName){
        return teamService.findTeamByName(teamName);
    }
    @MethodLog
    @PostMapping("/")
    public Long createTeam(@RequestBody TeamDto teamDto, @LoginUser SessionDto sessionDto){
        return teamService.createTeam(teamDto, sessionDto.getId());
    }

    @MethodLog
    @GetMapping("/rest")
    public List<RestTeamDto> findRestTeam(){
        return teamService.findRestTeamSeat();
    }

    @MethodLog
    @DeleteMapping("/{teamId}")
    public String deleteTeam(@PathVariable(name = "teamId") Long teamId){
        teamService.deleteTeam(teamId);
        return "ok";
    }

    @MethodLog
    @PatchMapping("/{teamId}")
    public Long updateTeam(@RequestBody TeamDto teamDto, @PathVariable(name = "teamId") Long teamId
            , @LoginUser SessionDto sessionDto){
        return teamService.updateTeam(teamDto,teamId, sessionDto.getId());
    }

    @MethodLog
    @PostMapping("/request/{requestId}")
    public String acceptMember(@PathVariable(name = "requestId") Long requestId){
        teamService.acceptMember(requestId);
        return "ok";
    }

    @MethodLog
    @GetMapping("/request/{teamId}")
    public List<RequestTeamDto> requestList(@PathVariable(name = "teamId") Long teamId){
        return teamService.findRequestList(teamId);
    }

    @MethodLog
    @PostMapping("/league/{leagueId}")
    public Long requestLeague(@PathVariable(name= "leagueId") Long leagueId,
                              @RequestParam(name = "teamId") Long teamId){
        return teamService.requestLeague(teamId,teamId);
    }

    @MethodLog
    @DeleteMapping("/league/{leagueId}")
    public String withdrawLeague(@PathVariable(name = "leagueId")Long leagueId, @RequestParam(name = "teamId") Long teamId){
        teamService.withdrawalLeagueRequest(teamId,leagueId);
        return "ok";
    }
}
@RestControllerAdvice(assignableTypes = TeamController.class)
public class TeamControllerAdvice {


    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorDto alreadyTeam(AlreadyTeamException e){
        return new ErrorDto(e.getMessage());
    }

    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorDto alreadyAccept(AlreadyAcceptMemberException e){
        return new ErrorDto(e.getMessage());
    }

    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorDto notMatchingTeamName(NotMatchingTeamNameException e){
        return new ErrorDto(e.getMessage());
    }

    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorDto duplicateRequest(DuplicateRequestToLeagueException e){
        return new ErrorDto(e.getMessage());
    }

    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorDto duplicateTeamName(DuplicateTeamNameException e){
        return new ErrorDto(e.getMessage());
    }

    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorDto notExistRequest(NotExistRequestException e){
        return new ErrorDto(e.getMessage());
    }
}
이전에 설명한 개념들이기 때문에 설명은 생략하겠습니다. 

 

2. TeamInterceptor (코드가 복잡하니 설명 참조바랍니다.)

@Component
@RequiredArgsConstructor
public class TeamInterceptor implements HandlerInterceptor {

    private final UserRepository userRepository;
    private final TeamRepository teamRepository;
    private final RequestTeamRepository requestTeamRepository;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession(false);

        if (session == null) {
            response.sendRedirect("/err/team/noTeam");
            return false;
        }
        SessionDto sessionUser = (SessionDto) session.getAttribute(SessionConst.LOGIN_MEMBER);
        String requestURI = request.getRequestURI();

        if(requestURI.equals("/team/") && request.getMethod().equals("GET")){
            return true;
        }

        if (sessionUser.getRole() != ROLE.TEAM_FOUNDER) {
            response.sendRedirect("/err/team/noAuthority");
            return false;
        }

        User user = userRepository.findById(sessionUser.getId()).get();
        if(requestURI.equals("/team/")){
            return true;
        }
        else if(requestURI.contains("/team/request")){
            long pathVariable = Long.parseLong(requestURI.substring(14));
            if(request.getMethod().equals("GET")){
                Team team = teamRepository.findById(pathVariable).get();
                if(team.getEmail().equals(user.getEmail())){
                    return true;
                }
                else{
                    response.sendRedirect("/err/team/notAuthority");
                    return false;
                }
            }
            else{
                Optional<RequestTeam> requestTeam = requestTeamRepository.findFetchTeamById(pathVariable);
                if(requestTeam.isEmpty()){
                    response.sendRedirect("/err/team/notExistRequest");
                    return false;
                }
                else{
                    String teamFounderEmail = requestTeam.get().getTeam().getEmail();
                    if(teamFounderEmail.equals(sessionUser.getEmail())){
                        return true;
                    }
                    else{
                        response.sendRedirect("/err/team/notAuthority");
                        return false;
                    }
                }
            }
        }
        else if(requestURI.contains("/team/league")){
            long teamId = Long.parseLong(request.getParameter("teamId"));
            Team team = teamRepository.findById(teamId).get();
            if(team.getEmail().equals(sessionUser.getEmail())){
                return true;
            }
            else{
                response.sendRedirect("/err/team/notAuthority");
                return false;
            }
        }
        else{
            long teamId = Long.parseLong(requestURI.substring(6));
            Team team = teamRepository.findById(teamId).get();
            if(team.getEmail().equals(user.getEmail())){
                return true;
            }
            else{
                response.sendRedirect("/err/team/notAuthority");
                return false;
            }
        }
    }
}
  • TeamController에 대한 요청이므로 로그인이 되어있어야 합니다. session을 통해 정보를 얻고 없다면 요청을 ErrorController로 redirect 합니다.
  • 마찬가지로 로그인을 했는데 역할이 TeamFounder가 아니라면 ErrorController로 redirect 합니다.
  • request.getRequestURI: ip:port/~~ port뒤에 부분이 String 값으로 반환됩니다.
  • requestURI=="/team/new"라면 TeamFounder의 역할만 가진다면 통과시킵니다.
  • requestURI=="/team/request"라면 2가지 Method로 나뉩니다.

        1. Get: Team에 신청한 리스트를 확인하는 Controller에 대한 접근입니다. 접근하는 사용자가 Team을 만든 사람인 지에 대한 검증을 추가합니다. 

        2. Post: Team에 들어온 신청을 수락하는 Controller에 대한 접근입니다. 실제 요청이 존재하는지 판별한 후 Get과 마찬가지로 접근하는 사용자가 Team을 만든 사람인지에 대한 검증을 추가합니다.

  • requestURI=="/team/league"라면 request.getParameter("teamId")를 이용하여 teamId를 가져오며, 마찬가지로 검증 로직을 넣습니다.
  • 나머지 요청에 대한 것은 팀의 수정과 팀의 삭제이므로 마찬가지로 검증 로직을 넣습니다. 

3. WebConfig를 다음과 같이 수정합니다.

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

    private final UserRepository userRepository;
    private final TeamRepository teamRepository;
    private final RequestTeamRepository requestTeamRepository;
    private final RequestLeagueRepository requestLeagueRepository;
    private final LeagueRepository leagueRepository;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new ArgumentResolver());
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MemberInterceptor(userRepository))
                .addPathPatterns("/member/**")
                .excludePathPatterns("/signup","/login");

        registry.addInterceptor(new TeamInterceptor(userRepository,teamRepository,requestTeamRepository))
                .addPathPatterns("/team/**")
                .excludePathPatterns("/team/","/team/rest");
    }
}
  • "/team/**" 모든 /team/~ 로 되는 URL에 TeamInterceptor를 적용합니다.
  • 단 "/team", "/team/rest"는 모든 사용자가 이용할 수 있어야 하므로 exclude 해줍니다.

 

4. PostMan Test

순서
1. Member 회원가입
2. 1번 TeamFounder 회원가입
3. 2번 TeamFounder 회원가입
4. 1번 TeamFounder 로그인하여 session 획득
5. 1번 TeamFounder의 팀 생성
6. Member 로그인하여 session 획득
7. 1번 TeamFounder가 생성한 팀에 가입신청
8. 2번 TeamFounder 로그인하여 session획득
9. 2번 TeamFounder가 올바른 접근 방식이 아닌 URL을 이용하여 Member가 신청한 request를 수락하려 하지만 권한이 없어 실패
10. 1번 TeamFounder가 로그인하여 session획득
11. 1번 TeamFounder가 Member 신청항 request를 수락함

 

1. Member 회원가입

2. 1번 TeamFounder 회원가입 

3. 2번 TeamFounder 회원가입

4. 1번 TeamFounder 로그인하여 session 획득

5. 1번 TeamFounder 팀 생성

6. Member 로그인하여 session 획득

7. 1번 TeamFounder가 생성한 팀에 가입신청

8. 2번 TeamFounder 로그인하여 session획득

9. 2번 TeamFounder가 올바른 접근 방식이 아닌 URL을 이용하여 Member가 신청한 request를 수락하려 하지만 권한이 없어 실패

10. 1번 TeamFounder가 로그인하여 session 획득

11. 1번 TeamFounder는 Member가 신청한 요청을 수락함

 

지금까지 TeamControlle, TeamAdvice와 MemberInterceptor와 다르게 올바르지 않은 URL 접근을 차단하는 TeamInterceptor을 만들어봤습니다. TeamInterceptor의 내부 로직의 경우 다양한 방법으로 제어 가능합니다. 각자가 생각하는 효율적인 코드로 작성하시면 좋을 거 같습니다. 위에서 말씀드렸듯이 MemberInterceptor 또한 이처럼의 접근 제한을 하는 로직을 추가해야 합니다. 여러분이 직접 추가해보시면 성장하는 시간을 가질 수 있을 거 같습니다. 감사합니다.

 

모든 코드는 아래 링크에서 확인 가능합니다.

https://github.com/rlaehdals/blogProject

 

GitHub - rlaehdals/blogProject

Contribute to rlaehdals/blogProject development by creating an account on GitHub.

github.com