스프링 시큐리티 적용

김동영·2024년 4월 25일
1

스프링 시큐리티를 적용하고 아이디, 패스워드 방식으로 로그인을 제어하기 위해 추가함.

적용 방법

1. 디펜던시 추가

// Spring security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

2. yaml 설정 및 프로퍼티 객체 추가

컨피그 객체로 관리할 예정이기 때문에 필수 설정은 없음.
검증 예외를 관리하기 위해 yaml 에 추가함.
1. yaml 설정 추가

security:
  permit-all-request-patterns:
    - "/actuator/**"
    - "/**/swagger-ui/**"
    - "/**/api-docs/**"
    - "/css/**"
    - "/_scripts/**"
    - "/favicon.ico"
    - "/images/**"
  1. 프로퍼티 객체 추가
@Configuration
@Setter
@Getter
@ConfigurationProperties(prefix = "security")
public class SecurityProperties {
    private List<String> permitAllRequestPatterns;
}

3. 시큐리티 설정 객체 추가

  1. CORS 빈 추가
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
...

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        var configuration = new CorsConfiguration();
        configuration.applyPermitDefaultValues();
        configuration.setAllowedOriginPatterns(List.of("*"));
        configuration.setAllowedMethods(List.of("*"));
        configuration.setAllowedHeaders(
                Arrays.asList(
                        HttpHeaders.AUTHORIZATION,
                        HttpHeaders.CONTENT_TYPE,
                        HttpHeaders.CONTENT_LENGTH,
                        HttpHeaders.ACCEPT_LANGUAGE));
        configuration.setMaxAge(1728000L);
        configuration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);

        return source;
    }
  1. UserDetails 구현체 추가
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

/**
 *
 *
 * <h3>Admin member details</h3>
 *
 * @author dongyoung.kim
 * @since 1.0
 */
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class UserDetailsImpl implements UserDetails, Serializable {
    private Long adminMemberNo;

    private String id;

    private String password;

    private String employeeNo;

    private String name;

    private String mobileNo;

    private String email;

    private Long createdAt;

    private List<AdminResourceAuthority> resourceAuthorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return resourceAuthorities;
    }

    @Override
    public String getUsername() {
        return this.id;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    @Getter
    @AllArgsConstructor
    @NoArgsConstructor
    public static class AdminResourceAuthority implements GrantedAuthority {

        private Long resourceAuthorityNo;
        private String resourceName;
        private Boolean isReadable;
        private Boolean isWriteable;
        private Boolean isDeletable;

        @Override
        public String getAuthority() {
            return Stream.of(
                            Boolean.TRUE.equals(isReadable) ? "READ" : "",
                            Boolean.TRUE.equals(isWriteable) ? "WRITE" : "",
                            Boolean.TRUE.equals(isDeletable) ? "DELETE" : "")
                    .filter(s -> !s.isEmpty())
                    .map(s -> resourceAuthorityNo + "_" + s)
                    .collect(Collectors.joining(", "));
        }
    }
}
  1. UserDetailsService 구현체 추가
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {

    private final MemberRepository memberRepository; // 회원 정보 가져올 레포지토리로 변경

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return memberRepository.getAdminMember(username);
    }
}
  1. Security Config 추가
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    private final CorsConfigurationSource corsConfigurationSource;
    private final UserDetailsService userDetailsService;
    private final UriProperties uriProperties;
    private final ObjectMapper objectMapper;
    private final LoginHistoryRepository loginHistoryRepository;

    private final SecurityProperties securityProperties;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.formLogin(
            configurer ->
                configurer
                    .loginPage(uriProperties.getLoginUri())
                    .successHandler(
                        ((request, response, authentication) -> {
                            var memberDetails =
                                (UserDetailsImpl) authentication.getPrincipal();
                            var remoteAddress =
                                Optional.ofNullable(authentication.getDetails())
                                        .map(
                                            WebAuthenticationDetails.class
                                                ::cast)
                                        .map(
                                            WebAuthenticationDetails
                                                ::getRemoteAddress)
                                        .orElse(null);
                            loginHistoryRepository
                                .saveLoginEvent(memberDetails, remoteAddress);
                            response.sendRedirect(uriProperties.getLoginSuccessUri());
                        }))
                    .failureUrl(uriProperties.getLoginUri()));
        httpSecurity.userDetailsService(userDetailsService);
        httpSecurity.csrf(AbstractHttpConfigurer::disable);
        httpSecurity.cors(configurer -> configurer.configurationSource(corsConfigurationSource));
        httpSecurity.headers().frameOptions().sameOrigin();
        httpSecurity.authorizeHttpRequests(
            request ->
                request.antMatchers(HttpMethod.POST, uriProperties.getLoginUri())
                       .permitAll()
                       .antMatchers(HttpMethod.GET, uriProperties.getLoginUri())
                       .permitAll()
                       .anyRequest()
                       .authenticated());
        httpSecurity.logout(
            configurer ->
                configurer
                    .logoutUrl(uriProperties.getLogoutUri())
                    .logoutSuccessHandler(
                        ((request, response, authentication) ->
                            response.sendRedirect(
                                uriProperties.getLogoutSuccessUri()))));
        httpSecurity.addFilterBefore(
            new AuthenticationProcessingFilter(objectMapper),
            UsernamePasswordAuthenticationFilter.class
        );
        return httpSecurity.build();
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    WebSecurityCustomizer ignoreSecurity() {
        var permitAllRequests =
            securityProperties.getPermitAllRequestPatterns().stream()
                  .map(AntPathRequestMatcher::new)
                  .toArray(AntPathRequestMatcher[]::new);
        return web -> web.ignoring().requestMatchers(permitAllRequests);
    }
}
  1. before filter 추가(선택사항)
