源码地址:demo-world (spring-sercurity模块)
目录
1.简介
前几期我们了解了Spring Security Authentication (认证),今天我们来说道说道 Spring Security Authorization(授权)
这里举一个通俗易懂的例子:你去火车站买票乘车,你需要两样东西,身份证和车票,在去月台之前,你需要刷一下身份证,确认你是本人,这就是认证,当你坐上了火车,这时候列车员回来检查你的车票,来确定你是否可以做这一班车,这就是授权
首先我们来回顾一下 认证的结果:
也就是Authentication中的内容,我们来逐一分析一下:
- Principal:这是登录用户的信息,一般是指 UserDetails (它的实现类,在前几期我们有定义)
- Credentials:通常是密码。在许多情况下,将在验证用户身份后清除此内容(设置为null),以确保它不会泄漏。
- Authorities:这里存放的是一个 GrantedAuthority 集合,也就是授予用户的权限, GrantedAuthority对象由AuthenticationManager插入到Authentication对象中,并在以后做出授权决策时由AccessDecisionManager读取。这是本节一个重点信息
2.GrantedAuthority
GrantedAuthority
s are high level permissions the user is granted. A few examples are roles or scopes.
可以从Authentication.getAuthorities()方法获得GrantedAuthoritys。此方法提供了GrantedAuthority对象的集合。GrantedAuthority是授予用户的权限。GrantedAuthority通常由UserDetailsService加载。 此类权限通常有两种:
- 角色:例如Admin或Common_user。稍后这些角色将作为配置 web authorization(web授权), method authorization(方法授权) domain object authorization(领域模型授权)的依据。 Spring Security的其他部分能够解释这些权限。使用基于用户名/密码的身份验证时。
- 范围:它们不特定于给定的域对象。因此,您不太可能就某个用户可以访问编号为18的部门信息而单独设置一个权限,因为如果有成千上万个这样的权限,您很快就会用光内存(或者至少导致应用程序花费很长时间)时间来认证用户。当然,Spring Security是专门为满足这一通用要求而设计的,但您可以为此目的使用项目的域对象安全功能。例如所有的用户都可以访问部门信息这个域对象,但每个人可以访问的信息范围不同,公司高管可以访问全部部门信息,而部门高管只能访问本部门的信息;
Spring Security包含一个具体的GrantedAuthority实现,即SimpleGrantedAuthority。它允许将任何用户指定的String转换为GrantedAuthority。安全体系结构中包含的所有AuthenticationProvider都使用SimpleGrantedAuthority来填充Authentication对象。
3.AccessDecisionManager
AccessDecisionManager 由 AbstractSecurityInterceptor 调用,并负责做出最终的访问控制决策。 AccessDecisionManager接口包含三种方法:
void decide(Authentication authentication, Object secureObject,
Collection<ConfigAttribute> 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支持安全拦截器将显示的安全对象的类型。
4.角色(role)
首先,GrantedAuthority通常由UserDetailsService加载的,因此,我们使用SimpleGrantedAuthority来简单为用户分配一个角色
首先我们来改造一下我们的UserDO,增加一个权限集合字段:
/**
* 权限集合
*/
Set<GrantedAuthority> 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<UserDO> 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会做出关于是否允许进行调用的调用前决定。
5.FilterSecurityInterceptor
FilterSecurityInterceptor为HttpServletRequests提供授权。它作为安全筛选器之一插入到FilterChainProxy中,要注意不要被名字迷惑,它不是拦截器,是过滤器
-
First, the
FilterSecurityInterceptor
obtains an Authentication from the SecurityContextHolder. -
Second,
FilterSecurityInterceptor
creates aFilterInvocation
from theHttpServletRequest
,HttpServletResponse
, andFilterChain
that are passed into theFilterSecurityInterceptor
. -
Next, it passes the
FilterInvocation
toSecurityMetadataSource
to get theConfigAttribute
s. -
Finally, it passes the
Authentication
,FilterInvocation
, andConfigAttribute
s to theAccessDecisionManager
.-
If authorization is denied, an
AccessDeniedException
is thrown. In this case theExceptionTranslationFilter
handles theAccessDeniedException
. -
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);
}
这里有几个很重要的点要注意一下:
- 分配角色的时候我们使用 ROLE_角色名 的标准格式,这样在读取角色时候,我们只用表示为 hasRole(角色名)即可
- anyRequest().denyAll 和 anyRequest.authenticatied 是不同的概念 前者表示对于前面没有配置访问策略的URL ,一律拒绝,而后者表示,对所有的url进行认证,认证通过后即可访问。
这里由一张由官方提供的内置表达式表格:
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 |
6.基于Handler方法的权限控制
上面我们使用了Role来控制某个用户是否可以访问一个接口(或handler 方法),但很明显,在实际开发中,这样使用会有很多局限性,例如对用户是否有权访问某一个接口的判断过程很复杂,那么,这种直接在HttpSecurity中配置的方法,显然就不太实用了,因此需要使用基于 Handler 方法的权限控制,Spring为我们提供了几个很有用的注解
要使用注解,首先在SecurityConfig中开启它
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
6.1.@PreAuthorize
这是一个很实用的注解,它可以决定方法是否可以被调用,如下:
/**
* 获取登录页面
*
* @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的用户才能访问此接口