Spring Security는 Spring 기반의 애플리케이션의 보안 (인증과 권한, 인가 등)을 담당하는 스프링 하위 프레임워크이다.
인증(Authentication) : 해당 사용자가 본인이 맞는지 신원을 확인하는 절차인가(Authorization) : 인증된 사용자가 요청한 자원에 접근 가능한지 권한을 확인하는 절차필터(Filter) : Dispatcher Servlet에 요청이 전달되기 전/후에 url 패턴에 맞는 모든 요청에 대한 작업을 처리할 수 있는 기능 제공 (웹 컨테이너(톰캣)에 의해 관리됨)인터셉터(Interceptor) : Spring이 제공하는 기술로 Dispatch Servlet이 컨트롤러를 호출하기 전과 후에 끼어들어 스프링 컨텍스트 내부에서 Controller(Handler)에 관한 요청과 응답에 대해 처리그러면, 스프링 시큐리티는 어떻게 톰캣의 필터를 사용할까?
Spring Security는 사용하고자 하는
FilterChain들을 Servlet Container 기반의 필터 위에서 동작시키기 위해DelegatingFIlterProxy라는 클래스를 이용한다. IOC 컨테이너에서 관리하는 빈이 아닌 표준 서블릿 필터를 구현하고 있고 내부에위임대상(FilterChainProxy)를 갖고 있다. 즉, DelegatingFilterProxy는 서블릿 필터이며, Spring IOC 컨테이너가 관리하는 Filter Bean을 갖고 있고 이 Filter Bean은 위임대상이며 이 객체안에서 security와 관련된 일들이 벌어진다.

Authentication Filter에서 Http Request을 받고 토큰 생성 (UsernamePasswordAuthenticationToken)AuthenticationManager에게 보내면서 인증을 위임AuthenticationProvider에게 인증용 객체를 다시 위임UserDetailsService 인터페이스의 loadUserByUsername()메서드를 호출하여 사용자 정보를 UserDetails타입으로 가져옴authenticate()를 이용하여 사용자 인증을 수행PasswordEncoder를 이용하여 해시한 후에 비교한다. (matches() 메서드로 비교)SecurityContextHolder라는 곳에 담은 훟, AuthenticationSuccessHandler를 실행<!-- needed for ContextLoaderListener -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:kr/or/ddit/spring/conf/*-context.xml</param-value>
</context-param>
<!-- Bootstraps the root web application context before servlet initialization -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<async-supported>true</async-supported>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<bean id="passwordEncoder" class="org.springframework.security.crypto.factory.PasswordEncoderFactories"
factory-method="createDelegatingPasswordEncoder"
/>
<bean id="daoAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider"
p:userDetailsService-ref="accountServiceImpl"
p:passwordEncoder-ref="passwordEncoder"
p:hideUserNotFoundExceptions="false"
/>
<security:authentication-manager>
<security:authentication-provider ref="daoAuthenticationProvider" />
</security:authentication-manager>
// User 생성자
public User(String username, String password, boolean enabled, boolean accountNonExpired,
boolean credentialsNonExpired, boolean accountNonLocked,
Collection<? extends GrantedAuthority> authorities) {
Assert.isTrue(username != null && !"".equals(username) && password != null,
"Cannot pass null or empty values to constructor");
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
}
@SuppressWarnings("serial")
public class AccountWrapper<T> extends User {
private T realUser;
private String accInfo;
public AccountWrapper(AccountVO account, T realUser) {
super(
account.getAccId(), // username
account.getAccPw(), // password
!account.isAccDel(), // enabled
true, // accountNonExpired
true, // credentialNonExpired
true, // accountNonLocked
AuthorityUtils.createAuthorityList(account.getAccAuth()) // authorities
);
this.realUser = realUser;
this.accInfo = account.getAccInfo();
}
public T getRealUser() {
return realUser;
}
public String getAccInfo() {
return accInfo;
}
}
public interface AccountService extends UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AccountVO account = accountDAO.selectAccountForAuth(username);
if (account == null) {
throw new UsernameNotFoundException(MessageFormat.format("{0} 사용자 없음", username));
}
// employee 정보 가져오기
EmployeeVO empVO = employeeDAO.selectEmp(username);
return new AccountWrapper<EmployeeVO>(account, empVO);
}
@Slf4j
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws ServletException, IOException {
// principal 객체를 가져와서 계정정보 읽기 (직원: EMP, 항공사: AL, 입점업체: VD)
AccountWrapper aw = (AccountWrapper) authentication.getPrincipal();
String accInfo = aw.getAccInfo();
super.setDefaultTargetUrl("/in");
super.onAuthenticationSuccess(request, response, authentication);
}
}
공항직원, 입점사, 항공사 계정을 하나의 계정테이블로 통합하여 관리하다보니, 각자의 다른 타입에 대하여 처리하기가 어려운 문제가 있었다.

전체를 아우르는 상위 클래스를 만들고 이를 상속하게 하여서 처리할 수 도 있지만, 이는 MyBatis 매핑을 따로 따로 설정해줘야 하는 번거로움이 있고, 각각의 클래스에서 구현한 개별 메서드가 필요한 경우도 있기 때문에 다른 방법을 생각해보았다.
따라서, User를 상속받고 제너릭 타입의 realUser와 로그인한 사용자가 어떠한 타입의 유저인지를 구분하기 위해서 String 타입의 accInfo를 두었다. 따라서 아래와 같이 getRealUser로 가져와서 타입에 맞게 형변환 할 수 있도록 설정하였다.
@GetMapping("/temp.do")
public ResponseEntity<?> myTempMethod(Authentication auth) {
AccountWrapper wrapper = (AccountWrapper) auth.getPrincipal();
String accInfo = wrapper.getAccInfo();
if (accInfo.equals("EMP")) {
EmployeeVO emp = (EmployeeVO) wrapper.getRealUser();
} else if (accInfo.equals("VD")) {
vendorVO vendor = (vendorVO) wrapper.getRealUser();
} else { // AL
AirlineVO airLine = (AirlineVO) wrapper.getRealUser();
}
return ResponseEntity.ok(null);
}
session vs token
토큰 기반 인증은 세션 정보를 서버에 저장하지 않고, 클라이언트 측에 토큰을 저장하여 인증하는 방식입니다. 각 요청에서 서버에 전송되는 토큰을 사용하여 사용자의 인증 상태를 확인하고 처리합니다. 이러한 방식으로 인증을 처리함으로써 서버는 세션 상태를 유지할 필요 없이 사용자를 식별하고 인증할 수 있습니다.
주로 JWT(JSON Web Token)를 사용하여 토큰 기반 인증을 구현합니다. 사용자가 로그인을 하면 서버에서 JWT를 발급하고, 클라이언트는 이 JWT를 저장하여 인증된 요청을 보낼 때마다 HTTP 헤더에 포함시켜 서버로 전송합니다. 서버는 이 토큰을 검증하여 사용자를 인증하고 요청을 처리합니다.
토큰 기반 인증은 세션 기반 인증과 달리 서버에 상태를 유지하지 않기 때문에 분산 환경에서 더 용이하며, 확장성이 높습니다. 또한 클라이언트와 서버 간의 독립성이 높아져서 다양한 클라이언트 플랫폼과 기술 스택을 사용하는 애플리케이션에 적합합니다.
Spring Security란?
Filter와 Interceptor
DelegatingFilterProxy
Spring Security 동작 원리 & 이미지 사용