[ JAVA ]/JAVA Spring Security

[ Security ] Spring Security - 사용자 권한 별 인증 처리하기

환이s 2024. 12. 30. 17:54
728x90


Intro

 

안녕하세요. 환이s입니다👋

오늘은 제가 실무에서 스프링 시큐리티 프레임워크를 도입할 때 필수로 적용했던 사용자 권한 별 인증 처리 로직에 대해 포스팅을 해보려고 합니다.

 

사용자 권한 별 인증 처리는 보안 및 시스템 효율성을 위한 중요한 메커니즘입니다. 권한 관리를 통해 각 사용자에게 특정 작업을 허용하거나 제한하는 방식으로, 효율적인 시스템 운영을 할 수 있습니다.

 

이번 포스팅에서는 스프링 시큐리티의 개념과 설정에 관한 내용은 다루지 않으므로, 해당 키워드에 대한 정보가 필요하신 분들은 아래의 포스팅을 참고해 주시면 도움이 될 것입니다🙂

 

 

[ Spring ] Security 개념

Security란? 시큐리티(Security)는 소프트웨어 시스템의 보안과 관련된 개념입니다. 주로 웹 애플리케이션, 모바일 앱, 서버 등에서 사용되며 사용자 인증(Authentication), 권한 부여(Authorization), 데이터

drg2524.tistory.com

 

 

[ Security ] Spring Security 6.x 설정 방법 알아가기

Intro 안녕하세요. 환이s입니다👋 이전 회사에서 프로젝트 안정화 기간을 끝내고 이직을 결심하게 돼서 이직 준비하느라 한동안 블로그 업로드를 못하고 있었는데요..😅  현재는 이직에 성공

drg2524.tistory.com


Spring Security - 구조 설명 및 요구사항 분석

 

스프링 시큐리티는 '인증''권한'에 대한 부분을 필터흐름에 따라 처리하고 있습니다.

필터는 Dispatcher Servlet으로 가기 전에 적용되므로 가장 먼저 URL 요청을 받지만 인터셉터는 Dispatcher와 Controller 사이에 위치한다는 점에서 적용 시기의 차이가 있습니다.

 

즉, 필터는 스프링 컨테이너가 아닌 톰캣과 같은 웹 컨테이너에 의해 관리가 되는 것이고, 스프링 범위 밖에서 처리됩니다.

반대로 인터셉터는 웹 컨테이너에서 동작하는 필터와 달리 인터셉터는 스프링 컨텍스트에서 동작합니다.

Client(request) -> Filter -> DispatcherServlet -> Interceptor -> Controller

(실제로 Interceptor가 Controller로 요청을 위임하는 것은 아니다. Interceptor를 거쳐서 가는것이다.)

 

 

스프링 시큐리티 구조의 처리 과정을 간단히 설명해 보겠습니다.


 

1️⃣ 사용자가 로그인 정보와 함께 인증 요청을 보냅니다.(HTTP Request)

 

2️⃣ AuthenticationFilter가 요청을 가로채고, 가로챈 정보를 통해 UsernamePasswordAuthenticationToken의 인증용 객체를 생성합니다.

 

3️⃣ AuthenticationManager의 구현체인 ProviderManager에게 생성한 UsernamePasswordToken 객체를 전달합니다.

 

4️⃣ AuthenticationManager는 등록된 AuthenticationProvider를 조회하여 인증을 요구합니다.

 

5️⃣ 실제 DB에서 사용자 인증정보를 가져오는 UserDetailsService에 사용자 정보를 넘겨줍니다.

 

6️⃣ 넘겨받은 사용자 정보를 통해 DB에서 찾은 사용자 정보인 UserDetails 객체를 생성합니다.

 

7️⃣ AuthenticationProvider(들)은 UserDetails를 넘겨받고 사용자 정보를 비교합니다.

 

8️⃣ 인증이 완료되면 권한 등의 사용자 정보를 담은 Authentication 객체를 반환합니다.

 

9️⃣ 다시 최초의 AuthenticationFilter에 Authentication 객체가 반환됩니다.

 

🔟 Authentication 객체를 SecurityContext에 저장합니다.


