开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Golang Time时间函数操作 Go主题月

发表于 2021-04-19

时间和日期是我们编程中经常会用到的,本文主要介绍了Go语言内置的time包的基本用法。

time包

time包提供了时间的显示和测量用的函数。日历的计算采用的是公历。

时间类型

time.Time类型表示时间。我们可以通过time.Now()函数获取当前的时间对象,然后获取时间对象的年月日时分秒等信息。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码func timeDemo() {
now := time.Now() //获取当前时间
fmt.Printf("current time:%v\n", now)

year := now.Year() //年
month := now.Month() //月
day := now.Day() //日
hour := now.Hour() //小时
minute := now.Minute() //分钟
second := now.Second() //秒
fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second)
}

时间戳

时间戳是自1970年1月1日(08:00:00GMT)至当前时间的总毫秒数。它也被称为Unix时间戳(UnixTimestamp)。

基于时间对象获取时间戳的示例代码如下:

1
2
3
4
5
6
7
go复制代码func timestampDemo() {
now := time.Now() //获取当前时间
timestamp1 := now.Unix() //时间戳
timestamp2 := now.UnixNano() //纳秒时间戳
fmt.Printf("current timestamp1:%v\n", timestamp1)
fmt.Printf("current timestamp2:%v\n", timestamp2)
}

使用time.Unix()函数可以将时间戳转为时间格式。

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码
func timestampDemo2(timestamp int64) {
timeObj := time.Unix(timestamp, 0) //将时间戳转为时间格式
fmt.Println(timeObj)
year := timeObj.Year() //年
month := timeObj.Month() //月
day := timeObj.Day() //日
hour := timeObj.Hour() //小时
minute := timeObj.Minute() //分钟
second := timeObj.Second() //秒
fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second)
}

时间间隔
time.Duration是time包定义的一个类型,它代表两个时间点之间经过的时间,以纳秒为单位。time.Duration表示一段时间间隔,可表示的最长时间段大约290年。

time包中定义的时间间隔类型的常量如下:

1
2
3
4
5
6
7
8
ini复制代码const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)

例如:time.Duration表示1纳秒,time.Second表示1秒。

时间操作
Add
我们在日常的编码过程中可能会遇到要求时间+时间间隔的需求,Go语言的时间对象有提供Add方法如下:

func (t Time) Add(d Duration) Time
举个例子,求一个小时之后的时间:

1
2
3
4
5
css复制代码func main() {
now := time.Now()
later := now.Add(time.Hour) // 当前时间加1小时后的时间
fmt.Println(later)
}

Sub

求两个时间之间的差值:

func (t Time) Sub(u Time) Duration
返回一个时间段t-u。如果结果超出了Duration可以表示的最大值/最小值,将返回最大值/最小值。要获取时间点t-d(d为Duration),可以使用t.Add(-d)。

Equal

func (t Time) Equal(u Time) bool
判断两个时间是否相同,会考虑时区的影响,因此不同时区标准的时间也可以正确比较。本方法和用t==u不同,这种方法还会比较地点和时区信息。

Before

func (t Time) Before(u Time) bool
如果t代表的时间点在u之前,返回真;否则返回假。

After

func (t Time) After(u Time) bool
如果t代表的时间点在u之后,返回真;否则返回假。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Java】一图看懂Java中的PO,BO,VO,DTO,P

发表于 2021-04-19

image.png

PO(Persistant Object - 持久化对象)

该概念随着ORM产生,可以看成是与数据库中的表相映射的Java对象。通常就是对应数据库中某个表中的一条记录。PO仅仅用于表示数据,没有任何数据操作。通常遵守Java Bean的规范,拥有 getter/setter方法。

  • PO的生命周期:是向数据库中添加新数据时创建,删除数据库中数据时削除的。并且它只能存活在一个数据库连接中,断开连接即被销毁。
  • PO的作用:可以把数据表中一条记录作为一个对象处理,可以方便的转为其它对象。PO是有状态的,每个属性代表其当前的状态。使用它,可以使我们的程序与物理数据解耦,并且可以简化对象数据与物理数据之间的转换。
  • PO的特点:
    • PO的属性是跟数据库表的字段一一对应的
    • PO对象需要实现序列化接口
    • 一个POJO持久化后就是PO

BO(Business Object - 业务对象)

BO用于表示一个业务对象,它包括了业务逻辑,常常封装了对DAO和RPC等的调用,可以进行PO与VO/DTO之间的转换。

BO通常位于业务层,要区别于直接对外提供服务的服务层:BO提供了基本业务单元的基本业务操作,在设计上属于被服务层业务流程调用的对象,一个业务流程可能需要调用多个BO来完成。

DO(Domain Object - 领域对象)

领域对象就是从现实世界中抽象出来的有形或无形的业务实体。通常位于业务层中。

VO(Value Object/View Object - 值对象/视图对象)

Value Object,值对象,也称为业务对象,是存活在业务层的,是业务逻辑使用的,它存活的目的就是为数据提供一个生存的地方(实际上跟DO有点类似)。

VO的属性是根据当前业务的不同而不同的,也就是说,它的每一个属性都一一对应当前业务逻辑所需要的数据的名称。

VO通常用于业务层之间的数据传递,其仅仅包含数据。但应是抽象出的业务对象。根据业务的需要,其可以和表对应或者不。用new关键字创建,由GC进行回收。


View Object,视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来,对应整个界面的值

DTO(Data Transfer Object - 数据传输对象)

DTO概念来源于J2EE的设计模式,原来的目的是为了EJB的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载。

DTO用于表示一个数据传输对象,通常用于不同服务或服务不同分层之间的数据传输。

DTO与VO与类似,但也有一些不同,这个不同主要是设计理念上的,比如API服务需要使用的是DTO,而用于展示层页面的使用的是VO。例如,为了展示方便,在VO的性别字段存的是男和女,而在DTO中存的是1或者2这样的代码。

DAO(Data Access Object - 数据访问对象)

DAO是SUN公司的一个标准J2EE设计模式,这个模式中有个接口就是 DAO,负责持久层的操作并为业务层提供接口。此对象用于访问数据库。通常和PO结合使用。

DAO中包含了各种数据库的操作方法。通过它的方法结合PO对数据库进行CRUD的操作。

POJO(Plain Ordinary Java Object - 简单无规则Java对象)

POJO表示一个简单Java对象。PO、VO、DTO都是典型的POJO。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JDK,JRE,JVM 三者区别

发表于 2021-04-19

作为一个 Java 开发者,拿到电脑的第一件时间肯定是安装 JDK 以及配置环境。相信配置环境肯定都是轻车熟路的,但是 JDK,JRE,JVM不一定能明明白白的讲出来。

1、JDK

JDK(Java SE Development Kit),Java标准开发包,它提供了编译、运行Java程序所需的各种工具和资源,包括Java编译器、Java运行时环境,以及常用的Java类库等。

2、JRE

JRE(Java Runtime Environment),Java 运行环境。

3. JVM

JVM(Java Virtual Mechinal),Java 虚拟机,是 JRE 的一部分。它是整个java实现跨平台的最核心的部分,负责解释执行字节码文件,是可运行 Java 字节码文件的虚拟计算机。所有平台的上的JVM向编译器提供相同的接口,而编译器只需要面向虚拟机,生成虚拟机能识别的代码,然后由虚拟机来解释执行。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Win10系统下JDK18下载、安装及环境变量配置

发表于 2021-04-19
  1. JDK1.8下载

网上资源很多,也可以去去Oracle官网下载(www.oracle.com/downloads/i…

这里给大家提供一个网盘链接资源

JDK1.8

链接:pan.baidu.com/s/1w3pO5jrV…

提取码:in1w
2. JDK安装完毕
3. 进入计算机-系统属性-高级系统设置-高级-环境变量
4. 新建系统变量:JAVA_HOME 、CLASSPATH 和Path
5. 变量名:JAVA_HOME

变量值:C:\Program Files\Java\jdk1.8.0_111
6. 变量名:CLASSPATH

变量值:.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar
7. 变量名:Path

变量值:C:\Program Files\Java\jdk1.8.0_111\bin;C:\Program Files\Java\jdk1.8.0_111\jre\bin;
8. 测试环境变量配置是否成功。同时按住Win和R键,桌面左下角弹出‘运行’窗口,输入cmd,再回车;跳出DOS命令行窗口输入依次输入“javac”、“java”、“java -version”

在这里插入图片描述

  1. 出现以上结果,说明安装完成,环境变量配置成功。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

ASPNET WebApi项目框架搭建 (一)创建项目

发表于 2021-04-19

(一)创建项目

一、新建webapi项目

1.打开VS2019,新建项目,选择ASP.NET Web 应用程序(.NET Framework),框架选择.NET Framework4.5,如下图所示。

image.png

2.选择空项目,勾选Web API选项,去掉https支持,如下图所示

image.png

3.一般在前后端分离的项目中,后端返回的事json格式的数据,但是我们浏览器中显示的是xml格式的,这里需要修改“WebApiConfig”,添加以下代码,让它默认显示JSON的数据
1
2
3
4
5
6
7
8
9
c#复制代码var formatters = config.Formatters.Where(formatter =>
formatter.SupportedMediaTypes.Where(media =>
media.MediaType.ToString() == "application/xml" || media.MediaType.ToString() == "text/html").Count() > 0) //找到请求头信息中的介质类型
.ToList();

foreach (var match in formatters)
{
config.Formatters.Remove(match); //移除请求头信息中的XML格式
}
4.Model文件夹下新建一个Person实体类
1
2
3
4
5
6
7
c#复制代码public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public string Sex { get; set; }
public int Age { get; set; }
}
5.在我们的控制器里写一个Get请求方法,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
c#复制代码 Person[] person = new Person[]
{
new Person { Id = 1, Name = "张三", Sex = "男", Age = 18 },
new Person { Id = 1, Name = "李四", Sex = "女", Age = 18 },
new Person { Id = 1, Name = "王二", Sex = "男", Age = 22 },
new Person { Id = 1, Name = "麻子", Sex = "男", Age = 23 },

};

[HttpGet]
public IHttpActionResult index()
{
return Ok(person);

}

二.参数检查验证

1.Nuget安装FluentValidation.WebApi

image.png

2.修改Pserson类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c#复制代码[Validator(typeof(PersonValidator))]
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public string Sex { get; set; }
public int Age { get; set; }
}

public class PersonValidator : AbstractValidator<Person>
{
public PersonValidator()
{
RuleFor(m => m.Id).NotEmpty().NotNull().WithMessage("Id不能为空");
RuleFor(m => m.Name).NotEmpty().NotNull().WithMessage("Name不能为空");
}
}
3.让 FluentValidation 生效,在 WebApiConfig中添加如下配置
1
2
3
4
5
6
7
8
c#复制代码public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
...
FluentValidationModelValidatorProvider.Configure(config);
}
}

复制代码

4.新建Filter文件夹并添加ParamsFilterAttribute类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c#复制代码public class ParamsFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
//如果参数非法
if ( !actionContext.ModelState.IsValid)
{
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);

}
//如果没有输入参数
else if (actionContext.ActionArguments.Values.First() == null)
{
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest,"请输入参数!");
}
}
}
5.控制器新建一个post请求
1
2
3
4
5
6
7
c#复制代码[HttpPost]
[ParamsFilter]
[Route("params")]
public IHttpActionResult Params([FromBody] Person person)
{
return Json(person);
}

postman模拟post请求,在body什么都不输入,提示请输入参数:

输入id,不输入name,提示name不能为空:

输入正确的参数,返回了数据:

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

手把手教你快速开发部署一个 Java 后端 Hello wo

发表于 2021-04-19
  1. 前言

使用 Java 语言实现服务端应用的企业占比很大,特别是现今 Web 应用大展异彩的时刻。作为 web 前端开发,掌握 Java 的开发有利于你的职业更进一步。为了让你快速入门 Java,今天纳撸多要分享的是如何快速开发部署一个 Java web 后端简单应用。

文章大纲:

  • 开发制品,编写 Java 代码
  • 制品构建,打包 Java 代码
  • 部署制品,部署 Java 代码
  1. 如何开发制品

一个 web 应用简单考虑由三部分组成:浏览器作为客户端,Java 应用作为服务端,通信协议是 HTTP。我们要实现的效果是:在浏览器地址栏,访问应用地址,在页面中打印出 “Hello, naluduo" 文字,为此我们需要使用 Java 编写一个 HTTP 服务器响应浏览器端的访问请求。

1.1 Servlet 入门

编写 HTTP 服务器其实不难,在 Java 中只需要先编写给予多线程的 TCP 服务器,然后在一个 TCP 连接中读取 HTTP 请求,发送 HTTP 响应即可,可以看到下图中的客户端应用与服务端应用通信结构:

1
2
3
4
5
6
7
8
9
sh复制代码┌───────────┐                                   ┌───────────┐
│Application│ │Application│
├───────────┤ ├───────────┤
│ Socket │ │ Socket │
├───────────┤ ├───────────┤
│ TCP │ │ TCP │
├───────────┤ ┌──────┐ ┌──────┐ ├───────────┤
│ IP │<────>│Router│<─────>│Router│<────>│ IP │
└───────────┘ └──────┘ └──────┘ └───────────┘

现在我们使用 Java 代码编写一个 HTTP 服务器,这块代码理解下传统的开发流程,相对底层。不想看,可以直接跳过。

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
java复制代码import java.io.BufferedReader; // 缓冲读取
import java.io.BufferedWriter; // 缓冲写入
import java.io.IOException; // 异常
import java.io.InputStream; // 输入流
import java.io.InputStreamReader; // 输入流读取
import java.io.OutputStream; // 输出流
import java.io.OutputStreamWriter; // 输出
import java.net.ServerSocket; // socket 包
import java.net.Socket;
import java.nio.charset.StandardCharsets;

public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(5050); // 监听指定端口
System.out.println("server is running...");
for (;;) {
Socket sock = ss.accept();
System.out.println("connected from " + sock.getRemoteSocketAddress());
Thread t = new Handler(sock); // 新建线程
t.start();
}
}
}

class Handler extends Thread {
Socket sock;

public Handler(Socket sock) {
this.sock = sock;
}

@Override
public void run() {
try (InputStream input = this.sock.getInputStream()) {
try (OutputStream output = this.sock.getOutputStream()) {
handle(input, output);
}
} catch (Exception e) {
try {
this.sock.close();
} catch (IOException ioe) {
}
System.out.println("client disconnected.");
}
}

private void handle(InputStream input, OutputStream output) throws IOException {
System.out.println("Process new http request...");
var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
// 读取HTTP请求:
boolean requestOk = false;
String first = reader.readLine();
if (first.startsWith("GET / HTTP/1.")) {
requestOk = true;
}
for (;;) {
String header = reader.readLine();
if (header.isEmpty()) { // 读取到空行时, HTTP Header读取完毕
break;
}
System.out.println(header);
}
System.out.println(requestOk ? "Response OK" : "Response Error");
if (!requestOk) {
// 发送错误响应:
writer.write("404 Not Found\r\n");
writer.write("Content-Length: 0\r\n");
writer.write("\r\n");
writer.flush();
} else {
// 发送成功响应:
String data = "<html><body><h1>Hello, Jecyu!</h1></body></html>";
int length = data.getBytes(StandardCharsets.UTF_8).length;
writer.write("HTTP/1.0 200 OK\r\n");
writer.write("Connection: close\r\n");
writer.write("Content-Type: text/html\r\n");
writer.write("Content-Length: " + length + "\r\n");
writer.write("\r\n"); // 空行标识Header和Body的分隔
writer.write(data);
writer.flush();
}
}
}

由上述代码可见,要编写一个完善的 HTTP 服务器,以 HTTP/1.1 为例,需要考虑的包括:

  • 识别正确和错误的 HTTP 请求
  • 识别正确和错误的 HTTP 头
  • 复用 TCP 连接
  • 复用线程
  • IO 异常处理
  • ……

为了简单起见,我们可以把处理 TCP 连接,解析 HTTP 协议这些底层工作统统交给现成的服务器去做,我们只需要把自己的应用程序跑在 Web 服务器上。为了实现这一目的,JavaEE 提供了 Servlet API,我们只需使用 Servlet API 编写自己的 Servlet 来处理 HTTP 请求,Web 服务器实现 Servlet API 接口(比如 Tomcat 服务器),实现底层功能:

1
2
3
4
5
6
7
sh复制代码                 ┌───────────┐
│My Servlet │
├───────────┤
│Servlet API│
┌───────┐ HTTP ├───────────┤
│Browser│<──────>│Web Server │
└───────┘ └───────────┘

综上,我们整体的开发部署步骤如下:

  • 编写 Servlet 代码。
  • 打包为 war 文件,即 Java Web Application Archive。
  • 复制到 Tomcat 的 webapps 目录下。
  • 启动 Tomcat 服务器,进行浏览器输入应用地址访问即可。

当然,在实践上述步骤之前,请确保你已经在电脑上安装了 Java 的运行环境,下载好 IntelliJ IDEA 编辑器,配置好 Java 版本环境。

1.2 使用 Maven 构建开发环境

在编写 Servlet 代码之前,为了提升开发效率,我们可以使用 Maven 构建开发环境。

Maven 是一个优秀的项目构建工具,可以很方便的对项目进行分模块构建,这样在开发和测试打包部署时,效率会提高很多。其次,Maven 进行依赖的管理,可以将不同系统的依赖进行统一管理,并且可以进行依赖之间的传递和继承。

Maven 遵循约定 》 配置 》编码,Maven 要负责项目的自动化构建,以编译为例,Maven 要想自动进行编译,那么它必须知道 Java 的源文件保存在哪里,这样约定之后,不用我们手动指定位置,Maven 能知道位置,从而帮我们完成自动编译。

  • Maven 使用 pom.xml 定义项目内容,并使用预设的目录结构;
  • 使用 Maven 中声明一个依赖项可以自动下载并导入 classpath;
  • Maven使用groupId,artifactId和version唯一定位一个依赖。
  1. 安装Maven

要安装Maven,可以从Maven官网下载最新的Maven 3.6.x,然后在本地解压,设置几个环境变量:

1
2
ini复制代码M2_HOME=/path/to/maven-3.6.x
PATH=$PATH:$M2_HOME/bin

然后,打开命令行窗口,输入mvn -version,应该看到Maven的版本信息:

1
2
3
sh复制代码Java version: 1.8.0_251, vendor: Oracle Corporation, runtime: /Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre
Default locale: zh_CN, platform encoding: UTF-8
OS name: "mac os x", version: "10.15.7", arch: "x86_64", family: "mac"
  1. 按照 Maven 的规约来设置目录:
  • /src/main/java/ :Java 源码。
  • /src/main/resource :Java 配置文件,资源文件。
  • /src/test/java/ :Java 测试代码。
  • /src/test/resource :Java 测试配置文件,资源文件。
  • /target :文件编译过程中生成的 .class 文件、jar、war 等等。
  • pom.xml :配置文件
  1. 使用 IntelliJ IDEA 编译器,在新建项目时选择 maven,可以自动帮我们生成以上目录:

image

  1. 生成目录后,我们要编写 pom.xml 文件,填写项目需要的依赖和编译配置,其中 maven 使用 groupId,artifactId和version唯一定位一个依赖。

本应用中,主要依赖为 javax.servlet 4.0.0 版本的 API,以及依赖的 java 版本为 1.8。注意到<scope>指定为provided,表示编译时使用,但不会打包到.war文件中,因为运行期Web服务器本身已经提供了Servlet API相关的jar包。

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>spring-framework-projects</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

1.3 编写 Servlet 代码

引入对应的 servlet 包,编写代码如下,可以看到代码比前面没使用 Servlet 时简洁不少。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

// WebServlet注解表示这是一个Servlet,并映射到地址/:
@WebServlet(urlPatterns = "/")
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 设置响应类型:
resp.setContentType("text/html");
// 获取输出流:
PrintWriter pw = resp.getWriter();
// 写入响应:
pw.write("<h1>Hello, naluduo!</h1>");
// 最后不要忘记flush强制输出:
pw.flush();
}
}

一个 Servlet 总是继承自HttpServlet,然后覆写doGet()或doPost()方法。注意到doGet()方法传入了HttpServletRequest和HttpServletResponse两个对象,分别代表 HTTP 请求和响应。我们使用 Servlet API 时,并不直接与底层 TCP 交互,也不需要解析 HTTP协议,因为 HttpServletRequest 和 HttpServletResponse 就已经封装好了请求和响应。以发送响应为例,我们只需要设置正确的响应类型,然后获取 PrintWriter ,写入响应即可。

  1. 怎样构建制品

2.1 Tomcat 部署规范

无论使用哪个服务器,只要它支持 Servlet API 4.0(因为我们引入的Servlet版本是4.0),我们的 war 包都可以在上面运行,这里我们使用开源免费的 Tomcat 服务器。

2.1.1 Tomcat 安装启动

点击 Tomcat 官网,选择对应的安装包进行下载,由于我本机上已经安装了一个 Tomcat 10 的版本,就不再重新安装。

image

安装好 Tomcat 后,目录如下,本指南只需要关注 bin 和 webapps、conf 目录。

  • BUILDING.txt
  • NOTICE
  • RUNNING.txt
  • lib:包含要在类路径上添加的更多资源。
+ 在大多数 servlet 容器中,Tomcat 还支持一种机制来安装库 JAR文件(或解压缩的类)一次,并使它们对所有已安装的 Web 应用程序可见(不必包含在 Web 应用程序本身中)。 “类装入器方法”文档中介绍了有关 Tomcat 如何查找和共享此类的详细信息。 在 Tomcat 安装中,共享代码通常使用的位置是 `$CATALINA_HOME / lib`。 放置在此处的 JAR 文件对于 Web 应用程序和内部 Tomcat 代码均可见。 这是放置应用程序或内部 Tomcat 使用(例如 JDBCRealm )所需的 JDBC 驱动程序的好地方。


开箱即用的标准 Tomcat 安装包括各种预安装的共享库文件,其中包括:Servlet 4.0 和 JSP 2.3 API是编写 Servlet 和 JavaServer Pages 的基础 。
  • webapps:Tomcat的主要Web发布目录,默认情况下把Web应用文件放于此目录
  • CONTRIBUTING.md
  • README.md
  • bin:存放 windows 或 Linux 平台上启动和关闭 Tomcat 的脚本文件
  • logs:存放 Tomcat 执行时的日志文件
  • work:存放 JSP 编译后产生的class文件
  • LICENSE
  • RELEASE-NOTES
  • conf:存放 Tomcat 服务器的各种全局配置文件,其中最重要的是 server.xml 和 web.xml。
  • temp:JVM 用于临时文件的目录

首先赋予启动 Catalia 权限,然后启动 Tomcat 服务器。

1
2
3
sh复制代码$ cd bin
chmod +x catalina.sh
sh startup.sh

如无意外,即可在 http://localhost:8080/ 进行访问如下页面:

image

接下来,我们要了解部署的容器的规范。

一个标准的应用目录如下:

  • *.html、*.jsp :HTML和JSP页面,以及应用程序的客户端浏览器必须可见的其他文件(例如JavaScript,样式表文件和图像)。 在较大的应用程序中,您可以选择将这些文件划分为子目录层次结构,但是对于较小的应用程序,通常只为这些文件维护一个目录要简单得多
  • /WEB-INF/web.xml:这是一个XML文件,描述了组成应用程序的servlet和其他组件,以及您希望服务器为您强制执行的所有初始化参数和容器管理的安全性约束。
  • /WEB-INF/classes/:此目录包含应用程序所需的所有Java类文件(和相关资源),包括servlet和非servlet类,这些文件未合并到JAR文件中。 如果将类组织为Java包,则必须在 /WEB-INF/classes/下的目录层次结构中反映出来。 例如,一个名为com.mycompany.mypackage.MyServlet 的 Java 类将需要存储在一个名为 /WEB-INF/classes/com/mycompany/mypackage/MyServlet.class 的文件中。
  • /WEB-INF/lib/:此目录包含 JAR 文件,这些文件包含您的应用程序所需的 Java 类文件(和相关资源),例如第三方类库或 JDBC 驱动程序。

使用 maven 可以把源代码打包输出如上文件。

2.2 打包构建

  1. 在 pom.xml 中 project 标签下添加声明,打包为 war 包
1
xml复制代码<packaging>war</packaging> <!-- 打包为 war 包 -->
  1. 在 src/main/webapp 新建 WEB-INF 文件夹,然后新建文件 web.xml,填写打包的应用信息
1
2
3
4
5
6
xml复制代码<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<display-name>Archetype Created Web Application</display-name>
</web-app>
  1. 执行打包命令
1
sh复制代码mvn clean package
  1. 获得打包后的文件如下:
    image
  1. 部署制品的做法

3.1 部署 war 应用

3. 1. 1 使用 GUI 界面
  1. 输入 Tomcat 地址 http://localhost:8080/manager/html,如果出现需要密码和账号登录,则需要进行配置,进入第 2 个步骤。
  2. 导航到按照 Tomcat 的目录文件夹,进入 /conf/tomcat-users.xml 进行配置,添加 manager-gui 角色和账号密码设置。
1
2
3
4
5
6
7
8
9
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<tomcat-users xmlns="http://tomcat.apache.org/xml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
version="1.0">

<role rolename="manager-gui"/>
<user username="admin" password="admin" roles="manager-gui"/>
</tomcat-users>
  1. 再次进入 manager 页面,并正确输入刚刚配置的账号和密码,导航到部署栏,在这里可以选择要上传的 war 文件,然后点击部署,即可进行访问。

image

  1. 在浏览器中进行访问如下图:

image

3.1.2 手动复制 war 文件到 webapps 内

直接将 war 包放到 webapps下,会自动解压为项目条件 server.xml 中。

1
2
xml复制代码<Host name="localhost"  appBase="webapps"
unpackWARs="true" autoDeploy="true">

3.2 解决 tomcat 部署 war 404 问题

按照 3.1 的部署方式,正常来说是没问题的。但是纳撸多还是踩了个坑,在手动复制 war 文件 webapps,并进行访问 http://localhost:8080/spring-framework-projects-1.0-SNAPSHOT/时出现 404 问题,经过多方排查,最终发现是自己安装的 Tomcat 版本 10 问题,不支持 Javax.servlet 4.0 接口规范,导致访问失败。

解决方案:重新安装 Tomcat 9 版本:apache-tomcat-9.0.4 ,重新进行部署即可。

servlet规范 JSP规范 tomcat版本 JDK版本
4 2.3 9.0.X JDK8
3.1 2.3 8.5.X JDK7
3.0 2.2 7.0.X JDK6
2.5 2.1 6.0.X JDK5
2.4 2.0 5.5.X JDK1.4
2.3 1.2 4.1.X JDK1.3
2.2 1.1 3.3.X JDK1.1

上图来源于:blog.csdn.net/qq_26264237…

  1. 小结

本文通过实现一个 Hello 级别的应用,让你快速了解一个 Java Web 应用的开发、构建、部署的最简单流程,助你敲开服务端开发的大门。

源码:github.com/naluduo233/…

参考资料

  • 廖雪峰的 Java 教程 强烈推荐。
  • Tomcat部署war包的n种方法
  • Springboot打war包部署,以及出现404错误的原因
  • Deploying a WAR file gives me a 404 Status Code on Tomcat?
  • 在Mac环境下配置tomcat

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MySQL权限管理实战

发表于 2021-04-19

前言:

不清楚各位同学对数据库用户权限管理是否了解,作为一名 DBA ,用户权限管理是绕不开的一项工作内容。特别是生产库,数据库用户权限更应该规范管理。本篇文章将会介绍下 MySQL 用户权限管理相关内容。

1.用户权限简介

当我们创建过数据库用户后,还不能执行任何操作,需要为该用户分配适当的访问权限。

关于 MySQL 用户权限简单的理解就是数据库只允许用户做你权利以内的事情,不可以越界。比如只允许你执行 select 操作,那么你就不能执行 update 操作。只允许你从某个 IP 上连接 MySQL ,那么你就不能从除那个 IP 以外的其他机器连接 MySQL 。

在 MySQL 中,用户权限也是分级别的,可以授予的权限有如下几组:

  • 列级别,和表中的一个具体列相关。例如,可以使用 UPDATE 语句更新表 students 中 student_name 列的值的权限。
  • 表级别,和一个具体表中的所有数据相关。例如,可以使用 SELECT 语句查询表 students 的所有数据的权限。
  • 数据库级别,和一个具体的数据库中的所有表相关。例如,可以在已有的数据库 mytest 中创建新表的权限。
  • 全局,和 MySQL 中所有的数据库相关。例如,可以删除已有的数据库或者创建一个新的数据库的权限。

权限信息存储在 mysql 系统库的 user、db、tables_priv、columns_priv、procs_priv 这几个系统表中。

  • user 表:存放用户账户信息以及全局级别(所有数据库)权限。
  • db 表:存放数据库级别的权限,决定了来自哪些主机的哪些用户可以访问此数据库。
  • tables_priv 表:存放表级别的权限,决定了来自哪些主机的哪些用户可以访问数据库的这个表。
  • columns_priv 表:存放列级别的权限,决定了来自哪些主机的哪些用户可以访问数据库表的这个字段。
  • procs_priv 表:存放存储过程和函数级别的权限。

参考官方文档,可授予的权限如下表所示:

看起来各种可授予的权限有很多,其实可以大致分为数据、结构、管理三类,大概可分类如下:

2.权限管理实战

我们一般用 grant 语句为数据库用户赋权,建议大家先用 create user 语句创建好用户之后再单独进行授权。下面通过示例来具体看下:

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
sql复制代码# 创建用户
create user 'test_user'@'%' identified by 'xxxxxxxx';

# 全局权限
GRANT super,select on *.* to 'test_user'@'%';

# 库权限
GRANT select,insert,update,delete,create,alter,execute on `testdb`.* to 'test_user'@'%';

# 表权限
GRANT select,insert on `testdb`.tb to 'test_user'@'%';

# 列权限
GRANT select (col1), insert (col1, col2) ON `testdb`.mytbl to 'test_user'@'%';

# GRANT命令说明:
super,select 表示具体要授予的权限。
ON 用来指定权限针对哪些库和表。
*.* 中前面的*号用来指定数据库名,后面的*号用来指定表名。
TO 表示将权限赋予某个用户。
'test_user'@'%' 表示test_user用户,@后面接限制的主机,可以是IP、IP段、域名以及%,%表示任何地方。

# 刷新权限
flush privileges;

# 查看某个用户的权限
show grants for 'test_user'@'%';

# 回收权限
revoke delete on `testdb`.* from 'test_user'@'%';

权限管理是一件不容忽视的事,我们不能为了方便而给数据库用户很大的权限。特别是对于生产库,更应该进行权限管控,建议程序用户只赋予增删改查等基础权限,个人用户只赋予查询权限。

出于安全考虑,建议遵循以下几个经验原则:

  • 只授予能满足需要的最小权限,防止用户干坏事。比如用户只是需要查询,那就只给 select 权限就可以了。
  • 创建用户的时候限制用户的登录主机,一般是限制成指定 IP 或者内网 IP 段。
  • 给各个服务单独创建数据库用户,单个用户最好只能操作单个库。
  • 及时记录各数据库用户权限等信息,以免忘记。
  • 若有外部系统调用,应配置只读用户,并且权限要精确到表或视图。
  • 定期清理不需要的用户,回收权限或者删除用户。

参考:

  • dev.mysql.com/doc/refman/…
  • www.cnblogs.com/richardzhu/…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

面试官:你知道Dubbo怎么做优雅上下线的吗?你:优雅上下线

发表于 2021-04-19

最近无论是校招还是社招,都进行的如火如荼,我也承担了很多的面试工作,在一次面试过程中,和候选人聊了一些关于Dubbo的知识。

Dubbo是一个比较著名的RPC框架,很多人对于他的一些网络通信、通信协议、动态代理等等都有一定的了解,这位候选人也一样。

但是,我接下来问了他一个问题:你们在使用Dubbo的时候,应用如果重启,怎么保证一个请求不会被中断处理的呢?

他没怎么说的上来,我以为他不理解我的问题,我接着问他:我就是想问下Dubbo是如何做优雅上下线的你知道吗?

接着他问我:优雅上下线是啥??

好吧。

这篇文章,我来介绍一下这个知识点吧。

优雅上下线

关于”优雅上下线”这个词,我没找到官方的解释,我尝试解释一下这是什么。

首先,上线、下线大家一定都很清楚,比如我们一次应用发布过程中,就需要先将应用服务停掉,然后再把服务启动起来。这个过成就包含了一次下线和一次上线。

那么,”优雅”怎么理解呢?

先说什么情况我们认为不优雅:

1、服务停止时,没有关闭对应的监控,导致应用停止后发生大量报警。

2、应用停止时,没有通知外部调用方,很多请求还会过来,导致很多调用失败。

3、应用停止时,有线程正在执行中,执行了一半,JVM进程就被干掉了。

4、应用启动时,服务还没准备好,就开始对外提供服务,导致很多失败调用。

5、应用启动时,没有检查应用的健康状态,就开始对外提供服务,导致很多失败调用。

以上,都是我们认为的不优雅的情况,那么,反过来,优雅上下线就是一种避免上述情况发生的手段。

一个应用的优雅上下线涉及到的内容其实有很多,从底层的操作系统、容器层面,到编程语言、框架层面,再到应用架构层面,涉及到的知识很广泛。

其实,优雅上下线中,最重要的还是优雅下线。因为如果下线过程不优雅的话,就会发生很多调用失败了、服务找不到等问题。所以很多时候,大家也会提优雅停机这样的概念。

本文后面介绍的优雅上下线也重点关注优雅停机的过程。

操作系统&容器的优雅上下线

关于操作系统,我之前有一篇文章专门介绍过这个话题,可能大家没有注意到,那时候介绍的主题是为什么不能在线上机器中随便执行kill -9。

其实,这背后的思考就是优雅上下线。

我们知道,kill -9之所以不建议使用,是因为kill -9特别强硬,系统会发出SIGKILL信号,他要求接收到该信号的程序应该立即结束运行,不能被阻塞或者忽略。

这个过程显然是不优雅的,因为应用立刻停止的话,就没办法做收尾动作。而更优雅的方式是kill -15。

当使用kill -15时,系统会发送一个SIGTERM的信号给对应的程序。当程序接收到该信号后,具体要如何处理是自己可以决定的。

kill -15会通知到应用程序,这就是操作系统对于优雅上下线的最基本的支持。

以前,在操作系统之上就是应用程序了,但是,自从容器化技术推出之后,在操作系统和应用程序之间,多了一个容器层,而Docker、k8s等容器其实也是支持优雅上下线的。

如Docker中同样提供了两个命令, docker stop 和 docker kill

docker stop就像kill -15一样,他会向容器内的进程发送SIGTERM信号,在10S之后(可通过参数指定)再发送SIGKILL信号。

而docker kill就像kill -9,直接发送SIGKILL信号。

JVM的优雅上下线

在操作系统、容器等对优雅上下线有了基本的支持之后,在接收到docker stop、kill -15等命令后,会通知应用进程进行进程关闭。

而Java应用在运行时就是一个独立运行的进程,这个进程是如何关闭的呢?

Java程序的终止运行是基于JVM的关闭实现的,JVM关闭方式分为正常关闭、强制关闭和异常关闭3种。

这其中,正常关闭就是支持优雅上下线的。正常关闭过程中,JVM可以做一些清理动作,比如删除临时文件。

当然,开发者也是可以自定义做一些额外的事情的,比如通知应用框架优雅上下线操作。

而这种机制是通过JDK中提供的shutdown hook实现的。JDK提供了Java.Runtime.addShutdownHook(Thread hook)方法,可以注册一个JVM关闭的钩子。

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typescript复制代码package com.hollis;

public class ShutdownHookTest {

public static void main(String[] args) {
boolean flag = true;
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("hook execute...");
}));

while (flag) {
// app is runing
}

System.out.println("main thread execute end...");
}
}

执行命令:

1
2
3
4
yaml复制代码➜ jps
6520 ShutdownHookTest
6521 Jps
➜ kill 6520

控制台输出内容:

1
2
vbnet复制代码hook execute...
Process finished with exit code 143 (interrupted by signal 15: SIGTERM)

可以看到,当我们使用kill(默认kill -15)关闭进程的时候,程序会先执行我注册的shutdownHook,然后再退出,并且会给出一个提示:interrupted by signal 15: SIGTERM

Spring的优雅上下线

有了JVM提供的shutdown hook之后,很多框架都可以通过这个机制来做优雅下线的支持。

比如Spring,他就会向JVM注册一个shutdown hook,在接收到关闭通知的时候,进行bean的销毁,容器的销毁处理等操作。

同时,作为一个成熟的框架,Spring也提供了事件机制,可以借助这个机制实现更多的优雅上下线功能。

ApplicationListener是Spring事件机制的一部分,与抽象类ApplicationEvent类配合来完成ApplicationContext的事件机制。

开发者可以实现ApplicationListener接口,监听到 Spring 容器的关闭事件(ContextClosedEvent),来做一些特殊的处理:

1
2
3
4
5
6
7
8
9
typescript复制代码@Component
public class MyListener implements ApplicationListener<ContextClosedEvent> {

@Override
public void onApplicationEvent(ContextClosedEvent event) {
// 做容器关闭之前的清理工作
}

}

Dubbo的优雅上下线

因为Spring中提供了ApplicationListener接口,帮助我们来监听容器关闭事件,那么,很多web容器、框架等就可以借助这个机制来做自己的优雅上下线操作。

如tomcat、dubbo等都是这么做的。

这里简答说一下Dubbo的,在Dubbo的官网中,有关于优雅停机的介绍:

-w971

应用在停机时,接收到关闭通知时,会先把自己标记为不接受(发起)新请求,然后再等待10s(默认是10秒)的时候,等执行中的线程执行完。

那么,之所以他能做这些事,是因为从操作系统、到JVM、到Spring等都对优雅停机做了很好的支持。

关于Dubbo各个版本中具体是如何借助JVM的shutdown hook机制、或者说Spring的事件机制昨的优雅停机,我的一位同事的一篇文章介绍的很清晰,大家可以看下:

www.cnkirito.moe/dubbo-grace…

在从Dubbo 2.5 到 Dubbo 2.7介绍了历史版本中,Dubbo为了解决优雅上下线问题所遇到的问题和方案。

目前,Dubbo中实现方式如下,同样是用到了Spring的事件机制:

1
2
3
4
5
6
7
8
9
10
scss复制代码public class SpringExtensionFactory implements ExtensionFactory {
public static void addApplicationContext(ApplicationContext context) {
CONTEXTS.add(context);
if (context instanceof ConfigurableApplicationContext) {
((ConfigurableApplicationContext) context).registerShutdownHook();
DubboShutdownHook.getDubboShutdownHook().unregister();
}
BeanFactoryUtils.addApplicationListener(context, SHUTDOWN_HOOK_LISTENER);
}
}

总结

本文从操作系统开始,分别介绍了Linux、Docker、JVM、Spring、Dubbo等对优雅停机的支持。

可以看到,一个简单的优雅停机功能,上下游需要这么多底层基础设施和上层应用的支持。

相信通过学习本文,你一定对优雅上下线有了更多的了解。

除此之外,我还希望你,通过本文以后,遇到一些实际问题的时候,可以想到文中提到的shutdown hook机制、Spring的event机制。很多时候,这些机制都能帮助我们解决很多问题。

我在工作中,就有很多次使用过这样的机制的实例,后面有机会给大家介绍几个实例。

参考 :

zhuanlan.zhihu.com/p/29093407

www.cnkirito.moe/dubbo-grace…

关于作者:Hollis,一个对Coding有着独特追求的人,阿里巴巴技术专家,《程序员的三门课》联合作者,《Java工程师成神之路》系列文章作者。

关注公众号【Hollis】,后台回复”成神导图”可以咯领取Java工程师进阶思维导图。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Golang 接口实现(十六)|Go主题月

发表于 2021-04-18

上一节我们讲了 Golang 接口概念 都是理论。正所谓光说不练假把式,归根结底要如何实现呢?我们接下来继续看:

接口语法格式

1
2
3
4
5
go复制代码type 接口名 interface{
方法名1 ( 参数列表1 ) 返回值 1
方法名2 ( 参数列表2 ) 返回值 2
…
}

需要留意的是:

接口名首字母、方法名首字母 都是大写的时候,该方法能够被包(package)以外的代码调用。

举个例子:

1
2
3
go复制代码type Filewriter interface{
Write([]byte) error
}

实现接口

Golang 语言要求只要 结构体 实现了接口中 全部方法,就算继承了该接口。

换而言之,接口就是提供了一个约束,约束继承的结构体需要实现的方法列表。

举个例子:
我们来定义一个接口 Speaker:

1
2
3
4
go复制代码// Speaker 接口
type Speaker interface {
speak()
}

定义两个继承 Speaker 的结构体 Teacher 和 Student:

1
2
3
go复制代码type Teacher struct {}

type Student struct {}

安照 Golang 要求,只需要实现了接口 Speaker 中 全部方法,就算继承了该接口。
因为接口 Speaker 中只有方法 speak() ,所以我们只需要给 Teacher、Student 分别实现方法 speak() 就可以 继承接口 Speaker

1
2
3
4
5
6
7
8
9
scss复制代码// Teacher 实现了接口 Speaker
func (teacher Teacher) say() {
fmt.Println("Teacher speaking")
}

// Student 实现了接口 Speaker
func (student Student) say() {
fmt.Println("Student speaking")
}

以上实现了接口中的所有方法,就实现了这个接口。

继承接口作用

到这里可能会有疑问,如果 接口只是用来 约束 继承的结构体 需要实现的方法列表。而且又是非强制(毕竟你不实现全部方法也并不会像 java 那样报错),岂不是 君子协议 毫无强制约束

这里我是这么理解的:
从实现角度:结构体实现接口所有方法,即为继承接口 ,看待 接口 对 同类结构体 的写法上约束确实是非强制的。

但是:(此处应有重点)
从使用角度:**利用接口实现面向对象概念中的多态,看待 接口 对 同类接口提 进行对外使用中可调用方法的约束 这里确实非常强制的。

举个例子:

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
go复制代码package main

import "fmt"

// Speaker 接口
type Speaker interface {
speak()
}

// Teacher 实现了接口 Speaker
type Teacher struct {}
func (teacher Teacher) speak() {
fmt.Println("Teacher speaking")
}

// Student 实现了接口 Speaker
type Student struct {}
func (student Student) speak() {
fmt.Println("Student speaking")
}

func (student Student) chat() {
fmt.Println("Student chatting")
}

func main() {

var teackerSpeaker Speaker // 声明一个 Speaker 类型的变量 teackerSpeaker
teackerSpeaker = Teacher{} // 实例化 Teacher 并赋值给 teackerSpeaker
teackerSpeaker.speak() // 成功调用方法 speak() 输出:Teacher speaking

var studentSpeaker Speaker // 声明一个 Speaker 类型的变量 studentSpeaker
studentSpeaker = Student{} // 实例化 Student 并赋值给 studentSpeaker
studentSpeaker.speak() // 成功调用方法 speak() 输出:Student speaking
// studentSpeaker.chat() // 无法调用方法 chat(),因为接口 Speaker 内并无方法 chat()
}

从上面代码我们可以看出,即使 结构体 Student 实现了方法 chat,但经过赋值给 类型 Speaker 变量后,也无法调用 方法 chat。

这就是我所说,使用角度,多态限制了 同类结构体 可调用方法的强制约束

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Boot 2 + Spring Securit

发表于 2021-04-17

准备

开始本教程的时候希望对下面知识点进行粗略的了解。

  • 知道 JWT 的基本概念
  • 了解过 Spring Security

本项目中 JWT 密钥是使用用户自己的登入密码,这样每一个 token 的密钥都不同,相对比较安全。

大体思路:

登入:

  1. POST 用户名密码到 \login
  2. 请求到达 JwtAuthenticationFilter 中的 attemptAuthentication() 方法,获取 request 中的 POST 参数,包装成一个 UsernamePasswordAuthenticationToken 交付给 AuthenticationManager 的 authenticate() 方法进行鉴权。
  3. AuthenticationManager 会从 CachingUserDetailsService 中查找用户信息,并且判断账号密码是否正确。
  4. 如果账号密码正确跳转到 JwtAuthenticationFilter 中的 successfulAuthentication() 方法,我们进行签名,生成 token 返回给用户。
  5. 账号密码错误则跳转到 JwtAuthenticationFilter 中的 unsuccessfulAuthentication() 方法,我们返回错误信息让用户重新登入。

请求鉴权:

请求鉴权的主要思路是我们会从请求中的 Authorization 字段拿取 token,如果不存在此字段的用户,Spring Security 会默认会用 AnonymousAuthenticationToken() 包装它,即代表匿名用户。

  1. 任意请求发起
  2. 到达 JwtAuthorizationFilter 中的 doFilterInternal() 方法,进行鉴权。
  3. 如果鉴权成功我们把生成的 Authentication 用 SecurityContextHolder.getContext().setAuthentication() 放入 Security,即代表鉴权完成。此处如何鉴权由我们自己代码编写,后序会详细说明。

准备 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
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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.inlighting</groupId>
<artifactId>spring-boot-security-jwt</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-security-jwt</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JWT 支持 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.2</version>
</dependency>

<!-- cache 支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<!-- cache 支持 -->
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>

<!-- cache 支持 -->
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>

<!-- ehcache 读取 xml 配置文件使用 -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>

<!-- ehcache 读取 xml 配置文件使用 -->
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.0</version>
</dependency>

<!-- ehcache 读取 xml 配置文件使用 -->
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0</version>
</dependency>

<!-- ehcache 读取 xml 配置文件使用 -->
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

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
xml复制代码<!-- ehcache 读取 xml 配置文件使用 -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>

<!-- ehcache 读取 xml 配置文件使用 -->
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.0</version>
</dependency>

<!-- ehcache 读取 xml 配置文件使用 -->
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0</version>
</dependency>

<!-- ehcache 读取 xml 配置文件使用 -->
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>

因为 ehcache 读取 xml 配置文件时使用了这几个依赖,而这几个依赖从 JDK 9 开始时是选配模块,所以高版本的用户需要添加这几个依赖才能正常使用。

基础工作准备

接下来准备下几个基础工作,就是新建个实体、模拟个数据库,写个 JWT 工具类这种基础操作。

UserEntity.java

关于 role 为什么使用 GrantedAuthority 说明下:其实是为了简化代码,直接用了 Security 现成的 role 类,实际项目中我们肯定要自己进行处理,将其转换为 Security 的 role 类。

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
typescript复制代码public class UserEntity {

public UserEntity(String username, String password, Collection<? extends GrantedAuthority> role) {
this.username = username;
this.password = password;
this.role = role;
}

private String username;

private String password;

private Collection<? extends GrantedAuthority> role;

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;
}

public Collection<? extends GrantedAuthority> getRole() {
return role;
}

public void setRole(Collection<? extends GrantedAuthority> role) {
this.role = role;
}
}

ResponseEntity.java

前后端分离为了方便前端我们要统一 json 的返回格式,所以自定义一个 ResponseEntity.java。

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
typescript复制代码public class ResponseEntity {

public ResponseEntity() {
}

public ResponseEntity(int status, String msg, Object data) {
this.status = status;
this.msg = msg;
this.data = data;
}

private int status;

private String msg;

private Object data;

public int getStatus() {
return status;
}

public void setStatus(int status) {
this.status = status;
}

public String getMsg() {
return msg;
}

public void setMsg(String msg) {
this.msg = msg;
}

public Object getData() {
return data;
}

public void setData(Object data) {
this.data = data;
}
}

Database.java

这里我们使用一个 HashMap 模拟了一个数据库,密码我已经预先用 Bcrypt 加密过了,这也是 Spring Security 官方推荐的加密算法(MD5 加密已经在 Spring Security 5 中被移除了,不安全)。

用户名 密码 权限
jack jack123 存 Bcrypt 加密后 ROLE_USER
danny danny123 存 Bcrypt 加密后 ROLE_EDITOR
smith smith123 存 Bcrypt 加密后 ROLE_ADMIN
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
dart复制代码@Component
public class Database {
private Map<String, UserEntity> data = null;

public Map<String, UserEntity> getDatabase() {
if (data == null) {
data = new HashMap<>();

UserEntity jack = new UserEntity(
"jack",
"$2a$10$AQol1A.LkxoJ5dEzS5o5E.QG9jD.hncoeCGdVaMQZaiYZ98V/JyRq",
getGrants("ROLE_USER"));
UserEntity danny = new UserEntity(
"danny",
"$2a$10$8nMJR6r7lvh9H2INtM2vtuA156dHTcQUyU.2Q2OK/7LwMd/I.HM12",
getGrants("ROLE_EDITOR"));
UserEntity smith = new UserEntity(
"smith",
"$2a$10$E86mKigOx1NeIr7D6CJM3OQnWdaPXOjWe4OoRqDqFgNgowvJW9nAi",
getGrants("ROLE_ADMIN"));
data.put("jack", jack);
data.put("danny", danny);
data.put("smith", smith);
}
return data;
}

private Collection<GrantedAuthority> getGrants(String role) {
return AuthorityUtils.commaSeparatedStringToAuthorityList(role);
}
}

UserService.java

这里再模拟一个 service,主要就是模仿数据库的操作。

1
2
3
4
5
6
7
8
9
10
kotlin复制代码@Service
public class UserService {

@Autowired
private Database database;

public UserEntity getUserByUsername(String username) {
return database.getDatabase().get(username);
}
}

JwtUtil.java

自己编写的一个工具类,主要负责 JWT 的签名和鉴权。

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
typescript复制代码public class JwtUtil {

// 过期时间5分钟
private final static long EXPIRE_TIME = 5 * 60 * 1000;

/**
* 生成签名,5min后过期
* @param username 用户名
* @param secret 用户的密码
* @return 加密的token
*/
public static String sign(String username, String secret) {
Date expireDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
try {
Algorithm algorithm = Algorithm.HMAC256(secret);
return JWT.create()
.withClaim("username", username)
.withExpiresAt(expireDate)
.sign(algorithm);
} catch (Exception e) {
return null;
}
}

/**
* 校验token是否正确
* @param token 密钥
* @param secret 用户的密码
* @return 是否正确
*/
public static boolean verify(String token, String username, String secret) {
try {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username", username)
.build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception e) {
return false;
}
}

/**
* 获得token中的信息无需secret解密也能获得
* @return token中包含的用户名
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
}

Spring Security 改造

登入这块,我们使用自定义的 JwtAuthenticationFilter 来进行登入。

请求鉴权,我们使用自定义的 JwtAuthorizationFilter 来处理。

也许大家觉得两个单词长的有点像,😜。

UserDetailsServiceImpl.java

我们首先实现官方的 UserDetailsService 接口,这里主要负责一个从数据库拿数据的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Service
public class UserDetailsServiceImpl implements UserDetailsService {

@Autowired
private UserService userService;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity userEntity = userService.getUserByUsername(username);
if (userEntity == null) {
throw new UsernameNotFoundException("This username didn't exist.");
}
return new User(userEntity.getUsername(), userEntity.getPassword(), userEntity.getRole());
}
}

后序我们还需要对其进行缓存改造,不然每次请求都要从数据库拿一次数据鉴权,对数据库压力太大了。

JwtAuthenticationFilter.java

这个过滤器主要处理登入操作,我们继承了 UsernamePasswordAuthenticationFilter,这样能大大简化我们的工作量。

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
scss复制代码public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

/*
过滤器一定要设置 AuthenticationManager,所以此处我们这么编写,这里的 AuthenticationManager
我会从 Security 配置的时候传入
*/
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
/*
运行父类 UsernamePasswordAuthenticationFilter 的构造方法,能够设置此滤器指定
方法为 POST [\login]
*/
super();
setAuthenticationManager(authenticationManager);
}

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// 从请求的 POST 中拿取 username 和 password 两个字段进行登入
String username = request.getParameter("username");
String password = request.getParameter("password");
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
// 设置一些客户 IP 啥信息,后面想用的话可以用,虽然没啥用
setDetails(request, token);
// 交给 AuthenticationManager 进行鉴权
return getAuthenticationManager().authenticate(token);
}

/*
鉴权成功进行的操作,我们这里设置返回加密后的 token
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
handleResponse(request, response, authResult, null);
}

/*
鉴权失败进行的操作,我们这里就返回 用户名或密码错误 的信息
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
handleResponse(request, response, null, failed);
}

private void handleResponse(HttpServletRequest request, HttpServletResponse response, Authentication authResult, AuthenticationException failed) throws IOException, ServletException {
ObjectMapper mapper = new ObjectMapper();
ResponseEntity responseEntity = new ResponseEntity();
response.setHeader("Content-Type", "application/json;charset=UTF-8");
if (authResult != null) {
// 处理登入成功请求
User user = (User) authResult.getPrincipal();
String token = JwtUtil.sign(user.getUsername(), user.getPassword());
responseEntity.setStatus(HttpStatus.OK.value());
responseEntity.setMsg("登入成功");
responseEntity.setData("Bearer " + token);
response.setStatus(HttpStatus.OK.value());
response.getWriter().write(mapper.writeValueAsString(responseEntity));
} else {
// 处理登入失败请求
responseEntity.setStatus(HttpStatus.BAD_REQUEST.value());
responseEntity.setMsg("用户名或密码错误");
responseEntity.setData(null);
response.setStatus(HttpStatus.BAD_REQUEST.value());
response.getWriter().write(mapper.writeValueAsString(responseEntity));
}
}
}

private void handleResponse() 此处处理的方法不是很好,我的想法是跳转到控制器中进行处理,但是这样鉴权成功的 token 带不过去,所以先这么写了,有点复杂。

JwtAuthorizationFilter.java

这个过滤器处理每个请求鉴权,我们选择继承 BasicAuthenticationFilter ,考虑到 Basic 认证和 JWT 比较像,就选择了它。

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
java复制代码public class JwtAuthorizationFilter extends BasicAuthenticationFilter {

private UserDetailsService userDetailsService;

// 会从 Spring Security 配置文件那里传过来
public JwtAuthorizationFilter(AuthenticationManager authenticationManager, UserDetailsService userDetailsService) {
super(authenticationManager);
this.userDetailsService = userDetailsService;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// 判断是否有 token,并且进行认证
Authentication token = getAuthentication(request);
if (token == null) {
chain.doFilter(request, response);
return;
}
// 认证成功
SecurityContextHolder.getContext().setAuthentication(token);
chain.doFilter(request, response);
}

private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String header = request.getHeader("Authorization");
if (header == null || ! header.startsWith("Bearer ")) {
return null;
}

String token = header.split(" ")[1];
String username = JwtUtil.getUsername(token);
UserDetails userDetails = null;
try {
userDetails = userDetailsService.loadUserByUsername(username);
} catch (UsernameNotFoundException e) {
return null;
}
if (! JwtUtil.verify(token, username, userDetails.getPassword())) {
return null;
}
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
}

SecurityConfiguration.java

此处我们进行 Security 的配置,并且实现缓存功能。缓存这块我们使用官方现成的 CachingUserDetailsService ,唯独的缺点就是它没有 public 方法,我们不能正常实例化,需要曲线救国,下面代码也有详细说明。

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
scss复制代码// 开启 Security
@EnableWebSecurity
// 开启注解配置支持
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

@Autowired
private UserDetailsServiceImpl userDetailsServiceImpl;

// Spring Boot 的 CacheManager,这里我们使用 JCache
@Autowired
private CacheManager cacheManager;

@Override
protected void configure(HttpSecurity http) throws Exception {
// 开启跨域
http.cors()
.and()
// security 默认 csrf 是开启的,我们使用了 token ,这个也没有什么必要了
.csrf().disable()
.authorizeRequests()
// 默认所有请求通过,但是我们要在需要权限的方法加上安全注解,这样比写死配置灵活很多
.anyRequest().permitAll()
.and()
// 添加自己编写的两个过滤器
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager(), cachingUserDetailsService(userDetailsServiceImpl)))
// 前后端分离是 STATELESS,故 session 使用该策略
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}

// 此处配置 AuthenticationManager,并且实现缓存
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 对自己编写的 UserDetailsServiceImpl 进一步包装,实现缓存
CachingUserDetailsService cachingUserDetailsService = cachingUserDetailsService(userDetailsServiceImpl);
// jwt-cache 我们在 ehcache.xml 配置文件中有声明
UserCache userCache = new SpringCacheBasedUserCache(cacheManager.getCache("jwt-cache"));
cachingUserDetailsService.setUserCache(userCache);
/*
security 默认鉴权完成后会把密码抹除,但是这里我们使用用户的密码来作为 JWT 的生成密钥,
如果被抹除了,在对 JWT 进行签名的时候就拿不到用户密码了,故此处关闭了自动抹除密码。
*/
auth.eraseCredentials(false);
auth.userDetailsService(cachingUserDetailsService);
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

/*
此处我们实现缓存的时候,我们使用了官方现成的 CachingUserDetailsService ,但是这个类的构造方法不是 public 的,
我们不能够正常实例化,所以在这里进行曲线救国。
*/
private CachingUserDetailsService cachingUserDetailsService(UserDetailsServiceImpl delegate) {

Constructor<CachingUserDetailsService> ctor = null;
try {
ctor = CachingUserDetailsService.class.getDeclaredConstructor(UserDetailsService.class);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
Assert.notNull(ctor, "CachingUserDetailsService constructor is null");
ctor.setAccessible(true);
return BeanUtils.instantiateClass(ctor, delegate);
}
}

Ehcache 配置

Ehcache 3 开始,统一使用了 JCache,就是 JSR107 标准,网上很多教程都是基于 Ehcache 2 的,所以大家可能在参照网上的教程会遇到很多坑。

JSR107:emm,其实 JSR107 是一种缓存标准,各个框架只要遵守这个标准,就是现实大一统。差不多就是我不需要更改系统代码,也能随意更换底层的缓存系统。

在 resources 目录下创建 ehcache.xml 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xml复制代码<ehcache:config
xmlns:ehcache="http://www.ehcache.org/v3"
xmlns:jcache="http://www.ehcache.org/v3/jsr107">

<ehcache:cache alias="jwt-cache">
<!-- 我们使用用户名作为缓存的 key,故使用 String -->
<ehcache:key-type>java.lang.String</ehcache:key-type>
<ehcache:value-type>org.springframework.security.core.userdetails.User</ehcache:value-type>
<ehcache:expiry>
<ehcache:ttl unit="days">1</ehcache:ttl>
</ehcache:expiry>
<!-- 缓存实体的数量 -->
<ehcache:heap unit="entries">2000</ehcache:heap>
</ehcache:cache>

</ehcache:config>

在 application.properties 中开启缓存支持:

1
2
ini复制代码spring.cache.type=jcache
spring.cache.jcache.config=classpath:ehcache.xml

统一全局异常

我们要把异常的返回形式也统一了,这样才能方便前端的调用。

我们平常会使用 @RestControllerAdvice 来统一异常,但是它只能管理 Controller 层面抛出的异常。Security 中抛出的异常不会抵达 Controller,无法被 @RestControllerAdvice 捕获,故我们还要改造 ErrorController 。

1
2
3
4
5
6
7
8
9
10
11
12
13
typescript复制代码@RestController
public class CustomErrorController implements ErrorController {

@Override
public String getErrorPath() {
return "/error";
}

@RequestMapping("/error")
public ResponseEntity handleError(HttpServletRequest request, HttpServletResponse response) {
return new ResponseEntity(response.getStatus(), (String) request.getAttribute("javax.servlet.error.message"), null);
}
}

测试

写个控制器试试,大家也可以参考我控制器里面获取用户信息的方式,推荐使用 @AuthenticationPrincipal 这个注解!!!

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
typescript复制代码@RestController
public class MainController {

// 任何人都可以访问,在方法中判断用户是否合法
@GetMapping("everyone")
public ResponseEntity everyone() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (! (authentication instanceof AnonymousAuthenticationToken)) {
// 登入用户
return new ResponseEntity(HttpStatus.OK.value(), "You are already login", authentication.getPrincipal());
} else {
return new ResponseEntity(HttpStatus.OK.value(), "You are anonymous", null);
}
}

@GetMapping("user")
@PreAuthorize("hasAuthority('ROLE_USER')")
public ResponseEntity user(@AuthenticationPrincipal UsernamePasswordAuthenticationToken token) {
return new ResponseEntity(HttpStatus.OK.value(), "You are user", token);
}

@GetMapping("admin")
@IsAdmin
public ResponseEntity admin(@AuthenticationPrincipal UsernamePasswordAuthenticationToken token) {
return new ResponseEntity(HttpStatus.OK.value(), "You are admin", token);
}
}

我这里还使用了 @IsAdmin 注解,@IsAdmin 注解如下:

1
2
3
4
5
less复制代码@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
public @interface IsAdmin {
}

这样能省去每次编写一长串的 @PreAuthorize() ,而且更加直观。

FAQ

如何解决JWT过期问题?

我们可以在 JwtAuthorizationFilter 中加点料,如果用户快过期了,返回个特别的状态码,前端收到此状态码去访问 GET /re_authentication 携带老的 token 重新拿一个新的 token 即可。

如何作废已颁发未过期的 token?

我个人的想法是把每次生成的 token 放入缓存中,每次请求都从缓存里拿,如果没有则代表此缓存报废。

项目地址:github.com/Smith-Cruis…

本文首发于公众号:Java版web项目,欢迎关注获取更多精彩内容

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…684685686…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%