目录
1、OAuth2基本概念可以参考 OAuth2.0最简向导 (iocoder.cn)
一、概述
OAuth2是一个授权框架。用来授权第三方应用获取数据,是目前最流行的授权机制。
1、OAuth2基本概念可以参考 OAuth2.0最简向导 (iocoder.cn)
2、OAuth2 主要流程
- 用户在客户应用点击第三方应用,请求第三方应用授权访问。
- 第三方应用授权服务器同意客户应用授权,返回授权码。
- 客户应用使用授权码向授权服务器申请令牌(token)。
- 授权服务器对客户应用进行身份验证,验证通过后发放令牌(token)。
- 客户应用就可以带着这个令牌(token),去访问资源服务器的api。
- 资源服务器校验客户应用带着的令牌(token),校验通过,返回访问的api数据信息。
3、OAuth2 应用场景
网易云第三方登录(QQ、微信、微博等)。实际生活当中许多地方都会用到第三方登录,进行授权后,对其资源进行访问。
用户请求第三方应用进行授权
实际的流程如下:
4、OAuth2 授权模式(4种)
OAuth 协议的授权模式共分为 4 种,分别说明如下:
授权码模式:授权码模式(authorization code)是功能最完整、流程最严谨的授权模式。它的特点就是通过客户端的服务器与授权服务器进行交互,国内常见的第三方平台登录功能基本都是使用这种模式。(最正统的方式,也是目前绝大多数系统所采用的)(支持refresh token) (用在服务端应用之间)
简化模式:简化模式不需要客户端服务器参与,直接在浏览器中向授权服务器中请令牌,一般若网站是纯静态页面,则可以采用这种方式。(为web浏览器应用设计)(不支持refresh token) (用在移动app或者web app,这些app是在用户的设备上的,如在手机上调起微信来进行认证授权)
密码模式:密码模式是用户把用户名密码直接告诉客户端,客户端使用这些信息向授权服务器中请令牌。这需要用户对客户端高度信任,例如客户端应用和服务提供商是同一家公司。(为遗留系统设计) (支持refresh token)
客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向服务提供者申请授权。严格来说,客户端模式并不能算作 OAuth 协议要解决的问题的一种解决方案,但是,对于开发者而言,在一些前后端分离应用或者为移动端提供的认证授权服务器上使用这种模式还是非常方便的。(为后台api服务消费者设计) (不支持refresh token) (为后台api服务消费者设计)
二、使用
对于 Spring Security OAuth2 的配置,大体来说,就是两步:
- 配置授权服务器( AuthorizationServer )
- 配置资源服务器( ResourceServer )
1、导入maven依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<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>
</dependency>
</dependencies>
SpringBoot 的版本为 1.5.16.RELEASE ,所以使用的 Spring Security 的版本为 4.2.8.RELEASE ,Spring Security OAuth2 的版本为 2.2.0.15.RELEASE
2、配置资源服务器
资源服务器一般提供Api服务,例如订单、商品服务等。考虑到让整个示例更加简单,本文先将它和授权服务器放在一个 Maven 项目中。
1.编写controller类
/**
* 资源服务器访问示例
*/
@RestController
@RequestMapping("/api")
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello World!";
}
}
2.编写资源服务器配置文件
@Configuration
@EnableResourceServer
public class OAuth2ResourceServer extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 对 "/api/**" 开启认证
.anyRequest()
.authenticated()
.and()
.requestMatchers()
.antMatchers("/api/**");
}
}
@Configuration 注解:保证 OAuth2ResourceServer 能够被 SpringBoot 扫描到配置。 @EnableResourceServer 注解:开启资源服务器。
@EnableResourceServer继承( extends ) ResourceServerConfigurerAdapter 类,并覆写 #configure(HttpSecurity http) 方法,配置对 HTTP 请求中,匹配 /api/**" 路径,开启认证的验证。( @EnableResourceServer导入了ResourceServerConfiguration配置类,该配置类继承了WebSecurityConfigurerAdapter,拥有了http security的相关能力。 )
3、配置授权服务器
这儿就用到了上面所说的四种授权模式:
- 授权码模式
- 简化模式
- 密码模式
- 客户端模式
1.授权码模式
1.1 配置授权服务器
// 授权服务器配置
@Configuration
@EnableAuthorizationServer //开启授权服务器
public class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() // 基于内存,实际当中应该存放数据库
.withClient("mobai").secret("123") // Client 账号、密码。
.redirectUris("http://localhost:9001/callback") // 配置回调地址,选填。
.authorizedGrantTypes("authorization_code") // 授权码模式
.scopes("user_info", "role_info"); // 可授权的 Scope
}
1.2 配置springSecurity登录账号
在application.yml中配置springSecurity登录账号密码。实际当中登录账号是放在数据库当中的。
security:
user:
name: admin
password: admin
1.3 启动项目获取授权码
访问 http://localhost:8080/oauth/authorize?client_id=mobai&redirect_uri=http://localhost:9001/callback&response_type=code&scope=user_info 获取
-client_id :必传,为我们在 OAuth2AuthorizationServer 中配置的 Client 的编号。
redirect_url :可选,回调地址。当然,如果 client_id 对应的 Client 未配置 redirectUris 属性,会报错。实际项目当中,会在后端编写一个接收授权码的接口进行授权码接收,或者跳转到对应的页面(回调地址)进行授权码接收,前端进行地址拦截,再进行下一步处理(如登录)。
response_type :必传,返回结果为授权码。
scope :可选,申请授权的 Scope 。如果多个,使用逗号分隔。
state : 可选,表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这 个值。未在上述 URL 中体现出来。
访问该地址后,会跳至 springSecurity 登录页面,输入上面配置的账号密码 admin/amdin,跳至oauth授权确认页面,如下:
选择批准,进行确认,确认后会返回回调地址及其授权码。
- code就是返回的授权码。
1.4 获取令牌token
此处我用postman工具进行请求发送。
配置Authorization,OAuth2AuthorizationServer 中配置的 Client 的账号和密码(mobai/123)。
配置body。
- code:上面1.3小结访问接口获取到的授权码。
- grant_type:authorization_code 表示以授权模式获取token。
- redirect_uri:http://localhost:9001/callback 这个redirect_uri参数是客户端注册时指定的回调URI,经过URL编码处理。当授权过程完成时,用户会被重定向到这个地址,并带上授权码或其他响应信息。在此处需要确保该URI与之前请求授权时使用的完全一致。
- scope:user_info scope参数定义了客户端所请求的权限范围,本例中是user_info,意味着客户端希望获得读取用户信息的权限
请求发送成功返回token等信息。
- access_token:返回的token。
- token_type: 令牌类型。可以是 "bearer"
或 "mac"
类型。
- expires_in: 过期时间。
- scope: 权限范围。如果与 Client 申请的范围一致,此项可省略。
- refresh_token:刷新令牌,用来获取下一次的访问令牌。在授权模式下为空。
注意:授权码只能使用一次,再次访问授权码失效。平时项目当中还会将token存放在redis里面。
1.5 调用资源服务器api
访问/api/hello 接口
没有携带token时:
携带返回的token进行访问时:
1.6 项目整体结构
由于方便测试,下方resour_serve资源服务器相关代码放在的授权服务器当中。实际项目根据场景编写代码。
2.密码模式
新建password_serve模块,资源服务器的编写还是和上面的(2、配置资源服务器)一样。
1.1 配置授权服务器
与授权码模式配置不一样的是,此处没有配置回调地址,并将授权类型改为了密码模式,通过账号和密码直接获取令牌token,省略了授权码获取这一步骤。
// 授权服务器配置 密码模式
@Configuration
@EnableAuthorizationServer //开启授权服务器
public class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter {
// 用户认证
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() // 基于内存,实际当中应该存放数据库
.withClient("mobai").secret("123") // Client 账号、密码。
.authorizedGrantTypes("password") // 密码模式
.scopes("user_info", "role_info"); // 可授权的 Scope
}
}
另外注意的是,密码模式还需要开启springSecurity用户认证支持密码模式,不然会报以下错误。
开启用户认证
1.2 配置springSecurity登录账号
与授权码模式,1.2配置springSecurity登录账号相同。
1.3 启动项目
1.4 获取令牌token
填写客户端账号密码
填写请求体body
- grant_type: 授权类型为以密码模式获取token
- scope: 客户端所请求的权限范围
- username: 配置的springSecurity账号
- password: 配置的springSecurity密码
具体参数介绍在授权模式下的1.5调用资源服务器api小节下有讲。
1.5 调用资源服务器api
与 授权码模式下1.5调用资源服务器api一样。
填写获取到token进请求头,请求api返回数据。
1.6 项目整体结构
3.简化模式
新建simply_serve模块,资源服务器的编写还是和上面的(2、配置资源服务器)一样。
1.1 配置授权服务器
与授权码模式配置不一样的是,简化模式(implicit grant type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌token,跳过了"授权码"这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。
简而言之,授权码模式在浏览器访问返回的是授权码,后续再通过授权码获取令牌token。而简化模式在浏览器访问直接返回的是令牌token,可以直接使用。
授权服务器配置与授权码模式大至相同,不同的是授权类型是 implicit 简化模式。
// 授权服务器配置 简化模式
@Configuration
@EnableAuthorizationServer //开启授权服务器
public class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() // 基于内存,实际当中应该存放数据库
.withClient("mobai").secret("123") // Client 账号、密码。
.redirectUris("http://www.baidu.com") // 配置回调地址,选填。
.authorizedGrantTypes("implicit") // 简化模式
.scopes("user_info", "role_info"); // 可授权的 Scope
}
}
1.2 配置springSecurity登录账号
与前面两种模式相同。
1.3 启动项目
1.4 获取令牌token
- client_id=mobai : 授权服务器配置的客户端账号
- redirect_uri=http://www.baidu.com : 配置的回调地址
- response_type=token : 返回类型为token,而不是授权码code。返回code,回调后浏览器地址栏会报错,不知道返回的类型。
- scope=user_info : 权限范围。
访问过程:
1.输入密码
2. 授权成功,跳转至回调地址,回调地址后会携带令牌token、token类型和过期时间
- 和【授权码模式】基本不一致的有两点:
- 登录成功后,无需选择允许所有申请的 Scope ,直接授权完成。
- 返回的不是授权码,而是访问令牌token。
总的来说,【简化模式】是【授权码模式】的简化模式。
1.5 调用资源服务器api
和前面两种模式相同。
1.6 项目整体结构
4.客户端模式
新建client_serve模块,资源服务器的编写还是和上面的(2、配置资源服务器)一样。
1.1 配置授权服务器
与密码模式差不多,但区别是客户端模式不需要用户登录,不用配置用户认证,不要配置回调地址,授权类型为 client_credentials 客户端模式。
// 授权服务器配置 客户端模式
@Configuration
@EnableAuthorizationServer //开启授权服务器
public class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() // 基于内存,实际当中应该存放数据库
.withClient("mobai").secret("123") // Client 账号、密码。
.authorizedGrantTypes("client_credentials") // 客户端模式
.scopes("user_info", "role_info"); // 可授权的 Scope
}
}
1.2 配置springSecurity登录账号
它无需配置登录账号。因为它没有用户的概念,直接与授权服务器交互,通过 Client 的编号( client_id
)和密码( client_secret
)来保证安全性。
1.3 启动项目
1.4 获取令牌token
1.填写客户端账号密码
2.填写请求体body
3.结果
参数介绍可以参考上面几种模式
1.5 调用资源服务器api
和前面几种模式相同。
1.6 项目整体结构
三、四种模式总结:
1、 总结
总的来说:
【简化模式】是【授权码模式】的简化模式,【客户端模式】是【密码模式】的简化模式。
授权码模式: 在浏览器需要用户确认授权,再访问获取授权码的请求,再用获取到的授权码 在服务端获取令牌token,调用api。
简化模式: 对比授权码模式,省略了获取授权码的步骤,在浏览器获取到的直接是令牌 token,调用api。
密码模式:直接用用户的登录密码,获取令牌token,再调用api。
客户端模式:对比密码模式,省略用户登录密码,只需要验证客户端账号密码即可。通过客 户端账号密码,获取token,再调用api。
2、 为什么有 Client 账号和密码?
从上面四种授权模式,可以知道,无论哪种授权模式,都需要传client的账号和密码。这是因为通过 Client 编号和密码,授权服务器可以知道调用的来源以及正确性。这样,即使“坏人”拿到 Access Token ,但是没有 Client 账号和密码,也不能和授权服务器发生有效的交互。
四、刷新token
由于token有过期时间,因此我们需要配置refresh_token,对token进行刷新。
1、获取token及刷新token
下面以密码模式进行验证:
刷新token只需要在授权服务器配置中authorizedGrantTypes后面加上refresh_token,再在参数链后面添加refresh_token的过期时间(refreshTokenValiditySeconds(1200))即可。
// 授权服务器配置 密码模式
@Configuration
@EnableAuthorizationServer //开启授权服务器
public class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter {
// 用户认证
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() // 基于内存,实际当中应该存放数据库
.withClient("mobai").secret("123") // Client 账号、密码。
.authorizedGrantTypes("password","refresh_token") // 密码模式 refresh_token 刷新token,获取token后会返回refresh_token回来用于token失效进行token刷新
.scopes("user_info", "role_info") // 可授权的 Scope
.refreshTokenValiditySeconds(1200); //刷新token 1200秒后失效
}
}
除了上诉配置外,其它操作跟密码模式步骤一模一样。
启动项目进行token获取:
可以看见返回参数多了一个过期时间1200秒的刷新token,refresh_token。
注意:经测试,客户端模式和简化模式不存在刷新token。
OAuth 2.0 中的客户端模式(Client Credentials Grant)允许客户端以自己的名义,而不是用户的名义,向授权服务器获取访问令牌。在客户端模式下,由于没有用户的概念,因此通常不涉及到刷新令牌(Refresh Token)的发放。
OAuth 2.0 简化授权(Implicit Grant)模式主要用于无服务器端的应用,如浏览器中的JavaScript应用。在简化模式中,访问令牌(Access Token)直接通过URL片段(hash fragment)返回给客户端,由于安全原因以及协议设计,Refresh Token通常不会在Implicit Grant流程中发放。
2、通过刷新token获取新token
请求与获取token及刷新token请求一样,只是参数不一样。
grant_type参数值password换成了 refresh_token,并增加了刷新token参数。刷新token就是上面返回的刷新token。
可对比当前返回的token与小节1里面的token不一样了,代表我们通过刷新token成功的获取到了新的token。
3、项目整体结构
五、删除token
新建delete_token_server服务,还是以密码模式作为示例,跟新建密码模式服务步骤一模一样。只不过在密码的基础添加东西。
1、新建删除令牌token接口
@RestController
@RequestMapping("/oauth")
public class DeleteTokenController {
@Autowired
private ConsumerTokenServices tokenServices;
@PostMapping("/deleteToken")
public String deleteToken(@RequestParam("token") String token) {
tokenServices.revokeToken(token);
return token + "token已删除";
}
}
2、资源服务器配置修改
添加删除token接口放行。
3、启动项目
跟密码模式一样。
4、获取token
跟密码模式一样。
5、调用删除token接口
1.1 删除token接口填写获取到的token值
1.2 删除token接口请求头中填写要删除的token值
1.3 结果
删除成功
再次访问的话,会报token无效,说明token删除成功。
6、项目整体结构
注意,实际生产环境下,授权服务器和资源服务器可能是不在一起的,所以此处删除控制器仅仅是示例。
六、删除刷新token
新建delete_refresh_token服务,还是以密码模式作为示例,跟新建密码模式服务步骤一模一样。只不过在密码的基础添加东西。
1、注入TokenStore
创建配置类,注入需要用到删除刷新token的bean
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
@Configuration
public class OAuth2Config {
@Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore(); // 或者使用JdbcTokenStore、RedisTokenStore等其他实现
}
}
2、新建删除刷新token接口
@RestController
@RequestMapping("/oauth")
public class DeleteTokenController {
@Autowired
private TokenStore tokenStore;
@PostMapping("/deleteRefreshToken")
public String deleteRefreshToken(@RequestParam("refreshToken") String refreshToken) {
tokenStore.removeRefreshToken(new DefaultOAuth2RefreshToken(refreshToken));
return refreshToken + " refreshToken已删除";
}
}
2、资源服务器配置修改
与删除token中资源服务器配置修改一样。
3、启动项目
与密码模式一样。
4、获取token及刷新token
跟获取刷新token小节(获取token及刷新token)请求一样
5、调用删除刷新token接口
1.1 填入获取到的token
1.2 填入要删除的刷新token
1.3 结果