Официальный пример безопасности 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
важный:
Воспроизводится только после запуска клиентского приложения.
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
получает перезапись
Обновление: 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;
}
И затем проверьте приложение, оно работает
Оригинальный ответ
Таким образом, проблема, что вы используете неправильный проект из репо. Проект, который вы запускаете, - это 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
, она работает
Ответ 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);
он также будет работать нормально. Все три проекта будут давать одинаковый результат.