在https://blog.csdn.net/icarusliu/article/details/87911093一文中,介绍了OAuth的一些背景知识;本文将编写一个简单的示例,演示授权模式中的密码模式及客户端模式如何实现。
本示例中涉及到的几个对象其关系如下图所示:
密码模式一般用于用户对客户端信任度最高的情况下,因为客户端需要保存用户在授权服务器中的用户名及密码信息,客户端可以访问所有用户资源,因此一般在公司内部应用之间使用的较多。比如一个公司前端一般有安卓应用、苹果应用、Web端等,这个时候用户通过客户端使用用户名与密码登录的时候,这些信息实际上是告诉了客户端了,客户端可以拿这些信息来做任何用户权限内的事情。
在密码模式中,其处理流程如下:
另外,资源服务器与授权服务器可以在一个应用中,也可以分开。本文主要讲述分开处理时的实现,关于集中式实现请参考Spring Security OAuth的官方示例。
授权服务器需要完成以下事情:
通过定义继承自AuthorizationServerConfigurerAdapter的一个配置类,以及Spring Security的配置,可以完成以上处理。
先来看授权服务器的最终配置:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.security.oauthgroupId>
<artifactId>spring-security-oauth2artifactId>
<version>2.3.4.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<version>2.1.2.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
通过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();
}
}
其中几个地方需要注意:
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);
}
}
说明如下:
最后注意需要使用EnableAuthorizationServer来启动授权服务器:
@SpringBootApplication
@EnableAuthorizationServer
public class AuthenticationServerApplication {
public static void main(String[] args) {
SpringApplication.run(AuthenticationServerApplication.class, args);
}
}
资源服务器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);
}
}
主要完成以下配置:
需要使用EnableResourceServer来启用资源服务器
@SpringBootApplication
@EnableResourceServer
@EnableWebSecurity
public class ResourceServerApplication {
public static void main(String[] args) {
SpringApplication.run(ResourceServerApplication.class, args);
}
}
@RestController
@RequestMapping("/test")
@PreAuthorize("hasRole('ROLE_USER')")
public class TestController {
@GetMapping
public String test() {
return "test";
}
}
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);
说明:
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"}
curl -X GET -H "Authorization:Bearerba3b7fa9-f206-4868-abd7-24ba8e83bc1b" http://localhost:8081/test
返回结果如下:
test
使用以下代码可以进行客户端模式访问:
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";
}
}
则可正常访问。
下一文中将分析JWT方式的实现。