SpringSecurity權限管理系統實戰—1、項目簡介和開發環境準備
SpringSecurity權限管理系統實戰—2、日誌、接口文檔等實現
SpringSecurity權限管理系統實戰—3、主要頁面及接口實現
SpringSecurity權限管理系統實戰—4、整合SpringSecurity(上)
SpringSecurity權限管理系統實戰—5、整合SpringSecurity(下)
SpringSecurity權限管理系統實戰—6、SpringSecurity整合jwt
SpringSecurity權限管理系統實戰—7、處理一些問題css
本篇文章的內容有點雜,搞得我都不知道怎麼取標題了。html
上次咱們已經搭建好了my-springsecurity-plus的基本環境,本次咱們咱們要實現功能有系統日誌配置、配置swagger接口文檔、配置druid鏈接池等java
能夠有些第一次接觸到這個名詞的小夥伴不清楚banner是什麼,其實就是在運行springboot項目時控制檯打印出的圖案,就是下面這個東西。mysql
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.3.1.RELEASE)
這下是否是就熟悉了,其實SpringBoot支持自定義banner圖案。只須要放在指定位置,SpringBoot會幫咱們自動替換。Spring Boot 默認尋找 Banner 的順序是:jquery
咱們只須要在 src/main/resources
下新建一個 banner.txt
,而後找一個在線生成banner的網站,例如patorjk,而後將生成的文本複製到banner.txt文件中。啓動項目,查看控制檯git
是否是很炫酷,一個知名項目的banner是這樣的github
//////////////////////////////////////////////////////////////////// // _ooOoo_ // // o8888888o // // 88" . "88 // // (| ^_^ |) // // O\ = /O // // ____/`---'\____ // // .' \\| |// `. // // / \\||| : |||// \ // // / _||||| -:- |||||- \ // // | | \\\ - /// | | // // | \_| ''\---/'' | | // // \ .-\__ `-` ___/-. / // // ___`. .' /--.--\ `. . ___ // // ."" '< `.___\_<|>_/___.' >'"". // // | | : `- \`.;`\ _ /`;.`/ - ` : | | // // \ \ `-. \_ __\ /__ _/ .-` / / // // ========`-.____`-.___\_____/___.-`____.-'======== // // `=---=' // // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // // 佛祖保佑 永不宕機 永無BUG // ////////////////////////////////////////////////////////////////////
在項目的開發中,日誌是必不可少的一個記錄事件的組件。應該不少很多剛入門的小夥伴對日誌都是不怎麼重視,對於我來講也是這樣,即便如今我對日誌也不是很重視,也沒有養成記錄日誌的習慣。但其實日誌在一個系統中尤其的重要,能夠幫助快速定位bug,來保證服務的高可用。web
Spring Boot默認使用LogBack日誌系統,若是不須要更改成其餘日誌系統如Log4j2等,則無需多餘的配置,LogBack默認將日誌打印到控制檯上。ajax
而Spring Boot項目通常都會引用spring-boot-starter
或者spring-boot-starter-web的依賴
,這兩個依賴中包含了spring-boot-starter-logging
的依賴,因此咱們若是不使用別的日誌框架,無需修改依賴。spring
若是咱們要使用日誌功能,只須要在相應類上加上@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 是一個規範和完整的框架,用於生成、描述、調用和可視化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是阿里開源的數據庫鏈接池,做爲後起之秀,性能比dbcp、c3p0更高,使用也愈來愈普遍。
固然Druid不只僅是一個鏈接池,還有不少其餘的功能。
如何使用??
導入依賴,以前給的依賴中就有不用重複導入
<!--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輸入用戶名密碼就能夠看到界面了。
呼,終於又寫完一篇,寫代碼的時候真沒感受這麼累,像我這種文筆差的常常寫着寫着就把本身寫亂了。。。。。