최종적으로 SecurityContextHolder는 세션 영역에 있는 SecurityContextAuthentication 객체를 저장합니다. 사용자 정보를 저장한다는 것은 Spring Security가 전통적인 세션/쿠키 기반의 인증 방식을 사용한다는 것을 의미합니다.

 

그렇다면 지금부터 위 개념을 바탕으로 실제 실무에서 직접 적용했던 방법을 진행하겠습니다. 사용자 권한별 인증 처리 로직을 구현할 때, 위 처리 과정을 인지하지 못하면 구현이 다소 어려워질 수 있으며, 그만큼 더 많은 시간이 소요될 수 있습니다. 따라서 이 과정을 꼭 숙지하는 것을 권장드립니다.

 

예제 시나리오:
관리자 사용자와 일반 사용자를 구분하여 메뉴를 보여줘야 한다.


1. 사용자 인증 처리

2. 로그인 Validation 처리
3. 사용자 권한 기반 접근 제어

 

 

이제 위 예제 시나리오를 바탕으로 단계별로 살펴보겠습니다.


Spring Security - 사용자 인증 처리 로직 구현

 

먼저 스프링 시큐리티에서 사용자 인증을 처리하기 위한 커스텀 필터를 생성해 보겠습니다.

 

✅ CustomAuthenticationFilter  - 생성

public class CustomAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
	
    @Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		// TODO Auto-generated method stub
		return null;
	}
}

 

CustomAuthenticationFilter는 AbstractAuthenticationProcessingFilter를 상속받아 사용자 인증을 처리하는 필터입니다. 해당 필터를 상속받으면 실제로 인증을 시도하는 부분의 로직을 작성할 수 있는 attemptAuthentication 메서드를 생성할 수 있습니다.

앞서 처리 과정에서 언급했듯이, AuthenticationFilter에서는 UsernamePasswordAuthenticationToken을 사용하여 인증용 객체를 생성해야 합니다.

그렇다면 코드에 UsernamePasswordAuthenticationToken 인증 토큰을 생성하고, 이를 인증 관리자에게 전달하여 인증을 시도하는 코드를 추가해 보겠습니다.

 

✅ CustomAuthenticationFilter - 코드 추가

public class CustomAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
	private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());
	
    @Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		LOGGER.debug(">>>>>>>>>>>>>>>CustomAuthenticationFilter attemptAuthentication");

		String username = request.getParameter("userId");
		String password = request.getParameter("password");

		LoginVO loginVO = null;
		Authentication authentication = null;

		UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password);

		authentication = getAuthenticationManager().authenticate(authToken);
        
        return authentication;
	}
}

 

HTTP 요청에서 사용자 ID와 비밀번호를 추출하여 해당 정보를 기반으로 인증 토큰을 생성했습니다. 그리고 이 인증 토큰을 인증 관리자에게 전달하여 인증을 시도할 수 있도록 설정했습니다

 

추가로 인증이 성공하면 LoginVO 객체를 세션에 저장하여 이후의 요청에서 사용자 정보를 사용할 수 있도록 세션에 담아두겠습니다.

 

✅ CustomAuthenticationFilter - 세션 추가 

public class CustomAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
	private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());
	
    @Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		LOGGER.debug(">>>>>>>>>>>>>>>CustomAuthenticationFilter attemptAuthentication");

		String username = request.getParameter("userId");
		String password = request.getParameter("password");

		LoginVO loginVO = null;
		Authentication authentication = null;

		UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password);

		authentication = getAuthenticationManager().authenticate(authToken);
        
        	loginVO = (LoginVO) authentication.getDetails();
		request.getSession().setAttribute("loginVO", loginVO);
		
        return authentication;
	}
}

 

최종적으로 인증 결과를 반환하고 사용자 인증 처리 필터 구성을 완료했습니다.

그렇다면 해당 필터를 시큐리티 Config에 커스텀 사용자 인증 필터 생성자로 빈 등록 시켜줍니다.

 

해당 필터를 빈으로 등록해서 생성자로 설정하는 이유는 필터를 통해 인증을 처리하는 커스텀 설정을 해줘야 하기 때문입니다. 

 

