Loading

品达通用权限系统-四

16. 数据模型

16.1 权限数据模型介绍

在项目中要进行权限控制,需要有一套权限相关的数据表来提供支持,这是整个权限控制的基础。本系统采用的权限数据模型是在经典的RBAC权限数据模型的基础之上进行的改进,共涉及到如下9张表:

pd_core_org----------------组织表
pd_core_station------------岗位表
pd_auth_user---------------用户表
pd_auth_role---------------角色表
pd_auth_resource-----------资源表
pd_auth_menu---------------菜单表
pd_auth_user_role----------用户角色关系表
pd_auth_role_authority-----角色权限关系表
pd_auth_role_org-----------角色组织关系表

需要说明的是菜单和资源其实都属于权限,是两种不同类型的权限,即菜单权限和资源权限。具体说明如下:

16.2 导入表结构

在MySQL中创建pd-auth数据库,在此数据库中执行授课资料中的"pd_auth.sql"脚本即可。执行完后可以看到如下11张表:

16.2.1 pd_common_login_log表

pd_common_login_log为用户登录日志表,具体的字段如下:

字段名 类型 说明
id bigint 主键
request_ip varchar 操作IP
user_id bigint 登录人ID
user_name varchar 登录人姓名
account varchar 登录人账号
description varchar 登录描述
login_date date 登录时间
ua varchar 浏览器请求头
browser varchar 浏览器名称
browser_version varchar 浏览器版本
operating_system varchar 操作系统
location varchar 登录地点
create_time datetime 创建时间
create_user bigint 创建人ID

16.2.2 pd_common_opt_log表

pd_common_opt_log为用户操作日志表,具体字段如下:

字段名 类型 说明
id bigint 主键
request_ip varchar 操作IP
type varchar 日志类型 OPT:操作类型 EX:异常类型
user_name varchar 操作人
description varchar 操作描述
class_path varchar 类路径
action_method varchar 请求方法
request_uri varchar 请求地址
http_method varchar 请求类型 GET:GET请求;POST:POST请求;PUT:PUT请求;DELETE:DELETE请求;PATCH:PATCH请求;TRACE:TRACE请求;HEAD:HEAD请求;OPTIONS:OPTIONS请求
params longtext 请求参数
result longtext 返回值
ex_desc longtext 异常详情信息
ex_detail longtext 异常描述
start_time timestamp 开始时间
finish_time timestamp 完成时间
consuming_time bigint 消耗时间
ua varchar 浏览器请求头
create_time datetime 创建时间
create_user bigint 创建人ID

16.2.3 pd_auth_menu表

pd_auth_menu为菜单表,具体字段如下:

字段名 类型 说明
id bigint 主键
name varchar 菜单名称
describe_ varchar 功能描述
is_public bit 是否是公开菜单
path varchar 对应路由path
component varchar 对应路由组件component
is_enable bit 是否启用
sort_value int 排序
icon varchar 菜单图标
group_ varchar 菜单分组
parent_id bigint 父级菜单id
create_user bigint 创建人id
create_time datetime 创建时间
update_user bigint 更新人id
update_time datetime 更新时间

16.2.4 pd_auth_resource表

pd_auth_resource为资源表,具体字段如下:

字段名 类型 说明
id bigint 主键
code varchar 资源编码
name varchar 接口名称
menu_id bigint 菜单ID
method varchar HTTP请求方式
url varchar 接口请求url
describe_ varchar 接口描述
create_user bigint 创建人id
create_time datetime 创建时间
update_user bigint 更新人id
update_time datetime 更新时间

16.2.5 pd_auth_role表

pd_auth_role为角色表,具体字段如下:

字段名称 类型 说明
id bigint 主键
name varchar 角色名称
code varchar 角色编码
describe_ varchar 角色描述
status bit 是否启用状态
readonly bit 是否内置角色
create_user bigint 创建人id
create_time datetime 创建时间
update_user bigint 更新人id
update_time datetime 更新时间

16.2.6 pd_auth_user表

pd_auth_user表为用户表,具体字段如下:

字段名 类型 说明
id bigint 主键
account varchar 账号
name varchar 姓名
org_id bigint 组织ID
station_id bigint 岗位ID
email varchar 邮箱
mobile varchar 手机号
sex varchar 性别
status bit 启用状态
avatar varchar 头像
work_describe varchar 工作描述
password_error_last_time datetime 最后一次输错密码时间
password_error_num int 密码错误次数
password_expire_time datetime 密码过期时间
password varchar 密码
last_login_time datetime 最后登录时间
create_user bigint 创建人id
create_time datetime 创建时间
update_user bigint 更新人id
update_time datetime 更新时间

16.2.7 pd_core_station表

pd_core_station表为岗位表,具体字段如下:

字段名称 类型 说明
id bigint 主键
name varchar 岗位名称
org_id bigint 组织ID
status bit 是否启用状态
describe_ varchar 描述
create_time datetime 创建时间
create_user bigint 创建人ID
update_time datetime 更新时间
update_user bigint 更新人ID

16.2.8 pd_core_org表

pd_core_org表为组织表,具体字段如下:

字段名称 类型 说明
id bigint 主键
name varchar 组织名称
abbreviation varchar 简称
parent_id bigint 父ID
tree_path varchar 树结构
sort_value int 排序
status bit 状态
describe_ varchar 描述
create_time datetime 创建时间
create_user bigint 创建人ID
update_time datetime 更新时间
update_user bigint 更新人ID

16.2.9 pd_auth_user_role表

pd_auth_user_role为用户角色关系表,具体字段为:

字段名称 类型 说明
id bigint 主键
role_id bigint 角色ID
user_id bigint 用户ID
create_user bigint 创建人ID
create_time datetime 创建时间

16.2.10 pd_auth_role_org表

pd_auth_role_org为角色组织关系表,具体字段为:

字段名称 类型 说明
id bigint 主键
role_id bigint 角色ID
org_id bigint 组织ID
create_time datetime 创建时间
create_user bigint 创建人ID

16.2.11 pd_auth_role_authority表

pd_auth_role_authority为角色权限关系表,具体字段为:

字段名称 类型 说明
id bigint 主键
authority_id bigint 权限ID
authority_type varchar 权限类型 MENU:菜单 RESOURCE:资源
role_id bigint 角色ID
create_time datetime 创建时间
create_user bigint 创建人ID

16.3 导入实体类

前面我们已经介绍了通用权限系统中涉及到的数据表,一般在开发过程中我们会创建和数据表对应的实体类来封装相关信息。在课程资料中已经提供了相关实体类Entity和相关DTO,直接复制到pd-auth-entity工程中即可。

17. 认证和鉴权流程

品达通用权限系统对外提供的功能中认证鉴权是其核心功能,通过导入的初始工程可以发现其中有两个服务,即网关服务和权限服务。其中用户认证需要在权限服务中完成,鉴权需要在网关服务中完成。在实现认证和鉴权之前我们必须明确认证和鉴权的整体执行流程。

17.1 认证流程

1、用户通过前端系统发送登录请求,请求中携带账号、密码、验证码等信息。

2、前端登录请求首先请求到网关服务,网关服务将请求路由到权限微服务。

3、权限微服务进行认证操作,如果认证通过则生成jwt token返回给前端,同时将用户拥有的资源权限使用userId作为key保存到缓存中。

注:缓存中保存的用户资源权限是由pd_auth_resource资源表中的method和url两个字段的值拼接成的。例如,某个用户拥有删除日志的权限,在表中删除日志权限对应一条数据,其中method的值为DELETE,url的值为/optLog,那么缓存中保存的用户拥有的资源权限为:DELETE/optLog。

17.2 鉴权流程

1、用户认证后访问其他功能时将jwt token放在请求头中,首先经过网关服务处理。

2、在网关服务的过滤器中获取请求头中的token并进行解析,将解析出的用户相关数据放在zuul的header中。

注:之所以要将用户相关数据放在zuul的header中,是因为在后续的网关AccessFilter过滤器和权限服务中都会使用到这些数据。

3、在网关服务的过滤器中进行鉴权相关处理。

18. 权限服务开发

18.1 基础环境搭建

在开发权限服务的业务功能之前,我们需要进行基础环境的搭建,这是权限服务的基础。这些基础环境包括:配置文件、配置类、启动类等。

18.1.1 配置文件

18.1.1.1 bootstrap.yml

由于我们当前使用的是Nacos作为整个项目的配置中心,所以Spring Boot的大部分配置文件都在Nacos中进行统一配置,我们的项目中只需要按照Spring Boot的要求在resources目录下提供bootstrap.yml配置文件即可,文件内容如下:

# @xxx@ 从pom.xml中取值, 所以 @xx@ 标注的值,都不能从nacos中获取
pinda:
  nacos:
    ip: ${NACOS_IP:@pom.nacos.ip@}
    port: ${NACOS_PORT:@pom.nacos.port@}
    namespace: ${NACOS_ID:@pom.nacos.namespace@}

spring:
  main:
    allow-bean-definition-overriding: true
  application:
    name: @project.artifactId@
  profiles:
    active: @pom.profile.name@
  cloud:
    nacos:
      config:
        server-addr: ${pinda.nacos.ip}:${pinda.nacos.port}
        file-extension: yml
        namespace: ${pinda.nacos.namespace}
        shared-dataids: common.yml,redis.yml,mysql.yml
        refreshable-dataids: common.yml
        enabled: true
      discovery:
        server-addr: ${pinda.nacos.ip}:${pinda.nacos.port}
        namespace: ${pinda.nacos.namespace}
        metadata: # 元数据,用于权限服务实时获取各个服务的所有接口
          management.context-path: ${server.servlet.context-path:}${spring.mvc.servlet.path:}${management.endpoints.web.base-path:}
  aop:
    proxy-target-class: true
    auto: true

# 只能配置在bootstrap.yml ,否则会生成 log.path_IS_UNDEFINED 文件夹
# window会自动在 代码所在盘 根目录下自动创建文件夹,  如: D:/data/projects/logs
logging:
  file:
    path: /data/projects/logs
    name: ${logging.file.path}/${spring.application.name}/root.log

# 用于/actuator/info
info:
  name: '@project.name@'
  description: '@project.description@'
  version: '@project.version@'
  spring-boot-version: '@spring.boot.version@'
  spring-cloud-version: '@spring.cloud.version@'
18.1.1.2 logback-spring.xml

由于pd-auth-server已经添加了pd-tools-log模块的依赖,所以可以在项目中使用logback记录日志信息。在resources目录下提供logback-spring.xml配置文件,Spring Boot默认就可以加载到,文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="com/itheima/pinda/log/logback/pinda-defaults.xml"/>

    <springProfile name="test,docker,prod">
        <logger name="com.itheima.pinda.authority.controller" additivity="true" 
                level="${log.level.controller}">
            <appender-ref ref="ASYNC_CONTROLLER_APPENDER"/>
        </logger>
        <logger name="com.itheima.pinda.authority.biz.service" additivity="true" 
                level="${log.level.service}">
            <appender-ref ref="ASYNC_SERVICE_APPENDER"/>
        </logger>
        <logger name="com.itheima.pinda.authority.biz.dao" additivity="false" 
                level="${log.level.dao}">
            <appender-ref ref="ASYNC_DAO_APPENDER"/>
        </logger>
    </springProfile>

    <springProfile name="dev">
        <logger name="com.itheima.pinda.authority.controller" additivity="true" 
                level="${log.level.controller}">
            <appender-ref ref="CONTROLLER_APPENDER"/>
        </logger>
        <logger name="com.itheima.pinda.authority.biz.service" additivity="true" 
                level="${log.level.service}">
            <appender-ref ref="SERVICE_APPENDER"/>
        </logger>
    </springProfile>
</configuration>
18.1.1.3 j2cache配置文件

在当前pd-auth-server项目中会使用到j2cache来操作缓存,在Nacos配置中心的redis.yml中已经配置了j2cache的相关配置:

j2cache:
  #  config-location: /j2cache.properties
  open-spring-cache: true
  cache-clean-mode: passive
  allow-null-values: true
  redis-client: lettuce
  l2-cache-open: true
  # l2-cache-open: false     # 关闭二级缓存
  broadcast: net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy
  #  broadcast: jgroups       # 关闭二级缓存
  L1:
    provider_class: caffeine
  L2:
    provider_class: net.oschina.j2cache.cache.support.redis.SpringRedisProvider
    config_section: lettuce
  sync_ttl_to_redis: true
  default_cache_null_object: false
  serialization: fst
caffeine:
  properties: /j2cache/caffeine.properties   # 这个配置文件需要放在项目中
lettuce:
  mode: single
  namespace:
  storage: generic
  channel: j2cache
  scheme: redis
  hosts: ${pinda.redis.ip}:${pinda.redis.port}
  password: ${pinda.redis.password}
  database: ${pinda.redis.database}
  sentinelMasterId:
  maxTotal: 100
  maxIdle: 10
  minIdle: 10
  timeout: 10000

通过上面的配置可以看到,还需要在项目中提供/j2cache/caffeine.properties,文件内容如下:

#########################################
# Caffeine configuration
# \u6682\u65F6\u6CA1\u7528
# [name] = size, xxxx[s|m|h|d]
#########################################
default=2000, 2h
captcha=1000, 5m
resource=2000, 2h
user_resource=3000, 2h
18.1.1.4 密钥文件
JWT签名算法中,一般有两个选择:HS256和RS256。

HS256 (带有 SHA-256 的 HMAC )是一种对称加密算法, 双方之间仅共享一个密钥。由于使用相同的密钥生成签名和验证签名, 因此必须注意确保密钥不被泄密。

RS256 (采用SHA-256 的 RSA 签名) 是一种非对称加密算法, 它使用公共/私钥对: JWT的提供方采用私钥生成签名, JWT 的使用方获取公钥以验证签名。

本项目中使用RS256非对称加密算法进行签名,这就需要使用RSA生成一对公钥和私钥。在授课资料中已经提供了一对公钥和私钥,其中pub.key为公钥,pri.key为私钥。

将授课资料中的pub.key和pri.key文件复制到项目的resources/client下。

注意:为什么必须要将这两个文件复制到项目的resources/client下呢?因为在Nacos配置中心的pd-auth-server.yml中通过配置的形式已经指定了这两个配置文件的位置和名称:

authentication:
  user:
    header-name: token
    expire: 43200               # 外部token有效期为12小时
    pri-key: client/pri.key    # 加密
    pub-key: client/pub.key    # 解密
18.1.1.5 spy.properties

spy.properties是p6spy所需的属性文件。p6spy是一个开源项目,通常使用它来跟踪数据库操作,查看程序运行过程中执行的sql语句,还可以输出执行sql语句消耗的时间。

在Nacos配置中心的pd-auth-server-dev.yml中进行了如下配置:

# p6spy是一个开源项目,通常使用它来跟踪数据库操作,查看程序运行过程中执行的sql语句
# 开发环境需要使用p6spy进行sql语句输出
# 但p6spy会有性能损耗,不适合在生产线使用,故其他环境无需配置
spring:
  datasource:
    driver-class-name: com.p6spy.engine.spy.P6SpyDriver
    url: jdbc:p6spy:mysql://${pinda.mysql.ip}:${pinda.mysql.port}/${pinda.mysql.database}?serverTimezone=CTT&characterEncoding=utf8&useUnicode=true&useSSL=false&autoReconnect=true&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
    db-type: mysql

我们在开发阶段使用的数据源其实就是P6Spy提供的数据源,这样就可以在控制台打印sql已经sql执行的时间了。

spy.properties配置文件内容如下:

module.log=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
deregisterdrivers=true
useprefix=true
excludecategories=info,debug,result,commit,resultset
dateformat=yyyy-MM-dd HH:mm:ss
driverlist=com.mysql.cj.jdbc.Driver
outagedetection=true
outagedetectioninterval=2
18.1.1.6 dozer

在resources下创建dozer目录并提供biz.dozer.xml和global.dozer.xml文件,内容如下:

biz.dozer.xml

<?xml version="1.0" encoding="UTF-8"?>
<mappings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xmlns="http://dozermapper.github.io/schema/bean-mapping"
          xsi:schemaLocation="http://dozermapper.github.io/schema/bean-mapping
                             http://dozermapper.github.io/schema/bean-mapping.xsd">
    <mapping date-format="yyyy-MM-dd HH:mm:ss">
        <class-a>com.itheima.pinda.authority.entity.auth.Menu</class-a>
        <class-b>com.itheima.pinda.authority.dto.auth.VueRouter</class-b>
        <field>
            <a>name</a>
            <b>meta.title</b>
        </field>
        <field>
            <a>name</a>
            <b>name</b>
        </field>
        <field>
            <a>icon</a>
            <b>meta.icon</b>
        </field>
    </mapping>
</mappings>

global.dozer.xml

<?xml version="1.0" encoding="UTF-8"?>
<mappings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xmlns="http://dozermapper.github.io/schema/bean-mapping"
          xsi:schemaLocation="http://dozermapper.github.io/schema/bean-mapping 
                              http://dozermapper.github.io/schema/bean-mapping.xsd">
    <!--
    @see: http://www.jianshu.com/p/bf8f0e8aee23
    @see: http://blog.csdn.net/whhahyy/article/details/48594657
    全局配置:
    <date-format>表示日期格式
    <stop-on-errors>错误处理开关
    <wildcard>通配符
    <trim-strings>裁剪字符串开关
     -->
    <configuration>
        <date-format>yyyy-MM-dd HH:mm:ss</date-format>
    </configuration>
</mappings>

18.1.2 配置类

全局异常处理的配置类:

package com.itheima.pinda.authority.config;

import com.itheima.pinda.common.handler.DefaultGlobalExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

/**
 * 全局异常处理
 */
@Configuration
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
public class ExceptionConfiguration extends DefaultGlobalExceptionHandler {
}

公共基础的配置类:

package com.itheima.pinda.authority.config;
import com.itheima.pinda.common.config.BaseConfig;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AuthorityWebConfiguration extends BaseConfig {
}

数据库相关的配置类:

package com.itheima.pinda.authority.config;

import cn.hutool.core.util.ArrayUtil;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusProperties;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusPropertiesCustomizer;
import com.itheima.pinda.database.datasource.BaseDatabaseConfiguration;
import com.itheima.pinda.database.properties.DatabaseProperties;
import com.p6spy.engine.spy.P6DataSource;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.mapping.DatabaseIdProvider;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.scripting.LanguageDriver;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.TypeHandler;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.aop.Advisor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ResourceLoader;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.interceptor.TransactionInterceptor;
import javax.sql.DataSource;
import java.util.List;

@Configuration
@Slf4j
@MapperScan(
        basePackages = {"com.itheima.pinda",},
        annotationClass = Repository.class,
        sqlSessionFactoryRef = AuthorityDatabaseAutoConfiguration.DATABASE_PREFIX + "SqlSessionFactory")
@EnableConfigurationProperties({MybatisPlusProperties.class, DatabaseProperties.class})
public class AuthorityDatabaseAutoConfiguration extends BaseDatabaseConfiguration {
    /**
     * 每个数据源配置不同即可
     */
    final static String DATABASE_PREFIX = "master";

    public AuthorityDatabaseAutoConfiguration(MybatisPlusProperties properties,
                                              DatabaseProperties databaseProperties,
                                              ObjectProvider<Interceptor[]> interceptorsProvider,
                                              ObjectProvider<TypeHandler[]> typeHandlersProvider,
                                              ObjectProvider<LanguageDriver[]> languageDriversProvider,
                                              ResourceLoader resourceLoader,
                                              ObjectProvider<DatabaseIdProvider> databaseIdProvider,
                                              ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider,
                                              ObjectProvider<List<MybatisPlusPropertiesCustomizer>> mybatisPlusPropertiesCustomizerProvider,
                                              ApplicationContext applicationContext) {
        super(properties, databaseProperties, interceptorsProvider, typeHandlersProvider,
                languageDriversProvider, resourceLoader, databaseIdProvider,
                configurationCustomizersProvider, mybatisPlusPropertiesCustomizerProvider, applicationContext);
    }

    @Bean(DATABASE_PREFIX + "SqlSessionTemplate")
    public SqlSessionTemplate getSqlSessionTemplate(@Qualifier(DATABASE_PREFIX + "SqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        ExecutorType executorType = this.properties.getExecutorType();
        if (executorType != null) {
            return new SqlSessionTemplate(sqlSessionFactory, executorType);
        } else {
            return new SqlSessionTemplate(sqlSessionFactory);
        }
    }

    /**
     * 数据源信息
     *
     * @return
     */
    @Bean(name = DATABASE_PREFIX + "DruidDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.druid")
    public DataSource druidDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = DATABASE_PREFIX + "DataSource")
    public DataSource dataSource(@Qualifier(DATABASE_PREFIX + "DruidDataSource") DataSource dataSource) {
        if (ArrayUtil.contains(DEV_PROFILES, this.profiles)) {
            return new P6DataSource(dataSource);
        } else {
            return dataSource;
        }
    }

    /**
     * mybatis Sql Session 工厂
     *
     * @return
     * @throws Exception
     */
    @Bean(DATABASE_PREFIX + "SqlSessionFactory")
    public SqlSessionFactory getSqlSessionFactory(@Qualifier(DATABASE_PREFIX + "DataSource") DataSource dataSource) throws Exception {
        return super.sqlSessionFactory(dataSource);
    }

    /**
     * 数据源事务管理器
     *
     * @return
     */
    @Bean(name = DATABASE_PREFIX + "TransactionManager")
    public DataSourceTransactionManager dsTransactionManager(@Qualifier(DATABASE_PREFIX + "DataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    /**
     * 事务拦截器
     *
     * @param transactionManager
     * @return
     */
    @Bean(DATABASE_PREFIX + "TransactionInterceptor")
    public TransactionInterceptor transactionInterceptor(@Qualifier(DATABASE_PREFIX + "TransactionManager") PlatformTransactionManager transactionManager) {
        return new TransactionInterceptor(transactionManager, this.transactionAttributeSource());
    }

    /**
     * 事务 Advisor
     *
     * @param transactionManager
     * @return
     */
    @Bean(DATABASE_PREFIX + "Advisor")
    public Advisor getAdvisor(@Qualifier(DATABASE_PREFIX + "TransactionManager") PlatformTransactionManager transactionManager, @Qualifier(DATABASE_PREFIX + "TransactionInterceptor") TransactionInterceptor ti) {
        return super.txAdviceAdvisor(ti);
    }

}

mybatis框架相关的配置类:

package com.itheima.pinda.authority.config;

import com.itheima.pinda.database.datasource.BaseMybatisConfiguration;
import com.itheima.pinda.database.properties.DatabaseProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
/**
 * Mybatis相关配置
 */
@Configuration
@Slf4j
public class AuthorityMybatisAutoConfiguration extends BaseMybatisConfiguration {
    public AuthorityMybatisAutoConfiguration(DatabaseProperties databaseProperties) {
        super(databaseProperties);
    }
}

18.1.3 启动类

package com.itheima.pinda;

import com.itheima.pinda.auth.server.EnableAuthServer;
import com.itheima.pinda.user.annotation.EnableLoginArgResolver;
import com.itheima.pinda.validator.config.EnableFormValidator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.env.Environment;
import java.net.InetAddress;
import java.net.UnknownHostException;

@SpringBootApplication
@EnableDiscoveryClient
@EnableAuthServer
@EnableFeignClients(value = {
        "com.itheima.pinda",
})
@Slf4j
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
@EnableLoginArgResolver
@EnableFormValidator
public class AuthorityApplication {
    public static void main(String[] args) throws UnknownHostException {
        ConfigurableApplicationContext application = 
            SpringApplication.run(AuthorityApplication.class, args);
        Environment env = application.getEnvironment();
        log.info("应用 '{}' 运行成功!  Swagger文档: http://{}:{}/doc.html",
                env.getProperty("spring.application.name"),
                InetAddress.getLocalHost().getHostAddress(),
                env.getProperty("server.port"));
    }
}

18.2 开发认证功能

18.2.1 easy-captcha

easy-captcha是生成图形验证码的Java类库,支持gif、中文、算术等类型,可用于Java Web、JavaSE等项目。参考地址:https://gitee.com/whvse/EasyCaptcha

maven坐标:

<dependency>
    <groupId>com.github.whvcse</groupId>
    <artifactId>easy-captcha</artifactId>
    <version>1.6.2</version>
</dependency>

效果展示:

使用方式:

package com.itheima.pinda;

import com.wf.captcha.ArithmeticCaptcha;
import com.wf.captcha.base.Captcha;
import java.io.File;
import java.io.FileOutputStream;

public class EasyCaptchaTest {
    public static void main(String[] args) throws Exception{
        //算术类型图片验证码
        Captcha captcha = new ArithmeticCaptcha(115, 42);//指定图片的宽度和高度
        captcha.setCharType(2);
        captcha.out(new FileOutputStream(new File("d:\\hello.png")));
        String text = captcha.text();
        System.out.println(text);
    }
}

18.2.2 开发验证码接口

第一步:创建LoginController并提供生成验证码的方法

package com.itheima.pinda.authority.controller.auth;
import com.itheima.pinda.authority.biz.service.auth.ValidateCodeService;
import com.itheima.pinda.base.BaseController;
import com.itheima.pinda.base.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
 * 登录
 */
@RestController
@RequestMapping("/anno")
@Api(value = "UserAuthController", tags = "登录")
@Slf4j
public class LoginController extends BaseController {
    @Autowired
    private ValidateCodeService validateCodeService;

    @ApiOperation(value = "验证码", notes = "验证码")
    @GetMapping(value = "/captcha", produces = "image/png")
    public void captcha(@RequestParam(value = "key") String key, 
                        HttpServletResponse response) throws IOException {
        this.validateCodeService.create(key, response);
    }
}

第二步:创建ValidateCodeService接口

package com.itheima.pinda.authority.biz.service.auth;
import java.io.IOException;
import javax.servlet.http.HttpServletResponse;
/**
 * 验证码
 */
public interface ValidateCodeService {
    /**
     * 生成验证码
     */
    void create(String key, HttpServletResponse response) throws IOException;
}

第三步:创建ValidateCodeServiceImpl

package com.itheima.pinda.authority.biz.service.auth.impl;
import java.io.IOException;
import javax.servlet.http.HttpServletResponse;
import com.itheima.pinda.authority.biz.service.auth.ValidateCodeService;
import com.itheima.pinda.common.constant.CacheKey;
import com.itheima.pinda.exception.BizException;
import com.wf.captcha.ArithmeticCaptcha;
import net.oschina.j2cache.CacheChannel;
import net.oschina.j2cache.CacheObject;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
/**
 * 验证码服务
 */
@Service
public class ValidateCodeServiceImpl implements ValidateCodeService {
    @Autowired
    private CacheChannel cache;

    @Override
    public void create(String key, 
                       HttpServletResponse response) throws IOException {
        if (StringUtils.isBlank(key)) {
            throw BizException.validFail("验证码key不能为空");
        }

        response.setContentType(MediaType.IMAGE_PNG_VALUE);
        response.setHeader(HttpHeaders.PRAGMA, "No-cache");
        response.setHeader(HttpHeaders.CACHE_CONTROL, "No-cache");
        response.setDateHeader(HttpHeaders.EXPIRES, 0L);

        Captcha captcha = new ArithmeticCaptcha(115, 42);
        captcha.setCharType(2);

        cache.set(CacheKey.CAPTCHA, key, StringUtils.lowerCase(captcha.text()));
        captcha.out(response.getOutputStream());
    }
}

验证码接口开发完成后可以启动服务,通过接口文档进行测试:

可以看到已经将验证码缓存到redis:

18.2.3 开发认证接口

第一步:在LoginController中创建login方法

@Autowired
private AuthManager authManager;//认证管理器对象

/**
 * 登录认证
*/
@ApiOperation(value = "登录", notes = "登录")
@PostMapping(value = "/login")
public R<LoginDTO> login(@Validated @RequestBody LoginParamDTO login) 
    throws BizException {
    log.info("account={}", login.getAccount());
    if (this.validateCodeService.check(login.getKey(), login.getCode())) {
        return this.authManager.login(login.getAccount(), login.getPassword());
    }
    return this.success(null);
}

第二步:在ValidateCodeService接口中扩展check方法完成校验验证码

/**
* 校验验证码
* @param key   前端上送 key
* @param value 前端上送待校验值
*/
boolean check(String key, String value);

第三步:在ValidateCodeServiceImpl实现类中实现check方法

//校验验证码
@Override
public boolean check(String key, String value) {
    if (StringUtils.isBlank(value)) {
        throw BizException.validFail("请输入验证码");
    }
    //根据key从缓存中获取验证码
    CacheObject cacheObject = cache.get(CacheKey.CAPTCHA, key);
    if (cacheObject.getValue() == null) {
        throw BizException.validFail("验证码已过期");
    }
    //比对验证码
    if (!StringUtils.equalsIgnoreCase(value, 
                                      String.valueOf(cacheObject.getValue()))) {
        throw BizException.validFail("验证码不正确");
    }
    //验证通过,立即从缓存中删除验证码
    cache.evict(CacheKey.CAPTCHA, key);
    return true;
}

第四步:创建AuthManager认证管理器类,提供用户名密码认证功能

package com.itheima.pinda.authority.biz.service.auth.impl;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.itheima.pinda.auth.server.utils.JwtTokenServerUtils;
import com.itheima.pinda.auth.utils.JwtUserInfo;
import com.itheima.pinda.auth.utils.Token;
import com.itheima.pinda.authority.biz.service.auth.ResourceService;
import com.itheima.pinda.authority.dto.auth.LoginDTO;
import com.itheima.pinda.authority.dto.auth.ResourceQueryDTO;
import com.itheima.pinda.authority.dto.auth.UserDTO;
import com.itheima.pinda.authority.entity.auth.Resource;
import com.itheima.pinda.authority.entity.auth.User;
import com.itheima.pinda.base.R;
import com.itheima.pinda.dozer.DozerUtils;
import com.itheima.pinda.exception.BizException;
import com.itheima.pinda.exception.code.ExceptionCode;
import com.itheima.pinda.utils.BizAssert;
import com.itheima.pinda.utils.NumberHelper;
import com.itheima.pinda.authority.biz.service.auth.UserService;
import com.itheima.pinda.utils.TimeUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
/**
 *认证管理器
 */
@Service
@Slf4j
public class AuthManager {
    @Autowired
    private JwtTokenServerUtils jwtTokenServerUtils;
    @Autowired
    private UserService userService;
    @Autowired
    private ResourceService resourceService;
    @Autowired
    private DozerUtils dozer;
    /**
     * 账号登录
     * @param account
     * @param password
     */
    public R<LoginDTO> login(String account, String password) {
        // 登录验证
        R<User> result = checkUser(account, password);
        if (result.getIsError()) {
            return R.fail(result.getCode(), result.getMsg());
        }
        User user = result.getData();

        // 生成jwt token
        Token token = this.generateUserToken(user);

        List<Resource> resourceList =this.resourceService.
                findVisibleResource(ResourceQueryDTO.builder().
                        userId(user.getId()).build());
        List<String> permissionsList = null;
        if(resourceList != null && resourceList.size() > 0){
            permissionsList = resourceList.stream().
                    map(Resource::getCode).
                    collect(Collectors.toList());
        }
        //封装数据
        LoginDTO loginDTO = LoginDTO.builder()
                .user(this.dozer.map(user, UserDTO.class))
                .token(token)
                .permissionsList(permissionsList)
                .build();
        return R.success(loginDTO);
    }

    //生成jwt token
    private Token generateUserToken(User user) {
        JwtUserInfo userInfo = new JwtUserInfo(user.getId(),
                user.getAccount(),
                user.getName(),
                user.getOrgId(),
                user.getStationId());

        Token token = this.jwtTokenServerUtils.generateUserToken(userInfo, null);
        log.info("token={}", token.getToken());
        return token;
    }

    // 登录验证
    private R<User> checkUser(String account, String password) {
        User user = this.userService.getOne(Wrappers.<User>lambdaQuery()
                .eq(User::getAccount, account));

        // 密码加密
        String passwordMd5 = DigestUtils.md5Hex(password);

        if (user == null || !user.getPassword().equals(passwordMd5)) {
            return R.fail(ExceptionCode.JWT_USER_INVALID);
        }

        return R.success(user);
    }
}

第五步:创建UserService接口、UserServiceImpl实现类、UserMapper接口

package com.itheima.pinda.authority.biz.service.auth;
import com.baomidou.mybatisplus.extension.service.IService;
/**
 * 业务接口
 */
public interface UserService extends IService<User> {
}
package com.itheima.pinda.authority.biz.service.auth.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.pinda.authority.biz.dao.auth.UserMapper;
import com.itheima.pinda.authority.biz.service.auth.UserService;
import com.itheima.pinda.authority.entity.auth.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
 * 业务实现类
 */
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> 
    						implements UserService {
}
package com.itheima.pinda.authority.biz.dao.auth;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.pinda.authority.entity.auth.User;
import org.springframework.stereotype.Repository;
/**
 * Mapper 接口
 */
@Repository
public interface UserMapper extends BaseMapper<User> {
}

第六步:创建ResourceService接口、ResourceServiceImpl实现类、ResourceMapper接口、ResourceMapper.xml

package com.itheima.pinda.authority.biz.service.auth;
import java.util.List;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.pinda.authority.dto.auth.ResourceQueryDTO;
import com.itheima.pinda.authority.entity.auth.Resource;
/**
 * 业务接口
 */
public interface ResourceService extends IService<Resource> {
    /**
     * 查询 用户拥有的资源权限
     */
    List<Resource> findVisibleResource(ResourceQueryDTO resource);
}
package com.itheima.pinda.authority.biz.service.auth.impl;
import java.util.List;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.pinda.authority.biz.service.auth.ResourceService;
import com.itheima.pinda.authority.dto.auth.ResourceQueryDTO;
import com.itheima.pinda.authority.entity.auth.Resource;
import com.itheima.pinda.common.constant.CacheKey;
import com.itheima.pinda.exception.BizException;
import com.itheima.pinda.utils.StrHelper;
import com.itheima.pinda.authority.biz.dao.auth.ResourceMapper;
import lombok.extern.slf4j.Slf4j;
import net.oschina.j2cache.CacheChannel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
 * 业务实现类
 * 资源
 */
@Slf4j
@Service
public class ResourceServiceImpl extends ServiceImpl<ResourceMapper, Resource> implements ResourceService {
    @Autowired
    private CacheChannel cache;

    /**
     * 查询用户的可用资源权限
     */
    @Override
    public List<Resource> findVisibleResource(ResourceQueryDTO resourceQueryDTO) {
        //查询当前用户可访问的资源
        List<Resource> visibleResource = 
            baseMapper.findVisibleResource(resourceQueryDTO);
        if(visibleResource != null && visibleResource.size() > 0){
            List<String> userResource = visibleResource.
                stream().
                map((Resource r) -> {
                return r.getMethod() + r.getUrl();
            }).collect(Collectors.toList());
            //将当前用户可访问的资源载入缓存,形式为:GET/user/page
            cache.set(CacheKey.USER_RESOURCE,
                      resourceQueryDTO.getUserId().toString(),
                      userResource);
        }
        return visibleResource;
    }
}
package com.itheima.pinda.authority.biz.dao.auth;

import java.util.List;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.pinda.authority.dto.auth.ResourceQueryDTO;
import com.itheima.pinda.authority.entity.auth.Resource;
import org.springframework.stereotype.Repository;
/**
 * Mapper 接口
 */
@Repository
public interface ResourceMapper extends BaseMapper<Resource> {
    /**
     * 查询用户拥有的资源权限
     */
    List<Resource> findVisibleResource(ResourceQueryDTO resource);
}

在resources目录下创建mapper_authority目录,在此目录中创建ResourceMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.pinda.authority.biz.dao.auth.ResourceMapper">
    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" 
               type="com.itheima.pinda.authority.entity.auth.Resource">
        <id column="id" jdbcType="BIGINT" property="id"/>
        <result column="create_user" jdbcType="BIGINT" property="createUser"/>
        <result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
        <result column="update_user" jdbcType="BIGINT" property="updateUser"/>
        <result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
        <result column="code" jdbcType="VARCHAR" property="code"/>
        <result column="name" jdbcType="VARCHAR" property="name"/>
        <result column="menu_id" jdbcType="BIGINT" property="menuId"/>
        <result column="describe_" jdbcType="VARCHAR" property="describe"/>
        <result column="method" jdbcType="VARCHAR" property="method"/>
        <result column="url" jdbcType="VARCHAR" property="url"/>
    </resultMap>

    <!-- 通用查询结果列 -->
    <sql id="Base_Column_List">
        id, create_user, create_time, update_user, update_time, 
        code, name, menu_id, describe_,method,url
    </sql>
    
    <select id="findVisibleResource"  resultMap="BaseResultMap">
        SELECT
        <include refid="Base_Column_List"/>
        from pd_auth_resource where 1=1
        and id in (
        SELECT authority_id FROM pd_auth_role_authority ra INNER JOIN pd_auth_user_role ur on ra.role_id = ur.role_id
        INNER JOIN pd_auth_role r on r.id = ra.role_id
        where ur.user_id = #{userId, jdbcType=BIGINT} and r.`status` = true
        and ra.authority_type = 'RESOURCE'
        )
    </select>
</mapper>

认证接口开发完成后可以使用接口文档进行测试:

18.3 开发操作日志功能

当前的权限服务已经依赖了pd-tools-log日志模块,此模块中已经定义好了SysLogAspect切面类用于拦截Controller中添加@SysLog注解的方法,在切面类中通过前置通知和后置通知方法收集操作日志相关信息并发布SysLogEvent日志事件,通过定义SysLogListener监听器来监听日志事件。

在权限服务中只需要定义配置类来创建SysLogListener,同时将SysLogListener所需的Consumer参数传递进行即可。

具体开发步骤:

第一步:创建OptLogService接口

package com.itheima.pinda.authority.biz.service.common;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.pinda.authority.entity.common.OptLog;
import com.itheima.pinda.log.entity.OptLogDTO;
/**
 * 业务接口
 * 操作日志
 */
public interface OptLogService extends IService<OptLog> {
    /**
     * 保存日志
     */
    boolean save(OptLogDTO entity);
}

第二步:创建OptLogServiceImpl实现类

package com.itheima.pinda.authority.biz.service.common.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.pinda.authority.biz.dao.common.OptLogMapper;
import com.itheima.pinda.authority.entity.common.OptLog;
import com.itheima.pinda.dozer.DozerUtils;
import com.itheima.pinda.log.entity.OptLogDTO;
import com.itheima.pinda.authority.biz.service.common.OptLogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
 * 业务实现类
 * 操作日志
 */
@Slf4j
@Service
public class OptLogServiceImpl extends ServiceImpl<OptLogMapper, OptLog> 
    							implements OptLogService {
    @Autowired
    DozerUtils dozer;

    @Override
    public boolean save(OptLogDTO entity) {
        return super.save(dozer.map(entity, OptLog.class));
    }
}

第三步:创建OptLogMapper接口

package com.itheima.pinda.authority.biz.dao.common;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.pinda.authority.entity.common.OptLog;
import org.springframework.stereotype.Repository;
/**
 * Mapper 接口
 * 系统日志
 */
@Repository
public interface OptLogMapper extends BaseMapper<OptLog> {
}

第四步:创建SysLogConfiguration配置类

package com.itheima.pinda.authority.config;
import com.itheima.pinda.authority.biz.service.common.OptLogService;
import com.itheima.pinda.log.entity.OptLogDTO;
import com.itheima.pinda.log.event.SysLogListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import java.util.function.Consumer;
/**
 * 日志自动配置
 */
@EnableAsync
@Configuration
public class SysLogConfiguration {
    //日志记录监听器
    @Bean
    public SysLogListener sysLogListener(OptLogService optLogService) {
        Consumer<OptLogDTO> consumer = (optLog) -> optLogService.save(optLog);
        return new SysLogListener(consumer);
    }
}

测试:

在已经开发的Controller的方法上加入@SysLog注解,然后通过接口文档访问,可以看到操作日志已经插入到pd_common_opt_log日志表中了。

18.4 导入其他功能代码

本课程的侧重点在于基础组件的构建以及认证和鉴权的开发实现,对于基础的数据维护不再作为重点内容讲解。

在授课资料中已经提供了其他功能的代码实现,这些功能包括:

1、岗位数据维护(CRUD)

2、组织数据维护(CRUD)

3、菜单数据维护(CRUD)

4、角色数据维护(CRUD)

5、用户数据维护(CRUD)

6、用户登录日志维护(CRD)

直接将这些功能代码复制到pd-auth-server工程中使用即可。

posted @ 2021-11-08 13:55  sstu  阅读(691)  评论(0编辑  收藏  举报