Spring Authorization Server 1.1.1 学习记录

该版本所推荐的认证模式都有哪些?

  • 客户端模式(Client Credentials)
  • 授权码模式(Authorization Code)
  • 授权码模式 + PKCE (Authorization Code + PKCE)
  • 刷新码模式(refresh token)(这种就不做记录了)
  • 设备码模式(Device Code)

OAuth2.0 官方文档

准备:首先搭建好认证服务端 Authorization Server 。这里使用Spring Boot 整合 pom

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
        </dependency>

可以参考官网 Spring Authorization Server 官方文档

往数据库中插入三种客户端记录。我这使用Java进行插入的:

@Bean
    @Primary
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);

        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();


        registeredClientRepository.save(RegisteredClient.withId(UUID.randomUUID().toString())
                        .clientId("client-client")
                        .clientName("client-client")
                        .clientIdIssuedAt(Instant.now())
                        .clientSecret(passwordEncoder.encode("client-client"))
                        .clientSecretExpiresAt(Instant.now().plus(14, ChronoUnit.DAYS))
                        .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                        .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
                        .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                        .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                        .postLogoutRedirectUri("http://127.0.0.1:8080/logout")
                        .scope("message.read")
                        .clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build())
                        .tokenSettings(TokenSettings.builder()
                                //access token 有效期
                                .accessTokenTimeToLive(Duration.ofMinutes(60))
                                .build())
                .build());
        // 授权码模式
        registeredClientRepository.save(RegisteredClient.withId(UUID.randomUUID().toString())
                        .clientId("code-client")
                        .clientName("code-client")
                        .clientIdIssuedAt(Instant.now())
                        .clientSecret(passwordEncoder.encode("code-client"))
                        .clientIdIssuedAt(Instant.now())
                        .clientSecretExpiresAt(Instant.now().plus(14, ChronoUnit.DAYS))
                        .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                        .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                        .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                        .redirectUri("http://127.0.0.1:8080/callback")
                        .postLogoutRedirectUri("http://127.0.0.1:8080/logout")
                        .scope(OidcScopes.OPENID)
                        .scope(OidcScopes.PROFILE)
                        .scope("message.read")
                        .scope("message.write")
                        .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
                        .tokenSettings(TokenSettings.builder()
                                //access token 有效期
                                .accessTokenTimeToLive(Duration.ofMinutes(60))
                                //refresh token 有效期
                                .refreshTokenTimeToLive(Duration.ofDays(7))
                                //执行刷新token时, 是否返回新的refresh token (默认true 既是重用 refresh token; false 则生成新的 refresh token 及有效期)
                                .reuseRefreshTokens(true)
                                .build())
                        .build());
//
//        // 授权码 + PKCE 模式
        registeredClientRepository.save(RegisteredClient.withId(UUID.randomUUID().toString())
                        .clientId("oidc-client")
                        .clientIdIssuedAt(Instant.now())
                        .clientSecret(passwordEncoder.encode("oidc-client"))
                        .clientSecretExpiresAt(Instant.now().plus(14, ChronoUnit.DAYS))
                        .clientName("oidc-client")
                        .redirectUri("http://127.0.0.1:8080/callback")
                        .postLogoutRedirectUri("http://127.0.0.1:8080/logout")
                //客户端认证方 none - 若开启PKCE 认证, 则需要添加 none,认证方法
                        .clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
                        .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                        .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                        .scope(OidcScopes.OPENID)
                        .scope(OidcScopes.PHONE)
                        .scope(OidcScopes.EMAIL)
                        .scope(OidcScopes.PROFILE)
                        .scope(OidcScopes.ADDRESS)
                        .scope("offline_access")
                        .clientSettings(ClientSettings.builder()
                                .requireAuthorizationConsent(true)
                                //是否需要开启PKCE模式
                                .requireProofKey(true)
                                .build())
                        .tokenSettings(TokenSettings.builder()
                                //access token 有效期
                                .accessTokenTimeToLive(Duration.ofMinutes(60))
                                .build())
                .build());


        //设备码模式
        registeredClientRepository.save(RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("device-client")
                .clientIdIssuedAt(Instant.now())
                .clientName("device-client")
                .postLogoutRedirectUri("http://127.0.0.1:8080/logout")
                        .clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
                        .authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
                        .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                        .scope("message.read")
                        .clientSettings(ClientSettings.builder()
                                .requireAuthorizationConsent(true)
                                .build())
                        .tokenSettings(TokenSettings.builder()
                                .accessTokenTimeToLive(Duration.ofMinutes(60))
                                 //refresh token 有效期
                                .refreshTokenTimeToLive(Duration.ofDays(7))
                                //执行刷新token时, 是否返回新的refresh token (默认true 既是重用 refresh token; false 则生成新的 refresh token 及有效期)
                                .reuseRefreshTokens(true)
                                .build())
                .build()
        );

        return registeredClientRepository;
    }

