chenxxxxxxxx / spring-security-family

A demo & share for how to use spring-security in micro-service system.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

spring-security-family

A demo & share for how to use spring-security in micro-service system.

我们但凡做一个系统,这个系统不是在封闭环境中,不是只给一个人用,为了保证系统与数据安全,那么就会涉及到权限控制,权限控制这个东西可以说是很多系统的基础,因为我们不能让所有人对系统上的所有资源都进行同样的操作。

这篇分享与我们之前的分享一样,我们以一个框架与DEMO为切入口,会比较系统的讲到主题所指的内容,也会交叉一些其他知识点,其实技术这个东西也是熟能生巧的,我们如果不断把一些知识,一些细节融汇在一起反复提及,那么你慢慢就在潜移默化中掌握了。还是那句话,念念不忘必有回响。

喜欢先动手的小伙伴,或者急着需要使用的小伙伴,可以先取得对应代码,可以直接跑起来,代码、数据库脚本都有了,请戳这里->>>>>>代码库地址 在这里插入图片描述

在系统中,权限的控制大致可以分为三层:

  1. 展示层:就是这个东西要不要让用户看到,对于用来讲是最直观的,看不到或者不让操作,但是这一层通过一些技术手段是可能被攻破的,用户直接调用接口怎么办?

  2. 控制层:这一层便是我们本次分享的主要内容,一个用户发起了接口调用请求,我们如何识别这个用户以及如何辨别这个用户能否进行该操作(这里就涉及到认证与鉴权,很多小伙伴容易将两者混为一谈)。这里的方式就会比较多样了,我们在这里先留下问题,在后面逐渐解答:1. 为什么控制层的权限控制比展示层可靠?2. 我们有哪些手段来进行控制?3. 分布式系统下如何进行控制?

  3. 数据层:到了这里表示我们已经通过接口的鉴权了,那么还会有什么问题呢?我们举一个例子,我们可以在A网站修改自己的用户信息,但是不能修改别的用户的,这代表什么?我们每个用户都调用的同一个接口,代表我们都有访问该接口的权限。

最后我们对这三层控制做一个简单总结: 我能不能看到这个功能 -> 我能不能使用该功能 -> 我能不能操作该数据。

铺垫的内容就先到这里,便于大家从整体上对我们要做的事有个理解以及为什么要做这件事。

因为我们的分享是一个DEMO配合分享内容,所以在这里我们先大致讲讲要使用该DEMO可以做哪些前期准备,也帮助心急想吃热豆腐的小伙伴快速掌握DEMO,就像九阴真经不修练上卷直接练下卷,这当然是可以的。

DEMO环境依赖

环境/工具 备注
JDK 1.8
Intellij IDEA 202001 社区版 安装Spring Assistant插件
Docker Desktop Windows 数据库用的Docker镜像
Mariadb 10.5.4 文档地址 如果有MySQL的小伙伴应该可以直接用替代,代码中客户端包替换为MySQL的
HeidiSQL Mariadb 的桌面客户端工具
Springboot 2.3.1
Spring Security 5
JJWT JWT的java工具包
其它工具及框架 Lombok, Swagger2, Flyway

基础知识

这里我们要再讲到一些基本知识点,不然到了后面的分享,我们讲到这些名词,大家的理解是不一样的,或者就不知道我们在讲什么那就尴尬了。

在这里插入图片描述

认识权限名词两兄弟

这两兄弟,很多小伙伴容易搞混,或者觉得就是一个东西的两种说法,其实别人还是有区别的,下面我们就讲讲两者的区别。

认证 - authentication

举个简单例子,什么是认证呢?你登录系统,输入用户名密码证明你就是你,那么这就是认证。

在这里插入图片描述

