计算机系统应用教程网站

网站首页 > 技术文章 正文

Spring Boot(十二):陌生又熟悉的 OAuth2.0 协议

btikc 2024-09-25 15:17:17 技术文章 26 ℃ 0 评论

大家好,我是 杰哥

本篇,我们来认识一个你一定陌生又熟悉的授权协议 :OAuth2.0(Open Authorization)协议

你可能也发现了,不知不觉中,在登录一些 APP 或者网站(应用 B)时,为了方便,我们总会直接使用他们所提供的第三方登录方式,如微信或者 QQ 登录,进行扫码登录。它们实际上采用的就是 OAuth2.0 协议

它是一个关于授权(authorization)的开放网络标准,是目前应用最广泛的标准之一。指的是我们通过微信、github等帐号的方式登录一些网站或应用,比如 gitee、有道云笔记、简书等。目前包括 github、facebook、twitter、微信、微博、QQ 等社交工具都集成了 OAuth2.0 协议以供第三方应用程序使用,实现一个社交帐号打通所有应用的效果

实例

比如说,我们采用微信账号的方式来登录码云gitee

1、选择使用微信账号登录

登录码云,默认采用用户名密码的方式登录,我们选择使用微信账号登录的方式

2、进入授权页面,生成授权链接

点击微信图标之后,进入如下页面


我们可以看到,该页面实际上是生成了一个请求授权的链接:


https://open.weixin.qq.com/connect/qrconnect?appid=wx63d402790645b7e6&redirect_uri=https%3A%2F%2Fgitee.com%2Fauth%2Fwechat%2Fcallback&response_type=code&scope=snsapi_login&state=7d4c668ff6b13870f6894f7153480d31ac16ce955a4a4669#wechat_redirect

这个链接中包含如下几个字段:

  • app_id:表示客户端的 ID,必选项
  • redirect_uri:表示重定向 URI,可选项。当点击确认授权时,会重定向跳转到 redirect_url ,并带上 code 的参数
  • scope:表示申请的权限范围,可选项
  • state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值
  • response_type:表示授权类型,必选项,此处的值固定为"code",即授权码当我们使用微信扫描了二维码之后,微信便进行对用户的身份验证

3、用户完成认证并决定是否同意该第三方的申请

验证成功之后,微信弹出如下页面,询问我们是否同意向这个应用开放我们的昵称、头像

点击"允许"之后,即表示通过了用户的同意

此时就成功完成了 gitee 的登录动作,接下来便可以正常使用 gitee 的功能了

这个例子中,用户想要登录 gitee, 但不想在这个网站上面进行用户名的注册,选择使用已有的微信账号进行快捷登录。而微信只需要验证一下用户的身份,并请求用户进行授权的确认即可实现登录效果

角色

通过这个例子,我们发现,OAuth2.0 授权协议包含了以下几种角色

1、Third-party application第三方应用程序,即这里的码云 gitee

2、Resource Owner资源所有者,也就是持有微信帐号的用户

3、User Agent用户代理,比如浏览器这样的工具

4、Authorization server:授权服务器,也就是像微信、github 配置的专门用来处理授权的服务器

5、Resource server:资源服务器,即微信、github 存放用户生成的资源如帐号、头像、昵称等的服务器。需要注意的是它与认证服务器,可以是同一台服务器,也可以是不同的服务器

流程

整体流程如下:



即:

1、用户通过浏览器访问第三方应用。比如一个网站地址,即上述例子中的码云

2、重定向至授权页面,用户选择是否同意授权。用户允许授权之后,第三方应用则采用授权服务器(也叫认证服务器)进行登录。这里的微信就是授权服务器,采用 appid、appSecret 获取 授权码 code

3、授权服务器返回 code

4、第三方应用再根据 code 授权服务器申请令牌 access_token

5、授权服务器询问用户是否同意授权该资源的访问

6用户同意授权

7、用户同意授权之后,授权服务器则生成并返回 access_token

8、第三方应用再携带 access_token 请求相应的资源,如昵称、头像等

9、资源服务器检验其 access_token 的正确性。若 access_token 正确,那么返回相应资源即可

10、浏览器显示用户登录成功

二、使用场景

OAuth2.0 协议的使用场景主要为:第三方登录开放接口的调用

第三方登录就是我们上面所展示的,使用微信等第三方的方式来登录一个应用或者网站

