青云开发

学习、记录、总结、侃大山

概要

Spring Boot包含许多其他功能,可在您将应用程序投入生产时帮助您监视和管理应用程序。您可以选择使用HTTP端点或JMX管理和监视应用程序。审核,运行状况和指标收集也可以自动应用于您的应用程序

maven依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

查看监控

添加依赖后启动项目控制台看到如下信息

1
2020-04-25 13:35:12.836  INFO 748 --- [  restartedMain] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 2 endpoint(s) beneath base path '/actuator'

在《Spring Cloud 微服务 入门、实战、进阶》这本书里描述说控制台输出了一系列的监控 路径(url)。可能是不同版本差异造成的吧。


我们通过端口+/actuator 接口(端点)得到下面的响应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"_links": {
"self": {
"href": "http://localhost:8082/actuator",
"templated": false
},
"health-component": {
"href": "http://localhost:8082/actuator/health/{component}",
"templated": true
},
"health": {
"href": "http://localhost:8082/actuator/health",
"templated": false
},
"health-component-instance": {
"href": "http://localhost:8082/actuator/health/{component}/{instance}",
"templated": true
},
"info": {
"href": "http://localhost:8082/actuator/info",
"templated": false
}
}
}

我们再来访问下这个端点

1
http://localhost:8082/actuator/health

得到响应:

1
2
3
{
"status": "UP"
}

在actuator中,status状态 其中up表示健康,down表示不健康

可以通过如下配置展示更多信息:

1
2
#显示健康详情
management.endpoint.health.show-details=always

再次请求得到响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"status": "UP",
"details": {
"diskSpace": {
"status": "UP",
"details": {
"total": 127718649856,
"free": 43498004480,
"threshold": 10485760
}
}
}
}

actuator的默认配置有很多端点是不暴露的我们可通过如下配置来暴露指定端点和所有端点

1
2
3
4
#暴露 configprops,和beans  使用逗号分隔
management.endpoints.web.exposure.exclude=configprops,beans
#暴露所有端点
management.endpoints.web.exposure.include=*

在spring cloud ‘全家桶’里集成不同的模块会有不同的端点,具体得参考官方文档

springboot 2.2.6 RELEASE actuator 文档地址:

1
https://docs.spring.io/spring-boot/docs/2.2.6.RELEASE/actuator-api/html/#overview

自定义actuator端点

有时候这些默认的信息并不能满足我们的业务场景与需求,我们可通过自定义添加额外信息或自定义新的端点来进行扩展。

扩展健康端点添加额外信息

添加自定义类MyHealthIndicator继承自AbstractHealthIndicator

1
2
3
4
5
6
7
8
@Component
public class MyHealthIndicator extends AbstractHealthIndicator {
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
builder.up().withDetail("my_status","success");
// builder.down().withDetail("my_status","err");
}
}

再次请求健康端点得到响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"status": "UP",
"details": {
"my": { // 我们的类名MyHealthIndicator已my开头所以这里是my
"status": "UP",
"details": {
"my_status": "success"
}
},
"diskSpace": {
"status": "UP",
"details": {
"total": 127718649856,
"free": 43498000384,
"threshold": 10485760
}
}
}
}

自定义新的端点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
@Endpoint(id = "user")
public class UserEndpoint {

@ReadOperation
public List<Map<String,Object>> health(){
List<Map<String,Object>> list = new ArrayList<>();
Map m = new HashMap();
m.put("username","wuzhiyong");
m.put("pwd","123456");
m.put("age",88);
list.add(m);
return list;
}
}

访问:ip:+port+/actuator/user 响应:

1
2
3
4
5
6
7
[
{
"pwd": "123456",
"age": 88,
"username": "wuzhiyong"
}
]

profiles多环境配置

定义多个配置文件,每个文件对应一个环境,格式为 application-环境.properties

application.properties 通用配置,不区分环境
application-dev.properties 开发环境
application-test.properties 测试环境
application-prod.properties 生产环境

application.properties

1
spring.profiles.active=prod

application-dev.properties

1
2
server.port=8081
com.study.name=WuZhiYong7777-dev

application-test.properties

1
2
server.port=8083
com.study.name=WuZhiYong7777-test

application-prod.properties

1
2
server.port=8082
com.study.name=WuZhiYong7777-prod

在开发环境中,可以通过修改 application.properties 中的spring.profiles.active 的值来激活对应环境的配置,在部署的时候可以通过 java -jar xxx.jar –spring.profiles.active=dev 来指定使用对应的环境

热部署

开发时为了减少项目启动的时间经行调试,可配置为热部署

maven依赖:

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional> <!-- 这个需要为 true 热部署才有效 -->
</dependency>

IDEA中还需要开启自动编译功能

第一步:同时按下 shift+ctrl+alt+/ 选择 registry 然后勾选蓝色条目

image-20200424234617475.png

第二部

image-20200424235127499.png

第三步:重启IDEA

配置属性值

application.properties

1
2
3
server.port=8080

com.study.name=WuZhiYong

Environment对象读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class HelloController {

/**
* 注入 Environment 对象
*/
@Autowired
private Environment env;

@GetMapping("/port")
public String getPort(){
//读取并返回配置
return env.getProperty("server.port");
}

}

运行项目 访问 http://localhost:8080/port

返回8080

@value注解读取

1
2
3
4
5
6
7
@Value("${server.port}")
private String port;

@GetMapping("/getPortByValeAnnotation")
public String getPortByValeAnnotation(){
return port;
}

prefix前缀读取

新建配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
@ConfigurationProperties(prefix = "com.study")
@Component
public class MyConfig {
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

控制器获取

1
2
3
4
5
6
7
@Autowired
private MyConfig myConfig;

@GetMapping("getPropByPrefix")
public String getPropByPrefix(){
return myConfig.getName();
}

问题描述

用hexo+typora写博客时会面临一个图片显示的问题。

md文件中插入图片,md的语法为

1
![]()

其中【】里写图片文件的描述

()里填写图片的src (可以是网络地址,也可以是本地的绝对或相对地址)

初期的时候为了图片的统一管理,我们会在myblog->source 文件下建立个img文件夹,图片都放在这个文件夹里,在编写博客文章的时候通过相对引用 比如:../img/xxx.png 来插入这张图片。(在typora的设置中的也对图片的存放提供了很好的支持)

通过hexo 发布服务后我们能在文章中正确浏览到该图片

但是在博客的主页中 文章中如果有图片却看不到图片。

在hexo的官方文档中也有说明:

https://hexo.io/zh-cn/docs/asset-folders

image-20200424165223145.png

解决办法

网络上有很多方案比如安装 hexo-asset-image 第三方插件、还有编写hexo过滤器等等。

比如这篇网站就说写给拦截器(我看到这篇尝试修改源码但没成功)

https://www.cnblogs.com/guide2it/p/11111715.html

安装第三方插件的方式我也没有使用,因为有人说很久没维护了。我没安装是因为实在是不喜欢给电脑上安装太多东西。


官方提供了解决方案

https://hexo.io/zh-cn/docs/asset-folders

image-20200424170427331.png

说实话我没看懂:

我曾以为是在 头部的标签中加入这些代码

还以为: “不是 markdown” 是不是不能下载md文件里,我差点去源代码的文章模块js中 写了。

然后把那段代码贴到typora 中,typora没有实时显示图片然后我以为错了。

。。。真的找了很久找到这篇博客才搞清除。

https://www.jianshu.com/p/cf0628478a4e


首先得参照官方的解决方案 在hexo配置文件中 把post_asset_folder 属性设置为true。

改完后 通过 hexo new “xxx” 就会多生成一个与md文件同名的文件夹。

如果是之前md文件没有同名文件夹 。手动建立一个就好

然后得把图片放在那个同名的文件夹里。

贴上官方的那段代码

1
{% asset_img example.jpg This is an example image %}

然后把 example.jpg 替换掉自己的图片名字(后面的提示随便写)并保存

这个时候 typora 中是不展示图片的 记住 typora中是不展示图片的

得通过 hexo发布成服务后 通过网页才能展示。

ps:如果有同名的文件夹。typora 中通过如下设置插入图片的时候会自动将图片存入到同名的文件夹中

image-20200424172520555.png

如果是后来手动建立的同名文件夹想把之前md文件里的图片搬过去。typora中鼠标右击图片可选择存入同名文件夹。然后再用官方解决办法的标签语法再写一遍就好了。

解决办法二

前两天在微信公众号里看了一篇 picgo + typoa 的文章。后来好奇的百度上搜了搜关于picgo的文章。

picgo就是一图片上传和管理的工具。图片上传到第三方平台后有个外链。picgo本身不存贮图片。只是通过第三方授权(token),管理在第三方的图片的工具。

picgo gitHub地址:https://github.com/Molunerfinn/PicGo


解决办法:

思路:hexo 主页图片不显示的主要是typora里的相对路径与hexo里的目录的处理方式不一致。如果我们引用的是外链图片那么这种相对路径的问题就不存在了。不管是博客首页还是文章里,图片的路径都是外链也就是都能正常显示。

typora提供了对picgo的支持(现在去官网下的typora都会支持)。

typora设置picgo.

image-20200502181611617

我是自己下好picgo 并配置了gitee.io的仓库。然后在这里把picgo 的路径设置在这里。然后点击验证图片上传:

image-20200502183107903

验证成功后会上传俩个图片。(如果图片没删除掉再次点击验证会提示验证失败和 {“success”:false})

好了:

我们把图片黏贴进来后会存到同名的文件夹中,并且在typora能预览到图片。然后鼠标右击图片选择上传。上传成功后会自动将外链地址替换掉图片的引用地址。

image-20200502194913442


picgo 配置 gitee 可参考这篇博客

https://www.jianshu.com/p/b69950a49ae2

next主页

https://github.com/theme-next/hexo-theme-next

image-20200424100945802.png

下载安装

1
2
$ cd hexo/themes
$ git clone https://github.com/theme-next/hexo-theme-next next

修改hexo项目根目录下的_config.yml文件

1
2
# theme: landscape
theme: next

命令行执行

1
2
3
D:\myblog>hexo s
INFO Start processing
INFO Hexo is running at http://localhost:4000 . Press Ctrl+C to stop.

配置标签和分类