예를 들어, 다음으로 구현해야 하는 인증 처리 및 로그인 validation 처리와 실패/성공에 대한 핸들러 처리 또한 추가할 수 있습니다. 

 

✅ CustomAuthenticationFilter - Security Config 추가 

	/**
	 * 커스텀 사용자 인증 필터
	 *
	 * @return CustomAuthenticationFilter
	 */
	@Bean
	CustomAuthenticationFilter customAuthenticationFilter() {
		CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
		return filter;
	}

 

생성한 필터에 인증 기능을 추가하기 위해, 사용자 인증을 처리하는 별도의 클래스를 생성합니다.

 

CustomAuthenticationManager  - 생성

public class CustomAuthenticationManager implements AuthenticationManager {

	private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());

	@Resource(name = "loginMapper")
	private LoginMapper loginMapper;

	@Value("${user.init_password}")
	private String USER_INIT_PASSWORD;

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		// TODO Auto-generated method stub
		return null;
	}
	
}

 

 

CustomAuthenticationManager 클래스는 AuthenticationManager 인터페이스를 구현하여 사용자 인증을 관리합니다.

 

AuthenticationManager 인터페이스를 implements 상속하면 authenticate 메서드를 추가할 수 있는데, 해당 메서드는 실제 사용자 인증을 수행하는 주요 로직을 처리할 수 있는데, 바로 추가해 보겠습니다.

 

✅ CustomAuthenticationManager  - 코드 추가

public class CustomAuthenticationManager implements AuthenticationManager {

	private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());

	@Resource(name = "loginMapper")
	private LoginMapper loginMapper;

	@Value("${user.init_password}")
	private String USER_INIT_PASSWORD;


	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {

		LOGGER.debug(">>>>>>>>>>>>>>>CustomAuthenticationManager authenticate : principan : "+authentication.getPrincipal().toString());
		LOGGER.debug(authentication.getCredentials().toString());
		System.out.println(authentication.getDetails());
		
		
		// HttpServletRequest를 RequestContextHolder를 통해 가져옵니다.
	    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
	    String referer = request.getHeader("Referer");
		UsernamePasswordAuthenticationToken authToken = null;

		String username = authentication.getPrincipal().toString();
		String password = authentication.getCredentials().toString();

		LoginVO loginVO = new LoginVO();
		loginVO.setUserId(username);

		try {
			loginVO = loginMapper.selectUserLogin(loginVO);

			if(loginVO != null) {

				if (loginVO != null && loginVO.getUserId() != null && !loginVO.getUserId().equals("") && referer.contains("/ua/ul/loginForm.do")) {

					String pswdEncpt = loginVO.getPswdEncpt();
					String initPwdEnc = null;
					initPwdEnc = EgovFileScrty.hashString(USER_INIT_PASSWORD, loginVO.getUserId());

					// 사용자 초기 비밀번호 사용 시 비밀번호 변경하여야 한다.
					if(initPwdEnc != null && initPwdEnc.equals(pswdEncpt)) {
						throw new BadCredentialsException("initPassword");
					}

					//사용자 등록 상태확인(신청 시, 승인반려 전 상태)
					if(Constants.USER_REG_STS_REQST.equals(loginVO.getRegSts())) {
						throw new BadCredentialsException("regStsReqst");
					}
					//사용자 등록 상태확인(반려됐을 떄)
					if(Constants.USER_REG_STS_RETURN.equals(loginVO.getRegSts())) {
						throw new BadCredentialsException("regStsReturn");
					}

					if("N".equals(loginVO.getUseYn())){
						throw new BadCredentialsException("unusedUser");
					}

					int loginFailCnt = Integer.parseInt(loginVO.getLgnFailrCo() != null ? loginVO.getLgnFailrCo() : "0");
					// 로그인 실패 5회 이상
					if(loginFailCnt >= 5) {
						throw new BadCredentialsException("failCountOver");
					}
				}


					//사용자에게 관리자 권한이 있을경우 패스워드와 비교하여 로그인 여부를 판단하고
					//관리자 권한이 없는 유저는 패스워드를 체크하지 않는다.
					if(loginVO.getAuthorCode().contains("ADMIN")) {
						if(!password.equals(loginVO.getPswdEncpt())){
							throw new BadCredentialsException("invalidIdPass");
						}else{
							//마지막 접속일자 갱신
							loginMapper.updateLoginLastCntnDt(loginVO);

							//사용자 권한코드 부여
							String[] arrAuthCode = new String[]{};
							if (loginVO.getAuthorCode() != null) {
								arrAuthCode = loginVO.getAuthorCode().split(",");
							}

							UserDetails userDetails = User.builder().username(username)
									.password(password)
									.roles(arrAuthCode)
									.build();


							//인증토큰 생성
							authToken = new UsernamePasswordAuthenticationToken(username, password, userDetails.getAuthorities());
							authToken.setDetails(loginVO);
						}
					}else {

							if("N".equals(loginVO.getUseYn())){
								throw new BadCredentialsException("unusedUser");
							}

							//마지막 접속일자 갱신
							loginMapper.updateLoginLastCntnDt(loginVO);

							//사용자 권한코드 부여
							String[] arrAuthCode = new String[]{};
							if (loginVO.getAuthorCode() != null) {
								arrAuthCode = loginVO.getAuthorCode().split(",");
							}

							UserDetails userDetails = User.builder().username(username)
									.password(password)
									.roles(arrAuthCode)
									.build();

							//인증토큰 생성
							authToken = new UsernamePasswordAuthenticationToken(username, password, userDetails.getAuthorities());
							authToken.setDetails(loginVO);
					}
			}else {
				throw new BadCredentialsException("invalidIdPass");
			}
		} catch (SQLException e) {
			throw new BadCredentialsException("databaseError");
		}


		return authToken;
	}

}

 

 