1. 客户端模式(client_credentials)

OAuht2.0 Client Credentials 模式官方文档

该模式最为简单。需要后端服务作为支持,可以存储 client_idclient_secret,认证流程为携带client_id, client_secret 加上基本参数grant_type(该模式下为: client_credentials) 直接进行访问。样例为下面所示:获取 token

备注:客户端模式不支持 refresh token,所以响应数据中没有refresh token

2. 授权码模式(Authorization Code)

OAuth2.0 Authorization Code 模式官方文档

该模式的流程不过多进行赘述了,可以自行查看文档或者百度有很多流程介绍,下面的PKCE在做详细介绍。引导客户端做认证,回调给客户端携带 code,客户端再携带code,client_secret 等请求access token, 该模式支持 refresh token

  • 先调用 /oauth2/authorize 接口,服务端引导客户到认证界面进行身份认证。
127.0.0.1:8080/oauth2/authorize?response_type=code&client_id=code-client&scope=message.read&state=code&redirect_uri=http://127.0.0.1:8080/callback

引导用户身份认证

  • 登录成功后进入授权确认界面

用户授权

  • 确认后回携带code回调到回调地址
    回调给客户端
  • 接下来使用 code 请求access token,这里要把回调地址再次传递给服务端,grant_type 为:authorization_code,这次client_secret 使用 client_secret_basic 来传输

获取token

获取token

3. 授权码模式 + PKCE(Authorization Code + PKCE)

OAuth2.0 PKCE模式官方文档

该模式需要做一个详细的说明:PKCE (Proof Key for Code Exchange),授权码模式需要客户端保存client_id 和 client_secret 认证流程虽然完善,但是无法给公开的客户端进行使用。因为公开的客户端没办法保存好client_secret。PKCE就是解决这个问题的。所以该模式下获取access token 时不再传递client_secret, 而是使用 code_verifier 代替,code_verfier 是由公共客户端临时生成的随机字符串。

因此该模式的流程就是首先公共客户端生成 code_verifier , 然后使用加密方式对 code_verifier 进行加密,再转Base64,生成 code_challenge,
在访问 /oauth2/authorize 的时候将 code_challenge 和 加密方法 code_challenge_method 如 SHA256 则传入S256。然后服务端引导用户做登录授权,之后服务端回调到回调地址携带生成的 code, 这里就和授权码模式一样了,接下来,再访问服务端 /oauth2/token 来获取token,需要携带 codecode_verifier, 具体流程如图所示:
PKCE认证模式流程
说明:Base64(SHA256(code_verifier)) 被称作为code_challengecode_verifier是43-128位的随机字符,字符范围是/A-Z/a-z/0-9/"."/"-"/"_"/"~"

接下来演示下认证流程:

  • 先放个生成 code_verifier 的 Java 工具类
@Getter
public class RandomCodeVerifier {


    private static final Random r = new Random();