  1. 生成标签页

    阅读全文 »

简介

ArtiPub (Article Publisher 的简称,意为 “文章发布者”) 是一款开源的一文多发平台,可以帮助文章作者将编写好的文章自动发布到掘金、SegmentFault、CSDN、知乎、开源中国等技术媒体平台,传播优质知识,获取最大的曝光度。ArtiPub 安装简单,提供了多种安装方式,可以一键安装使用,安装一般只要 5 分钟。

ArtiPub 目前支持文章编辑、文章发布、数据统计的功能,后期我们会加入存量文章导入、数据分析的功能,让您更好的管理、优化您的技术文章。此外,我们还会接入更多媒体渠道,真正做到让文章随处可阅。

(gitHub原文)

地址:https://github.com/crawlab-team/artipub

下载安装

尝试过npm 在线安装发现num 地址下下来的不是最新版本。启动的时候有mongoDB 链接的错误。

后手动下载zip包到本地进行安装

image-20200422114015781.png

解压出 artipub-master

安装参考gitHub

1
2
3
cd artipub-master

npm install

启动

1
2
3
4
5
6
7
8
9
#启动前端
npm run start:frontend

#启动后端
npm run start:backend

#备注:
#1、这两个命令需在当前目录执行 即解压后的项目根目录
#2、命令执行成功后会hold命令行,所以两个命令需分别用两个命令行执行

image-20200422114747648.png

预览

访问 localhost:8000(命令行提示)

image-20200422115009963.png

安装说明操作即可

image-20200422114903962.png

注意项

  1. 编写文章时文章标题无法输入,且提示字数限制

    请检查后端是否正常启动