코드 흐름을 간단히 설명해 보겠습니다.


1️⃣ 현재 HTTP 요청 정보를 가져와서 리퍼러 헤더를 확인합니다.

TIP) Referer?

Referer는 HTTP 헤더 중 하나입니다.
HTTP 프로토콜에는 Referer라는 헤더값이 있는데, 브라우저가 서버로 이 헤더값을 설정해서 보내게 됩니다.

그리고 서버는 Referer를 참조함으로써 현재 표시하는 웹페이지가 어떤 웹페이지에서 요청되었는지 알 수 있으며, 어떤 웹사이트나 웹서버에서 방문자가 왔는지를 파악할 수 있는 기능을 Referer를 통해 할 수 있습니다.

예를 들어, http://www.test.com/1 이라는 웹페이지에 있는 링크를 클릭하여
http://www.test.com/2으로 이동했을 때 Refererhttp://www.test.com/1이 됩니다.

 

2️⃣ 주어신 사용자 ID와 비밀번호를 기반으로 LoginVO 객체를 생성하고, 데이터베이스에서 사용자 정보를 조회합니다.

 

3️⃣ 요구사항에 언급했듯 다양한 조건을 체크하는 로그인 Validation 처리를 해줍니다.

  • 초기 비밀번호 사용 여부
  • 사용자 등록 상태
  • 계정 사용 가능 여부
  • 로그인 실패 횟수

 

4️⃣ 관리자인 경우 비밀번호를 검증하고, 일반 사용자에 대해서는 SSO 로그인 여부를 체크합니다.

 

5️⃣ 인증이 성공하면 UsernamePasswordAuthenticationToken을 생성하여 인증 정보를 포함합니다.


최종적으로 CustomAuthenticationManager 인증 처리 로직 추가를 완료했습니다. 바로 Config에 등록해서 인증 필터에 추가해주겠습니다.

 

✅ CustomAuthenticationManager  - Security Config 추가

	/**
	 * 커스텀 사용자 인증 처리
	 *
	 * @return CustomAuthenticationManager
	 */
	@Bean
	CustomAuthenticationManager customAuthenticationManager() {
		return new CustomAuthenticationManager();
	}

 

Config 파일에 빈으로 생성해 주고 인증 필터에 추가하고 로그인 처리 URL 경로를 필터가 처리할 요청 URL로 명시적으로 지정해주려고 합니다.

 

