Как выполнить базовую аутентификацию ресурса в Dropwizard

Я считаю, что у меня работает базовая аутентификация, но я не уверен, как защитить ресурсы, чтобы к ним можно было получить доступ только тогда, когда пользователь вошел в систему.

public class SimpleAuthenticator implements Authenticator<BasicCredentials, User> {
    UserDAO userDao;

    public SimpleAuthenticator(UserDAO userDao) {this.userDao = userDao;}

    @Override
    public Optional<User> authenticate(BasicCredentials credentials) throws AuthenticationException    
    {
        User user = this.userDao.getUserByName(credentials.getUsername());
        if (user!=null &&
                user.getName().equalsIgnoreCase(credentials.getUsername()) &&
                BCrypt.checkpw(credentials.getPassword(), user.getPwhash())) {
            return Optional.of(new User(credentials.getUsername()));
        }
        return Optional.absent();
    }
}

Мой ресурс входа выглядит следующим образом:

@Path("/myapp")
@Produces(MediaType.APPLICATION_JSON)
public class UserResource {
    @GET
    @Path("/signin")
    public User signin(@Auth User user) {
        return user;
    }
}

И я подписываю пользователя с помощью:

~/java/myservice $ curl -u "someuser" http://localhost:8080/myapp/signin
Enter host password for user 'someuser':
{"name":"someuser"}

Вопрос

Допустим, пользователь входит в систему из браузера или пользовательского интерфейса мобильного приложения, используя конечную точку /myapp/signin. Как тогда я могу защитить другую конечную точку, скажем, /myapp/{username}/getstuff, для которой требуется вход пользователя

@GET
@Path("/myapp/{username}/getstuff")
public Stuff getStuff(@PathParam("username") String username) {
    //some logic here
    return new Stuff();
}

person birdy    schedule 18.12.2013    source источник


Ответы (2)


Есть две вещи, когда вы пытаетесь внедрить REST. Один из них - Аутентификация (кажется, что он у вас работает), а другой - Авторизация (именно это, как я полагаю, ваш вопрос).

То, как я обрабатывал это в dropwizard раньше, заключается в том, что при каждом входе пользователя вы возвращаете какой-то access_token (это доказывает, что они аутентифицированы) обратно клиенту, который должен быть возвращен ими в КАЖДОМ последующем вызове, который они делают как часть некоторого заголовок (обычно это делается через заголовок «Авторизация»). На стороне сервера вам нужно будет сохранить/сопоставить этот access_token с ЭТИМ пользователем, прежде чем возвращать его обратно клиенту, и когда все последовательные вызовы выполняются с этим access_token, вы ищете пользователя, сопоставленного с этим access_token, и определяете, является ли этот пользователь авторизован для доступа к этому ресурсу или нет. Теперь пример:

1) Пользователь входит в систему с помощью /myapp/signin

2) Вы аутентифицируете пользователя и отправляете обратно access_token в качестве ответа, сохраняя его на своей стороне, например, access_token -> userIdABCD

3) Клиент возвращается в /myapp/{username}/getstuff. Если клиент не предоставил заголовок «Авторизация» с предоставленным им access_token, вы должны немедленно вернуть 401 Неавторизованный код.

4) Если клиент предоставляет access_token, вы можете найти пользователя на основе того access_token, который вы сохранили на шаге № 2, и проверить, имеет ли этот userId доступ к этому ресурсу или нет. Если нет, вернуть 401 неавторизованный код, а если есть доступ, вернуть актуальные данные обратно.

Теперь перейдем к заголовку «Авторизация». Вы можете получить доступ к заголовку «Авторизация» во всех своих вызовах, используя параметр «@Context HttpServletRequest hsr», но имеет ли смысл добавлять этот параметр в каждый вызов? Нет, это не так. В этом помогают фильтры безопасности в dropwizard. Вот пример того, как добавить фильтр безопасности.

public class SecurityFilter extends OncePerRequestFilter{
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException{
String accessToken = request.getHeader("Authorization");
// Do stuff here based on the access token (check for user's authorization to the resource ...
}

Итак, какой ресурс действительно защищает этот фильтр безопасности? Для этого вам нужно будет добавить этот фильтр к конкретным ресурсам, которые вы хотите защитить, что можно сделать следующим образом:

environment.addFilter(SecurityFilter, "/myapp/*");

Помните, что оба ваших URL-адреса /myapp/signin и /myapp/{username}/getstuff, оба будут проходить через этот фильтр безопасности, НО, /myapp/signin НЕ будет иметь access_token, очевидно, потому что вы не дали ни одного клиенту еще. Об этом нужно будет позаботиться в самом фильтре, например:

String url = request.getRequestURL().toString();
if(url.endsWith("signin"))
{
// Don't look for authorization header, and let the filter pass without any checks
}
else
{
// DO YOUR NORMAL AUTHORIZATION RELATED STUFF HERE
}

URL-адрес, который вы защищаете, будет зависеть от того, как структурированы ваши URL-адреса и что вы хотите защитить. Чем лучше вы спроектируете URL-адреса, тем проще будет написать фильтры безопасности для их защиты. С добавлением этого фильтра безопасности процесс будет выглядеть следующим образом:

1) Пользователь переходит в /myapp/signin. Вызов пройдет через фильтр, и из-за этого оператора «если» он продолжит ваш АКТУАЛЬНЫЙ ресурс /myapp/signin, и вы назначите access_token на основе успешной аутентификации.

