usage

[Spring] Interceptor를 이용한 인증시스템 구현

common developer 2022. 8. 4. 20:13

Spring을 사용하는 많은 개발자들이 Spring Security를 사용하여 인증 시스템을 구현한다. 하지만 Spring Security는 러닝커브가 높은 편이라서 몇몇 개발자들은 Spring Security 사용을 불편해한다. Spring Security사용이 불편한 개발자들을 위해서 Spring mvc의 Interceptor를 사용하여 인증시스템을 구현하는 방법에 대해서 포스트한다.

  • 구현코드는 여기에 저장되어 있다.

Interceptor란?

Interceptor는 spring mvc에 속한 기능으로 Dispatcher Servlet이 Controller를 호출하기 전후에 요청과 응답에 대한 참조 및 가공 기능을 제공해준다. 추가로 요청에 대해서는 통과 및 차단 기능도 제공해주므로 해당 기능을 이용하여 인증시스템을 구현할 수 있다. Interceptor의 개념에 대한 자세한 내용은 여기를 참고하기 바란다.

디렉토리 구조 및 파일기능

├── LoginSampleApplication.java
├── config
│   └── web
│       ├── SessionUser.java
│       ├── WebConfig.java
│       └── interceptor
│           ├── GuestAuthInterceptor.java
│           └── UserAuthInterceptor.java
├── controller
│   ├── LoginController.java
│   └── LoginRequestDto.java
├── domain
│   ├── Role.java
│   ├── User.java
│   └── UserRepository.java
└── service
    └── LoginService.java

디렉토리 구조는 위와 같고 주요기능를 하는 클래스을 간단히 설명한다.

  • User: 사용자정보를 저장하는 클래스이다. 사용자의 권한정보를 저장하기 위해서 Role를 참조하고 있다.
  • Role: Role은 enum클래스로 게스트와 사용자 두가지에 대한 역할을 정의하였다.
  • LoginController: 컨트롤로이며 로그인, 게스트 권한테스트, 사용자 권한테스트 api를 제공한다.
  • LoginService: 로그인로직을 구현한 서비스 클래스이다.
  • GuestAuthInterceptor: 게스트가 사용 할 수 있는 api에 대한 권한 체크를 한다.
  • UserAuthInterceptor: 유저가 사용 할 수 있는 api에 대한 권한 체크를 한다.
  • WebConfig: api와 interceptor를 맵핑시키므로서 롤에 대한 사용가능한 api를 정의한다.

User.java, Role.java

@Getter
@NoArgsConstructor
@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(nullable = false)
    private String email;
    @Column(nullable = false)
    private String pw;
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Role role;

    @Builder
    public User(String email, String pw, Role role) {
        this.email = email;
        this.pw = pw;
        this.role = role;
    }
    public User checkPw(String pw){

        if(!this.pw.equals(pw))
            throw new IllegalArgumentException("비밀번호 불일치");

        return this;
    }

}

@Getter
@RequiredArgsConstructor
public enum Role {
    GUEST("ROLE_GUEST", "손님"),
    USER("ROLE_USER", "일반 사용자");

    private final String key;
    private final String title;
}

User클래스에는 email, pw가 존재하고 해당 데이터를 통해서 사용자를 인증한다. 그리고 인증이 완료 된 사용자는 Role데이터를 통해서 사용자에 대한 권한을 참조할수 있다.

LoginController.java, LoginService.java

@Slf4j
@RequiredArgsConstructor
@RestController
public class LoginController {

    private final LoginService loginService;

    @GetMapping("/login")
    public boolean login(@RequestParam String email,
                         @RequestParam String pw,
                         HttpSession httpSession){

        try {
            SessionUser sessionUser = loginService.login(email, pw);
            httpSession.setAttribute("user", sessionUser);
            return true;
        }catch (RuntimeException exception){
            log.error("login err: " + exception.getMessage());
            exception.printStackTrace();

            return false;
        }

    }

    @GetMapping("/test/guest")
    public String guest(){

        return "hello guest";
    }

    @GetMapping("/test/user")
    public String user(){

        return "hello user";
    }
}

@RequiredArgsConstructor
@Service
public class LoginService {

    private final UserRepository userRepository;

    public SessionUser login(String email, String pw){
        User user = userRepository.findByEmail(email)
                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자"))
                .checkPw(pw);

        return new SessionUser(user);
    }
}

LoginController는 로그인 api를 제공한다. 해당 api에서 email과 pw를 받아서 LoginService의 login 메소드를 호출시킨다. login 메소드는 인증에 문제가 없으면 세션에 넣을 사용자정보를 리턴한다. LoginController는 리턴 된 사용자정보를 세션에 저장한다. 세션에 사용자정보의 정보가 있는지 없지는에 따라 인증을 했는지 하지 않았는지 판단한다.

GuestAuthInterceptor.java, UserAuthInterceptor.java

@Component
public class GuestAuthInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession httpSession = request.getSession();
        SessionUser sessionUser = (SessionUser)httpSession.getAttribute("user");
        if(sessionUser == null){
            response.setStatus(401);
            return false;
        }


        if(sessionUser.getRole() == Role.GUEST) {
            return true;
        }else{
            response.setStatus(403);
            return false;
        }


    }
}

@Component
public class UserAuthInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        HttpSession httpSession = request.getSession();
        SessionUser sessionUser = (SessionUser)httpSession.getAttribute("user");
        if(sessionUser == null){
            response.setStatus(401);
            return false;
        }

        if(sessionUser.getRole() == Role.USER) {
            return true;
        }else{
            response.setStatus(403);
            return false;
        }

    }
}

인터셉터는 디스패치 서블릿이 컨트롤러를 호출하기 전에 호출된다. 두 인터셉터는 컨트롤러가 호출되기 전 게스트 혹은 사용자에게 제공되는 api의 인증처리를 및 권한 처리를 하는 인터셉터이다. 세션에 사용자 정보가 없으면 인증이 되지 않은 사용자로 판단하여 401로 반응한다. 세션이 존재하여 세션정보를 확인했을 때 인터셉터와 맞지 않은 Role정보가 있을 때 권한이 없음으로 판단하고 403으로 반응한다.

WebConfig.java

@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {

    private final HandlerInterceptor guestAuthInterceptor;

    private final HandlerInterceptor userAuthInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(guestAuthInterceptor)
                .addPathPatterns("/test/guest")
                .excludePathPatterns();

        registry.addInterceptor(userAuthInterceptor)
                .addPathPatterns("/test/user")
                .excludePathPatterns();
    }
}

마지막으로 Role별로 제공 될 api를 해당 인터셉터와 맵핑하는 설정코드이다. 코드를 보면 엔드포인트 "/test/guest" api는 게스트 Role과 맵핑되고 "/test/user" api는 사용자 Role과 맵핑된다.

정리

인터셉터를 통해서 인증/권한 관리를 로직은 구현하는 건 그렇게 어렵지않다. 하지만 추가적인 기능이 생기다보면 유지보수가 어려울수도 있다는 문제가 있다. 자신의 상황에 맞게 Spring Security를 사용할지 Interceptor를 사용할지 선택하는 것이 중요하다고 생각한다.