그 이유는 특정 경로에만 필터가 적용되도록 하여 불필요한 요청 처리를 방지하고, 환경에 따라 쉽게 URL 경로를 변경할 수 있게 설정하려고 합니다.

 

 Security Config

	/**
	 * 커스텀 사용자 인증 필터
	 *
	 * @return CustomAuthenticationFilter
	 */
	@Bean
	CustomAuthenticationFilter customAuthenticationFilter() {
		CustomAuthenticationFilter filter = new CustomAuthenticationFilter("/actionLogin.do");
		filter.setAuthenticationManager(customAuthenticationManager());
		return filter;
	}

 

이렇게 경로와 생성자를 명시적으로 설정하면 스프링 시큐리티의 필터 체인 내에서 CustomAuthenticationFilter가 정확하고 유연하게 동작하도록 설계할 수 있습니다.

 

지금까지 사용자 인증 처리하는 로직을 생성해서 추가했습니다. 

다음으로는 인증 실패/성공 핸들러를 생성해서 필터에 적용해 보겠습니다.


Spring Security - 인증 실패 / 성공 핸들러 생성

 

먼저 인증 실패 핸들러를 생성해 보겠습니다.

 

✅  CustomAuthenticationFailureHandler - 생성

public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {

	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {
		// TODO Auto-generated method stub
		
	}
	
	
}

 

AuthenticationFailureHandler 인터페이스를 상속받으면 로그인 인증에 실패했을 때(onAuthenticationFailure) 사용자에게 실패 메시지를 전달하거나 특정 URL을 설정해서 리다이렉트 시킬 수 있는데, 코드를 추가해 보겠습니다.

 

✅  CustomAuthenticationFailureHandler - 코드 추가

public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
	
	private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());
	
    private String defaultFailureUrl = "/login-error";

    public CustomAuthenticationFailureHandler(String defaultFailureUrl) {
        this.defaultFailureUrl = defaultFailureUrl;
    }

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, 
                                        HttpServletResponse response, 
                                        AuthenticationException ex) 
            throws IOException, ServletException {
    	LOGGER.debug(">>>>>>>>>>>>>>>CustomAuthenticationFailureHandler onAuthenticationFailure");
        // 실패로그를 남긴다
        // 실패이벤트를 발송한다

    	String failureMessage = ex.getMessage();
    	String userId = request.getParameter("userId");
    	
    	response.setContentType("text/html");
        PrintWriter out = response.getWriter();

        out.println("<html>");
        out.println("<body onload='document.forms[\"redirectForm\"].submit()'>");
        out.println("<form name='redirectForm' method='POST' action='"+defaultFailureUrl+"'>");
        out.println("<input type='hidden' name='message' value='"+failureMessage+"'>");
        out.println("<input type='hidden' name='userId' value='"+userId+"'>");
        out.println("</form>");
        out.println("</body>");
        out.println("</html>");

        out.close();
 
    }
}

 

코드 흐름을 간단히 설명해 보겠습니다.

 


1️⃣ 인증 실패 시 onAuthenticationFailure가 호출됩니다.

 

2️⃣ 실패 원인과 사용자 ID를 가져옵니다.

 

3️⃣ 동적으로 HTML 폼을 생성해 브라우저가 실패 URL로 POST 요청을 보내도록 설정했습니다.

 

4️⃣ 서버에서 실패 URL을 처리하여 사용자에게 적절한 응답을 제공합니다.


다음으로는 인증 성공 시 동작하는 커스텀 핸들러를 생성해 보겠습니다.

 

✅  CustomAuthenticationSuccessHandler - 생성

public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		// TODO Auto-generated method stub
		
	}
}

 

CustomAuthenticationSuccessHandler 클래스는 AuthenticationSuccessHandler 인터페이스를 구현하여, 인증 성공 시 추가 처리를 onAuthenticationSuccess 메서드를 통해 처리할 수 있습니다.

 

인증 성공 핸들러에 추가해야 될 사항은 "사용자가 이미 로그인되어 있는지 확인"이 필요합니다.

따라서, 추가로 기존 세션 무효화 시키는 로직과 중복 로그인 방지 처리 코드도 추가해 보겠습니다.

 

✅  CustomAuthenticationSuccessHandler - 코드 추가

