Официальный пример безопасности Spring oauth2 не работает из-за конфликта файлов cookie (механизм кода авторизации)

Согласно руководству Spring Boot и OAuth2

У меня есть следующая структура проекта:

введите описание изображения здесь

И следующий исходный код:

SocialApplication.class:

@SpringBootApplication
@RestController
@EnableOAuth2Client
@EnableAuthorizationServer
@Order(200)
public class SocialApplication extends WebSecurityConfigurerAdapter {

    @Autowired
    OAuth2ClientContext oauth2ClientContext;

    @RequestMapping({ "/user", "/me" })
    public Map<String, String> user(Principal principal) {
        Map<String, String> map = new LinkedHashMap<>();
        map.put("name", principal.getName());
        return map;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // @formatter:off
        http.antMatcher("/**").authorizeRequests().antMatchers("/", "/login**", "/webjars/**").permitAll().anyRequest()
                .authenticated().and().exceptionHandling()
                .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/")).and().logout()
                .logoutSuccessUrl("/").permitAll().and().csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and()
                .addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class);
        // @formatter:on
    }

    @Configuration
    @EnableResourceServer
    protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
        @Override
        public void configure(HttpSecurity http) throws Exception {
            // @formatter:off
            http.antMatcher("/me").authorizeRequests().anyRequest().authenticated();
            // @formatter:on
        }
    }

    public static void main(String[] args) {
        SpringApplication.run(SocialApplication.class, args);
    }

    @Bean
    public FilterRegistrationBean<OAuth2ClientContextFilter> oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {
        FilterRegistrationBean<OAuth2ClientContextFilter> registration = new FilterRegistrationBean<OAuth2ClientContextFilter>();
        registration.setFilter(filter);
        registration.setOrder(-100);
        return registration;
    }

    @Bean
    @ConfigurationProperties("github")
    public ClientResources github() {
        return new ClientResources();
    }

    @Bean
    @ConfigurationProperties("facebook")
    public ClientResources facebook() {
        return new ClientResources();
    }

    private Filter ssoFilter() {
        CompositeFilter filter = new CompositeFilter();
        List<Filter> filters = new ArrayList<>();
        filters.add(ssoFilter(facebook(), "/login/facebook"));
        filters.add(ssoFilter(github(), "/login/github"));
        filter.setFilters(filters);
        return filter;
    }

    private Filter ssoFilter(ClientResources client, String path) {
        OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(
                path);
        OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext);
        filter.setRestTemplate(template);
        UserInfoTokenServices tokenServices = new UserInfoTokenServices(
                client.getResource().getUserInfoUri(),
                client.getClient().getClientId());
        tokenServices.setRestTemplate(template);
        filter.setTokenServices(new UserInfoTokenServices(
                client.getResource().getUserInfoUri(),
                client.getClient().getClientId()));
        return filter;
    }

}

class ClientResources {

    @NestedConfigurationProperty
    private AuthorizationCodeResourceDetails client = new AuthorizationCodeResourceDetails();

    @NestedConfigurationProperty
    private ResourceServerProperties resource = new ResourceServerProperties();

    public AuthorizationCodeResourceDetails getClient() {
        return client;
    }

    public ResourceServerProperties getResource() {
        return resource;
    }
}

index.html:

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <title>Demo</title>
    <meta name="description" content=""/>
    <meta name="viewport" content="width=device-width"/>
    <base href="/"/>
    <link rel="stylesheet" type="text/css"
          href="/webjars/bootstrap/css/bootstrap.min.css"/>
    <script type="text/javascript" src="/webjars/jquery/jquery.min.js"></script>
    <script type="text/javascript"
            src="/webjars/bootstrap/js/bootstrap.min.js"></script>
</head>
<body>
<h1>Login</h1>
<div class="container unauthenticated">
    With Facebook: <a href="/login/facebook">click here</a>
</div>
<div class="container authenticated" style="display: none">
    Logged in as: <span id="user"></span>
    <div>
        <button onClick="logout()" class="btn btn-primary">Logout</button>
    </div>
</div>
<script type="text/javascript"
        src="/webjars/js-cookie/js.cookie.js"></script>
