OAuth2 简单介绍及密码模式
原地址:http://www.iocoder.cn/Spring-Security/OAuth2-learning/
1.1 OAuth2.0 是什么?
OAuth(Open Authorization)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。
OAuth 允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。
每一个令牌授权一个特定的网站(例如,视频编辑网站)在特定的时段(例如,接下来的 2 小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth 让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。
OAuth2.0 是用于授权的行业标准协议。OAuth2.0 为简化客户端开发提供了特定的授权流,包括 Web 应用、桌面应用、移动端应用等。
1.2 OAuth2.0 角色解释
在 OAuth2.0 中,有如下角色:
① Authorization Server:认证服务器,用于认证用户。如果客户端认证通过,则发放访问资源服务器的令牌。
② Resource Server:资源服务器,拥有受保护资源。如果请求包含正确的访问令牌,则可以访问资源。
(友情提示:提供管理后台、客户端 API 的服务,都可以认为是 Resource Server。)
③ Client:客户端。它请求资源服务器时,会带上访问令牌,从而成功访问资源。
(友情提示:Client 可以是浏览器、客户端,也可以是内部服务。)
④ Resource Owner:资源拥有者。最终用户,他有访问资源的账号与密码。
(友情提示:可以简单把 Resource Owner 理解成人,她在使用 Client 访问资源。)
1.3 OAuth 2.0 运行流程
如下是 OAuth 2.0 的授权码模式的运行流程:
(A)用户打开客户端以后,客户端要求用户给予授权。 (B)用户同意给予客户端授权。 (C)客户端使用上一步获得的授权,向认证服务器申请令牌。 (D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。 (E)客户端使用令牌,向资源服务器申请获取资源。 (F)资源服务器确认令牌无误,同意向客户端开放资源。
上述的六个步骤,B 是关键,即用户如何给客户端进行授权。有了授权之,客户端就可以获取令牌,进而凭令牌获取资源。
这个时候的资源,资源主要指的是三方开放平台的用户资料等等。
1.4 OAuth 2.0 授权模式
客户端必须得到用户的授权(Authorization Grant),才能获得访问令牌(Access Token)。
OAuth2.0 定义了四种授权方式:
- 授权码模式(Authorization Code)
- 密码模式(Resource Owner Password Credentials)
- 简化模式(Implicit)
- 客户端模式(Client Credentials)
其中,密码模式和授权码模式比较常用。
当然,对于黄框部分,对于笔者还是比较困惑的。笔者认为,第三方的单页应用 SPA ,也是适合采用 Authorization Code Grant 授权模式的。例如,《微信网页授权》 :
具体而言,网页授权流程分为四步:
- 1、引导用户进入授权页面同意授权,获取 code
- 2、通过 code 换取网页授权 access_token(与基础支持中的 access_toke n不同)
- 3、如果需要,开发者可以刷新网页授权 access_token,避免过期
- 4、通过网页授权 access_token 和 openid 获取用户基本信息(支持 UnionID 机制)
所以,我猜测,之所以图中画的是 Implicit Grant 的原因是,受 Google 的 《OAuth 2.0 for Client-side Web Applications》 一文中,推荐使用了 Implicit Grant 。
当然,具体使用 Implicit Grant 还是 Authorization Code Grant 授权模式,没有定论。笔者,偏向于使用 Authorization Code Grant,对于第三方客户端的场景。
2. 密码模式
示例代码对应仓库:
本小节,我们来学习密码模式(Resource Owner Password Credentials Grant)。
密码模式,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向授权服务器索要授权。
在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而授权服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。
- (A)用户向客户端提供用户名和密码。
- (B)客户端将用户名和密码发给授权服务器,向后者请求令牌。
- (C)授权服务器确认无误后,向客户端提供访问令牌。
下面,我们来新建两个项目,搭建一个密码模式的使用示例。如下图所示:(使用Spring_Book创建两个模块)
2.1 搭建授权服务器
创建 authorization-server-with-resource-owner-password-credentials
项目,搭建授权服务器。
2.1.1 引入依赖
创建 pom.xml
文件,引入 Spring Security OAuth 依赖。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>lab-68</artifactId> <groupId>cn.iocoder.springboot.labs</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>lab-68-demo02-authorization-server-with-resource-owner-password-credentials</artifactId> <properties> <!-- 依赖相关配置 --> <spring.boot.version>2.2.4.RELEASE</spring.boot.version> <!-- 插件相关配置 --> <maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.source>1.8</maven.compiler.source> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>${spring.boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- 实现对 Spring MVC 的自动配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 实现对 Spring Security OAuth2 的自动配置 --> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>${spring.boot.version}</version> </dependency> </dependencies> </project>
添加 spring-security-oauth2-autoconfigure
依赖,引入 Spring Security OAuth 并实现自动配置。同时,它也引入了 Spring Security 依赖。如下图所示:
2.1.2 SecurityConfig
创建 SecurityConfig 配置类,提供一个账号密码为「yunai/1024」的用户。代码如下:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override @Bean(name = BeanIds.AUTHENTICATION_MANAGER) public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public static NoOpPasswordEncoder passwordEncoder() { return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth. // 使用内存中的 InMemoryUserDetailsManager inMemoryAuthentication() // 不使用 PasswordEncoder 密码编码器 .passwordEncoder(passwordEncoder()) // 配置 yunai 用户 .withUser("yunai").password("1024").roles("USER"); } }
我们通过 Spring Security 提供认证功能,所以这里需要配置一个用户。
友情提示:看不懂这个配置的胖友,后续可回《芋道 Spring Boot 安全框架 Spring Security 入门》重造下。
2.1.3 OAuth2AuthorizationServerConfig
创建 OAuth2AuthorizationServerConfig 配置类,进行授权服务器。代码如下:
@Configuration @EnableAuthorizationServer public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { /** * 用户认证 Manager */ @Autowired private AuthenticationManager authenticationManager; //配置使用的 AuthenticationManager 实现用户认证的功能 @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authenticationManager(authenticationManager); } //设置 /oauth/check_token 端点,通过认证后可访问。 //这里的认证,指的是使用 client-id + client-secret 进行的客户端认证,不要和用户认证混淆。 //其中,/oauth/check_token 端点对应 CheckTokenEndpoint 类,用于校验访问令牌的有效性。 //在客户端访问资源服务器时,会在请求中带上访问令牌。 //在资源服务器收到客户端的请求时,会使用请求中的访问令牌,找授权服务器确认该访问令牌的有效性。 @Override public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { oauthServer.checkTokenAccess("isAuthenticated()"); } //进行 Client 客户端的配置。 //设置使用基于内存的 Client 存储器。实际情况下,最好放入数据库中,方便管理。 /* * * 创建一个 Client 配置。如果要继续添加另外的 Client 配置,可以在 <4.3> 处使用 #and() 方法继续拼接。 * 注意,这里的 .withClient("clientapp").secret("112233") 代码段,就是 client-id 和 client-secret。 *补充知识:可能会有胖友会问,为什么要创建 Client 的 client-id 和 client-secret 呢? *通过 client-id 编号和 client-secret,授权服务器可以知道调用的来源以及正确性。这样, *即使“坏人”拿到 Access Token ,但是没有 client-id 编号和 client-secret,也不能和授权服务器发生有效的交互。 */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() // <4.1> .withClient("clientapp").secret("112233") // <4.2> Client 账号、密码。 .authorizedGrantTypes("password") // <4.2> 密码模式 .scopes("read_userinfo", "read_contacts") // <4.2> 可授权的 Scope // .and().withClient() // <4.3> 可以继续配置新的 Client ; } }
2.1.4 AuthorizationServerApplication
创建 AuthorizationServerApplication 类,授权服务器的启动类。代码如下:
//@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) 不调用数据库启动
@SpringBootApplication public class AuthorizationServerApplication { public static void main(String[] args) { SpringApplication.run(AuthorizationServerApplication.class, args); } }
2.1.5 简单测试
执行 AuthorizationServerApplication 启动授权服务器。下面,我们使用 Postman 模拟一个 Client。
① POST
请求 http://localhost:8080/oauth/token 地址,使用密码模式进行授权。如下图所示:
请求说明:
- 通过 Basic Auth 的方式,填写
client-id
+client-secret
作为用户名与密码,实现 Client 客户端有效性的认证。
- 请求参数
grant_type
为"password"
,表示使用密码模式。
- 请求参数
username
和password
,表示用户的用户名与密码。
响应说明:
- 响应字段
access_token
为访问令牌,后续客户端在访问资源服务器时,通过它作为身份的标识。
- 响应字段
token_type
为令牌类型,一般是bearer
或是mac
类型。
- 响应字段
expires_in
为访问令牌的过期时间,单位为秒。
- 响应字段
scope
为权限范围。
友情提示:/oauth/token
对应 TokenEndpoint 端点,提供 OAuth2.0 的四种授权模式。感兴趣的胖友,可以后续去撸撸。
② POST
请求 http://localhost:8080/oauth/check_token 地址,校验访问令牌的有效性。如下图所示:
请求和响应比较简单,胖友自己瞅瞅即可。
2.2 搭建资源服务器
创建 lab-68-demo02-resource-server
项目,搭建资源服务器。
2.2.1 引入依赖
创建 pom.xml
文件,引入 Spring Security OAuth 依赖。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>lab-68</artifactId> <groupId>cn.iocoder.springboot.labs</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>lab-68-demo02-resource-server</artifactId> <properties> <!-- 依赖相关配置 --> <spring.boot.version>2.2.4.RELEASE</spring.boot.version> <!-- 插件相关配置 --> <maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.source>1.8</maven.compiler.source> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>${spring.boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- 实现对 Spring MVC 的自动配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 实现对 Spring Security OAuth2 的自动配置 --> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>${spring.boot.version}</version> </dependency> </dependencies> </project>
友情提示:和「2.1.1 引入依赖」小节,是一致的哈。
2.2.2 配置文件
创建 application.yml
配置文件,添加 Spring Security OAuth 相关配置。
server: port: 9090 security: oauth2: # OAuth2 Client 配置,对应 OAuth2ClientProperties 类 client: client-id: clientapp client-secret: 112233 # OAuth2 Resource 配置,对应 ResourceServerProperties 类 resource: token-info-uri: http://127.0.0.1:8080/oauth/check_token # 获得 Token 信息的 URL # 访问令牌获取 URL,自定义的 access-token-uri: http://127.0.0.1:8080/oauth/token
① security.oauth2.client
配置项,OAuth2 Client 配置,对应 OAuth2ClientProperties 类。在这个配置项中,我们添加了客户端的 client-id
和 client-secret
。
为什么要添加这个配置项呢?因为资源服务器会调用授权服务器的 /oauth/check_token
接口,而考虑到安全性,我们配置了该接口需要进过客户端认证。
友情提示:这里艿艿偷懒了,其实单独给资源服务器配置一个 Client 的 client-id
和 client-secret
。我们可以把资源服务器理解成授权服务器的一个特殊的客户端。
② security.oauth2.resource
配置项,OAuth2 Resource 配置,对应 ResourceServerProperties 类。
这里,我们通过 token-info-uri
配置项,设置使用授权服务器的 /oauth/check_token
接口,校验访问令牌的有效性。
③ security.access-token-uri
配置项,是我们自定义的,设置授权服务器的 oauth/token
接口,获取访问令牌。因为稍后我们将在 LoginController 中,实现一个 /login
登录接口。
2.2.3 OAuth2ResourceServerConfig
创建 OAuth2ResourceServerConfig 类,进行资源服务器。代码如下:
@Configuration @EnableResourceServer public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 设置 /login 无需权限访问 .antMatchers("/login").permitAll() // 设置其它请求,需要认证后访问 .anyRequest().authenticated() ; } }
① 在类上添加 @EnableResourceServer
注解,声明开启 OAuth 资源服务器的功能。
同时,继承 ResourceServerConfigurerAdapter 类,进行 OAuth 资源服务器的配置。
② #configure(HttpSecurity http)
方法,设置 HTTP 权限。这里,我们设置 /login
接口无需权限访问,其它接口认证后可访问。
这样,客户端在访问资源服务器时,其请求中的访问令牌会被资源服务器调用授权服务器的 /oauth/check_token
接口,进行校验访问令牌的正确性。
2.2.4 ExampleController
创建 ExampleController 类,提供 /api/example/hello
接口,表示一个资源。代码如下:
@RestController @RequestMapping("/api/example") public class ExampleController { @RequestMapping("/hello") public String hello() { return "world"; } }
2.2.5 ResourceServerApplication
创建 ResourceServerApplication 类,资源服务器的启动类。代码如下:
@SpringBootApplication public class ResourceServerApplication { public static void main(String[] args) { SpringApplication.run(ResourceServerApplication.class, args); } }
2.2.6 简单测试(第一弹)
执行 ResourceServerApplication 启动资源服务器。下面,我们来请求服务器的 <127.0.0.1:9090/api/example/hello> 接口,进行相应的测试。
① 首先,请求 <127.0.0.1:9090/api/example/hello> 接口,不带访问令牌,则请求会被拦截。如下图所示:
② 然后,请求 <127.0.0.1:9090/api/example/hello> 接口,带上错误的访问令牌,则请求会被拦截。如下图所示:
友情提示:访问令牌需要在请求头 "Authorization"
上设置,并且以 "Bearer "
开头。
③ 最后,请求 <127.0.0.1:9090/api/example/hello> 接口,带上正确的访问令牌,则请求会被通过。如下图所示:
2.2.7 LoginController
创建 LoginController 类,提供 /login
登录接口。代码如下:
@RestController @RequestMapping("/") public class LoginController { @Autowired private OAuth2ClientProperties oauth2ClientProperties; @Value("${security.oauth2.access-token-uri}") private String accessTokenUri; @PostMapping("/login") public OAuth2AccessToken login(@RequestParam("username") String username, @RequestParam("password") String password) { // <1> 创建 ResourceOwnerPasswordResourceDetails 对象 ResourceOwnerPasswordResourceDetails resourceDetails = new ResourceOwnerPasswordResourceDetails(); resourceDetails.setAccessTokenUri(accessTokenUri); resourceDetails.setClientId(oauth2ClientProperties.getClientId()); resourceDetails.setClientSecret(oauth2ClientProperties.getClientSecret()); resourceDetails.setUsername(username); resourceDetails.setPassword(password); // <2> 创建 OAuth2RestTemplate 对象 OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(resourceDetails); restTemplate.setAccessTokenProvider(new ResourceOwnerPasswordAccessTokenProvider()); // <3> 获取访问令牌 return restTemplate.getAccessToken(); } }
在 /login
接口中,资源服务器扮演的是一个 OAuth 客户端的角色,调用授权服务器的 /oauth/token
接口,使用密码模式进行授权,获得访问令牌。
① <1>
处,创建 ResourceOwnerPasswordResourceDetails 对象,填写密码模式授权需要的请求参数。
② <2>
处,创建 OAuth2RestTemplate 对象,它是 Spring Security OAuth 封装的工具类,用于请求授权服务器。
同时,将 ResourceOwnerPasswordAccessTokenProvider 设置到其中,表示使用密码模式授权。
友情提示:这一步非常重要,艿艿在这里卡了非常非常非常久,一度自闭要放弃。
③ <3>
处,调用 OAuth2RestTemplate 的 #getAccessToken()
方法,调用授权服务器的 /oauth/token
接口,进行密码模式的授权。
注意,OAuth2RestTemplate 是有状态的工具类,所以需要每次都重新创建。
2.2.8 简单测试(第二弹)
重新执行 ResourceServerApplication 启动资源服务器。下面,我们来进行 /login
接口的测试。
① 首先,请求 http://127.0.0.1:9090/login 接口,使用用户的用户名与密码进行登录,获得访问令牌。如下图所示:
响应结果和授权服务器的 /oauth/token
接口是一致的,因为就是调用它,嘿嘿~
② 然后,请求 <127.0.0.1:9090/api/example/hello> 接口,带刚刚的访问令牌,则请求会被通过。如下图所示: