源码地址:demo-world (spring-sercurity模块)
目录
1.简介
2.GrantedAuthority
3.AccessDecisionManager
4.角色(role)
5.FilterSecurityInterceptor
6.基于Handler方法的权限控制
6.1.@PreAuthorize
前几期我们了解了Spring Security Authentication (认证),今天我们来说道说道 Spring Security Authorization(授权)
这里举一个通俗易懂的例子:你去火车站买票乘车,你需要两样东西,身份证和车票,在去月台之前,你需要刷一下身份证,确认你是本人,这就是认证,当你坐上了火车,这时候列车员回来检查你的车票,来确定你是否可以做这一班车,这就是授权
首先我们来回顾一下 认证的结果:
也就是Authentication中的内容,我们来逐一分析一下:
GrantedAuthority
s are high level permissions the user is granted. A few examples are roles or scopes.
可以从Authentication.getAuthorities()方法获得GrantedAuthoritys。此方法提供了GrantedAuthority对象的集合。GrantedAuthority是授予用户的权限。GrantedAuthority通常由UserDetailsService加载。 此类权限通常有两种:
Spring Security包含一个具体的GrantedAuthority实现,即SimpleGrantedAuthority。它允许将任何用户指定的String转换为GrantedAuthority。安全体系结构中包含的所有AuthenticationProvider都使用SimpleGrantedAuthority来填充Authentication对象。
AccessDecisionManager 由 AbstractSecurityInterceptor 调用,并负责做出最终的访问控制决策。 AccessDecisionManager接口包含三种方法:
void decide(Authentication authentication, Object secureObject,
Collection attrs) throws AccessDeniedException;
boolean supports(ConfigAttribute attribute);
boolean supports(Class clazz);
decide方法的参数包括它进行授权决策所需的所有相关信息。secureObject是安全对象,也就是我要保护的访问资源,例如 method authorization(方法授权)中方法就是受保护的资源,然后在AccessDecisionManager中实现某种安全性逻辑以确保安全访问。如果访问被拒绝,则预期实现将引发AccessDeniedException。
在启动时,AbstractSecurityInterceptor将调用support(ConfigAttribute)方法,以确定AccessDecisionManager是否可以处理传递的ConfigAttribute。安全拦截器实现调用support(Class)方法,以确保配置的AccessDecisionManager支持安全拦截器将显示的安全对象的类型。
首先,GrantedAuthority通常由UserDetailsService加载的,因此,我们使用SimpleGrantedAuthority来简单为用户分配一个角色
首先我们来改造一下我们的UserDO,增加一个权限集合字段:
/**
* 权限集合
*/
Set authorities;
然后我们将回到UserDetails的配置中来,实现该接口的方法:
/**
* 返回授予用户的权限
*
* @return 权限集合
*/
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return userDO.getAuthorities();
}
我们还需要修改一下伪数据源
public class DataSource {
public static UserDO getUserByUsername(String username) {
SimpleGrantedAuthority roleIntern = new SimpleGrantedAuthority("ROLE_INTERN");
SimpleGrantedAuthority roleDba = new SimpleGrantedAuthority("ROLE_DBA");
SimpleGrantedAuthority roleAdmin = new SimpleGrantedAuthority("ROLE_ADMIN");
List userList = new ArrayList<>();
//初始化三个用户
userList.add(new UserDO(1L, "swing", new BCryptPasswordEncoder().encode("123456"), 20, Arrays.asList(roleDba, roleIntern)));
userList.add(new UserDO(2L, "sky", new BCryptPasswordEncoder().encode("123456"), 20, Collections.singletonList(roleIntern)));
userList.add(new UserDO(3L, "admin", new BCryptPasswordEncoder().encode("123456"), 20, Arrays.asList(roleDba, roleIntern, roleAdmin)));
return userList.stream().distinct().filter(userDO -> userDO.getUsername().equals(username)).collect(Collectors.toList()).get(0);
}
}
验证我们配置的结果:
OK!成功给用户分配了角色,角色也是权限的集合,一个角色可以拥有很多权限,例如实习生可以查看文件和修改文件(两个权限),但不可以增加文件和删除文件,那么Spring是如何做到这一点的呢?答案就在 AccessDecisionManager 中,顾名思义,这是访问决策中心,一个访问是否被允许,就是由此类决定的
Spring Security提供了拦截器,用于控制对安全对象的访问,例如方法调用或Web请求。 AccessDecisionManager会做出关于是否允许进行调用的调用前决定。
FilterSecurityInterceptor为HttpServletRequests提供授权。它作为安全筛选器之一插入到FilterChainProxy中,要注意不要被名字迷惑,它不是拦截器,是过滤器
First, the FilterSecurityInterceptor
obtains an Authentication from the SecurityContextHolder.
Second, FilterSecurityInterceptor
creates a FilterInvocation
from the HttpServletRequest
, HttpServletResponse
, and FilterChain
that are passed into the FilterSecurityInterceptor
.
Next, it passes the FilterInvocation
to SecurityMetadataSource
to get the ConfigAttribute
s.
Finally, it passes the Authentication
, FilterInvocation
, and ConfigAttribute
s to the AccessDecisionManager
.
If authorization is denied, an AccessDeniedException
is thrown. In this case the ExceptionTranslationFilter
handles the AccessDeniedException
.
If access is granted, FilterSecurityInterceptor
continues with the FilterChain which allows the application to process normally.
默认情况下,Spring Security 将会对所有的请求进行身份认证,如下:
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
);
}
也可以自定义认证规则,如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
//禁用csrf (跨站请求伪造)
http.csrf().disable();
http.authorizeRequests(authorize -> authorize
//允许直接访问
.mvcMatchers("/login").permitAll()
.mvcMatchers("/file/1/**").hasRole("INTERN")
.mvcMatchers("/file/4/**").hasRole("ADMIN")
//同时具有两种角色才可访问的api
.mvcMatchers("/login/page").access("hasRole('INTERN') and hasRole('DBA')")
//剩下的都拒绝
.anyRequest().denyAll()
);
//将认证设置在UsernamePasswordAuthenticationFilter之前
http.addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
这里有几个很重要的点要注意一下:
这里由一张由官方提供的内置表达式表格:
Expression | Description |
---|---|
|
Returns For example, By default if the supplied role does not start with 'ROLE_' it will be added. This can be customized by modifying the |
|
Returns For example, By default if the supplied role does not start with 'ROLE_' it will be added. This can be customized by modifying the |
|
Returns For example, |
|
Returns For example, |
|
Allows direct access to the principal object representing the current user |
|
Allows direct access to the current |
|
Always evaluates to |
|
Always evaluates to |
|
Returns |
|
Returns |
|
Returns |
|
Returns |
|
Returns |
|
Returns |
上面我们使用了Role来控制某个用户是否可以访问一个接口(或handler 方法),但很明显,在实际开发中,这样使用会有很多局限性,例如对用户是否有权访问某一个接口的判断过程很复杂,那么,这种直接在HttpSecurity中配置的方法,显然就不太实用了,因此需要使用基于 Handler 方法的权限控制,Spring为我们提供了几个很有用的注解
要使用注解,首先在SecurityConfig中开启它
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
这是一个很实用的注解,它可以决定方法是否可以被调用,如下:
/**
* 获取登录页面
*
* @return 登录页面
*/
@PreAuthorize("hasRole('INTERN') and hasRole('DBA')")
@GetMapping("/page")
String login() {
return "login";
}
这种写法和之前我们配置的作用相同,当然此注解还有更强大的地方,例如还可以这么写(表示只有id =2才可以访问):
@PreAuthorize("hasRole('INTERN') and #id==2")
@GetMapping("/1/{id}")
@ResponseBody
public FileDO getFileNameById(@PathVariable Long id) {
return new FileDO(id, "这个杀手不太冷.mp4", 1231232423L);
}
不过,大部分时候这些功能还是不能满足我们的需求,所以该注解还支持注入Bean,和自定义权限认证,我们来写一个简单的例子:
/**
* 自定义认证方法
*
* @author swing
*/
@Service("as")
public class AuthorizeService {
/**
* 是否授权
*
* @return 是否可以访问呢
*/
public boolean hasPermission(String username) {
UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return userDetails.getUsername().equals(username);
}
}
/**
* 获取文件
*
* @param id 文件Id
* @return 文件信息
*/
@PreAuthorize("@as.hasPermission('swing')")
@GetMapping("/1/{id}")
@ResponseBody
public FileDO getFileNameById(@PathVariable Long id) {
return new FileDO(id, "这个杀手不太冷.mp4", 1231232423L);
}
以上代码表示,只有用户名为swing的用户才能访问此接口