    private static final char[] lowerChar = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'p', 'q',
            'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' };
    private static final char[] upperChar = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q',
            'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' };
    private static final char[] numberChar = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };

    private static final char[] symbolChar = { '.', '-', '_', '~'};

    private static final int MAX_CODE_VERIFIER_LENGTH = 128;

    private static final int MIN_CODE_VERIFIER_LENGTH = 43;


    /**
     * 获取code verifier
     * @return code verifier
     */
    public static String getCodeVerifier() {
        Random random = new Random();
        int i = random.nextInt(MAX_CODE_VERIFIER_LENGTH - MIN_CODE_VERIFIER_LENGTH + 1) + MIN_CODE_VERIFIER_LENGTH;
        return getRandomString(i);
    }

    /**
     * 获取随机字符串,包含大小写字母和数字,可以有重复字符
     *
     * @param strLength 字符串长度
     */
    public static String getRandomString(int strLength) {
        return getRandomString(strLength,  true);
    }

    /**
     * 获取随机字符串
     *
     * @param strLength  字符串长度
     * @param repeat     是否可以有重复字符,true表示可以重复,false表示不允许重复。如果生成字符长度大于可用字符数量则默认采用true值。
     */
    public static String getRandomString(int strLength,  boolean repeat) {
        StringBuilder result = new StringBuilder();
        char[] validChar = null;// 可用字符数组

        validChar = Arrays.copyOf(lowerChar, lowerChar.length + upperChar.length + numberChar.length + symbolChar.length);
        System.arraycopy(upperChar, 0, validChar, lowerChar.length, upperChar.length);
        System.arraycopy(numberChar, 0, validChar, lowerChar.length + upperChar.length, numberChar.length);
        System.arraycopy(symbolChar, 0, validChar, lowerChar.length + upperChar.length + numberChar.length, symbolChar.length);
        if (strLength > validChar.length) {// 字符串长度大于可用字符数量
            repeat = true;// 字符可重复
        }
        if (repeat) {
            for (int i = 0; i < strLength; i++) {
                result.append(validChar[r.nextInt(validChar.length)]);
            }
        } else {
            HashSet<Integer> indexset = new HashSet<Integer>();
            int index = 0;
            for (int i = 0; i < strLength; i++) {
                do {
                    index = r.nextInt(validChar.length);// 随机获得一个字符的索引
                } while (indexset.contains(index));// 如果已经使用过了,则重新获得
                result.append(validChar[index]);
                indexset.add(index);// 记录已使用的字符索引
            }
        }
        return result.toString();
    }

    /**
     * 获取随机字符串
     *
     * @param strLength 字符串长度
     * @param repeat    是否可以存在重复的字符
     * @param ch        自定义字符集,可传入多个字符数组
     */
    public static String getRandomString(int strLength, boolean repeat, char[]... ch) {
        StringBuilder result = new StringBuilder();
        HashSet<Character> validChar = new HashSet<>();
        for (char[] chars : ch) {
            for (char aChar : chars) {
                validChar.add(aChar);
            }
        }
        if (validChar.isEmpty()) {
            return "";
        }
        if (strLength > validChar.size()) {// 字符串长度大于可用字符数量
            repeat = true;// 字符可重复
        }
        List<Character> list = new LinkedList<>(validChar);
        for (int i = 0; i < strLength; i++) {
            if (repeat) {
                result.append(list.get(r.nextInt(list.size())));
            } else {
                result.append(list.remove(r.nextInt(list.size())));
            }
        }
        return result.toString();
    }
}
  • 生成 code_verifier 访问 /oauth2/authorize
http://127.0.0.1:8080/oauth2/authorize?response_type=code&client_id=oidc-client&redirect_uri=http://127.0.0.1:8080/callback&state=oidc&scope=openid+profile+email+phone&code_challenge_method=S256&code_challenge=_A4xg9nNxk0hlwpOlBnldncuQZG6S_ebxNjxT6j6gfM

其格式大概是这样
引导用户去做认证参数
服务端引导用户做用户认证
用户登录认证
这里确认授权

用户授权
回调携带code参数, 我们把 code 复制下来 ,用 postman 来访问 /oauth2/token 接口

回调给客户端

获取token
到这里就访问结束了。我们能看到,返回了 除 access_token 外的 id_token
id_token : 身份令牌(JWT),用于认证,标识用户身份已经认证,可用于获取用户身份信息(如用户名、头像等),同时客户端需对获取到的ID Token进行签名验证、属性验证(aud、azp、nonce等)。

问题:该模式下貌似无法使用 refresh_token, 在找了一些资料发现,说是可以支持, 需要访问/oauth2/authorize scope 中传递 offline_access 才可以获取到 refresh_token,在经过多次尝试后,发现不能获取。再看下Spring Authorization Server 源码中发现,client属性中 client_authentication_methods 如果为none则不签发 refresh_token, 和网上很多人都不太一样, 但是OIDC模式该字段必须是none,因为没有 client_secret。源码如图所示:
PKCE源码展示

在github 上 issue 上 确实也发布相关的说明,考虑到 公共客户端模式 refresh token 泄露风险, 默认是不开放该模式下签发 refresh token, 这里说明了不支持, 并且说明了针对 Public Client 也没有计划实现 refresh tokens

Spring Authorization Server issue-297 中回复了 公共客户端 refresh token 的问题
文档说明
由此看来,想要公共客户端签发 refresh token 只能自定义 OAuth2AuthorizationCodeAuthenticationProvider。 这个话题可以以后有时间试试。

4. 设备码模式(Device Code)

OAuth2.0 Device Code 模式官方文档
Spring Authorization Server Demo

