从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第1张图片

本文承接上文《从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (上)》

依然是介绍认证中心搭建细节 

1.流程介绍

​​​​​​​

上文中介绍了总体流程并且完成了以下环境

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第2张图片

##1.创建authentication-center 服务 ✔
##2.将authentication-center 服务集成入spring cloud nacos注册中心 ✔
##3.选择oauth2 认证模式,分离认证服务器以及资源服务器,设计下沉式资源端认证模式 ✔
##4.按照spring 官方内存式认证demo 搭建oauth2 client 认证 ✔
##5.添加测试服务module 并集成如spring cloud nacos 中 ✔
##6.测试服务 作为resource server 进行认证测试 ✔
##7.将配置文件参数纳入nacos 配置中心进行管理
##8.将内存式认证改为实际redis 以及clientdetail ,userdetail 为数据库中获取
##9.将demo 权限认证改为根据当前登陆用户角色动态校验权限
##10.抽取认证服务器与资源服务器共通部分变为common module

本文覆盖内容 

 今天主要介绍后续的7,8,9环节,10作为共通抽取将单独作为一篇重构篇

本文流程详细划分

##7.将配置文件参数纳入nacos 配置中心进行管理
###7.1 为什么要纳入nacos 配置中心进行管理
###7.2 spring cloud nacos 配置中心交互流程
###7.3 本地环境配置文件说明
###7.4 创建nacos 配置中心创建共通配置以及各服务对应环境配置文件
###7.5 区分哪些是共同配置放入nacos 共通文件中
###7.6 将各自服务独立的配置放入各自对应环境的nacos 配置文件中
###7.7 启动各个服务测试
##8.将内存式认证改为实际redis 以及clientdetail ,userdetail 为数据库中获取
###8.1 如何搭建redis 单机多级集群以及如何接入项目
###8.2 集成以及使用持久层框架fluent-mybatis
###8.3 创建upms 用户统一权限管理中心服务并开发用户查询接口
###8.4 集成feign远程调用接口以及feign相关权限设置
###8.5 将tokenStore 由InMemory方式变为redis 方式
###8.6 将clientdetail 由InMemory方式变为持久化方式
###8.7 将userdetail 由InMemory方式变为持久化方式

##9.权限认证改为根据当前登陆用户角色动态校验权限
###9.1 什么是动态权限校验
###9.2 权限系统动态校验流程
###9.3 创建权限关联表
###9.4 权限匹配拦截
###9.5 单元测试造数据并且测试

好了我们按照流程一 一说明

2.将配置文件参数纳入nacos 配置中心进行管理

2.1 为什么要纳入nacos 配置中心进行管理

我们目前所有配置都是写在本地.yml 获取properties里面的,虽然可以区分环境设置多个,比如

-dev.yml,-test.yml,-prod.yml,然后打包时可以打包成对应配置jar或者war,这样一般情况是可以的,但是如果是一些可能需要动态变化的变量,不如一些不在数据库管理的一些黑白名单,中间件的扩容或者ip变化等,如果每次修改时都要从新打包太麻烦了,因此就要引入配置中心的概念,将对应环境的配置纳入配置中心管理,如果是eureka 可以是git ,也可以用apollo ,本文是在nacos体系开发的,用的是nacos

2.2 spring cloud nacos 配置中心交互流程

逻辑交互流程图

简单画了一个逻辑上的交胡流程图

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第3张图片

简要顺序说明: 后面会有每一步的详细说明和截图

1.首先nacos配置中心 创建服务A 和B指定的启动环境文件

2.启动A和B连接了nacos配置中心的话,会自动获取到对应各自环境的配置文件内容

3.如果用户对某个配置做了修改,该修改会主动推送到对应的连接应用

 2.3 创建本地dev 环境配置文件

我们本文只针对认证中心的本地配置进行nacos管理,其他应用默认也放进nacos ,就不在多余描述

首先看看目前我们认证中心的本地配置结构

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第4张图片

 我们通用里面基本没有配置,目前都是直接放在dev里面的,dev.yml配置如下

server:
  port: 8800

