OAuth2.0 - 介绍与使用 及 授权码模式讲解

Posted 小毕超

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OAuth2.0 - 介绍与使用 及 授权码模式讲解相关的知识,希望对你有一定的参考价值。

一、OAuth2.0

前面我们已经学习了SpringSecuritySpringMVC环境下和WebFlux环境下用户认证授权以及整合JWT作为Token无状态认证授权,但是在前面的演示中都会发现全都是基于单体项目而言的,现在分布式微服务的环境下难不成还要每个服务都做一套自己的认证授权吗?

今天我们要讲解的OAuth2.0便可以解决分布式环境下的统一认证授权,我们要学习的是Spring推出的Spring-Oauth2.0框架,可以完美的和SpringBoot 以及 SpringCloud进行整合,而不需要复杂的配制,在学习前最好已经了解学习了SpringSecurity的使用,因为SpringSecuritySpringOauth2一般是一起使用的,SpringOauth2包含SpringSecurity,所以很多配制都需要结合SpringSecurity来完成。

下面是上篇博客的地址,不了解SpringSecurity的可以参考下:

https://blog.csdn.net/qq_43692950/article/details/122513687

下面我们就一起学习下OAuth2.0吧!

OAuth(开放授权)是一个开放标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不 需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。OAuth2.0OAuth协议的延续版本,但不向 后兼容OAuth 1.0即完全废止OAuth1.0。很多大公司如Google,Yahoo,Microsoft等都提供了OAUTH认证服 务,这些都足以说明OAUTH标准逐渐成为开放资源授权的标准。

在我们平常开发中,或多或少的应该都接触过Oauth2,比如在用户登录时,经常采用的第三方登录的方式,如:微信登录、QQ登录、微博登录等,这些都是基于Oauth2协议来实现的认证授权过程。

就比如下面微信登录的过程,就是Aauth2的授权码模式的一个实现:

上面这种是授权码的方式,这种方式安全级别较高,另外Oauth2还提供了密码模式、客户端模式、简化模式,但不仅限于这四种,用户完全可以自定义认证授权过程,要知道Oauth2只是一个协议,并不限于具体哪个框架,SpringOauth2Oauth2的一个实现。

由于Oauth2可以解决分布式情况下的授权认证或第三方的授权认证,已经不是单体的架构,其中肯定有着某种的分工合作,其中在Oauth2中一般可以分为四个角色,包括:客户端、资源拥有者、授权服务器(也称认证服务器)、资源服务器。

  • 客户端可以理解为web浏览器、移动客户端、微信小程序等,就是用户看到的前端。

  • 资源拥有者通常指的是用户,也可以是应用程序,即该资源的最终给了谁。

  • 授权服务器,也称认证服务器,主要用户对用户的身份认证即权限的授权,以及全局令牌的颁发,就如上面微信认证的微信认证服务器。

  • 资源服务器也就是提供资源的服务器,就比如上面微信认证的微信用户数据服务器,存储着我闷需要的资源。

本篇文章我们先体验下授权码的方式进行认证授权,下篇文章我们对剩下三种模式进行演示。

二、基于授权码的认证授权

上面已经大概对Oauth2进行了介绍,并画出了微信认证的过程,上面提到微信认证是基于授权码的认证模式,下面我们借助SpringOauth2一起来简单实现下授权码的认证授权模式。

maven父项目

首先我们先创建一个maven的父项目,在父项目中我们指定下SpringBoot以及SpringCloud的版本:

<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>$spring-boot.version</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <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>

授权服务

下面首先在父项目下添加一个auth授权服务module,作为统一认证授权和颁发token的服务器,端口我们设为8020

在pom中引入以下依赖:

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

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

SpringOauth2的配制要通过继承AuthorizationServerConfigurerAdapter这个类来配制信息,并声明@EnableAuthorizationServer注解,表示这是认证服务。

其中AuthorizationServerConfigurerAdapter配制类,有三个方法:

ClientDetailsServiceConfigurer用来配置客户端详情服务,客户端详情信息在 这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。能够使用内存或者JDBC来实现客户端详情服务(ClientDetailsService),而ClientDetails有几个重要的属性如下:

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

AuthorizationServerEndpointsConfigurer用来配置令牌(token)的访问端点和令牌服务(token services),可以通过设定以下属性决定支持的授权类型(Grant Types):

  • authenticationManager:认证管理器,当你选择了资源所有者密码(password)授权类型的时候,请设置 这个属性注入一个 AuthenticationManager 对象。

  • userDetailsService:如果你设置了这个属性的话,那说明你有一个自己的UserDetailsService 接口的实现, 或者你可以把这个东西设置到全局域上面去(例如 GlobalAuthenticationManagerConfigurer 这个配置对 象),当你设置了这个之后,那么 “refresh_token” 即刷新令牌授权类型模式的流程中就会包含一个检查,用 来确保这个账号是否仍然有效,假如说你禁用了这个账户的话。

  • authorizationCodeServices:这个属性是用来设置授权码服务的(即 AuthorizationCodeServices 的实例对 象),主要用于 “authorization_code” 授权码类型模式。

  • implicitGrantService:这个属性用于设置隐式授权模式,用来管理隐式授权模式的状态。

  • tokenGranter:当你设置了这个东西(即 TokenGranter 接口实现),那么授权将会交由你来完全掌控,并 且会忽略掉上面的这几个属性,这个属性一般是用作拓展用途的,即标准的四种授权模式已经满足不了你的 需求的时候,才会考虑使用这个。

AuthorizationServerSecurityConfigurer用来配置令牌端点的安全约束。permitAll()表示允许所有人。

首先声明出Token的模式,这里先将Token存放在内存中:

@Configuration
public class TokenConfig 
    @Bean
    public TokenStore tokenStore() 
        return new InMemoryTokenStore();
    

然后声明AuthorizationServer并继承AuthorizationServerConfigurerAdapter

@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter 

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private AuthorizationCodeServices authorizationCodeServices;


    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception 
//         clients.withClientDetails(clientDetailsService);
        clients.inMemory()// 使用in‐memory存储
                .withClient("c1")// client_id
                .secret(new BCryptPasswordEncoder().encode("secret"))
                .resourceIds("res1")
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")// 该client允许的授权类型 authorization_code,password,refresh_token,implicit,client_credentials
                .scopes("all")// 允许的授权范围
                .autoApprove(false) //加上验证回调地址
                .authorities("admin")
                .redirectUris("http://www.baidu.com");
    

    //设置授权码模式的授权码如何存取,暂时采用内存方式
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() 
        return new InMemoryAuthorizationCodeServices();
    

    @Bean
    public AuthorizationServerTokenServices tokenService() 
        DefaultTokenServices service = new DefaultTokenServices();
        service.setClientDetailsService(clientDetailsService);
        service.setSupportRefreshToken(true);
        service.setTokenStore(tokenStore);
        service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
        service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
        return service;
    

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) 
        endpoints
                .authenticationManager(authenticationManager)//认证管理器
                .authorizationCodeServices(authorizationCodeServices)//授权码服务
                .tokenServices(tokenService())//令牌管理服务
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception 
        security
                .tokenKeyAccess("permitAll()")                    //oauth/token_key是公开
                .checkTokenAccess("permitAll()")                  //oauth/check_token公开
                .allowFormAuthenticationForClients();                //表单认证(申请令牌)
    

