Spring Security系列之五 前后端分离项目用户授权
Spring Security系列之五 前后端分离项目用户授权
章节
Spring Security系列之五 前后端分离项目用户授权
通过前面几篇文章相信大家对登录认证也比较熟悉了,所以从现在开始就进入Spring Security的另外一个核心功能点的实践:用户权限认证。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
对于web系统来说,其实就是相当于指定拥有哪些角色的用户才能访问接口 ,现在就假定我们的系统有两个角色,一个为普通用户,一个为管理员。普通用户能做的事儿管理员都能做,管理员能做的事儿普通人不一定能做。当然真实的业务情况可能比这个复杂好几倍,我这里只是抛砖引玉而已,希望大家不要不知好歹,继续追问下去。
创建两个Controller接口,一个为普通用户和管理员都能访问的:
@RestController
@RequestMapping("normal")
public class NormalResourceController {
@GetMapping("/resource")
public ResponseEntity<String> getResource(){
return ResponseEntity.ok("成功获取normal资源");
}
}
再创建一个Controller接口,只有管理员才能访问:
@RestController
@RequestMapping("/admin")
public class AdminResourceController {
@GetMapping("/resource")
public ResponseEntity<String> getResource(){
return ResponseEntity.ok("成功获取admin资源");
}
}
在数据库role表中添加两个角色:
id | role_name |
---|---|
1 | admin |
2 | normal_user |
目前我们在数据库中有两个用户:
id | username | password |
---|---|---|
1 | test1 | 2a2a2a10$pjHyw9MSGC/i6k546Ii/0uLFgTK4WYB4.8bSRq7yB4dy.ZpBLxOha |
2 | test2 | 2a2a2a10$pjHyw9MSGC/i6k546Ii/0uLFgTK4WYB4.8bSRq7yB4dy.ZpBLxOha |
我们给test1
用户添加一个admin
角色和normal_user
角色,给test2
用户添加normal_user
角色,在user_role表中添加数据:
id | user_id | role_id |
---|---|---|
1 | 1 | 1 |
2 | 1 | 2 |
3 | 2 | 2 |
我们系统的user对象是实现了UserDetails
接口的,并且重写了getAuthorities
方法,在这个方法里我们需要把用户的角色信息查出来,包装成一个GrantedAuthority
对象:
public class User implements UserDetails {
private static final long serialVersionUID = -16523804109585173L;
private Integer id;
private String username;
private String password;
private List<Role> roleList;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roleList.stream().map(role ->
new SimpleGrantedAuthority(role.getRoleName())).collect(Collectors.toList());
}
}
简单实现
上面的准备工作做好了,接下来我们在security中添加下面的配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
//super.configure(http);
http.formLogin()
.disable()
//添加header设置,支持跨域和ajax请求 本地测试先注释了
//.cors().and()
//.addFilterAfter(corsFilter(), CorsFilter.class)
.apply(smsAuthenticationSecurityConfig).and()
.apply(jwtAuthenticationSecurityConfig).and()
.apply(jwtRequestSecurityConfig).and()
// 设置URL的授权
.authorizeRequests()
// 这里需要将登录页面放行
.antMatchers("/login","/verifyCode","/smsLogin","/failure","/jwtLogin").permitAll()
.antMatchers("/admin/**").hasAuthority("admin")
.antMatchers("/normal/**").hasAnyAuthority("admin","normal_user")
// anyRequest() 所有请求 authenticated() 必须被认证
.anyRequest()
.authenticated().and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 关闭csrf
.csrf().disable();
}
调用antMatchers().hasAuthority()表明用户必须拥有某个角色才能访问这些路径。
测试一下,使用test1用户登录生成的token,访问上面两个controller都没有问题:
使用test2用户登录生成的token访问admin会提示403错误:
这样一个简单的用户权限认证就实现了。
自定义权限认证
这样做显然不够灵活,我们如果增加了其他接口还需要在这里配置,这种硬编码的方式不是很好,而且前后端分离项目,前端路由也需要做验证,我们需要动态的创建资源和角色对应的关系。
实现权限认证接口
要实现动态的权限验证,当然要先要获取对应的资源,然后再将他们对应哪些角色可以访问的关系表示出来。
Spring Security是通过SecurityMetadataSource
来加载访问资源时所需要的具体权限,所以第一步需要实现SecurityMetadataSource
,SecurityMetadataSource
是一个接口:
public interface SecurityMetadataSource extends AopInfrastructureBean {
Collection<ConfigAttribute> getAttributes(Object var1) throws IllegalArgumentException;
Collection<ConfigAttribute> getAllConfigAttributes();
boolean supports(Class<?> var1);
}
它继承了AopInfrastructureBean
接口,这个接口并没有任何方法,它只是作为一个标记,标记为实现AOP的基类,如果任何类实现了这个接口,那么这个类是不会被AOP给代理的,即使它能被切面切进去。
然后就是SecurityMetadataSource
的几个方法:
Collection<ConfigAttribute> getAttributes(Object var1) throws IllegalArgumentException;
获取某个受保护的安全对象object的所需要的权限信息,返回一组ConfigAttribute
对象的集合,如果该安全对象object不被当前SecurityMetadataSource
对象支持,则抛出异常IllegalArgumentException
。

