package com.base.oauth2.client.template;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestOperations;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails;
import org.springframework.security.oauth2.client.token.grant.implicit.ImplicitResourceDetails;
import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordResourceDetails;
import org.springframework.security.oauth2.common.AuthenticationScheme;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client;
import org.springframework.transaction.annotation.Transactional;

import java.util.Arrays;

/**
 * Description : 生成的 token 和浏览器相关,如果浏览器以 authorization_code 方式访问成功是,如果 AuthorizationServer 设置该 client 同时也支持 password ,那么浏览器也 passwrod 方式方法,就会用同一个 token ,不会在进行验证,直接获得结果
 * User: h819
 * Date: 2015/12/4
 * Time: 11:19
 * To change this template use File | Settings | File Templates.
 */
@Configuration
@EnableOAuth2Client
@Transactional
public class Oauth2ClientRestTemplate {

    //private static final Logger logger = LoggerFactory.getLogger(Oauth2ClientRestTemplate.class);

    @Autowired
    private OAuth2ClientContext oAuth2ClientContext;

    /**
     *
     * === oauth 2.0
     *  是一种安全验证协议,有很多实现其 oauth 协议的项目,如 spring security oauth2
     *  其授权类型(grant_type)有四种,分布是 authorization_code , implicit , password , client_credentials
     *
     *  主要验证过程:通过身份验证以后,获得 access_token ,access_token 是获得授权的标志。access_token 有有效期,过了有效期,需要重新获取 access_token。
     *
     *  1. authorization_code (或 implicit ,不常用):
     *
     *     第三方网站提供用户身份验证服务。
     *     在 client 在不知道用户名和密码的情况下,通过自己登陆到第三方网站,client 在第三方网站登录成功后,获取到 access_token ,表明已通过身份验证,之后再访问资源服务器内容。
     *     目前这种认证方式是 oauth 的主要用途,如各个网站提供的单点登录功能,就是利用第三方提供的 oauth 的认证,让用户通过第三方的账号,登陆本网站,而网站本身不保留用户的信息。
     *
     *  2. password 和 client_credentials 方式:
     *
     *  2.1  password :
     *     知道了用户名和密码,通过登录 AuthorizationServer 获得 access_token 后,通过 access_token 访问 ResourceServer 资源;
     *
     *  2.2  client_credentials :
     *     client 知道资源的 client_id (也可以是 client_id 和 client_secret),获得 access_token 后,通过access_token 访问资源。
     *
     *     对于这两种种情况,都需要用户向验证服务器提供身份信息,获得 access_token 后,在 access_token 的有效期内,不用再提供身份信息,通过 access_token ,访问资源。
     *
     *  3. 关于信息传递安全
     *     oauth 验证分为两个步骤
     *     1)身份验证,获得 access_token
     *     2)利用 access_token 访问资源服务器(此时不再需要身份信息,access_token 过期后,重新获取)
     *
     *     为了防止传递的信息被截获,
     *     步骤 1) 需要通过 https 协议进行数据传输。https 协议是加密协议,传递的用户身份信息(用户名、密码等)不会被截获。
     *     完成了 1)的身份验证以后,为了提供效率, 2)步骤可以通过 http 协议 ,这是目前大多数网站采取的策略。(2)步骤也可以用 https)
     *
     *  4. 关于开放 API 的安全验证
     *
     *  4.1 采用上文提到的 oauth 方式验证
     *      grant_type=client_credentials 的方式
     *      此种方式,服务器端需要搭建验证服务器(ssh 协议模式)和资源服务器,客户端需要 oauth 客户端技术进行访问。
     *
     *  4.2 利用 HMAC(Hash-based Message Authentication Code 基于散列的消息认证码)
     *
     *
     *
     *  5. 关于生成的 access_token
     *  四种方式生成的 token 和该 client 的相关信息相关:
     *  如用一台电脑的同一浏览器登陆 AuthorizationServer 后,验证成功以后,该电脑的这个浏览器无论以哪种方式,在 token 有效期内不必登陆,直接返回验证成功信息。
     *  登陆一次之后不需要再次登陆,是因为第一次登陆的 token 还没有过期,所以可以继续自动登陆。

     *  如果出现有时验证成功,有时不成功的情况,大多数是因为更改了相关参数,此时删除以后的  token 信息,重新启动电脑即可。
     *
     *  常用的有两种模式
     *  grant_type=client_credentials ,用于对外开放 api
     *  grant_type=authorization_code ,用于单点登录
     *
     *
     *  === oauth2 四种验证方式说明和应用场景 ===
     * 1.  grant_type=authorization_code (重点,常用)
     *
     * 这种验证方式,需要 client 到验证服务器登录页面进行登陆,登陆之后验证服务器保存该 client 的 token
     * -
     * 有的网站引入了第三方登陆方式,登陆一次之后不需要再次登陆,是因为第一次登陆的 token 还没有过期,所以可以继续自动登陆。
     * -
     * 实际应用场景:
     * 单点登录: 不同的第三方应用,用验证服务器统一进行验证登陆,登陆成功之后,返回一个登陆成的 access_token ,表示该用户存在
     * 那么登陆成功后,该 client 的权限信息保存在哪里呢,每个用户的权限不一样,如有的仅有读权限,有的有写权限 ?登陆成功仅能证明该用户存在。
     * 可以有两种模式:
     * - 1) 用户的权限信息保存在验证服务器,可以在登陆成功时,直接返回该用户的权限信息。缺点是,验证服务器上要根据不同的应用,设置不同的用户权限,到底是管理员还是普通用户
     * - 2) 权限信息保存在第三方应用上,这样验证服务器只保证用户存在,不管该用户有什么权限。
     * - 现在看来,应该是 2) 方式,要不然 google 提供的登陆,没办法给那么多的第三方应用分别设置权限了。
     * - 例子:google 开放的登陆,网易新闻保存到有道云笔记等,都会出现一个授权页面,就是该方式的例子。
     * -

     * ------------------------------------------------------------------------------------------------------------------------
     * 2.  grant_type=implicit
     *   该方式没有实验成功,据说是用在 js 中 。
     *   不再尝试该方法,等需要时再看是为什么(google 了半个月的资料,不再试了)。
     *
     * ------------------------------------------------------------------------------------------------------------------------
     *  3.  grant_type=password
     *  该方式需要提供在验证服务器上存在的用户名和密码(如果验证服务器上设置了 client 用户角色信息,则该用户还应该满足角色要求)
     *
     *------------------------------------------------------------------------------------------------------------------------
     *  4.  grant_type=client_credentials (重点,常用)
     *  用于 web 应用向第三方应用提供 api 接口,此时应设置 client_secret ,增加安全性。
     *  典型用例 :
     *  微信 -
     *  微信对公众号提供的接口,就是用了 grant_type=client_credentials 的方式。第三方应用 通过 access_token 来获取相关信息,access_token 的有效期是两个小时(见“微信公众平台开发者文档 / 获取接口凭证 http://mp.weixin.qq.com/wiki/14/9f9c82c1af308e3b14ba9b973f99a8ba.html”)。
     *  对外提供的资源用 get 方法。
     *  不知道微信用的是不是 ouath2 协议,因为 spring ouath2 的 client_credentials 不提供 refresh_token ,而微信的公众号接口提到可以有。
     *  - 所以如果想向外提供 api ,就用此方式。
     *  - 对于每个对外的用户,都单独设置一个 client_id 和 client_secret ,微信的方案是每个用户都有一个。相应限制用户的连接次数,在接收到其发送的查询请求的时候处理,如果通过,在转发到 oauth server。
     */

