Кастомная авторизация Spring REST API

196
14 марта 2019, 01:10

Как грамотно и удобно организовать аутентификацию в Spring REST Api через JWT? Сессию не хочу использовать, потому что это REST и JWT. Проверка токена должна проходить при каждом запросе, который требует аутентификации. Токен хранится или в куки или передается в хедерах(это не столь важно).

Вопрос в том, как можно сделать аутентификацию, максимально простой в использовании, что бы написать, допустим, фильтр, и забыть про это. Что бы внутри каждого метода где нужна аутентификация, не дергать что то типо getUser(). Хочется просто написать и забыть, но что бы этот юзер уже был внутри метода и с ним можно было работать.

До этого писать Api на SparkJava, там все было очень просто. Вместо стандартного Route, там где надо было, возвращал самописный SecuredRoute, который в свою очередь имплементился от стандартного Route, и переопределял его стандартный метод handle, в котором была написана логика проверки авторизации, и если юзер авторизован, то дальше передавался handleSecured, в параметрах которого уже был сам юзер.

Код для наглядности, который был на spark

public interface SecuredRoute extends Route {
    @Override
    default Object handle(Request request, Response response) {
        // Логика проверки...
        if (юзер не прошел провеку) {
            response.removeCookie("/", "accessToken");
            return new ErrorResponse(response).errorResponseUnauthenticated();
        }
        return handleSecured(request, response, user);
    }
    Object handleSecured(Request request, Response response, User user);
}

Можно ли что-либо подобное сделать на Spring?

Мне кажется что в идеале было бы создать какую то свою аннотацию, которая ставилась бы перед методом, в котором нужна авторизация, и как то инжектила этого юзера в метод

Answer 1

Итак, после продолжительных поисков, в интернете было найдено пара статей, на основе которых удалось сделать то что хотелось. Получилось добавить немного "магии" в Spring.

Я не хотел использовать Spring Security, потому что мне показалось не очень удобных прятать все урлы, доступ к которым есть только у авторизованных пользователей, за общий, например "/sicret/**" поэтому пришлось написать небольшой кусок этого Security самому.

Просто оставлю это здесь, вдруг у кого то будет такая же потребность.

Аутентификация происходит "за кулисами" с помощью аннотации, в которой достается jwt из куки или из хедеров. Происходит парсинг этого jwt, достается юзер и "прокидывается" внутрь метода, в котором нужен авторизованный пользователь. В случае если все ок, то есть токен был распаршен и юзер получен, то он "прокидывается" в метод, если же что-то пошло не так, то выбрасываем свое исключение, которое потом будем обрабатывать в глобальном обработчике эксепшенов, и возвращать JSON ответ вместо стандартного респонса в виде html страницы.

Теперь сам код

Аннотация. Никакие параметры в нее передаваться не будут

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthUser {
}

"Обработчик" аннотации. Оставлю только основной код, так как логика обработки токена и получения юзера здесь не столь важна

public class AuthUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {    
    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        return methodParameter.getParameterAnnotation(AuthUser.class) != null && methodParameter.getParameterType().equals(User.class);
    }
    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
        if (this.supportsParameter(methodParameter)) {
            // Получаем токен из куки или хедера, парсим его
            if (с юзером все ок) {
                return user;
            } else {
                throw new FailedAuthenticatedException();
            }
        }
        return WebArgumentResolver.UNRESOLVED;
    }
}

Регистрация обработчика

@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new AuthUserHandlerMethodArgumentResolver());
    }
}

Контроллер

@RequestMapping(value = EndPoints.AUTH_CHECK_AUTHORIZED_WITH_COOKIE, method = RequestMethod.GET)
public Response getUserInfo(@AuthUser User user) {
// здесь уже можно работать с юзером как с аутентифицированным
}

Глобальный обработчик эксепшенов

@ControllerAdvice
public class GlobalHandlerExceptionControllerAdvice {
    @ExceptionHandler({Exception.class})
    public Response databaseError(Exception exception, HttpServletResponse response) {
        if (exception instanceof FailedAuthenticatedException) {
            // сюда попадаем в том случае, если ловим наш эксепшен, то есть, когда юзер не аутентифицирован. И возвращаем наш response в виде json в качестве ошибки
           return new ErrorResponse(ErrorResponse.Error.AUTHENTICATION_ERROR);
        }
        // в остальных случаях возвращаем сообщение об ошибке
        return new ErrorResponse(ErrorResponse.Error.INTERNAL_ERROR);
    }    
}
READ ALSO
приложение OCR для андроида

приложение OCR для андроида

Я создаю приложение, которое распознает текст из фото, У меня почти всё готово, но приложение распознает весь участок изображения, Мне необходимо...

168
Java Возврат неправильного числа

Java Возврат неправильного числа

Строка лишь один раз возвращает отрицательное числоЧего не хватает чтобы возвращала при каждом вводе отрицательно числа в if (ln < 0 );

152
Как получить данные из json при помощи ajax

Как получить данные из json при помощи ajax

Пытаюсь реализовать Ajax Long polling в своем проекте вот скрин структуры:

167
Java работа с Timer (Circle)

Java работа с Timer (Circle)

Начинаю изучать Java

208