目錄
SpringSecurity權限管理系統實戰—一、項目簡介和開發環境准備
SpringSecurity權限管理系統實戰—二、日志、接口文檔等實現
SpringSecurity權限管理系統實戰—三、主要頁面及接口實現
SpringSecurity權限管理系統實戰—四、整合SpringSecurity(上)
SpringSecurity權限管理系統實戰—五、整合SpringSecurity(下)
SpringSecurity權限管理系統實戰—六、SpringSecurity整合jwt
SpringSecurity權限管理系統實戰—七、處理一些問題
SpringSecurity權限管理系統實戰—八、AOP 記錄用戶日志、異常日志
SpringSecurity權限管理系統實戰—九、數據權限的配置
前言
本篇文章的內容有點雜,搞得我都不知道怎么取標題了。
上次我們已經搭建好了my-springsecurity-plus的基本環境,本次我們我們要實現功能有系統日志配置、配置swagger接口文檔、配置druid連接池等
一、Banner替換
可以有些第一次接觸到這個名詞的小伙伴不清楚banner是什么,其實就是在運行springboot項目時控制台打印出的圖案,就是下面這個東西。
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.3.1.RELEASE)
這下是不是就熟悉了,其實SpringBoot支持自定義banner圖案。只需要放在指定位置,SpringBoot會幫我們自動替換。Spring Boot 默認尋找 Banner 的順序是:
- 依次在 Classpath 下找 文件 banner.gif , banner.jpg , 和 banner.png , 先找到誰就用誰。
- 繼續 Classpath 下找 banner.txt
- 上面都沒有找到的話, 用默認的 SpringBootBanner
我們只需要在 src/main/resources
下新建一個 banner.txt
,然后找一個在線生成banner的網站,例如patorjk,然后將生成的文本復制到banner.txt文件中。啟動項目,查看控制台
是不是很炫酷,一個知名項目的banner是這樣的
////////////////////////////////////////////////////////////////////
// _ooOoo_ //
// o8888888o //
// 88" . "88 //
// (| ^_^ |) //
// O\ = /O //
// ____/`---'\____ //
// .' \\| |// `. //
// / \\||| : |||// \ //
// / _||||| -:- |||||- \ //
// | | \\\ - /// | | //
// | \_| ''\---/'' | | //
// \ .-\__ `-` ___/-. / //
// ___`. .' /--.--\ `. . ___ //
// ."" '< `.___\_<|>_/___.' >'"". //
// | | : `- \`.;`\ _ /`;.`/ - ` : | | //
// \ \ `-. \_ __\ /__ _/ .-` / / //
// ========`-.____`-.___\_____/___.-`____.-'======== //
// `=---=' //
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //
// 佛祖保佑 永不宕機 永無BUG //
////////////////////////////////////////////////////////////////////
二、日志
在項目的開發中,日志是必不可少的一個記錄事件的組件。應該很多不少剛入門的小伙伴對日志都是不怎么重視,對於我來說也是這樣,即使現在我對日志也不是很重視,也沒有養成記錄日志的習慣。但其實日志在一個系統中尤為的重要,可以幫助快速定位bug,來保證服務的高可用。
Spring Boot默認使用LogBack日志系統,如果不需要更改為其他日志系統如Log4j2等,則無需多余的配置,LogBack默認將日志打印到控制台上。
而Spring Boot項目一般都會引用spring-boot-starter
或者spring-boot-starter-web的依賴
,這兩個依賴中包含了spring-boot-starter-logging
的依賴,所以我們如果不使用別的日志框架,無需修改依賴。
如果我們要使用日志功能,只需要在相應類上加上@Slf4j(需要lambok插件)注解,在對應方法中log.indf(),log.error()等就可以輸出日志。我們把HelloController改造成如下這樣
@Controller
@Slf4j
public class HelloController {
@GetMapping(value = "/index")
public String index(){
log.info("測試");
log.error("測試");
return "index";
}
@GetMapping(value = "/login")
public String login(){
return "login";
}
@GetMapping(value = "/console/console1")
public String console1(){
return "console/console1";
}
}
重啟項目,訪問http://localhost:8080/index控制台會打印如下信息
那么如何把日志存貯到文件里呢?我們只要在application.yml中簡單定義一下
logging:
file:
path: src\main\resources\logger\ # logger文件夾需要提前生成
啟動項目,會在logger目錄下生成一個spring.log文件,內容和控制台輸出的一致。
日志的輸出格式支持自定義,但是自定義后在控制台輸出的內容就不是彩色的了,當然也能定義成彩色的,還有日志文件生成的大小(總不能一直存在一個文件里吧,那不就無限大了)和存儲時間等等,都可以自定義。我這里不詳細介紹了,有興趣的小伙伴可以自己了解。
三、Swagger接口文檔
Swagger 是一個規范和完整的框架,用於生成、描述、調用和可視化RESTful風格的 Web 服務。總體目標是使客戶端和文件系統作為服務器以同樣的速度來更新。文件的方法,參數和模型緊密集成到服務器端的代碼,允許API來始終保持同步。Swagger讓部署管理和使用功能強大的API變得非常簡單。官方網站:http://swagger.io/。
Swagger也可以用來測試接口(很多人會用postman,但是swagger可能用起來更簡單一點)
那么我們首先要在maven添加相關依賴
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!--swagger ui-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
這個我上一章給的依賴中有了,不要重復添加,這里只是為了說明。
在啟動類的那一層級中新建config包,在其中新建SwaggerConfig類
@Configuration//表明這是一個配置類
@EnableSwagger2//開啟Swagger
public class SwaggerConfig {
@Bean
public Docket webApiConfig(){
return new Docket(DocumentationType.SWAGGER_2)
.groupName("webApi")//組名稱
.apiInfo(webApiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.codermy.myspringsecurityplus.controller"))//掃描的包
.paths(PathSelectors.any())
.build();
}
/**
* 該套 API 說明,包含作者、簡介、版本、等信息
* @return
*/
private ApiInfo webApiInfo(){
return new ApiInfoBuilder()
.title("my-springsecurity-plus-API文檔")
.description("本文檔描述了my-springsecurity-plus接口定義")
.version("1.0")
.build();
}
}
然后我們訪問http://localhost:8080/swagger-ui.html
接口的名字也可以自定義,詳細見Swagger 常用注解使用詳解
我們再改造一下HelloController
@Controller
@Slf4j
@Api(tags = "前期測試后面會刪")
public class HelloController {
@GetMapping(value = "/index")
public String index(){
return "index";
}
@GetMapping(value = "/login")
public String login(){
return "login";
}
@GetMapping(value = "/console/console1")
@ApiOperation(value = "轉發console1請求")
public String console1(){
return "console/console1";
}
}
重啟訪問
四、主要界面接口
接下來我們把用戶管理,角色管理,和權限管理三個界面的的接口換成我們自己的。
首先我們新建一個類來統一返回數據格式,新建utiils包,在其中新建Result類
//統一返回結果的類
@Data
public class Result<T> implements Serializable {
@ApiModelProperty(value = "是否成功")
private Boolean success;
@ApiModelProperty(value = "返回碼")
private Integer code;
@ApiModelProperty(value = "返回消息")
private String msg;
@ApiModelProperty(value = "總數")
private Integer count;
@ApiModelProperty(value = "返回數據")
private List<T> data = new ArrayList<T>();
//把構造方法私有
private Result() {}
public static Result table_sucess() {
Result r = new Result();
r.setSuccess(true);
r.setCode(ResultCode.TABLE_SUCCESS);
r.setMsg("成功");
return r;
}
//成功靜態方法
public static Result ok() {
Result r = new Result();
r.setSuccess(true);
r.setCode(ResultCode.SUCCESS);
r.setMsg("成功");
return r;
}
//失敗靜態方法
public static Result error() {
Result r = new Result();
r.setSuccess(false);
r.setCode(ResultCode.ERROR);
r.setMsg("失敗");
return r;
}
public Result success(Boolean success){
this.setSuccess(success);
return this;
}
public Result message(String message){
this.setMsg(message);
return this;
}
public Result code(Integer code){
this.setCode(code);
return this;
}
public Result data(List<T> list){
this.data.addAll(list);
return this;
}
public Result count(Integer count){
this.count = count;
return this;
}
}
在新建一個ReslutCode接口來定義常用的狀態碼
public interface ResultCode {
/**
* 請求t成功
*/
public static Integer SUCCESS = 200;
/**
* 請求table成功
*/
public static Integer TABLE_SUCCESS = 0;
/**
* 請求失敗
*/
public static Integer ERROR = 201;
/**
* 請求已經被接受
*/
public static final Integer ACCEPTED = 202;
/**
* 操作已經執行成功,但是沒有返回數據
*/
public static final Integer NO_CONTENT = 204;
/**
* 資源已被移除
*/
public static final Integer MOVED_PERM = 301;
/**
* 重定向
*/
public static final Integer SEE_OTHER = 303;
/**
* 資源沒有被修改
*/
public static final Integer NOT_MODIFIED = 304;
/**
* 參數列表錯誤(缺少,格式不匹配)
*/
public static final Integer BAD_REQUEST = 400;
/**
* 未授權
*/
public static final Integer UNAUTHORIZED = 401;
/**
* 訪問受限,授權過期
*/
public static final Integer FORBIDDEN = 403;
/**
* 資源,服務未找到
*/
public static final Integer NOT_FOUND = 404;
/**
* 不允許的http方法
*/
public static final Integer BAD_METHOD = 405;
/**
* 資源沖突,或者資源被鎖
*/
public static final Integer CONFLICT = 409;
/**
* 不支持的數據,媒體類型
*/
public static final Integer UNSUPPORTED_TYPE = 415;
/**
* 接口未實現
*/
public static final Integer NOT_IMPLEMENTED = 501;
}
自定義異常處理(這里不過多解釋,只簡單實現直接貼代碼
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
//指定處理什么異常
@ExceptionHandler(Exception.class)
@ResponseBody
public Result error(Exception e){
e.printStackTrace();
return Result.error().message("執行了全局異常");
}
//自定義異常
@ExceptionHandler(MyException.class)
@ResponseBody
public Result error(MyException e){
log.error(e.getMessage());
e.printStackTrace();
return Result.error().code(e.getCode()).message(e.getMsg());
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MyException extends RuntimeException {
private Integer code;//狀態碼
private String msg;//異常信息
}
新建PageTableRequest 分頁工具類
@Data
public class PageTableRequest implements Serializable {
private Integer page;//初始頁
private Integer limit;//一頁幾條數據
private Integer offset;//頁碼
public void countOffset(){
if(null == this.page || null == this.limit){
this.offset = 0;
return;
}
this.offset = (this.page - 1) * limit;
}
}
下面進入正題
因為我這里用的是druid的連接池(之后介紹),我直接把application.yml貼出來
server:
port: 8080
spring:
profiles:
active: dev
application:
name: my-springsecurity-plus
datasource:
driver:
driver-class-name: com.mysql.cj.jdbc.Driver
# 后面時區不要忘了如果你是mysql8.0以上的版本
url: jdbc:mysql://localhost:3306/my-springsecurity-plus?serverTimezone=Asia/Shanghai
username: root
password: 180430121
type: com.alibaba.druid.pool.DruidDataSource #druid連接池之后會解釋這里先復制
druid:
# 初始化配置
initial-size: 3
# 最小連接數
min-idle: 3
# 最大連接數
max-active: 15
# 獲取連接超時時間
max-wait: 5000
# 連接有效性檢測時間
time-between-eviction-runs-millis: 90000
# 最大空閑時間
min-evictable-idle-time-millis: 1800000
test-while-idle: true
test-on-borrow: false
test-on-return: false
validation-query: select 1
# 配置監控統計攔截的filters
filters: stat
web-stat-filter:
url-pattern: /*
exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
#StatViewServlet配置,說明請參考Druid Wiki,配置_StatViewServlet配置
stat-view-servlet:
enabled: true #是否啟用StatViewServlet默認值true
url-pattern: /druid/*
reset-enable: true
login-username: admin
login-password: admin
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
# mybatis配置
mybatis:
type-aliases-package: com.codermy.myspringsecurityplus.entity
mapper-locations: classpath:/mybatis-mappers/*
configuration:
map-underscore-to-camel-case: true
logging:
file:
path: src\main\resources\logger\ # logger文件夾需要提前生成
用戶管理菜單接口,之前應該都創建好了相應的類,只拿這一個接口做例子,另外兩個都一樣
MyUser實體類
@Data
@EqualsAndHashCode(callSuper = true)
public class MyUser extends BaseEntity<Integer>{
private static final long serialVersionUID = -6525908145032868837L;
private String userName;
private String password;
private String nickName;
private String phone;
private String email;
private Integer status;
public interface Status {
int LOCKED = 0;
int VALID = 1;
}
}
UserDao中新建兩個方法,分頁會用到
@Mapper
public interface UserDao {
//分頁返回所有用戶
@Select("SELECT * FROM my_user t ORDER BY t.id LIMIT #{startPosition}, #{limit}")
List<MyUser> getAllUserByPage(@Param("startPosition")Integer startPosition,@Param("limit")Integer limit);
//計算所有用戶數量
@Select("select count(*) from My_user")
Long countAllUser();
}
UserService和UserServiceImlpl
public interface UserService {
Result<MyUser> getAllUsersByPage(Integer startPosition, Integer limit);
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public Result<MyUser> getAllUsersByPage(Integer startPosition, Integer limit) {
return Result.ok().count(userDao.countAllUser().intValue()).data(userDao.getAllUserByPage(startPosition,limit)).code(ResultCode.TABLE_SUCCESS);
}
}
UserController
@Controller
@RequestMapping("/api/user")
@Api(tags = "用戶相關接口")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
@ResponseBody
@ApiOperation(value = "用戶列表")
public Result<MyUser> index(PageTableRequest pageTableRequest){
pageTableRequest.countOffset();
return userService.getAllUsersByPage(pageTableRequest.getOffset(),pageTableRequest.getLimit());
}
}
我們可以比較一下他需要的json(user.json,在admin/data/user.json)和我們返回的json格式
他原先設置空值的可以不看,說明也用不着,然后在usr.html中把對應相同的數據,但是命名不一樣的地方修改一下即可。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title></title>
<link rel="stylesheet" th:href="@{/PearAdmin/component/layui/css/layui.css}" />
<link rel="stylesheet" th:href="@{/PearAdmin/admin/css/pearCommon.css}"/>
</head>
<body class="pear-container">
<div class="layui-card">
<div class="layui-card-body">
<form class="layui-form" action="">
<div class="layui-form-item">
<label class="layui-form-label">用戶名</label>
<div class="layui-input-inline">
<input type="text" name="nickName" placeholder="" class="layui-input">
</div>
<label class="layui-form-label">賬號</label>
<div class="layui-input-inline">
<input type="text" name="userName" placeholder="" class="layui-input">
</div>
<label class="layui-form-label">地點</label>
<div class="layui-input-inline">
<select name="city" lay-verify="required">
<option value=""></option>
<option value="0">北京</option>
<option value="1">上海</option>
<option value="2">廣州</option>
<option value="3">深圳</option>
<option value="4">杭州</option>
</select>
</div>
<button class="pear-btn pear-btn-md pear-btn-primary" lay-submit lay-filter="user-query">
<i class="layui-icon layui-icon-search"></i>
查詢
</button>
<button type="reset" class="pear-btn pear-btn-md">
<i class="layui-icon layui-icon-refresh"></i>
重置
</button>
</div>
</form>
</div>
</div>
<div class="layui-card">
<div class="layui-card-body">
<table id="user-table" lay-filter="user-table"></table>
</div>
</div>
<script type="text/html" id="user-toolbar">
<button class="pear-btn pear-btn-primary pear-btn-md" lay-event="add">
<i class="layui-icon layui-icon-add-1"></i>
新增
</button>
<button class="pear-btn pear-btn-danger pear-btn-md" lay-event="batchRemove">
<i class="layui-icon layui-icon-delete"></i>
刪除
</button>
</script>
<script type="text/html" id="user-bar">
<button class="pear-btn pear-btn-primary pear-btn-sm" lay-event="edit"><i class="layui-icon layui-icon-edit"></i></button>
<button class="pear-btn pear-btn-danger pear-btn-sm" lay-event="remove"><i class="layui-icon layui-icon-delete"></i></button>
</script>
<script type="text/html" id="user-status">
<input type="checkbox" name="status" value="{{d.id}}" lay-skin="switch" lay-text="啟用|禁用" lay-filter="user-status" checked = "{{ d.status == 0 ? 'true' : 'false' }}">
</script>
<script type="text/html" id="user-createTime">
{{layui.util.toDateString(d.createTime, 'yyyy-MM-dd HH:mm:ss')}}
</script>
<script th:src="@{/PearAdmin/component/layui/layui.js}" charset="utf-8"></script>
<script>
layui.use(['table','form','jquery'],function () {
let table = layui.table;
let form = layui.form;
let $ = layui.jquery;
let MODULE_PATH = "operate/";
//這里對應的field要和自己返回的json名稱一致
let cols = [
[
{type:'checkbox'},
{title: '賬號', field: 'userName', align:'center', width:100},
{title: '姓名', field: 'nickName', align:'center'},
{title: '電話', field: 'phone', align:'center'},
{title: '郵箱', field: 'email', align:'center'},
{title: '啟用', field: 'status', align:'center', templet:'#user-status'},
{title: '創建時間', field: 'createTime', align:'center',templet:'#user-createTime'},
{title: '操作', toolbar: '#user-bar', align:'center', width:130}
]
]
table.render({
elem: '#user-table',
url: '/api/user',//+++++++++++看這里 這里的url換成自己接口的url++++++++++++++
page: true ,
cols: cols ,
skin: 'line',
toolbar: '#user-toolbar',
defaultToolbar: [{
layEvent: 'refresh',
icon: 'layui-icon-refresh',
}, 'filter', 'print', 'exports']
});
table.on('tool(user-table)', function(obj){
if(obj.event === 'remove'){
window.remove(obj);
} else if(obj.event === 'edit'){
window .edit(obj);
}
});
table.on('toolbar(user-table)', function(obj){
if(obj.event === 'add'){
window.add();
} else if(obj.event === 'refresh'){
window.refresh();
} else if(obj.event === 'batchRemove'){
window.batchRemove(obj);
}
});
form.on('submit(user-query)', function(data){
table.reload('user-table',{where:data.field})
return false;
});
form.on('switch(user-status)', function(obj){
layer.tips(this.value + ' ' + this.name + ':'+ obj.elem.checked, obj.othis);
});
window.add = function(){
layer.open({
type: 2,
title: '新增',
shade: 0.1,
area: ['500px', '400px'],
content: MODULE_PATH + 'add.html'
});
}
window.edit = function(obj){
layer.open({
type: 2,
title: '修改',
shade: 0.1,
area: ['500px', '400px'],
content: MODULE_PATH + 'edit.html'
});
}
window.remove = function(obj){
layer.confirm('確定要刪除該用戶', {icon: 3, title:'提示'}, function(index){
layer.close(index);
let loading = layer.load();
$.ajax({
url: MODULE_PATH+"remove/"+obj.data['id'],
dataType:'json',
type:'delete',
success:function(result){
layer.close(loading);
if(result.success){
layer.msg(result.msg,{icon:1,time:1000},function(){
obj.del();
});
}else{
layer.msg(result.msg,{icon:2,time:1000});
}
}
})
});
}
window.batchRemove = function(obj){
let data = table.checkStatus(obj.config.id).data;
if(data.length === 0){
layer.msg("未選中數據",{icon:3,time:1000});
return false;
}
let ids = "";
for(let i = 0;i<data.length;i++){
ids += data[i].id+",";
}
ids = ids.substr(0,ids.length-1);
layer.confirm('確定要刪除這些用戶', {icon: 3, title:'提示'}, function(index){
layer.close(index);
let loading = layer.load();
$.ajax({
url: MODULE_PATH+"batchRemove/"+ids,
dataType:'json',
type:'delete',
success:function(result){
layer.close(loading);
if(result.success){
layer.msg(result.msg,{icon:1,time:1000},function(){
table.reload('user-table');
});
}else{
layer.msg(result.msg,{icon:2,time:1000});
}
}
})
});
}
window.refresh = function(param){
table.reload('user-table');
}
})
</script>
</body>
</html>
這樣當我們再次點擊用戶管理時,訪問的就是自己的接口了
原本自己看別人的教學博客時,是真的希望人家把所有的代碼一字不差的貼上來。等到自己寫的時候就覺得還是有道理的,代碼太占篇幅了,還影響博客的觀感。所以另外兩個界面我就補貼代碼了,大家仿照這個來就行。
放兩張圖片,讓大家看一下改完的效果。
五、Druid連接池
Druid是阿里開源的數據庫連接池,作為后起之秀,性能比dbcp、c3p0更高,使用也越來越廣泛。
當然Druid不僅僅是一個連接池,還有很多其他的功能。
druid的優點
- 高性能。性能比dbcp、c3p0高很多。
- 只要是jdbc支持的數據庫,druid都支持,對數據庫的支持性好。並且Druid針對oracle、mysql做了特別優化。
- 提供監控功能。可以監控sql語句的執行時間、ResultSet持有時間、返回行數、更新行數、錯誤次數、錯誤堆棧等信息,來了解連接池、sql語句的工作情況,方便統計、分析SQL的執行性能
如何使用??
導入依賴,之前給的依賴中就有不用重復導入
<!--druid連接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.21</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
application.yml中配置
spring:
profiles:
active: dev
application:
name: my-springsecurity-plus
datasource:
driver:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/my-springsecurity-plus?serverTimezone=Asia/Shanghai
username: root
password: 180430121
type: com.alibaba.druid.pool.DruidDataSource
druid:
# 初始化配置
initial-size: 3
# 最小連接數
min-idle: 3
# 最大連接數
max-active: 15
# 獲取連接超時時間
max-wait: 5000
# 連接有效性檢測時間
time-between-eviction-runs-millis: 90000
# 最大空閑時間
min-evictable-idle-time-millis: 1800000
test-while-idle: true
test-on-borrow: false
test-on-return: false
validation-query: select 1
# 配置監控統計攔截的filters
filters: stat
web-stat-filter:
url-pattern: /*
exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
#StatViewServlet配置,說明請參考Druid Wiki,配置_StatViewServlet配置
stat-view-servlet:
enabled: true #是否啟用StatViewServlet默認值true
url-pattern: /druid/*
reset-enable: true
login-username: admin #用戶名
login-password: admin #密碼
更詳細的配置這里就不介紹了。
然后重啟項目訪問http://localhost:8080/druid/login.html輸入用戶名密碼就可以看到界面了。
呼,終於又寫完一篇,寫代碼的時候真沒感覺這么累,像我這種文筆差的經常寫着寫着就把自己寫亂了。。。。。