  2. 点击 平台管理菜单->更新Cookie状态 按钮没反应

    有可能是页面没有刷新 点击左上角的项目logo刷新

    有可能是后端卡住了 在后端命令行 执行 ctrl + c 结束进程3

  3. 没看到发布按钮

    在文章的条目上点击发布(云状图标)

    在弹出的列表页勾选平台后 滚动到最下边有个发布按钮

  4. 点击发布 与 前面的更新Cookie状态按钮是会弹出内置浏览器打开相应的平台

    不要担心 功能执行完毕后 会自动关掉的。

  5. 在启动前端后在命令行 看到了一个 3000 端口上的服务

    打开地址呈现如下。似乎是一个前端的服务框架(容器),不太懂。

    (不知道为什么代码里会嵌入这个,内容(依赖)似乎还挺大的,下了蛮久)

    image-20200422123253222.png

总结

由于项目正处在开发阶段,许多功能还不稳定。比如我尝试发布文章,却没有一个发布成功的(简书里只新建了个草稿文章且文章内容还没有)。看错误信息有提示 type 什么的。(应该是分类不好处理吧,毕竟每个平台的分类是不一样的)

相比较于OpenWrite,功能上都差不多,界面布局也很相近。只是技术上还不够成熟。

不过OpenWrite是正式运营中的项目了(开始赚钱)Cookie 什么的可能会有泄露的风险。且免费的功能限制比较多。费用的话不打折还是挺贵的。

artiPub 则是自己部署,数据都是保存在服务器或本地,理论上安全些。

启动服务

Yapi server

1
2
D:\Program Files\nodejs\node_modules\npm>yapi server
在浏览器打开 http://0.0.0.0:9090 访问。非本地服务器,请将 0.0.0.0 替换成指定的域名或ip

表示 yapi 初始化服务 访问 http://0.0.0.0:9090 后有一个表单填写完提交后会在指定的目录创建一个yapi 的配置文件 config.json 文件内容如下:存放在 D:\YApi

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"port": "3000",
"adminAccount": "admin@admin.com",
"db": {
"servername": "127.0.0.1",
"DATABASE": "yapi",
"port": 27017,
"user": "",
"pass": "",
},
"mail": {
"enable": true,
"host": "smtp.163.com",
"port": 465,
"from": "***@163.com",
"auth": {
"user": "***@163.com",
"pass": "*****"
}
}
}

然后控制台 CTRL + C

1
2
3
4
5
6
7
8
9
10
^C终止批处理操作吗(Y/N)? y

D:\Program Files\nodejs\node_modules\npm>cd \YApi

D:\YApi>node vendors\server\app.js
log: -------------------------------------swaggerSyncUtils constructor-----------------------------------------------
log: 服务已启动,请打开下面链接访问:
http://127.0.0.1:3000/
log: mongodb load success...

我的yapi安装在D:\YApi\YApi目录下

在当前配置文件目录执行命令 node vendors\server\app.js 发布yapi服务

访问http://127.0.0.1:3000/得到如下界面:

image-20200422013510685.png

点击登录

用户名:admin@admin.com(配置文件里的用户名)

密 码:ymfe.org (默认)

清除无效的导包

ctrl + alt + o

代码块上下移动

上移:ctrl + shift + ↑

下移:ctrl + shift + ↓

类中的方法间跳动

往上跳动:alt + ↑

往下跳动:alt + ↓

光标下另起一行

shift + Enter

选定代码转大写

ctrl + shift + U

撤销与反撤销

撤销:ctrl + z

反撤销: ctrl + shift + z

查看子类

ctrl + h

security作为一个流行的安全框架,很多公司都用其来做web的认证与授权。activiti7 工作流默认使用其的 用户-角色 功能。所以我们必须了解它。

建立web项目

