在上一篇文章中,我们已经实现了授权服务器,拿到访问token已经不是问题了。本文主要讲述如何搭建一个资源服务器,根据第三方客户端访问请求和token来实现资源的权限控制。
一、资源服务器配置
资源服务器的核心配置类就是需要继承ResourceServerConfigurerAdapter
,并重写其中的资源配置方法和安全鉴权方法。
@Configuration
// 开启资源服务器
@EnableResourceServer
public class MyResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private ResourceServerTokenServices tokenService;
/**
* 系统启动时就加载——资源配置
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
// 本资源服务的ID,用于在授权服务器那里验证是否存在这个ID的资源服务
resources.resourceId("user")
// 加载token规则
.tokenServices(tokenService)
// 禁用session
.stateless(true);
}
/**
* 系统启动时就加载——对请求进行鉴权的配置
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 任何请求都必须具有all这个域的授权
.antMatchers("/**").access("#oauth2.hasScope('all')")
.and()
.csrf().disable()
// 禁用session,因为我们使用的是token,session没有用途
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
/**
* 定义校验token的规则,见下面第二小节的描述
* @return
*/
@Bean
public ResourceServerTokenServices tokenService(){
// 当授权服务器和资源服务器不是同一台机器时需要使用RemoteTokenServices
RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
// 指定授权服务器校验token的端点
remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:8081/oauth/check_token");
// 资源服务器自己独有的身份信息
remoteTokenServices.setClientId("iSchool");
remoteTokenServices.setClientSecret("mysecret");
return remoteTokenServices;
}
}
二、Token校验设置
token校验有两种方式:
- DefaultTokenServices,适用于资源服务器和授权服务器是同一台机器的场景;
- RemoteTokenServices,适用于资源服务器和授权服务器是不同机器的场景;
我们该案例中资源服务器和授权服务器是不同的机器,大多数实际业务场景中也是不同的,所以我们以RemoteTokenServices
为例进行演示,这里放到和资源服务器配置类一块:
/**
* 定义校验token的规则
* @return
*/
@Bean
public ResourceServerTokenServices tokenService(){
// 当授权服务器和资源服务器不是同一台机器时需要使用RemoteTokenServices
RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
// 指定授权服务器校验token的端点
remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:8081/oauth/check_token");
// 资源服务器自己独有的身份信息
remoteTokenServices.setClientId("iSchool");
remoteTokenServices.setClientSecret("mysecret");
return remoteTokenServices;
}
三、安全策略配置
资源服务器也是一个独立的应用,所以也需要增加安全访问的配置,这个在先前讲Spring Security的时候已经详细介绍过了。
@EnableWebSecurity
// 开启prePost的方法注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private ResourceUserDetailsService userDetailsService;
/**
* 对请求进行鉴权的配置
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
// 没有权限进入内置的登录页面
.formLogin()
.and()
// 暂时关闭CSRF校验,允许get请求登出
.csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
/**
* 密码加密器,供在UserDetailsService中验证密码时使用
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
@Service
public class ResourceUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return User
.withUsername("root")
.password(passwordEncoder.encode("root"))
// 设置当前用户可以拥有的权限信息
.authorities("login")
.build();
}
}
四、定义资源
最后,增加我们的访问资源即可,这里就是Controller。
@RestController
public class UserController {
@GetMapping("info/queryUserInfo")
@PreAuthorize("hasAuthority('user:query')")
public String queryUserInfo(){
return "user info return!";
}
@GetMapping("delete/deleteUserInfo")
@PreAuthorize("hasAuthority('user:delete')")
public String deleteUserInfo(){
return "user info delete!";
}
}
此时,我们就完成了资源服务器的所有内容。
五、测试
5.1 授权服务器
我们以客户端模式启动上一节的授权服务器后,通过HTTP工具(我这里使用IDEA的插件RestClient)来进行如下的测试:
首先访问如下请求以获取访问token:
GET http://localhost:8081/oauth/token?client_id=iSchool&client_secret=mysecret&grant_type=client_credentials
得到的内容如下:
{
"access_token": "1146bac1-ade0-4b2d-a919-99ecf5acb4dd",
"token_type": "bearer",
"expires_in": 43199,
"scope": "all"
}
此时我们先不使用资源服务器,我们使用如下的请求来模拟资源服务器的验证token,以此来判断授权服务器是否能正常工作:
POST http://localhost:8081/oauth/check_token
body {
"token":"1146bac1-ade0-4b2d-a919-99ecf5acb4dd"
}
得到的返回内容是:
{
"aud": [
"user"
],
"scope": [
"all"
],
"active": true,
"exp": 1628545020,
"authorities": [
"user:query"
],
"client_id": "iSchool"
}
可以看到,允许访问的资源ID是user
、访问的资源域是all
,拥有的权限是user:query
,供client_id为iSchool
的第三方应用访问,这和我们前面配置授权服务器时的内容一致,因此授权服务是正常的。
5.2 资源服务器
然后,我们就启动资源服务器,现在来请求资源服务器的资源,并带上我们从授权服务器那里得到的token:
GET http://localhost:8082/info/queryUserInfo
head {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": "Bearer 1146bac1-ade0-4b2d-a919-99ecf5acb4dd"
}
得到的返回结果为:
user info return!
但是,如果我们访问另外一个接口:
GET http://localhost:8082/delete/deleteUserInfo
head {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": "Bearer 1146bac1-ade0-4b2d-a919-99ecf5acb4dd"
}
得到的结果就是:
{
"error": "access_denied",
"error_description": "不允许访问"
}
很明显,当资源服务请求授权服务的时候,授权服务告诉资源服务,当前这个访问token只有user:query
权限,所以需要其它权限才能访问的资源就被禁止。
如果我们的资源服务器域名称不是all
,而是改成了user
,那么访问资源就会报错如下信息:
{
"error": "insufficient_scope",
"error_description": "Insufficient scope for this resource",
"scope": "user"
}
六、拓展
在实际的使用场景中,一个访问token并不仅仅限制只能访问拥有权限范围内的接口,还会限制只能访问某个客户的信息,因此我们还需要在授权中增加用户的身份信息。待研究。
网友评论