<script type="text/javascript">
    $.ajaxSetup({
        beforeSend: function (xhr, settings) {
            if (settings.type == 'POST' || settings.type == 'PUT'
                || settings.type == 'DELETE') {
                if (!(/^http:.*/.test(settings.url) || /^https:.*/
                        .test(settings.url))) {
                    // Only send the token to relative URLs i.e. locally.
                    xhr.setRequestHeader("X-XSRF-TOKEN",
                        Cookies.get('XSRF-TOKEN'));
                }
            }
        }
    });
    $.get("/user", function (data) {
        $("#user").html(data.userAuthentication.details.name);
        $(".unauthenticated").hide();
        $(".authenticated").show();
    });
    var logout = function () {
        $.post("/logout", function () {
            $("#user").html('');
            $(".unauthenticated").show();
            $(".authenticated").hide();
        });
        return true;
    }
</script>
</body>
</html>

application.yml:

server:
  port: 8080
security:
  oauth2:
    client:
      client-id: acme
      client-secret: acmesecret
      scope: read,write
      auto-approve-scopes: '.*'

facebook:
  client:
    clientId: 233668646673605
    clientSecret: 33b17e044ee6a4fa383f46ec6e28ea1d
    accessTokenUri: https://graph.facebook.com/oauth/access_token
    userAuthorizationUri: https://www.facebook.com/dialog/oauth
    tokenName: oauth_token
    authenticationScheme: query
    clientAuthenticationScheme: form
  resource:
    userInfoUri: https://graph.facebook.com/me
github:
  client:
    clientId: bd1c0a783ccdd1c9b9e4
    clientSecret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1
    accessTokenUri: https://github.com/login/oauth/access_token
    userAuthorizationUri: https://github.com/login/oauth/authorize
    clientAuthenticationScheme: form
  resource:
    userInfoUri: https://api.github.com/user

logging:
  level:
    org.springframework.security: DEBUG

Но когда я открываю браузер и пытаюсь нажать http://localhost:8080

В консоли браузера я вижу:

(index):44 Uncaught TypeError: Cannot read property 'details' of undefined
    at Object.success ((index):44)
    at j (jquery.js:3073)
    at Object.fireWith [as resolveWith] (jquery.js:3185)
    at x (jquery.js:8251)
    at XMLHttpRequest.<anonymous> (jquery.js:8598)

в коде:

$.get("/user", function (data) {
        $("#user").html(data.userAuthentication.details.name);
        $(".unauthenticated").hide();
        $(".authenticated").show();
    });

Это происходит потому, что ответ /user с кодом состояния 302 и обратным вызовом js пытается проанализировать результат localhost:8080:

введите описание изображения здесь

Я не понимаю, почему происходит это перенаправление. Можете ли вы объяснить это поведение и помочь исправить его?

ОБНОВИТЬ

Я взял этот код из https://github.com/spring-guides/tut-spring-boot-oauth2

важный:

Он воспроизводится только после запуска клиентского приложения.

P.S.

Как воспроизвести:

Чтобы протестировать новые функции, вы можете просто запустить оба приложения и посетить localhost:9999/client в своем браузере. Клиентское приложение будет перенаправляться на локальный сервер авторизации, который затем предоставляет пользователю обычный выбор аутентификации с помощью Facebook или Github. Как только это будет завершено, управление вернется к тестовому клиенту, локальный токен доступа будет предоставлен, и аутентификация будет завершена (вы должны увидеть сообщение «Hello» в своем браузере). Если вы уже прошли аутентификацию с помощью Github или Facebook, вы можете даже не заметить удаленную аутентификацию.

ОТВЕЧАТЬ:

https://stackoverflow.com/a/50349078/2674303


person gstackoverflow    schedule 20.04.2018    source источник
comment
Можете ли вы создать минимальное репо для воспроизведения? Было бы проще предоставить исправление   -  person Tarun Lalwani    schedule 24.04.2018
comment
@Tarun Lalwani, вы можете найти его здесь: github.com/spring-guides/tut-spring-boot-oauth2/tree/master/   -  person gstackoverflow    schedule 24.04.2018
comment
Вы доступны для быстрого чата. Пожалуйста, присоединяйтесь к chat.stackoverflow.com/rooms/170569/   -  person vsoni    schedule 07.05.2018
comment
что должно быть установлено в качестве URL-адреса обратного вызова для Github?   -  person MariuszS    schedule 09.07.2018
comment
разрешение: stackoverflow.com/a/50349078/2674303   -  person gstackoverflow    schedule 12.12.2018


Ответы (3)


Обновление: 15 мая 2018 г.

Как вы уже нашли решение, проблема возникает из-за того, что JSESSIONID перезаписывается.

Идентификатор сеанса заменен

Обновление: 10 мая 2018 г.

