스프링 시큐리티를 적용하고 아이디, 패스워드 방식으로 로그인을 제어하기 위해 추가함.
// Spring security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
컨피그 객체로 관리할 예정이기 때문에 필수 설정은 없음.
검증 예외를 관리하기 위해 yaml 에 추가함.
1. yaml 설정 추가security: permit-all-request-patterns: - "/actuator/**" - "/**/swagger-ui/**" - "/**/api-docs/**" - "/css/**" - "/_scripts/**" - "/favicon.ico" - "/images/**"
@Configuration
@Setter
@Getter
@ConfigurationProperties(prefix = "security")
public class SecurityProperties {
private List<String> permitAllRequestPatterns;
}
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;
}
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(", "));
}
}
}
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final MemberRepository memberRepository; // 회원 정보 가져올 레포지토리로 변경
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return memberRepository.getAdminMember(username);
}
}
@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);
}
}
@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";
}
}
}