首先建立一个web项目(springboot)同时我们写上一个接口用于测试。

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
@RestController
public class TestController {
private Logger logger = LoggerFactory.getLogger(TestController.class);
@GetMapping(value = "/admin/1")
public Object admin(){
Map map = new HashMap();
map.put("code",200);
map.put("msg","success");
map.put("data","admin");
return map;
}
@GetMapping("/user/1")
public Object user(){
Map map = new HashMap();
map.put("code",200);
map.put("msg","success");
map.put("data","user");
return map;
}
@GetMapping("/free/1")
public Object free(){
Map map = new HashMap();
map.put("code",200);
map.put("msg","success");
map.put("data","free");
return map;
}

@PostMapping("/logout/success")
public Object logoutSuccess(){
return "logout success POST";
}
@GetMapping("/logout/success")
public Object logoutSuccessGet(){
return "logout success Get";
}
@PostMapping("/login/error2")
public Object loginError(){
return "logoin error";
}
@GetMapping("/test")
public Object test(){
return "test";
}
}

启动项目访问我们的接口

(正常返回数据)

image-20200331163706253.png

添加security依赖

1
2
3
4
5
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

再次访问接口:

image-20200331164035818.png

接口提示401 msg显示没有认证 说明security已经其作用了

接着我们再用浏览器看看

http://localhost:8080/admin/1

image-20200331164253612.png

发现浏览器重定向到一个登录页面

从IDEA控制台我们发现密码巴拉巴拉什么的

image-20200331164557365.png

然后我们使用 用户名:user 还有控制台的密码登录及可返回我们想要的数据。

从spring security 的文档中也可以找到说明:

image-20200331165141402.png

当然这种用户名和密码我们也可以自定义:

1
2
spring.security.user.name=abc
spring.security.user.password=123457

简单配置与说明

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
package com.example.springbootactiviti.demo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
* @ClassName SpringSecurityCustomConfig
* @Author wuzhiyong
* @Date 2020/3/4 21:17
* @Version 1.0
**/
@EnableWebSecurity
public class SpringSecurityCustomConfig extends WebSecurityConfigurerAdapter {

@Autowired
private PasswordEncoder encoding;
// @Autowired
// private SpringDataUserDetailsService userDetailsService;

/**
* 指定加密方式(默认会自动配上BCrypt加密)
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
// return new Md4PasswordEncoder();
//配置不加密的时候 下方的用户的密码用明文即可
// return NoOpPasswordEncoder.getInstance(); //不加密
}

// @Bean
// public SpringDataUserDetailsService customUserDetailsService() {
// return new SpringDataUserDetailsService();
// }

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
// .userDetailsService(userDetailsService)//配置自定义的验证(从数据库查询)逻辑
.inMemoryAuthentication()
.withUser("user").password(encoding.encode("123")).roles("USER").and()
.withUser("admin").password(encoding.encode("1234")).roles("USER", "ADMIN")
.and().withUser("zhnagsan").password(encoding.encode("12345")).roles("LEADER")
;
}

@Override
public void configure(HttpSecurity http) throws Exception {
http
//关闭 CSRF 保护(如果不关闭 访问logoutUrl 必须为post方式 见源码注释)
.csrf().disable()
.authorizeRequests()
//允许匿名访问
.antMatchers("/free/**","/logout/success").permitAll()
//匹配/admin/** API需要ADMIN的角色
.antMatchers("/admin/**").hasRole("ADMIN")
//拥有其中任意权限
.antMatchers("/user/**").hasAnyAuthority("ROLE_USER","ROLE_ADMIN")
//拥有其中任意权限(与上面不同的是这里不用 ROLE_前缀)
.antMatchers("/test").hasAnyRole("USER","ADMIN")
//任何请求都需要认证
.anyRequest().authenticated()
.and()
//配置登录页面,登录错误后的路径(同样是登录的页面)
.formLogin().failureUrl( "/login/error2" )
//配置登出的 url 以及 登出成功后 重定向的url地址
//注意:logoutSuccessUrl url 必须是 能够匿名访问的 否则 重定向过去后 会被拦截 再跳转到登录页面
.and().logout().logoutUrl("/logout/out").logoutSuccessUrl("/logout/success")
;
}
}

在配置权限认证的时候,遵循自上而下的匹配规则。

上面我们的用户是在代码中写死的,而实际项目中我们的用户信息都是在数据库里接下来我们来实现从数据库里读取用户的逻辑。

新建一个类:

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
package com.example.springbootactiviti.demo.config;

import com.example.springbootactiviti.demo.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.ArrayList;
import java.util.List;
/**
* @ClassName SpringDataUserDetailsService
* @Author wuzhiyong
* @Date 2020/3/6 12:37
* @Version 1.0
**/
public class SpringDataUserDetailsService implements UserDetailsService {

@Autowired
JdbcTemplate jdbcTemplate;

/**
* 需新建配置类注册一个指定的加密方式Bean,或在下一步Security配置类中注册指定
*/
@Autowired
private PasswordEncoder passwordEncoder;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 通过用户名从数据库获取用户信息
// User userInfo = userInfoService.queryForObject("select * fron user", com.example.springbootactiviti.demo.model.User.class);
List<User> userList = jdbcTemplate.query("select * from user where username = ?",new Object[]{username}, new BeanPropertyRowMapper<>(User.class));

if (userList == null) {
throw new UsernameNotFoundException("用户不存在");
}
userList.forEach(i-> System.out.println(i.toString()));
// 得到用户角色
String role = userList.get(0).getRole();

// 角色集合
List<GrantedAuthority> authorities = new ArrayList<>();
// 角色必须以`ROLE_`开头,数据库中没有,则在这里加
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));