spring:
  application:
    name: @artifactId@
  cloud:
    nacos:
      discovery:
        server-addr: ${NACOS_HOST:127.0.0.1}:${NACOS_PORT:8848}
      config:
        server-addr: ${spring.cloud.nacos.discovery.server-addr}
        file-extension: yml
        shared-configs:
          - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
  profiles:
    active: @profiles.active@
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    #    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://${MYSQL_HOST:192.168.1.59}:${MYSQL_PORT:3306}/${MYSQL_DB:mini_cloud_auth}?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&allowMultiQueries=true&allowPublicKeyRetrieval=true
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    druid:
      #   Druid数据源配置
      # 初始连接数
      initialSize: 5
      # 最小连接池数量
      minIdle: 10
      # 最大连接池数量
      maxActive: 20
      # 配置获取连接等待超时的时间
      maxWait: 60000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      minEvictableIdleTimeMillis: 300000
      # 配置一个连接在池中最大生存的时间,单位是毫秒
      maxEvictableIdleTimeMillis: 900000
      # 配置检测连接是否有效
      validationQuery: SELECT 1
      #申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
      testWhileIdle: true
      #配置从连接池获取连接时,是否检查连接有效性,true每次都检查;false不检查。做了这个配置会降低性能。
      testOnBorrow: false
      #配置向连接池归还连接时,是否检查连接有效性,true每次都检查;false不检查。做了这个配置会降低性能。
      testOnReturn: false
      #打开PsCache,并且指定每个连接上PSCache的大小
      poolPreparedStatements: true
      maxPoolPreparedStatementPerConnectionSize: 20
      #   配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
      #合并多个DruidDatasource的监控数据
      useGlobalDataSourceStat: true
      #通过connectProperties属性来打开mergesql功能罗慢sQL记录
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500;

  feign:
    sentinel:
      enabled: true
    okhttp:
      enabled: true
    httpclient:
      enabled: false
    client:
      config:
        default:
          connectTimeout: 100000
          readTimeout: 100000

  redis:
    mode: sentinel
    password: 123456
    sentinel:
      master: local-master
      nodes:
        - 192.168.1.177:26379
        - 192.168.1.177:26380
        - 192.168.1.177:26381
    lettuce:
      pool:
        max-active: 10
        max-wait: -1
        max-idle: 5
        min-idle: 1
    database: 7

 2.4 创建nacos 配置中心创建共通配置以及各服务对应环境配置文件

 我们先启动nacos 注册中心

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第5张图片

浏览器输入http://localhost:8848/nacos  进入nacos配置中心

2.4.1nacos 配置中心文件创建命名规则

我要创建认证中心的dev环境的配置文件

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第6张图片

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第7张图片

 

所以命名要servername + env.yml,如图

 

 从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第8张图片

 2.4.2  哪些配置无法放入nacos 配置中心

之前配置文件的下图部分一定要在本地配置文件中,因为只有启动后连接了nacos才能获取里面配置内容

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第9张图片

 其他部分我们都挪进nacos ,注意spring: 这个不要落下了

authentication-center-dev.yml

server:
  port: 8800

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    #    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://${MYSQL_HOST:192.168.1.59}:${MYSQL_PORT:3306}/${MYSQL_DB:mini_cloud_auth}?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&allowMultiQueries=true&allowPublicKeyRetrieval=true
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    druid:
      #   Druid数据源配置
      # 初始连接数
      initialSize: 5
      # 最小连接池数量
      minIdle: 10
      # 最大连接池数量
      maxActive: 20
      # 配置获取连接等待超时的时间
      maxWait: 60000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      minEvictableIdleTimeMillis: 300000
      # 配置一个连接在池中最大生存的时间,单位是毫秒
      maxEvictableIdleTimeMillis: 900000
      # 配置检测连接是否有效
      validationQuery: SELECT 1
      #申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
      testWhileIdle: true
      #配置从连接池获取连接时,是否检查连接有效性,true每次都检查;false不检查。做了这个配置会降低性能。
      testOnBorrow: false
      #配置向连接池归还连接时,是否检查连接有效性,true每次都检查;false不检查。做了这个配置会降低性能。
      testOnReturn: false
      #打开PsCache,并且指定每个连接上PSCache的大小
      poolPreparedStatements: true
      maxPoolPreparedStatementPerConnectionSize: 20
      #   配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
      #合并多个DruidDatasource的监控数据
      useGlobalDataSourceStat: true
      #通过connectProperties属性来打开mergesql功能罗慢sQL记录
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500;

  feign:
    sentinel:
      enabled: true
    okhttp:
      enabled: true
    httpclient:
      enabled: false
    client:
      config:
        default:
          connectTimeout: 100000
          readTimeout: 100000

  #  redis:
  #    mode: sentinel
  #    password: 123456
  #    sentinel:
  #      master: local-master
  #      nodes:
  #        - 192.168.1.177:26379
  #        - 192.168.1.177:26380
  #        - 192.168.1.177:26381
  #    lettuce:
  #      pool:
  #        max-active: 10
  #        max-wait: -1
  #        max-idle: 5
  #        min-idle: 1
  #    database: 7
  #
  redis:
    mode: singleten
    host: 127.0.0.1
    port: 6379
    password: 123456
    database: 7
    lettuce:
      pool:
        max-active: 10
        max-wait: -1
        max-idle: 5
        min-idle: 1

 