设备码模式是为了可以连接网络的设备但是缺少浏览器去引导用户授权或者输入受限。让用户去输入文本是不切实际的。该模式支持像智能电视,媒体控制台,打印机等,获得用户授权才能访问一些资源。具体搭建该模式的方式请参考上述Spring Authorization Server 的 Demo 基本流程如下:
设备码认证流程

  • A: 客户端携带 client_id, scope, 访问 /oauth2/device_authorization 来获取 deivce code, user code, verification uri
  • B: 服务端生成 device code,user code,verification uri 返回给客户端
  • C: 客户端拿到user code ,verification uri 引导用户去访问,然后认证(例如生成二维码)
  • D: 用户登录并授权
  • E: 在C,D 步骤期间,客户端轮询访问服务端 /oauth2/token 接口 来获取 access_token,失败则返回未认证,成功则返回相关token
  • F: 返回 access_token 和可选的 refresh_token

接下来实践一下:

先访问 /oauth2/device_authorization
请求获取设备码
参数说明:

  • user_code: 生成的用户编码,访问 verification_uri时需要填写进去(如果访问verification_uri_complete 则不需要手动拼接)一般来说直接用verification_uri_complete生成二维码直接让用户用手机扫描即可,具体看业务情况而定。
  • device_code: 由客户端保留,在访问token时需要作为参数传给服务端。
  • verification_uri:让用户直接访问的地址。

访问 verification_uri
引导用户去做认证
跳转到登录
![用户认证](https://img-blog.csdnimg.cn/ef6d72d5cc1b49c9878d085de1676834.png
登录成功跳转授权界面
用户授权
授权成功
授权成功
接下来我们就可以访问 /oauth2/token 接口来获取token了

获取token
到这里我们就获取到了 token,我们在试验一下 没有通过授权时获取 token 的返回结果
未认证获取token
可以看到返回结果是这样子的,authorization_pending,下面是文档地址。源码GitHub

总结

到这里我们除了 refresh token 来获取 access token 外的所有方式都学习完毕,有疑惑的可以提出来,一起探讨一下。

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
### 回答1: Spring Authorization Server是一个基于OAuth 2.0和OpenID Connect(OIDC)协议的身份验证和授权服务器,它提供了一个工具集来构建安全的、可伸缩的和互操作的身份验证和授权解决方案。它可以通过Java Spring框架进行集成,是Java开发人员用于保护Web应用程序的最佳选择。 ### 回答2: Spring Authorization Server是一个基于Spring框架构建的授权服务器。授权服务器是OAuth 2.0授权框架的一部分,用于控制和管理资源的访问权限。通过Spring Authorization Server,用户可以验证和授权第三方应用程序来访问其资源。 Spring Authorization Server 提供了一种安全可靠的方式,用于管理用户的身份验证和授权请求。它使得开发人员能够轻松地构建和定制一个符合其需求的授权服务器。 Spring Authorization Server实现了OAuth 2.0授权框架中的各种授权流程,包括客户端模式、授权码模式、隐式模式和密码模式等。开发者可以根据应用程序的需求来选择和配置适当的授权流程。 Spring Authorization Server还提供了丰富的扩展机制,使得开发者能够对其进行定制和扩展。它与Spring Security和Spring Boot等框架紧密集成,使得开发者能够使用这些强大的工具来增强授权服务器的安全性和功能。 总之,Spring Authorization Server 是一个用于构建和定制授权服务器的框架,通过它可以实现OAuth 2.0授权框架中各种授权流程。它为开发者提供了简单、安全和可扩展的解决方案,使得他们能够轻松地控制和管理应用程序的资源访问权限。 ### 回答3: Spring Authorization Server是一个基于Spring框架开发的认证和授权服务器。它提供了一套全面而灵活的身份验证和授权解决方案,旨在帮助开发者构建安全可靠的应用程序。 首先,Spring Authorization Server支持多种认证方式,包括常见的用户名密码认证、基于OAuth2的授权码认证、密码授权和客户端凭证授权等。开发者可以根据实际需求选择合适的认证方式,确保用户身份验证的安全性。 其次,Spring Authorization Server还支持灵活的授权管理。开发者可以通过定义不同的权限范围和角色来限制用户对资源的访问权限,保护敏感数据的安全。同时,它提供了可扩展的授权策略和自定义认证逻辑的能力,使开发者能够根据具体业务需求进行灵活配置。 另外,Spring Authorization Server支持分布式部署,可以轻松处理高并发的请求。它基于Spring框架的优势,具有良好的扩展性和可维护性,可以与其他Spring生态系统组件无缝集成,如Spring Security、Spring Boot等。 总之,Spring Authorization Server是一个功能强大、安全可靠的认证和授权服务器,为开发者提供了完善的解决方案来保护应用程序的安全性和用户隐私。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值