SpringBoot 整合oauth2实现授权第三方应用(实战版)

共 11543字,需浏览 24分钟

 ·

2021-12-28 19:14

程序员的成长之路
互联网/程序员/技术/资料共享 
关注


阅读本文大概需要 11 分钟。

来自:blog.csdn.net/u014365523/article/details/112317015

什么是OAuth2

OAuth(open authorization开放授权)是一个开放标准,允许用户授权第三方应用访问他们服务器资源,而不需要将用户名和密码提供给第三方应用。OAuth2.0是OAuth协议的延续版本,但是不向后兼容OAuth1.0,即完全废止了OAuth1.0。

1.快递员问题

我住在一个大型的居民小区
小区有门禁系统
小区进入小区的时候需要输入密码
我经常网购和点外卖,每天都有快递员来送货,我必须找到一个办法,让快递员通过门禁系统,进入小区
如果我把自己的密码告诉快递员,他就拥有了和我同样的权限,这样好像不太合适。万一我想取消他进入小区的权力,也很麻烦,我需要修改米啊么,还的通知其他的快递员。
有没有一种办法,让快递员能够自由进入小区,又不必知道小区居民的密码,而且他的唯一权限就是送货其他需要密码的场合,他都没有权限

2.授权机制的设计

于是,我设计了一套授权机制。
  • 第一步:门禁系统的密码输入器下方,增加一个按钮,叫做 “获取授权”。快递员需要进入小区的时候,首先按这个按钮,去申请授权。
  • 第二步:他按下按钮以后,业主(也就是我)的手机就会跳出对话框:有人正在要求授权。系统会显示该快要的姓名,工号和所属的快递公司。我确认信息属实,就点击按钮,告诉门禁系统,我同意给予他进入这个小区的权限。
  • 第三步:门禁系统得到我的确认以后,向快递员显示一个进入该下区的令牌(access token)。令牌就是类似密码的一串数字,只在短期能有效(可设置,比如7天)
  • 第四步:快递员向门禁系统输入令牌,进入小区。
有人可能会问,为什么不使用远程为快递员开门,而要生成一个令牌呢?这是因为快递员可能每天都会来送货,令牌可以服用。另外,有的下区有多重门禁,快递员可以使用同一个令牌通过它们。

3.互联网应用场景

它们我们把上面的例子搬到互联网,就是OAuth的设计了。
首先,居民小区就是存储用户数据的应用服务,比如微信微信存储了我的好友消息,获取这些信息,就必须经过微信的 “门禁系统”,其次,快要(或者说快递公司)就是第三方应用,想要穿过门禁系统进入小区,最后,我就是用户本人,同意授权第三方应用进入小区,获取我的数据
简单说,OAuth 就是一种授权机制。数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。
互联令牌与密码的区别
数据令牌(token)与密码(password)的作用是一样的,都可以进入系统,但是有三点差异:
  • 令牌是短期的,到期后自动失效,用户自己无法修改。密码一般长期有效,用户步修改,就不会发生变化
  • 令牌可以被数据所有者撤销,会立即失效。如上例中,业主可以随时取消快递员的令牌。
  • 令牌有权限反问(scope),比如只能进去小区的二号门,对于网络服务来说,只读令牌就比读写令牌更安全,密码一般是完整的权限。
上面这些设计,保证了令牌既可以让第三方应用获取权限,获取数据,同时又随时可控,不会危机系统安全,这就是OAuth2.0的优点。
注意:只要得到令牌,就能进入系统。系统一般不会再次确认身份,所以令牌必须保密,泄露令牌和泄漏密码的后果是一样的。这也是为什么令牌的有效期一般都设置的很短的原因。

4.OAuth2中的几个重要角色

  • 资源所有者(Resource Owner):即代表授权客户端访问本身资源信息的用户,客户端访问用户帐户的权限仅限于用户授权的“范围”。
  • 客户端(Client):即代表意图访问受限资源的第三方应用。在访问实现之前,它必须先经过用户者授权,并且获得的授权凭证将进一步由授权服务器进行验证。
  • 授权服务器(Authorization Server):授权服务器用来验证用户提供的信息是否正确,并返回一个令牌给第三方应用。
  • 资源服务器(Resource Server):资源服务器是提供给用户资源的服务器,例如头像、照片、视频等。

5.OAuth2 授权流程

  • 步骤1:客户端(第三方应用)向用户请求授权。
  • 步骤2:用户单击客户端所呈现的服务授权页面上的同意授权按钮后,服务端返回一个授权许可凭证给客户端。
  • 步骤3:客户端拿着授权许可凭证去授权服务器申请令牌。
  • 步骤4:授权服务器验证信息无误后,发放令牌给客户端。
  • 步骤5:客户端拿着令牌去资源服务器访问资源。
  • 步骤6:资源服务器验证令牌无误后开放资源。
注意:只要得到令牌,就能进入系统。系统一般不会再次确认身份,所以令牌必须保密,泄露令牌和泄漏密码的后果是一样的。这也是为什么令牌的有效期一般都设置的很短的原因。