根据不同的安全级别,也有不同级别的认证方式,大致如下:

  • 单因素 身份验证 : 这是最简单的身份验证方法,通常依赖于简单的密码来授予用户对特定系统(如网站或网络)的访问权限。单因素身份验证的最常见示例是登录凭据,其仅需要针对用户名的密码。

  • 双因素身份验证 : 它是一个两步验证过程,不仅需要用户名和密码,还需要用户知道的东西,以确保更高级别的安全性。使用用户名和密码以及额外的机密信息,冒充的代价就会更大了。

  • 多重身份验证: 它使用来自独立身份验证类别的两个或更多级别的安全性来授予用户对系统的访问权限。所有因素应相互独立,以消除系统中的任何漏洞。现在常见的比如短信验证码、人脸识别、指纹识别等。

鉴权 - authorization

鉴权总在认证后,你登录了系统就能为所欲为了吗?如果这么认为那就是too young too simple,而控制你能干嘛不能干嘛的就是鉴权。

下面通过这个对比表格进一步帮助大家理解两者的区别:

认证 授权
身份验证确认您的身份以授予对系统的访问权限。 授权确定您是否有权访问资源。
这是验证用户凭据以获得用户访问权限的过程。 这是验证是否允许访问的过程。
它决定用户是否是他声称的用户。 它确定用户可以访问和不访问的内容。
身份验证通常需要用户名和密码。 授权所需的身份验证因素可能有所不同,具体取决于安全级别。
身份验证是授权的第一步,因此始终是第一步。 授权在成功验证后完成。
例如,特定大学的学生在访问大学官方网站的学生链接之前需要进行身份验证。这称为身份验证。 例如,授权确定成功验证后学生有权在大学网站上访问哪些信息。

客户端鉴权

这里所指的客户端是一个广义的客户端,意指使用Token并通过Token携带数据进行鉴权的系统,可能是APP,可能是H5,也可能是其他分散在世界各地的后端服务。

我们为什么要使用这样的鉴权呢?这样安全吗?

我们这样去想:

  1. 用户信息及用户权限我们做了统一管理;
  2. 我们有很多服务分散在各地,每个服务的权限控制可能各有自己的述求,我们把用户的角色提供了,资源要不要让用户访问操作,由各个服务自己决定;
  3. 集中式进行鉴权带来的访问压力会比较大;
  4. 如果用户的电脑、手机就能帮忙做一道拦截会大大减少可能的无效请求。

那我们要怎么做呢?基于session-cookie的体系就不行了,我们需要让请求是无状态的,那么我们就要用到Token。

Token

一个客户端鉴权的Token流程:

  1. 客户端使用用户名跟密码请求登录;
  2. 服务端收到请求,验证用户名与密码;
  3. 验证成功后,服务端会签发一个 Token,Token包含了用户鉴权需要的基本信息,再把这个 Token 发送给客户端;
  4. 客户端收到 Token 以后可以把它(或者角色信息)存储起来,比如放在 Cookie 里或者 Local Storage或者Local Session 里;
  5. 客户端每次向服务端请求资源的时候先解析Token,看看用户的角色是否符合请求资源的要求,如果符合需要则带着 Token一并请求给服务提供端;
  6. 服务端收到请求,然后去验证客户端请求里面带着的 Token及进行鉴权,如果验证成功,就向客户端返回请求的数据。

那么这个Token有没有比较标准化的实践呢?使用比较广泛的由JWT - JSON Web Token,那么接下来我们就来简单分享一下JWT。

JWT

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。

一个JWT就是一个字符串,它由三部分组成,头部、载荷与签名,每个部分之间用·符号分隔(注意左边真的由一个符号)。

头部(Header)

头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。

{"typ":"JWT","alg":"HS256"}

在头部指明了签名算法是HS256算法。 最后这个头部会被BASE64编码得到一个字符串。

载荷(playload)

载荷是存放Token需要携带的有效信息的地方。这些有效信息包含三个部分:

(1)标准中注册的声明(建议但不强制使用)

iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token。

(2)公共的声明

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

(3)私有的声明

私有声明是提供者和消费者所共同定义的声明,不建议存放敏感信息如密码等,因为载荷中的数据是需要能被解密的,意味着该部分信息可以归类为明文信息。

