[spring]-【spring-oauth2】-笔记1

oauth2的简单体验,后续会继续更新多点的内容(是自己看网课和文档,自己慢慢摸索出来的,所以写的比较杂,涉及简单的源码部分)

一、简单体验

1. 搭建项目

没什么操作,注意控制住spring-cloud 和 spring-boot 的版本就OK。

依赖:

-pom.xml

     <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
      <dependencies>
      
        <!--        安全框架-->
        <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-oauth2 -->
        <!--这里面就会依赖 spring-cloud-starter-security 所以依赖这一个就 OK-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>
        <!--todo 后续会使用到的-->
        <!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-jwt -->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
            <version>1.1.1.RELEASE</version>
        </dependency>

      
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

application.yaml文件

server:
  port: 18001

spring:
  application:
#    授权服务
    name: oauth2-service

启动类:

主要是注解 @EnableAuthorizationServer
/*  google 翻译
  *  用于在当前应用程序上下文中启用授权服务器(即AuthorizationEndpoint和TokenEndpoint )的便利注释,
  *  该上下文必须是DispatcherServlet上下文。
  *  可以使用AuthorizationServerConfigurer类型的@Beans自定义服务器的许多功能
  *  (例如,通过扩展AuthorizationServerConfigurerAdapter )。
  *  用户负责使用普通 Spring Security 功能( @EnableWebSecurity等)保护授权端点(/oauth/authorize),
  *  但令牌端点(/oauth/token)将使用客户端凭据上的 HTTP 基本身份验证自动保护。
  *  必须通过一个或多个 AuthorizationServerConfigurer 提供ClientDetailsService来注册客户端。
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({
    
    AuthorizationServerEndpointsConfiguration.class, AuthorizationServerSecurityConfiguration.class})
public @interface EnableAuthorizationServer {
    
    

}

尝试启动项目试试:

Using generated security password: 578f7a57-d39a-4470-a967-f3aa600f2b75

2022-02-28 21:10:39.959  INFO 22060 --- [           main] a.OAuth2AuthorizationServerConfiguration : Initialized OAuth2 Client

security.oauth2.client.client-id = 07d8e2f0-3829-4c48-bffc-1735fbb04eec
security.oauth2.client.client-secret = c48feec5-3e26-4546-8ef7-448a2646c65d
Will not secure Or [Ant [pattern='/oauth/token'], Ant [pattern='/oauth/token_key'], Ant [pattern='/oauth/check_token']]

按照Spring Security 的步骤,此时应该直接访问项目端口,但是却出现了 404。这是因为oauth2 版本的security 只开放了几个默认的访问路径,需要自己自定义。

/oauth/authorize:      验证
/oauth/token:          获取token
/oauth/confirm_access: 用户授权
/oauth/error:          认证失败
/oauth/check_token:    资源服务器用来校验token
/oauth/token_key:      如果jwt模式则可以用此来从认证服务器获取公钥

在这里插入图片描述

扫描二维码关注公众号,回复: 15626706 查看本文章

尝试访问 /oauth/authorize ,获取授权码,会发现这样的错误,提示“无效的客户端”。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wiFOVKm9-1649223363624)(https://secure2.wostatic.cn/static/r4KHW5qjbP1SxPraF5E5VU/image.png)]

这时我们查看Idea控制台的输出,会提示你输入的客户端Id 为 null。

2022-02-28 21:22:37.364  INFO 34584 --- [o-18001-exec-10] o.s.s.o.p.e.AuthorizationEndpoint        : Handling ClientRegistrationException error:
 No client with requested id: null

2. Oauth2.0中的一些概念

概念性的东西看看文档就好
Oauth2
Spring Security之Oauth2 2.0 client
Oahth2 五种授权模式介绍
接着上面:我们发现并没有指定 client_id,那么我们通过拼接Url的形式,指定 client_id ,会发现依然不能访问。因为我们的后台并没有对客户端进行配置,授权中心是无法识别的。
在这里插入图片描述

3. 配置客户端

上面@EnableAuthorizationServllet中的注解就有提到,我们需要重写AuthorizationServerConfigurerAdapter 类来实现自定义效果。
重写方法:

@Configuration
public class MyAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
    
    
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    
    
        super.configure(clients);
    }
}

ClientDetailsServiceConfigurer :(主要方法)

public class ClientDetailsServiceConfigurer extends
    SecurityConfigurerAdapter<ClientDetailsService, ClientDetailsServiceBuilder<?>> {
    
    
 
  // 内存中生成 客户配置信息的构造器
  public InMemoryClientDetailsServiceBuilder inMemory() throws Exception {
    
    
    InMemoryClientDetailsServiceBuilder next = getBuilder().inMemory();
    setBuilder(next);
    return next;
  }
  // 数据库中查找出来的 客户配置信息的 构造器
  public JdbcClientDetailsServiceBuilder jdbc(DataSource dataSource) throws Exception {
    
    
    JdbcClientDetailsServiceBuilder next = getBuilder().jdbc().dataSource(dataSource);
    setBuilder(next);
    return next;
  }
  

那么我们先使用内存中配置客户信息的方式,观察InMemoryClientDetailsServiceBuilder类(源码)。

// 继承了 客户详细信息服务生成器
public class InMemoryClientDetailsServiceBuilder extends
    ClientDetailsServiceBuilder<InMemoryClientDetailsServiceBuilder> {
    
    

  private Map<String, ClientDetails> clientDetails = new HashMap<String, ClientDetails>();

  @Override
  protected void addClient(String clientId, ClientDetails value) {
    
    
    clientDetails.put(clientId, value);
  }

  @Override
  protected ClientDetailsService performBuild() {
    
    
    InMemoryClientDetailsService clientDetailsService = new InMemoryClientDetailsService();
    clientDetailsService.setClientDetailsStore(clientDetails);
    return clientDetailsService;
  }

}

回到配置类,我们配置一个客户端Id,重启继续访问。观察页面和控制台。

 clients.inMemory() // 使用内存配置方式
                .withClient("client"); // 指定 客户端 Id
2022-02-28 21:47:34.872  INFO 22916 --- [io-18001-exec-1] o.s.s.o.p.e.AuthorizationEndpoint        : Handling OAuth2 error: 
error="unsupported_response_type", error_description="Unsupported response types: []"

# 提示 不支持这个响应类型  

(补充一下,经过查阅源码,这里指定响应类型就好,系统底层会给clientid配置""的secret)

我们会发现,除了指定clientId,还要指定其他的信息,通过观察,配置的客户端信息都是一个叫做 BaseClientDetails(实现ClientDetails)的类,这个类中封装着一些重要的属性。

这里只列举几个,具体自己查看:

clientId:             ( 必须的)用来标识客户的Id。
secret:               (需要值得信任的客户端)客户端安全码,如果有的话。
scope :                用来限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围。
authorizedGrantTypes:  此客户端可以使用的授权类型,默认为空。
authorities:           此客户端可以使用的权限(基于Spring Security authorities)。
jti:                   TOKEN_ID ,refreshToken标识
ati:                   ACCESS_TOKEN_ID,accessToken 标识

了解配置后,我们全部都配置上,重启项目访问。

配置类

// 上面有提到 需要实现这个接口实现自定义功能
@Configuration
public class MyAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
    

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    
    
        clients.inMemory() // 使用in-memory存储
                .withClient("client") // client_id
                .secret("secret") // client_secret
                .authorizedGrantTypes("authorization_code") // 该client允许的授权类型
                .redirectUris("http://www.baidu.com") // 不指定的话,页面访问后会提示的
                .scopes("app"); // 允许的授权范围

    }
}

这时我们发现,浏览器重定向到了百度的页面。但是,项目后台还是在报错。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kebGhk7t-1649223363624)(https://secure2.wostatic.cn/static/95UKE7kWYsLhBj1iQrLxmP/image.png)]

4. 配置用户信息

还是提示响应类型不正确,并且出现了新的错误,说我们没有指定用户信息。(用户必须先通过 Spring Security 进行身份验证才能完成授权)

这里配置的用户信息,我们先配置一个简单的固定的用户。重启项目继续访问。

@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    @Bean
    public PasswordEncoder passwordEncoder(){
    
    
        return new BCryptPasswordEncoder();
    }

    // 定义加密方式,以及用户校验
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
        auth.userDetailsService(
                new UserDetailsService() {
    
    
                    @Override
                    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    
                        return new User("admin", passwordEncoder().encode("admin"), Collections.emptyList());
                    }
                }
        );
    }
}

这时就是熟悉的 界面,然后输入用户名密码。会发现百度页面还是正常的跳转。后台用户未授权的提示信息也没有了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ypDx1o9a-1649223363625)(https://secure2.wostatic.cn/static/2Pxt8CBMKkdghh6EdyWC3u/image.png)]

响应类型问题:我们可以在Url后面通过拼接参数指定。http://localhost:18001/oauth/authorize?client_id=client&response_type=code

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ufrKdKvA-1649223363625)(https://secure2.wostatic.cn/static/bBheboNH8npscm83eSGncr/image.png)]

点击同意后点击授权,页面正常跳转。并且观察 url 地址,发生了变化 :https://www.baidu.com/?code=naPnfX

5. 授权码获取token

到这一步。授权中的授权码就获取完毕了,接下来就是换取 token的步骤。因为框架默认不提供 token的实现,这里我们需要自己配置以下。

官网提供了三种模式:默认应该是内存实现的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NR077M2a-1649223363625)(https://secure2.wostatic.cn/static/31cmqbQQxe8mRs1UPvRqdk/image.png)]

我们可以查看以下 InMemoryClientDetailsService类,使用Debug工具打一个端点。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LPJLRM09-1649223363626)(https://secure2.wostatic.cn/static/6d89EBEarwkUnQ6yfd8ztd/image.png)]

我们访问http://localhost:18001/oauth/token。会发现弹出一个密码框,这个有点像security的用户密码框,但是这里不是。我们随便填写信息,点击登录。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wjp91iFW-1649223363626)(https://secure2.wostatic.cn/static/oxEhKbpoCTsy9UJubD9kmr/image.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xXeiiVLr-1649223363627)(https://secure2.wostatic.cn/static/unRETTwcc8CvR4oJPr5UqU/image.png)]

如上图,我们发现访问时,后台Debug生效了,查看断点位置,可以发现,我们输入的用户名对应的后台的 clientId,系统会去内存中存储的 客户端 管理信息缓存中查找对应的配置信息,咱们这里当然时没有的,那么我们指定对应的 clientId 试试。 继续观察 debug .

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C1SUa2mH-1649223363627)(https://secure2.wostatic.cn/static/5smDEBUxLaeBoRvF2FkUi5/image.png)]

通过Debug 我们发现,第一次 debug 位置返回的客户端配置信息,传递 到了ClientDetailsUserDetailsService (客户详细信息用户详细信息服务)类中。上面是 查找 username 对应的 客户端,

最后创建一个security提供的 User对象(之前学习过 security应该比较熟悉)。用户名为 clientId,密码为 clientSecrect,权限为客户端配置信息的所有权限。继续 Debug.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iOaYCvap-1649223363627)(https://secure2.wostatic.cn/static/t7X2pbiKTF38EDfemR8kPG/image.png)]

上面大致就是返回一个 用户配置信息对象,就是刚才 返回User 实现的类。继续debug

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0xB2T0LA-1649223363628)(https://secure2.wostatic.cn/static/8MGzsQUmJJdbvc18BU9cxm/image.png)]

我们发现来到了AbstractUserDetailsAuthenticationProvider(用户详细信息身份验证提供程序),Assert.notNull验证用户不为空。继续网下面看,我们会发现,检查用户配置信息时,抛出一个异常。

提示用户有不好的信誉历史。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7UBkgI9A-1649223363628)(https://secure2.wostatic.cn/static/8WoycpFQo8fWBdJJLB1Gs8/image.png)]

我们再试一次,点进去 additionalAuthenticationChecks方法,可以看到,走进去的错误为,我们提供的密码不正确。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7IynYWDf-1649223363629)(https://secure2.wostatic.cn/static/7y5xX7cRh3qDLL7LX2zxe7/image.png)]

上面提示我们输入的 secret密码和 用户配置信息中对比到的不正确,猜想可能是因为在MyWebSecurityConfig中配置了用户密码加密方式(BCryptPasswordEncoder),然而我们的客户端信息配置时,没有使用到加密。

配置加密

    // 客户配置
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    
    
        clients.inMemory() // 使用in-memory存储
                .withClient("client") // client_id
                .secret(new BCryptPasswordEncoder().encode("secret")) // client_secret 使用相同的加密方式
                .authorizedGrantTypes("authorization_code") // 该client允许的授权类型
                .redirectUris("http://www.baidu.com") // 不指定的话,页面访问后会提示的
                .scopes("app"); // 允许的授权范围
    }

继续访问,我们发现后台提示错误信息 ,这是因为请求方式不支持 post,我们换成测试工具测试。

2022-03-01 10:52:31.231  INFO 22860 --- [io-18001-exec-4] o.s.s.o.p.e.AuthorizationEndpoint        : Handling OAuth2 error: error="unsupported_response_type", error_description="Unsupported response types: []"
2022-03-01 10:53:32.488  INFO 22860 --- [io-18001-exec-1] o.s.s.o.provider.endpoint.TokenEndpoint  : Handling error: HttpRequestMethodNotSupportedException, Request method 'GET' not supported

工具配置

在这里插入图片描述

配置解释:请求体中指定授权类型,同时加上第一次测试拿到的授权码。因为第一次弹出了用户名密码框,所以这里配置 Auth信息,用户名为 ClientId,密码为 secret。

{
    
    
    "access_token": "eac28570-3abd-4225-bb42-ac5faa94fcf0",
    "token_type": "bearer",
    "expires_in": 43117,
    "scope": "app"
}

6. 校验 token

当我们获取到 access_token后,按理来说应该可以访问接口/oauth/check_token,但是,通过访问可以看到,我们的请求被拒绝了。提示未授权,

在这里插入图片描述

我们可以发现自定义的MyAuthorizationServerConfig,只是重写了其中的一个功能(客户信息配置),不难发现,还有一个是关于授权服务器安全的配置(AuthorizationServerSecurityConfigurer security)。查看他的属性(源码)。

public final class AuthorizationServerSecurityConfigurer extends
    SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    
    
  
  // 身份验证入口点
  private AuthenticationEntryPoint authenticationEntryPoint;
  
  // 拒绝访问处理程序
  private AccessDeniedHandler accessDeniedHandler = new OAuth2AccessDeniedHandler();

  // 密码编码器
  private PasswordEncoder passwordEncoder; // for client secrets
  
  // https://blog.csdn.net/xichenguan/article/details/77965208
  // https://blog.csdn.net/qq_35067322/article/details/120030403
  // 领域(我也不理解,可以看其他的博客)
  private String realm = "oauth2/client";
  
  // 允许客户端的表单身份验证
  private boolean allowFormAuthenticationForClients = false;
  
  // 令牌密钥访问(默认全部拒绝)
  private String tokenKeyAccess = "denyAll()";
  
  // 检查令牌访问
  private String checkTokenAccess = "denyAll()";
  
  // 仅限于 ssl
  private boolean sslOnly = false;

  /**
   * Custom authentication filters for the TokenEndpoint. Filters will be set upstream of the default
   * BasicAuthenticationFilter.
   */
  // 令牌端点身份验证过滤器
  private List<Filter> tokenEndpointAuthenticationFilters = new ArrayList<Filter>();
  }

接下来我们给 checkTokenAccess放行,加上以下配置

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    
    
        security.checkTokenAccess("permitAll()"); // 全部放行
    }

重启项目再次测试,会得到以下结果,以下结果就是验证通过的意思。

{
    
    
    "active": true,
    "exp": 1646157970,
    "user_name": "admin",
    "client_id": "client",
    "scope": [
        "app"
    ]
}

以下为错误信息,提示 token 未通过验证

{
    
    
    "error": "invalid_token",
    "error_description": "Token was not recognised"
}

1)校验 token的过程

根据控制台提示报错的信息,可以发现是CheckTokenEndpoint这个类中做的校验,搜索该类,可以发现下面的方法,打断点进行测试.(源码)

  @RequestMapping(value = "/oauth/check_token")
  @ResponseBody
  public Map<String, ?> checkToken(@RequestParam("token") String value) {
    
    
    
    // 从指定的token 缓存中获取这个 token 
    OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value);
    if (token == null) {
    
    
      throw new InvalidTokenException("Token was not recognised");
    }

    if (token.isExpired()) {
    
    
      throw new InvalidTokenException("Token has expired");
    }

    OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());
    
    // 返回的信息就是这里面定义的,
    Map<String, Object> response = (Map<String, Object>)accessTokenConverter.convertAccessToken(token, authentication);

    // gh-1070
    response.put("active", true);  // Always true if token exists and not expired

    return response;
  }

通过Debug ,可以找到更具体地校验流程实在 InMemoryTokenStore中做地,也就是我们之前实现的 token 存储在内存中,他会拿着我们输入的toekn 作为key去内存中 查找。

[(img-LF0feZKI-1649223363630)(https://secure2.wostatic.cn/static/ivfcgyZQ2YQ2SJj4Drw4zK/image.png)]

这里就简单的配置就完啦,后续有时间会继续更新的,同时也是对自己的一个学习的过程。

猜你喜欢

转载自blog.csdn.net/m0_56186460/article/details/123987964