OAuth2.0
小了来说作为前后端分离项目来讲经常需要传递数据。专业上来说也就是客户端想要访问资源服务器的数据,那么就能通过资源服务器内部提供的api来访问数据,那么问题就来了,这安全吗?当然不安全,因为客户端可以伪装和伪造请求资源。所以我们需要权限验证。
那么怎么做嘞,OAuth2.0协议就是用来做这个的。
OAuth2是一个关于授权的开放标准,核心思路是通过各类认证手段(具体什么手段OAuth2不关心)认证用户身份,并颁发token(令牌)
,使得第三方应用可以使用该令牌在限定时间
、限定范围
访问指定资源
。主要涉及的RFC规范有RFC6749(整体授权框架),RFC6750(令牌使用),RFC6819(威胁模型)这几个,一般我们需要了解的就是RFC6749。获取令牌的方式主要有四种,分别是授权码模式
,简单模式
,密码模式
和客户端模式
,如何获取token不在本篇文章的讨论范围。
引用:https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2
+--------+ +---------------+
| |--(A)------- Authorization Grant --------->| |
| | | |
| |<-(B)----------- Access Token -------------| |
| | & Refresh Token | |
| | | |
| | +----------+ | |
| |--(C)---- Access Token ---->| | | |
| | | | | |
| |<-(D)- Protected Resource --| Resource | | Authorization |
| Client | | Server | | Server |
| |--(E)---- Access Token ---->| | | |
| | | | | |
| |<-(F)- Invalid Token Error -| | | |
| | +----------+ | |
| | | |
| |--(G)----------- Refresh Token ----------->| |
| | | |
| |<-(H)----------- Access Token -------------| |
+--------+ & Optional Refresh Token +---------------+
Figure 2: Refreshing an Expired Access Token
(A) 客户端通过与授权服务器进行身份验证并提供授权授权来请求访问令牌。
(B) 授权服务器验证客户端并验证授权授权,如果有效,则发出访问令牌和刷新令牌。
© 客户端通过呈现访问令牌向资源服务器发出受保护的资源请求。
(D) 资源服务器验证访问令牌,如果有效,则为请求提供服务。
(E) 重复步骤©和(D),直到访问令牌到期。如果客户端知道访问令牌过期,它会跳到步骤(G),否则它会发出另一个受保护的资源请求。
(F) 由于访问令牌无效,资源服务器将返回一个无效令牌错误。
(G) 客户机通过与授权服务器进行身份验证并呈现刷新令牌来请求新的访问令牌。客户端身份验证要求基于客户端类型和授权服务器策略。
(H) 授权服务器验证客户端并验证刷新令牌,如果有效,则发出一个新的访问令牌(也可以是一个新的刷新令牌)。
通过阅读以上官方文档也可以看到,这个过程有客户端
,授权服务器
,资源服务器
。
我们经常听说第三方授权,比如跳转支付宝支付接口、跳转微信授权登录、JWT鉴权等好像就是这么回事啊,所以在上面例子所说道的安全问题可以用此方法这么做。
怎么做嘞,就是一般直接能拿到的资源我们要做一个权限校验。首先每次访问资源会检查cookie有没有refresh_token,第一次肯定啥都没有,重定向到帐号密码登录,就是上面提到的四种获取Token中的密码模式,登陆拿到access_token和refresh_token,refresh_token存到cookie里,access_token一般缓存到验证服务器,然后每次访问都要携带access_token,但是access_token是有时效的,所以到时间再拿去访问肯定就过期了,接着就要用cookie中refresh_token去刷新拿到新的access_token和refresh_token,再存到缓存、cookie。这样就能安全访问资源了。
Spring Security
既然有了理论知识作为基础,接着实践出真知。想要实现OAuth2.0过程就要有现成的框架吧,总不能自己写个啥权限认证系统吧,所以基于Spring Boot开发当然选择了Spring Security。
查了下官方文档,发现有很多实现鉴权方式,比如默认就是使用单独一个账号密码进行资源拦截,密码在控制台。
好,接着我们就用Spring Boot搭建一个简单的OAuth2.0的认证系统,Spring Security OAuth2.0既可以把授权服务器(AuthorizationServer)和资源服务器(ResourceServer)配置在一个应用中,也可以分开配置。我会把授权服务器和资源服务器都放在一个项目中。
授权服务器负责用户登录、授权、token验证等。
资源服务器负责提供受保护资源,只是需要到授权服务器进行token验证。
搭建 Spring Security OAuth2.0
我用的Spring Boot 2.5.5
pom.xml
引入spring-security-oauth2,web,热启动可devtools
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
DoController.java
Result自己封装的传回json而已,想传String改改就好。
主要是三个api测试,普通的无角色,user和admin角色。
@Controller
public class DoController {
@ResponseBody
@GetMapping("/admin/hello")
public Result admin(){
return Result.ok("hello admin");
}
@ResponseBody
@GetMapping("/user/hello")
public Result user(){
return Result.ok("hello user");
}
@ResponseBody
@GetMapping("/hello")
public Result hello(){
return Result.ok("hello");
}
}
接着就是三个很重要的Config配置类
AuthorizationServerConfig
该配置类用于配置理论上的授权服务器,包括了client名称,客户端secret,可授权类型有哪些,令牌过期时间,服务资源的唯一标识resourceIds,scopes范围。用于密码授予的 AuthenticationManager。允许客户端进行表单身份验证。
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client_id")
.authorizedGrantTypes("password", "refresh_token")
.accessTokenValiditySeconds(60 * 30) //30分钟
.refreshTokenValiditySeconds(60 * 60 * 2) //2小时
.resourceIds("rid")
.scopes("all")
.secret(new BCryptPasswordEncoder().encode("123456"));
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
}
ResourceServerConfig
配置授权服务器对应的资源服务器,对/user授予角色为user,admin等同。
第一级path设为匿名用户也就是任何人访问。
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("rid");
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.antMatchers("/**").anonymous()
.anyRequest().authenticated();
}
}
WebSecurityConfig
设置授权管理者超类、用户数据、密码编码器。为了简单在内存中构建身份验证。允许任何人使用/oauth/**路径,因为系统默认的获取token的api在这子路径下。
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
@Override
protected UserDetailsService userDetailsService() {
return super.userDetailsService();
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("admin")
.and()
.withUser("user").password(new BCryptPasswordEncoder().encode("123456")).roles("user");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/oauth/**")
.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.and()
.csrf()
.disable();
}
}
测试
至此就完成了OAuth2.0的功能。我们可以测试下:
先访问 http://localhost:8080/hello ,访问成功。
再访问 http://localhost:8080/user/hello ,http://localhost:8080/admin/hello 提示:完全身份验证需要在未授权的情况下访问此资源。
再用Postman工具,通过token的Api模拟Post请求:http://localhost:8080/oauth/token 以下使用user的权限,附带的body参数:
username,password,grant_type,client_id,client_secret,scope
可以,拿到了access_token和refresh_token。接着用access_token再去访问以上user和admin两不同权限的hello。
通过http的get请求,添加参数access_token。
再试试refresh_token刷新access_token。
需要附带的body参数:
grant_type,refresh_token,client_id,client_secret
通过设置访问令牌过期时间10s,刷新令牌过期时间20s。得出:
- 访问令牌过期则无效访问,需要调用刷新令牌api刷新一个新的访问令牌。
- 在访问令牌过期时间内,调用刷新令牌api则之前的访问令牌会失效。
- 调用刷新令牌api会得到新的访问令牌,和自身刷新令牌。
- 所以刷新令牌过期了就必须重新获得全部token。
再通过源码可以看到默认过期时间:
private int accessTokenValiditySeconds = 60 * 60 * 12; // default 12 hours.
private int refreshTokenValiditySeconds = 60 * 60 * 24 * 30; // default 30 days.
通过之前的FODI项目部署OneDrive列表实践得出微软的设置一般访问3小时,刷新3个月。
引用
https://gitee.com/kdyzm/spring-security-oauth-study https://blog.kdyzm.cn/post/24