写在前面

本篇来学习Spring Security中的授权操作,建议先阅读之前Shiro框架相关的几篇内容,通过对比学习可以加深对授权的理解。

授权

授权就是当用户通过认证之后,需要访问某一资源的时候,我们需要检查用户是否具备访问该资源的权限,如果具备就允许访问;反之则不允许。

认证

我们知道用户想要进行授权,前提是已经通过了认证,而SpringSecurity存在多种认证方式,查看一下这个AuthenticationManagerBuilder类,可以发现它存在inMemoryAuthentication(内存)、jdbcAuthentication(数据库)和ldapAuthentication(LDAP)等三种认证方式:

1
2
3
4
5
6
7
8
9
10
11
public InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> inMemoryAuthentication() throws Exception {
return (InMemoryUserDetailsManagerConfigurer)this.apply(new InMemoryUserDetailsManagerConfigurer());
}

public JdbcUserDetailsManagerConfigurer<AuthenticationManagerBuilder> jdbcAuthentication() throws Exception {
return (JdbcUserDetailsManagerConfigurer)this.apply(new JdbcUserDetailsManagerConfigurer());
}

public LdapAuthenticationProviderConfigurer<AuthenticationManagerBuilder> ldapAuthentication() throws Exception {
return (LdapAuthenticationProviderConfigurer)this.apply(new LdapAuthenticationProviderConfigurer());
}

其实这些不同来源的数据认证都被被封装为一个UserDetailsService接口,任何实现了该接口的对象都可以作为认证的数据源,如下所示:

1
2
3
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

这个接口中只有一个loadUserByUsername方法,它通过用户名来加载用户信息。前面说过SpringSecurity中与Web安全相关的配置都在WebSecurityConfigurerAdapter类中,而这类中有一个userDetailsService方法:

1
2
3
4
protected UserDetailsService userDetailsService() {
AuthenticationManagerBuilder globalAuthBuilder = (AuthenticationManagerBuilder)this.context.getBean(AuthenticationManagerBuilder.class);
return new WebSecurityConfigurerAdapter.UserDetailsServiceDelegator(Arrays.asList(this.localConfigureAuthenticationBldr, globalAuthBuilder));
}

因此开发者可以通过重写这个userDetailsService方法,进而提供一个UserDetailsService实例来配置用户信息。

在MyWebSecurityConfig类中新增一个userDetailsService方法,里面的代码如下所示:

1
2
3
4
5
6
7
8
    @Bean
protected UserDetailsService userDetailsService(){
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("envy").password("1234").roles("admin").build());
manager.createUser(User.withUsername("test").password("1234").roles("user").build());
return manager;
}
}

当前之前基于内存的验证方式也是可以使用的,但是两者只能选择其中任意一种:

1
2
3
4
5
6
7
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("envy").password("1234").roles("admin")
.and()
.withUser("test").password("1234").roles("user");
}

提供测试接口

为了后续测试需要,接下来提供一些测试接口。修改HelloController类中的代码为如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello,envy!";
}

@GetMapping("/admin/hello")
public String admin() {
return "hello,admin!";
}

@GetMapping("/user/hello")
public String user() {
return "hello,user!";
}
}

对于这三个接口来说,/hello接口是认证后所有人都可以访问的;/admin/hello接口是认证后具有admin角色的人才可以访问;/user/hello接口是认证后具有user角色的人才可以访问的;还有具备admin角色的人也能访问所有具备user角色才能访问的资源,言外之意就是具备admin角色的人自动具备user角色。

权限拦截设置

接下来设置权限的拦截规则,开发者只需在configure(HttpSecurity http)方法中新增如下代码:

1
2
3
4
5
6
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.anyRequest().authenticated()
.and()
...

可以看到这里我们采用了Ant风格的路径匹配符,Ant风格的路径匹配符在Spring全家桶中使用非常广泛,且易于使用。其中通配符**表示匹配多层路径;*表示匹配一层路径;?表示匹配任意单个字符。

以之前在HelloController中提供的测试接口为例,当请求路径为/hello,那么用户只需登录即可访问;当请求路径为/admin/hello,那么用户需要登录且必须具备admin角色才能访问;当请求路径为/user/hello,那么用户需要登录且必须具备user角色才能访问。

请注意上面三行代码的书写顺序,在前面学习Shiro框架的时候就说过,URL在匹配的时候是按照从上到下的顺序来匹配的,一旦匹配成功后续就不再匹配了,而SpringSecurity和Shiro一样,因此上述拦截规则非常重要。

