int## 基础名词概念
**权限:**属于系统的安全范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全控制策略用户可以访问而且只能访问自己被授权的资源,主要包括用户身份认证和请求鉴权两部分,简称认证鉴权
认证判断一个用户是否为合法用户的处理过程,最常用的简单身份认证是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确
在这里插入图片描述

鉴权:即访问控制,控制谁能访问那些资源;进行身份认证后需要分配权限可访问的系统资源,对于某些资源没有权限是无法访问的,如下图所示

在这里插入图片描述
权限控制:用户是某个角色、或拥有某个资源时,才可访问系统资源我们称之为权限控制,权限控制分为下列2类型:
基于角色
RBAC基于角色的访问控制是以角色为中心进行访问控制,比如:主体的角色为总经理可以查询企业运营报表,查询员工薪资信息等,访问控制流程如下:
在这里插入图片描述
基于资源
RBAC基于资源的访问控制,是以资源中心进行访问控制,企业中常用的权限管理方法,实现思路是:将系统操作的每个URL配置在资源表中,将资源对应到角色,将角色分配给用户,用户访问系统功能通过Filter进行过滤,过滤器获取到用户的url,只要访问的url是用户分配角色中的URL是用户分配角色的url则进行访问,其具体流程如下:
在这里插入图片描述
匿名资源:无需认证鉴权就可以访问的资源
公共资源:只需登录既可以访问的资源

多平台权限控制

xxxx作为一个SaaS平台,商家提供运营主体信息后,运营平台会为商家开通系统,各个商家平台都需要在运营平台的管理下去工作:
1、运营平台可以管理所有商家平台的企业信息
2、运营平台可以管理所有商家平台的资源信息
3、运营平台可以管理所有商家平台的角色信息
4、运营平台可以管理商家平台的用户信息

第二章 基础信息简介

在开始做权限开发之前我们需要看下权限设计的数据库结构:
在这里插入图片描述
通过上图,我们可以得到如下的信息:
一个企业可以有多个用户
一个用户可以有多个角色
一个角色可以有多个资源
这个是经典的权限设计,也就是:企业,用户,角色,资源通过它们可以来完成整个权限的控制。

企业信息

商家想申请入驻平台,首先在申请页面【也可以后端录入】进行信息填写,填写完成【运营平台】对商家资质进行审核,审核通过后商家即可入职使用,如图所示:

在这里插入图片描述
数据库结构设计