    /**
     * 演示 grant_type=authorization_code 时,获取资源的方法
     * -
     *
     * @param client_id
     * @param client_secret     取决于 AuthorizationServer 设置,如果 client 设置了secret,则此项参数为必需,否则可以没有
     * @param access_token_uri
     * @param authorization_uri
     * @param scope
     * @return
     */

    public OAuth2RestOperations authorizationCodeRestTemplate(String client_id, String client_secret, String authorization_uri, String access_token_uri, String... scope) {

        // 防止 url 写错
        if (!access_token_uri.contains("token") || !authorization_uri.contains("authorize"))
            throw new RuntimeException("uri is wrong :  access_token_uri = " + access_token_uri + " , authorization_uri" + authorization_uri);


        AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
        details.setId("1");
        details.setClientId(client_id);
        if (client_secret != null && !client_secret.isEmpty())
            details.setClientSecret(client_secret);
        details.setAccessTokenUri(access_token_uri);
        details.setUserAuthorizationUri(authorization_uri);
        details.setUseCurrentUri(true); //将当前请求的 uri 作为参数 redirect_uri 接受返回值。设置为 faslse 是,需要设置 redirect_uri 参数, details.setPreEstablishedRedirectUri("http://anywhere");
        details.setScope(Arrays.asList(scope));
        return new OAuth2RestTemplate(details, oAuth2ClientContext);
    }

    /**
     * 该方式没有实验成功,设置为 Deprecated!
     * <p>
     * 演示 grant_type=implicit 时,获取资源的方法
     *
     * @param client_id
     * @param client_secret     取决于 AuthorizationServer 设置,如果 client 设置了secret,则此项参数为必需,否则可以没有
     * @param authorization_uri
     * @param access_token_uri
     * @param scope
     * @return
     */
    @Deprecated
    public OAuth2RestOperations implicitResourceRestTemplate(String client_id, String client_secret, String authorization_uri, String access_token_uri, String... scope) {

        // 防止 url 写错
        if (!authorization_uri.contains("authorize"))
            throw new RuntimeException("uri is wrong :  authorization_uri" + authorization_uri);

        ImplicitResourceDetails details = new ImplicitResourceDetails();
        details.setId("2");
        details.setClientId(client_id);
        if (client_secret != null && !client_secret.isEmpty())
            details.setClientSecret(client_secret);
        details.setAccessTokenUri(authorization_uri);
        details.setClientAuthenticationScheme(AuthenticationScheme.header);
        details.setUseCurrentUri(true);
        details.setScope(Arrays.asList(scope));
        // return restTemplate;
        return new OAuth2RestTemplate(details, oAuth2ClientContext);
    }

    /**
     * 演示 grant_type=password 时,获取资源的方法
     * 用的场景还不知道,@Deprecated
     *
     * @param client_id
     * @param client_secret    取决于 AuthorizationServer 设置,如果 client 设置了secret,则此项参数为必需,否则可以没有
     * @param access_token_uri
     * @param username
     * @param password
     * @param scope
     * @return
     */
    @Deprecated
    public OAuth2RestOperations resourceOwnerPasswordRestTemplate(String client_id, String client_secret, String access_token_uri, String username, String password, String... scope) {

        // 防止 url 写错
        if (!access_token_uri.contains("token"))
            throw new RuntimeException("uri is wrong :  access_token_uri = " + access_token_uri);

        // 防止 client_secret 写错
        if (username == null || password == null || username.isEmpty() || password.isEmpty())
            throw new RuntimeException("username or password  is wrong :  username or password is a required parameter");

        ResourceOwnerPasswordResourceDetails details = new ResourceOwnerPasswordResourceDetails();
        details.setId("3");
        details.setClientId(client_id);
        if (client_secret != null && !client_secret.isEmpty())
            details.setClientSecret(client_secret);
        details.setAccessTokenUri(access_token_uri);
        details.setUsername(username);
        details.setPassword(password);
        details.setScope(Arrays.asList(scope));
        return new OAuth2RestTemplate(details, oAuth2ClientContext);


    }

    /**
     * 演示 grant_type=client_credentials 时,获取资源的方法
     *
     * @param client_id
     * @param client_secret    取决于 AuthorizationServer 设置,如果 client 设置了secret,则此项参数为必需,否则可以没有
     * @param access_token_uri
     * @param scope
     * @return
     */
    public OAuth2RestOperations clientCredentialsRestTemplate(String client_id, String client_secret, String access_token_uri, String... scope) {

        // 防止 url 写错
        if (!access_token_uri.contains("token"))
            throw new RuntimeException("uri is wrong :  access_token_uri = " + access_token_uri);

        ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails();
        details.setId("4");
        details.setClientId(client_id);
        if (client_secret != null && !client_secret.isEmpty())
            details.setClientSecret(client_secret);
        details.setAccessTokenUri(access_token_uri);
        details.setScope(Arrays.asList(scope));
        return new OAuth2RestTemplate(details, oAuth2ClientContext);
    }


    private OAuth2RestOperations refreshRestTemplate(String resource) {

        //https://github.com/spring-projects/spring-security-oauth/blob/master/samples/oauth2/tonr/src/test/java/org/springframework/security/oauth/examples/tonr/RefreshTokenGrantTests.java
        // 如果 client 设置了可以 refresh ,那么上述四种获取方法中,就会自动利用 refresh 的方式获取资源,不必单独实现

        return null;
    }

}