2.5 区分哪些是共同配置放入nacos 共通文件中

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第10张图片

 我们可以针对各个dev 环境应用共通的配置抽出来作为一个application-dev.yml 管理,如上图

我们因为同样的环境一般来说各个应用某些配置是一样的,比如:

redis 配置

mq 配置

分布式文件系统配置

监控配置

环境常量配置

 我们将redis 抽出为共通放入,将原来放入authentication-center-dev.yml 中 的redis删掉

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第11张图片

 2.6 将各自服务独立的配置放入各自对应环境的nacos 配置文件中

我们同上操作,创建authentication-center-test dev以及upms-center-biz-dev.yml

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第12张图片

放入nacos 管理后内容变化如下

nacos中有了这些配置文件

 从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第13张图片

原来本地的配置文件内容只剩下如下

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第14张图片

看起来是不是简洁多了

 2.7 启动各个服务测试

我们分别启动几个服务看看是否没有问题

 从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第15张图片

都启动了,我们进入nacos中看看是否注册成功

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第16张图片

也都没有问题

3.将内存式认证改为实际redis 以及clientdetail ,userdetail 为数据库中获取

这段是本文的重点,将之前的认证处理由内存形式改完实际应用中的内存或者数据库形式

3.1 如何搭建redis 单机多级集群以及如何接入项目

我缓存用的redis,网上相关介绍太多了,就不做详细描述,大家自行集成,也可以请参考我之前的文章

《docker-compose 搭建单机版/多机版 redis sentinel 哨兵集群》

《spring boot redis集成 代码不变,通过配置文件一键切换 sentinel模式 ,cluster模式 以及单机模式》

3.2 集成以及使用持久层框架fluent-mybatis

我持久层选用的是的fluent-mybatis,主要是看中了自动生成mapper以及所有场景均可代码实现,网上也是很多介绍,大家自行集成,也可以请参考我之前的文章

《spring cloud alibaba 集成feign 自定义feign权限注解 某接口只允许feign访问 附带所有流程以及代码》

3.3 创建upms 用户统一权限管理中心服务并开发用户查询接口 

 upms module是我们mini-cloud框架中的用户统一权限管理中心

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第17张图片从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第18张图片

 本文不做过多描述,后面会有单独的一篇介绍,这里简要做个介绍就是

upm 服务主要做了两件事:

1. 提供了对用户,角色,权限的管理。

2.提供了一个fegin接口,可以让认证中心通过fegin 通过用户名可以返回用户基本信息,角色信息和权限信息等

3.4 集成feign远程调用接口以及feign相关权限设置

请参考我之前文章《spring cloud alibaba 集成feign 自定义feign权限注解 某接口只允许feign访问 附带所有流程以及代码》

3.5 将tokenStore 由InMemory方式变为redis 方式

上文中我们是以内存形式集成的tokenStore,也就是说我们的登陆认证信息都保存到了本地内存中,实际应用肯定是不可以的,我们现在集成RedisTokenStore

位置

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第19张图片

新增了TokenStore bean

  /**
     * 使用reids 保存token 替换原来的内存存储,并设置前缀为统一mini-cloud-token: 便于查询和管理
     * */
    @Bean
    public TokenStore tokenStore(){
        RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
        redisTokenStore.setPrefix(MINI_CLOUD_PREFOX);
        return redisTokenStore;
    }

 然后

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第20张图片

就可以了,现在访问试一下,看看是否redis中会有token存入

http://localhost:8800/oauth/token?username=user3&password=123&grant_type=password&scope=read&client_id=test-auth-client&client_secret=123

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第21张图片

 从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第22张图片

 确认进入了redis

3.6 将clientdetail 由InMemory方式变为持久化方式

上篇中我们内存的形式

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第23张图片

具体改为我们自己的service去处理,service涉及到的表结构我们直接使用官网的schema.sql创建表

具体地址:https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql

创建authentication-center 的对应数据库mini_cloud_auth ,执行sql后的表结构如下:

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第24张图片

 然后我们将authentication-center 服务连接到mini_cloud_auth库,具体在之前的nacos中管理了

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第25张图片

 连接之后尝试启动,没问题之后我们创建自己的clientService ,位置

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第26张图片

MiniCloudClientDetailServiceImpl.java
package com.minicloud.authentication.service;

import org.springframework.security.oauth2.common.exceptions.InvalidClientException;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.ClientRegistrationException;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.stereotype.Service;

import javax.sql.DataSource;

/**
 * @Author alan.wang
 * @date: 2022-01-18 10:51
 */
@Service
public class MiniCloudClientDetailServiceImpl extends JdbcClientDetailsService {


    public MiniCloudClientDetailServiceImpl(DataSource dataSource) {
        super(dataSource);
    }

    @Override
    public ClientDetails loadClientByClientId(String clientId) throws InvalidClientException {
        return super.loadClientByClientId(clientId);
    }
}

 原来的内存形式替换为

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第27张图片

 将数据库中添加一条数据,可以和原来内存数据保持一致

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第28张图片

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第29张图片

 3.7 将userdetail 由InMemory方式变为持久化方式

原来的代码

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第30张图片

@Bean
    public UserDetailsService userDetailsService()   {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.builder().username("user1").password("{bcrypt}" + new BCryptPasswordEncoder().encode("123")).roles("USER").build());
        manager.createUser(User.builder().username("admin").password("{bcrypt}" + new BCryptPasswordEncoder().encode("123")).roles("USER", "ADMIN").build());
        return manager;
    }
 

我们新建 UserDetailsService

package com.minicloud.authentication.service;

import com.minicloud.authentication.model.MiniCloudGrantedAuthority;
import com.minicloud.authentication.model.MiniCloudUserDetails;
import com.minicloud.upms.perms.dto.UpmsPermDTO;
import com.minicloud.upms.user.dto.UpmsUserDTO;
import com.minicloud.upms.user.fegin.UpmsCenterRemoteUserService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import javax.annotation.Resource;
import java.util.HashSet;
import java.util.List;
import java.util.Set;


/**
 * @Author alan.wang
 * @date: 2022-01-21 12:02
 * @desc: UserDetailsService 实现类,实现自定义userdetails 查询接口
 */
public class MiniCloudUserDetailServiceImpl implements UserDetailsService {

    @Resource
    private UpmsCenterRemoteUserService upmsCenterRemoteUserService;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //通过upms 服务获取用户基本信息,角色以及权限信息
        UpmsUserDTO upmsUserDTO = upmsCenterRemoteUserService.queryUpmsUserByUsername(username);
        Set  upmsPermDTOSet = new HashSet<>();
        List upmsPermDTOSList = upmsUserDTO.getUpmsRoleDTOS().stream().map(upmsRoleDTO -> upmsRoleDTO.getUpmsPermDTOS()).reduce((result, upmsRoleDTOS) -> {
            result.addAll(upmsRoleDTOS);
            return result;
        }).get();
        upmsPermDTOSet.addAll(upmsPermDTOSList);

        //调整为自定义的GrantedAuthority
        List authorities = MiniCloudGrantedAuthority.loadAuthorities(upmsPermDTOSet);

        //自定义MiniCloudUserDetails 保存进redisTokenStore
        MiniCloudUserDetails userDetails = new MiniCloudUserDetails(upmsUserDTO.getUserId(),upmsUserDTO.getUsername(), upmsUserDTO.getPassword(),authorities );
        return userDetails;
    }
}

 上面代码主要就是通过feign调用upms的findUserbyName 获取用户信息,后面会有详细说明

 然后集成进入auth管理

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第31张图片

4 权限认证改为根据当前登陆用户角色动态校验权限

4.1 什么是动态权限校验

动态权限校验一般是指每个角色有不通的访问权限,例如管理员角色可以将普通用户角色加入黑名单等,角色又是挂载到每个用户身上的,一般一个用户可以又多个用户角色,每个角色绑定多个权限,可以设计成用户登陆时,根据用户账号关联查询到角色和用户,然后针对访问的路径进行校验

4.2权限系统动态校验流程

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第32张图片