CREATE TABLE `tab_enterprise` (
  `id` bigint(18) NOT NULL,
  `enterprise_id` bigint(18) NOT NULL COMMENT '商户ID【系统内部识别使用】',
  `enterprise_name` varchar(200) COLLATE utf8_bin NOT NULL COMMENT '企业名称',
  `enterprise_no` varchar(32) COLLATE utf8_bin NOT NULL COMMENT '工商号',
  `province` varchar(32) COLLATE utf8_bin NOT NULL COMMENT '地址(省)',
  `area` varchar(32) COLLATE utf8_bin NOT NULL COMMENT '地址(区)',
  `city` varchar(32) COLLATE utf8_bin NOT NULL COMMENT '地址(市)',
  `address` varchar(200) COLLATE utf8_bin NOT NULL COMMENT '详细地址',
  `status` varchar(8) COLLATE utf8_bin NOT NULL COMMENT '状态(试用:trial,停用:stop,正式:official)',
  `proposer_Id` bigint(18) DEFAULT NULL COMMENT '申请人Id',
  `enable_flag` varchar(18) CHARACTER SET utf8 NOT NULL COMMENT '是否有效',
  `created_time` datetime NOT NULL COMMENT '创建时间',
  `updated_time` datetime NOT NULL COMMENT '创建时间',
  `expire_time` datetime NOT NULL COMMENT '到期时间 (试用下是默认七天后到期,状态改成停用)',
  `web_site` varchar(100) COLLATE utf8_bin DEFAULT NULL COMMENT '商户门店web站点',
  `sharding_id` bigint(18) NOT NULL COMMENT '分库id',
  `app_web_site` varchar(100) COLLATE utf8_bin DEFAULT NULL COMMENT '商户h5web站点',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='企业账号管理';

实现细节
对于这个功能的CRUD这里就不做赘述,这里主要思考2个问题:
为什么我们要为商家绑定域名
通过一个图来分析下整个的工作流程
在这里插入图片描述
员工在浏览器中发起ppsk.shop.eehp.cn的访问请求
阿里云域名解析会把ppsk.shop.eehp网址解析到阿里云ECS服务器101.101.108.2
阿里云ECS服务器【101.101.108.2】宿主机会把信息转发到docker-nginx服务器
docker-nginx服务器配置的serverName【*.shop.eehp.cn】转发到gateway服务
gateway服务根据ppsk.shop.eehp.cn兑换企业号100001
根据企业号100001访问目标的商家A
域名和企业号如何建立关联
在security模块的initEnterpriseWeb方法,这里主要有四个方法:
init:初始化企业站点信息到redis,此方法上有==@PostConstruct==注解,表示项目启动时即加载信息
addWebSiteforRedis:添加缓存中的站点,当我们【新增】企业主体信息时调用此方法
deleteWebSiteForRedis:移除缓存中的站点,当我们【删除,仅用】企业主体信息时调用此方法
updateWebSiteforRedis:更新缓存中的站点,当我们修改禁用企业主体信息时调用此方法

/**
 * @ClassName initEnterpriseWebSIteInfo.java
 * @Description 初始化企业站点信息到redis
 */
@Component
public class InitEnterpriseSite {

    @Autowired
    IEnterpriseService enterpriseService;

    @Autowired
    RedissonClient redissonClient;

    /**
     *获得两时间的秒间隔
     */
    public Long secondInterval(Date date1, Date date2) {
        long secondInterval = (date2.getTime() - date1.getTime()) / 1000;
        return secondInterval;
    }

    /***
     * @description 初始化企业站点信息到redis
     */
    @PostConstruct
    public void init(){
        QueryWrapper<Enterprise> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(Enterprise::getEnableFlag, SuperConstant.YES)
            .and(wrapper->wrapper
                .eq(Enterprise::getStatus,SuperConstant.TRIAL)
                .or()
                .eq(Enterprise::getStatus,SuperConstant.OFFICIAL));
        List<Enterprise> list = enterpriseService.list(queryWrapper);
        List<EnterpriseVo> enterpriseVos = BeanConv.toBeanList(list, EnterpriseVo.class);
        for (EnterpriseVo enterpriseVo : enterpriseVos) {
            String webSiteKey = SecurityCacheConstant.WEBSITE+enterpriseVo.getWebSite();
            RBucket<EnterpriseVo> webSiteBucket = redissonClient.getBucket(webSiteKey);
            String appWebSiteKey = SecurityCacheConstant.APP_WEBSITE+enterpriseVo.getAppWebSite();
            RBucket<EnterpriseVo> appWebSiteBucket = redissonClient.getBucket(appWebSiteKey);
            Long secondInterval = this.secondInterval(new Date(), enterpriseVo.getExpireTime());
            if (secondInterval.longValue()>0){
                webSiteBucket.set(enterpriseVo,secondInterval, TimeUnit.SECONDS);
                appWebSiteBucket.set(enterpriseVo,secondInterval, TimeUnit.SECONDS);
            }
        }
    }

    /***
     * @description 添加缓存中的站点
     * @param enterpriseVo 企业号
     * @return:
     */
    public void addWebSiteforRedis(EnterpriseVo enterpriseVo){
        String webSiteKey = SecurityCacheConstant.WEBSITE+enterpriseVo.getWebSite();
        RBucket<EnterpriseVo> webSiteBucket = redissonClient.getBucket(webSiteKey);
        String appWebSiteKey = SecurityCacheConstant.APP_WEBSITE+enterpriseVo.getAppWebSite();
        RBucket<EnterpriseVo> appWebSiteBucket = redissonClient.getBucket(appWebSiteKey);
        Long secondInterval = this.secondInterval(new Date(), enterpriseVo.getExpireTime());
        if (secondInterval.longValue()>0){
            webSiteBucket.trySet(enterpriseVo,secondInterval, TimeUnit.SECONDS);
            appWebSiteBucket.trySet(enterpriseVo,secondInterval, TimeUnit.SECONDS);
        }
    }

    /***
     * @description 移除缓存中的站点
     * @param enterpriseVo 企业号
     * @return:
     */
    public void deleteWebSiteforRedis( EnterpriseVo enterpriseVo){
        String webSiteKey = SecurityCacheConstant.WEBSITE+enterpriseVo.getWebSite();
        RBucket<EnterpriseVo> webSiteBucket = redissonClient.getBucket(webSiteKey);
        String appWebSiteKey = SecurityCacheConstant.APP_WEBSITE+enterpriseVo.getAppWebSite();
        RBucket<EnterpriseVo> appWebSiteBucket = redissonClient.getBucket(appWebSiteKey);
        webSiteBucket.delete();
        appWebSiteBucket.delete();
    }


    /***
     * @description 更新缓存中的站点
     * @param enterpriseVo 企业号
     * @return:
     */
    public void updataWebSiteforRedis(EnterpriseVo enterpriseVo){
        String webSiteKey = SecurityCacheConstant.WEBSITE+enterpriseVo.getWebSite();
        RBucket<EnterpriseVo> webSiteBucket = redissonClient.getBucket(webSiteKey);
        String appWebSiteKey = SecurityCacheConstant.APP_WEBSITE+enterpriseVo.getAppWebSite();
        RBucket<EnterpriseVo> appWebSiteBucket = redissonClient.getBucket(appWebSiteKey);
        Long secondInterval = this.secondInterval(new Date(), enterpriseVo.getExpireTime());
        if (secondInterval.longValue()>0){
            webSiteBucket.set(enterpriseVo,secondInterval, TimeUnit.SECONDS);
            appWebSiteBucket.set(enterpriseVo,secondInterval, TimeUnit.SECONDS);
        }
    }

考虑到企业表【Enterprise】做CRUD的时候会影响缓存的更新,需要在EnterpriseFaceImpl中做同步的处理

@Override
public EnterpriseVo createEnterprise(EnterpriseVo eterperiseVo) {
    Enterprise enterpriseResult = EnterpriseService.createEnterprise(eterperiseVo);
    //同步缓存
    if (!EmptyUtil.isNullOrEmpty(enterpriseResult)){
        initEnterpriseWebSiteInfo.addWebSiteforRedis(eterperiseVo.getWebSite(),eterperiseVo);
    }
    return BeanConv.toBean(enterpriseResult,EnterpriseVo.class);
}

@Override
public Boolean updateEnterprise(EnterpriseVo enterpriseVo) {
    Boolean flag = EnterpriseService.updateEnterprise(enterpriseVo);
    //同步缓存
    if (flag){
        if (enterpriseVo.getEnableFlag().equals(SuperConstant.YES)){
            initEnterpriseWebSiteInfo.updataWebSiteforRedis(enterpriseVo.getWebSite(),enterpriseVo);
        }else {
            initEnterpriseWebSiteInfo.deleteWebSiteforRedis(enterpriseVo.getWebSite(),enterpriseVo);
        }
    }
    return flag;
}

@Override
public Boolean deleteEnterprise(String[] checkedIds) {
    //同步缓存
    for (String checkedId : checkedIds) {
        Enterprise enterprise = EnterpriseService.getById(checkedId);
        EnterpriseVo enterpriseVo = BeanConv.toBean(enterprise, EnterpriseVo.class);
        initEnterpriseWebSiteInfo.deleteWebSiteforRedis(enterprise.getWebSite(),enterpriseVo);
    }
    Boolean flag =  EnterpriseService.deleteEnterprise(checkedIds);
    return flag;
}
CREATE TABLE `tab_resource` (
  `id` bigint(18) NOT NULL COMMENT '主键',
  `parent_id` bigint(18) DEFAULT NULL COMMENT '父Id',
  `resource_name` varchar(36) DEFAULT NULL COMMENT '资源名称',
  `request_path` varchar(200) DEFAULT NULL COMMENT '资源路径',
  `icon` varchar(20) DEFAULT NULL COMMENT '图标',
  `is_leaf` varchar(18) DEFAULT NULL COMMENT '是否叶子节点',
  `resource_type` varchar(36) DEFAULT NULL COMMENT '资源类型',
  `sort_no` int(11) DEFAULT NULL COMMENT '排序',
  `description` varchar(200) DEFAULT NULL COMMENT '描述',
  `system_code` varchar(36) DEFAULT NULL COMMENT '系统归属',
  `is_system_root` varchar(18) DEFAULT NULL COMMENT '是否根节点',
  `enable_flag` varchar(18) DEFAULT NULL COMMENT '是否有效',
  `created_time` datetime DEFAULT NULL COMMENT '创建时间',
  `updated_time` datetime DEFAULT NULL COMMENT '创建时间',
  `sharding_id` bigint(18) DEFAULT NULL,
  `label` varchar(200) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='资源表';

为了解决信息系统中的访问控制管理的问题,适当简化授权工作量,提高权限管理效率,需要建立基于角色的多系统授权管理模型,其业务管理模式如下:
运营平台系统管理员负责角色的权限及用户权限及用户分配。
运营平台系统管理员负责角色的权限匹配,同时赋予商家管理员对角色分配用户的权限,定义标准角色,实现权限管理的部分下放
在此模式下,系统管理员不再兼任单位管理员工作,需要实现权限的多级下放,其架构设计如图所示
在这里插入图片描述

数据库结构:
角色表:

CREATE TABLE `tab_role` (
  `id` bigint(18) NOT NULL COMMENT '主键',
  `role_name` varchar(36) DEFAULT NULL COMMENT '角色名称',
  `label` varchar(36) DEFAULT NULL COMMENT '角色标识',
  `description` varchar(200) DEFAULT NULL COMMENT '角色描述',
  `sort_no` int(36) DEFAULT NULL COMMENT '排序',
  `enable_flag` varchar(18) DEFAULT NULL COMMENT '是否有效',
  `created_time` datetime DEFAULT NULL COMMENT '创建时间',
  `updated_time` datetime DEFAULT NULL COMMENT '创建时间',
  `sharding_id` bigint(18) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='角色表';

角色资源表:

CREATE TABLE `tab_role_resource` (
  `id` bigint(18) NOT NULL,
  `enable_flag` varchar(18) DEFAULT NULL,
  `role_id` bigint(18) DEFAULT NULL,
  `resource_id` bigint(18) DEFAULT NULL,
  `created_time` datetime DEFAULT NULL COMMENT '创建时间',
  `updated_time` datetime DEFAULT NULL COMMENT '创建时间',
  `sharding_id` bigint(18) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='角色资源表';

用户信息

xxx系统中用户分为:运营商员工、商家平台员工,其信息的维护规则如下:
运营平台系统管理员:可以定义,管理所有的用户,并且从角色中选择权限
运营平台系统管理员:负责定义角色,同时赋予商家管理员对角色,商家管理员分配用户的权限(定义标准角色,实现权限管理的部分下放)。
多个运营商之间的员工信息是相互隔绝的

数据库结构
用户表:

CREATE TABLE `tab_user` (
  `id` bigint(18) NOT NULL COMMENT '主键',
  `store_id` bigint(32) DEFAULT NULL COMMENT '门店Id',
  `enterprise_id` bigint(18) NOT NULL COMMENT '商户号',
  `username` varchar(36) DEFAULT NULL COMMENT '登录名称',
  `real_name` varchar(36) DEFAULT NULL COMMENT '真实姓名',
  `password` varchar(150) DEFAULT NULL COMMENT '密码',
  `sex` varchar(11) DEFAULT NULL COMMENT '性别',
  `mobil` varchar(36) DEFAULT NULL COMMENT '电话',
  `email` varchar(36) DEFAULT NULL COMMENT '邮箱',
  `discount_limit` decimal(10,2) DEFAULT NULL COMMENT '折扣上线',
  `reduce_limit` decimal(10,2) DEFAULT NULL COMMENT '减免金额上线',
  `duties` varchar(36) DEFAULT NULL COMMENT '职务',
  `sort_no` int(11) DEFAULT NULL COMMENT '排序',
  `enable_flag` varchar(18) DEFAULT NULL COMMENT '是否有效',
  `created_time` datetime DEFAULT NULL COMMENT '创建时间',
  `updated_time` datetime DEFAULT NULL COMMENT '创建时间',
  `sharding_id` bigint(18) DEFAULT NULL COMMENT '分库id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='用户表';

用户角色表

CREATE TABLE `tab_user_role` (
  `id` bigint(36) NOT NULL,
  `enable_flag` varchar(18) DEFAULT NULL,
  `user_id` bigint(18) DEFAULT NULL,
  `role_id` bigint(18) DEFAULT NULL,
  `created_time` datetime DEFAULT NULL COMMENT '创建时间',
  `updated_time` datetime DEFAULT NULL COMMENT '创建时间',
  `sharding_id` bigint(18) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='用户角色表';

统一权限认证

认证:判断一个用户是否为合法用户的处理过程,最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户和口令一致,来判断用户身份是否正确,如下图所示:
在这里插入图片描述
集成的方式:本权限是基于spring-cloud-gateway网关来做权限的控制,因为gateway是基于响应式webflux【响应式编程】的机制进行处理的,所以这里的用法和原本httpservlet是有所区别的,首先我们来看一下整体的模块依赖处理:
在这里插入图片描述
商家发起请求到gateway-shop网关
gateway网关调用model-security-client,进行认证或鉴权过滤器
model-security-client作为服务消费者通过model-security-interface接口进行用户认证、鉴权接口调用
model-security-producer作为服务生产者进行当前用户登录、角色、权限的查询
model-security-client:模块是本权限系统核心,他提供了具体的认证、鉴权的逻辑,如果一个gateway想要实现权限的控制只需要依赖此客户端
在这里插入图片描述
在这里插入图片描述

认证流程总述

在这里插入图片描述
认证总体流程如下:
用户在登录页选择登录方式
判断登录方式是短信登录、账号密码登录,进行域名校验,兑换企业ID【enterpriseid】
通过服务鉴权转换器ServerAuthenticationConverter构建权限对象Authentication
Authentication对象交于认证管理器【ReactiveAuthenticationManager】进行认证

服务鉴权转换器
在这里插入图片描述
ServerAuthenticationConverter:主要是负责表单的自动转换,在spring-security中的默认的登录页面是long页面,我们需要从表单中获取用户名和密码或者用户短信验证码

package com.xxxx.restkeeper.converter;

import com.itheima.restkeeper.converter.LoginConverter;
import com.itheima.restkeeper.utils.EmptyUtil;
import com.itheima.restkeeper.utils.RegisterBeanHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @ClassName ReactiveFormLoginAuthenticationConverter.java
 * @Description 自定义表单转换
 */
@Component
public class ReactiveServerAuthenticationConverter implements ServerAuthenticationConverter {

    //登录方式
    private String loginTypeParameter = "loginType";

    //站点类型
    private String siteTypeParameter = "siteType";

    @Autowired
    RegisterBeanHandler registerBeanHandler;

    @Override
    public Mono<Authentication> convert(ServerWebExchange exchange) {
        String loginType = exchange.getRequest().getHeaders().getFirst("loginType");
        String siteType = exchange.getRequest().getHeaders().getFirst("siteType");
        if (EmptyUtil.isNullOrEmpty(loginType)){
            throw  new BadCredentialsException("客户登陆异常");
        }
        LoginConverter loginConverter = registerBeanHandler.getBean(loginType, LoginConverter.class);
        return loginConverter.convert(exchange,loginType,siteType);
    }
}

**LoginConverter:**登录转换接口定义

import org.springframework.security.core.Authentication;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @ClassName LoginTypeConverterHandler.java
 * @Description 登录类型转换接口
 */
public interface LoginConverter {

    /***
     * @description 登录转换
     * @param exchange
     * @param loginType
     * @param siteType
     * @return
     */
    public Mono<Authentication> convert(ServerWebExchange exchange,
                                        String loginType,
                                        String siteType);
}

**MobilLoginConverter:**手机验证码登录转换器

import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @ClassName SystemMobilLoginConverter.java
 * @Description 手机登录
 */
@Component("mobilLogin")
public class MobilLoginConverter implements LoginConverter {

    //手机
    private String mobileParameter = "mobile";

    //验证码
    private String authCodeParameter = "authCode";

    @Autowired
    RedissonClient redissonClient;

    @Override
    public Mono<Authentication> convert(ServerWebExchange exchange,
                                        String loginType,
                                        String siteType) {
        String hostName = exchange.getRequest().getURI().getHost();
        String key = null;
        if (siteType.equals(SuperConstant.WEBSITE)){
            key = SecurityCacheConstant.WEBSITE+hostName;
        }else if (siteType.equals(SuperConstant.APP_WEBSITE)){
            key = SecurityCacheConstant.APP_WEBSITE+hostName;
        }else {
            return  Mono.error(new BadCredentialsException("站点类型未定义"));
        }
        //域名校验
        RBucket<EnterpriseVo> bucket = redissonClient.getBucket(key);
        EnterpriseVo enterpriseVo = bucket.get();
        if (EmptyUtil.isNullOrEmpty(enterpriseVo)){
            return  Mono.error(new BadCredentialsException("Invalid hostName"));
        }
        //获得enterpriseId
        String enterpriseId = String.valueOf(enterpriseVo.getEnterpriseId());
        return exchange.getFormData().map( data -> {
            String mobile = data.getFirst(this.mobileParameter);
            String authCode = data.getFirst(this.authCodeParameter);
            if (EmptyUtil.isNullOrEmpty(mobile)||
                    EmptyUtil.isNullOrEmpty(authCode)){
                throw  new BadCredentialsException("客户登陆异常");
            }
            String principal = mobile+":"+enterpriseId+":"+loginType+":"+siteType;
            return new UsernamePasswordAuthenticationToken(principal, authCode);
        });
    }
}

**UsernameLoginConverter:**用户名密码登录

import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @ClassName SysUsernameLoginConverterHandler.java
 * @Description 系统账号密码转换
 */
@Component("usernameLogin")
public class UsernameLoginConverter implements LoginConverter {

    //账号
    private String usernameParameter = "username";

    //密码
    private String passwordParameter = "password";

    @Autowired
    RedissonClient redissonClient;

    @Override
    public Mono<Authentication> convert(ServerWebExchange exchange,
                                        String loginType,
                                        String siteType) {
        String hostName = exchange.getRequest().getURI().getHost();
        String key = null;
        if (siteType.equals(SuperConstant.WEBSITE)){
            key = SecurityCacheConstant.WEBSITE+hostName;
        }else if (siteType.equals(SuperConstant.APP_WEBSITE)){
            key = SecurityCacheConstant.APP_WEBSITE+hostName;
        }else {
            return  Mono.error(new BadCredentialsException("站点类型未定义"));
        }
        //域名校验
        RBucket<EnterpriseVo> bucket = redissonClient.getBucket(key);
        EnterpriseVo enterpriseVo = bucket.get();
        if (EmptyUtil.isNullOrEmpty(enterpriseVo)){
            return  Mono.error(new BadCredentialsException("Invalid hostName"));
        }
        //获得enterpriseId
        String enterpriseId = String.valueOf(enterpriseVo.getEnterpriseId());
        return exchange.getFormData().map( data -> {
            String username = data.getFirst(this.usernameParameter);
            String password = data.getFirst(this.passwordParameter);
            if (EmptyUtil.isNullOrEmpty(username)||
                EmptyUtil.isNullOrEmpty(password)){
                throw  new BadCredentialsException("用户登陆异常");
            }
            String principal = username+":"+enterpriseId+":"+loginType+":"+siteType;
            return new UsernamePasswordAuthenticationToken(principal, password);
        });
    }
}

用户信息明细

在这里插入图片描述
ReactiveUserDetailsServiceImpl:主要负责认证过程,对于用户信息的获得,这里分为四种获得方式
user账户登录
user手机登录
customer账户登录
customer手机登录
统一调用UserAdapterFace或者CustomerAdapterFace进行用户登录消息的获得方式

import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.util.HashSet;

/**
 * @ClassName ReactiveUserDetailsServiceImpl.java
 * @Description 支持flum的身份类实现ReactiveUserDetailsService接口
 */
@Component("reactiveUserDetailsService")
@Slf4j
public class ReactiveUserDetailsServiceImpl implements ReactiveUserDetailsService{

    //调用RPC原创服务
    @DubboReference(version = "${dubbo.application.version}", check = false)
    UserAdapterFace userAdapterFace;

    //调用RPC原创服务
    @DubboReference(version = "${dubbo.application.version}", check = false)
    CustomerAdapterFace customerAdapterFace;

    //验证身份
    @Override
    public Mono<UserDetails> findByUsername(String principal) {
        String[] principals = principal.split(":");
        if (principals.length!=4){
            log.warn("用户:{}登录信息不完整",principal);
            return Mono.empty();
        }
        String mobile =principals[0];
        String username =principals[0];
        Long enterpriseId =Long.valueOf(principals[1]);
        String loginType =principals[2];
        String siteType =principals[3];
        UserVo userVo = null;
        //user账户登录
        if (loginType.equals(SuperConstant.USERNAME_LOGIN)
            &&siteType.equals(SuperConstant.WEBSITE)){
            userVo = userAdapterFace.findUserByUsernameAndEnterpriseId(username, enterpriseId);
        }
        //user手机登录
        if (loginType.equals(SuperConstant.MOBIL_LOGIN)
            &&siteType.equals(SuperConstant.WEBSITE)){
            userVo = userAdapterFace.findUserByMobilAndEnterpriseId(mobile, enterpriseId);
        }
        //customer账户登录
        if (loginType.equals(SuperConstant.USERNAME_LOGIN)
            &&siteType.equals(SuperConstant.APP_WEBSITE)){
            userVo = customerAdapterFace.findCustomerByUsernameAndEnterpriseId(username, enterpriseId);
        }
        //customer手机登录
        if (loginType.equals(SuperConstant.MOBIL_LOGIN)
            &&siteType.equals(SuperConstant.APP_WEBSITE)){
            userVo = customerAdapterFace.findCustomerByMobilAndEnterpriseId(mobile, enterpriseId);
        }
        if (EmptyUtil.isNullOrEmpty(userVo)){
            log.warn("用户:{}不存在",principal);
            return Mono.empty();
        }
        UserAuth userAuth = new UserAuth(
            userVo.getUsername(),
            userVo.getPassword(),
            new HashSet<>(),
            userVo.getId(),
            userVo.getShardingId(),
            userVo.getEnterpriseId(),
            userVo.getStoreId(),
            userVo.getJwtToken(),
            userVo.getRealName(),
            userVo.getSex(),
            userVo.getMobil(),
            userVo.getEmail(),
            userVo.getDiscountLimit(),
            userVo.getReduceLimit(),
            userVo.getDuties(),
            userVo.getCreatedTime(),
            userVo.getUpdatedTime()
        );
        return Mono.just(userAuth);
    }
}

认证管理器
在这里插入图片描述
**JwtReactiveAuthenticationManager:**查询用户明细信息之后,与请求传递过来的密码或者短信验证码进行比对,如果比对成功则表示登录成功

import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;

/**
 * @ClassName JwtUserDetailsRepositoryReactiveAuthenticationManager.java
 * @Description 认证管理器
 */
@Component
public class JwtReactiveAuthenticationManager implements ReactiveAuthenticationManager {

    //密码编辑者
    private PasswordEncoder passwordEncoder = PasswordEncoderFactories
            .createDelegatingPasswordEncoder();

    //调度程序
    private Scheduler scheduler = Schedulers.parallel();

    //用户明细信息服务
    @Autowired
    private ReactiveUserDetailsService reactiveUserDetailsService;

    @Autowired
    RedissonClient redissonClient;

    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {
        final String principal = authentication.getName();
        final String password = (String) authentication.getCredentials();
        String[] principals = principal.split(":");
        //mobile+":"+enterpriseId+":"+loginType+":"+siteType
        String mobile =principals[0];
        String enterpriseId =principals[1];
        String loginType =principals[2];
        String siteType =principals[3];
        Mono<UserDetails> userDetailsMono = this.reactiveUserDetailsService.findByUsername(principal);
        //密码校验
        if (loginType.equals(SuperConstant.USERNAME_LOGIN)){
            return userDetailsMono.publishOn(this.scheduler)
                //密码比较
                .filter(u -> this.passwordEncoder.matches(password, u.getPassword()))
                //失败处理
                .switchIfEmpty(Mono.defer(()->
                    Mono.error(new BadCredentialsException("Invalid Credentials"))))
                //成功处理
                .map(u ->
                    new UsernamePasswordAuthenticationToken(u, u.getPassword(), u.getAuthorities()));
        }
        //短信校验
        if (loginType.equals(SuperConstant.MOBIL_LOGIN)){
            //redis中获得验证码
            String key = SmsCacheConstant.LOGIN_CODE+principals[0];
            RBucket<String> bucket = redissonClient.getBucket(key);
            String authCode = bucket.get();
            if (EmptyUtil.isNullOrEmpty(authCode)){
                Mono.error(new BadCredentialsException("Invalid Credentials"));
            }
            return userDetailsMono.publishOn(this.scheduler)
                //密码比较
                .filter(u -> authCode.equals(password))
                //失败处理
                .switchIfEmpty(Mono.defer(()->
                    Mono.error(new BadCredentialsException("Invalid Credentials"))))
                //成功处理
                .map(u ->
                    new UsernamePasswordAuthenticationToken(u, u.getPassword(), u.getAuthorities()));
        }
        throw new BadCredentialsException("Invalid Credentials");
    }

    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
        this.passwordEncoder = passwordEncoder;
    }

}

认证成功
在这里插入图片描述

import io.netty.util.CharsetUtil;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.util.*;

/**
 * @ClassName JsonServerAuthenticationSuccessHandler.java
 * @Description 登录成功handler
 */
@Component
public class JsonServerAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {

    @Autowired
    JwtTokenManager jwtTokenManager;

    @DubboReference(version = "${dubbo.application.version}", check = false)
    UserAdapterFace userAdapterFace;

    @Override
    public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, 
                                              Authentication authentication) {
        ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
        response.setStatusCode(HttpStatus.OK);
        response.getHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8");
        UserAuth authUser = (UserAuth) authentication.getPrincipal();
        //构建userVo返回对象
        UserVo userVo = UserVo.builder()
                .id(authUser.getId())
                .username(authUser.getUsername())
                .reduceLimit(authUser.getReduceLimit())
                .discountLimit(authUser.getDiscountLimit())
                .enterpriseId(authUser.getEnterpriseId())
                .storeId(authUser.getStoreId())
                .build();
        //处理角色构建
        List<RoleVo> roleByUserId = userAdapterFace.findRoleByUserId(userVo.getId());
        Set<String> roles =  new HashSet<>();
        for (RoleVo roleVo : roleByUserId) {
            roles.add(roleVo.getLabel());
        }
        //处理资源构建
        List<ResourceVo> resourceByUserId = userAdapterFace.findResourceByUserId(userVo.getId());
        Set<String> resources = new HashSet<>();
        for (ResourceVo resourceVo : resourceByUserId) {
            resources.add(resourceVo.getRequestPath());
        }
        //用户指定角色、资源
        userVo.setRoles(roles);
        userVo.setResources(resources);
        //构建JWT令牌
        Map<String,Object> claims = new HashMap<>();
        String userVoJsonString= JSONObject.toJSONString(userVo);
        claims.put("currentUser",userVoJsonString);
        String jwtToken = jwtTokenManager.issuedToken("system",
                jwtTokenManager.getJwtProperties().getTtl(),
                authUser.getId().toString(),
                claims);
        userVo.setJwtToken(jwtToken);
        //返回信息给前端
        ResponseWrap<UserVo> responseWrap = ResponseWrapBuild.build(AuthEnum.SUCCEED, userVo);
        String result = JSONObject.toJSONString(responseWrap);
        DataBuffer buffer = response.bufferFactory().wrap(result.getBytes(CharsetUtil.UTF_8));
        return response.writeWith(Mono.just(buffer));
    }
}

认证失败
认证失败:只需要返回错误信息即可

import io.netty.util.CharsetUtil;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

/**
 * @ClassName JsonServerAuthenticationFailureHandler.java
 * @Description 登录失败
 */
@Component
public class JsonServerAuthenticationFailureHandler implements ServerAuthenticationFailureHandler {

    @Override
    public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
        //指定应答状态
        ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
        response.setStatusCode(HttpStatus.OK);
        response.getHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8");
        //返回信息给前段
        ResponseWrap<UserAuth> responseWrap = ResponseWrapBuild.build(AuthEnum.FAIL, null);
        String result = JSONObject.toJSONString(responseWrap);
        DataBuffer buffer = response.bufferFactory().wrap(result.getBytes(CharsetUtil.UTF_8));
        return response.writeWith(Mono.just(buffer));
    }
}
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