2) Пользователь делает вызов /myapp/{username}/mystuff с помощью access_token. Этот вызов пройдет через тот же фильтр безопасности и пройдет через оператор «else», где вы фактически выполняете свою авторизацию. Если авторизация пройдет, вызов продолжится для вашего фактического обработчика ресурсов, и если он не авторизован, должен быть возвращен 401.

public class SecurityFilter extends OncePerRequestFilter
{

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException
    {
        String url = request.getRequestURL().toString();
        String accessToken = request.getHeader("Authorization");
        try
        {
            if (accessToken == null || accessToken.isEmpty())
            {
                throw new Exception(Status.UNAUTHORIZED.getStatusCode(), "Provided access token is either null or empty or does not have permissions to access this resource." + accessToken);
            }
            if (url.endsWith("/signin"))
            {
    //Don't Do anything
                filterChain.doFilter(request, response);
            }
            else
            {
    //AUTHORIZE the access_token here. If authorization goes through, continue as normal, OR throw a 401 unaurhtorized exception

                filterChain.doFilter(request, response);
            }
        }
        catch (Exception ex)
        {
            response.setStatus(401);
            response.setCharacterEncoding("UTF-8");
            response.setContentType(MediaType.APPLICATION_JSON);
            response.getWriter().print("Unauthorized");
        }
    }
}

Надеюсь, это поможет! Мне понадобилось около 2 дней, чтобы понять это самостоятельно!

person shahshi15    schedule 18.12.2013
comment
Потрясающий. спасибо за отличное объяснение. два вопроса: 1) что вы используете для создания уникального идентификатора сеанса каждый раз? UUID? 2) что вы используете для хранения идентификаторов сеансов и сопоставления их с входящими? Хэш-карта? - person birdy; 18.12.2013
comment
когда вы говорите уникальный идентификатор сеанса, что вы имеете в виду? Токен доступа? Если да, то да, я использую UUID для токена доступа. Срок действия этих токенов доступа истекает каждый час (я использую OAuth, так что это соответствует стандартам, для базовой аутентификации это может быть по-другому), и поэтому в кластерной среде они хранятся не в памяти, а на всем пути к базе данных. Мы используем базу данных Couchbase (NoSql), так что, по сути, вся база данных для нас представляет собой большой огромный Hashmap :). Если вы делаете это в памяти, в кластерной среде, у вас начнутся проблемы с управлением этой картой в памяти на серверах приложений. - person shahshi15; 19.12.2013
comment
Забыл упомянуть. REST не имеет состояния в соответствии со стандартами, поэтому нет идентификаторов сеанса (хотя иногда я видел, как люди реализуют сеанс в REST, а также... не рекомендуется). Каждый запрос не имеет состояния, и для авторизации каждого запроса необходимо выполнить какой-либо токен (или базовую аутентификацию для каждого запроса). - person shahshi15; 19.12.2013
comment
Хорошо знать. На данный момент я не представляю себе кластерную среду. У меня будет только один сервер приложений, поэтому пока я просто сохраню токены на карте в памяти. Наконец, вы разрешаете пользователям регистрироваться с помощью OAuth? Не могли бы вы рассказать, как вы реализовали OAuth вкратце? Спасибо еще раз - person birdy; 19.12.2013
comment
Кроме того, в приведенном выше фрагменте. Что такое OncePerRequestFilter? - person birdy; 19.12.2013
comment
похоже, вы используете Широ - person birdy; 19.12.2013

Извините за то, что я простой пользователь. Я считаю, что вы можете защитить ресурс, используя пользователя @Auth User.

public Service1Bean Service1Method1(
    @Auth User user,
    @QueryParam("name") com.google.common.base.Optional<String> name) {
person Harihara Vinayakaram    schedule 07.09.2014
comment
Да, вы можете защитить ресурсы таким образом, но, как упоминает @xmenymenzmen, это только аутентификация, а не авторизация. - person Geert; 24.09.2014