web-security第六期:畅谈 Spring Security Authorization(授权)

源码地址:demo-world (spring-sercurity模块)

目录

1.简介

2.GrantedAuthority

3.AccessDecisionManager

4.角色(role)

5.FilterSecurityInterceptor

6.基于Handler方法的权限控制

6.1.@PreAuthorize


1.简介

前几期我们了解了Spring Security Authentication (认证),今天我们来说道说道  Spring Security Authorization(授权)

这里举一个通俗易懂的例子:你去火车站买票乘车,你需要两样东西,身份证和车票,在去月台之前,你需要刷一下身份证,确认你是本人,这就是认证,当你坐上了火车,这时候列车员回来检查你的车票,来确定你是否可以做这一班车,这就是授权

首先我们来回顾一下 认证的结果:

web-security第六期:畅谈 Spring Security Authorization(授权)_第1张图片

也就是Authentication中的内容,我们来逐一分析一下:

  • Principal:这是登录用户的信息,一般是指 UserDetails (它的实现类,在前几期我们有定义)
  •  Credentials:通常是密码。在许多情况下,将在验证用户身份后清除此内容(设置为null),以确保它不会泄漏。
  • Authorities:这里存放的是一个  GrantedAuthority 集合,也就是授予用户的权限, GrantedAuthority对象由AuthenticationManager插入到Authentication对象中,并在以后做出授权决策时由AccessDecisionManager读取。这是本节一个重点信息

2.GrantedAuthority

GrantedAuthoritys are high level permissions the user is granted. A few examples are roles or scopes.

可以从Authentication.getAuthorities()方法获得GrantedAuthoritys。此方法提供了GrantedAuthority对象的集合。GrantedAuthority是授予用户的权限。GrantedAuthority通常由UserDetailsS​​ervice加载。 此类权限通常有两种:

  1. 角色:例如Admin或Common_user。稍后这些角色将作为配置 web authorization(web授权), method authorization(方法授权) domain object authorization(领域模型授权)的依据。 Spring Security的其他部分能够解释这些权限。使用基于用户名/密码的身份验证时。
  2. 范围:它们不特定于给定的域对象。因此,您不太可能就某个用户可以访问编号为18的部门信息而单独设置一个权限,因为如果有成千上万个这样的权限,您很快就会用光内存(或者至少导致应用程序花费很长时间)时间来认证用户。当然,Spring Security是专门为满足这一通用要求而设计的,但您可以为此目的使用项目的域对象安全功能。例如所有的用户都可以访问部门信息这个域对象,但每个人可以访问的信息范围不同,公司高管可以访问全部部门信息,而部门高管只能访问本部门的信息;

Spring Security包含一个具体的GrantedAuthority实现,即SimpleGrantedAuthority。它允许将任何用户指定的String转换为GrantedAuthority。安全体系结构中包含的所有AuthenticationProvider都使用SimpleGrantedAuthority来填充Authentication对象。

3.AccessDecisionManager

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支持安全拦截器将显示的安全对象的类型。

4.角色(role)

首先,GrantedAuthority通常由UserDetailsS​​ervice加载的,因此,我们使用SimpleGrantedAuthority来简单为用户分配一个角色

首先我们来改造一下我们的UserDO,增加一个权限集合字段:

    /**
     * 权限集合
     */
    Set authorities;

然后我们将回到UserDetails的配置中来,实现该接口的方法:

     /**
     * 返回授予用户的权限
     *
     * @return 权限集合
     */
    @Override
    public Collection 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);
    }
}

验证我们配置的结果: 

web-security第六期:畅谈 Spring Security Authorization(授权)_第2张图片

OK!成功给用户分配了角色,角色也是权限的集合,一个角色可以拥有很多权限,例如实习生可以查看文件和修改文件(两个权限),但不可以增加文件和删除文件,那么Spring是如何做到这一点的呢?答案就在 AccessDecisionManager 中,顾名思义,这是访问决策中心,一个访问是否被允许,就是由此类决定的

Spring Security提供了拦截器,用于控制对安全对象的访问,例如方法调用或Web请求。 AccessDecisionManager会做出关于是否允许进行调用的调用前决定。

5.FilterSecurityInterceptor

FilterSecurityInterceptor为HttpServletRequests提供授权。它作为安全筛选器之一插入到FilterChainProxy中,要注意不要被名字迷惑,它不是拦截器,是过滤器

web-security第六期:畅谈 Spring Security Authorization(授权)_第3张图片

  • number 1 First, the FilterSecurityInterceptor obtains an Authentication from the SecurityContextHolder.

  • number 2 Second, FilterSecurityInterceptor creates a FilterInvocation from the HttpServletRequestHttpServletResponse, and FilterChain that are passed into the FilterSecurityInterceptor.

  • number 3 Next, it passes the FilterInvocation to SecurityMetadataSource to get the ConfigAttributes.

  • number 4 Finally, it passes the AuthenticationFilterInvocation, and ConfigAttributes to the AccessDecisionManager.

    • number 5 If authorization is denied, an AccessDeniedException is thrown. In this case the ExceptionTranslationFilter handles the AccessDeniedException.

    • number 6 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

hasRole(String role)

Returns true if the current principal has the specified role.

For example, hasRole('admin')

By default if the supplied role does not start with 'ROLE_' it will be added. This can be customized by modifying the defaultRolePrefix on DefaultWebSecurityExpressionHandler.

hasAnyRole(String…​ roles)

Returns true if the current principal has any of the supplied roles (given as a comma-separated list of strings).

For example, hasAnyRole('admin', 'user')

By default if the supplied role does not start with 'ROLE_' it will be added. This can be customized by modifying the defaultRolePrefix on DefaultWebSecurityExpressionHandler.

hasAuthority(String authority)

Returns true if the current principal has the specified authority.

For example, hasAuthority('read')

hasAnyAuthority(String…​ authorities)

Returns true if the current principal has any of the supplied authorities (given as a comma-separated list of strings)

For example, hasAnyAuthority('read', 'write')

principal

Allows direct access to the principal object representing the current user

authentication

Allows direct access to the current Authentication object obtained from the SecurityContext

permitAll

Always evaluates to true

denyAll

Always evaluates to false

isAnonymous()

Returns true if the current principal is an anonymous user

isRememberMe()

Returns true if the current principal is a remember-me user

isAuthenticated()

Returns true if the user is not anonymous

isFullyAuthenticated()

Returns true if the user is not an anonymous or a remember-me user

hasPermission(Object target, Object permission)

Returns true if the user has access to the provided target for the given permission. For example, hasPermission(domainObject, 'read')

hasPermission(Object targetId, String targetType, Object permission)

Returns true if the user has access to the provided target for the given permission. For example, hasPermission(1, 'com.example.domain.Message', 'read')

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的用户才能访问此接口

 

 

 

 

 

 

你可能感兴趣的:(Spring,spring,spring-security,java,Authorization,安全)