4.3 创建权限关联表

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第33张图片

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第34张图片

  最简表结构

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for upms_perm
-- ----------------------------
CREATE TABLE `upms_perm` (
  `perm_id` int(6) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `perm_url` varchar(255) NOT NULL COMMENT '权限url',
  `perm_method` varchar(10) NOT NULL COMMENT '请求方式:get,post,put,delete 等',
  `perm_name` varchar(255) NOT NULL COMMENT '权限名称',
  `perm_desc` varchar(500) DEFAULT NULL COMMENT '权限描述',
  `perm_server` varchar(20) DEFAULT NULL COMMENT '所属服务',
  PRIMARY KEY (`perm_id`)
) ENGINE=InnoDB AUTO_INCREMENT=45 DEFAULT CHARSET=utf8mb4;


-- ----------------------------
-- Table structure for upms_role
-- ----------------------------
CREATE TABLE `upms_role` (
  `role_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `role_name` varchar(64) DEFAULT NULL COMMENT '角色名称',
  `role_code` varchar(64) DEFAULT NULL COMMENT '角色code',
  `role_desc` varchar(255) DEFAULT NULL COMMENT '角色描述',
  PRIMARY KEY (`role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=36 DEFAULT CHARSET=utf8mb4;


-- ----------------------------
-- Table structure for upms_role_perm
-- ----------------------------
CREATE TABLE `upms_role_perm` (
  `role_id` int(11) NOT NULL DEFAULT '0' COMMENT '角色id',
  `perm_id` int(5) NOT NULL COMMENT '权限url',
  PRIMARY KEY (`role_id`,`perm_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Table structure for upms_user
-- ----------------------------
CREATE TABLE `upms_user` (
  `user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `username` varchar(64) DEFAULT NULL COMMENT '用户名',
  `password` varchar(255) DEFAULT NULL COMMENT '密码',
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=39 DEFAULT CHARSET=utf8mb4;


-- ----------------------------
-- Table structure for upms_user_role
-- ----------------------------
CREATE TABLE `upms_user_role` (
  `user_id` int(11) NOT NULL COMMENT '用户ID',
  `role_id` int(11) NOT NULL COMMENT '角色ID',
  PRIMARY KEY (`user_id`,`role_id`),
  KEY `user_id` (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

 4.4 权限匹配拦截

我们结合 4.2权限系统动态校验流程 梳理我们自己开发流程

#1.首先我们登陆需要保存下用户信息,这个必须是 UserDetails的子类,我们需要自定义我们自己的UserDetails 由于我们要校验权限,所以里面需要包含角色和权限

#2.校验的时候我们需要获取到当前登陆用户的角色和权限,匹配当前要访问的url,校验是否登陆

首先我们自定义自己的包含角色和权限的UserDetails的子类

MiniCloudUserDetails.java
package com.minicloud.authentication.model;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.security.core.userdetails.User;
import org.w3c.dom.stylesheets.LinkStyle;

import java.util.Collection;
import java.util.Collections;
import java.util.List;

/**
 * @Author alan.wang
 * @date: 2022-01-21 12:05
 * @desc: 保存到oauth 缓存中的数据,携带自定义属性的话需要自己添加
 */
public class MiniCloudUserDetails  extends User  {

    private Integer id;
    private Collection miniCloudGrantedAuthorities;

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    public MiniCloudUserDetails(Integer id,String username, String password, Collection authorities) {
        super(username, password, Collections.EMPTY_LIST);
        this.id = id;
        this.miniCloudGrantedAuthorities = (Collection)authorities;
    }



    public MiniCloudUserDetails(Integer id,String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection authorities) {
        super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, Collections.EMPTY_LIST);
        this.id = id;
        this.miniCloudGrantedAuthorities = (Collection)authorities;
    }

    public Integer getId() {
        return id;
    }

    public Collection getMiniCloudGrantedAuthorities() {
        return miniCloudGrantedAuthorities;
    }

    public void setMiniCloudGrantedAuthorities(Collection miniCloudGrantedAuthorities) {
        this.miniCloudGrantedAuthorities = miniCloudGrantedAuthorities;
    }
}

然后我们希望可以通过getAuthentication().getPrincipal() 直接获取到我们登录信息

例如:

MiniCloudUserDetails miniCloudUserDetails = (MiniCloudUserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal();

 需要以下几步

自定义 TokenEnhancer 为了可以扩展/oauth/check_token 返回的map信息

MiniCloudTokenEnhancer.java
package com.minicloud.authentication.config;

import com.minicloud.authentication.model.MiniCloudUserDetails;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

/**
 * @Author alan.wang
 * @date: 2022-01-25 15:06
 */
@Component
public class MiniCloudTokenEnhancer implements TokenEnhancer {

    /**
     * 自定义用户基本信息
     */
    private static final String DETAILS_USER = "user_info";
    /**
     * 客户端模式
     */
    private static final String CLIENT_CREDENTIALS  ="client_credentials";
    /**
     * 协议字段
     */
    private static final String DETAILS_LICENSE = "license";

    /**
     * 激活字段 兼容外围系统接入
     */
    private static final String ACTIVE = "active";

    /**
     * 扩展auth 认证中map 存放token 内容,client 模式不处理
     * */
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        if (CLIENT_CREDENTIALS.equals(authentication.getOAuth2Request().getGrantType())) {
            return accessToken;
        }

        final Map additionalInfo = new HashMap<>(8);
        MiniCloudUserDetails miniCloudUserDetails = (MiniCloudUserDetails) authentication.getUserAuthentication().getPrincipal();
        additionalInfo.put(DETAILS_USER, miniCloudUserDetails);
        additionalInfo.put(DETAILS_LICENSE, "made by mini-cloud");
        additionalInfo.put(ACTIVE, Boolean.TRUE);
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
        return accessToken;
    }
}

 自定义 TokenEnhancer 集成入 AuthorizationServerConfigurerAdapter

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第35张图片

ResourceServerConfigurerAdapter资源服务中自定义 UserAuthenticationConverter 拼接成自定义userdetails

MiniCloudUserAuthenticationConverter.java
package com.minicloud.authentication.test.config;

import cn.hutool.core.map.MapUtil;
import com.minicloud.authentication.test.model.MiniCloudUserDetails;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.provider.token.UserAuthenticationConverter;
import org.springframework.util.StringUtils;

import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @Author alan.wang
 * @date: 2022-01-25 13:59
 */
public class MiniCloudUserAuthenticationConverter implements UserAuthenticationConverter {


    /**
     * 不适用标记,即密码在这不给出来
     * */
    private static final String N_A = "N/A";

    @Override
    public Map convertUserAuthentication(Authentication authentication) {
        Map response = new LinkedHashMap<>();
        response.put(USERNAME, authentication.getName());
        if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
            response.put(AUTHORITIES, AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
        }
        return response;
    }

    /**
     *
     * 将check_token 中返回的OAuth2Authentication的getPrincipal 重写为我们自己的miniclouddetail
     * */
    @Override
    public Authentication extractAuthentication(Map responseMap) {
        if (responseMap.containsKey(USERNAME)) {
            Map map = MapUtil.get(responseMap, "user_info", Map.class);
            List authorities = MapUtil.get(map,"miniCloudGrantedAuthorities",List.class);
            List miniCloudGrantedAuthorities = authorities.stream().map(authoritity->{
                String method =  MapUtil.getStr((Map)authoritity,"method");
                String url = MapUtil.getStr((Map)authoritity,"url");
                return new MiniCloudGrantedAuthority(method,url);
            }).collect(Collectors.toList());
            MiniCloudUserDetails miniCloudUserDetails = new MiniCloudUserDetails(MapUtil.getInt(map,"id"),MapUtil.getStr(map,"username"),N_A,miniCloudGrantedAuthorities);
            return new UsernamePasswordAuthenticationToken(miniCloudUserDetails, N_A, miniCloudGrantedAuthorities);
        }
        return null;
    }

    /**
     * @desc: 将check_token中map获取authorities
     * */
    private Collection getAuthorities(Map map) {
        Object authorities = map.get(AUTHORITIES);
        if (authorities instanceof String) {
            return AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities);
        }
        if (authorities instanceof Collection) {
            return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils
                    .collectionToCommaDelimitedString((Collection) authorities));
        }
        throw new IllegalArgumentException("Authorities must be either a String or a Collection");
    }
}

以上几步完成后重新启动认证服务会发现返回数据多了我们存储的部分

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第36张图片

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第37张图片

 下面完成最后一步校验部分

我们在资源服务中自定义AccessDecisionManager 完成权限校验

MiniCloudAccessDecisionManager.java
package com.minicloud.authentication.test.config;

import com.minicloud.authentication.test.model.MiniCloudUserDetails;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.FilterInvocation;

import java.util.Collection;

/**
 * @Author alan.wang
 * @date: 2022-01-25 11:48
 */
public class MiniCloudAccessDecisionManager implements AccessDecisionManager {




    /**
     * @desc :通过获取自定义的MiniCloudUserDetails
     * 取得当前登陆人的所有grantedAuthorities 然后一 一和当前访问路径匹配,如果请求方式与url均一致则说明认证成功
     * 否则抛出AccessDeniedException 异常
     *
     * */
    @Override
    public void decide(Authentication authentication, Object filterInvocation, Collection collection) throws AccessDeniedException, InsufficientAuthenticationException {


        String requestUrl = ((FilterInvocation) filterInvocation).getRequestUrl();
        String method = ((FilterInvocation) filterInvocation).getRequest().getMethod();
        if(authentication.getPrincipal() instanceof String){
            throw new AccessDeniedException(authentication.getName()+",无权访问url:"+requestUrl);
        }
        Collection grantedAuthorities = ((MiniCloudUserDetails)authentication.getPrincipal()).getMiniCloudGrantedAuthorities();
        for (MiniCloudGrantedAuthority grantedAuthority : grantedAuthorities) {
             if(requestUrl.equals(grantedAuthority.getAuthority())&&method.equals(grantedAuthority.getMethod())){
                 return;
             }
        }

        throw new AccessDeniedException(authentication.getName()+",无权访问url:"+requestUrl);
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class aClass) {
        return true;
    }
}

自定义 AccessDecisionManager 代码描述

1. 我们通过authentication.getPrincipal()获取到当前登陆用户的信息,也就是我们自定义的MiniCloudUserDetails

2.取得当前登陆用户信息里的所有grantedAuthorities

3.取得当前访问url

4.循环遍历匹配grantedAuthorities 中的url以及method,如果有则通过,没有则抛出异常

4.5 单元测试造数据并且测试

我们写一个初始化代码初始化一批用户,角色,权限,并挂载上

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第38张图片

package com.minicloud.upms;

import com.alibaba.nacos.common.utils.HttpMethod;
import com.minicloud.upms.perms.dto.UpmsPermDTO;
import com.minicloud.upms.perms.dto.UpmsRolePermDTO;
import com.minicloud.upms.perms.service.UpmsPermService;
import com.minicloud.upms.perms.service.UpmsRolePermService;
import com.minicloud.upms.role.dto.UpmsRoleDTO;
import com.minicloud.upms.role.service.UpmsRoleService;
import com.minicloud.upms.user.dto.UpmsUserDTO;
import com.minicloud.upms.user.service.UpmsUserService;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @Author alan.wang
 * @date: 2022-01-20 13:51
 */
@RunWith(SpringRunner.class)
@SpringBootTest(classes = MiniCloudUPMSApplication.class)
@Slf4j
public class UpmsInitTest {

    @Autowired
    private UpmsUserService upmsUserService;

    @Autowired
    private UpmsRoleService upmsRoleService;

    @Autowired
    private UpmsPermService upmsPermService;

    @Autowired
    private UpmsRolePermService upmsRolePermService;


    /**
     *
     * 初始化角色
     * */
    @Test
    public void testInitRoles() throws InterruptedException {
        UpmsRoleDTO upmsRoleDTO1 = UpmsRoleDTO.builder().roleName("超级管理员").roleCode("SUPER_ADMIN").roleDesc("最大权限").build();
        UpmsRoleDTO upmsRoleDTO2 = UpmsRoleDTO.builder().roleName("普通用户1").roleCode("USER1").roleDesc("普通用户1").build();
        UpmsRoleDTO upmsRoleDTO3 = UpmsRoleDTO.builder().roleName("普通用户2").roleCode("USER1").roleDesc("普通用户2").build();

        List upmsRoleDTOS= Stream.of(upmsRoleDTO1,upmsRoleDTO2,upmsRoleDTO3).collect(Collectors.toList());
        List upmsRolesIds = upmsRoleService.saveRoles(upmsRoleDTOS);
        log.info("init roles successful:{} ",upmsRolesIds.toArray().toString());
        testInitUsers(upmsRolesIds);
        testInitPerms(upmsRolesIds);
    }

    /**
     * 初始化接口权限列表
     * */
    public void testInitPerms(List rolesIds){

        //mini-cloud test 服务
        UpmsPermDTO upmsPermDTO1 = UpmsPermDTO.builder().permServer("test").permUrl("/test/hello").permMethod(HttpMethod.GET).permName("hello接口").permDesc("测试hello get接口").build();
        UpmsPermDTO upmsPermDTO2 = UpmsPermDTO.builder().permServer("test").permUrl("/test/hello").permMethod(HttpMethod.POST).permName("hello接口").permDesc("测试hello post接口").build();
        UpmsPermDTO upmsPermDTO3 = UpmsPermDTO.builder().permServer("test").permUrl("/test/hello2").permMethod(HttpMethod.GET).permName("hello2接口").permDesc("测试hello2 get接口").build();
        UpmsPermDTO upmsPermDTO4 = UpmsPermDTO.builder().permServer("test").permUrl("/test/hello2").permMethod(HttpMethod.POST).permName("hello2接口").permDesc("测试hello2 post接口").build();

        //mini-cloud upms  服务
        UpmsPermDTO upmsPermDTO5 = UpmsPermDTO.builder().permServer("upms").permUrl("/user/save").permMethod(HttpMethod.PUT).permName("保存用户接口").permDesc("测试保存用户接口").build();
        UpmsPermDTO upmsPermDTO6 = UpmsPermDTO.builder().permServer("upms").permUrl("/findById/{userId}").permMethod(HttpMethod.GET).permName("根据userId 查询user接口").permDesc("测试根据userId 查询user接口").build();

        List upmsPermDTOS = Stream.of(upmsPermDTO1,upmsPermDTO2,upmsPermDTO5,upmsPermDTO6,upmsPermDTO3,upmsPermDTO4).collect(Collectors.toList());
        List permIds =  upmsPermService.savePerms(upmsPermDTOS);

        //关联角色1的权限集
        testInitRolePerms(rolesIds.get(1),permIds.subList(0,4));

        //关联角色2的权限集
        testInitRolePerms(rolesIds.get(2),permIds.subList(4,6));

    }


    /**
     * 初始化角色权限关联表
     * */
    public void testInitRolePerms(Integer roleId,List permsId){

        List upmsRolePermDTOS = new ArrayList<>();
        permsId.stream().forEach(pid->{
            upmsRolePermDTOS.add(UpmsRolePermDTO.builder().roleId(roleId).permId(pid).build());
        });
        upmsRolePermService.saveRolePerms(upmsRolePermDTOS);
    }


    /**
     *
     * 初始化用户
     * */

    public void testInitUsers(List upmsRolesIds) throws InterruptedException {

        UpmsUserDTO upmsUserDTO1 = UpmsUserDTO.builder().username("admin").password("{bcrypt}" + new BCryptPasswordEncoder().encode("admin")).upmsRoleDTOS(Stream.of(UpmsRoleDTO.builder().roleId(upmsRolesIds.get(0)).build()).collect(Collectors.toList())).build();
        UpmsUserDTO upmsUserDTO2 = UpmsUserDTO.builder().username("user3").password("{bcrypt}" + new BCryptPasswordEncoder().encode("123")).upmsRoleDTOS(Stream.of(UpmsRoleDTO.builder().roleId(upmsRolesIds.get(1)).build()).collect(Collectors.toList())).build();
        UpmsUserDTO upmsUserDTO3 = UpmsUserDTO.builder().username("user4").password("{bcrypt}" + new BCryptPasswordEncoder().encode("123")).upmsRoleDTOS(Stream.of(UpmsRoleDTO.builder().roleId(upmsRolesIds.get(2)).build()).collect(Collectors.toList())).build();

        upmsUserService.saveUser(upmsUserDTO1);
        upmsUserService.saveUser(upmsUserDTO2);
        upmsUserService.saveUser(upmsUserDTO3);



    }


}

 执行完毕后如下

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第39张图片

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第40张图片

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第41张图片

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第42张图片

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第43张图片 

用户user3 具有/test/hello 路径的GET权限

我们测试一下

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第44张图片

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第45张图片

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (中)_第46张图片

可以看到确实匹配到了,也执行完毕。

重构代码篇

到此我们所有动态权限校验的代码部分就全部完成了,目前还都是最小闭环,真实应用还需要在此基础上扩展,我们开发中很多代码其实都是冗余的,一个类在多个服务中都会出现,也会出现很多魔法值,下篇会讲如何重构抽取共通部分,等到网关服务集成完毕之后会开源1.0版本代码~有需要得call me

你可能感兴趣的:(mini-cloud,微服务,spring,cloud,alibaba,oauth2,aop,java)