上面是机翻的,可能看得有点懵,通俗的来讲就是,传入了一个object,然后返回了需要访问这个object对象所需要的权限。如果当前SecurityMetadataSource
这个对象不支持当前的object,就会报错,支不支持就看第三个方法。
Collection<ConfigAttribute> getAllConfigAttributes();
获取该SecurityMetadataSource
对象中保存的针对所有安全对象的权限信息的集合。该方法的主要目的是被AbstractSecurityInterceptor
用于启动时校验每个ConfigAttribute
对象。
这个方法就没什么好说的了,只是返回所有权限信息列表而已。
可以查看一下SecurityMetadataSource
接口的关系继承图:
可以看到SecurityMetadataSource
有两个子接口,
-
FilterInvocationSecurityMetadataSource
只是一个标识接口,表示安全对象是web请求FilterInvocation
的安全元数据源,本身并无任何内容。 -
MethodSecurityMetadataSource
表示安全对象是方法调用MethodInvocation
的安全元数据源,接口如下:public interface MethodSecurityMetadataSource extends SecurityMetadataSource { Collection<ConfigAttribute> getAttributes(Method method, Class<?> targetClass); }
一般用于方法间调用时的权限验证。
我们这里是web项目,实现FilterInvocationSecurityMetadataSource
就行了:
@Component
public class UrlMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
private PowerService powerService;
private final AntPathMatcher matcher = new AntPathMatcher();
public static final String NEED_LOGIN = "NEED_LOGIN";
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
String requestUrl = ((FilterInvocation) object).getRequestUrl();
List<Power> powers = powerService.queryAll();
for (Power power : powers) {
if (matcher.match(power.getUrl(),requestUrl)){
//数据库中存了这个路由,说明是需要角色才能访问的
List<Role> roleList = power.getRoleList();
if (CollectionUtils.isEmpty(roleList)){
break;
}
return roleList.stream().map(item -> new SecurityConfig(item.getRoleName().trim())).collect(Collectors.toList());
}
}
//该路径没有对应哪一个角色才能访问
return SecurityConfig.createList(NEED_LOGIN);
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
/**
* 告知调用者当前SecurityMetadataSource是否支持此类安全对象,只有支持的时候,才能对这类安全对象调用getAttributes方法
*/
@Override
public boolean supports(Class<?> clazz) {
return clazz.isAssignableFrom(FilterInvocation.class);
}
}
首先我们从request中获取了当前访问的资源,然后使用PowerService
查询出数据库中所有资源,资源类如下:
@Data
public class Power implements Serializable {
private static final long serialVersionUID = -25876673587503659L;
private Integer id;
private String title;
private String url;
private List<Role> roleList;
}
然后将这个请求url和数据库中查询出来的所有url pattern一一对照,看符合哪一个url pattern,然后就获取到该url pattern所对应的角色。
如果getAttributes(Object o)方法返回null的话,意味着当前这个请求不需要任何角色就能访问,甚至不需要登录。但是在我的整个业务中,并不存在这样的请求,对于所有未匹配到的路径,都需要认证后才可以访问,所以我在这里返回一个NEED_LOGIN的角色,这种角色在数据库中并不存在,因此我将在下一步的角色比对过程中特殊处理这种角色。
getAttributes(Object o)方法返回的角色列表最终传给AccessDecisionManager
,所以我们接下来看AccessDecisionManager
的实现。
实现权限决策器
知道了当前访问的url需要的具体权限,接下来就是决策当前的访问是否能通过权限验证了。实现如下:
@Component
public class UrlAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) {
for (ConfigAttribute attribute : configAttributes) {
String needRole = attribute.getAttribute();
if (UrlMetadataSource.NEED_LOGIN.equals(needRole) && authentication instanceof AnonymousAuthenticationToken) {
throw new InsufficientAuthenticationException("用户需要登录");
}
//当前用户的角色
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
//对比访问需要的角色,只要有一个满足就行
for (GrantedAuthority userRole : authorities) {
if (userRole.getAuthority().equals(needRole)){
return;
}
}
}
throw new AccessDeniedException("权限不足");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
我们主要讲decide
方法,这个方法有三个参数
- authentication:包含了当前的用户信息,包括拥有的权限。这里的权限来源就是前面我们系统user类的
getAuthorities
返回的角色权限列表。 - object:
FilterInvocation
对象,可以得到request
等web资源 configAttributes
:就是上面的getAttributes
方法返回的角色列表
因为我们系统中对于所有的资源,都需要登录才能访问,通过authentication instanceof AnonymousAuthenticationToken
判断用户有没有登录,没有登录就抛出异常。
然后就是用户权限的判断,我们这里判断条件是只要当前用户具有一种角色,就可以访问这个资源。兄弟们也可以根据自己的业务来实现。
配置实现类
上面权限的资源和验证我们已经都实现了,接下来就是指定让Spring Security使用我们自定义的实现类了:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SmsAuthenticationSecurityConfig smsAuthenticationSecurityConfig;
@Autowired
private JwtAuthenticationSecurityConfig jwtAuthenticationSecurityConfig;
@Autowired
private JwtRequestSecurityConfig jwtRequestSecurityConfig;
@Autowired
private UrlMetadataSource urlMetadataSource;
@Autowired
private UrlAccessDecisionManager urlAccessDecisionManager;
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/login","/verifyCode","/smsLogin","/failure","/jwtLogin");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//super.configure(http);
http.formLogin()
.disable()
//添加header设置,支持跨域和ajax请求
//.cors().and()
//.addFilterAfter(corsFilter(), CorsFilter.class)
.apply(smsAuthenticationSecurityConfig).and()
.apply(jwtAuthenticationSecurityConfig).and()
.apply(jwtRequestSecurityConfig).and()
// 设置URL的授权
.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(urlAccessDecisionManager);
object.setSecurityMetadataSource(urlMetadataSource);
return object;
}
})
// 这里需要将登录页面放行
//.antMatchers("/login","/verifyCode","/smsLogin","/failure","/jwtLogin").permitAll()
//.antMatchers("/admin/**").hasAuthority("admin")
//.antMatchers("/normal/**").hasAnyAuthority("admin","normal_user")
// anyRequest() 所有请求 authenticated() 必须被认证
.anyRequest()
.authenticated().and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 关闭csrf
.csrf().disable();
}
}
我们这里使用withObjectPostProcessor
方法,在创建默认的FilterSecurityInterceptor
的时候把我们的accessDecisionManager
和securityMetadataSource
设置进去。
这里需要注意的是,我们用原来.antMatchers().permitAll()
做的拦截白名单,实际上security会通过设置一个匿名用户来访问资源,这样就会被我们自定义的UrlMetadataSource
给拦截掉,所以说这个地方的白名单需要提到最外面配置。
转载自:https://juejin.cn/post/6937190128263626760