Что ж, ваша настойчивость с 3-й наградой наконец-то окупилась. Я начал копаться в том, что отличалось между двумя примерами, которые у вас были в репо.

Если вы посмотрите на репозиторий manual и сопоставление /user

@RequestMapping("/user")
public Principal user(Principal principal) {
    return principal;
}

Как видите, здесь вы возвращаете principal, вы получаете больше деталей от того же объекта. Теперь в вашем коде, который вы запускаете из папки auth-server

@RequestMapping({ "/user", "/me" })
public Map<String, String> user(Principal principal) {
    Map<String, String> map = new LinkedHashMap<>();
    map.put("name", principal.getName());
    return map;
}

Как вы можете видеть, вы вернули только name в сопоставлении /user, и ваша логика пользовательского интерфейса работает ниже

$.get("/user", function(data) {
    $("#user").html(data.userAuthentication.details.name);
    $(".unauthenticated").hide();
    $(".authenticated").show();
});

Таким образом, ответ json, возвращенный из /user API, который, как ожидается, будет иметь userAuthentication.details.name в пользовательском интерфейсе, не содержит этих деталей. Теперь, если я обновлю метод, как показано ниже, в том же проекте

@RequestMapping({"/user", "/me"})
public Map<String, Object> user(Principal principal) {
    Map<String, Object> map = new LinkedHashMap<>();
    map.put("name", principal.getName());
    OAuth2Authentication user = (OAuth2Authentication) principal;
    map.put("userAuthentication", new HashMap<String, Object>(){{
       put("details", user.getUserAuthentication().getDetails());
    }});
    return map;
}

А потом проверьте приложение, оно работает

Успех OAuth

Исходный ответ

Итак, проблема в том, что вы запускаете неправильный проект из репо. Вы работаете над проектом auth-server, который предназначен для запуска вашего собственного сервера oauth. Проект, который вам нужно запустить, находится внутри папки manual.

Теперь, если вы посмотрите на приведенный ниже код

OAuth2ClientAuthenticationProcessingFilter facebookFilter = new OAuth2ClientAuthenticationProcessingFilter(
        "/login/facebook");
OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(facebook(), oauth2ClientContext);
facebookFilter.setRestTemplate(facebookTemplate);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(facebookResource().getUserInfoUri(),
        facebook().getClientId());
tokenServices.setRestTemplate(facebookTemplate);
facebookFilter.setTokenServices(
        new UserInfoTokenServices(facebookResource().getUserInfoUri(), facebook().getClientId()));
return facebookFilter;

И фактический код, который вы запускаете, имеет

private Filter ssoFilter(ClientResources client, String path) {
    OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(
            path);
    OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext);
    filter.setRestTemplate(template);
    UserInfoTokenServices tokenServices = new UserInfoTokenServices(
            client.getResource().getUserInfoUri(), client.getClient().getClientId());
    tokenServices.setRestTemplate(template);
    filter.setTokenServices(tokenServices);
    return filter;
}

В вашем текущем userdetails из facebook не собираются. Вот почему вы видите ошибку

Ошибка

Потому что, когда вы вошли в систему пользователя, вы не собрали его данные о пользователе. Поэтому, когда вы получаете доступ к деталям, их там нет. И поэтому вы получаете ошибку

Если запустить правильную папку manual, то работает

Рабочий

person Tarun Lalwani    schedule 24.04.2018
comment
Это все еще не работает. Похоже, вы не поняли проблемы. Вам нужно запустить Клиент и сервер - это 2 разных приложения!!! Пожалуйста, перечитайте мой вопрос с нуля - person gstackoverflow; 11.05.2018
comment
Пожалуйста, укажите, какую папку вы используете для клиентского приложения? - person Tarun Lalwani; 11.05.2018
comment
вы можете найти его в src/test/java - person gstackoverflow; 11.05.2018
comment
есть новости с вашей стороны? - person gstackoverflow; 14.05.2018
comment
@gstackoverflow, я все еще не на одной волне с тобой. Код в папке auth-server работает нормально после моих изменений, он прослушивает 8080, зачем вам запускать и src/test/java, и src/main/java? - person Tarun Lalwani; 15.05.2018
comment
потому что это упоминается в учебнике. spring.io/guides/tutorials/spring-boot-oauth2/ - person gstackoverflow; 15.05.2018
comment
Тестирование клиента OAuth2 Чтобы протестировать новые функции, просто запустите оба приложения и посетите localhost. :9999/client в вашем браузере. Клиентское приложение будет перенаправляться на локальный сервер авторизации, который затем предоставляет пользователю обычный выбор аутентификации с помощью Facebook или Github. Как только это будет завершено, управление вернется к тестовому клиенту, локальный токен доступа будет предоставлен, и аутентификация будет завершена (вы должны увидеть сообщение Hello в своем браузере). Если вы уже прошли аутентификацию в Github или Facebook, вы можете даже не заметить удаленную аутентификацию. - person gstackoverflow; 15.05.2018
comment
Это вы также должны были добавить в вопрос, чтобы было ясно. Я не отлаживал это с самого начала, в любом случае сейчас посмотрю - person Tarun Lalwani; 15.05.2018
comment
я добавила в тему - person gstackoverflow; 15.05.2018
comment
@gstackoverflow, отлично. Я просто отлаживал только вашу проблему. Я воспроизвел 302, но просто отлаживал вызов java, чтобы увидеть, что не так. - person Tarun Lalwani; 15.05.2018