public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

	private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());

	// 세션객체 생성
	private static final ConcurrentHashMap<String, HttpSession> activeSessions = new ConcurrentHashMap<>();

	private String redirectUrl;

	public CustomAuthenticationSuccessHandler(String redirectUrl) {
		this.redirectUrl = redirectUrl;
	}

	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {

		String username = authentication.getPrincipal().toString();

		//중복 로그인 방지 처리
		if(checkUserAlreadyLoggedIn(username)) {
			invalidatePreviousSessions(request, response, username);
		}

		activeSessions.put(username, request.getSession());

		if(!response.isCommitted()) {
			response.sendRedirect(redirectUrl);
		}
	}

	// 사용자가 이미 로그인되어 있는지 확인
	private boolean checkUserAlreadyLoggedIn(String username) {
	    for (String loggedInUser : activeSessions.keySet()) {
	        if (loggedInUser.equals(username)) {
	            return true;
	        }
	    }
	    return false;
	}

	// 기존 세션 무효화
	private void invalidatePreviousSessions(HttpServletRequest req, HttpServletResponse res, String username) {
        // 현재 사용자의 모든 세션 무효화
        Iterator<Map.Entry<String, HttpSession>> iterator = activeSessions.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, HttpSession> entry = iterator.next();
            if (entry.getKey().equals(username) && !entry.getValue().getId().equals(req.getSession().getId())) {
                HttpSession session = entry.getValue();
                try {
                    session.invalidate();
                    LOGGER.info("기존 세션 로그아웃: " + username);
                } catch (IllegalStateException e) {
                    // 세션이 이미 무효화된 경우에 대한 예외 처리
                	LOGGER.error("세션 무효화 중 예외 발생: " + e.getMessage());
                }
                iterator.remove();
                break; // 루프 종료
            }
        }
    }

}

 

코드 흐름을 간단히 설명해 보겠습니다.

 


1️⃣ 인증 성공 시 onAuthenticationSuccess 메서드가 호출됩니다.

 

2️⃣ authentication 객체에서 사용자 ID를 가져옵니다.

 

3️⃣ 중복 로그인을 방지합니다.

  • 현재 사용자가 이미 로그인되어 있는지 확인 ( checkUserAlreadyLoggedIn )
  • 기존 세션이 있다면 해당 세션을 무효화 ( invalidatePreviousSessions )

4️⃣ 새로운 세션을 activeSessions에 저장합니다.

 

5️⃣ 인증 성공 후, 지정된 URL로 리다이렉트 시켜줍니다.


 인증 실패/성공 핸들러 로직 추가를 완료했기 때문에 필터에 추가해서 적용시켜 줍니다.

 

✅  Security Config - 핸들러 추가

	/**
	 * 커스텀 사용자 인증 필터
	 *
	 * @return CustomAuthenticationFilter
	 */
	@Bean
	CustomAuthenticationFilter customAuthenticationFilter() {
		CustomAuthenticationFilter filter = new CustomAuthenticationFilter("/actionLogin.do");
		filter.setAuthenticationManager(customAuthenticationManager());
		filter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler("/loginForm.do"));
		filter.setAuthenticationSuccessHandler(new CustomAuthenticationSuccessHandler("/index.do"));
		return filter;
	}

 

지금까지 사용자 인증 처리 필터를 생성해서 처리하는 로직을 추가하고 인증 실패/성공 핸들러까지 추가해 줬습니다.

 

마지막으로 요청된 URL에 대해 현재 사용자가 필요한 권한을 가지고 있는 확인하는 코드를 추가해 보겠습니다.


Spring Security - 사용자 권한별 처리 

 

✅  CustomAccessDecisionManager  - 생성

@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {

    private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());

    @Autowired
	private CmmnMenuMapper cmmnMenuMapper;

    //권한별 메뉴 목록 조회
    private List<EgovMap> authMenuList;

	@Override
	public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
			throws AccessDeniedException, InsufficientAuthenticationException {
		// TODO Auto-generated method stub
		
	}

	@Override
	public boolean supports(ConfigAttribute attribute) {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public boolean supports(Class<?> clazz) {
		// TODO Auto-generated method stub
		return false;
	}

   
}

 

위 코드는 스프링 시큐리티의 AccessDecisionManager 인터페이스를 구현한 CustomAccessDecisionManager 클래스입니다. 

 