6.OAuth2 授权模式

OAuth 协议的授权模式共分为 4 种,分别说明如下:
  • 授权码模式:授权码模式(authorization code)是功能最完整、流程最严谨的授权模式。它的特点就是通过客户端的服务器与授权服务器进行交互,国内常见的第三方平台登录功能基本 都是使用这种模式。(最正统的方式,也是目前绝大多数系统所采用的)(支持refresh token) (用在服务端应用之间)
  • 简化模式:简化模式不需要客户端服务器参与,直接在浏览器中向授权服务器中请令牌,一般若网站是纯静态页面,则可以采用这种方式。(为web浏览器应用设计)(不支持refresh token) (用在移动app或者web app,这些app是在用户的设备上的,如在手机上调起微信来进行认证授权)
  • 密码模式:密码模式是用户把用户名密码直接告诉客户端,客户端使用这些信息向授权服务器中请令牌。这需要用户对客户端高度信任,例如客户端应用和服务提供商是同一家公司。(为遗留系统设计) (支持refresh token)
  • 客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向服务提供者申请授权。严格来说,客户端模式并不能算作 OAuth 协议要解决的问题的一种解决方案,但是,对于开发者而言,在一些前后端分离应用或者为移动端提供的认证授权服务器上使用这种模式还是非常方便的。(为后台api服务消费者设计) (不支持refresh token) (为后台api服务消费者设计)
实现步骤:
1、添加依赖
由于 Spring Boot 中的 OAuth 协议是在 Spring Security 基础上完成的。因此首先编辑 pom.xml,添加 Spring Security 以及 OAuth 依赖。
<dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
        <groupId>org.springframework.security.oauthgroupId>
        <artifactId>spring-security-oauth2artifactId>
        <version>2.3.3.RELEASEversion>
dependency>
2、配置授权服务器
授权服务器和资源服务器可以是同一台服务器,也可以是不同服务器,本案例中假设是同一台服务器,通过不同的配置开启授权服务器和资源服务器。
下面是授权服务器配置代码。创建一个自定义类继承自 AuthorizationServerConfigurerAdapter,完成对授权服务器的配置,然后通过 @EnableAuthorizationServer 注解开启授权服务器:
注意:authorizedGrantTypes(“password”, “refresh_token”)表示 OAuth 2 中的授权模式为“password”和“refresh_token”两种。在标准的 OAuth 2 协议中,授权模式并不包括“refresh_token”,但是在 Spring Security 的实现中将其归为一种,因此如果需要实现 access_token 的刷新,就需要这样一种授权模式。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;


/**
@Description: 配置授权服务
@Author: Top
@Version: V1.0
*/

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

// 该对象用来支持 password 模式
@Autowired
AuthenticationManager authenticationManager;

// 该对象用来将令牌信息存储到内存中
@Autowired(required = false)
TokenStore inMemoryTokenStore;

// 该对象将为刷新token提供支持
@Autowired
UserDetailsService userDetailsService;

//指定密码的加密方式
@Bean
PasswordEncoder passwordEncoder() {
// 使用BCrypt强哈希函数加密方案(密钥迭代次数默认为10)
return new BCryptPasswordEncoder();
}

// 配置 password 授权模式
@Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception 
{
// 这里client使用存在模式,可以实际过程调整为jdbc的方式
// 这里说明一下,redirectUris的连接可以是多个,这里通过access_token都可以访问的
// 简单点,就是授权的过程
clients.inMemory()
.withClient("password")
.authorizedGrantTypes("password""refresh_token"//授权模式为password和refresh_token两种
.accessTokenValiditySeconds(1800// 配置access_token的过期时间
.resourceIds("rid"//配置资源id
.scopes("all")
.secret("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq"); //123加密后的密码
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.tokenStore(inMemoryTokenStore) //配置令牌的存储(这里存放在内存中)
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}

@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
// 表示支持 client_id 和 client_secret 做登录认证
security.allowFormAuthenticationForClients();
}
}
3、配置资源服务器
接下来配置资源服务器。自定义类继承自 ResourceServerConfigurerAdapter,并添加 @EnableResourceServer 注解开启资源服务器配置。
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;


/**
@Description: 配置资源服务
@Author: Top
@Version: V1.0
*/

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

@Override
public void configure(ResourceServerSecurityConfigurer resources) {

resources.resourceId("rid"// 配置资源id,这里的资源id和授权服务器中的资源id一致
.stateless(true); // 设置这些资源仅基于令牌认证
}

// 配置 URL 访问权限
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.antMatchers("/secure/**").authenticated()
.anyRequest().permitAll();
}
}
如何放行某个子路径,这样就可以实现了,先对子路径进行放行,然后操作父路径进行拦截,然后再对其他所有的路径放行,这样就可以实现,拦截/web/开头的路径,但是放行/web/user/和其他所有不是web开头的路径。
注意:声明的顺序,必须先声明范围小的,再声明范围大的
4、配置 Security
这里 Spring Security 的配置与传统的 Security 大体相同,不同在于:这里多了两个 Bean,这两个 Bean 将注入授权服务器配置类中使用。
另外,这里的 HttpSecurity 配置主要是配置“oauth/**”模式的 URL,这一类的请求直接放行。
注意:在这个 Spring Security 配置和上面的资源服务器配置中,都涉及到了 HttpSecurity。其中 Spring Security 中的配置优先级高于资源服务器中的配置,即请求地址先经过 Spring Security 的 HttpSecurity,再经过资源服务器的 HttpSecurity。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;


/**
@Description: 配置 Security
@Author: Top
@Version: V1.0
*/

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}