载荷部分自己定义的数据内容,需要让Token使用方知道如何解析如何使用,比较好的方式就是提供SDK给到使用方。

签名(signature)

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

header (base64后的)

payload (base64后的)

secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

如果我们集中式鉴权,那么这个Token这样就没问题了,可是如果我们要做上面的客户端健全分布式鉴权,如何保证Token的安全可靠呢?这就需要加入加解密体系。

加解密体系

这是一个很庞大也很复杂的体系,我们就讲讲我们要在这次分享中使用到的部分。

非对称加密

非对称加密是相对于对称加密的,对称加密是指加解密都使用同一把密钥,就像我们锁门与开门,如果这把钥匙丢了,坏人就能打开门也能锁上门了。 而非对称加密则是包含两把密钥,一把加密一把解密,作为研发人员最熟悉的场景就是使用github或gitlab,我们会生成一个RSA的SSH密钥对,在本机保存私钥文件,在远端存放着公钥。

RSA

加解密方式

  • 公钥加密私钥解密
  • 私钥加密公钥解密(这将是我们要使用的方式)
  • 私钥加密私钥解密

密钥长度

RSA 是目前应用最广泛的数字加密和签名技术,比如国内的支付宝就是通过RSA算法来进行签名验证。它的安全程度取决于秘钥的长度,目前主流可选秘钥长度为 1024位、2048位、4096位等,理论上秘钥越长越难于破解,按照维基百科上的说法,小于等于256位的秘钥,在一台个人电脑上花几个小时就能被破解,512位的秘钥和768位的秘钥也分别在1999年和2009年被成功破解,虽然目前还没有公开资料证实有人能够成功破解1024位的秘钥,但显然距离这个节点也并不遥远,所以目前业界推荐使用 2048 位或以上的秘钥,不过目前看 2048 位的秘钥已经足够安全了,支付宝的官方文档上推荐也是2048位,当然更长的秘钥更安全,但也意味着会产生更大的性能开销。

国密

这里我们不做深入分享,只做一些科普。

国密即国家密码局认定的国产密码算法,即商用密码。

国密算法是国家密码局制定标准的一系列算法。其中包括了对称加密算法,椭圆曲线非对称加密算法,杂凑算法。具体包括SM1, SM2, SM3, SM4, SM7, SM9, 祖冲之密码算法等,其中:

  • SM1

对称加密算法,加密强度为128位,采用硬件实现,该算法不公开;

  • SM2 - 替代RSA

为国家密码管理局公布的公钥算法,其加密强度为256位,官方文档显示比RSA性能好,我们在DEMO中实测下来与官方有些差异。

  • SM3 - 替代MD5、SHA-1

密码杂凑算法,杂凑值长度为32字节,和SM2算法同期公布,参见《国家密码管理局公告(第 22 号)》;

  • SM4 - 替代DES、3DES、AES

对称加密算法,随WAPI标准一起公布,可使用软件实现,加密强度为128位。

Spring Security

我们分享Spring Security依然希望大家在理解的基础上,未来可以选择Shiro也可以自己去写实现。

而且目前Spring Security在Springboot的加持下,基本使用已经很简单了,一个配置类几个注解,其实已经可以满足很多的应用了。

但是我们的目标是什么呢?就是让业务的灵活变动尽量少修改代码甚至不修改代码,能用工具给到业务部门自己弄的就不用配置文件,能用配置文件的就不用改代码。