return new org.springframework.security.core.userdetails.User(
userList.get(0).getUsername(),
// 如果数据库是明文,这里需加密,除非指定配置了不加密(一般数据库里存的都是加密后的字符串)
userList.get(0).getPassword(),
authorities
);
}
}

实体类:

1
2
3
4
5
6
7
8
9
10
public class User {
private String id;

private String username;

private String password;

private String role;
}
//set get 方法就省略不贴了

准备数据:

其中user密码明文是123 ,admin 密码明文是1234.这里插入的都是BCrypt加密后的密文。

1
2
3
4
5
6
7
8
9
10
11
create table user
(
id varchar(20) not null,
username varchar(50) not null,
password varchar(100) not null,
role varchar(50) not null
)
collate = utf8_bin;

INSERT INTO spring_security.user (id, username, password, role) VALUES ('1', 'user', '$2a$10$elDLIbuSf9UZ9XpLr2FVPOgfAQARQURnbymSg7HyxCTW.copZR3Y6', 'USER');
INSERT INTO spring_security.user (id, username, password, role) VALUES ('2', 'admin', '$2a$10$P0mwGYvKDgK5KBr7ybQ7D.GJJ4Ban3wSB/1DvOo17qjktvgH5Pwh6', 'ADMIN');

修改配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Autowired
private SpringDataUserDetailsService userDetailsService;

@Bean
public SpringDataUserDetailsService customUserDetailsService() {
return new SpringDataUserDetailsService();
}

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)//配置自定义的验证(从数据库查询)逻辑
// .inMemoryAuthentication()
// .withUser("user").password(encoding.encode("123")).roles("USER").and()
// .withUser("admin").password(encoding.encode("1234")).roles("USER", "ADMIN")
// .and().withUser("zhnagsan").password(encoding.encode("12345")).roles("LEADER")
;
}

好了!如果是一般的单体架构web项目,了解这些基本满足使用了。

由于restful API 与分布式、微服务的流行,现在很多项目都已经实现了前后端的分离。服务端(后端)只提供调用方API,前后端采用token认证的方式进行数据交互。而在token认证方式中 jwt 是比较流行的一种。接下来开始spring security 与 jwt 的整合。

security+JWT

首先简单说下token认证授权的逻辑3步走

  1. 用户通过名称和密码访问登录接口 登录成功返回 token
  2. 用户带上token(一般存放在head中) 访问 资源地址 服务端拦截请求解析token 如果正确则返回资源
  3. 用户访问登出接口 服务端清除token

再简单认识下security中的常见的过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//可简单认为是  登录前置 的过滤器
UsernamePasswordAuthenticationFilter

