官方的Spring安全oauth2示例由于cookie冲突(授权码机制)而无法正常工作。

30
根据教程 Spring Boot and 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中获取了此代码。

重要提示:

只有在启动客户端应用程序后才会出现此问题。

P.S.

如何重现:

要测试新功能,您只需运行两个应用程序并在浏览器中访问localhost:9999/client。客户端应用程序将重定向到本地授权服务器,然后给用户提供使用Facebook或Github进行身份验证的常规选择。完成后,控制权返回测试客户端,授予本地访问令牌并完成身份验证(您应该在浏览器中看到“Hello”消息)。如果您已经通过Github或Facebook进行了身份验证,您甚至可能不会注意到远程身份验证。

答案:

https://dev59.com/oOk5XIcBkEYKwwoY8eTT#50349078


你能否创建一个最小化的仓库来重现问题吗?这样更容易提供修复。 - Tarun Lalwani
@Tarun Lalwani,您可以在此处找到它:https://github.com/spring-guides/tut-spring-boot-oauth2/tree/master/auth-server - gstackoverflow
你现在有时间聊一下吗?请加入 https://chat.stackoverflow.com/rooms/170569/room-for-vsoni-and-gstackoverflow - vsoni
Github的回调URL应该设置为什么? - MariuszS
分辨率:https://dev59.com/oOk5XIcBkEYKwwoY8eTT#50349078 - gstackoverflow
3个回答

16

更新:2018年5月15日

如你已经发现的解决方法,这个问题是由于JSESSIONID被覆盖导致的。

Session ID replaced

更新:2018年5月10日

坚持使用第三个赏金终于有了回报。 我开始深入研究在repo中的两个示例之间的区别

如果您查看手动repo和/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;
}

如您所见,您仅在/user映射中返回了name,而您的UI逻辑则在其下运行。

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

因此,UI期望从/user API返回的JSON响应中具有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;
}

你当前的代码没有收集来自 facebookuserdetails,这就是为什么会出现错误。

错误

因为当你登陆用户时,你没有收集它的用户详细信息。所以当你访问这些详细信息时,它们不存在。因此你会得到一个错误。

如果你运行正确的 manual 文件夹,它就可以工作了。

运行正常


还是不行。看起来你没有理解问题所在。你需要启动客户端和服务器——它们是两个不同的应用程序!!!请从头重新阅读我的问题。 - gstackoverflow
你可以在 src/test/java 中找到它。 - gstackoverflow
你那边有什么新消息吗? - gstackoverflow
@gstackoverflow,我还没有跟上你的步伐。在我修改后,auth-server 文件夹中的代码运行良好,它监听 8080 端口,为什么你还要同时运行 src/test/javasrc/main/java 呢? - Tarun Lalwani
我已将其添加到主题中。 - gstackoverflow
显示剩余5条评论

6

I see two queries in your post.

ONE-

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

这是因为您可能运行了一个有错误的项目(即auth-server)。存储库中还包含其他相似的无错误项目。如果您运行手动或Github项目,则不会出现此错误。在这些项目中,JavaScript代码正确处理服务器在认证后返回的数据。
第二点:
/user响应302状态码:
要理解为什么会发生这种情况,请查看此应用程序的安全配置。
端点“/”,“/login **”和“/logout”对所有用户均可访问。所有其他端点,包括“/user”,都需要身份验证,因为您已使用身份验证策略。
.anyRequest().authenticated().and().exceptionHandling()
                .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/"))

任何未经身份验证的请求都将被重定向到身份验证入口点,即"/",要求用户进行身份验证。这不取决于您的客户端应用程序是否已启动。只要请求未经身份验证,它就会被重定向到"/"。这就是为什么spring控制器响应状态302的原因。一旦使用FacebookGithub中的任何一个进行了身份验证,则对"/user"端点的后续请求将以200成功响应。
在应用程序中,端点"/me"作为受保护的资源被@EnableResourceServer保护。由于ResourceServerConfiguration具有更高的优先级(默认为3),而WebSecurityConfigurerAdapter具有较低的优先级(默认为100,无论如何,在代码中已经显式地使用@Order注释将其排序为低于3),因此ResourceServerConfiguration将适用于此端点。这意味着如果请求未经身份验证,则它不会被重定向到身份验证入口点,而是返回401响应。一旦进行了身份验证,它将以200成功响应。
希望这能澄清您所有的问题。 更新-回答您的问题
您在帖子中提供的存储库链接包含许多项目。项目auth-servermanualgithub都是相似的(提供相同的功能,即使用Facebook和Github进行身份验证)。只是auth-server项目中的index.html有一个错误。如果更正此错误,则将
$("#user").html(data.userAuthentication.details.name);

使用

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

它也会正常运行。所有三个项目都会给出相同的输出。


你说的“不对”是什么意思?我想演示这个。我应该怎么做才能实现呢? - gstackoverflow
它真的没有帮助。 - gstackoverflow
我正在运行正确的项目。我的问题与 auth-server 项目有关。 - gstackoverflow
@gstackoverflow- 你方便聊一下吗?如果可以的话,请加入聊天室 https://chat.stackoverflow.com/rooms/170569/room-for-vsoni-and-gstackoverflow。 - vsoni

5

最终我找到了问题所在。如果你在本地主机上启动两个应用程序,由于客户端和服务器的cookie冲突,会出现这种行为。

这是因为使用了错误的上下文属性导致的。

因此,要修复应用程序,您需要替换:

server:
  context-path: /client

使用

server:  
  servlet:
    context-path: /client

附言

我在Github上创建了一个问题:

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

并提出了请求:

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

附言2

最终我的请求被合并了:

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接