带着这样的目标,我们就需要对其有基本的了解(想要深入了解?

简介

Spring Security 是一个可以帮助我们做用户认证、鉴权、以及预防一些基础攻击的框架。

因为我们在分享中使用的Spring Security 5,所以需要的JDK版本必须是8以上。

Spring Security的实现是基于其内部的一系列 Filter,我们可以在这些Filter基础上实现一些自己的Filter,来实现我们自定义的认证、鉴权及一些基础防御。

它支持基于Servlet、WebFlux的接口进行权限控制。

基本工作流程

我们先看一个用户在Spring Security的生命周期大概经历哪些过程:

在这里插入图片描述

我们简单解释一下上图: Spring Security的核心配置类是 WebSecurityConfigurerAdapter这个抽象类 这是权限管理启动的入口,我们在实现自己的应用时需要自己去实现这个类,里面根据我们自己的需求去做一些配置。

进入这个过程之后,我们的系统会经历一系列的Filter来层层过滤请求,保障我们的系统安全。

因此理解一些主要Filter对于理解后面的DEMO会有所帮助。

SecurityFilterChain

在这里插入图片描述

Security Filters

  • ChannelProcessingFilter

  • ConcurrentSessionFilter

    WebAsyncManagerIntegrationFilter

    SecurityContextPersistenceFilter

    HeaderWriterFilter

    CorsFilter

    CsrfFilter

    LogoutFilter

    OAuth2AuthorizationRequestRedirectFilter

    Saml2WebSsoAuthenticationRequestFilter

    X509AuthenticationFilter

    AbstractPreAuthenticatedProcessingFilter

    CasAuthenticationFilter

    OAuth2LoginAuthenticationFilter

    Saml2WebSsoAuthenticationFilter

    UsernamePasswordAuthenticationFilter

    ConcurrentSessionFilter

    OpenIDAuthenticationFilter

    DefaultLoginPageGeneratingFilter

    DefaultLogoutPageGeneratingFilter

    DigestAuthenticationFilter

    BearerTokenAuthenticationFilter

    BasicAuthenticationFilter

    RequestCacheAwareFilter

    SecurityContextHolderAwareRequestFilter

    JaasApiIntegrationFilter

    RememberMeAuthenticationFilter

    AnonymousAuthenticationFilter

    OAuth2AuthorizationCodeGrantFilter

    SessionManagementFilter

    ExceptionTranslationFilter

    FilterSecurityInterceptor

    SwitchUserFilter

处理异常

在这里插入图片描述

用户认证

在这里插入图片描述

在这里插入图片描述

UserDetails

UserDetailsService

基于用户名密码验证

在这里插入图片描述

上图的大致流程如下:

  1. AbstractAuthenticationProcessingFilter#doFilter()
  2. UsernamePasswordAuthenticationFilter#attemptAuthentication()
  3. ProviderManager#authenticate()
  4. AbstractUserDetailsAuthenticationProvider#authenticate()
  5. DaoAuthenticationProvider#retrieveUser()
  6. UserDetailsService#loadUserByUsername()

AuthenticationProvider

在这里插入图片描述

鉴权

在这里插入图片描述

在前面我们已经分享了会涉及到的一些基础知识,现在我们要结合DEMO工程来看看如何实现,这个工程涵盖了注册、登录、权限配置、角色管理,从而实现了自定义的用户注册、登录以及基于数据库的动态权限管理。 在DEMO中为了便于部署测试,将token下发方与使用方放在一个工程内,体验的小伙伴可以部署两个工程,一个下发一个校验。

工程结构

在这里插入图片描述

PhoenixWebConfig

该配置类继承自WebSecurityConfigurerAdapter,是我们认证鉴权的入口,因为我们的工程以前后端分离为前提,所以在config里面我们重写了入参为HttpSecurity的configure方法。

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class PhoenixWebConfig extends WebSecurityConfigurerAdapter {

  @Autowired private UserDetailsServiceImpl userDetailsService;
  @Autowired private PhoenixAuthSuccessFilter phoenixAuthSuccessFilter;
  @Autowired private PhoenixAuthFailHandler phoenixAuthFailHandler;
  // @Autowired private RsaKeyProperties rsaKeyProperties;
  @Autowired private PhoenixAccessDecisionManager phoenixAccessDecisionManager;
  @Autowired private PhoenixSecurityMetadataSource phoenixSecurityMetadataSource;

  /**
   * 指定密码加密算法
   * @return
   */
  @Bean
  public BCryptPasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

  /**
   * 指定Auth的UserDetailsService实现类以及密码加密方法
   * @param auth
   * @throws Exception
   */
  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    // super.configure(auth);
    auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
  }

  /**
   * 重写configure方法
   * 关闭csrf及cors拦截,因为我们的请求是来自于其他系统的,不是一个前后一体系统
   * 对于一般请求,通过自定义后置处理器写入配置信息
   * @PhoenixSecurityMetadataSource
   * 在决策处理器中,根据配置信息判断请求是否拦截
   * @PhoenixAccessDecisionManager
   * @param http
   * @throws Exception
   */
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    // super.configure(http);
    http.cors()
        .and()
        .csrf()
        .disable()
        .authorizeRequests()
        .withObjectPostProcessor(
            new ObjectPostProcessor<FilterSecurityInterceptor>() {
              @Override
              public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                object.setSecurityMetadataSource(phoenixSecurityMetadataSource);
                object.setAccessDecisionManager(phoenixAccessDecisionManager);
                return object;
              }
            })
        .antMatchers("/login", "/signup", "/swagger-ui.html", "/v2/api-doc")
        .permitAll()
        .anyRequest()
        .authenticated()
        .and()
        .formLogin()
        .successHandler(phoenixAuthSuccessFilter)
        .failureHandler(phoenixAuthFailHandler)
        .and()
        .sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    // .addFilter(new JwtAuthenticationFilter(authenticationManager(), rsaKeyProperties));
  }
}