@Bean
@Override
protected UserDetailsService userDetailsService() {

return super.userDetailsService();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin")
.password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq"//123
.roles("admin")
.and()
.withUser("sang")
.password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq"//123
.roles("user");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/oauth/**").authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.and().csrf().disable();
}
}

5、Controller方法测试
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/secure")
public class UserController {


@PostMapping("/user")
public String user() //需要提供访问的token才能访问,资源服务也需要验证token是否有效
System.out.println("张富");
return "非常自然1";
}

@GetMapping("/hello")
public String hello() //需要提供访问的token才能访问,资源服务也需要验证token是否有效
System.out.println("张贵");
return "非常自然2";
}
}
5、使用Postman开始接口测试
(1)启动项目,首先通过 POST 请求获取 token:
  • 请求地址:oauth/token
  • 请求参数:用户名、密码、授权模式、客户端 id、scope、以及客户端密码
  • 返回结果:access_token表示获取其它资源是要用的令牌,refresh_token 用来刷新令牌,expires_in 表示 access_token 过期时间。
(2)访问资源时,我们只需要携带上 access_token 参数即可:
(3)当 access_token 过期后,可以使用 refresh_token 重新获取新的 access_token(前提是 access_token 未过期),这里也是 POST 请求:
  • 请求地址:oauth/token(不变)
  • 请求参数:授权模式(变成了refresh_token)、refresh_token、客户端 id、以及客户端密码
  • 返回结果:与获取前面登录获取 token 返回的内容项一样。不过每次请求,access_token 和 access_token有效期都会变化。
(4)如果非法访问一个资源(没有token参数),比如 admin 用户访问“user/hello”接口,结果如下:
技术文档:
https://www.hangge.com/blog/cache/detail_2683.html#
特别注意:oauth2 支持四种授权模式:密码模式、授权码模式、简化模式和 客户端模式。
其中 客户端模式(clinet)是搭建微服务建构的关键。
1、密码模式
最不推荐的,因为第三方应用可能存储了用户密码;这种模式主要用来做遗留项目升级oauth2的适配方案;当然如果第三方应用是自家的应用,也是可以使用的:支持refresh token
请求地址类似如下:
http://localhost:8080/uaa/oauth/token?grant_type=password&username=lixx&password=dw123456
2、授权码模式(aurhorization code)
算是正宗的oauth2授权模式了,设计了auth_code,通过这个code再获取token,支持refresh token
请求流程如下:
  • 使用client_id和client_secret换取授权码,调用地址如:http://localhost:8097/oauth/authorize?response_type=code&client_id=password&redirect_uri=http://www.baidu.com
  • 上述地址是被拦截的,跳转到登录页面:http://localhost:8097/login,输入用户名、密码,登录之后返回第一地址,进行授权
  • 授权之后,跳转到回调地址,并携带授权码code,地址如:http://www.baidu.com?code=1LFD3E
  • 根据授权码去获取token,地址如:http://localhost:8080/oauth/token?client_id=client_code&grant_type=authorization_code&redirect_uri=http://ww.baidu.com&client_secret=123456&code=nBYrX5
  • 拿到token之后,访问需要授权的接口的时候,携带上就可以了
使用授权码模式,需要跳转到登录页面,由用户手动登录。这也是微服务不能使用授权模式的原因。因为程序不能通过登录页面进行登录。
3、简化模式 (implicit)
这种模式比授权模式少了code环节,回调url直接携带token,使用场景是基于浏览器的应用,这种模式基于安全行考虑,建议把token的时效设置短一些;不支持refresh token;与授权模式一样么,简化模式只能应用于基于浏览器的应用,并且需要用户手动登录,也就是说同样不适用于微服务架构。
4、客户端模式(clinet credentials)在微服务环境下,上述3种模式都被否定了,所以只能采用client credentials模式了。
这种模式直接根据client_id和client_secret即可获取token,无需用户蚕户
这种模式比较适合消费api的后端服务,也就是微服务架构,。这正是我们需要的。

推荐阅读:

离开互联网上岸1年后,我后悔了!重回大厂内卷

商品库存的扣除过程,如何防止超卖?

互联网初中高级大厂面试题(9个G)

内容包含Java基础、JavaWeb、MySQL性能优化、JVM、锁、百万并发、消息队列、高性能缓存、反射、Spring全家桶原理、微服务、Zookeeper、数据结构、限流熔断降级......等技术栈!

戳阅读原文领取!                                       朕已阅 

浏览 28
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报