如果开发者将anyRequest放在antMatchers的前面,即如下所示:

1
2
3
4
5
6
http.authorizeRequests()
.anyRequest().authenticated()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.and()
...

此时启动项目,系统就会抛出异常,提示anyRequest不能放在antMatchers前面,因为anyRequest包括所有请求,而antMatchers只是其中的某一个请求,因此将anyRequest放在antMatchers前面是毫无意义的。也就是说anyRequest应该放在最后面,表示除了前面设置的拦截规则外,其余请求应当如何处理。(SpringSecurity 2.1.16版本不抛异常,但是后续测试权限访问会失效,即用户可以越权访问它不具备角色的资源。

SpringSecurity框架中与规则拦截相关的配置类为AbstractRequestMatcherRegistry,里面有一个antMatchers(HttpMethod method)方法,这个方法源码如下所示:

1
2
3
public C antMatchers(HttpMethod method) {
return this.antMatchers(method, "/**");
}

可以看到它直接返回的就是匹配任意URL的规则,并没有旧版本中对anyRequest是否配置进行检测,因此启动时是不会抛出异常。

项目测试

接下来启动项目,然后以test用户登录,之后依次访问/hello/admin/hello/user/hello这三个接口:

登录成功:

可以访问/hello接口:

可以访问/user/hello接口:

无法访问/admin/hello接口:

之后使用envy用户进行登录,可以发现结果类似,由于envy用户不具备user角色,因此无法访问/user/hello接口:

角色继承

现在有一个问题,就是之前我们要求的:具备admin角色的人也能访问所有具备user角色才能访问的资源,言外之意就是具备admin角色的人自动具备user角色。但是目前上述代码还未实现这个功能,需要使用角色继承来实现。

角色继承顾名思义就是让上一级角色自动具备下一级角色的权限。开发者只需在MyWebSecurityConfig类中新增roleHierarchy方法,并设置角色继承信息:

1
2
3
4
5
6
7
//角色继承
@Bean
RoleHierarchy roleHierarchy(){
RoleHierarchyImpl hierarchy= new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_admin > ROLE_user");
return hierarchy;
}

请注意,在配置角色继承时,需要给角色添加ROLE_前缀,查看一下之前test用户登录时的信息:

看到没有在这个authority中,之前设置的user变成了ROLE_user,而这个就是user权限信息,同时ROLE_admin > ROLE_user表示ROLE_admin权限自动具备ROLE_user权限。

之后重新启动项目,让envy用户登录,之后访问/user/hello接口,可以看到此时由于envy用户具备admin角色,而admin角色自动具备user角色,因此现在envy用户是可以访问该接口的:

现在有一个问题就是明明开发者在userDetailsService方法中设置的用户角色是没有添加ROLE_前缀的,怎么后续会有这个前缀呢?

1
2
3
4
5
6
7
@Bean
protected UserDetailsService userDetailsService(){
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("envy").password("1234").roles("admin").build());
manager.createUser(User.withUsername("test").password("1234").roles("user").build());
return manager;
}

点击这个roles,查看一下它的源码,发现这其实是一个roles方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public User.UserBuilder roles(String... roles) {
List<GrantedAuthority> authorities = new ArrayList(roles.length);
String[] var3 = roles;
int var4 = roles.length;

for(int var5 = 0; var5 < var4; ++var5) {
String role = var3[var5];
Assert.isTrue(!role.startsWith("ROLE_"), () -> {
return role + " cannot start with ROLE_ (it is automatically added)";
});
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
}

return this.authorities((Collection)authorities);
}

仔细看这个遍历的代码,可以发现当用户设置的role中以ROLE_开头时,那么就返回role和一句提示语:不能以ROLE_开头,它会自动添加ROLE_Assert.isTrue(boolean,expression)方法是当boolean为true则不抛出异常,如果boolean为false,则抛出异常并执行expression语句。

开发者可以将之前的userDetailsService方法进行修改:

1
2
3
4
5
6
7
@Bean
protected UserDetailsService userDetailsService(){
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("envy").password("1234").roles("ROLE_admin").build());
manager.createUser(User.withUsername("test").password("1234").roles("ROLE_user").build());
return manager;
}

之后启动项目,可以发现控制台的确输出上述信息:

1
Factory method 'userDetailsService' threw exception; nested exception is java.lang.IllegalArgumentException: ROLE_admin cannot start with ROLE_ (it is automatically added)

这样本篇关于Spring Security中基于内存授权操作的学习就到此为止,后续学习如何基于数据库进行认证。