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

По материалам учебника Spring Boot и OAuth2

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

enter image description here

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

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:

enter image description here

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

ОБНОВИТЬ

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

важный:

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

PS

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

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

ОТВЕТ:

qaru.site/info/15545832/...

Ответ 1

Наконец, я нашел проблему. Я вижу это поведение из-за того, что cookie-клиент сталкивается с клиентом и сервером, если вы запускаете оба приложения на localhost.

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

Поэтому для исправления приложения вам необходимо заменить:

server:
  context-path: /client

с

server:  
  servlet:
    context-path: /client

PS

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

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

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

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

PS2

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

Ответ 2

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

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

Session ID replaced

Обновление: 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 Success

Оригинальный ответ

Таким образом, проблема, что вы используете неправильный проект из репо. Проект, который вы запускаете, - это 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 не собираются. Вот почему вы видите ошибку

Error

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

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

Working

Ответ 3

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

ONE-

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

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

TWO-

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

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

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

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

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

И NEXT-

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

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

UPDATE- ответить на ваш вопрос

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

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

с

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

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