在https://blog.csdn.net/icarusliu/article/details/87911093一文中,介绍了OAuth的一些背景知识;本文将编写一个简单的示例,演示授权模式中的密码模式及客户端模式如何实现。
本示例中涉及到的几个对象其关系如下图所示:
密码模式一般用于用户对客户端信任度最高的情况下,因为客户端需要保存用户在授权服务器中的用户名及密码信息,客户端可以访问所有用户资源,因此一般在公司内部应用之间使用的较多。比如一个公司前端一般有安卓应用、苹果应用、Web端等,这个时候用户通过客户端使用用户名与密码登录的时候,这些信息实际上是告诉了客户端了,客户端可以拿这些信息来做任何用户权限内的事情。
在密码模式中,其处理流程如下:
- 客户端在授权服务器端的注册;
- 用户登录客户端,输入用户名及密码(或以某种其它方式保存用户在授权服务器中的用户名与密码);
- 客户端访问授权服务器验证授权,并获取访问Token;
- 客户端在后续的每次访问中都带上Token进行访问。
- 资源服务器接收到请求后,调用授权服务器相关接口校验Token有效性;
- Token有效时,进行实际的业务逻辑处理;
- Token有效时,返回相应错误信息给客户端;
另外,资源服务器与授权服务器可以在一个应用中,也可以分开。本文主要讲述分开处理时的实现,关于集中式实现请参考Spring Security OAuth的官方示例。
1. 授权服务器
授权服务器需要完成以下事情:
- 管理客户端及其授权信息;
- 管理用户及其授权信息;
- 管理Token的生成及其存储;
- 管理Token的校验及校验Key;
通过定义继承自AuthorizationServerConfigurerAdapter的一个配置类,以及Spring Security的配置,可以完成以上处理。
先来看授权服务器的最终配置:
1.1 MVN
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</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-web</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
1.2 Spring Security配置
通过Spring Security来完成用户及密码加解密等配置。
@Configuration
@EnableWebSecurity
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("test").password("test").roles("USER")
.and().passwordEncoder(passwordEncoder());
}
@Bean
public static NoOpPasswordEncoder passwordEncoder() {
return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and().formLogin().permitAll();
}
}
其中几个地方需要注意:
- 必须要配置一个密码加密解密器,示例中使用了NoOpPasswordEncoder,但这种方式是不安全的,因此这个类已经过时。实际项目中注意修改成BCryptPasswordEncoder等实现类;
- 需要将AuthenticationManager注入到容器中,在进行OAuth2配置时需要使用到。
1.3 Spring Security OAuth2配置
OAuth2的配置通过继承AuthorizationServerConfigurerAdapter的配置类实现。
@Configuration
public class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {
@Resource
private AuthenticationManager authenticationManager;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("test")
.resourceIds("testResource")
.authorizedGrantTypes("password")
.authorities("ROLE_CLIENT")
.scopes("read", "write")
.secret("secret")
.redirectUris("http://localhost:8080");
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")
.checkTokenAccess("hasAuthority('ROLE_CLIENT')")
.allowFormAuthenticationForClients();
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
}
}
说明如下:
- 客户端的注册:本文通过inMemory的方式在内存中注册客户端相关信息;实际项目中可以通过一些管理接口及界面动态实现客户端的注册;
- 获取Token权限控制:客户端需要通过/oauth/token获取Token,此时实际上是未进行登录的,如果不配置将会报未授权错误;因此需要配置成tokenKeyAccess(“permitAll()”)
- 校验Token权限控制:资源服务器如果需要调用授权服务器的/oauth/check_token接口校验token有效性,那么需要配置checkTokenAccess(“hasAuthority(‘ROLE_CLIENT’)”),注意到角色是
ROLE_CLIENT,可见这种情况下资源服务器也需要当成一个客户端来进行注册。 - authenticationManager配置:需要通过endpoints.authenticationManager(authenticationManager)将Security中的authenticationManager配置到Endpoints中,否则,在Spring Security中
配置的权限控制将不会在进行OAuth2相关权限控制的校验时生效。
1.4 授权服务器启用
最后注意需要使用EnableAuthorizationServer来启动授权服务器:
@SpringBootApplication
@EnableAuthorizationServer
public class AuthenticationServerApplication {
public static void main(String[] args) {
SpringApplication.run(AuthenticationServerApplication.class, args);
}
}
2. 资源服务器配置
2.1 主配置
资源服务器MVN依赖与授权服务器基本一致。而配置则通过继承自ResourceServerConfigurerAdapter的配置类来实现:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OAuth2Configurer extends ResourceServerConfigurerAdapter {
@Bean
public ResourceServerTokenServices tokenServices() {
RemoteTokenServices tokenServices = new RemoteTokenServices();
tokenServices.setClientId("test");
tokenServices.setClientSecret("secret");
tokenServices.setCheckTokenEndpointUrl("http://localhost:8080/oauth/check_token");
return tokenServices;
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("testResource")
.tokenServices(tokenServices());
super.configure(resources);
}
}
主要完成以下配置:
- TokenService配置:在不采用JWT的情况下,需要配置RemoteTokenServices来充当tokenServices,它主要完成Token的校验等工作。因此需要指定校验Token的授权服务器接口地址。
- 同时,由于在授权服务器中配置了/oauth/check_token需要客户端登录后才能访问,因此也需要配置客户端编号及Secret;在校验之前先进行登录;
- 最后通过ResourceServerSecurityConfigurer来配置需要访问的资源编号及使用的TokenServices;
2.2 启用
需要使用EnableResourceServer来启用资源服务器
@SpringBootApplication
@EnableResourceServer
@EnableWebSecurity
public class ResourceServerApplication {
public static void main(String[] args) {
SpringApplication.run(ResourceServerApplication.class, args);
}
}
2.3 测试接口
@RestController
@RequestMapping("/test")
@PreAuthorize("hasRole('ROLE_USER')")
public class TestController {
@GetMapping
public String test() {
return "test";
}
}
3. 测试
3.1 main函数测试
ResourceOwnerPasswordResourceDetails details = new ResourceOwnerPasswordResourceDetails();
details.setId("testResource");
details.setClientId("test");
details.setClientSecret("secret");
details.setScope(Arrays.asList("read", "write"));
details.setGrantType("password");
details.setAccessTokenUri("http://localhost:8080/oauth/token");
details.setUsername("test");
details.setPassword("test");
OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(details);
AccessTokenProviderChain provider = new AccessTokenProviderChain(Collections.singletonList(new ResourceOwnerPasswordAccessTokenProvider()));
restTemplate.setAccessTokenProvider(provider);
System.out.println(restTemplate.getAccessToken());
ResponseEntity<String> responseEntity = restTemplate.getForEntity(
URI.create("http://localhost:8081/test"), String.class);
System.out.println(responseEntity);
说明:
- 通过ResourceOwnerPasswordResourceDetails及OAuth2RestTemplate可以完成密码模式的OAuth2接口调用;
- 在调用最终需要调用的接口前,通过restTemplate.getAccessToken()可以获取到AccessToken;
- 最终调用的时候,restTemplate中会自动在报文头中带上authentication信息,不再需要手工处理。
3.2 通过curl测试
3.2.1 获取Token:
curl -X POST -d grant_type=password -d username=test -d password=test http://test:secret@localhost:8080/oauth/token
或者
curl -X POST -d id=test -d client_id=test -d client_secret=secret -d scope=read -d grant_type=password -d username=test -d password=test http://localhost:8080/oauth/token
返回结果如下:
{"access_token":"ba3b7fa9-f206-4868-abd7-24ba8e83bc1b","token_type":"bearer","expires_in":43199,"scope":"read write"}
3.2.2 使用获取的Token访问接口:
curl -X GET -H "Authorization:Bearerba3b7fa9-f206-4868-abd7-24ba8e83bc1b" http://localhost:8081/test
返回结果如下:
test
4. 客户端模式
使用以下代码可以进行客户端模式访问:
public static void main(String[] args) {
ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails();
details.setClientId("test");
details.setClientSecret("secret");
details.setGrantType("client_credentials");
details.setAccessTokenUri("http://localhost:8080/oauth/token");
OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(details);
AccessTokenProviderChain provider = new AccessTokenProviderChain(Collections.singletonList(new ClientCredentialsAccessTokenProvider()));
restTemplate.setAccessTokenProvider(provider);
ResponseEntity<String> responseEntity = restTemplate.getForEntity(
URI.create("http://localhost:8081/test"), String.class);
System.out.println(responseEntity.getBody());
}
但此时,由于资源服务器test接口需要ROLE_USER角色才能访问,此时会返回以下异常:
11:08:17.765 [main] DEBUG org.springframework.web.client.RestTemplate - Response 401 UNAUTHORIZED
Exception in thread "main" error="access_denied", error_description="Error requesting access token."
如果将Test接口修改下:
@RestController
@RequestMapping("/test")
@PreAuthorize("hasRole('ROLE_CLIENT')")
public class TestController {
@GetMapping
public String test() {
return "test";
}
}
则可正常访问。
5. 总结
- 每一个客户端需要配置以下信息:
- id: 客户端编号,用于区分不同客户端;
- secret: 客户端访问密码;
- resourceIds: 客户端能够访问的资源编号清单;
- authorizedGrantTypes:客户端支持的授权模式;
- authorities:客户端所拥有的角色;
- scopes:客户端能够对所拥有的资源进行的操作;
- redirectUris: 用户授权成功后跳转到客户端的对应页面;
- 使用RemoteServices时,资源服务器与授权服务器中存在调用关系,当资源服务器接收到请求时,将会调用授权服务器的/oauth/check_token接口来验证Token有效性。
- 可以将资源服务器也配置成一个特定的客户端,这样在访问那些需要ROLE_CLIENT的授权服务器的接口时,可以进行调用;并且这种方式也可以实现资源服务器之间的接口调用(不需要特定用户
登录时); - 密码模式一般用于可信客户端的环境下,如公司内部的客户端。对于外部不信任环境,使用该模式对于用户信息及数据的泄露风险巨大。
下一文中将分析JWT方式的实现。