0%

[TOC]

Spring 定时任务详解(@Scheduled注解)

  1. initialDelay :初次执行任务之前需要等待的时间

    1
    2
    3
    @Scheduled(initialDelay =5000)
    public void doSomething() {
    }
  2. fixedDelay:每次执行任务之后间隔多久再次执行该任务。(项目启动时,会立即执行任务。可以配合initialDelay一起使用(项目启动后延时执行任务)

    1
    2
    3
    4
    @Scheduled(fixedDelay=5000)
    public void doSomething() {
    // something that should execute periodically
    }
  3. fixedRate:执行频率,每隔多少时间就启动任务,不管该任务是否启动完成。(项目启动时,会立即执行任务。可以配合initialDelay一起使用(项目启动后延时执行任务))

    1
    2
    3
    @Scheduled(fixedRate=5000)
    public void doSomething() {
    }
  4. cron=”” 设置时分秒等具体的定时,网上很很多相关列子。

    1
    2
    3
    4
    @Scheduled(cron="*/5 * * * * MON-FRI")
    public void doSomething() {
    // something that should execute on weekdays only
    }

    @Scheduled(cron = “10 0/10 * * * ?”)

cron表达式详解

一个cron表达式有至少6个(也可能7个)有空格分隔的时间元素。按顺序依次为:

1  秒(0~59)
2  分钟(0~59)
3  小时(0~23)
4  天(0~31)
5  月(0~11)
6  星期(1~7 1=SUN 或 SUN,MON,TUE,WED,THU,FRI,SAT)
7  年份(1970-2099)

其中每个元素可以是一个值(如6),一个连续区间(9-12),一个间隔时间(8-18/4)(/表示每隔4小时),一个列表(1,3,5),通 配符。由于”月份中的日期”和”星期中的日期”这两个元素互斥的,必须要对其中一个设置?.

"0 0 10,14,16 * * ?"       每天上午10点,下午2点,4点
"0 0/30 9-17 * * ?"         朝九晚五工作时间内每半小时
"0 0 12 ? * WED"            表示每个星期三中午12点
"0 0 12 * * ?"              每天中午12点触发
"0 15 10 ? * *"             每天上午10:15触发
"0 15 10 * * ?"             每天上午10:15触发
"0 15 10 * * ? *"           每天上午10:15触发
"0 15 10 * * ? 2005"        2005年的每天上午10:15触发
"0 * 14 * * ?"              在每天下午2点到下午2:59期间的每1分钟触发
"0 0/5 14 * * ?"            在每天下午2点到下午2:55期间的每5分钟触发
"0 0/5 14,18 * * ?"         在每天下午2点到2:55期间和下午6点到6:55期间的每5钟触发
"0 0-5 14 * * ?"            在每天下午2点到下午2:05期间的每1分钟触发
"0 10,44 14 ? 3 WED"        每年三月的星期三的下午2:10和2:44触发
"0 15 10 ? * MON-FRI"       周一至周五的上午10:15触发
"0 15 10 15 * ?"            每月15日上午10:15触发
"0 15 10 L * ?"             每月最后一日的上午10:15触发
"0 15 10 ? * 6L"            每月的最后一个星期五上午10:15触发
"0 15 10 ? * 6L 2002-2005"  2002年至2005年的每月的最后一个星期五上午10:15发
"0 15 10 ? * 6#3"           每月的第三个星期五上午10:15触发

有些子表达式能包含一些范围或列表

例如:

  1. 子表达式(天(星期))可以为 MON-FRI, MON,WED,FRI, MON-WED,SAT
  2. * 字符代表所有可能的值
  3. / 字符用来指定数值的增量

例如:

  1. 在子表达式(分钟)里的 0/15 表示从第0分钟开始,每15分钟

  2. 在子表达式(分钟)里的 3/20 表示从第3分钟开始,每20分钟(它和“3,23,43”)的含义一样

  3. ? 字符仅被用于天(月)和天(星期)两个子表达式,表示不指定值。

    当2个子表达式其中之一被指定了值以后,为了避免冲突,需要将另一个子表达式的值设为 ?

  4. L 字符仅被用于天(月)和天(星期)两个子表达式,它是单词 last 的缩写

  5. 如果在 L 前有具体的内容,它就具有其他的含义了。例如:6L 表示这个月的倒数第 6 天

  6. 注意:在使用 L 参数时,不要指定列表或范围,因为这会导致问题

  7. W 字符代表着平日(Mon-Fri),并且仅能用于日域中。它用来指定离指定日的最近的一个平日。

    大部分的商业处理都是基于工作周的,所以 W 字符可能是非常重要的。
    例如,日域中的 15W 意味着 “离该月15号的最近一个平日。” 假如15号是星期六,那么 trigger 会在14号(星期五)触发,因为星期四比星期一离15号更近。

  8. C:代表“Calendar”的意思。

    它的意思是计划所关联的日期,如果日期没有被关联,则相当于日历中所有日期。
    例如5C在日期字段中就相当于日历5日以后的第一天。1C在星期字段中相当于星期日后的第一天。

字段 允许值 允许的特殊字符
0-59 , - * /
0-59 , - * /
小时 0-23 , - * /
日期 1-31 , - * ? / L W C
月份 1-12 或者 JAN-DEC , - * /
星期 1-7 或者 SUN-SAT , - * ? / L C #
年(可选) 留空, 1970-2099 , - * /

定时任务执行原理:https://blog.csdn.net/gaodebao1/article/details/51789225

我们是通过 logback 打印日志,然后将日志通过 kafka 消息队列发送到 Logstash,经过处理以后存储到 Elasticsearch 中,然后通过 Kibana 图形化界面进行分析和处理。

在 spring boot 应用程序中,默认使用 logback 来记录日志,并用 INFO 级别输出日志到控制台。

日志级别和顺序:TRACE < DEBUG < INFO < WARN < ERROR < FATAL

Spring Boot官方推荐优先使用带有-spring的文件名作为,按照如下规则组织配置文件名,就能被正确加载:

logback-spring.xml > logback-spring.groovy > logback.xml > logback.groovy

1. logback 与 Kafka 的集成

logback 记录日志到 Kafka 消息队列中去,主要使用的是 com.github.danielwegener:logback-kafka-appender:0.2.0-RC2 这个依赖.

1.1. KafkaAppender 配置说明

由于Logback Encoder API中的重大更改,您需要至少使用logback版本1.2。

确保项目依赖中有:

[maven pom.xml]

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>com.github.danielwegener</groupId>
<artifactId>logback-kafka-appender</artifactId>
<version>0.2.0-RC2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<scope>runtime</scope>
</dependency>

[maven pom.xml]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<configuration>

<!-- This is the kafkaAppender -->
<appender name="kafkaAppender" class="com.github.danielwegener.logback.kafka.KafkaAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<topic>logs</topic>
<keyingStrategy class="com.github.danielwegener.logback.kafka.keying.NoKeyKeyingStrategy" />
<deliveryStrategy class="com.github.danielwegener.logback.kafka.delivery.AsynchronousDeliveryStrategy" />

<!-- 可选参数, 用于固定分区 -->
<!-- <partition>0</partition> -->

<!-- 可选参数,用于在kafka消息中包含日志时间戳 -->
<!-- <appendTimestamp>true</appendTimestamp> -->

<!-- 每个<producerConfig>转换为常规kafka-client配置(格式:key = value) -->
<!-- 生产者配置记录在这里:https//kafka.apache.org/documentation.html#newproducerconfigs -->
<!-- bootstrap.servers是唯一必需的 producerConfig -->
<producerConfig>bootstrap.servers=localhost:9092</producerConfig>

<!-- 如果kafka不可用,这是后备appender。 -->
<appender-ref ref="STDOUT" />
</appender>

<!-- 标准输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<root level="info">
<appender-ref ref="kafkaAppender" />
</root>
</configuration>

1.2. 兼容性:Compatibility

logback-kafka-appender 依赖于 org.apache.kafka:kafka-clients:1.0.0:jar。它可以将日志附加到版本为 0.9.0.0 或更高版本的 kafka 代理。

kafka-clients 的依赖性不会被遮蔽,并且可以通过依赖性覆盖升级到更高的 api 兼容版本。

1.3. 分发策略:Delivery strategies

直接通过网络进行日志记录并不是一件容易的事情,因为它可能不如本地文件系统可靠,并且如果传输出现问题,对应用程序性能的影响要大得多。

您需要做出一个重要的决定:是将所有日志传递到远程 Kafka 更重要,还是让应用程序保持平稳运行更为重要?这两个决定都允许您调整此 appender 以获得吞吐量。

  • AsynchronousDeliveryStrategy:

    将每个日志消息分派给Kafka生成器。如果由于某些原因传递失败,则将消息发送给 fallback appenders。
    但是,如果生产者发送的缓冲区已满,这个交付策略就会阻塞(如果到代理的连接丢失,就会发生这种情况)。
    为了避免这种阻塞,可以启用 producerConfig block.buffer.full=false
    所有不能足够快地交付的日志消息都将立即转到 fallback appenders。

  • BlockingDeliveryStrategy:

    将每条日志消息分派给Kafka Producer。如果由于某些原因导致传递失败,则会将消息分派给备用追加程序(fallback appender)。
    但是,如果生成器发送缓冲区已满,则此DeliveryStrategy 阻止每个调用线程,直到实际传递日志消息。
    通常不鼓励这种策略,因为它对吞吐量有很大的负面影响。警告:此策略不应与 producerConfig 一起使用 linger.ms

1.3.1. 关于 broker 的中断

AsynchronousDeliveryStrategy 不会阻止被Kafka元数据交换阻塞的应用程序。
这意味着:如果在日志记录上下文启动时无法访问所有代理,或者所有代理在较长时间内无法访问(> metadata.max.age.ms),
则 appender 最终将阻塞。这种行为通常是不受欢迎的,可以使用 kafka-clients 0.9 进行迁移(参见#16)。
在此之前,您可以使用 logback 自己的 AsyncAppender 包装 KafkaAppender。

示例配置可能如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<configuration>

<!-- This is the kafkaAppender -->
<appender name="kafkaAppender" class="com.github.danielwegener.logback.kafka.KafkaAppender">
<!-- Kafka Appender configuration -->
</appender>

<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="kafkaAppender" />
</appender>

<root level="info">
<appender-ref ref="ASYNC" />
</root>
</configuration>

1.3.2. 自定义分发策略(delivery strategies)

你可能使用自己的分发策略,只需继承 com.github.danielwegener.logback.kafka.delivery.DeliveryStrategy

1.3.3. 备用追加程序:fallback appender

如果由于某种原因,kafka-producer决定它无法发布日志消息,那么该消息仍然可以记录到 fallback appender(STDOUT 或 STDERR 上的 ConsoleAppender 将是一个合理的选择)。

只需将您的后备appender作为logback appender-ref添加到logback.xml中的KafkaAppender部分。 每个无法传递给kafka的消息都将写入所有已定义的appender-ref。

1.1 章节示例:<appender-ref ref ="STDOUT">STDOUT 是已定义的 appender。

请注意,AsynchronousDeliveryStrategy 将重用 kafka 生成器io线程将消息写入备用 appender。 因此,所有后备追加者应该是合理的快速,所以他们不会减慢或打破卡夫卡生产者。

1.3.4. 生产者调整

这个appender使用kafka-0.8.2中引入的 kafka生成器。 它使用生成器默认配置。

您可以使用 <producerConfig> Name = Value </ producerConfig> 块覆盖任何已知的kafka生成器配置(请注意,boostrap.servers配置是必需的)。
这允许很多微调潜力(例如,使用batch.size,compression.type 和 linger.ms)。

1.3.5. 序列化

该模块支持任何 ch.qos.logback.core.encoder.Encoder。这允许您使用能够编码 ILoggingEventIAccessEvent 的任何编码器,
如众所周知的logback PatternLayoutEncoder
或者例如 logstash-logback-encoder的LogstashEncoxer

1.3.5.1 自定义序列化

如果要在kafka日志记录主题上编写与字符串不同的内容,可以使用编码机制。 用例将是生产或消费方面的较小消息大小和/或更好的序列化/反序列化性能。
有用的格式可以是BSON,Avro或其他。

要推出自己的实现,请参阅logback文档
请注意,logback-kafka-appender永远不会调用headerBytes()或footerBytes()方法。

您的编码器应该针对您要支持的事件类型的任何子类型(通常是 ILoggingEvent)进行类型参数化,例如:

public class MyEncoder extends ch.qos.logback.core.encoder.Encoder<ILoggingEvent> {/*..*/}

1.4 键控策略/分区:Keying strategies / Partitioning

Kafka的可扩展性和排序保证严重依赖于分区的概念(这里有更多细节)。
对于应用程序日志记录,这意味着我们需要决定如何在多个kafka主题分区上分发日志消息。
这个决定的一个含义是消息在从任意多分区消费者消费时如何排序,因为kafka仅在每个单独的分区上提供有保证的读取顺序。
另一个含义是我们的日志消息在所有可用分区中的分布均匀,因此在多个代理之间保持平衡。

日志消息的顺序可能重要,也可能不重要,具体取决于预期的消费者 - 受众(例如,logstash索引器无论如何都会按时间戳重新排序所有消息)。

您可以使用partition属性为kafka appender提供固定分区,或让生产者使用消息密钥对消息进行分区。 因此logback-kafka-appender支持以下键控策略策略:

  • NoKeyKeyingStrategy :

    不生成 message key。如果未提供固定分区,则导致跨分区的循环分布。

  • HostNameKeyingStrategy :

    此策略使用 HOSTNAME 作为 message key。 这很有用,因为它可以确保此主机发出的所有日志消息对于任何使用者都保持正确的顺序。
    但是这种策略可能导致少量主机的日志分配不均匀(与分区数量相比)。

  • ContextNameKeyingStrategy :

    此策略使用 logback 的 CONTEXT_NAME 作为 message key。
    这可以确保由同一日志记录上下文记录的所有日志消息将保持在任何使用者的正确顺序中。
    但是这种策略可能导致少量主机的日志分配不均匀(与分区数量相比)。
    此策略仅适用于ILoggingEvents。

  • ThreadNameKeyingStrategy :

    此策略使用调用线程名(thread name)称作为 message key。
    这可确保同一线程记录的所有消息将保持正确的顺序,供任何使用者使用。
    但是这种策略可能会导致少量线程(-names)的日志分配不均匀(与分区数量相比)。
    此策略仅适用于 ILoggingEvents。

  • LoggerNameKeyingStrategy :

    *此策略使用记录器名称(logger name)作为 message key。
    这可确保同一记录器记录的所有消息都将保持在任何使用者的正确顺序中。
    但是这种策略可能会导致少量不同记录器的日志分配不均匀(与分区数量相比)。
    此策略仅适用于 ILoggingEvents。

1.4.1. 自定义键控策略 Custom keying strategies

如果上述键控策略都不满足您的要求,您可以通过实现自定义 KeyingStrategy 轻松实现自己的:

1
2
3
4
5
6
7
8
9
10
package foo;
import com.github.danielwegener.logback.kafka.keying.KeyingStrategy;

/* 这是一个有效的例子,但并没有多大意义 */
public class LevelKeyingStrategy implements KeyingStrategy<ILoggingEvent> {
@Override
public byte[] createKey(ILoggingEvent e) {
return ByteBuffer.allocate(4).putInt(e.getLevel()).array();
}
}

作为大多数自定义 logback 组件,您的自定义分区策略还可以实现 ch.qos.logback.core.spi.ContextAwarech.qos.logback.core.spi.LifeCycle 接口。

当您想要使用kafka的日志压缩工具时,自定义键控策略可能会特别方便。

1.5. logback-spring.xml 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<contextName>oauth2-auth-server</contextName>
<!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->
<property name="LOG_HOME" value="logs"/>

<!--输出到控制台-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<encoder>
<pattern>%-12(%d{yyyy-MM-dd HH:mm:ss.SSS}) %contextName [%thread] %highlight(%-5level) %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<!--输出到kafka-->
<appender name="kafka" class="com.github.danielwegener.logback.kafka.KafkaAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%-12(%d{yyyy-MM-dd HH:mm:ss.SSS}) %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<topic>oauth2-auth-server</topic>
<!-- 我们不关心如何对日志消息进行分区 -->
<keyingStrategy class="com.github.danielwegener.logback.kafka.keying.NoKeyKeyingStrategy"/>

<!-- 使用异步传递。 日志记录不会阻止应用程序线程 -->
<deliveryStrategy class="com.github.danielwegener.logback.kafka.delivery.AsynchronousDeliveryStrategy"/>

<!-- 每个<producerConfig>转换为常规kafka-client配置(格式:key = value) -->
<!-- 生产者配置记录在这里:https://kafka.apache.org/documentation.html#newproducerconfigs -->
<!-- bootstrap.servers是唯一必需的 producerConfig -->
<producerConfig>bootstrap.servers=192.168.213.13:9092,192.168.213.14:9092,192.168.213.21:9092</producerConfig>
<!-- 不用等待代理对批的接收进行打包。 -->
<producerConfig>acks=0</producerConfig>
<!-- 等待最多1000毫秒并收集日志消息,然后再批量发送 -->
<producerConfig>linger.ms=1000</producerConfig>
<!-- 即使生产者缓冲区运行已满,也不要阻止应用程序而是开始丢弃消息 -->
<producerConfig>max.block.ms=0</producerConfig>
<!-- 定义用于标识kafka代理的客户端ID -->
<producerConfig>client.id=${HOSTNAME}-${CONTEXT_NAME}-logback-relaxed</producerConfig>
<!-- 如果kafka不可用,这是后备appender。 -->
<appender-ref ref="file"/>
</appender>

<!--输出到文件-->
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名-->
<FileNamePattern>${LOG_HOME}/oauth2-auth-server.log.%d{yyyy-MM-dd}.log</FileNamePattern>
<!--日志文件保留天数-->
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder>
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%-12(%d{yyyy-MM-dd HH:mm:ss.SSS}) %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>

<root level="info">
<appender-ref ref="console"/>
<appender-ref ref="kafka"/>
</root>
</configuration>

2. Kafka 与 Logstash 的集成

logstash 与 Kafka 的简单配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
input {
kafka {
topics => "applog"
bootstrap_servers => "Kafka服务器IP:9092,Kafka服务器IP:9092"
codec => "json"
}
}
filter {
}
output {
//控制台输入
stdout { codec => rubydebug }
elasticsearch {
hosts => [ "elasticsearch服务器IP:9200" ]
index => "kafka"
}

}

启动 logstash:

.\bin\logstash -f .\conf\logstash-kaka.conf

在 oauth2 的授权模式中有4种:

  • 授权码模式
  • 隐式授权模式
  • 密码模式
  • 客户端模式

但如果我们想要增加一个自定义的授权模式,又该怎么做呢?

相关的源码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class CompositeTokenGranter implements TokenGranter {

private final List<TokenGranter> tokenGranters;

public CompositeTokenGranter(List<TokenGranter> tokenGranters) {
this.tokenGranters = new ArrayList<TokenGranter>(tokenGranters);
}

//四种授权模式+刷新令牌的模式根据grant_type判断
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
for (TokenGranter granter : tokenGranters) {
OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
if (grant!=null) {
return grant;
}
}
return null;
}

public void addTokenGranter(TokenGranter tokenGranter) {
if (tokenGranter == null) {
throw new IllegalArgumentException("Token granter is null");
}
tokenGranters.add(tokenGranter);
}

}

oauth2 端点配置类部分源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public final class AuthorizationServerEndpointsConfigurer {

// 省略部分代码

private TokenGranter tokenGranter;

public AuthorizationServerEndpointsConfigurer tokenGranter(TokenGranter tokenGranter) {
this.tokenGranter = tokenGranter;
return this;
}

// 默认的四种授权模式+刷新令牌的模式的配置
private TokenGranter tokenGranter() {
if (tokenGranter == null) {
tokenGranter = new TokenGranter() {
private CompositeTokenGranter delegate;

@Override
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
if (delegate == null) {
delegate = new CompositeTokenGranter(getDefaultTokenGranters());
}
return delegate.grant(grantType, tokenRequest);
}
};
}
return tokenGranter;
}

// 默认的四种授权模式+刷新令牌的模式的配置
private List<TokenGranter> getDefaultTokenGranters() {
ClientDetailsService clientDetails = clientDetailsService();
AuthorizationServerTokenServices tokenServices = tokenServices();
AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices();
OAuth2RequestFactory requestFactory = requestFactory();

List<TokenGranter> tokenGranters = new ArrayList<TokenGranter>();
// 添加授权码模式
tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetails,
requestFactory));
// 添加刷新令牌的模式
tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetails, requestFactory));
// 添加隐式授权模式
ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, clientDetails, requestFactory);
tokenGranters.add(implicit);
// 添加客户端模式
tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory));
if (authenticationManager != null) {
// 添加密码模式
tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices,
clientDetails, requestFactory));
}

// 可以复制相关代码,然后这里添加自定义的授权模式

return tokenGranters;
}
}

看到这里就可以发现 spring 已经把默认的四种授权模式+刷新令牌的模式的配置在代码中写死了!

那又如何添加自定义的授权模式呢?

我的思路是这样的:

直接把这部分的代码复制,在其中添加自定义的授权模式。

我直接把密码模式复制,将其中的 GRANT_TYPE 的值改为 sms_code,然后使用 /oauth/token?grant_type=sms_code&scope=read&username=user&password=123456 来验证结果。

自定义授权模式:SmsCodeTokenGranter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* @author fengxuechao
* @version 0.1
* @date 2019/5/17
*/
public class SmsCodeTokenGranter extends AbstractTokenGranter {

// 仅仅复制了 ResourceOwnerPasswordTokenGranter,只是改变了 GRANT_TYPE 的值,来验证自定义授权模式的可行性
private static final String GRANT_TYPE = "sms_code";

private final AuthenticationManager authenticationManager;

public SmsCodeTokenGranter(
AuthenticationManager authenticationManager,
AuthorizationServerTokenServices tokenServices,
ClientDetailsService clientDetailsService,
OAuth2RequestFactory requestFactory) {
this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);

}

protected SmsCodeTokenGranter(
AuthenticationManager authenticationManager,
AuthorizationServerTokenServices tokenServices,
ClientDetailsService clientDetailsService,
OAuth2RequestFactory requestFactory,
String grantType) {
super(tokenServices, clientDetailsService, requestFactory, grantType);
this.authenticationManager = authenticationManager;
}

@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
String username = parameters.get("username");
String password = parameters.get("password");
// Protect from downstream leaks of password
parameters.remove("password");

Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
try {
userAuth = authenticationManager.authenticate(userAuth);
} catch (AccountStatusException ase) {
//covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
throw new InvalidGrantException(ase.getMessage());
} catch (BadCredentialsException e) {
// If the username/password are wrong the spec says we should send 400/invalid grant
throw new InvalidGrantException(e.getMessage());
}
if (userAuth == null || !userAuth.isAuthenticated()) {
throw new InvalidGrantException("Could not authenticate user: " + username);
}

OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
}

授权模式配置类:TokenGranterConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
/**
* @author fengxuechao
* @version 0.1
* @date 2019/5/17
*/
@Configuration
@Profile("inMemory")
public class TokenGranterConfig {

@Autowired
private ClientDetailsService clientDetailsService;

@Autowired
private UserDetailsService userDetailsService;

@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private TokenStore tokenStore;

@Autowired
TokenEnhancer tokenEnhancer;

private AuthorizationCodeServices authorizationCodeServices;

private boolean reuseRefreshToken = true;

private AuthorizationServerTokenServices tokenServices;

private TokenGranter tokenGranter;

/**
* 授权模式
*
* @return
*/
@Bean
public TokenGranter tokenGranter() {
if (tokenGranter == null) {
tokenGranter = new TokenGranter() {
private CompositeTokenGranter delegate;

@Override
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
if (delegate == null) {
delegate = new CompositeTokenGranter(getDefaultTokenGranters());
}
return delegate.grant(grantType, tokenRequest);
}
};
}
return tokenGranter;
}

/**
* 程序支持的授权类型
*
* @return
*/
private List<TokenGranter> getDefaultTokenGranters() {
AuthorizationServerTokenServices tokenServices = tokenServices();
AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices();
OAuth2RequestFactory requestFactory = requestFactory();

List<TokenGranter> tokenGranters = new ArrayList<TokenGranter>();
// 添加授权码模式
tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetailsService, requestFactory));
// 添加刷新令牌的模式
tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetailsService, requestFactory));
// 添加隐士授权模式
tokenGranters.add(new ImplicitTokenGranter(tokenServices, clientDetailsService, requestFactory));
// 添加客户端模式
tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetailsService, requestFactory));
if (authenticationManager != null) {
// 添加密码模式
tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory));
// 添加自定义授权模式(实际是密码模式的复制)
tokenGranters.add(new SmsCodeTokenGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory));
}
return tokenGranters;
}

/**
* TokenServices
*
* @return
*/
private AuthorizationServerTokenServices tokenServices() {
if (tokenServices != null) {
return tokenServices;
}
this.tokenServices = createDefaultTokenServices();
return tokenServices;
}

/**
* 授权码API
*
* @return
*/
private AuthorizationCodeServices authorizationCodeServices() {
if (authorizationCodeServices == null) {
authorizationCodeServices = new InMemoryAuthorizationCodeServices();
}
return authorizationCodeServices;
}

/**
* OAuth2RequestFactory的默认实现,它初始化参数映射中的字段,
* 验证授权类型(grant_type)和范围(scope),并使用客户端的默认值填充范围(scope)(如果缺少这些值)。
*
* @return
*/
private OAuth2RequestFactory requestFactory() {
return new DefaultOAuth2RequestFactory(clientDetailsService);
}

/**
* 默认 TokenService
*
* @return
*/
private DefaultTokenServices createDefaultTokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore);
tokenServices.setSupportRefreshToken(true);
tokenServices.setReuseRefreshToken(reuseRefreshToken);
tokenServices.setClientDetailsService(clientDetailsService);
tokenServices.setTokenEnhancer(tokenEnhancer);
addUserDetailsService(tokenServices, this.userDetailsService);
return tokenServices;
}

/**
* 添加预身份验证
*
* @param tokenServices
* @param userDetailsService
*/
private void addUserDetailsService(DefaultTokenServices tokenServices, UserDetailsService userDetailsService) {
if (userDetailsService != null) {
PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
provider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken>(userDetailsService));
tokenServices.setAuthenticationManager(new ProviderManager(Arrays.<AuthenticationProvider>asList(provider)));
}
}
}

授权认证服务端点配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.fengxuechao.examples.auth.config.inmemory;
/**
* @author fengxuechao
* @version 0.1
* @date 2019/5/8
*/
@Slf4j
@EnableAuthorizationServer
@Configuration
@Profile("inMemory")
public class AuthorizationServerConfigInMemory extends AuthorizationServerConfigurerAdapter {

// 省略部分代码

@Autowired
private TokenGranter tokenGranter;

/**
* 认证服务器节点配置
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.tokenGranter(tokenGranter) // 四种授权模式+刷新令牌的模式+自定义授权模式
.tokenStore(tokenStore)
.approvalStore(approvalStore)
.userDetailsService(userDetailsService)
.authenticationManager(authenticationManager)
.setClientDetailsService(clientDetailsService);
}
}

演示效果

在这里插入图片描述

request
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
POST http://localhost:8080/oauth/token?grant_type=sms_code&scope=read&username=user&password=123456

HTTP/1.1 200
X-Application-Context: application:inMemory
Cache-Control: no-store
Pragma: no-cache
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 18 Jun 2019 07:13:12 GMT

{
"access_token": "19f2e44a-6c9a-45c4-be7e-0aada6a0a9e6",
"token_type": "bearer",
"refresh_token": "f59336a8-03c4-4c85-bc31-16c6d80f1381",
"expires_in": 359,
"scope": "read",
"organization": "userWqTI"
}

Response code: 200; Time: 335ms; Content length: 190 bytes

在搭建完 spring-security-oauth2 整个微服务框架后,来了一个需求:

每个微服务都需要对访问进行鉴权,每个微服务应用都需要明确当前访问用户和他的权限。

auth 系统的主要功能是授权认证和鉴权。

授权认证已经完成,那么如何对用户的访问进行鉴权呢?

首先需要明确什么时候发生鉴权?

鉴权发生在用户已经认证后携带了 access_token 信息但还没用访问到目标资源的时候。

知道了鉴权发生的时间,需要明白怎么鉴权?

我的想法是添加一个用于鉴权的过滤器,Spring Security 默认的过滤器链(官网):

别名 类名称 Namespace Element or Attribute
CHANNEL_FILTER ChannelProcessingFilter http/intercept-url@requires-channel
SECURITY_CONTEXT_FILTER SecurityContextPersistenceFilter http
CONCURRENT_SESSION_FILTER ConcurrentSessionFilter session-management/concurrency-control
HEADERS_FILTER HeaderWriterFilter http/headers
CSRF_FILTER CsrfFilter http/csrf
LOGOUT_FILTER LogoutFilter http/logout
X509_FILTER X509AuthenticationFilter http/x509
PRE_AUTH_FILTER AbstractPreAuthenticatedProcessingFilter( Subclasses) N/A
CAS_FILTER CasAuthenticationFilter N/A
FORM_LOGIN_FILTER UsernamePasswordAuthenticationFilter http/form-login
BASIC_AUTH_FILTER BasicAuthenticationFilter http/http-basic
SERVLET_API_SUPPORT_FILTER SecurityContextHolderAwareRequestFilter http/@servlet-api-provision
JAAS_API_SUPPORT_FILTER JaasApiIntegrationFilter http/@jaas-api-provision
REMEMBER_ME_FILTER RememberMeAuthenticationFilter http/remember-me
ANONYMOUS_FILTER AnonymousAuthenticationFilter http/anonymous
SESSION_MANAGEMENT_FILTER SessionManagementFilter session-management
EXCEPTION_TRANSLATION_FILTER ExceptionTranslationFilter http
FILTER_SECURITY_INTERCEPTOR FilterSecurityInterceptor http
SWITCH_USER_FILTER SwitchUserFilter N/A

过滤器顺序从上到下

FilterSecurityInterceptor 是 filterchain 中比较复杂,也是比较核心的过滤器,主要负责web应用安全授权的工作。

我想添加的过滤器是添加在 FilterSecurityInterceptor 之后。

Oauth2FilterSecurityInterceptor 是模仿 FilterSecurityInterceptor 实现,继承 AbstractSecurityInterceptor 和实现 Filter 接口。

整个过程需要依赖 AuthenticationManager、AccessDecisionManager 和 FilterInvocationSecurityMetadataSource。

  • AuthenticationManager是认证管理器,实现用户认证的入口;
  • AccessDecisionManager是访问决策器,决定某个用户具有的角色,是否有足够的权限去访问某个资源;
  • FilterInvocationSecurityMetadataSource是资源源数据定义,即定义某一资源可以被哪些角色访问。

自定义鉴权过滤器 Oauth2FilterSecurityInterceptor 的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package com.fengxuechao.examples.auth.authorization;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.web.FilterInvocation;

import javax.servlet.*;
import java.io.IOException;

/**
* 比较核心的过滤器: 主要负责web应用鉴权的工作。
* 需要依赖:
* - AuthenticationManager:认证管理器,实现用户认证的入口;
* - AccessDecisionManager:访问决策器,决定某个用户具有的角色,是否有足够的权限去访问某个资源;
* - FilterInvocationSecurityMetadataSource:资源源数据定义,即定义某一资源可以被哪些角色访问.
*
* @author fengxuechao
* @version 0.1
* @date 2019/6/17
*/
@Slf4j
public class Oauth2FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {

private Oauth2FilterInvocationSecurityMetadataSource securityMetadataSource;

@Override
public void init(FilterConfig filterConfig) throws ServletException {
if (log.isInfoEnabled()) {
log.info("Oauth2FilterSecurityInterceptor init");
}
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (log.isInfoEnabled()) {
log.info("Oauth2FilterSecurityInterceptor doFilter");
}
FilterInvocation filterInvocation = new FilterInvocation(request, response, chain);
invoke(filterInvocation);
}

public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
// filterInvocation里面有一个被拦截的url
// 里面调用 Oauth2AccessDecisionManager 的 getAttributes(Object object) 这个方法获取 filterInvocation 对应的所有权限
// 再调用 Oauth2AccessDecisionManager 的 decide方法来校验用户的权限是否足够
InterceptorStatusToken interceptorStatusToken = super.beforeInvocation(filterInvocation);
try {
// 执行下一个拦截器
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
} finally {
super.afterInvocation(interceptorStatusToken, null);
}
}

@Override
public void destroy() {

}

@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}

/**
* 资源源数据定义,设置为自定义的 SecureResourceFilterInvocationDefinitionSource
*
* @return
*/
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return securityMetadataSource;
}

public void setOauth2AccessDecisionManager(Oauth2AccessDecisionManager accessDecisionManager) {
super.setAccessDecisionManager(accessDecisionManager);
}

@Override
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
super.setAuthenticationManager(authenticationManager);
}

public void setSecurityMetadataSource(Oauth2FilterInvocationSecurityMetadataSource securityMetadataSource) {
this.securityMetadataSource = securityMetadataSource;
}
}

看下父类的 beforeInvocation 方法,其中省略了一些不重要的代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public abstract class AbstractSecurityInterceptor implements InitializingBean, ApplicationEventPublisherAware, MessageSourceAware {
protected InterceptorStatusToken beforeInvocation(Object object) {
// 代码省略

// 根据 SecurityMetadataSource 获取配置的权限属性
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);

// 代码省略

// 判断是否需要对认证实体重新认证,默认为否
Authentication authenticated = authenticateIfRequired();

// Attempt authorization
try {
// 决策管理器开始决定是否授权,如果授权失败,直接抛出 AccessDeniedException
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));

throw accessDeniedException;
}

// 代码省略
}
}

自定义资源源数据定义 Oauth2FilterInvocationSecurityMetadataSource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package com.fengxuechao.examples.auth.authorization;

import com.fengxuechao.examples.auth.service.UserRolePermissionService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;

import java.util.Collection;

/**
* 资源源数据定义,即定义某一资源可以被哪些角色访问
*
* @author fengxuechao
* @version 0.1
* @date 2019/6/14
*/
@Slf4j
@Component
public class Oauth2FilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource, InitializingBean {

private UserRolePermissionService service;

public Oauth2FilterInvocationSecurityMetadataSource(UserRolePermissionService service) {
this.service = service;
}


@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
if ("/user/profile".equals(((FilterInvocation) object).getRequestUrl())) {
// [/user/profile] 不需要鉴权
return null;
}
/*if (object instanceof FilterInvocation) {
FilterInvocation fi = (FilterInvocation) object;
String requestUrl = fi.getRequestUrl();
// 返回请求所需的权限
List<Role> roleList = service.findRoleListByPermissionUrl(requestUrl);
String[] roleArray = new String[roleList.size()];
roleArray = roleList.toArray(roleArray);
return SecurityConfig.createList(roleArray);
}
return Collections.EMPTY_LIST;*/
return SecurityConfig.createList("ROLE_ADMIN");
}

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

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

@Override
public void afterPropertiesSet() throws Exception {

}
}

为了调试的方便,直接定死任何访问请求都需要管理员权限(/user/profile 除外),调试通过后,再往里面添加业务逻辑代码。

自定义决策管理器 Oauth2AccessDecisionManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package com.fengxuechao.examples.auth.authorization;

import lombok.extern.slf4j.Slf4j;
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 org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.Iterator;

/**
* 访问决策器,决定某个用户具有的角色,是否有足够的权限去访问某个资源
*
* @author fengxuechao
* @version 0.1
* @date 2019/6/14
*/
@Slf4j
@Component
public class Oauth2AccessDecisionManager implements AccessDecisionManager {

/**
* @param authentication 用户凭证
* @param resource 资源 URL
* @param configAttributes 资源 URL 所需要的权限
* @throws AccessDeniedException 资源拒绝访问
* @throws InsufficientAuthenticationException 用户凭证不符
*/
@Override
public void decide(Authentication authentication, Object resource, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
log.info("[决策管理器]:开始判断请求 {} 需要的权限", ((FilterInvocation) resource).getRequestUrl());
if (configAttributes == null || configAttributes.isEmpty()) {
log.info("[决策管理器]:请求 {} 无需权限", ((FilterInvocation) resource).getRequestUrl());
return;
}
log.info("[决策管理器]:请求 {} 需要的权限 - {}", ((FilterInvocation) resource).getRequestUrl(), configAttributes);
// 判断用户所拥有的权限,是否符合对应的Url权限,用户权限是实现 UserDetailsService#loadUserByUsername 返回用户所对应的权限
Iterator<ConfigAttribute> ite = configAttributes.iterator();
log.info("[决策管理器]:用户 {} 拥有的权限 - {}", authentication.getName(), authentication.getAuthorities());
while (ite.hasNext()) {
ConfigAttribute neededAuthority = ite.next();
String neededAuthorityStr = neededAuthority.getAttribute();
for (GrantedAuthority existingAuthority : authentication.getAuthorities()) {
if (neededAuthorityStr.equals(existingAuthority.getAuthority())) {
return;
}
}
}
log.info("[决策管理器]:用户 {} 没有访问资源 {} 的权限!", authentication.getName(), ((FilterInvocation) resource).getRequestUrl());
throw new AccessDeniedException("权限不足!");
}

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

/**
* 是否支持 FilterInvocationSecurityMetadataSource 需要将这里的false改为true
*
* @param clazz
* @return
*/
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}

配置自定义鉴权过滤器 Oauth2FilterSecurityInterceptor 在 Spring Security 过滤器链中的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package com.fengxuechao.examples.auth.config;

import com.fengxuechao.examples.auth.authorization.Oauth2AccessDecisionManager;
import com.fengxuechao.examples.auth.authorization.Oauth2FilterInvocationSecurityMetadataSource;
import com.fengxuechao.examples.auth.authorization.Oauth2FilterSecurityInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;

/**
* @author fengxuechao
* @version 0.1
* @date 2019/5/8
*/
@Slf4j
@EnableResourceServer
@Configuration
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

@Autowired
AuthenticationManager manager;

@Autowired
Oauth2AccessDecisionManager accessDecisionManager;

@Autowired
Oauth2FilterInvocationSecurityMetadataSource securityMetadataSource;

@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
http.addFilterAfter(createApiAuthenticationFilter(), FilterSecurityInterceptor.class);
}

/**
* API权限控制
* 过滤器优先度在 FilterSecurityInterceptor 之后
* spring-security 的默认过滤器列表见 https://docs.spring.io/spring-security/site/docs/5.0.0.M1/reference/htmlsingle/#ns-custom-filters
*
* @return
*/
private Oauth2FilterSecurityInterceptor createApiAuthenticationFilter() {
Oauth2FilterSecurityInterceptor interceptor = new Oauth2FilterSecurityInterceptor();
interceptor.setAuthenticationManager(manager);
interceptor.setAccessDecisionManager(accessDecisionManager);
interceptor.setSecurityMetadataSource(securityMetadataSource);
return interceptor;
}
}

配置用户权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.fengxuechao.examples.auth.userdetails;

import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

/**
* @author fengxuechao
* @version 0.1
* @date 2019/5/15
*/
@Component
public class UserDetailsServiceImpl implements UserDetailsService {

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return new User(username, "123456", AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
}
}

演示结果

用户拥有资源所需权限

请求:

request
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GET http://localhost:8080/order/1

HTTP/1.1 200
X-Application-Context: application:inMemory
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/plain;charset=UTF-8
Content-Length: 12
Date: Tue, 18 Jun 2019 01:50:48 GMT

order id : 1

Response code: 200; Time: 57ms; Content length: 12 bytes

日志:

1
2
3
4
5
6
7
8
2019-06-18 09:50:48.955  INFO 5288 --- [nio-8080-exec-3] .f.e.a.a.Oauth2FilterSecurityInterceptor : Oauth2FilterSecurityInterceptor doFilter
2019-06-18 09:50:48.955 DEBUG 5288 --- [nio-8080-exec-3] .f.e.a.a.Oauth2FilterSecurityInterceptor : Secure object: FilterInvocation: URL: /order/1; Attributes: [ROLE_USER]
2019-06-18 09:50:48.956 DEBUG 5288 --- [nio-8080-exec-3] .f.e.a.a.Oauth2FilterSecurityInterceptor : Previously Authenticated: org.springframework.security.oauth2.provider.OAuth2Authentication@f5aeefea: Principal: org.springframework.security.core.userdetails.User@36ebcb: Username: user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: remoteAddress=127.0.0.1, tokenType=bearertokenValue=<TOKEN>; Granted Authorities: ROLE_USER
2019-06-18 09:50:48.956 INFO 5288 --- [nio-8080-exec-3] c.f.e.a.a.Oauth2AccessDecisionManager : [决策管理器]:开始判断请求 /order/1 需要的权限
2019-06-18 09:50:48.956 INFO 5288 --- [nio-8080-exec-3] c.f.e.a.a.Oauth2AccessDecisionManager : [决策管理器]:请求 /order/1 需要的权限 - [ROLE_USER]
2019-06-18 09:50:48.956 INFO 5288 --- [nio-8080-exec-3] c.f.e.a.a.Oauth2AccessDecisionManager : [决策管理器]:用户 user 拥有的权限 - [ROLE_USER]
2019-06-18 09:50:48.956 DEBUG 5288 --- [nio-8080-exec-3] .f.e.a.a.Oauth2FilterSecurityInterceptor : Authorization successful
2019-06-18 09:50:48.957 DEBUG 5288 --- [nio-8080-exec-3] .f.e.a.a.Oauth2FilterSecurityInterceptor : RunAsManager did not change Authentication object

用户没有资源所需权限

请求:

request
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GET http://localhost:8080/order/1

HTTP/1.1 403
Cache-Control: no-store
Pragma: no-cache
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 18 Jun 2019 01:44:49 GMT

{
"error": "access_denied",
"error_description": "权限不足!"
}

Response code: 403; Time: 35ms; Content length: 53 bytes

日志:

1
2
3
4
5
6
7
8
9
2019-06-18 09:44:44.684  INFO 10624 --- [nio-8080-exec-2] .f.e.a.a.Oauth2FilterSecurityInterceptor : Oauth2FilterSecurityInterceptor doFilter
2019-06-18 09:44:44.685 DEBUG 10624 --- [nio-8080-exec-2] .f.e.a.a.Oauth2FilterSecurityInterceptor : Public object - authentication not attempted
2019-06-18 09:44:49.448 INFO 10624 --- [nio-8080-exec-6] .f.e.a.a.Oauth2FilterSecurityInterceptor : Oauth2FilterSecurityInterceptor doFilter
2019-06-18 09:44:49.449 DEBUG 10624 --- [nio-8080-exec-6] .f.e.a.a.Oauth2FilterSecurityInterceptor : Secure object: FilterInvocation: URL: /order/1; Attributes: [ROLE_ADMIN]
2019-06-18 09:44:49.449 DEBUG 10624 --- [nio-8080-exec-6] .f.e.a.a.Oauth2FilterSecurityInterceptor : Previously Authenticated: org.springframework.security.oauth2.provider.OAuth2Authentication@22d262ad: Principal: org.springframework.security.core.userdetails.User@36ebcb: Username: user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: remoteAddress=127.0.0.1, tokenType=bearertokenValue=<TOKEN>; Granted Authorities: ROLE_USER
2019-06-18 09:44:49.450 INFO 10624 --- [nio-8080-exec-6] c.f.e.a.a.Oauth2AccessDecisionManager : [决策管理器]:开始判断请求 /order/1 需要的权限
2019-06-18 09:44:49.450 INFO 10624 --- [nio-8080-exec-6] c.f.e.a.a.Oauth2AccessDecisionManager : [决策管理器]:请求 /order/1 需要的权限 - [ROLE_ADMIN]
2019-06-18 09:44:49.450 INFO 10624 --- [nio-8080-exec-6] c.f.e.a.a.Oauth2AccessDecisionManager : [决策管理器]:用户 user 拥有的权限 - [ROLE_USER]
2019-06-18 09:44:49.451 INFO 10624 --- [nio-8080-exec-6] c.f.e.a.a.Oauth2AccessDecisionManager : [决策管理器]:用户 user 没有访问资源 /order/1 的权限!

返回结果和日志符合期望结果

参考资源

http://www.spring4all.com/article/422

在实现了 Oauth2 后,我想要在令牌增加中额外信息,那么该怎么做?

下面是我的做法,首先实现 org.springframework.security.oauth2.provider.token.TokenEnhancer 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.fengxuechao.examples.auth.config;

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 java.util.HashMap;
import java.util.Map;

import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;

/**
* token 额外信息
*
* @author fengxuechao
* @version 0.1
* @date 2019/5/16
*/
public class CustomTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Map<String, Object> additionalInfo = new HashMap<String, Object>();
additionalInfo.put("organization", authentication.getName() + randomAlphabetic(4));
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
}
}

然后在 AuthorizationServerConfigurerAdapter 认证服务代码中配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class AuthorizationServerConfigInJwt extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// token 携带额外信息
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), jwtTokenEnhancer()));
endpoints
.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancerChain)
.userDetailsService(userDetailsService)
.authenticationManager(authenticationManager)
.setClientDetailsService(clientDetailsService);
}

/**
* Token 额外信息
*
* @return
*/
@Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}

/**
* jwt token:使用了非对称密钥对来签署令牌:
* 1.生成 JKS Java KeyStore 文件:keytool -genkeypair -alias jwt_rsa -keyalg RSA -keypass 123456 -keystore jwt_rsa.jks -storepass 123456
* 2.导出公钥:keytool -list -rfc --keystore jwt_rsa.jks | openssl x509 -inform pem -pubkey
* 3.将 PUBLIC KEY 保存至 public.txt
*
* @return
*/
@Bean
public JwtAccessTokenConverter jwtTokenEnhancer() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(new KeyStoreKeyFactory(resource, keyStorePass.toCharArray()).getKeyPair(keyPairAlias));
// 使用对称密钥来签署令牌
// converter.setSigningKey("fengxuechao.littlefxc");
return converter;
}
}

或者 tokenServices.setTokenEnhancer(tokenEnhancer);

最后演示一下最终效果:

1
2
3
4
5
6
7
8
{
"access_token": "4aae3856-bc33-4e4d-86bc-eb475fc45569",
"token_type": "bearer",
"refresh_token": "fe2ed35d-5c53-4610-abb7-c1053cba6803",
"expires_in": 119,
"scope": "read",
"organization": "userAKqz"
}

jwt

1
2
3
4
5
6
7
8
9
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsicmVhZCJdLCJvcmdhbml6YXRpb24iOiJ1c2VyZWx2ayIsImV4cCI6MTU2MDQ4NDI0OCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9VU0VSIl0sImp0aSI6IjliNTU2ZTBiLTZlZmQtNDkwZC05OGMwLWIzYzYwNjM2ZDczMCIsImNsaWVudF9pZCI6ImNsaWVudCJ9.oaqlviXcQPCLAZP8cV7v-WA75AoiodiG6d2WR9yqJhOFCg7LDsnCjk63J59sq434CZHRIOkCgMi2hVJHOc4MTIFce61Kk046G3-yK313CtMy5LWeVXdKbAHH0gcuoDO3OCJ7u7GzngPtA6bVfxjJFNJ6MmFxEnFPjB5dos9Bb8zYduE2ELMH2aTCS-67R_aQ0BCZaYo5NMH1_jqz9d1hI_kpBx3auR_d2Vh1eJiC_f9Z-rTmRvXdwQefhwgXZ1UCWjV0NuoCqFO3KicEhjGOkqXZ5eh0vGR5zKwKJfCys1lNgXjXVVntHYkXt96ymQ9477pCAWCONZsbkM7244500Q",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsicmVhZCJdLCJvcmdhbml6YXRpb24iOiJ1c2VyZWx2ayIsImF0aSI6IjliNTU2ZTBiLTZlZmQtNDkwZC05OGMwLWIzYzYwNjM2ZDczMCIsImV4cCI6MTU2MDQ4NzcyOCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9VU0VSIl0sImp0aSI6IjJhNTUxOWJjLWUzYTAtNGJjOC1hNTRkLTlmMDNiMjYwNjZkNCIsImNsaWVudF9pZCI6ImNsaWVudCJ9.BgY6N0kzxVApFD-C7UVMDmczSoMY9tglnKzTkybfneoeAAs8ljftIwA5sPWua28Xhl-MNAQ9HL6Q6ou-EbgFlcHC2uPPbJ5silnPLPdTnvVko9l-8w-3WLPk96YbODdQemqFZHSrR1lPmXHB5sR7QjncxGxvuSYZEtPXxZz39lJbyQLSflXADqlk4ZV3BxS-M7d8FcTJEM1uTgwUBSns2N6AZnTkd2FnGskadaV2qhky5TznJjQqRETVS8xCiZCFYwCq5sAMHOj-_BrwlmCeoPfcy38ofbql-qVWfQJiAeU7yWLlAu_hd-zRIIbv-dqRmSF9T9rCxVPv84ptddO1Hw",
"expires_in": 119,
"scope": "read",
"organization": "userelvk",
"jti": "9b556e0b-6efd-490d-98c0-b3c60636d730"
}

最终返回的 Token 信息中多了一个属性 organization,结果符合期望结果。

简介

@JsonView是Jackson的一个注解,可以用来过滤序列化对象的字段属性,是你可以选择序列化对象哪些属性,哪些过滤掉。

使用步骤

  1. 使用接口来声明多个视图

  2. 在值对象的get方法上指定视图

  3. 在Controller方法上指定视图

步骤 1:使用接口来声明多个视图

使用同一个对象,面对不同的场景,去声明多个视图。

例如:

有一个 User 对象,里面有id、username、password、birthday等属性

  • 场景1:获得对象的用户名、密码

  • 场景2:获得对象的全部属性

为了测试,创建一个User实体对象,加入两个接口 UserSimpleView,UserDetailView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class User {

public interface UserSimpleView {};

public interface UserDetailView extends UserSimpleView {};

private String id;

private String username;

@NotBlank(message = "密码不能为空")
private String password;

@Past(message = "生日必须是过去的时间")
private Date birthday;

@JsonView(UserSimpleView.class)
public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

@JsonView(UserSimpleView.class)
public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

@JsonView(UserSimpleView.class)
public Date getBirthday() {
return birthday;
}

public void setBirthday(Date birthday) {
this.birthday = birthday;
}
}

步骤 2:在值对象的get方法上指定视图

在实体类 User 里的 get 方法上面加上 @JsonView 注解,并将它绑定到一个指定接口

分两类

  • @JsonView(UserSimpleView.class):绑定 id、username、birthday属性

  • @JsonView(UserDetailView.class):绑定 password 属性,继承 UserSimpleView 接口(相当于绑了 UserSimpleView 绑定的属性)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class User {

public interface UserSimpleView {};

public interface UserDetailView extends UserSimpleView {};

private String id;

private String username;

@NotBlank(message = "密码不能为空")
private String password;

@Past(message = "生日必须是过去的时间")
private Date birthday;

@JsonView(UserSimpleView.class)
public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

@JsonView(UserSimpleView.class)
public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

@JsonView(UserSimpleView.class)
public Date getBirthday() {
return birthday;
}

public void setBirthday(Date birthday) {
this.birthday = birthday;
}
}

步骤 3:在Controller方法上指定视图

在controller中俩个方法分别加上@JsonView注解,在分配上不同场景的接口

  • user1:输出视图1

  • user2:输出视图2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@RestController
@RequestMapping("user")
public class UserController {

@GetMapping
@JsonView(User.UserSimpleView.class)
public List<User> query(UserQueryCondition condition) {
System.out.println(ReflectionToStringBuilder.toString(condition, ToStringStyle.MULTI_LINE_STYLE));

List<User> users = new ArrayList<>();
users.add(new User());
users.add(new User());
users.add(new User());
return users;
}

/**
* {id:\d+}:正则表示只接受数字
*
* @param id
* @return
*/
@GetMapping("{id:\\d+}")
@JsonView(User.UserDetailView.class)
public User getInfo(@PathVariable String id) {
User user = new User();
user.setId(id);
user.setUsername("tom");
user.setPassword("tom");
return user;
}
}

测试

request
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
GET http://localhost:8080/user

HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 26 Jun 2019 00:41:03 GMT

[
{
"id": null,
"username": null,
"birthday": null
},
{
"id": null,
"username": null,
"birthday": null
},
{
"id": null,
"username": null,
"birthday": null
}
]

返回结果中没有密码属性

request
1
2
3
4
5
6
7
8
9
10
11
12
13
GET http://localhost:8080/user/1

HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 26 Jun 2019 00:41:56 GMT

{
"id": "1",
"username": "tom",
"password": "tom",
"birthday": null
}

返回结果中包含了密码字段。

总结

说明 @OneView 只会序列化 username、password 属性,TwoView 会序列化username、password、realName、sex属性。

因此想设置不同接口的不同场景,可以用 @JsonView 达到某些目的。

MySQL数据类型

  • 数字类型
    • 整数: tinyint、smallint、mediumint、int、bigint
    • 浮点数: float、double、real、decimal
  • 日期和时间: date、time、datetime、timestamp、year
  • 字符串类型
    • 字符串: char、varchar
    • 文本: tinytext、text、mediumtext、longtext
  • 二进制(可用来存储图片、音乐等): tinyblob、blob、mediumblob、longblob

数字类型

整型

type Storage(Bytes) Signed Range Unsigned Range
TINYINT 1 -128-127 0-255
SMALLINT 2 -32768-32767 0-65535
MEDIUMINT 3 -8388608-8388607 0-16777215
INT 4 -2147483648-2147483647 0-4294967295
BIGINT 8 -9223372036854775808-9223372036854775807 0-18446744073709551615

浮点型

属性 存储空间 精度 精确性 说明
FLOAT(M, D) 4 bytes 单精度 非精确 单精度浮点型,m总个数,d小数位
DOUBLE(M, D) 8 bytes 双精度 比Float精度高 双精度浮点型,m总个数,d小数位
  • FLOAT容易造成精度丢失

定点数DECIMAL

  • 高精度的数据类型,常用来存储交易相关的数据
  • DECIMAL(M,N).M代表总精度,N代表小数点右侧的位数(标度)
  • 1 < M < 254, 0 < N < 60;
  • 存储空间变长

时间类型

类型 字节 精确性
DATE 三字节 2015-05-01 精确到年月日
TIME 三字节 11:12:00 精确到时分秒
DATETIME 八字节 2015-05-01 11::12:00 精确到年月日时分秒
TIMESTAMP 2015-05-01 11::12:00 精确到年月日时分秒
  • MySQL在5.6.4版本之后,TIMESTAMPDATETIME支持到微秒。
  • TIMESTAMP会根据系统时区进行转换,DATETIME则不会
  • 存储范围的区别
    • TIMESTAMP存储范围:1970-01-01 00::00:01 to 2038-01-19 03:14:07
    • DATETIME的存储范围:1000-01-01 00:00:00 to 9999-12-31 23:59:59
  • 一般使用TIMESTAMP国际化
  • 如存时间戳使用数字类型BIGINT

字符串类型

类型 单位 最大 特性
CHAR 字符 最大为255字符 存储定长,容易造成空间的浪费
VARCHAR 字符 可以超过255个字符 存储变长,节省存储空间
TEXT 字节 总大小为65535字节,约为64KB -
  • TEXT在MySQL内部大多存储格式为溢出页,效率不如CHAR
  • Mysql默认为utf-8,那么在英文模式下1个字符=1个字节,在中文模式下1个字符=3个字节。

其它

SERIALBIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE 的别名。

《SQL反模式》 阅读笔记

反模式分类

  • 逻辑数据库设计反模式
  • 物理书库设计反模式
  • 查询反模式
  • 应用程序开发反模式

反模式分解

  • 目的
  • 反模式
  • 如何识别反模式
  • 合理使用反模式
  • 解决方案

ER 图示例

ER 图示例

范例数据库

SERIAL 是 MySQL 中 BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE 的别名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
CREATE TABLE IF NOT EXISTS Accounts (
account_id SERIAL PRIMARY KEY,
account_name VARCHAR(20),
first_name VARCHAR(20),
last_name VARCHAR(20),
email VARCHAR(100),
password_hash CHAR(64),
portrait_image BLOB,
hourly_rate NUMERIC(9,2)
);

CREATE TABLE IF NOT EXISTS BugStatus (
`status` VARCHAR(20) PRIMARY KEY
);

CREATE TABLE IF NOT EXISTS Bugs (
bug_id SERIAL PRIMARY KEY,
date_reported DATE NOT NULL,
summary VARCHAR(80),
description VARCHAR(1000),
resolition VARCHAR(1000),
reported_by BIGINT UNSIGNED NOT NULL,
assigned_to BIGINT UNSIGNED,
verified_by BIGINT UNSIGNED,
`status` VARCHAR(20) NOT NULL DEFAULT 'NEW',
priority VARCHAR(20),
hours NUMERIC(9,2),
FOREIGN KEY (reported_by) REFERENCES Accounts(account_id),
FOREIGN KEY (assigned_to) REFERENCES Accounts(account_id),
FOREIGN KEY (verified_by) REFERENCES Accounts(account_id),
FOREIGN KEY (`status`) REFERENCES BugStatus(`status`)
);

CREATE TABLE IF NOT EXISTS Comments (
comment_id SERIAL PRIMARY KEY,
bug_id BIGINT UNSIGNED NOT NULL,
author BIGINT UNSIGNED NOT NULL,
comment_date DATETIME NOT NULL,
comment TEXT NOT NULL,
FOREIGN KEY (bug_id) REFERENCES Bugs(bug_id),
FOREIGN KEY (author) REFERENCES Accounts(account_id)
);

CREATE TABLE IF NOT EXISTS Screenhots (
bug_id BIGINT UNSIGNED NOT NULL,
image_id BIGINT UNSIGNED NOT NULL,
screenshot_image BLOB,
caption VARCHAR(100),
PRIMARY KEY (bug_id,image_id),
FOREIGN KEY (bug_id) REFERENCES Bugs(bug_id)
);

CREATE TABLE IF NOT EXISTS Tags (
bug_id BIGINT UNSIGNED NOT NULL,
tag VARCHAR(20) NOT NULL,
PRIMARY KEY (bug_id,tag),
FOREIGN KEY (bug_id) REFERENCES Bugs(bug_id)
);

CREATE TABLE IF NOT EXISTS Products (
product_id SERIAL PRIMARY KEY,
product_name VARCHAR(50) NOT NULL
);

CREATE TABLE IF NOT EXISTS BugsProducts (
bug_id BIGINT UNSIGNED NOT NULL,
product_id BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (bug_id,product_id),
FOREIGN KEY (bug_id) REFERENCES Bugs(bug_id),
FOREIGN KEY (product_id) REFERENCES Products(product_id)
);

文件系统常用命令

ls / : 查看根目录下的子节点(文件夹和文件)信息

ls -al : -a 显示隐藏文件 -l 以更详细的列表形式显示

mkdir aaa : 创建相对路径文件夹的写法

mkdir -p aaa/bbb/ccc

rmdir : 可以删除空目录

rm -r aaa : 可以把 aaa 整个文件夹及其中的所有子节点全都删除

rm -fr aaa : 强制删除 aaa

touch somefile.txt : 创建一个空文件

> : 重定向 echo "利用 > 重定向的功能,将一定指令的输出结果写入到一个文件中,会覆盖原文件内容" > somefile.txt

>> : 追加 echo "利用 >> 可以把字符串追加到一个文件中,不会覆盖原文件内容" >> somefile.txt

vi 编辑器的一些快捷键

一般模式

a : 在光标后一位开始插入

A : 在该行的最后插入

I : 在该行的最前面插入

gg : 直接跳到文件的首行

G :

编辑模式

1. consul 集群模式

1
consul agent -server -data-dir=./data/ -node=s1 -bind=192.168.217.86 -ui -rejoin -client=0.0.0.0 -bootstrap-expect=1 -ui

参数介绍:

  • -server 表示是以服务端身份启动,去掉这个参数表示 client 模式
  • -bind 表示绑定到哪个ip(有些服务器会绑定多块网卡,可以通过bind参数强制指定绑定的ip),一般是本机IP
  • -client 指定客户端访问的ip(consul有丰富的api接口,这里的客户端指浏览器或调用方),0.0.0.0表示不限客户端ip
  • -bootstrap-expect=3 表示server集群最低节点数为3,低于这个值将工作不正常(注:类似zookeeper一样,通常集群数为奇数,方便选举,consul采用的是raft算法)
  • -data-dir 表示指定数据的存放(持久化)目录(该目录必须存在)
  • -node 表示节点在web ui中显示的名称

启动成功后,终端窗口不要关闭,可以在浏览器里,访问下,类似 http://192.168.212.73:8500/, 正常的话,可以正常打开web界面。

为了防止终端关闭后,consul退出,可以在刚才命令上,加点东西,类似:nohup xxx > /dev/null 2>&1 &

1.1. 命令行建立 consul 集群

服务端1:

1
consul agent -server -bootstrap-expect=2 -data-dir=./data/ -bind=192.168.217.134 -client=0.0.0.0 -node=s1 -ui

服务端2:

1
consul agent -server -bootstrap-expect=2 -data-dir=./data/ -bind=192.168.217.72 -client=0.0.0.0 -join 192.168.217.134 -node=s2 -ui

服务端3:

1
consul agent -server -bootstrap-expect=2 -data-dir=./data/ -bind=192.168.217.86 -client=0.0.0.0 -join 192.168.217.134 -node=s3 -ui

客户端1:

1
consul agent -data-dir=./data/ -bind=192.168.217.87 -client=0.0.0.0 -join 192.168.217.134 -node=c1 -ui

1.2. 配置文件方式建立 consul 集群

在这里插入图片描述

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
/* 表示server集群最低节点数 */
"bootstrap_expect": 2,
/* 表示该节点绑定的地址, 默认0.0.0.0 */
"bind_addr": "192.168.217.86",
/* 访问该节点的地址, 默认127.0.0.1*/
"client_addr": "0.0.0.0",
/* 访问该节点的数据中心*/
"datacenter": "data-center-1",
/* 必须, 数据保存的地址*/
"data_dir": "/home/awifi/consul-server/data",
"addresses": {
/* 表示该节点绑定的dns地址, 以空格分隔的要绑定的地址列表*/
"dns": "192.168.217.86",
/* 表示该节点绑定的http地址, 以空格分隔的要绑定的地址列表*/
"http": "192.168.217.86 127.0.0.1"
},
/* 节点名,必须*/
"node_name": "server:192.168.212.73:8500",
"rejoin_after_leave": true,
/* 表示该节点类型是server*/
"server": true,
/* 允许在第一次尝试失败时重试连接 */
"retry_join": [
"192.168.217.72",
"192.168.217.134"
],
/* 表示开启web界面*/
"ui": true
}

然后在另一台机器上执行
./consul join 192.168.217.86
或者可以加入配置

1
2
3
4
{
"start_join":[],
"retry_join":[]
}

start_join:等同于 命令行参数 -join : 表示启动时加入的集群地址
retry_join:等同于 命令行参数 -retry-join : 允许在第一次尝试失败时重试连接,该列表可以包含IPv4,IPv6或DNS地址。

1.2.1. 为什么 http 地址要绑定 127.0.0.1 ?

如果不绑定127.0.0.1,在该服务器上就会无法使用除 ./consul agent 以外的命令,

例如:
组建集群模式的核心命令:consul join 192.168.217.86
优雅的注销节点的命令: consul leave
在这里插入图片描述

2. 查看集群状态

1
consul operator raft list-peers

运行结果:

查看集群状态

运行结果红色方框部分可以看出集群模式中谁是 leader