프로젝트 내에 특정 패키지에 아래 CustomAccessTokenConfig.java를 생성해준다.
@Configuration
public class CustomAccessTokenConfig {
@Bean
public AccessTokenFilter accessFilter(AuthenticationManager authManager, JwtAuthentication jwtAuthentication, AuthenticationSuccessHandler authenticationSuccessHandler, AuthenticationFailureHandler authenticationFailureHandler) {
AccessTokenFilter accessTokenFilter = new AccessTokenFilter(authManager, jwtAuthentication);
// 로그인 URL 설정
accessTokenFilter.setFilterProcessesUrl("/security/jwt/authenticate");
// 인증 성공 핸들러
accessTokenFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
// 인증 실패 핸들러
accessTokenFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
// BeanFactory에 의해 모든 property가 설정되고 난 뒤 실행
accessTokenFilter.afterPropertiesSet();
return accessTokenFilter;
}
}
CustomAccessTokenConfig 클래스는 Spring Security 필터 체인에 사용자 정의 인증 필터를 등록한다.
AccessTokenFilter는 실제 토큰을 생성하며, 자세한 설명은 아래의 AccessTokenFilter 섹션에서 진행한다.
로그인 URL 설정은 .setFilterProcessesUrl("/security/jwt/authenticate")을 호출하여, 인증 요청이 해당 URL로 전송되도록 설정한다.
인증 성공시 .setAuthenticationSuccessHandler(authenticationSuccessHandler)를 호출하며 , 인증 실패 시 .setAuthenticationFailureHandler(authenticationFailureHandler)를 호출하여 해당 핸들러를 필터에 연결한다.
AccessTokenFilter.java**
AccessTokenFilter는 사용자 계정 정보 확인 후 실제 AccessToken 및 RefreshToken을 생성한다.
비밀번호, 계정 유효성 검사에 대한 로직을 수정 할 수 있다.
프로젝트 내에 특정 패키지에 아래 AccessTokenFilter.java를 생성해준다.
@Configuration
public class AccessTokenFilter extends UsernamePasswordAuthenticationFilter implements AccessTokenInterface {
@Autowired
private JdbcUserDetailsService jdbcUserDetailsService;
@Autowired
private PasswordDecoder passwordDecoder;
private final JwtAuthentication jwtAuthentication;
public AccessTokenFilter(AuthenticationManager authenticationManager, JwtAuthentication jwtAuthentication) {
super.setAuthenticationManager(authenticationManager);
this.jwtAuthentication = jwtAuthentication;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
JwtRequest loginRequest;
DefaultUser userDetails;
Map<String, String> token;
try {
loginRequest = new ObjectMapper().readValue(request.getInputStream(), JwtRequest.class);
LoggingUtils.userAccessLogging(loginRequest.getUsername(), UserAccessActionType.LOGIN_ATTEMPT);
userDetails = (DefaultUser) jdbcUserDetailsService.loadUserByUsername(loginRequest.getUsername());
String decryptPassword = passwordDecoder.decrypt(loginRequest.getPassword());
// 비밀번호 확인
jwtAuthentication.checkCredentials(decryptPassword, userDetails);
// 계정 상태 검사 (만료, 잠김 등의 상태)
jwtAuthentication.checkAccountStatus(userDetails);
// JWT 토큰 생성
token = jwtAuthentication.generateAndStoreTokens(userDetails);
} catch (ChamomileException ex) {
throw new AuthenticationServiceException(ex.getMessage(), new ChamomileException(((ChamomileException) ex).getChamomileCode(), ex.getMessage()));
} catch (UsernameNotFoundException ex) {
throw new AuthenticationServiceException(ex.getMessage(), new ChamomileException(ChamomileExceptionCode.AuthenticationFailed, "authentication.fail.login"));
} catch (Exception ex) {
throw new AuthenticationServiceException(ex.getMessage());
}
// 요청 속성에 토큰 저장
request.setAttribute("jwtToken", token);
//계정 유효성 체크
return new JwtAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
}
}
AccessTokenFilter 클래스는 UsernamePasswordAuthenticationFilter를 확장하여 사용자 이름과 비밀번호를 기반으로 인증을 시도하고, JWT 토큰을 생성하는 역할을 한다.
jdbcUserDetailsService.loadUserByUsername를 사용하여 데이터베이스로부터 해당 사용자명을 가진 사용자의 정보를 조회한다. 이 때 반환 되는 DefaultUser 객체는 사용자의 상세 정보를 담고 있다. 사용자 정보 조회, 권한 정보 조회, 그룹 권한 정보 조회 등에 데이터 조회에 대한 쿼리 수정이 필요 할 경우 setUsersByUsernameQuery(), setAuthoritiesByUsernameQuery, setGroupAuthoritiesByUsernameQuery 메서드를 통하여 수정이 가능하다.
비밀번호 검증 로직은 jwtAuthentication.checkCredentials 메서드를 통해 수행된다.
jwtAuthentication.checkAccountStatus사용자의 계정이 활성 상태인지 확인한다. 계정이 만료되었거나 잠겨 있는지 등의 상태를 검사하여, 사용이 가능한 상태인지 확인한다.
jwtAuthentication.generateAndStoreTokens 사용자의 상세정보를 기반으로 JWT 토큰을 생성한다.
LoginSuccessHandler.java
SampleLoginSuccessHandler는 AccessTokenFilter 에서 정상적인 토큰 발급 후 후 처리 역할을 한다.
프로젝트 내에 특정 패키지에 아래 SampleTokenLoginSuccessHandler.java를 생성해준다.
@Configuration
public class SampleTokenLoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private JdbcLoginService jdbcLoginService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
try {
Map<String, String> token = (Map<String, String>) request.getAttribute("jwtToken");
jdbcLoginService.resetLockCnt(authentication.getName());
// ObjectMapper를 사용하여 토큰을 JSON 문자열로 변환
ObjectMapper objectMapper = new ObjectMapper();
String tokenJson = objectMapper.writeValueAsString(new ChamomileResponse<>(token));
LoggingUtils.userAccessLogging(authentication.getName(), UserAccessActionType.LOGIN_SUCCESS);
// 응답에 JSON 형태로 토큰 추가
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_OK);
// 응답 본문에 토큰 작성
PrintWriter out = response.getWriter();
out.print(tokenJson);
out.flush();
} catch (Exception ex) {
throw new ChamomileException(ChamomileExceptionCode.USER_AUTHENTICATION, ex.getMessage());
}
}
}
AccessTokenFilter.java 에서 성공적인 토큰 발급시 AuthenticationSuccessHandler 를 구현한 SuccessHandler를 통해 값을 리턴한다.
성공시 필요한 기능을 커스텀하여 제공 할 수 있다.
LoginFailureHandler.java
SampleLoginFailureHandler는 AccessTokenFilter 에서 토큰 발급 실패시 후 처리 역활을 한다.
프로젝트 내에 특정 패키지에 아래 SampleLoginFailureHandler.java를 생성해준다.
@Configuration
public class SampleTokenLoginFailureHandler implements AuthenticationFailureHandler {
@Autowired
private JdbcLoginService jdbcLoginService;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
try {
String userID = request.getParameter("username");
LoggingUtils.userAccessLogging(userID, UserAccessActionType.LOGIN_FAIL);
ChamomileCode chamomileCode = null;
Throwable cause = exception.getCause();
if (cause instanceof ChamomileException) {
chamomileCode = ((ChamomileException) cause).getFrameworkCode();
}
jdbcLoginService.lockProcessAccount(userID, exception);
// 조건에 따른 코드 선택 및 메시지 설정
int errorCode = chamomileCode != null ? chamomileCode.getCode() : ChamomileExceptionCode.USER_AUTHENTICATION.getCode();
String errorMessage = chamomileCode != null ? chamomileCode.toString() : exception.getMessage();
ChamomileResponse<Object> chamomileResponse = new ChamomileResponse<>(errorCode, errorMessage, null);
// 응답 JSON 변환 및 설정
ObjectMapper objectMapper = new ObjectMapper();
String tokenJson = objectMapper.writeValueAsString(chamomileResponse);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
PrintWriter out = response.getWriter();
out.print(tokenJson);
out.flush();
} catch (Exception ex) {
throw new ChamomileException(ChamomileExceptionCode.USER_AUTHENTICATION, ex.getMessage());
}
}
}
AccessTokenFilter.java 내 토큰 발급 실패시 AuthenticationFailureHandler 를 구현한 FailureHandler를 통해 값을 리턴한다.
토큰 재발급 커스텀 예제
토큰 재발급의 경우 프로젝트 상황에 맞게 수정이 가능하다.
CustomRefreshTokenConfig.java
프로젝트 내에 특정 패키지에 아래 CustomAccessTokenConfig.java를 생성해준다.
@Configuration
public class CustomRefreshTokenConfig {
@Bean
public RefreshTokenFilter refreshTokenFilter(AuthenticationManager authManager, JwtAuthentication jwtAuthentication, AuthenticationSuccessHandler authenticationSuccessHandler, AuthenticationFailureHandler authenticationFailureHandler) {
RefreshTokenFilter refreshTokenFilter = new RefreshTokenFilter(authManager, jwtAuthentication);
// 필터 URL 설정
refreshTokenFilter.setFilterProcessesUrl(securityProperties.getRefreshProcessingUrl());
// 인증 성공 핸들러
refreshTokenFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
// 인증 실패 핸들러
refreshTokenFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
// BeanFactory에 의해 모든 property가 설정되고 난 뒤 실행
refreshTokenFilter.afterPropertiesSet();
return refreshTokenFilter;
}
}
CustomRefreshTokenConfig 클래스는 Spring Security 필터 체인에 사용자 정의 인증 필터를 등록하는 역할을 한다.
RefreshTokenFilter는 실제 토큰을 생성하는 필터로 자세한 설명은 아래(RefreshTokenFilter)설명 에서 진행한다.
토큰 재발급 URL 설정: setFilterProcessesUrl("/security/jwt/refresh-token")을 호출하여, 인증 요청이 이 URL로 보내지도록 설정한다.
토큰 재발급 성공 및 실패 핸들러: 인증 성공시 setAuthenticationSuccessHandler(authenticationSuccessHandler)를 호출하며 , 인증 실패 시 .setAuthenticationFailureHandler(authenticationFailureHandler)를 호출하여 해당 핸들러를 필터에 연결한다.
RefreshTokenFilter.java
RefreshTokenFilter은 Refresh 토큰 검증과 사용자 계정 정보 확인 후 실제 AccessToken 및 RefreshToken을 재발급 해준다.
프로젝트 내에 특정 패키지에 아래 RefreshTokenFilter.java를 생성해준다.
@Configuration
public class RefreshTokenFilter extends UsernamePasswordAuthenticationFilter implements RefreshTokenInterface{
@Autowired
private JdbcUserDetailsService jdbcUserDetailsService;
@Autowired
private JwtTokenUtils jwtTokenUtils;
private final JwtAuthentication jwtAuthentication;
public RefreshTokenFilter(AuthenticationManager authenticationManager, JwtAuthentication jwtAuthentication) {
super.setAuthenticationManager(authenticationManager);
this.jwtAuthentication = jwtAuthentication;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String refreshToken;
String userName;
DefaultUser userDetails;
// refreshToken 에서 Subject 반환(userId)
try {
// Header에서 토큰 파싱
refreshToken = jwtTokenUtils.resolveToken(request);
userName = jwtTokenUtils.extractUsername(refreshToken);
// 리프레시 토큰 유효성 검사
jwtAuthentication.validateRefreshToken(userName, refreshToken);
// 사용자 조회
userDetails = (DefaultUser) jdbcUserDetailsService.loadUserByUsername(userName);
// 계정 상태 검사 (만료, 잠김 등의 상태)
jwtAuthentication.checkAccountStatus(userDetails);
// JWT 토큰 생성
Map<String, String> token = jwtAuthentication.generateAndStoreTokens(userDetails);
// 요청 속성에 토큰 저장
request.setAttribute("jwtToken", token);
// 계정 유효성 체크
} catch (ChamomileException ex) {
throw new AuthenticationServiceException(ex.getMessage(), new ChamomileException(((ChamomileException) ex).getChamomileCode(), ex.getMessage()));
} catch (Exception ex) {
throw new AuthenticationServiceException(ex.getMessage());
}
return new JwtAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
}
}
RefreshTokenFilter 클래스는 UsernamePasswordAuthenticationFilter를 확장하여 RefreshToken 검증과 사용자 이름과 비밀번호를 기반으로 인증을 시도하고, JWT 토큰을 재발급하는 역할을 한다.
jwtTokenUtils.resolveToken, jwtTokenUtils.extractUsername 를 사용하여 Request Header에서 RefreshToken을 파싱한다.
jwtAuthentication.validateRefreshToke를 사용하여 RefreshToekn에 유효성을 검사한다.
jdbcUserDetailsService.loadUserByUsername를 사용하여 데이터베이스로부터 해당 사용자명을 가진 사용자의 정보를 조회한다. 이 때 반환 되는 DefaultUser 객체는 사용자의 상세 정보를 담고 있다.
비밀번호 검증 로직은 jwtAuthentication.checkCredentials 메소드를 통해 수행된다.
jwtAuthentication.checkAccountStatus사용자의 계정이 활성 상태인지 확인한다. 계정이 만료되었거나 잠겨 있는지 등의 상태를 검사하여, 사용이 가능한 상태인지 확인한다.
jwtAuthentication.generateAndStoreTokens 사용자의 상세정보를 기반으로 JWT 토큰을 생성한다.
API 인증 커스텀 예제
API 인증의 경우 프로젝트 상황에 맞게 수정이 가능하다.
JwtAuthenticationProvider.java
프로젝트 내에 특정 패키지에 아래 JwtAuthenticationProvider.java를 생성해준다.
@Configuration
public class JwtAuthenticationProvider implements AuthenticationProvider {
@Autowired
private JdbcUserDetailsService jdbcUserDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JwtTokenUtils tokenUtils;
@Autowired
private JwtAuthentication jwtAuthentication;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String jwtToken = (String) authentication.getCredentials();
//refreshToken을 통한 API 호출 차단
if ("refresh".equals(tokenUtils.extractClaimName(jwtToken))){
throw new ChamomileException(ChamomileExceptionCode.ServerError, "The refresh token cannot be used");
}
String userName = tokenUtils.extractUsername(jwtToken);
//입력받은 AccessToken 내 userName으로 사용자 정보(UserDetails)를 조회하여 반환한다. (사용자정보, 사용자권한 사용자그룹권한)
DefaultUser userDetails = (DefaultUser) jdbcUserDetailsService.loadUserByUsername(userName);
//계정 유효성 체크
jwtAuthentication.checkAccountStatus(userDetails);
return new JwtAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return JwtAuthenticationToken.class.isAssignableFrom(authentication);
}
}
tokenUtils.extractUsername AccessToken 을 파싱하여 userName을 반환한다.
checkAccountStatus 사용자에 대한 유효성 검사를 한다.
ChamomileSecurityJwtConfiguration 커스텀
캐모마일 JWT 시큐리티 설정에 대한 전반적인 수정이 필요한 경우 변경을 진행 할 빈을 프로젝트에서 같은 이름으로 선언하여 사용자가 작성한 빈을 오버라이딩 할 수 있다.
캐모마일 Security에서 JWT용으로 선언된 환경은 다음과 같다. 원하는 부분의 빈만 오버라이딩하여 커스터마이징을 진행하면 된다.
@Configuration
public class ChamomileSecurityJwtConfiguration {
private final ChamomileSecurityProperties securityProperties;
private final ChamomileSecurityCorsProperties chamomileSecurityCorsProperties;
private final DataSource dataSource;
public ChamomileSecurityJwtConfiguration(ChamomileSecurityProperties securityProperties, ChamomileSecurityCorsProperties chamomileSecurityCorsProperties, DataSource dataSource) {
this.chamomileSecurityCorsProperties = chamomileSecurityCorsProperties;
this.dataSource = dataSource;
this.securityProperties = securityProperties;
}
/**
* 인증 요청에 대한 처리 메서드
* JdbcUserDetailsService PasswordEncoder가 자동으로 설정 (provider 사용안할시)
* 복수 공급자 사용시 provider 수정 사용하여 이용 가능
* @param authenticationConfiguration
* @return
* @throws Exception
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
/**
* 인증 프로세스 provider
* {@link AuthenticationProvider} 구현 후 Bean 등록시 사용자단에서 커스텀 가능
* 다중 provider 구성 가능
* @return
* @throws Exception
*/
@Bean
@ConditionalOnMissingBean({AuthenticationProvider.class})
public AuthenticationProvider getDaoAuthProvider() {
return new JwtAuthenticationProvider();
}
/**
* 기본 구성은 '{@code chmm.security.ignorePatterns}'에 설정된 URL 패턴을 무시한다.
* HttpSecurity 보다 우선실행 (filterChain 이전)
* @throws Exception if an error occurs
* @see WebSecurity.IgnoredRequestConfigurer
*/
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> {
Optional.ofNullable(securityProperties.getIgnorePatterns())
.ifPresent(patterns -> web.ignoring().antMatchers(patterns));
};
}
/**
*
* @param http
* @param authManager
* @param chamomileAuthenticationProvider
* @param roleHierarchyService
* @return
* @throws Exception
*/
@Bean
@ConditionalOnMissingBean(SecurityFilterChain.class)
public SecurityFilterChain filterChain(HttpSecurity http,AuthenticationManager authManager,AuthenticationProvider chamomileAuthenticationProvider
, RoleHierarchyService roleHierarchyService, CorsConfigurationSource corsConfigurationSource, AccessTokenInterface accessTokenInterface, RefreshTokenInterface refreshTokenInterface) throws Exception {
http.httpBasic();
http.antMatcher("/**").csrf().disable();
http.cors().configurationSource(corsConfigurationSource);
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore((Filter) accessTokenInterface, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore((Filter) refreshTokenInterface, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class);
addAuthenticationProvider(http, chamomileAuthenticationProvider);
addSecurityInterceptorFilter(http, authManager, roleHierarchyService);
http.exceptionHandling()
.authenticationEntryPoint(new CustomAuthenticationEntryPoint());
http.exceptionHandling()
.accessDeniedHandler(new CustomAccessDeniedHandler());
return http.build();
}
/**
* Jwt Access Token 발급 (로그인)
* @param authManager
* @return
*/
@Bean
@ConditionalOnMissingBean(AccessTokenInterface.class)
public AccessTokenFilter accessFilter(AuthenticationManager authManager, JwtAuthentication jwtAuthentication, AuthenticationSuccessHandler authenticationSuccessHandler, AuthenticationFailureHandler authenticationFailureHandler) {
AccessTokenFilter accessTokenFilter = new AccessTokenFilter(authManager, jwtAuthentication);
// 필터 URL 설정
accessTokenFilter.setFilterProcessesUrl(securityProperties.getLoginProcessingUrl());
// 인증 성공 핸들러
accessTokenFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
// 인증 실패 핸들러
accessTokenFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
// BeanFactory에 의해 모든 property가 설정되고 난 뒤 실행
accessTokenFilter.afterPropertiesSet();
return accessTokenFilter;
}
/**
* Jwt Refresh Token 발급
* @param authManager
* @return
*/
@Bean
@ConditionalOnMissingBean(RefreshTokenInterface.class)
public RefreshTokenFilter refreshTokenFilter(AuthenticationManager authManager, JwtAuthentication jwtAuthentication, AuthenticationSuccessHandler authenticationSuccessHandler, AuthenticationFailureHandler authenticationFailureHandler) {
RefreshTokenFilter refreshTokenFilter = new RefreshTokenFilter(authManager, jwtAuthentication);
// 필터 URL 설정
refreshTokenFilter.setFilterProcessesUrl(securityProperties.getRefreshProcessingUrl());
// 인증 성공 핸들러
refreshTokenFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
// 인증 실패 핸들러
refreshTokenFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
// BeanFactory에 의해 모든 property가 설정되고 난 뒤 실행
refreshTokenFilter.afterPropertiesSet();
return refreshTokenFilter;
}
/**
*
* {@link AuthenticationSuccessHandler} 사용자 단에서 Bean 등록후 사용가능
* @return
*/
@Bean
@ConditionalOnMissingBean(AuthenticationSuccessHandler.class)
public AuthenticationSuccessHandler authenticationSuccessHandler() {
return new SampleLoginSuccessHandler();
}
/**
*
* {@link AuthenticationFailureHandler} 사용자 단에서 Bean 등록후 사용가능
* @return
*/
@Bean
@ConditionalOnMissingBean(AuthenticationFailureHandler.class)
public AuthenticationFailureHandler authenticationFailureHandler() {
return new SampleLoginFailureHandler();
}
/**
* 생성된 provider 등록
* @param http
* @param customAuthenticationProvider
*/
private void addAuthenticationProvider(HttpSecurity http, AuthenticationProvider customAuthenticationProvider) {
http.authenticationProvider(customAuthenticationProvider);
}
/**
* 보호되는 자원을 RDB로 처리하기 위한 FilterSecurityInterceptor
* 모든 요청에 대해 권한이 적절한지 검사를 수행한다.
* 권한에 대한 유효성 검사
* @param http
* @param authManager
*/
private void addSecurityInterceptorFilter(HttpSecurity http, AuthenticationManager authManager,RoleHierarchyService roleHierarchyService) {
http.addFilterBefore(this.filterSecurityInterceptor(authManager, this.accessDecisionManager(roleHierarchyService), this.reloadableFilterInvocationSecurityMetadataSource()), FilterSecurityInterceptor.class);
}
/**
* 권한 없을시 Handler 를 추가할 수 있도록 제공하는 메서드
* @param http
* @throws Exception
*/
/**
* 사용자 계정 비밀번호 다중 인코더 기본(sha256)
* 사용자 계정 비밀번호를 인코딩할 인코더를 여러개 설정할 수 있는 MultiplePasswordEncoder 제공한다
* @return
*/
@Bean
@ConditionalOnMissingBean({PasswordEncoder.class})
public PasswordEncoder passwordEncoder() {
return new MultiplePasswordEncoder(this.securityProperties.getDefaultPasswordEncoder(), this.securityProperties.getPasswordEncoderList());
}
@Bean
public PasswordDecoder passwordDecoder() {
return new PasswordDecoder();
}
/**
* 사용자정보, 사용자권한, 사용자그룹권한. 획득
* chamomile에서 제공하는 테이블 구조로 사용자 정보를 제공한다.
* 사용자 정보를 RDB를 이용하여 처리 한다.
* 별도 Provider 생성시 Provider에서 진행
* @see ChamomileAuthenticationProvider
* {@code chmm.security.}'
* @param dataSource
* @return
* @throws Exception
*/
@Bean
@ConditionalOnMissingBean({UserDetailsService.class })
public JdbcUserDetailsService jdbcUserDetailsService(DataSource dataSource) throws Exception {
JdbcUserDetailsService jdbcUserDetailsService = new JdbcUserDetailsService();
jdbcUserDetailsService.setDataSource(dataSource);
return jdbcUserDetailsService;
}
/**
* 권한의 계층정보를 제공한다.
* @return
*/
@Bean
public ReloadableRoleHierarchy reloadableRoleHierarchy(RoleHierarchyService RoleHierarchyService) {
ReloadableRoleHierarchy reloadableRoleHierarchy = new ReloadableRoleHierarchy();
reloadableRoleHierarchy.setRoleHierarchyService(RoleHierarchyService);
return reloadableRoleHierarchy;
}
/**
* DB를 통해 권한의 계층정보를 획득
* @param dataSource
* @return
*/
@Bean
@ConditionalOnMissingBean({RoleHierarchyService.class})
public JdbcRoleHierarchyService jdbcRoleHierarchyService(DataSource dataSource) {
JdbcRoleHierarchyService roleHierarchyService = new JdbcRoleHierarchyService();
roleHierarchyService.setDataSource(dataSource);
return roleHierarchyService;
}
/**
* 웹 표현식 처리 검사를 수행하기 위한 핸들러
* @return
*/
@Bean
public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler(RoleHierarchyService RoleHierarchyService) {
DefaultWebSecurityExpressionHandler expressionHandler = new DefaultWebSecurityExpressionHandler();
expressionHandler.setRoleHierarchy(this.reloadableRoleHierarchy(RoleHierarchyService));
expressionHandler.setDefaultRolePrefix("");
return expressionHandler;
}
/**
* 획득한 권한의 계층정보를 갖고 URL리소스에 대해 사용자가 접근할 권한이 있는지 결정 해주는 판단주체
* @return
*/
@Bean
public AccessDecisionManager accessDecisionManager(RoleHierarchyService RoleHierarchyService) {
WebExpressionVoter webExpressionVoter = new WebExpressionVoter();
webExpressionVoter.setExpressionHandler(this.webSecurityExpressionHandler(RoleHierarchyService));
RoleHierarchyVoter roleHierarchyVoter = new RoleHierarchyVoter(this.reloadableRoleHierarchy(RoleHierarchyService));
roleHierarchyVoter.setRolePrefix("");
List<AccessDecisionVoter<?>> decisionVoters = Arrays.asList(webExpressionVoter, roleHierarchyVoter);
return new AffirmativeBased(decisionVoters);
}
/**
* 모든 요청에 대해 권한이 적절한지 검사를 수행하는 Interceptor
* 주어진 인증과 요청한 URL리소스에 대해 사용자가 접근할 권한이 있는지 결정
* @param authenticationManager
* @param accessDecisionManager
* @param reloadableFilterInvocationSecurityMetadataSource
* @return
*/
public FilterSecurityInterceptor filterSecurityInterceptor(AuthenticationManager authenticationManager, AccessDecisionManager accessDecisionManager, ReloadableFilterInvocationSecurityMetadataSource reloadableFilterInvocationSecurityMetadataSource) {
FilterSecurityInterceptor customFilterSecurityInterceptor = new FilterSecurityInterceptor();
//인증 처리 주체로서, 사용자의 인증 요청을 처리하는 AuthenticationManager를 설정
customFilterSecurityInterceptor.setAuthenticationManager(authenticationManager);
//RDB로 관리되는 보호되는 자원(리소스)를 제공하는 메타데이터소스
customFilterSecurityInterceptor.setSecurityMetadataSource(reloadableFilterInvocationSecurityMetadataSource);
//리소스에 대한 접근 여부를 판단하게 될 AccessDecisionManager를 설정
//주어진 인증과 요청한 URL리소스에 대해 사용자가 접근할 권한이 있는지 결정
customFilterSecurityInterceptor.setAccessDecisionManager(accessDecisionManager);
//권한을 부여하지 않은 리소스에 대한 접근 시 오류 발생 여부 설정
customFilterSecurityInterceptor.setRejectPublicInvocations(true);
return customFilterSecurityInterceptor;
}
/**
* URL 리소스 정보를 런타임에서 다시 로드(reload)할 수 있는 기능
* @return
*/
@Bean
public ReloadableFilterInvocationSecurityMetadataSource reloadableFilterInvocationSecurityMetadataSource() {
ReloadableFilterInvocationSecurityMetadataSource reloadableFilterInvocationSecurityMetadataSource = new ReloadableFilterInvocationSecurityMetadataSource();
reloadableFilterInvocationSecurityMetadataSource.setSecuredUrlResourceService(securedUrlResourceService(dataSource));
return reloadableFilterInvocationSecurityMetadataSource;
}
/**
* URL 리소스에 대한 권한정보를 제공하는 서비스
* @param dataSource
* @return
*/
@Bean
public SecuredUrlResourceService securedUrlResourceService(DataSource dataSource) {
SecuredUrlResourceService securedUrlResourceService = new SecuredUrlResourceService();
securedUrlResourceService.setDataSource(dataSource);
return securedUrlResourceService;
}
/**
* Security 관련 된 API를 제공하는 서비스
* @param reloadableFilterInvocationSecurityMetadataSource
* @param reloadableRoleHierarchy
* @return
*/
@Bean
public SecurityService securityService(ReloadableFilterInvocationSecurityMetadataSource reloadableFilterInvocationSecurityMetadataSource, ReloadableRoleHierarchy reloadableRoleHierarchy) {
SecurityService securityService = new SecurityService();
securityService.setReloadableFilterInvocationSecurityMetadataSource(reloadableFilterInvocationSecurityMetadataSource);
securityService.setReloadableRoleHierarchy(reloadableRoleHierarchy);
return securityService;
}
/**
* 인증 성공과 실패에 따른 후처리에 필요한 다양한 기능
* @param dataSource
* @param messageSource
* @return
* @throws SQLException
*/
@Bean
public JdbcLoginService jdbcLoginService(DataSource dataSource, MessageSource messageSource)
throws SQLException {
JdbcLoginService jdbcLoginService = new JdbcLoginService();
jdbcLoginService.setDataSource(dataSource);
jdbcLoginService.setMessageSource(messageSource);
return jdbcLoginService;
}
/**
* JwtFilter
* @return
*/
public JwtFilter jwtFilter() {
return new JwtFilter(jwtTokenUtils());
}
/**
* JwtUtils
* @return
*/
@Bean
public JwtTokenUtils jwtTokenUtils() {
return new JwtTokenUtils();
}
/**
* Jwt인증, 갱신 Bean
* @return
*/
@Bean
@ConditionalOnMissingBean({JwtAuthInterface.class})
public JwtAuthentication jwtAuthentication()
{
return new JwtAuthentication();
}
/**
* Cors 설정
* @return
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
for (ChamomileSecurityCorsProperties.CorsConfigurationProperties configProps : chamomileSecurityCorsProperties.getConfigurations()) {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(configProps.getAllowedOrigins());
config.setAllowedMethods(configProps.getAllowedMethods());
config.setAllowedHeaders(configProps.getAllowedHeaders());
config.setExposedHeaders(configProps.getExposedHeaders());
config.setAllowCredentials(configProps.getAllowCredentials());
config.setMaxAge(configProps.getMaxAge());
source.registerCorsConfiguration(configProps.getPattern(), config);
}
return source;
}
/**
* Webfilter
* IgnoreParrterns 설정시 HttpSecurity 보다 우선실행 (filterChain 이전) 되어 Cors 적용 불가
* client 에서 pre-flight request 요청시 필요 ((Access-Control-Allow 헤더 전역설정)
* @return
*/
@Bean
public FilterRegistrationBean<CorsFilter> loggingFilter() {
FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new CorsFilter());
registrationBean.addUrlPatterns(securityProperties.getIgnorePatterns()); // 이 URL 패턴에만 필터 적용
return registrationBean;
}
}