특정 URL 요청에 대해 사용자 권한을 기반으로 접근을 허용할지 결정하는 커스텀 로직을 추가해 보겠습니다.

 

✅  CustomAccessDecisionManager  - 코드 추가

@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {

    private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());

    @Autowired
	private CmmnMenuMapper cmmnMenuMapper;

    //권한별 메뉴 목록 조회
    private List<EgovMap> authMenuList;

    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
            throws AccessDeniedException {

        if(authMenuList == null){
            try {
                authMenuList = cmmnMenuMapper.selectAuthMenuList();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        // configAttributes: 요청 URL에 필요한 권한들
        // authentication: 현재 사용자의 권한 정보

        if (configAttributes == null || configAttributes.isEmpty()) {
            return; // 권한 체크 필요 없음
        }

        // 현재 사용자의 권한 목록 확인
        Collection<? extends GrantedAuthority> userAuthorities = authentication.getAuthorities();

        for (ConfigAttribute attribute : configAttributes) {
            // 필요한 권한 이름
            String requiredAuthority = attribute.getAttribute();
            LOGGER.debug("ConfigAttribute [{}] requiredAuthority [{}]", attribute, requiredAuthority);
            String autheConfig = attribute.toString();

            if("permitAll".equals(autheConfig)){
                return;
            }else if("authenticated".equals(autheConfig)){
                FilterInvocation fi =(FilterInvocation) object;
                String requestUrl = fi.getRequestUrl();

                boolean isPermit = false;
                for(EgovMap authMenu : authMenuList){
                    String menuUrl = authMenu.get("menuUrl").toString();
                    String authorCode = "ROLE_"+authMenu.get("authorCode").toString();

                    if(menuUrl.equals(requestUrl)){
                        for(GrantedAuthority authority : userAuthorities){
                            if(authority.getAuthority().equals(authorCode)){
                                isPermit = true;
                                break;
                            }
                        }
                    }
                }

                if(isPermit){
                    return;
                }
            }
        }

        // 필요한 권한이 없으면 예외 발생
        throw new AccessDeniedException("접근 권한이 없습니다.");
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true; // 모든 ConfigAttribute를 지원
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true; // 모든 Security 클래스 지원
    }
}


코드 흐름을 간단히 설명해 보겠습니다.

 


1️⃣ 요청 URL에 대한 권한 정보(ConfigAttribute)와 사용자의 권한 정보(GrantedAuthority)를 가져옵니다.

 

2️⃣ DB에서 메뉴 URL과 권한 매핑 정보를 조회합니다.

 

3️⃣요청 URL과 매핑된 권한이 사용자의 권한 목록에 있는지 확인합니다.

 

4️⃣ 적합한 권한이 있으면 요청을 허용하고, 없으면 접근 거부( AccessDeniedException ) 시킵니다.


다음으로 권한 별 처리 클래스를 시큐리티 Config 파일에 추가해 줍니다.

 

✅  Security Config - 추가 

	@Bean
	AccessDecisionManager accessDecisionManager() {
		return new CustomAccessDecisionManager();
	}

 

마지막으로 지금까지 생성한 로직을 필터 체인에 설정해 줍니다.

 

✅  Security Config - Filter Chain 설정

//로그인 처리 및 로그인 성공 시 이동할 페이지 설정
    authorizeRequests
        .anyRequest().authenticated()
        .accessDecisionManager(accessDecisionManager())
        .and()
        .addFilterBefore(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
        }
    )

마치며

 

오늘은 스프링 시큐리티 사용자 권한 별 인증 처리 하는 방법에 대해 알아봤습니다.

 

지금까지 스프링 시큐리티에 대해 포스팅을 하면서 느낀 점으로는 내부 구조를 모르고 사용하는 스프링 시큐리티는 언젠가 터지게 될 이슈에 대응하기 힘들어집니다.

 

내부 구조와 동작과 같은 개념을 잡아가야 실무에 더욱 효율적으로 적용할 수 있고 이슈가 발생했을 때  유연하게 대처가 가능해집니다.

 

다음 포스팅에서 뵙겠습니다👋

 

728x90