//可简单认为是 认证与授权的 过滤器
BasicAuthenticationFilter

//对用户名密码的一层封装
UsernamePasswordAuthenticationToken

//认证对象的封装 也是UsernamePasswordAuthenticationToken的顶级父类
Authentication

//security 中对用户的抽象 通常业务中的User类继承自此类
UserDetails

//通过传入的名称 返回UserDetails 对象 相当于userDao,在这里可以自定从不同的数据源里获取所需的对象数据
UserDetailsService

//认证管理
authenticationManager

security 默认的 /login(可配置) 作为登录的url 。当访问该login时。会被

AbstractAuthenticationProcessingFilter拦截后调用UsernamePasswordAuthenticationFilter 的方法得到UsernamePasswordAuthenticationToken做认证。然后调用过滤器链继续过滤

其中UsernamePasswordAuthenticationFilter 里是将读取用户名密码等信息封装成UsernamePasswordAuthenticationToken

当访问需要某项权限的url时(/login 接口不需要权限)会经过BasicAuthenticationFilter过滤器时会进行认证授权。并把认证结果保存在上下文中。

在BasicAuthenticationFilter中这里会在request中解析(通过存在请求头中的内容)出UsernamePasswordAuthenticationToken委托给authenticationManager的authenticate方法进行认证。

在authenticate的具体实现中会取出前面解析出的UsernamePasswordAuthenticationToken 中的用户名

通过UserDetailsService获取到数据库的用户信息(密码)与UsernamePasswordAuthenticationToken 中的密码进行比对。如果发生异常则抛出。

抛出的异常会被BasicAuthenticationFilter捕获 并交给认证失败处理方法(可重写)进行后续处理。

如果没有异常就继续交给过滤器链过滤处理。

还记得上面的4步走么

其中第一步

我们可以手动写一个restful接口用于登录。接口内验证用户名密码无误后通过jwt工具生成一个token返回给客户端。也可以继承自UsernamePasswordAuthenticationFilter 类在请求中拿到用户名密码验证无误后返回token给客户端。(返回客户端前 可把token存入redis 等)

其中第二步

用户访问资源链接时带上token会被BasicAuthenticationFilter拦截。

我们自定义一个类继承与它。把请求头中的token用jwt工具类解析然后封装成UsernamePasswordAuthenticationToken 其它代码不变。(当然如果前面把相关信息存在了redis里这里可直接在redis里取)

其中第三步

我们可以写个restful接口,客户端访问后,我们通过token解析出用户后,清除token(如果是redis做token验证这里清空redis里的token即可,如果不是可通过jwt工具类设置token的时效性使其失效)

补充说明

由于前后端的交互统一的json格式。所以我们需要重写掉security验证失败的默认处理unsuccessfulAuthentication方法。

很多地方是通过AuthenticationFailureHandler类来做默认处理的这里我们继承重写掉这个类即可

补充

1
2
3
4
5
6
7
8
9
extends
UsernamePasswordAuthenticationFilter
BasicAuthenticationFilter
AbstractSecurityInterceptor
OncePerRequestFilter
AbstractAuthenticationProcessingFilter
implements
FilterInvocationSecurityMetadataSource
AccessDecisionManager

本实例 是在 springboot-base 的基础上进行的。

第一步:添加依赖

1
2
3
4
5
6
7
8
9
	<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 这里不需要版本号 -->

这里我们另外再添加两个依赖。等下写http接口的发布之后,可以帮我们生成一个可访问的接口文档。

1
2
3
4
5
6
7
8
9
10
11
12
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>

其中的版本是在上方定义的:

1
2
3
<properties>
<swagger.version>2.9.2</swagger.version>
</properties>

第二步:配置数据库连接信息

在 applcatiion.properties 文件里:

1
2
3
4
5
6
# 这里根据自己的数据库信息填写
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/web?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.name=test

第三步:在数据库建表:

这里可以通过idea 本身的database 模块连接数据库建表:

1569679502067.png

