3

本文讲一下spring security关于权限认证相关的内容

spring security 过滤器链

先来讲一下spring security的工作过程。它其实就是一系列的filter过滤器和拦截器。

我们最常用的一般是身份认证过滤器过滤器: usernamePassword Authentication Filter,以及今天要讲到的权限拦截器 FilterSecuity Interceptor

image.png

以下是完整的过滤器链, 但是我们并不需要完全关心所有的。
image.png

Spring Security的核心逻辑都在这一套过滤器中,过滤器里会调用各种组件完成功能,掌握了这些过滤器和组件我们就基本掌握了Spring Security,这个框架的使用方式就是对这些过滤器和组件进行扩展。

UsernamePasswordAuthenticationFilter

我们先来简单回顾一下用户认证,因为我们需要的权限信息,需要从 Authentication 中获取。

Authentication 是什么呢?这里简单介绍一下

里面最重要的有三项信息:

  • Principal:用户信息,没有认证时一般是用户名,认证后一般是用户对象
  • Credentials:用户凭证,一般是密码
  • Authorities:用户权限

而 Authentication 就代表 当前登录用户


image.png

首先 在 UsernamePasswordAuthenticationFilter中,将用户名密码封装成UsernamePasswordAuthenticationToken。并调用authenticate方法认证

image.png

而 authenticate 方法由 AuthenticationManager 提供, 是Spring Security用于执行身份验证的组件,只需要调用它的authenticate 方法即可完成认证

authenticate方法的大概逻辑:

  1. this.getUserDetailsService().loadUserByUsername(username); 获取 UserDtails类
  2. 调用passwordEncoder.matches(password, userDetails.getPassword()判断用户名密码是否相同
  3. 返回的已认证Authentication,将整个UserDetails放进去充当Principal

所以我们的目的就很清楚了: 我们自己实现UserDetialsService、UserDetails、PasswordEncoder,这三个组件/类

UserDetialsService

自定义UserDetailService,重写 loadByUsername,获取用户信息。

@Service
public class UserServiceImpl implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = this.userRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
        user.getAuthorities();
        return user;   
    }
}

UserDetials类

可以注意到的是 loadUserByUsername 返回类型是 UserDetails,这是很重要的一个类。因为我们的权限就是通过该类的 getAuthorities()方法获取的.

// UserDetails接口方法
public interface UserDetails extends Serializable {

    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();
}

所以我们的权限哪里来? 就是通过返回 UserDetails 类型的对象,spring security就可以调用 getAuthorities 来获取我们的权限。

如何返回这个 UserDetails 类型的对象?

第一种方案: 我们直接 new 一个 spring security 实现 UserDetails接口的 类。

@Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User user = this.userRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("用户不存在"));

    // 设置用户角色
    List<SimpleGrantedAuthority> authorities = new ArrayList<>();
    // 这里替换成你获取权限的方法
    authorities.add(new SimpleGrantedAuthority("admin"));

    return new org.springframework.security.core.userdetails.User(username, user.getPassword(), authorities);
  }

第二种方案: 我们自己的类,继承 UserDetails 类,并重写其中的方法

public class User implements UserDetails {
  @Override
  Collection<? extends GrantedAuthority> getAuthorities() {
    // 返回权限
   }
}

现在的一般都是基于RBAC(Role-Based Access Control)模型来进行权限控制,即:基于角色的权限控制。

image.png

所以上面的代码可以替换成此逻辑: 获取用户所有角色,获取对应角色对应的所有权限,返回所有权限。

至此,我们已经成功返回了一个 UserDetails 类型的对象,且其中有我们的权限信息。

PasswordEncoder

可用 自带的 BCryptPasswordEncoder

@Configuration
@EnableWebSecurity
public class MvcSecurityConfig extends WebSecurityConfigurerAdapter {

private final BCryptPasswordEncoder passwordEncoder;

    public MvcSecurityConfig() {
        this.passwordEncoder = new BCryptPasswordEncoder();
    }
    @Bean
    PasswordEncoder passwordEncoder() {
        return this.passwordEncoder;
    }
}

经过上述校验完后, 我们获得了一个 UsernamePasswordAuthenticationToken 类型的对象,其中有我们的用户名,密码,权限

UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(userDetails,
            authentication.getCredentials(), userDetails.getAuthorities());

SecurityContextHolder.getContext().setAuthentication(result);

之后就会存到我们的 SecurityContextHolder 安全上下文中。

至此,我们已经走完了 UsernamePasswordAuthenticationFilter。当前登录用户已经存在 SecurityContextHolder 中, 注销或者session过期前我们都不需要重新认证。直接从上下文中获取就可以。

FilterSecuity Interceptor

启动权限认证

修改WebSecurityConfig类

配置类添加注解:

开启基于方法的安全认证机制,也就是说在web层的controller启用注解机制的安全确认

@EnableGlobalMethodSecurity(prePostEnabled = true)

至此我们就可以在controller层使用 @PreAuthorize 进行校验了。

@RestController
@RequestMapping("/V1.0/syllabus")
@PreAuthorize("hasAuthority('SCOPE_all')")
public class ApiSyllabusController {
}

表示访问该Controller下的所有方法,都需要当前登录用户有 SCOPE_all权限。

我们来简单看一下 hasAuthority 方法

调用了 hasAnyAuthorityName

public final boolean hasAnyAuthority(String... authorities) {
        return hasAnyAuthorityName(null, authorities);
}

从 getAuthoritySet()方法中获取所有权限, 然后判断 SCOPE_all 是否在 Set<String> 中, 如果是,则证明当前登录用户有 SCOPE_all 权限,允许访问。

private boolean hasAnyAuthorityName(String prefix, String... roles) {
        Set<String> roleSet = getAuthoritySet();
        for (String role : roles) {
            String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
            if (roleSet.contains(defaultedRole)) {
                return true;
            }
        }
        return false;
    }

获取权限方法:

private Set<String> getAuthoritySet() {
        if (this.roles == null) {
            Collection<? extends GrantedAuthority> userAuthorities = this.authentication.getAuthorities();
            if (this.roleHierarchy != null) {
                userAuthorities = this.roleHierarchy.getReachableGrantedAuthorities(userAuthorities);
            }
            this.roles = AuthorityUtils.authorityListToSet(userAuthorities);
        }
        return this.roles;
    }

重点是这行, 看到了我们熟悉的东西 this.authentication.getAuthorities()

Collection<? extends GrantedAuthority> userAuthorities = this.authentication.getAuthorities();

所以原理很简单,我们之间将包含权限的 UserDetails 封装在 authentication中。直接调用 getAuthorities() 方法就能获取当前登录用户所有权限了。

@PreAuthorize("hasAuthority('SCOPE_all')")

至此 hasAuthority 返回了 true, 权限校验成功


weiweiyi
1k 声望123 粉丝