UserInfoDto

UserInfoDto实现了UserDetails接口,这是我们认证与鉴权中非常关键与重要的一个实体,这里也很好的体现了Spring Security的依赖倒置、面向接口编程。 UserDetails接口为我们定义好了一个Spring Security的User一定要具有的一组行为,那么我们在根据我们自己的需要扩展或重写方法,去扩展实现时,都能保证Spring Security能识别我们马甲下的本质。

在这里插入图片描述

public class UserInfoDto implements UserDetails {
  @ApiModelProperty("open id")
  private String openId;

  @ApiModelProperty("用户数据")
  private UserDao user;

  @ApiModelProperty("一组角色id")
  private List<Long> roles;

  public UserInfoDto(
      UserDao userDao, UserOpenIdDao userOpenIdDao, List<OpenIdRoleDao> roleDaoList) {
    this.openId = userOpenIdDao.getOpenId();
    this.user = userDao;
    this.roles = getRoleString(roleDaoList);
  }

  private List<Long> getRoleString(List<OpenIdRoleDao> roleDaoList) {
    return roleDaoList.stream().map(OpenIdRoleDao::getRid).collect(Collectors.toList());
  }

  /**
   * 返回权限的集合,是这里非常关键的一步。
   * @return
   */
  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    return this.roles.stream()
        .map(role -> new SimpleGrantedAuthority(role.toString()))
        .collect(Collectors.toList());
  }

  public UserDao getUser() {
    return user;
  }

  public void setUser(UserDao user) {
    this.user = user;
  }

  @Override
  public String getPassword() {
    return this.user.getPassword();
  }

  @Override
  public String getUsername() {
    return this.user.getUsername();
  }

  @Override
  public boolean isAccountNonExpired() {
    return this.user.isExpired();
  }

  @Override
  public boolean isAccountNonLocked() {
    return this.user.isLocked();
  }

  @Override
  public boolean isCredentialsNonExpired() {
    return this.user.isExpired();
  }

  @Override
  public boolean isEnabled() {
    return this.user.isEnabled();
  }
}

UserDetailsServiceImpl

该类实现了UserDetailsService接口,最重要的就是要返回一个UserDetails,而这里的UserDetails就是我们上面的UserInfoDto,其中最重要的方法就是 loadUserByUsername,我们需要根据我们自己的逻辑去实现这个方法,使符合Spring Security的标准,也满足我们的要求。

@Service
public class UserDetailsServiceImpl implements UserDetailsService, PnxUser, Role {
  @Autowired private UserMapper userMapper;
  @Autowired private UserOpenIdMapper userOpenIdMapper;
  @Autowired private OpenIdRoleMapper openIdRoleMapper;

