ChamomileGuides 3.0.4 Help

시큐리티(JWT) 모듈 커스터마이징 가이드

토큰발급(로그인요청) 커스텀 예제

토큰발급은 프로젝트 요구 사항에 맞게 수정이 가능하다.

CustomAccessTokenConfig.java

프로젝트 내에 특정 패키지에 아래 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; } }
Last modified: 21 4월 2025