【10】扩展:Spring Security OAuth2 实战
·
文章目录
1、OAuth2.0介绍
- OAuth(Open Authorization)是一个关于授权的开放网络标准,允许用户给第三方应用授权访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或者分享他们数据的所有内容。
OAuth协议:https://datatracker.ietf.org/doc/html/rfc6749 - 协议特点:
- 简单:不管是OAuth服务提供者还是应用开发者,都很易于理解和使用;
- 安全:没有涉及到用户密钥等信息,更安全更灵活;
- 开放:任何服务提供商都可以实现OAuth,任何应用开发商都可以使用OAuth;
1.1 应用场景
- 原生APP授权:app登录请求后台接口,为了安全认证,所有的请求都携带Token信息;
- 前后端分页单页面应用:前端请求后台数据,需要进行OAuth2安全认证;
- 第三方应用授权登录:比如QQ、微信的授权登录;
- 单点登录、资源访问控制、权限管理;
1.1.1 云冲印
- 云冲印是一个冲印照片的网站,可以将用户存储在Google中的照片打印出来。用户想要使用该服务,就必须要让云冲印可以访问到Google上的照片。而且,只有得到用户的授权,Google才会允许云冲印读取这些照片。此时面临的问题是:云冲印如何才能获得用户的授权?
- 传统的做法是将Google的账号密码告诉云冲印,但这样做有以下几点弊端:
- 云冲印为了后续提供服务,会保存用户的账号密码,这样很不安全;
- Google不得不给第三方应用提供账号密码登录的服务,而单纯的密码登录也并不安全;
- 云冲印拥有了获取存储在Google上所有资料的权力,用户无法限制云冲印获得的授权范围和有效期;
- 用户只要修改密码,才能收回赋予给云冲印的权力,但,这样做,会使得其他获得用户授权的第三方应用全部失效;
- 一旦当第三方应用被破解后,就会导致账号密码泄露;

1.1.2 京东微信登录
- 用户需要访问京东,通过微信登录来进行第三方应用授权:

- 步骤1:

- 步骤2:

1.2 基本概念
- 第三方应用(Third-party application):又称为“客户端”,例如:云冲印/京东商城;
- HTTP服务提供商:简称“服务提供商“”,例如 Google;
- 资源所有者:又“称用户“”;
- 用户代理:比如浏览器;
- 授权服务器:服务提供商专门用来处理认证授权的服务器;
- 资源服务器:服务提供商存放用户资源的服务器;
- OAuth的作用就是让客户端安全可控地获取用户的授权,与服务提供商进行交互;
2、OAuth的设计思路
- OAuth是在客户端与服务提供商之间设置的一个授权层,客户端不能直接登录服务提供商,只能登录授权层,以此将用户与客户端分开。客户端登录授权层所用的令牌(Token),与用户的密码不同,用户可以在登录的时候,指定授权令牌的权限范围和有效期,客户端在登录授权层之后,服务提供商会根据令牌的权限范围和有效期,来向客户端开发资源;
- 运行流程如下图所示:
- A:用户打开客户端后,要求用户给予授权;
- B:用户同意给客户端授权;
- C:客户端使用上一步获得的授权,向授权服务器申请令牌;
- D:授权服务器对客户端进行认证之后,同意发放令牌;
- E:客户端使用令牌,向资源服务器申请获取资源;
- F:资源服务器确认令牌无误,同意向客户端开放资源;
2.1 客户端的授权模式
- 客户端必须要得到用户的授权,才能获得令牌。OAuth2.0对于如何颁发令牌,规定的非常详细。具体来说,一共分为四种授权类型:
授权码模式(Authorization Code)密码模式(Resource Owner Password Credentials)简化(隐式)模式(implicit)客户端模式(Client Credentials)
- 不管是哪一种授权模式,第三方应用在申请令牌之前,都必须先到系统备案,说明自己的身份,然后会拿到两个身份识别码:客户端ID(Client ID)、客户端密钥(Client Secret),这是为了防止令牌被滥用,没有备案的第三方应用,是无法拿到令牌的;
2.1.1 授权码模式
- 授权码方式指的是第三方应用先要向授权服务器申请一个授权码,然后再用该码获取令牌。
这种方式是最常用的,安全性也最高。它适用于那些有后端的Web应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都是在后端完成的。这样的前后端分离,可以避免令牌泄漏;
- A:用户访问客户端,B会将A导向授权服务器;
- B:用户选择是否给客户端授权;
- C:假设用户给予授权,授权服务器将用户导向客户端事先指定的重定向URL,同时再附上一个授权码;
- D:客户端收到授权码,附上指定的重定向URL,向授权服务器申请令牌。这一步是在客户端的后台服务器上完成的,用户不可见;
- E:授权服务器核对了授权码和重定向URL,确认无误后,向客户端发送访问令牌(
access token)和更新令牌(refresh token);
- 步骤如下:
- 1、A网站提供一个链接,用户点击后会跳转B网站,授权用户数据给A网站使用。下面是A网站跳转B网站的一个示意链接:
客户端申请授权的URL包含以下参数:https://b.com/oauth/authorize? response_type=code& #要求返回授权码(code) client_id=CLIENT_ID& #让B网站知道是谁在请求 redirect_url=CALLBACK_URL& #B接受或者拒绝请求后跳转的地址 scope=read #要求的授权范围(这里是只读)
(1)response_type:表示授权类型(必选项);
(2)client_id:表示客户端的ID(必选项);
(3)redirect_url:表示重定向的URL(可选项);
(4)scope:表示申请的权限范围(可选项);
(5)state:表示客户端当前的状态,可以指定任意值,授权服务器会原值返回; - 2、用户跳转后,B网站会要求用户登录,然后询问是否同意给A网站授权。如果用户表示同意,这时,B网站就会跳回redirect_url所对应的地址。跳转时,会回传一个授权码给A网站,如下所示:
https://a.com/callback?code=AUTHORIZATION_CODE #code参数就是授权码 - 3、A网站拿到授权码之后,就可以在后端向B网站发起请求,获取令牌
https:/b.com/oauth/token? client_id=CLIENT_ID& client_secret=CLIENT_SECRET& # client_id和client_secret是用来让B确认A的身份,client_secret参数是保密的,因此只能在后端发送请求 grant_type=authorization_code& # 采用的授权方式为授权码 code=AUTHORIZATION_CODE& # 之前拿到的授权码 redirect_url=CALLBACK_URL # 令牌颁发后需要回调的地址 - 4、B网站收到请求后,就会颁发令牌(会保存在服务端)。具体做法是跳转至redirect_url指定的网址,并发送一段JSON数据:
{ "access_token":"ACCESS_TOKEN", "token_type":"bearer", "expires_in": 2564765232, "refresh_token":"REFRESH_TOKEN", "scope":"read", "uid":1000243121, "info":{...} }
- 1、A网站提供一个链接,用户点击后会跳转B网站,授权用户数据给A网站使用。下面是A网站跳转B网站的一个示意链接:
2.1.2 简化(隐式)模式
- 有些Web应用是纯前端应用,没有后端服务。此时,授权码模式就不适用了,必须要将令牌存储在前端。RFC-6749就规定了第二种方式,允许直接向前端颁发令牌,这种没有授权码这个中间步骤的模式,称之为简化模式;
简化模式不通过第三方应用的服务器,可以直接在浏览器中向授权服务器申请令牌,跳过了授权码的步骤,所有步骤都在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。
这种方式直接把令牌传给前端,非常不安全。因此,只能在一些安全要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期有效,浏览器关掉,令牌就失效了。

- 步骤如下:
- 1、A网站提供一个链接,要求用户跳转到B网站,授权用户数据给A网站用。
https://b.com/oauth/authorize? response_type=token& # response_type参数的值为token,表示要求直接返回令牌 client_id=CLIENT_ID& redirect_uri=CALLBACK_URL& scope=read - 2、用户跳转B网站后,登录后同意给予A网站授权,这时,B网站就会跳回redirect_uri指定的地址,并把令牌作为参数传给A网站。
https://a.com/callback#token=ACCESS_TOKEN #token参数就是令牌,A网站可以直接在前端拿到令牌
- 1、A网站提供一个链接,要求用户跳转到B网站,授权用户数据给A网站用。
2.1.3 密码模式
- 如果你高度信任某个应用,RFC 6749也允许用户直接把账号和密码直接告诉该应用,该应用通过你的密码来申请获取令牌,这种方式称之为:“密码模式”。
在这种模式中,用户必须把自己的密码给客户端,但是,客户端不得存储密码。这通常在用户对客户端高度信任的情况下使用,例如,客户端是某个系统的一部分或者是同一公司的产品。而授权服务器只有在其他授权模式无法执行的情况下,才会考虑使用这种模式。
适用场景:自家公司搭建的授权服务器
- A:用户向客户端提供用户名和密码
- B:客户端将用户名和密码发送给授权服务器,向后者申请令牌
- C:授权服务器确认无误后,向客户端提供访问令牌
- 步骤如下:
- A网站要求用户提供B网站的用户名和密码,拿到以后,A可直接向B请求令牌,整个过程中,客户端不允许保存用户的密码;
https://oauth.b.com/token? grant_type=password& # 授权方式为 “密码模式” username=USERNAME& password=PASSWORD& client_id=CLIENT_ID& client_secret=CLIENT_SECRET - B网站验证身份通过之后,直接给出令牌,注意,此时不需要跳转,而是把令牌放到JSON数据中,通过请求的响应返回,A就拿到了令牌; 例如:http://localhost:8080/oauth/token?username=admin&password=123456&grant_type=password&scope=all&client_id=client111&client_secret=abc123
{ "access_token":"bsdhjsdj232ksd-gh2-sdsnj-343nsd0832", "token_type":"bearer", "refresh_token":"agsajsa-asgahsa-126hgsd-12yhsdk911", "expires_in":1956, "scope":"all" }
- A网站要求用户提供B网站的用户名和密码,拿到以后,A可直接向B请求令牌,整个过程中,客户端不允许保存用户的密码;
2.1.4 客户端模式
- 客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的米易,向服务提供商进行授权。适用于没有前端界面的命令行应用,即在命令行中请求令牌。一般用来提供给我们完全信任的服务器端服务。

- 步骤如下:
- 1、A应用在命令行向B发出请求
https://oauth.b.com/token? grant_type=client_credentials& client_id=CLIENT_ID& client_secret=CLIENT_SECRET - 2、B网站验证通过后,向A返回令牌。例如:http://localhost:8080/oauth/token?grant_type=client_credentials&scope=all&client_id=client123&client_secret=abc123
{ "access_token":"bsdhjsdj232ksd-gh2-sdsnj-343nsd0832", "token_type":"bearer", "expires_in":1956, "scope":"all" }
- 1、A应用在命令行向B发出请求
2.2 令牌的使用
- A网站拿到令牌之后,就可以向B网站的API请求数据了。此时,每个发到API的请求,都必须携带令牌。具体做法就是:在请求的请求头中,加上一个
Authorization字段,将令牌放到这个字段中。 - 注意:
access_token是用于拿来访问某个资源信息的,通常,指的是其他系统的资源。例如,上面的京东利用微信登录,认证授权通过后,就可以使用access_token来获取微信用户的资源信息。curl -H "Authorization: Bearer ACCESS_TOKEN"\ https://api.b.com
2.3 令牌的更新
-
当令牌的有效期到了,如果让用户再重新按之前的流程来申请新的令牌,体验感十分不好,而且也没有必要。OAuth 2.0就允许用户自动更新令牌。具体做法是:B网站颁发令牌时,一次性颁发了两个令牌,一个(access_token)用于获取数据,另一个(refresh_token)用于重新获取新的令牌。令牌到期前,用户可以使用refresh_token来发送一个请求,去更新令牌。
https://b.com/oauth/token? grant_type=refresh_token& # grant_type参数为refresh_token,表示要求更新令牌 client_id=CLIENT_ID& client_secret=CLIENT_SECRET& refresh_token=REFRESH_TOKEN # 用户更新令牌的令牌
3、OAuth2.0的快速开始
3.1 授权服务器

3.2 整体架构

3.3 授权码模式
-
第一步:创建项目,引入相关依赖
<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> <version>2.3.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> -
第二步:配置WebSecurityConfig配置类
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration //@EnableWebSecurity 默认开启 public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("admin") .password(passwordEncoder().encode("12345")) .authorities("admin") .roles("ADMIN") .and() .withUser("user") .password(passwordEncoder().encode("12345")) .authorities("user") .roles("USER"); } @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin().permitAll() .and().authorizeRequests() .antMatchers("/oauth/**","/login").permitAll() .anyRequest().authenticated() .and() .logout().permitAll() .and().csrf().disable(); } //如果是密码模式,则需要配置一个认证管理器 @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } } -
第三步:配置认证授权服务器
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; 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; /** * 认证(授权)服务器 */ @Configuration @EnableAuthorizationServer //开启认证服务器的服务 public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private PasswordEncoder passwordEncoder; @Autowired private AuthenticationManager authenticationManager; @Autowired(required = false) private TokenStore tokenStore; //获取授权码 http://localhost:8080/oauth/authorize?response_type=code&client_id=client_123456 // http://localhost:8080/oauth/authorize?response_type=code&client_id=client_123456&redirect_uri=http://www.baidu.com&scope=all //简化模式 http://localhost:8080/oauth/authorize?response_type=token&client_id=client_123456&redirect_uri=http://www.baidu.com&scope=all //密码模式:http://localhost:8080/oauth/token?username=admin&password=12345&grant_type=password&client_id=client_123456&client_secret=123456&scope=all @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.realm("oauth2-resources") .tokenKeyAccess("permitAll()") .checkTokenAccess("isAuthenticated()") .allowFormAuthenticationForClients(); } //客户端的配置 @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("client_123456") //客户端ID .secret(passwordEncoder.encode("123456")) //客户端密钥 .redirectUris("http://www.baidu.com") //认证成功后跳转的地址 .scopes("all") //授权范围 .autoApprove(true) .resourceIds("oauth2-resource") .accessTokenValiditySeconds(3600) //Token的有效期 .refreshTokenValiditySeconds(86400) //RefreshToken的有效期 /** * authorization_code:授权码模式 * implicit:简化模式 * password:密码模式 * client_credentials:客户端模式 * refresh_token:更新令牌 */ .authorizedGrantTypes("authorization_code", "refresh_token", "implicit", "password", "client_credentials"); //授权许可的类型 } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authenticationManager(authenticationManager) //使用密码模式需要配置 .tokenStore(tokenStore) //指定token存储在哪儿 .reuseRefreshTokens(false) //refresh_token是否重复使用 .userDetailsService(null) //刷新令牌授权包含对用户信息的检查 .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);//支持GET、POST请求 } } -
第四步:配置资源服务器
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; /** * 资源服务器:对资源的授权(访问控制) */ @Configuration @EnableResourceServer //开启资源的授权控制 public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { //任何请求都要认证 http.authorizeRequests() //资源服务器设置了/testApi/**下的接口访问需要用户登录授权,还需要ADMIN角色,而/user/**仅仅需要用户登录授权即可 .antMatchers("/user/**").authenticated() .antMatchers("/testApi/**").hasRole("ADMIN") .anyRequest().authenticated() .and() .requestMatchers().antMatchers("/user/**"); //指定资源可以匹配的路径 } } -
测试:
-
1、认证授权,并获取验证码:
请求访问固定格式的地址:http://localhost:8080/oauth/authorize?response_type=code&client_id=client_123456&redirect_uri=http://www.baidu.com&scope=all
首先,会自动跳转至登录界面进行认证:

接着,同意授权:

最后,跳转至指定地址,并返回授权码:

-
2、使用授权码,获取令牌Token:
这里使用ApiFox来模拟客户端,获取令牌,请求访问:http://localhost:8080/oauth/token

输入上面获取到的授权码,获取令牌:

-
3、使用获取到的令牌,访问系统资源:
如果,不携带令牌访问:

正常携带令牌访问:

-
3.4 简化模式
-
第一步:在认证授权服务器的配置中加上简化模式
implicit的授权方式:

-
第二步:测试
- 访问固定格式的地址 http://localhost:8080/oauth/authorize?response_type=token&client_id=client_123456&redirect_uri=http://www.baidu.com&scope=all ,直接请求获取令牌。同样,需要先登录认证,并同意授权;
- 认证授权之后,它会跳转至指定地址,并返回令牌信息:

- 使用上述获取的令牌,访问资源接口:

3.5 密码模式
- 第一步:需要先配置一个认证管理器,可以直接使用适配器已经提供的一个管理器:
- 第二步:在认证服务器中使用该管理器:
- 第三步 测试:获取令牌,可以通过浏览器也可以使用Apifox来进行测试:
- 在浏览器通过GET请求访问:http://localhost:8080/oauth/token?username=admin&password=12345&grant_type=password&client_id=client_123456&client_secret=123456&scope=all,请求参数中已经携带了用户密码等信息,可以直接获取令牌信息:

- 也可在ApiFox通过POST请求来获取令牌信息:


- 在浏览器通过GET请求访问:http://localhost:8080/oauth/token?username=admin&password=12345&grant_type=password&client_id=client_123456&client_secret=123456&scope=all,请求参数中已经携带了用户密码等信息,可以直接获取令牌信息:
- 使用上述获取的令牌,访问资源接口:

3.6 客户端模式
- 客户端模式与上面的配置过程类似,首先,获取令牌,只需要传递授权模式、客户端ID、密钥等信息,就资源服务器就可以识别客户端的身份,进而返回令牌信息。
- 例如:http://localhost:8080/oauth/token?grant_type=client_credentials&client_id=client_123456&client_secret=123456&scope=all

3.7 更新令牌
- 使用Auth2时,如果令牌失效,可以使用刷新令牌的机制,通过
refresh_token的授权模式,再次获取新的access_token的信息。只需要修改认证服务器的配置,添加refresh_token的授权模式即可。 也可以在认证服务器的配置中,设置refresh_token是否可以重复使用。

- token的生成方式是可以替换的,有一个接口
TokenGranter可以实现token的生成策略:

- 这里,我以密码模式来测试更新令牌:
- 首先拿到
refresh_token的信息

- 请求获取新的token令牌信息:http://localhost:8080/oauth/token?grant_type=refresh_token&client_id=client_123456&client_secret=123456&refresh_token=40139b1f-fd22-4ef9-b93d-fbff96d104c1

- 首先拿到
3.8 基于Redis存储Token
- 引入redis和连接池的依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> - 创建基于Redis存储的TokenStore对象
@Configuration public class RedisConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean public TokenStore tokenStore() { return new RedisTokenStore(redisConnectionFactory); } } - 设置Token的存储基于Redis来实现

更多推荐


所有评论(0)