目錄
SpringSecurity許可權管理系統實戰—一、專案簡介和開發環境準備
SpringSecurity許可權管理系統實戰—二、日誌、介面文件等實現
SpringSecurity許可權管理系統實戰—三、主要頁面及介面實現
SpringSecurity許可權管理系統實戰—四、整合SpringSecurity(上)
SpringSecurity許可權管理系統實戰—五、整合SpringSecurity(下)
SpringSecurity許可權管理系統實戰—六、SpringSecurity整合jwt
SpringSecurity許可權管理系統實戰—七、處理一些問題
SpringSecurity許可權管理系統實戰—八、AOP 記錄使用者日誌、異常日誌
前言
這幾天的時間去弄部落格了,這個專案就被擱在一邊了。
在之前我是用wordpress來搭的部落格,用的阿里雲的學生機,就卡的不行,體驗極差,也沒有釋出過多少內容。後來又想著自己寫一個部落格系統,後臺部分已經開發了大半,懶癌犯了,就一直擱置了(圖片上的所有能點選的介面都實現了)。現在回過去一看,介面十分混亂,冗餘。可能不會再用來作為自己的部落格了(隨便再寫寫,做個畢設專案吧)
然後又想著用靜態部落格,繞來繞去後,最終選用了vuepress來搭建靜態部落格,部署的時候又順帶著複習了下git的知識(平時idea外掛用的搞得我git命令都忘得差不多了)。現在的部落格是根據vuepress-theme-roco主題魔改的,給張照片感受下
已經部署到github pages。可以訪問www.codermy.cn檢視。 目前還沒有備案成功,尚未配置cdn,所以可能會載入有點慢。國內也可以訪問 witmy.gitee.io 檢視。
一、Spring Security 介紹
Spring Security 是Spring專案之中的一個安全模組,可以非常方便與spring專案整合。自從有了 Spring Boot 之後,Spring Boot 對於 Spring Security 提供了 自動化配置方案,可以零配置使用 Spring Security。
其實Spring Security 最早不叫 Spring Security ,叫 Acegi Security,後來才發展成為Spring的子專案。由於SpringBoot的大火,讓Spring系列的技術都得到了非常多的關注度,SpringSecurity同樣也沾了一把光。
一般來說,Web 應用的安全性包括兩部分:
- 使用者認證(Authentication)
- 使用者授權(Authorization)
簡單來說,認證就是登入,授權其實就是許可權的鑑別,看使用者是否具備相應請求的許可權。
二、整合SpringSecurity
在SpringBoot中想要使用SpringSecurity,只要新增SpringSecurity的依賴即可
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
這個依賴在最初給的pom中已經有了,不過給註釋了,取消掉就可以,其餘什麼都不用做,啟動專案。
啟動完成後,我們訪問http://localhost:8080或者其中的任何介面,都會重定向到登入頁面。
SpringSecurity預設的使用者名稱是user,密碼則在啟動專案時會列印在控制檯上。
Using generated security password: 21d26148-7f1e-403a-9041-1bc62a034871
21d26148-7f1e-403a-9041-1bc62a034871
就是密碼,每次啟動都會分配不一樣的密碼。SpringSecurity同樣支援自定義密碼,只要在application.yml中簡單配置一下即可
spring:
security:
user:
name: admin
password: 123456
輸入使用者名稱密碼,登入後就能訪問index頁面了
三、自定義登入頁
SpringSecurity預設的登入頁在SpringBoot2.0之後已經做過升級了,以前的更醜,就是一個沒有樣式的form表單。現在這個雖然好看了不少,但是感覺還是單調了些。
那麼我們需要新建一個SpringSecurityConfig類繼承WebSecurityConfigurerAdapter
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/PearAdmin/**");//放行靜態資源
}
/**
* anyRequest | 匹配所有請求路徑
* access | SpringEl表示式結果為true時可以訪問
* anonymous | 匿名可以訪問
* denyAll | 使用者不能訪問
* fullyAuthenticated | 使用者完全認證可以訪問(非remember-me下自動登入)
* hasAnyAuthority | 如果有引數,參數列示許可權,則其中任何一個許可權可以訪問
* hasAnyRole | 如果有引數,參數列示角色,則其中任何一個角色可以訪問
* hasAuthority | 如果有引數,參數列示許可權,則其許可權可以訪問
* hasIpAddress | 如果有引數,參數列示IP地址,如果使用者IP和引數匹配,則可以訪問
* hasRole | 如果有引數,參數列示角色,則其角色可以訪問
* permitAll | 使用者可以任意訪問
* rememberMe | 允許通過remember-me登入的使用者訪問
* authenticated | 使用者登入後可訪問
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")//登入頁面
.loginProcessingUrl("/login")//登入介面
.permitAll()
.and()
.csrf().disable();//關閉csrf
}
}
把login.html移動到static目錄下,不要忘記把form表單的action替換成/login
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title></title>
<link rel="stylesheet" href="/PearAdmin/admin/css/pearForm.css" />
<link rel="stylesheet" href="/PearAdmin/component/layui/css/layui.css" />
<link rel="stylesheet" href="/PearAdmin/admin/css/pearButton.css" />
<link rel="stylesheet" href="/PearAdmin/assets/login.css" />
</head>
<body background="PearAdmin/admin/images/background.svg" >
<form class="layui-form" action="/login" method="post">
<div class="layui-form-item">
<img class="logo" src="PearAdmin/admin/images/logo.png" />
<div class="title">M-S-P Admin</div>
<div class="desc">
Spring Security 權 限 管 理 系 統 實 戰
</div>
</div>
<div class="layui-form-item">
<input id="username" name="username" placeholder="使用者名稱 : " type="text" hover class="layui-input" />
</div>
<div class="layui-form-item">
<input d="password" name="password" placeholder="密 碼 : " type="password" hover class="layui-input" />
</div>
<div class="layui-form-item">
<input type="checkbox" name="" title="記住密碼" lay-skin="primary" checked>
</div>
<div class="layui-form-item">
<button style="background-color: #5FB878!important;" class="pear-btn pear-btn-primary login">
登 入
</button>
</div>
</form>
<script src="/PearAdmin/component/layui/layui.js" charset="utf-8"></script>
<script>
layui.use(['form', 'element','jquery'], function() {
var form = layui.form;
var element = layui.element;
var $ = layui.jquery;
$("body").on("click",".login",function(){
location.href="index"
})
})
</script>
</body>
</html>
重啟專案檢視
四、動態獲取選單
目前我們的專案還是根據PeaAdmin的menu.json來獲取的選單。這明顯不行,沒有許可權的使用者登入後點來點去,發現什麼都用不了,這對使用者體驗來說非常差。所有要根據使用者的id來動態的生成選單。
首先看一下menu.json的格式。
之後的返回的json格式也要像這樣才能被正確解析。
新建一個MenuIndexDto用於封裝資料
@Data
public class MenuIndexDto implements Serializable {
private Integer id;
private Integer parentId;
private String title;
private String icon;
private Integer type;
private String href;
private List<MenuIndexDto> children;
}
MenuDao中新增通過使用者id查詢選單的方法
@Select("SELECT DISTINCT sp.id,sp.parent_id,sp.name,sp.icon,sp.url,sp.type " +
"FROM my_role_user sru " +
"INNER JOIN my_role_menu srp ON srp.role_id = sru.role_id " +
"LEFT JOIN my_menu sp ON srp.menu_id = sp.id " +
"WHERE " +
"sru.user_id = #{userId}")
@Result(property = "title",column = "name")
@Result(property = "href",column = "url")
List<MenuIndexDto> listByUserId(@Param("userId")Integer userId);
MenuService
List<MenuIndexDto> getMenu(Integer userId);
MenuServiceImpl
@Override
public List<MenuIndexDto> getMenu(Integer userId) {
List<MenuIndexDto> list = menuDao.listByUserId(userId);
List<MenuIndexDto> result = TreeUtil.parseMenuTree(list);
return result;
}
這裡我寫了一個工具方法,用於轉換返回格式。TreeUtil新增如下方法
public static List<MenuIndexDto> parseMenuTree(List<MenuIndexDto> list){
List<MenuIndexDto> result = new ArrayList<MenuIndexDto>();
// 1、獲取第一級節點
for (MenuIndexDto menu : list) {
if(menu.getParentId() == 0) {
result.add(menu);
}
}
// 2、遞迴獲取子節點
for (MenuIndexDto parent : result) {
parent = recursiveTree(parent, list);
}
return result;
}
public static MenuIndexDto recursiveTree(MenuIndexDto parent, List<MenuIndexDto> list) {
List<MenuIndexDto>children = new ArrayList<>();
for (MenuIndexDto menu : list) {
if (Objects.equals(parent.getId(), menu.getParentId())) {
children.add(menu);
}
parent.setChildren(children);
}
return parent;
}
MenuController新增如下方法
@GetMapping(value = "/index")
@ResponseBody
@ApiOperation(value = "通過使用者id獲取選單")
public List<MenuIndexDto> getMenu(Integer userId) {
return menuService.getMenu(userId);
}
在index.html檔案中把選單資料載入地址 先換成/api/menu/index/?userId=1
(這裡先寫死,之後自定義SpringSecurity的userdetail時再改)
啟動專案,檢視效果
這裡顯示拒絕連結是因為SpringSecurity預設拒絕frame中訪問。這裡我們可以寫一個SuccessHandler設定Header,或者在SpringSecurityConfig重寫的configure方法中新增如下配置
http.headers().frameOptions().sameOrigin();
再重啟專案,就可以正常訪問了。
五、改寫選單路由
之前選單的路由我們是寫再HelloController中的,現在我們規定下格式。新建AdminController
@Controller
@RequestMapping("/api")
@Api(tags = "系統:選單路由")
public class AdminController {
@Autowired
private MenuService menuService;
@GetMapping(value = "/index")
@ResponseBody
@ApiOperation(value = "通過使用者id獲取選單")
public List<MenuIndexDto> getMenu(Integer userId) {
return menuService.getMenu(userId);
}
@GetMapping("/console")
public String console(){
return "console/console1";
}
@GetMapping("/403")
public String error403(){
return "error/403";
}
@GetMapping("/404")
public String error404(){
return "error/404";
}
@GetMapping("/500")
public String error500(){
return "error/500";
}
@GetMapping("/admin")
public String admin(){
return "index";
}
}
再去相應頁面改寫下路由就可以
六、圖形驗證碼
驗證碼主要是防止機器大規模註冊,機器暴力破解資料密碼等危害。
EasyCaptcha是一個Java圖形驗證碼生成工具,可生成的型別有如下幾種
首先引入maven
<dependencies>
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
<version>1.6.2</version>
</dependency>
</dependencies>
新建一個CaptchaController
@Controller
public class CaptchaController {
@RequestMapping("/captcha")
public void captcha(HttpServletRequest request, HttpServletResponse response) throws Exception {
CaptchaUtil.out(request, response);
}
}
再login.html 密碼所在的div後面新增如下程式碼(這裡我新增了一下css格式,具體不貼了,自己操作吧)
<div class="layui-form-item">
<input id="captcha" name="captcha" placeholder="驗 證 碼:" type="text" hover class="layui-verify" style="border: 1px solid #dcdfe6;">
<img src="/captcha" width="130px" height="44px" onclick="this.src=this.src+'?'+Math.random()" title="點選重新整理"/>
</div>
重啟專案來看一下
目前只是讓驗證碼在前端繪製了出來,我們如果想要使用,還需要自定義一個過濾器
新建VerifyCodeFilter繼承OncePerRequestFilter
@Component
public class VerifyCodeFilter extends OncePerRequestFilter {
private String defaultFilterProcessUrl = "/login";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
if ("POST".equalsIgnoreCase(request.getMethod()) && defaultFilterProcessUrl.equals(request.getServletPath())) {
// 登入請求校驗驗證碼,非登入請求不用校驗
HttpSession session = request.getSession();
String requestCaptcha = request.getParameter("captcha");
String genCaptcha = (String) request.getSession().getAttribute("captcha");//驗證碼的資訊存放在seesion種,具體看EasyCaptcha官方解釋
if (StringUtils.isEmpty(requestCaptcha)){
session.removeAttribute("captcha");//刪除快取裡的驗證碼資訊
throw new AuthenticationServiceException("驗證碼不能為空!");
}
if (!genCaptcha.toLowerCase().equals(requestCaptcha.toLowerCase())) {
session.removeAttribute("captcha");
throw new AuthenticationServiceException("驗證碼錯誤!");
}
}
chain.doFilter(request, response);
}
}
最後在SpringSecurity種配置該過濾器
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private VerifyCodeFilter verifyCodeFilter;
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/PearAdmin/**");//放行靜態資源
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().frameOptions().sameOrigin();
http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);
http.authorizeRequests()
.antMatchers("/captcha").permitAll()//任何人都能訪問這個請求
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")//登入頁面 不設限訪問
.loginProcessingUrl("/login")//攔截的請求
.successForwardUrl("/api/admin")
.permitAll()
.and()
.csrf().disable();//關閉csrf
}
}
即
http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);
重啟專案,這時需要我們輸入正確的驗證碼後才能進行登入
剩下的一些我們下一節再來完成
本系列gitee和github中同步更新