开发接口的调用,则比如说微信、美团等平台向外提供一些自己的接口,这些接口会由很多商家调用来进行开发,对于这些接口的调用则同样可以使用 OAuth2.0 协议

上面为大家展示了第三方登录的例子,接下来我们一起来看下采用 OAuth2.0 协议进行开放接口的开发实战

三、Spring Boot 集成 Security oauth2(实战)

(一)实战说明

包含两个项目:

  • 授权服务器springSecurityOathu2
  • 资源服务器resource_server

实现用户在访问该资源服务器上的资源时,必须首先经过授权服务器的授权才可以正常访问

两者的关系如下:

也就是说,用户在调用资源服务上的接口时,需要

1)根据 appid、appSecret 请求授权服务器获取 code

2)授权服务器需要检验用户的认证信息(若用户未登录,则需要登录),若认证信息校验通过,则返回授权码 code

3)用户再根据 code 获取 令牌 access_token

4)授权服务器询问用户是否允许授权

5)用户同意授权

6)用户同意授权之后,授权服务器生成并返回 令牌 access_token

7)用户再带上 access_token 请求资源

8)资源服务器会首先访问授权服务器,对该 access_token 的验证

9)授权服务器返回 access_token 的验证结果,若验证成功,则返回成功

10)资源服务器返回其请求的资源

(二)实战步骤

授权服务器配置

springSecurity (Spring Boot Security 实现认证授权功能的示例项目)项目的基础上,添加如下步骤:

1、引入依赖

<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.5.2.RELEASE</version>
</dependency>

2、权限配置类 AuthorityConfig

首先采用注解 @EnableAuthorizationServer 声明该项目为授权服务器,然后继承 AuthorizationServerConfigurerAdapter 类来进行具体的权限配置

注意:在 Spring 2.7 中,类 AuthorizationServerConfigurerAdapter 已被标记为 deprecated ,也就是说我们这里的实现方式并不是最新的。最新的方案可以参考链接

https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guide

但是无论是最新的还是旧的实现方案,思想都是一样的,我们先通过该方案了解其思想即可。最新的方案,我也会将代码更新至 github

1)通过 configure(AuthorizationServerSecurityConfigurer security) 方法配置允许表单模式的授权

/**
     * 允许表单模式的认证
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients()
                .checkTokenAccess("permitAll()");
    }

2)配置允许通过的所有客户端的信息

  • 写入内存的方式
  • 通过覆盖 configure(ClientDetailsServiceConfigurer clients) 方法配置所授权的所有客户端信息
  • 这里,我们先采用在代码中直接写入内存的方式进行配置,以便清晰地了解其配置方式
/**
     * 配置所有客户端的信息(client_id、secret、授权模式、scope、资源id 以及重定向地址)
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        /**
         * 方法一:内存配置
         */
        clients.inMemory()
                .withClient("user-client")
                .secret(passwordEncoder.encode("123456"))
                .authorizedGrantTypes("authorization_code")
                .scopes("all")
                .resourceIds("wj_resource")
                .redirectUris("http://localhost:8082/user/hello");
    }

配置了一个 client_id user-client,密码为 123456,采用授权码模式进行认证、范围为 all,即所有,resourceIdwj_resourceredirectUrishttp://localhost:8082/user/hello

即作为授权服务器,允许 client_iduser-client,密码为 123456,采用授权码模式进行认证,对于 资源标识为 wj_resource 的客户端通过授权,当用户确认授权之后的重定向地址为 http://localhost:8082/user/hello

如果还有有其他客户端同样需要使用授权服务器进行授权操作,那么可以继续配置

当然也可以直接将这些客户端管理在 db 中,我们来看看使用 db 来配置客户端的步骤

  • db 的方式

先创建表 oauth_client_details ,包含字段 client_id、resource_ids、client_secret、scope、authorized_grant_types、web_server_redirect_uri 等关键字段。然后插入需要配置的所有客户端信息

建表语句:

create table oauth_client_details (    client_id VARCHAR(256) PRIMARY KEY,    resource_ids VARCHAR(256),    client_secret VARCHAR(256),    scope VARCHAR(256),    authorized_grant_types VARCHAR(256),    web_server_redirect_uri VARCHAR(256),    authorities VARCHAR(256),    access_token_validity INTEGER,    refresh_token_validity INTEGER,    additional_information VARCHAR(4096),    autoapprove VARCHAR(256));
INSERT INTO oauth_client_details    (client_id, client_secret, scope, authorized_grant_types,    web_server_redirect_uri, authorities, access_token_validity,    refresh_token_validity, additional_information, autoapprove)VALUES    ('user-client', '$2a$10$4zd/aj2BNJhuM5PIs5BupO8tiN2yikzP7JMzNaq1fXhcXUefWCOF2', 'all',    'authorization_code,refresh_token,password', null, null, 3600, 36000, null, true);
INSERT INTO oauth_client_details    (client_id, client_secret, scope, authorized_grant_types,    web_server_redirect_uri, authorities, access_token_validity,    refresh_token_validity, additional_information, autoapprove)VALUES    ('order-client', '$2a$10$4zd/aj2BNJhuM5PIs5BupO8tiN2yikzP7JMzNaq1fXhcXUefWCOF2', 'all',    'authorization_code,refresh_token,password', null, null, 3600, 36000, null, true);

这里我们配置了 user-clientorder-client 两个客户端 代码中直接通过配置数据库信息,代码中只需要通过 clients.jdbc(dataSource) 指定数据源信息,框架便会自动加载数据库中 oauth_client_details 表的信息

/**
     * 配置所有客户端的信息(client_id、secret、授权模式、scope、资源id 以及重定向地址)
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        /**
         * 方法二:db配置
         */
        JdbcClientDetailsServiceBuilder jcsb = clients.jdbc(dataSource);
        jcsb.passwordEncoder(passwordEncoder);
    }

整体类配置如下:

/**
 * 授权的 server 端
 */
@Component
@EnableAuthorizationServer
public class AuthorityConfig extends AuthorizationServerConfigurerAdapter {
    @Resource
    private PasswordEncoder passwordEncoder;
    @Resource
    private DataSource dataSource;
    /**
     * 允许表单模式的认证
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients()
                .checkTokenAccess("permitAll()");
    }
    /**
     * 配置所有客户端的信息(client_id、secret、授权模式、scope、资源id 以及重定向地址)
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        /**
         * 方法一:内存配置
         */
//        clients.inMemory()
//                .withClient("user-client")
//                .secret(passwordEncoder.encode("123456"))
//                .authorizedGrantTypes("authorization_code")
//                .scopes("all")
//                .resourceIds("wj_resource")
//                .redirectUris("http://localhost:8082/user/hello");
        /**
         * 方法二:db配置
         */
        JdbcClientDetailsServiceBuilder jcsb = clients.jdbc(dataSource);
        jcsb.passwordEncoder(passwordEncoder);
    }
}

资源服务器配置

1、引入同样的依赖

<!--security 依赖 -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.security.oauth</groupId>
   <artifactId>spring-security-oauth2</artifactId>
   <version>2.5.2.RELEASE</version>
</dependency>

2、建立资源 API

@RestController
@RequestMapping("/rs")
public class ResourceController {
    @GetMapping("/get_resource")
    public String hello() {
        return "我是被保护的资源接口";
    }
}

3、资源配置类