@Slf4j
@RequiredArgsConstructor
public class AuthenticationProcessingFilter extends OncePerRequestFilter {

    private final ObjectMapper objectMapper;

    @Override
    public void doFilterInternal(
            HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
        var responseWrapper = new ContentCachingResponseWrapper(response);
        try {
            filterChain.doFilter(request, responseWrapper);
            if (responseWrapper.getStatus() != HttpStatus.OK.value()) {
                log.error(
                        "Authentication failed. Request logging.\nRequest URI: [{}]{}\nheaders: {}\nrequestBody: {}",
                        request.getMethod(),
                        request.getRequestURI(),
                        getHeaderLog(request),
                        getRequestBody(request));

                // WWW-Authenticate
                var status = responseWrapper.getStatus();
                var responseHeader = getResponseHeader(responseWrapper);
                var body = responseWrapper.getContentAsByteArray();
                var bodyString =
                        objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(body);
                log.error(
                        "Response logging.\nstatus: {}\nheaders: {} \nresponseBody: {}",
                        status,
                        responseHeader,
                        bodyString);
                //                if (body.length == 0) {
                //                    ExceptionCode exceptionCode;
                //                    if (status == HttpStatus.UNAUTHORIZED.value()) {
                //                        exceptionCode = ExceptionCode.UNAUTHORIZED_USER;
                //                    } else {
                //                        log.error("Exception! responseBody is Null. Set 5001
                // Manually.");
                //                        exceptionCode =
                // ExceptionCode.INTERNAL_SERVER_ERROR_TOKEN_ISSUED;
                //                    }
                //                    var internalResponse = CommonResponse.error(exceptionCode);
                //
                // responseWrapper.setStatus(exceptionCode.getHttpStatus().value());
                //
                // responseWrapper.setContentType(MediaType.APPLICATION_JSON_VALUE);
                //                    responseWrapper.setCharacterEncoding("utf-8");
                //
                // responseWrapper.getWriter().write(objectMapper.writeValueAsString(internalResponse));
                //                }
            }
            responseWrapper.copyBodyToResponse();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            //            ExceptionCode exceptionCode;
            //            if (responseWrapper.getStatus() == HttpStatus.UNAUTHORIZED.value()) {
            //                exceptionCode = ExceptionCode.UNAUTHORIZED_USER;
            //            } else {
            //                log.error("Unknown exception occurred. Logging and set 5001
            // Manually.");
            //                exceptionCode = ExceptionCode.INTERNAL_SERVER_ERROR_TOKEN_ISSUED;
            //            }
            //            var internalResponse = CommonResponse.error(exceptionCode);
            //            response.setStatus(exceptionCode.getHttpStatus().value());
            //            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            //            response.setCharacterEncoding("utf-8");
            //
            // response.getWriter().write(objectMapper.writeValueAsString(internalResponse));
        }
    }

    private String getHeaderLog(HttpServletRequest request) {
        var headerNames = request.getHeaderNames();
        var headerMap = new HashMap<String, String>();
        while (headerNames.hasMoreElements()) {
            var key = headerNames.nextElement();
            var value = request.getHeader(key);
            headerMap.put(key, value);
        }
        String headerString;
        try {
            headerString =
                    objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(headerMap);
        } catch (JsonProcessingException | RuntimeException e) {
            log.error(e.getMessage(), e);
            headerString = "parse exception";
        }
        return headerString;
    }

    private String getResponseHeader(HttpServletResponse response) {
        var headerNames = response.getHeaderNames();
        var headerMap = new HashMap<String, String>();
        for (var headerName : headerNames) {
            headerMap.put(headerName, response.getHeader(headerName));
        }
        String headerString;
        try {
            headerString =
                    objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(headerMap);
        } catch (JsonProcessingException | RuntimeException e) {
            log.error(e.getMessage(), e);
            headerString = "parse exception";
        }
        return headerString;
    }

    private String getRequestBody(HttpServletRequest request) {
        try {
            var requestBody = IOUtils.toString(request.getInputStream());
            return StringUtils.isNoneBlank(requestBody) ? requestBody : "RequestBody is null.";
        } catch (RuntimeException | IOException e) {
            log.error(e.getMessage(), e);
            return "Failed get requestBody";
        }
    }
}
profile
k8s, 프레임워크와 함께하는 백엔드 개발자입니다.

0개의 댓글