也可以用其它工具(例如navicat)连接数据库建表:

1569679591601.png

这边我们来建个学生表:

1569680104922.png

(建表时记得把备注(comment)填写全,后面生成代码时,可以帮助我们生成注释)

sql语句为:

1
2
3
4
5
6
7
8
9
create table student
(
id int auto_increment comment '数据id' primary key,
study_no varchar(20) not null comment '学号',
stu_name varchar(20) not null comment '学生姓名',
stu_age int not null comment '学生年龄',
class_name varchar(32) not null comment '班级名称'
)
comment '学生表';

我们再双击student 表添加两条数据:

1569680601726.png

控制台自动把插入数据的sql 打印了出来

1
2
3
4
[2019-09-28 22:20:43] 0 rows retrieved in 43 ms (execution: 8 ms, fetching: 35 ms)
sql> INSERT INTO `web`.`student` (`study_no`, `stu_name`, `stu_age`, `class_name`) VALUES ('00110', '张三', 12, '初一6班')
[2019-09-28 22:22:33] 1 row affected in 14 ms
sql> INSERT INTO `web`.`student` (`study_no`, `stu_name`, `stu_age`, `class_name`) VALUES ('00111', '李四', 12, '初一3班')

第四步:我们来自动生成个实体类(也可以自己手动写这个类)

我们在这里新建一个包来存放实体类。

1569680881837.png

然后我们再右击表名 选择 generate java class

1569680862129.png

没有这个选项的需要安装一个插件。(插件为收费版)

1569681007667.png

点击generate java class 后:

1569681201640.png

java class path 选择我们前面建的包名。

勾选 comment 能够帮我们生成备注。

勾选swagger 能够帮我们生成swagger 的一些注解注释。

点击ok 就会生成好一个实体类。

1569681488344.png

第五步:我们来写调用数据库的一些方法。

1569682912018.png

这里的代码先简单这样写了。实际开发中业务相关的代码一般不放在 controller.

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
package com.example.springbootbase.controller;

import com.example.springbootbase.model.Student;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;
/**
* @ClassName StudentController
* @Author wuzhiyong
* @Date 2019/9/28 22:41
* @Version 1.0
**/
@RestController
public class StudentController {
@Autowired
JdbcTemplate jdbcTemplate;

@ResponseBody
@GetMapping("/stu/{id}")
public Student getStuById(@PathVariable("id") int id){
return (Student) jdbcTemplate.queryForObject("select * from student where id = "+id,new BeanPropertyRowMapper(Student.class));
}
@ResponseBody
@DeleteMapping("/stu/{id}")
public Map delStuById(@PathVariable("id") int id){
jdbcTemplate.execute("delect from student when id = "+id);
Map result = new HashMap(2);
result.put("code",200);
result.put("msg","success");
return result;
}
}

我们今天测接口不用postman 也不用浏览器,我们来用一个叫 restfulToolKit 的插件

装好之后 侧边栏有个 restServices 的选项卡点开后 找到我们编写的接口。修改参数后点击send 即可发送请求测试

1569683461753.png

点击send 后 response 里可看到请求的结果。下面看到了数据表的数据表示我们成功的从数据库里拿到了数据

1569683819259.png

第六步:利用swagger框架生成接口文档同时测试接口。

最简单的使用我们只需要在启动类上加上@EnableSwagger2注解然后重启就可以了

1569683988703.png

然后我们就可以通过浏览器访问地址 http://localhost:8080/swagger-ui.html 看到我们的接口了

第七步:配置连接池

由于spring boot默认集成hikari 链接池

所以我们直接复制一段连接池的配置就可以了。不需要额外添加依赖。当然如果你不想用这个链接池那么得自行添加依赖了

1
2
3
4
5
spring.datasource.type=com.zaxxer.hikari.HikariDataSourcespring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.maximum-pool-size=15spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.idle-timeout=30000spring.datasource.hikari.pool-name=DatebookHikariCP
spring.datasource.hikari.max-lifetime=1800000spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.connection-test-query=SELECT 1
0%