@Configuration
@EnableResourceServer
public class ResourceConfig  extends ResourceServerConfigurerAdapter {
    @Value("${wj.appid}")
    private String wjAppId;
    @Value("${wj.appsecret}")
    private String wjAppSecret;
    /**
     *  密码解密类
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    /**
     * 校验 token 的正确性
     * @return
     */
    @Bean
    @Primary
    public RemoteTokenServices remoteTokenServices(){
        final RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
        remoteTokenServices.setClientId(wjAppId);
        remoteTokenServices.setClientSecret(wjAppSecret);
        remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:8082/oauth/check_token");
        return remoteTokenServices;
    }
    /**
     *  定义资源id,以及无状态声明
     * @param resources
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId("wj_resource").stateless(true);
    }
    /**
     * 设置 所有请求必须先授权之后再进行请求
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        //设置创建session策略
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
        //@formatter:off
        //所有请求必须授权
        http.authorizeRequests()
                .anyRequest().authenticated();
        //@formatter:on
    }
}

由于资源服务器与授权服务器不在同一个服务,那么需要

1)通过 remoteTokenServices() 类来配置校验 token 正确性的信息

2)通过 configure(ResourceServerSecurityConfigurer resources) 方法来指定资源 id 等信息

3)再通过 configure(HttpSecurity http) 设置所有请求必须先授权之后再进行请求

4、配置文件

配置该资源服务器的 appidappsecret

wj:
  appid: user-client
  appsecret: 123456
server:
  port: 8085

完成配置之后,我们预期实现的效果是,在访问资源 id 时需要带上令牌才能正常访问

而在此之前:

1)授权服务器会首先校验用户的信息

2)然后根据用户所提供的 appId、appSecret 等信息返回授权码 code 的值

3)再根据 code 的值返回 access_token 的值

让我们访问测试一下试试~

(三)访问测试

1、认证接口

请求如下认证接口的链接,询问用户是否同意授权

http://localhost:8082/oauth/authorize?client_id=user-client&response_type=code

会进入登录页面

输入用户名密码,点击登录以后,进入授权认证页面。浏览器提示我们,你是否允许 user-client 访问你的受保护的资源?


选择允许之后,点击授权

会跳转到我们提前配置好的重定向地址 http://localhost:8082/user/hello,同时带上授权码 code 的值

2、根据授权码 code 获取 token

拿到了授权码,我们再根据 grant_type、redirect_url 以及 scope 来使用如下链接,申请令牌 access_token(该链接为 post 请求方式)

http://localhost:8082/oauth/token?code=eQVgni&grant_type=authorization_code&redirect_uri=http://localhost:8082/user/hello&scope=all

可以看到,我们得到的 token 的值为 :Rt-oo359d5dVQviNeMlyr4PDskU

3、校验 token 的正确性

采用 链接 http://localhost:8082/oauth/check_token?token=Rt-oo359d5dVQviNeMlyr4PDskU,进行 token 正确性的检验

根据返回结果,发现该 access_token 的值,被检验通过

4、资源服务器的访问

1)资源服务端直接访问首先我们直接来访问资源 /rs/get_resource


由于在代码中配置了所有请求必须先授权之后再进行请求的规则,所以这样直接访问,会返回一个 401-未授权的错误

2)指定 token 进行访问


带上 token 进行访问,可以看到成功访问了到我们的接口

符合预期,那么我们的开放接口就开发好了

总结

本篇文章,我们主要介绍了OAuth2.0 授权协议及其两个主要的使用场景:第三方登录开放接口的调用。分别通过使用微信账号登录 gitee 的实例与 Spring Boot 实现开放接口调用的实战,使大家进一步认识了 OAuth2.0 授权协议的机制

那么,你一定总结出来了,OAuth2.0 授权协议是用来干什么的呢?

一句话, OAuth2.0 授权协议其实就是将用户与应用之间的认证与授权动作,交给可信的第三方,如微信等授权服务器,而不用再次进行用户信息注册的一种协议

文章演示代码地址:

https://github.com/helemile/Spring-Boot-Notes/tree/master/spring_OAthu2


嗯,就这样。每天学习一点,时间会见证你的强大~


欢迎大家关注我们的公众号,一起持续性学习吧~


往期精彩回顾


总结复盘

架构设计读书笔记与感悟总结

带领新人团队的沉淀总结

复盘篇:问题解决经验总结复盘

网络篇

网络篇(四):《图解 TCP/IP》读书笔记

网络篇(一):《趣谈网络协议》读书笔记(一)

事务篇章

事务篇(四):Spring事务并发问题解决

事务篇(三):分享一个隐性事务失效场景

事务篇(一):毕业三年,你真的学会事务了吗?

Docker篇章

Docker篇(六):Docker Compose如何管理多个容器?

Docker篇(二):Docker实战,命令解析

Docker篇(一):为什么要用Docker?

..........

SpringCloud篇章

Spring Cloud(十三):Feign居然这么强大?

Spring Cloud(十):消息中心篇-Kafka经典面试题,你都会吗?

Spring Cloud(九):注册中心选型篇-四种注册中心特点超全总结

Spring Cloud(四):公司内部,关于Eureka和zookeeper的一场辩论赛

..........

Spring Boot篇章

Spring Boot(七):你不能不知道的Mybatis缓存机制!

Spring Boot(六):那些好用的数据库连接池们

Spring Boot(四):让人又爱又恨的JPA

SpringBoot(一):特性概览

..........

翻译

[译]用 Mint 这门强大的语言来创建一个 Web 应用

【译】基于 50 万个浏览器指纹的新发现

使用 CSS 提升页面渲染速度

WebTransport 会在不久的将来取代 WebRTC 吗?

.........

职业、生活感悟

你有没有想过,旅行的意义是什么?

程序员的职业规划

灵魂拷问:人生最重要的是什么?

如何高效学习一个新技术?

如何让自己更坦然地度过一天?

..........

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表