  @Override
  public List<RoleDao> getUserRoles(String openId) {
    return null;
  }

  @Override
  public Long createOrUpdateRole(RoleDao role) {
    return null;
  }

  @Override
  public void createOrUpdateRoles(List<RoleDao> roleList) {}

  @Override
  public void createOrUpdateUserRoles(String openId, List<RoleDao> roleList) {}

  /**
   * 为用户添加角色
   * @param openIdRolesDto
   * @return
   */
  //TODO: 该方法未对插入结果做校验,未对已存在数据做校验
  public boolean createUserRoles(OpenIdRolesDto openIdRolesDto) {
    String openId = openIdRolesDto.getOpenId();
    List<Long> rids = openIdRolesDto.getRids();
    rids.stream()
        .map(
            rid -> {
              openIdRoleMapper.insert(OpenIdRoleDao.builder().openId(openId).rid(rid).build());
              return true;
            });
    return true;
  }

  @Override
  public UserDao getPnxUserByName(String username) {
    return userMapper.selectOne(new QueryWrapper<UserDao>().eq("username", username));
  }

  @Override
  public UserDao getPnxUserById(Long uid) {
    return null;
  }

  @Override
  @Transactional(rollbackFor = SQLDataException.class)
  public Long createOrUpdateUser(BaseUserDto user) {
    return null;
  }

  /**
   * 根据用户名返回UserDetails的实现类对象UserInfoDto
   * @param username
   * @return
   * @throws UsernameNotFoundException
   */
  @Override
  @Cacheable(value = "userInfo", key = "#username")
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    UserDao userDao = getPnxUserByName(username);
    if (null == userDao) {
      throw new UsernameNotFoundException(UserRespMsg.NOT_EXIST.getMsg());
    }
    UserOpenIdDao userOpenIdDao =
        userOpenIdMapper.selectOne(new QueryWrapper<UserOpenIdDao>().eq("uid", userDao.getUid()));
    List<OpenIdRoleDao> openIdRoleDaoList =
        openIdRoleMapper.selectList(
            new QueryWrapper<OpenIdRoleDao>().eq("open_id", userOpenIdDao.getOpenId()));
    return new UserInfoDto(userDao, userOpenIdDao, openIdRoleDaoList);
  }
}

PhoenixSecurityMetadataSource

该类实现了FilterInvocationSecurityMetadataSource,这里主要是为了进行配置信息的操作,而我们要做的就是在这个类中解析Token,获取用户信息及权限,并将通过校验的用户信息写入AuthenticationToken,以便后续使用。

@Component
public class PhoenixSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
  @Autowired private RsaKeyProperties rsaKeyProperties;
  @Autowired private PermissionServiceImpl permissionService;

  @Override
  public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {

    FilterInvocation filterInvocation = (FilterInvocation) object;
    HttpServletRequest request = filterInvocation.getRequest();
    String token = request.getHeader("Authorization");
    //TODO: 这里为了方便登录,没有token默认不拦截,其实应该将没有token与白名单一起校验。
    if (null == token || !token.startsWith("Bearer ")) {
      return null;
    }
    try {
      UsernamePasswordAuthenticationToken upt = getUserAuthToken(token.replace("Bearer ", ""));
    } catch (JsonProcessingException e) {
      e.printStackTrace();
    }
    return getConfigAttributeList(request.getRequestURI());
  }

  /**
   * @param requestURI
   * @return
   */
  private List<ConfigAttribute> getConfigAttributeList(String requestURI) {
    if (isMatchWhiteList(requestURI)) {
      return null;
    }
    List<Long> ridList = permissionService.getPermListByUri(requestURI);
    if (0 == ridList.size()) {
      List<ConfigAttribute> cfgList = new ArrayList<>();
      cfgList.add(new SecurityConfig("ROLE_DENIED"));
      return cfgList;
    }
    return ridList.stream()
        .map(rid -> new SecurityConfig(rid.toString()))
        .collect(Collectors.toList());
  }

  /**
   * check whether this path in the white list.
   *
   * @param requestURI
   * @return
   */
  private boolean isMatchWhiteList(String requestURI) {
    return false;
  }

  /**
   * @param token
   * @return
   */
  private UsernamePasswordAuthenticationToken getUserAuthToken(String token)
      throws JsonProcessingException {
    PayLoad<UserClaim> payload =
        JwtUtils.getInfoFromToken(token, rsaKeyProperties.getPublicKey(), UserClaim.class);
    UserClaim userInfo = payload.getUserInfoDto();
    if (null == userInfo) {
      return null;
    }
    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
        new UsernamePasswordAuthenticationToken(
            userInfo,
            null,
            userInfo.getRoles().stream()
                .map(role -> new SimpleGrantedAuthority(role.toString()))
                .collect(Collectors.toList()));
    SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
    return usernamePasswordAuthenticationToken;
  }

  @Override
  public Collection<ConfigAttribute> getAllConfigAttributes() {
    return null;
  }

  @Override
  public boolean supports(Class<?> clazz) {
    return FilterInvocation.class.isAssignableFrom(clazz);
  }
}

