스프링 세션방식을 이용한 인증/인가

2023. 8. 10. 15:04Technology[Back]

로그인을 통한 인증/인가는 세션방식와 JWT 방식을 사용할 수 있습니다.

세션방식 : 백앤드 서버의 메모리 상에 세션정보를 저장하기 때문에 보안이 우수하고 세션이 탈취당하기 쉽지 않기 때문에 별도의 보안로직이 필요하지 않는 등 JWT에 비해 구현이 상대적으로 쉽다는 장점이 있습니다. 다만 Stateful하기 때문에 백앤드 서버가 종료되면 로그인 정보는 유실될 수 있고 운영서비스의 경우 사용자가 많을 시 서버에 부담이 될 수 있다는 단점이 존재합니다.

JWT방식 : 백앤드서버에서는 로그인 정보를 저장하지 않고 모두 암호화된 토큰정보로 저장하여 프론트에게 넘기기 때문에 Stateless해서 백앤드 서버가 종료되거나 로드밸런싱을 통한 다른 백앤드 서버에 요청하더라고 로그인 정보는 유실되지 않고 사용자가 많이 몰릴 때에도 서버에서 메모리 상에 저장하지 않기 때문에 서버의 부담이 적은 방식입니다. 다만 프론트서버에 로그인 정보가 저장되고 해당 토큰이 탈취당하면 백앤드 입장에서 알 수 없기 때문에 보안상 위험성이 높아 토큰은 반드시 암호화를 해야하고 Access 토큰과 별도로 Refresh 토큰을 이용하여 보안을 강화할 필요가 있습니다.

이번에는 세션 방식에 의한 인증 및 인가방식에 대해 알아보겠습니다.

1. WebConfig

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
        		// CORS 허용 Origins
                .allowedOrigins("[ 통신을 허용할 Origin ]")
                .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE", "HEAD")
                // 쿠키 허용
                .allowCredentials(true)
                .exposedHeaders("Token", "Access-Control-Allow-Origin");
    }
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthLoginInterceptor())
                .addPathPatterns("/*") // 해당 경로에 접근하기 전에 인터셉터가 가로챈다.
                // session 검증 불필요한 API 요청
                .excludePathPatterns("/goods", "/login", "/sendEmail", "/checkEmail", "/sendMessage", "/checkMessage", "/signup", "/existCheck", "/updatePwd"); // 해당 경로는 인터셉터가 가로채지 않는다.
    }
}

우선 WebConfig에서 세션은 쿠키방식으로 전달되기 때문에 CORS와 Credentials를 허용해주고 addInterceptors 메서드를 오버라이드해서 인가가 필요한 요청과 인가가 필요하지 않은 요청을 설정할 수 있습니다.

 

2. Interceptor

public class AuthLoginInterceptor implements HandlerInterceptor{
	
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		// POST나 GET 요청이 아닌 Preflight 요청 등은 쿠키를 보내지 않으므로 패스
		if (request.getMethod().equals("POST") || request.getMethod().equals("GET")) {
			if(request.getSession(false) != null) {
				User user = (User) request.getSession(false).getAttribute("loginUserId");
				if(user != null) {
					return true;
				}
			}
			
            response.setStatus(HttpStatus.NOT_FOUND);
            return false; // 컨트롤러 실행을 중지하고 요청을 중단
		}
		return true;
	}
	
	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			ModelAndView modelAndView) throws Exception {
		// TODO Auto-generated method stub
		HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
	}
	
}

만약 권한인가가 필요한 작업의 경우 interceptor에서 우선검증하게 되고 세션방식이므로 기존에 생성한 세션이 존재해야하고 해당 세션내에 로그인정보가 포함되어 있으면 true를 반환해서 정상적으로 요청을 처리하게 됩니다. 이 때 POST 요청의 경우 본요청을 보내기 이전에 Preflight 요청을 보내서 요청이 보내지는지 확인을 하는데 이 경우 쿠키가 포함되지 않기 때문에 분기해서 처리하였습니다. 만약 기존에 생성한 세션이 존재하지 않거나 세션 내에 로그인정보가 존재하지 않을 경우 false를 리턴해서 해당 요청은 처리하지 않습니다.

 

3. Login / Logout

@RestController
public class UserController {

	@Autowired
    private UserService userService;
    
    @GetMapping("/login")
    @Transactional(value="txManager")
    public ResponseEntity<?> loginCheck(@RequestParam Map<String, String> param, HttpServletRequest request, HttpServletResponse response) throws Exception {
    	
    	HttpSession session = request.getSession();
    	String selected = param.get("selected");
    	
    	// 사용자가 로그인유지 선택 시 유효시간 MAX_VALUE 할당
    	if(selected.equals("1")) {
    		Cookie cookie = new Cookie("JSESSIONID", session.getId());
        	cookie.setMaxAge(Integer.MAX_VALUE);
        	response.addCookie(cookie);
    	}
    	
    	String id = param.get("id");
    	String pwd = param.get("pwd");
    	User user = new User();
    	user.setId(id);
    	user.setPwd(pwd);
    	User checkedUser = userService.existCheck(user);
    	
    	// 비밀번호 검증은 (입력받은 비밀번호 + 회원가입 시 설정된 salt값)의 인코딩값 과 DB에 저장되어있는 인코딩 결과값을 비교
    	if(checkedUser != null && encryptService.encrypt(user.getPwd() + checkedUser.getSalt()).equals(checkedUser.getPwd())) {
    		// 로그인 검증완료 시 세션에 로그인 정보 저장
    		session.setAttribute("loginUserId", checkedUser);
    		return new ResponseEntity<>("ok", HttpStatus.OK); 
    	}
    	else {
	    	session.invalidate();
	    	return new ResponseEntity<>("notFound", HttpStatus.NOT_FOUND);
    	}
    }
    
    @GetMapping("/logout")
    @Transactional(value="txManager")
    public ResponseEntity<?> logOut(HttpServletRequest request, HttpServletResponse response) throws Exception {
    	HttpSession session = request.getSession(false);
    	
    	if(session != null) {
    		session.invalidate();
    		return new ResponseEntity<>("ok", HttpStatus.OK); 
    	}
    	else {
    		return new ResponseEntity<>("notFound", HttpStatus.NOT_FOUND);
    	}
    }
}

로그인 요청이 들어오면 사용자가 입력한 id, pwd와 DB에 저장되어있는 id,pwd와 비교 후 인증이 되면 메모리상 저장되어있는 session에 로그인 정보를 저장해놓습니다. 반대로 로그아웃 요청이 들어오면 메모리상 저장되어있는 session을 invalidate 메서드를 이용해서 더 이상 해당 세션을 사용할 수 없도록 합니다.

 

이상으로 세션방식을 통한 인증/인가 방식을 알아보았습니다.

지금까지 읽어주셔서 감사합니다.