Я вижу два вопроса в вашем сообщении.

ОДИН-

(index):44 Uncaught TypeError: Cannot read property 'details' of undefined

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

ДВА-

/user ответ с кодом состояния 302:

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

Конечные точки "/", "/login**" и "/logout" доступны всем. Все остальные конечные точки, включая "/user", требуют аутентификации, поскольку вы использовали

.anyRequest().authenticated().and().exceptionHandling()
                .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/"))

Таким образом, любой неаутентифицированный запрос будет перенаправлен на точку входа аутентификации, т. е. "/", с запросом аутентификации пользователя. Это не зависит от того, запущено ваше клиентское приложение или нет. Пока запрос не аутентифицирован, он будет перенаправлен на "/". Вот почему контроллер Spring отвечает со статусом 302. После того, как вы прошли аутентификацию с помощью facebook или github, последующие запросы к конечной точке "/user" будут отвечать успехом с 200.

И СЛЕДУЮЩЕЕ-

Конечная точка "/me" в вашем приложении защищена как защищенный ресурс с помощью @EnableResourceServer. Поскольку ResourceServerConfiguration имеет более высокий приоритет (порядок 3 по умолчанию), чем WebSecurityConfigurerAdapter (по умолчанию 100, в любом случае он уже заказан явно ниже 3 с аннотацией @Order в коде), поэтому ResourceServerConfiguration будет применяться для этой конечной точки. Это означает, что если запрос не прошел проверку подлинности, он не будет перенаправлен на точку входа для проверки подлинности, а скорее вернет ответ 401. Как только вы пройдете аутентификацию, он ответит успешно с 200.

Надеюсь, это прояснит все ваши вопросы.

ОБНОВЛЕНИЕ-, чтобы ответить на ваш вопрос

Ссылка на репозиторий, которую вы указали в своем посте, содержит множество проектов. Проекты auth-server, manual и github похожи (обеспечивают одинаковую функциональность, т. е. аутентификацию с помощью facebook и github). Только в проекте index.html в auth-server есть одна ошибка. Если вы исправите эту ошибку, которая заменит

$("#user").html(data.userAuthentication.details.name);

с участием

$("#user").html(data.name);

тоже пойдет нормально. Все три проекта дадут одинаковый результат.

person vsoni    schedule 05.05.2018
comment
Что значит неправильно? Я хочу продемонстрировать это. Как я могу достичь этого? - person gstackoverflow; 06.05.2018
comment
это действительно не помогает - person gstackoverflow; 07.05.2018
comment
Я запускаю правильный проект. Мой вопрос связан с проектом auth-server - person gstackoverflow; 07.05.2018
comment
@gstackoverflow- вы можете быстро поболтать? присоединиться к чату chat.stackoverflow.com/rooms/170569/, если вы доступны. - person vsoni; 07.05.2018

Наконец-то я нашел проблему. Я вижу такое поведение из-за конфликта файлов cookie для клиента и сервера, если вы запускаете оба приложения на локальном хосте.

Происходит это из-за использования неправильного свойства для контекста.

Итак, чтобы исправить приложение, вам нужно заменить:

server:
  context-path: /client

с участием

server:  
  servlet:
    context-path: /client

P.S.

Я создал проблему на github:

https://github.com/spring-guides/tut-spring-boot-oauth2/issues/80

и сделал запрос на вытягивание:

https://github.com/spring-guides/tut-spring-boot-oauth2/pull/81

P.S.2

Наконец, мой запрос на включение был объединен: https://github.com/spring-guides/tut-spring-boot-oauth2/pull/81

person gstackoverflow    schedule 15.05.2018