PhoenixAccessDecisionManager

该类实现了AccessDecisionManager,在这里将最终决定本次用户访问是否可以通过鉴权,在这里面我们将用户的角色与访问uri所对应的资源角色进行匹配,如果匹配中则不返回任何数据,表示校验通过。如果没有命中,则抛出AccessDeniedException。

@Component
public class PhoenixAccessDecisionManager implements AccessDecisionManager {
  /**
   * 判断用户是否由权限进行资源访问 如果权限不足则抛出AccessDeniedException
   *
   * @param authentication
   * @param object
   * @param configAttributes
   * @throws AccessDeniedException
   * @throws InsufficientAuthenticationException
   */
  @Override
  public void decide(
      Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
      throws AccessDeniedException, InsufficientAuthenticationException {
    if (!isAccessiable(authentication, configAttributes)) {
      throw new AccessDeniedException("这不是开往幼儿园的车");
    }
  }

  /**
   * 根据auth里取出来的用户role与配置中的uri对应roles进行匹配
   * authentication与configAttributes中数据来自于PhoenixSecurityMetadataSource
   *
   * @param authentication
   * @param configAttributes
   * @return
   */
  private boolean isAccessiable(
      Authentication authentication, Collection<ConfigAttribute> configAttributes) {
    if (null == authentication) {
      return false;
    }
    List<String> userRidList =
        authentication.getAuthorities().stream()
            .map(auth -> auth.getAuthority())
            .distinct()
            .sorted()
            .collect(Collectors.toList());
    List<String> uriRidList =
        configAttributes.stream()
            .map(configAttribute -> configAttribute.getAttribute())
            .distinct()
            .sorted()
            .collect(Collectors.toList());

    for (String userRid : userRidList) {
      if (uriRidList.contains(userRid)) {
        return true;
      }
    }

    return false;
  }

  @Override
  public boolean supports(ConfigAttribute attribute) {
    return false;
  }

  @Override
  public boolean supports(Class<?> clazz) {
    return false;
  }
}

到此关键代码我们都已做了简单解释,完整代码请访问,包含了注册、登录的逻辑以及初始化的SQL脚本,SQL脚本会在初次运行程序时自动执行:https://github.com/zerozhao13/spring-security-family

结语

这里便是基于Servlet的Spring Security的部分了,除了关于Spring Security本身的知识外,对于面向接口编程及依赖倒置要有一定的理解,不然这个实现看起来会感觉很抽象。 示例中有类手写实现了建造者模式,不过实际工程中在集成了Lombok的情况下,直接使用@builder注解即可。 后续我们会继续讲到Spring Security与响应式编程的结合,以及如何来做OAuth2的认证与鉴权。

分享的视频讲解: 基础概念 代码讲解

About

A demo & share for how to use spring-security in micro-service system.


Languages

Language:Java 89.6%Language:TSQL 10.4%