一、概述
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
:资源服务器,拥有受保护资源。如果请求包含正确的访问令牌,则可以访问资源。Client
:客户端。它请求资源服务器时,会带上访问令牌,从而成功访问资源。Resource Owner
:资源拥有者。最终用户,他有访问资源的账号与密码。
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
)
其中,密码模式和授权码模式比较常用。
二、密码模式
2.1 搭建授权服务器
2.1.1 引入依赖
<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>
</dependency>
</dependencies>
添加 spring-security-oauth2-autoconfigure
依赖,引入 Spring Security OAuth
并实现自动配置。同时,它也引入了 Spring Security
依赖。
2.1.2 SecurityConfig
创建 SecurityConfig
配置类,提供一个账号密码为「canying/123456
」的用户。
@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())
// 配置 canying 用户
.withUser("canying").password("123456").roles("USER");
}
}
2.1.3 OAuth2AuthorizationServerConfig
创建 OAuth2AuthorizationServerConfig
配置类,进行授权服务器。
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
/**
* 用户认证 Manager
*/
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.checkTokenAccess("isAuthenticated()");
}
@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
@SpringBootApplication
public class AuthorizationServerApplication {
public static void main(String[] args) {
SpringApplication.run(AuthorizationServerApplication.class,args);
}
}
2.1.5 简单测试
① 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
为权限范围。
② POST
请求 http://localhost:8080/oauth/check_token
地址,校验访问令牌的有效性。
2.2 搭建资源服务器
2.2.1 引入依赖
<!-- 实现对 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>
</dependency>
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
接口,而考虑到安全性,我们配置了该接口需要进过客户端认证。
② security.oauth2.resource
配置项,OAuth2 Resource
配置,对应 ResourceServerProperties 类。这里,我们通过 token-info-uri
配置项,设置使用授权服务器的 /oauth/check_token
接口,校验访问令牌的有效性。
③ security.access-token-uri
配置项,是我们自定义的,设置授权服务器的 oauth/token
接口,获取访问令牌。因为稍后我们将在 LoginController
中,实现一个 /login
登录接口。
2.2.3 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 简单测试
① 首先,请求 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
是有状态的工具类,所以需要每次都重新创建。