还需创建一个WebSecurityConfig来继承WebSecurityConfigurerAdapter来控制auth的权限控制,这里的配制可以参考本专栏讲解SpringSecurity的博客:

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter 

    //认证管理器
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception 
        return super.authenticationManagerBean();
    
    //密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() 
        return new BCryptPasswordEncoder();
    

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception 
        auth.inMemoryAuthentication().withUser("admin").password(new BCryptPasswordEncoder().encode("1234")).authorities("admin");
        auth.inMemoryAuthentication().withUser("common").password(new BCryptPasswordEncoder().encode("1234")).authorities("common");
    

    //安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception 
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/auth/*").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin();
    

到这里auth服务就已经简单搭建好了,先启动该服务,下面还需创建一个资源服务器。

资源服务

和上面一样新建一个resource的SpringBoot module,其中pom中引入的依赖和auth服务一致,设定端口为8080,在资源服务我们首先创建出资源,就是提供两个接口:

@RestController
public class ResourceTestController 

    @GetMapping("/admin/test")
    public ResponseTemplate adminTest()
        return ResponseTemplate.builder().message("admin test resource !").build();
    

    @GetMapping("/common/test")
    public ResponseTemplate commonTest()
        return ResponseTemplate.builder().message("common test resource !").build();
    

下面就要资源服务器配制了,但资源服务不是继承AuthorizationServerConfigurerAdapter了,而是继承ResourceServerConfigurerAdapter,其中指定的TokenStore要和auth服务的一致,下面创建ResouceServerConfig继承ResourceServerConfigurerAdapter

@Configuration
@EnableResourceServer
public class ResouceServerConfig extends ResourceServerConfigurerAdapter 


    public static final String RESOURCE_ID = "res1";

    @Autowired
    TokenStore tokenStore;

    @Bean
    public TokenStore tokenStore() 
        return new InMemoryTokenStore();
    

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) 
        resources.resourceId(RESOURCE_ID)//资源 id
                .tokenStore(tokenStore)
                .tokenServices(tokenService())//验证令牌的服务
                .stateless(true);
    

    @Override
    public void configure(HttpSecurity http) throws Exception 
        http
                .authorizeRequests()
                .antMatchers("/admin/**").hasAuthority("admin")
                .antMatchers("/common/**").hasAuthority("common")
                .and().csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    

    //资源服务令牌解析服务
    @Bean
    public ResourceServerTokenServices tokenService() 
        //使用远程服务请求授权服务器校验token,必须指定校验token 的url、client_id,client_secret
        RemoteTokenServices service = new RemoteTokenServices();
        service.setCheckTokenEndpointUrl("http://localhost:8020/oauth/check_token");
        service.setClientId("c1");
        service.setClientSecret("secret");
        return service;
    


同样也需要创建一个WebSecurityConfig继承WebSecurityConfigurerAdapter来控制资源服务的授权:

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter 


    //安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception 
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/**").authenticated()
                .anyRequest().permitAll()
        ;
    

到这里就可以启动我们的资源服务了。

三、授权码模式测试

假如我们直接访问http://localhost:8080/admin/test接口,可以看到无法直接访问:

我们知道授权码模式,第一步就是先获取授权码,向浏览器访问下面路径:

http://localhost:8020/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com

会弹出登录的页面,在这输入我们配制的admin/1234

进入后,还要点击下授权:

下面应该发现页面调到的百度的页面,因为我们配制的就是重定向到百度,但是可以发现后面多出了一个参数code,这个就是授权码:

下面我们使用PostMan拿着这个授权码,使用POST方式获取Token

http://localhost:8020/oauth/token?client_id=c1&client_secret=secret&grant_type=authorization_code&code=lgLnVP&redirect_uri=http://www.baidu.com


然后使用上面的Token,再次访问http://localhost:8080/admin/test接口:

注意填写Token时key为Authorization,value加一个Bearer 中间还有一个空格。

加入访问http://localhost:8080/common/test应该是无权访问的:


喜欢的小伙伴可以关注我的个人微信公众号,获取更多学习资料!

以上是关于OAuth2.0 - 介绍与使用 及 授权码模式讲解的主要内容,如果未能解决你的问题,请参考以下文章

一图搞定Oauth2.0授权码模式

oauth2.0授权码模式详解

OAuth2.0 - 使用数据库存储客户端信息 及 授权码

OAuth2.0系列之授权码模式实践教程

从安全角度看Oauth2.0授权码模式

iOS OAuth2.0的使用