@ExceptionHandler로 스프링 보안 인증 예외 처리
저는 Spring MVC를 사용 @ControllerAdvice
하고 있으며 @ExceptionHandler
REST Api의 모든 예외를 처리합니다. 웹 mvc 컨트롤러에서 throw 된 예외에 잘 작동하지만 컨트롤러가 호출되기 실행되기 때문에 스프링 보안 사용자 지정 필터에 의해 던지기 전에 작동하지 않습니다.
토큰 기반 인증을 수행하는 사용자 지정 스프링 보안 필터가 있습니다.
public class AegisAuthenticationFilter extends GenericFilterBean {
...
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
try {
...
} catch(AuthenticationException authenticationException) {
SecurityContextHolder.clearContext();
authenticationEntryPoint.commence(request, response, authenticationException);
}
}
}
이 사용자 지정 진입 점 사용 :
@Component("restAuthenticationEntryPoint")
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint{
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authenticationException.getMessage());
}
}
이 클래스를 사용하여 예외를 전역 적으로 처리합니다.
@ControllerAdvice
public class RestEntityResponseExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler({ InvalidTokenException.class, AuthenticationException.class })
@ResponseStatus(value = HttpStatus.UNAUTHORIZED)
@ResponseBody
public RestError handleAuthenticationException(Exception ex) {
int errorCode = AegisErrorCode.GenericAuthenticationError;
if(ex instanceof AegisException) {
errorCode = ((AegisException)ex).getCode();
}
RestError re = new RestError(
HttpStatus.UNAUTHORIZED,
errorCode,
"...",
ex.getMessage());
return re;
}
}
내가해야 할 일은 스프링 보안 AuthenticationException에 상세한 JSON 본문을 반환하는 것입니다. 스프링 보안 AuthenticationEntryPoint와 spring mvc @ExceptionHandler가 함께 작동하는 방법이 있습니까?
저는 스프링 보안 3.1.4와 스프링 mvc 3.2.4를 사용하고 있습니다.
좋아, AuthenticationEntryPoint에서 json을 직접 작성하는 것이 좋습니다.
테스트를 위해 response.sendError를 제거하여 AutenticationEntryPoint를 변경했습니다.
@Component("restAuthenticationEntryPoint")
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint{
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException, ServletException {
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getOutputStream().println("{ \"error\": \"" + authenticationException.getMessage() + "\" }");
}
}
이런 식으로 Spring Security AuthenticationEntryPoint를 사용하는 경우에도 승인되지 않은 401과 함께 사용자 정의 json 데이터를 보낼 수 있습니다.
분명히 테스트 목적으로 한 것처럼 json을 빌드하지 않는 일부 클래스 인스턴스를 소규모화합니다.
이것은 Spring Security 와 Spring Web 프레임 워크가 응답을 처리하는 방식이 매우 일관 적이 지 않다는 매우 흥미로운 문제입니다 . MessageConverter
방법으로 오류 메시지 처리를 기본적으로 지원해야 할 것 입니다.
나는 MessageConverter
그들의 예외를 적용하고 내용 협상에 따라 올바른 형식으로 봄 보안 에 필요한 우아한 방법을 도입하고 노력 했다 . 그래도 아래의 솔루션은 우아하지만 스프링 코드를 사용합니다.
Jackson 및 JAXB 라이브러리를 포함하는 방법을 알고 가정합니다. 정렬되지 않습니다. 총 3 단계가 있습니다.
1 단계 -MessageConverters를 저장하는 독립 실행 형 클래스 만들기
이 클래스는 마술을하지 않습니다. 메시지 변환기와 프로세서 만 저장합니다 RequestResponseBodyMethodProcessor
. 마법은 콘텐츠 협상을 포함하여 모든 작업을 수행하고 그에 따라 응답 본문을 변환하는 프로세서 내부에 있습니다.
public class MessageProcessor { // Any name you like
// List of HttpMessageConverter
private List<HttpMessageConverter<?>> messageConverters;
// under org.springframework.web.servlet.mvc.method.annotation
private RequestResponseBodyMethodProcessor processor;
/**
* Below class name are copied from the framework.
* (And yes, they are hard-coded, too)
*/
private static final boolean jaxb2Present =
ClassUtils.isPresent("javax.xml.bind.Binder", MessageProcessor.class.getClassLoader());
private static final boolean jackson2Present =
ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", MessageProcessor.class.getClassLoader()) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", MessageProcessor.class.getClassLoader());
private static final boolean gsonPresent =
ClassUtils.isPresent("com.google.gson.Gson", MessageProcessor.class.getClassLoader());
public MessageProcessor() {
this.messageConverters = new ArrayList<HttpMessageConverter<?>>();
this.messageConverters.add(new ByteArrayHttpMessageConverter());
this.messageConverters.add(new StringHttpMessageConverter());
this.messageConverters.add(new ResourceHttpMessageConverter());
this.messageConverters.add(new SourceHttpMessageConverter<Source>());
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
if (jaxb2Present) {
this.messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
}
if (jackson2Present) {
this.messageConverters.add(new MappingJackson2HttpMessageConverter());
}
else if (gsonPresent) {
this.messageConverters.add(new GsonHttpMessageConverter());
}
processor = new RequestResponseBodyMethodProcessor(this.messageConverters);
}
/**
* This method will convert the response body to the desire format.
*/
public void handle(Object returnValue, HttpServletRequest request,
HttpServletResponse response) throws Exception {
ServletWebRequest nativeRequest = new ServletWebRequest(request, response);
processor.handleReturnValue(returnValue, null, new ModelAndViewContainer(), nativeRequest);
}
/**
* @return list of message converters
*/
public List<HttpMessageConverter<?>> getMessageConverters() {
return messageConverters;
}
}
2 단계 -AuthenticationEntryPoint 만들기
많은 안내에서와 같이이 클래스는 사용자 지정 오류 처리를 구현하는 데 존재합니다.
public class CustomEntryPoint implements AuthenticationEntryPoint {
// The class from Step 1
private MessageProcessor processor;
public CustomEntryPoint() {
// It is up to you to decide when to instantiate
processor = new MessageProcessor();
}
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
// This object is just like the model class,
// the processor will convert it to appropriate format in response body
CustomExceptionObject returnValue = new CustomExceptionObject();
try {
processor.handle(returnValue, request, response);
} catch (Exception e) {
throw new ServletException();
}
}
}
3 단계-진입 점 등록
언급했듯이 Java Config로 수행합니다. 여기에 관련 구성 만 표시하고 세션 상태 비 저장 같은 다른 구성이 있어야합니다 .
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling().authenticationEntryPoint(new CustomEntryPoint());
}
}
일부 인증 실패 사례로 시도 시도. 요청 헤더에 Accept : XXX 가 포함 되어야하며 JSON, XML 또는 기타 형식으로 예외가 발생해야합니다.
내가 가장 좋은 방법은 예외를 HandlerExceptionResolver에 맡기는 것입니다.
@Component("restAuthenticationEntryPoint")
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Autowired
private HandlerExceptionResolver resolver;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
resolver.resolveException(request, response, null, exception);
}
}
그런 다음 @ExceptionHandler를 사용하여 원하는 방식으로 응답 형식을 수 있습니다.
Spring Boot 및의 경우 Java 구성 대신 @EnableResourceServer
확장 하고 메서드 내부에서 재정의 및 사용하여 사용자 지정 을 등록하는 것이 좋습니다 .ResourceServerConfigurerAdapter
WebSecurityConfigurerAdapter
AuthenticationEntryPoint
configure(ResourceServerSecurityConfigurer resources)
resources.authenticationEntryPoint(customAuthEntryPoint())
이 같은 :
@Configuration
@EnableResourceServer
public class CommonSecurityConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.authenticationEntryPoint(customAuthEntryPoint());
}
@Bean
public AuthenticationEntryPoint customAuthEntryPoint(){
return new AuthFailureHandler();
}
}
또한 OAuth2AuthenticationEntryPoint
확장 할 수 있고 (최종이 아니기 때문에) 사용자 지정 구현하는 동안을 부분적으로 재사용 할 수 있는 멋진 기능 현관 도 계명 있습니다 AuthenticationEntryPoint
. 특히 오류 관련 세부 정보와 함께 "WWW-Authenticate"헤더를 추가합니다.
이것이 누군가를 도울 수 있기를 바랍니다.
@Nicola 및 @Victor Wing의 답변을 받아보다 표준화 된 방법을 추가합니다.
import org.springframework.beans.factory.InitializingBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class UnauthorizedErrorAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean {
private HttpMessageConverter messageConverter;
@SuppressWarnings("unchecked")
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
MyGenericError error = new MyGenericError();
error.setDescription(exception.getMessage());
ServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
outputMessage.setStatusCode(HttpStatus.UNAUTHORIZED);
messageConverter.write(error, null, outputMessage);
}
public void setMessageConverter(HttpMessageConverter messageConverter) {
this.messageConverter = messageConverter;
}
@Override
public void afterPropertiesSet() throws Exception {
if (messageConverter == null) {
throw new IllegalArgumentException("Property 'messageConverter' is required");
}
}
}
이제는 Jackson, Jaxb 또는 MVC 주석 또는 XML 기반 구성의 응답 본문을 serializer, deserializer 등으로 변환하는 데 사용하는 모든 것을 삽입 할 수 있습니다.
HandlerExceptionResolver
이 경우 에 경우에 합니다.
@Component
public class RESTAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Autowired
//@Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
resolver.resolveException(request, response, null, authException);
}
}
또한 개체를 반환 예외 처리기 클래스를 추가해야합니다.
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(AuthenticationException.class)
public GenericResponseBean handleAuthenticationException(AuthenticationException ex, HttpServletResponse response){
GenericResponseBean genericResponseBean = GenericResponseBean.build(MessageKeys.UNAUTHORIZED);
genericResponseBean.setError(true);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return genericResponseBean;
}
}
의 여러 구현으로 인해 프로젝트를 실행할 때 오류가 발생할 수 있습니다 HandlerExceptionResolver
.이 경우 추가해야합니다 @Qualifier("handlerExceptionResolver")
.HandlerExceptionResolver
필터에서 'unsuccessfulAuthentication'메서드를 간단히 처리 할 수 있습니다. 거기에서 원하는 HTTP 상태 코드로 클라이언트에 오류 응답을 보냅니다.
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
if (failed.getCause() instanceof RecordNotFoundException) {
response.sendError((HttpServletResponse.SC_NOT_FOUND), failed.getMessage());
}
}
objectMapper를 사용하고 있습니다. 모든 휴식 서비스는 대부분 json과 함께 작동하며 구성 중 하나에서 이미 개체 매퍼를 구성했습니다.
코드는 Kotlin으로 만들어져 괜찮을 것입니다.
@Bean
fun objectMapper(): ObjectMapper {
val objectMapper = ObjectMapper()
objectMapper.registerModule(JodaModule())
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
return objectMapper
}
class UnauthorizedAuthenticationEntryPoint : BasicAuthenticationEntryPoint() {
@Autowired
lateinit var objectMapper: ObjectMapper
@Throws(IOException::class, ServletException::class)
override fun commence(request: HttpServletRequest, response: HttpServletResponse, authException: AuthenticationException) {
response.addHeader("Content-Type", "application/json")
response.status = HttpServletResponse.SC_UNAUTHORIZED
val responseError = ResponseError(
message = "${authException.message}",
)
objectMapper.writeValue(response.writer, responseError)
}}
업데이트 : 당신이 좋아하고 코드를 직접 보는 것을 선호한다면 두 가지 예가 있습니다. 하나는 당신이보고있는 표준 Spring Security를 사용하고 다른 하나는 Reactive Web과 Reactive Security에서 동등한 것을 사용하는 것입니다.
- 일반 웹 + Jwt 보안
- 반응 형 Jwt
JSON 기반 엔드 포인트는 항상 사용하는 것은 다음과 같습니다.
@Component
public class JwtAuthEntryPoint implements AuthenticationEntryPoint {
@Autowired
ObjectMapper mapper;
private static final Logger logger = LoggerFactory.getLogger(JwtAuthEntryPoint.class);
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException e)
throws IOException, ServletException {
// Called when the user tries to access an endpoint which requires to be authenticated
// we just return unauthorizaed
logger.error("Unauthorized error. Message - {}", e.getMessage());
ServletServerHttpResponse res = new ServletServerHttpResponse(response);
res.setStatusCode(HttpStatus.UNAUTHORIZED);
res.getServletResponse().setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
res.getBody().write(mapper.writeValueAsString(new ErrorResponse("You must authenticated")).getBytes());
}
}
객체 매퍼는 스프링 웹 스타터를 추가 한 후에 이미 빈이지만 사용자 정의하는 것을 선호하므로 ObjectMapper에 대해 수행하는 작업은 다음과 같습니다.
@Bean
public Jackson2ObjectMapperBuilder objectMapperBuilder() {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.modules(new JavaTimeModule());
// for example: Use created_at instead of createdAt
builder.propertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
// skip null fields
builder.serializationInclusion(JsonInclude.Include.NON_NULL);
builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return builder;
}
WebSecurityConfigurerAdapter 클래스에서 기본값으로 설정 한 AuthenticationEntryPoint :
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ............
@Autowired
private JwtAuthEntryPoint unauthorizedHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
// .antMatchers("/api/auth**", "/api/login**", "**").permitAll()
.anyRequest().permitAll()
.and()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.headers().frameOptions().disable(); // otherwise H2 console is not available
// There are many ways to ways of placing our Filter in a position in the chain
// You can troubleshoot any error enabling debug(see below), it will print the chain of Filters
http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
// ..........
}
'IT' 카테고리의 다른 글
어느 정도 : 왜 *와 **가 /와 sqrt ()보다 빠릅니까? (0) | 2020.10.10 |
---|---|
사전 값 검색 모범 사례 (0) | 2020.10.10 |
로그인 후 SSH 사용자를 사전 정의 된 명령 세트로 제한하는 방법은 무엇입니까? (0) | 2020.10.10 |
인덱싱 된 열의 MongoDB 선택 횟수 (고유 x)-대용량 데이터 세트에 대한 고유 한 결과 계산 (0) | 2020.10.10 |
flask-sqlalchemy 또는 sqlalchemy (0) | 2020.10.10 |