SpringSecurity入門使用教程
1. 第一部分 SpringSecurity入門
1.1 Spring Security簡介
? Spring Security是一個功能強大且高度可定制的身份驗證和訪問控制框架。它是用于保護基于Spring的應(yīng)用程序的實際標準。Spring Security是一個框架,致力于為Java應(yīng)用程序提供身份驗證和授權(quán)。與所有Spring項目一樣,Spring Security的真正強大之處在于可以輕松擴展以滿足自定義要求.
1.2 Spring Security框架功能簡介
- 認證: 用戶登錄, 解決的是"你是誰?"
- 授權(quán): 判斷用戶擁有什么權(quán)限,可以訪問什么資源. 解決的是"你能干什么?"
- 安全防護,防止跨站請求,session 攻擊等
1.3 SpringSecurity應(yīng)用場景
- 用戶登錄, 傳統(tǒng)基于web開發(fā)的項目的登錄功能.
- 用戶授權(quán), 在系統(tǒng)中用戶擁有哪些操作權(quán)限
- 單一登錄, 一個賬號只能在同一時間只能在一個地方進行登錄, 如果在其他地方進行第二次登錄,則剔除之前登錄操作
集成cas,做單點登錄,即多個系統(tǒng)只需登錄一次,無需重復(fù)登錄
集成oauth2 ,做登錄授權(quán), 可以用于app登錄和第三方登錄(QQ,微信等), 也可以實現(xiàn)cas的功能.
1.5 SpringSecurity入門案例
? 快速體驗SpringSecurity功能
1.創(chuàng)建Spring Boot 工程
使用Spring Initializr 快速過構(gòu)建Spring Boot工程
Spring Boot版本選擇2.3.5 , 并選中Spring Web 模塊
2.編寫Controller
package com.boxuegu.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * security入門案例 */ @RestController public class HelloSecurityController { @RequestMapping("/hello") public String hello() { return "hello security"; } }
3.訪問http://localhost:8080/hello
4.添加SpringSecurity依賴
<!--添加Spring Security 依賴 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
5.重啟Spring Boot啟動類,再次訪問http://localhost:8080/hello
我們來觀察下幫我們生成的表單頁面
咱們先看看這個頁面源代碼,這里有三點需要大家注意下:
- 表單的提交方式和路徑:
post
/login
- input輸入項的name值:
username
password
- 隱藏域input的name: 值為:
_csrf
value值為d4329889-796a-447a-9d08-69e56bc7c296
SpringBoot已經(jīng)為SpringSecurity提供了默認配置,默認所有資源都必須認證通過才能訪問。那么問題來了!此刻并沒有連接數(shù)據(jù)庫,也并未在內(nèi)存中指定認證用戶,如何認證呢?
其實SpringBoot已經(jīng)提供了默認用戶名user,密碼在項目啟動時隨機生成
認證通過后可以繼續(xù)訪問處理器資源:
2. 第二部分 SpringSecurity認證
2.1建表語句
/* Navicat Premium Data Transfer Source Server : localhost Source Server Type : MySQL Source Server Version : 50540 Source Host : localhost:3306 Source Schema : security_management Target Server Type : MySQL Target Server Version : 50540 File Encoding : 65001 Date: 31/10/2020 14:35:33 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for t_permission -- ---------------------------- DROP TABLE IF EXISTS `t_permission`; CREATE TABLE `t_permission` ( `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '編號', `permission_name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '權(quán)限名稱', `permission_tag` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '權(quán)限標簽', `permission_url` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '權(quán)限地址', PRIMARY KEY (`ID`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; -- ---------------------------- -- Records of t_permission -- ---------------------------- INSERT INTO `t_permission` VALUES (1, '查詢所有用戶', 'user:findAll', '/user/findAll'); INSERT INTO `t_permission` VALUES (2, '用戶添加或修改', 'user:saveOrUpdate', '/user/saveOrUpadate'); INSERT INTO `t_permission` VALUES (3, '用戶刪除', 'user:delete', '/delete/{id}'); INSERT INTO `t_permission` VALUES (4, '根據(jù)ID查詢用戶', 'user:getById', '/user/{id}'); INSERT INTO `t_permission` VALUES (5, '查詢所有商品', 'product:findAll', '/product/findAll'); INSERT INTO `t_permission` VALUES (6, '商品添加或修改', 'product:saveOrUpdate', '/product/saveOrUpadate'); INSERT INTO `t_permission` VALUES (7, '商品刪除', 'product:delete', '/product//delete/{id}'); INSERT INTO `t_permission` VALUES (8, '商品是否顯示', 'product:show', '/product/show/{id}/{isShow}'); -- ---------------------------- -- Table structure for t_role -- ---------------------------- DROP TABLE IF EXISTS `t_role`; CREATE TABLE `t_role` ( `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '編號', `ROLE_NAME` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色名稱', `ROLE_DESC` varchar(60) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色描述', PRIMARY KEY (`ID`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; -- ---------------------------- -- Records of t_role -- ---------------------------- INSERT INTO `t_role` VALUES (1, 'ADMIN', '超級管理員'); INSERT INTO `t_role` VALUES (2, 'USER', '用戶管理'); INSERT INTO `t_role` VALUES (3, 'PRODUCT', '商品管理員'); INSERT INTO `t_role` VALUES (4, 'PRODUCT_INPUT', '商品錄入員'); INSERT INTO `t_role` VALUES (5, 'PRODUCT_SHOW', '商品審核員'); -- ---------------------------- -- Table structure for t_role_permission -- ---------------------------- DROP TABLE IF EXISTS `t_role_permission`; CREATE TABLE `t_role_permission` ( `RID` int(11) NOT NULL COMMENT '角色編號', `PID` int(11) NOT NULL COMMENT '權(quán)限編號', PRIMARY KEY (`RID`, `PID`) USING BTREE, INDEX `FK_Reference_12`(`PID`) USING BTREE, CONSTRAINT `FK_Reference_11` FOREIGN KEY (`RID`) REFERENCES `t_role` (`ID`) ON DELETE RESTRICT ON UPDATE RESTRICT, CONSTRAINT `FK_Reference_12` FOREIGN KEY (`PID`) REFERENCES `t_permission` (`ID`) ON DELETE RESTRICT ON UPDATE RESTRICT ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; -- ---------------------------- -- Records of t_role_permission -- ---------------------------- INSERT INTO `t_role_permission` VALUES (1, 1); INSERT INTO `t_role_permission` VALUES (2, 1); INSERT INTO `t_role_permission` VALUES (1, 2); INSERT INTO `t_role_permission` VALUES (2, 2); INSERT INTO `t_role_permission` VALUES (1, 3); INSERT INTO `t_role_permission` VALUES (2, 3); INSERT INTO `t_role_permission` VALUES (1, 4); INSERT INTO `t_role_permission` VALUES (2, 4); INSERT INTO `t_role_permission` VALUES (1, 5); INSERT INTO `t_role_permission` VALUES (3, 5); INSERT INTO `t_role_permission` VALUES (4, 5); INSERT INTO `t_role_permission` VALUES (5, 5); INSERT INTO `t_role_permission` VALUES (1, 6); INSERT INTO `t_role_permission` VALUES (3, 6); INSERT INTO `t_role_permission` VALUES (4, 6); INSERT INTO `t_role_permission` VALUES (1, 7); INSERT INTO `t_role_permission` VALUES (3, 7); INSERT INTO `t_role_permission` VALUES (4, 7); INSERT INTO `t_role_permission` VALUES (1, 8); INSERT INTO `t_role_permission` VALUES (3, 8); INSERT INTO `t_role_permission` VALUES (5, 8); -- ---------------------------- -- Table structure for t_user -- ---------------------------- DROP TABLE IF EXISTS `t_user`; CREATE TABLE `t_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL, `password` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL, `status` int(1) NULL DEFAULT NULL COMMENT '用戶狀態(tài)1-啟用 0-關(guān)閉', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact; -- ---------------------------- -- Records of t_user -- ---------------------------- INSERT INTO `t_user` VALUES (1, 'admin', '$2a$10$m8WqgTzr0TO.XG.aR91.jegJJmDnGSvWs69aMWPR.WNvCzemHpLum', 1); INSERT INTO `t_user` VALUES (2, 'zhaoyang', '$2a$10$m8WqgTzr0TO.XG.aR91.jegJJmDnGSvWs69aMWPR.WNvCzemHpLum', 1); INSERT INTO `t_user` VALUES (3, 'user1', '$2a$10$m8WqgTzr0TO.XG.aR91.jegJJmDnGSvWs69aMWPR.WNvCzemHpLum', 1); INSERT INTO `t_user` VALUES (4, 'user2', '$2a$10$m8WqgTzr0TO.XG.aR91.jegJJmDnGSvWs69aMWPR.WNvCzemHpLum', 1); INSERT INTO `t_user` VALUES (5, 'user3', '$2a$10$Wk1jWJPoMQ5s7UIp0S/tu.WTcUZUspUUQH6K3BQpa8uHXWRUQc3/a', 1); -- ---------------------------- -- Table structure for t_user_role -- ---------------------------- DROP TABLE IF EXISTS `t_user_role`; CREATE TABLE `t_user_role` ( `UID` int(11) NOT NULL COMMENT '用戶編號', `RID` int(11) NOT NULL COMMENT '角色編號', PRIMARY KEY (`UID`, `RID`) USING BTREE, INDEX `FK_Reference_10`(`RID`) USING BTREE, CONSTRAINT `FK_Reference_10` FOREIGN KEY (`RID`) REFERENCES `t_role` (`ID`) ON DELETE RESTRICT ON UPDATE RESTRICT, CONSTRAINT `FK_Reference_9` FOREIGN KEY (`UID`) REFERENCES `t_user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; -- ---------------------------- -- Records of t_user_role -- ---------------------------- INSERT INTO `t_user_role` VALUES (1, 1); INSERT INTO `t_user_role` VALUES (2, 2); INSERT INTO `t_user_role` VALUES (3, 4); INSERT INTO `t_user_role` VALUES (4, 5); SET FOREIGN_KEY_CHECKS = 1;
2.2 SpringSecurity認證基本原理與認證2種方式
在工程中添加Spring Security的依賴
<!--添加Spring Security 依賴 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
? 在使用SpringSecurity框架,該框架會默認自動地替我們將系統(tǒng)中的資源進行保護,每次訪問資源的時候都必須經(jīng)過一層身份的校驗,如果通過了則重定向到我們輸入的url中,否則訪問是要被拒絕的。那么SpringSecurity框架是如何實現(xiàn)的呢? Spring Security功能的實現(xiàn)主要是由一系列過濾器相互配合完成。也稱之為過濾器鏈
2.2.1 過濾器鏈介紹
過濾器是一種典型的AOP思想,下面簡單了解下這些過濾器鏈,后續(xù)再源碼剖析中在涉及到過濾器鏈在仔細講解.
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
根據(jù)請求封裝獲取WebAsyncManager,從WebAsyncManager獲取/注冊的安全上下文可調(diào)用處理攔截器
org.springframework.security.web.context.SecurityContextPersistenceFilter
SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存或更新一個SecurityContext,并將SecurityContext給以后的過濾器使用,來為后續(xù)fifilter建立所需的上下文。SecurityContext中存儲了當前用戶的認證以及權(quán)限信息。
org.springframework.security.web.header.HeaderWriterFilter
向請求的Header中添加相應(yīng)的信息,可在http標簽內(nèi)部使用security:headers來控制
org.springframework.security.web.csrf.CsrfFilter
csrf又稱跨域請求偽造,SpringSecurity會對所有post請求驗證是否包含系統(tǒng)生成的csrf的token信息,如果不包含,則報錯。起到防止csrf攻擊的效果。
org.springframework.security.web.authentication.logout.LogoutFilter
匹配URL為/logout的請求,實現(xiàn)用戶退出,清除認證信息。
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
表單認證操作全靠這個過濾器,默認匹配URL為/login且必須為POST請求。
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
如果沒有在配置文件中指定認證頁面,則由該過濾器生成一個默認認證頁面。
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
由此過濾器可以生產(chǎn)一個默認的退出登錄頁面
org.springframework.security.web.authentication.www.BasicAuthenticationFilter
此過濾器會自動解析HTTP請求中頭部名字為Authentication,且以Basic開頭的頭信息。
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
通過HttpSessionRequestCache內(nèi)部維護了一個RequestCache,用于緩存HttpServletRequest
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
針對ServletRequest進行了一次包裝,使得request具有更加豐富的API
org.springframework.security.web.authentication.AnonymousAuthenticationFilter
當SecurityContextHolder中認證信息為空,則會創(chuàng)建一個匿名用戶存入到SecurityContextHolder中。spring security為了兼容未登錄的訪問,也走了一套認證流程,只不過是一個匿名的身份。
org.springframework.security.web.session.SessionManagementFilter
securityContextRepository限制同一用戶開啟多個會話的數(shù)量
org.springframework.security.web.access.ExceptionTranslationFilter
異常轉(zhuǎn)換過濾器位于整個springSecurityFilterChain的后方,用來轉(zhuǎn)換整個鏈路中出現(xiàn)的異常
org.springframework.security.web.access.intercept.FilterSecurityInterceptor
獲取所配置資源訪問的授權(quán)信息,根據(jù)SecurityContextHolder中存儲的用戶信息來決定其是否有權(quán)限。
Spring Security默認加載15個過濾器, 但是隨著配置可以增加或者刪除一些過濾器.
2.2.2 認證方式
1.HttpBasic認證
? HttpBasic登錄驗證模式是Spring Security實現(xiàn)登錄驗證最簡單的一種方式,也可以說是最簡陋的一種方式。它的目的并不是保障登錄驗證的絕對安全,而是提供一種“防君子不防小人”的登錄驗證。
? 在使用的Spring Boot早期版本為1.X版本,依賴的Security 4.X版本,那么就無需任何配置,啟動項目訪問則會彈出默認的httpbasic認證?,F(xiàn)在使用的是spring boot2.0以上版本(依賴Security 5.X版本),HttpBasic不再是默認的驗證模式,在spring security 5.x默認的驗證模式已經(jīng)是表單模式。
? HttpBasic模式要求傳輸?shù)挠脩裘艽a使用Base64模式進行加密。如果用戶名是 “admin” ,密碼是“ admin”,則將字符串"admin:admin" 使用Base64編碼算法加密。加密結(jié)果可能是:YWtaW46YWRtaW4=。HttpBasic模式真的是非常簡單又簡陋的驗證模式,Base64的加密算法是可逆的,想要破解并不難.
2.formLogin登錄認證模式
? Spring Security的HttpBasic模式,該模式比較簡單,只是進行了通過攜帶Http的Header進行簡單的登錄驗證,而且沒有定制的登錄頁面,所以使用場景比較窄。對于一個完整的應(yīng)用系統(tǒng),與登錄驗證相關(guān)的頁面都是高度定制化的,非常美觀而且提供多種登錄方式。這就需要Spring Security支持我們自己定制登錄頁面, spring boot2.0以上版本(依賴Security 5.X版本)默認會生成一個登錄頁面.
2.3 表單認證
2.3.1 自定義表單登錄頁面
在config包下編寫SecurityConfiguration配置類
package com.boxuegu.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; /** * Security配置類 */ @Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { /** * http請求處理方法 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { /*http.httpBasic()//開啟httpbasic認證 .and().authorizeRequests(). anyRequest().authenticated();//所有請求都需要登錄認證才能訪問*/ http.formLogin()//開啟表單認證 .and().authorizeRequests(). .anyRequest().authenticated();//所有請求都需要登錄認證才能訪問; } }
問題一: localhost將您重定向次數(shù)過多
因為設(shè)置登錄頁面為login.html 后面配置的是所有請求都登錄認證,陷入了死循環(huán). 所以需要將login.html放行不需要登錄認證
http.formLogin().loginPage("/login.html")//開啟表單認證 .and().authorizeRequests(). antMatchers("/login.html").permitAll()//放行登錄頁面 .anyRequest().authenticated();//所有請求都需要登錄認證才能訪問;
問題二: 訪問login.html 報404錯誤
spring boot整合thymeleaf 之后 所有的靜態(tài)頁面以放在resources/templates下面,所以得通過請求訪問到模板頁面, 將/login.html修改為/toLoginPage
http.formLogin().loginPage("/toLoginPage")//開啟表單認證 .and().authorizeRequests(). antMatchers("/toLoginPage").permitAll()//放行登錄頁面 .anyRequest().authenticated();//所有請求都需要登錄認證才能訪問;
問題三: 訪問login.html 后發(fā)現(xiàn)頁面沒有相關(guān)樣式
因為訪問login.html需要一些js , css , image等靜態(tài)資源信息, 所以需要將靜態(tài)資源放行, 不需要認證
/** * WebSecurity * * @param web * @throws Exception */ @Override public void configure(WebSecurity web) throws Exception { //解決靜態(tài)資源被攔截的問題 web.ignoring().antMatchers("/css/**", "/images/**", "/js/**", "/favicon.ico"); }
Spring Security
中,安全構(gòu)建器HttpSecurity
和WebSecurity
的區(qū)別是 :
WebSecurity
不僅通過HttpSecurity
定義某些請求的安全控制,也通過其他方式定義其他某些請求可以忽略安全控制;HttpSecurity
僅用于定義需要安全控制的請求(當然HttpSecurity
也可以指定某些請求不需要安全控制);- 可以認為
HttpSecurity
是WebSecurity
的一部分,WebSecurity
是包含HttpSecurity
的更大的一個概念; - 構(gòu)建目標不同
- WebSecurity
構(gòu)建目標是整個
Spring Security安全過濾器
FilterChainProxy`, HttpSecurity
的構(gòu)建目標僅僅是FilterChainProxy
中的一個SecurityFilterChain
。
- WebSecurity
2.3.2 表單登錄
? 通過講解過濾器鏈中我們知道有個過濾器UsernamePasswordAuthenticationFilter是處理表單登錄的. 那么下面我們來通過源碼觀察下這個過濾器.
? 在源碼中可以觀察到, 表單中的input的name值是username和password, 并且表單提交的路徑為/login
, 表單提交方式method為post
, 這些可以修改為自定義的值.
代碼如下:
/** * http請求處理方法 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { /*http.httpBasic()//開啟httpbasic認證 .and().authorizeRequests(). anyRequest().authenticated();//所有請求都需要登錄認證才能訪問*/ http.formLogin()//開啟表單認證 .loginPage("/toLoginPage")//自定義登錄頁面 .loginProcessingUrl("/login")// 登錄處理Url .usernameParameter("username").passwordParameter("password"). //修改自定義表單name值. .successForwardUrl("/")// 登錄成功后跳轉(zhuǎn)路徑 .and().authorizeRequests(). antMatchers("/toLoginPage").permitAll()//放行登錄頁面 .anyRequest().authenticated();//所有請求都需要登錄認證才能訪問; // 關(guān)閉csrf防護 http..csrf().disable(); }
頁面代碼:
代碼修改后重啟完成登錄:
這個時候又出現(xiàn)新的問題了. 這個是什么原因呢? 我們來看出現(xiàn)問題的具體是哪里?
發(fā)現(xiàn)行內(nèi)框架iframe這里出現(xiàn)問題了. Spring Security下,X-Frame-Options默認為DENY,非Spring Security環(huán)境下,X-Frame-Options的默認大多也是DENY,這種情況下,瀏覽器拒絕當前頁面加載任何Frame頁面,設(shè)置含義如下:
- DENY:瀏覽器拒絕當前頁面加載任何Frame頁面 此選擇是默認的.
- SAMEORIGIN:frame頁面的地址只能為同源域名下的頁面
允許iframe加載
http.formLogin()//開啟表單認證 .loginPage("/toLoginPage")//自定義登錄頁面 .loginProcessingUrl("/login")// 登錄處理Url .usernameParameter("username").passwordParameter("password"). //修改自定義表單name值. .successForwardUrl("/")// 登錄成功后跳轉(zhuǎn)路徑 .and().authorizeRequests(). antMatchers("/toLoginPage").permitAll()//放行登錄頁面與靜態(tài)資源 .anyRequest().authenticated();//所有請求都需要登錄認證才能訪問; // 關(guān)閉csrf防護 http.csrf().disable(); // 允許iframe加載頁面 http.headers().frameOptions().sameOrigin();
2.3.3 基于數(shù)據(jù)庫實現(xiàn)認證功能
之前我們所使用的用戶名和密碼是來源于框架自動生成的, 那么我們?nèi)绾螌崿F(xiàn)基于數(shù)據(jù)庫中的用戶名和密碼功能呢? 要實現(xiàn)這個得需要實現(xiàn)security的一個UserDetailsService接口, 重寫這個接口里面loadUserByUsername即可
編寫MyUserDetailsService并實現(xiàn)UserDetailsService接口,重寫loadUserByUsername方法
package com.boxuegu.service.impl; import com.boxuegu.domain.User; import com.boxuegu.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.Collection; /** * 基于數(shù)據(jù)庫中完成認證 */ @Service public class MyUserDetailsService implements UserDetailsService { @Autowired UserService userService; /** * 根據(jù)username查詢用戶實體 * * @param username * @return * @throws UsernameNotFoundException */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userService.findByUsername(username); if (user == null) { throw new UsernameNotFoundException(username);// 用戶名沒有找到 } // 先聲明一個權(quán)限集合, 因為構(gòu)造方法里面不能傳入null Collection<? extends GrantedAuthority> authorities = new ArrayList<>(); // 需要返回一個SpringSecurity的UserDetails對象 UserDetails userDetails = new org.springframework.security.core.userdetails.User(user.getUsername(), "{noop}" + user.getPassword(),// {noop}表示不加密認證。 true, // 用戶是否啟用 true 代表啟用 true,// 用戶是否過期 true 代表未過期 true,// 用戶憑據(jù)是否過期 true 代表未過期 true,// 用戶是否鎖定 true 代表未鎖定 authorities); return userDetails; } }
在SecurityConfiguration配置類中指定自定義用戶認證
/** * 身份驗證管理器 * * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(myUserDetailsService);// 使用自定義用戶認證 }
2.3.4 密碼加密認證
? 在基于數(shù)據(jù)庫完成用戶登錄的過程中,我們所是使用的密碼是明文的,規(guī)則是通過對密碼明文添加{noop}
前綴。那么下面 Spring Security 中的密碼編碼進行一些探討。
? Spring Security 中PasswordEncoder
就是我們對密碼進行編碼的工具接口。該接口只有兩個功能: 一個是匹配驗證。另一個是密碼編碼。
BCrypt算法介紹
? 任何應(yīng)用考慮到安全,絕不能明文的方式保存密碼。密碼應(yīng)該通過哈希算法進行加密。 有很多標準的算法比如SHA或者MD5,結(jié)合salt(鹽)是一個不錯的選擇。 Spring Security 提供了BCryptPasswordEncoder類,實現(xiàn)Spring的PasswordEncoder接口使用BCrypt強哈希方法來加密密碼。BCrypt強哈希方法 每次加密的結(jié)果都不一樣,所以更加的安全。
? bcrypt算法相對來說是運算比較慢的算法,在密碼學界有句常話:越慢的算法越安全。黑客破解成本越高.通過salt和const這兩個值來減緩加密過程,它的加密時間(百ms級)遠遠超過md5(大概1ms左右)。對于計算機來說,Bcrypt 的計算速度很慢,但是對于用戶來說,這個過程不算慢。bcrypt是單向的,而且經(jīng)過salt和cost的處理,使其受攻擊破解的概率大大降低,同時破解的難度也提升不少,相對于MD5等加密方式更加安全,而且使用也比較簡單
bcrypt加密后的字符串形如:$2a$10$wouq9P/HNgvYj2jKtUN8rOJJNRVCWvn1XoWy55N3sCkEHZPo3lyWq
其中$是分割符,無意義;2a是bcrypt加密版本號;10是const的值;而后的前22位是salt值;再然后的字符串就是密碼的密文了;這里的const值即生成salt的迭代次數(shù),默認值是10,推薦值12。
在項目中使用BCrypt
首先看下PasswordEncoderFactories 密碼器工廠
之前我們在項目中密碼使用的是明文的是noop
, 代表不加密使用明文密碼, 現(xiàn)在用BCrypt只需要將noop
換成bcrypt
即可
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userService.findByUsername(username); if (user == null) { throw new UsernameNotFoundException(username);// 用戶名沒有找到 } // 先聲明一個權(quán)限集合, 因為構(gòu)造方法里面不能傳入null Collection<? extends GrantedAuthority> authorities = new ArrayList<>(); // 需要返回一個SpringSecurity的UserDetails對象 UserDetails userDetails = new org.springframework.security.core.userdetails.User(user.getUsername(), "{bcrypt}" + user.getPassword(),// {noop}表示不加密認證。{bcrypt} 加密認證 true, // 用戶是否啟用 true 代表啟用 true,// 用戶是否過期 true 代表未過期 true,// 用戶憑據(jù)是否過期 true 代表未過期 true,// 用戶是否鎖定 true 代表未鎖定 authorities); return userDetails; }
同時需要將數(shù)據(jù)庫中的明文密碼修改為加密密碼
選擇一個放入數(shù)據(jù)庫即可.
2.3.5 獲取當前登錄用戶
? 在傳統(tǒng)web系統(tǒng)中, 我們將登錄成功的用戶放入session中, 在需要的時候可以從session中獲取用戶, 那么Spring Security中我們?nèi)绾潍@取當前已經(jīng)登錄的用戶呢?
SecurityContextHolder
保留系統(tǒng)當前的安全上下文SecurityContext,其中就包括當前使用系統(tǒng)的用戶的信息。
SecurityContext
安全上下文,獲取當前經(jīng)過身份驗證的主體或身份驗證請求令牌
代碼實現(xiàn):
/** * 獲取當前登錄用戶 * * @return */ @RequestMapping("/loginUser") @ResponseBody public UserDetails getCurrentUser() { UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); return userDetails; }
除了上述方法, Spring Security 還提供了2種方式可以獲取.
/** * 獲取當前登錄用戶 * * @return */ @RequestMapping("/loginUser1") @ResponseBody public UserDetails getCurrentUser() { UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); return userDetails; } /** * 獲取當前登錄用戶 * * @return */ @RequestMapping("/loginUser2") @ResponseBody public UserDetails getCurrentUser(Authentication authentication) { UserDetails userDetails = (UserDetails) authentication.getPrincipal(); return userDetails; } /** * 獲取當前登錄用戶 * * @return */ @RequestMapping("/loginUser3") @ResponseBody public UserDetails getCurrentUser(@AuthenticationPrincipal UserDetails userDetails) { return userDetails; }
2.3.5 remember me 記住我
? 在大多數(shù)網(wǎng)站中,都會實現(xiàn)RememberMe這個功能,方便用戶在下一次登錄時直接登錄,避免再次輸入用戶名以及密碼去登錄,Spring Security針對這個功能已經(jīng)幫助我們實現(xiàn), 下面我們來看下他的原理圖.
簡單的Token生成方法
? Token=MD5(username+分隔符+expiryTime+分隔符+password)
注意: 這種方式不推薦使用, 有嚴重的安全問題. 就是密碼信息在前端瀏覽器cookie中存放. 如果cookie被盜取很容易破解.
代碼實現(xiàn):
前端頁面需要增加remember-me的復(fù)選框
<div class="form-group"> <div > <!--記住我 name為remember-me value值可選true yes 1 on 都行--> <input type="checkbox" name="remember-me" value="true"/>記住我 </div> </div>
后臺代碼開啟remember-me功能
.and().rememberMe()//開啟記住我功能 .tokenValiditySeconds(1209600)// token失效時間默認2周 .rememberMeParameter("remember-me")// 自定義表單name值
登錄成功后前臺cookie
持久化的Token生成方法
? 存入數(shù)據(jù)庫Token包含:
? token: 隨機生成策略,每次訪問都會重新生成
? series: 登錄序列號,隨機生成策略。用戶輸入用戶名和密碼登錄時,該值重新生成。使用remember-me功能,該值保持不變
? expiryTime: token過期時間。
? CookieValue=encode(series+token)
代碼實現(xiàn):
后臺代碼
/** * http請求處理方法 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { /*http.httpBasic()//開啟httpbasic認證 .and().authorizeRequests(). anyRequest().authenticated();//所有請求都需要登錄認證才能訪問*/ http.formLogin()//開啟表單認證 .loginPage("/toLoginPage")//自定義登錄頁面 .loginProcessingUrl("/login")// 登錄處理Url //.usernameParameter().passwordParameter(). 修改自定義表單name值. .successForwardUrl("/")// 登錄成功后跳轉(zhuǎn)路徑 .and().authorizeRequests(). antMatchers("/toLoginPage").permitAll()//放行登錄頁面與靜態(tài)資源 .anyRequest().authenticated()//所有請求都需要登錄認證才能訪問; .and().rememberMe()//開啟記住我功能 .tokenValiditySeconds(1209600)// token失效時間默認2周 .rememberMeParameter("remember-me")// 自定義表單name值 .tokenRepository(getPersistentTokenRepository());// 設(shè)置tokenRepository // 關(guān)閉csrf防護 http.csrf().disable(); // 允許iframe加載頁面 http.headers().frameOptions().sameOrigin(); } @Autowired DataSource dataSource; /** * 持久化token,負責token與數(shù)據(jù)庫之間的相關(guān)操作 * * @return */ @Bean public PersistentTokenRepository getPersistentTokenRepository() { JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl(); tokenRepository.setDataSource(dataSource);//設(shè)置數(shù)據(jù)源 // 啟動時創(chuàng)建一張表, 第一次啟動的時候創(chuàng)建, 第二次啟動的時候需要注釋掉, 否則會報錯 tokenRepository.setCreateTableOnStartup(true); return tokenRepository; }
項目啟動成功后,觀察數(shù)據(jù)庫,會幫助我們創(chuàng)建persistent_logins表
再次完成登錄功能.
觀察數(shù)據(jù)庫,會插入一條記錄.說明持久化token方式已經(jīng)生效
再觀察cookie值
Cookie竊取偽造演示
使用網(wǎng)頁登錄系統(tǒng),記錄remember-me的值
使用postman 偽造cookie
安全驗證
/** * 根據(jù)用戶ID查詢用戶 * * @return */ @GetMapping("/{id}") @ResponseBody public User getById(@PathVariable Integer id) { //獲取認證信息 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // 判斷認證信息是否來源于RememberMe if (RememberMeAuthenticationToken.class.isAssignableFrom(authentication.getClass())) { throw new RememberMeAuthenticationException("認證信息來源于RememberMe,請重新登錄"); } User user = userService.getById(id); return user; }
在重要操作步驟可以加以驗證, true代表自動登錄,則引導(dǎo)用戶重新表單登錄, false正常進行 #### 2.3.6 自定義登錄成功處理和失敗處理 ? 在某些場景下,用戶登錄成功或失敗的情況下用戶需要執(zhí)行一些后續(xù)操作,比如登錄日志的搜集, 或者在現(xiàn)在目前前后端分離的情況下用戶登錄成功和失敗后需要給前臺頁面返回對應(yīng)的錯誤信息, 有前臺主導(dǎo)登錄成功或者失敗的頁面跳轉(zhuǎn). 這個時候需要要到用到AuthenticationSuccessHandler與AnthenticationFailureHandler. **自定義成功處理:** ? 實現(xiàn)AuthenticationSuccessHandler接口,并重寫onAnthenticationSuccesss()方法. **自定義失敗處理:** ? 實現(xiàn)AuthenticationFailureHandler接口,并重寫onAuthenticationFailure()方法; 1. 代碼實現(xiàn)登錄成功或失敗的自定義處理 - SecurityConfiguration類 ```java .successHandler(myAuthenticationService)//自定義登錄成功處理 .failureHandler(myAuthenticationService)//自定義登錄失敗處理
MyAuthenticationService類
package com.boxuegu.service.impl; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.RedirectStrategy; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; import java.util.Map; /** * 自定義登錄成功或失敗處理類 */ @Service public class MyAuthenticationService implements AuthenticationSuccessHandler, AuthenticationFailureHandler { private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException { } @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { System.out.println("登錄成功后續(xù)處理...."); //重定向到index頁 redirectStrategy.sendRedirect(request, response, "/"); } @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { System.out.println("登錄失敗后續(xù)處理...."); //重定向到login頁 redirectStrategy.sendRedirect(request, response, "/toLoginPage"); } }
異步用戶登錄實現(xiàn)
前端頁面改造
<form id="formLogin" action="/login" method="post"> <div class="panel loginbox"> ..... <div style="padding:30px;"> <input type="button" onclick="login()" class="button button-block bg-main text-big input-big" value="登錄"> </div> </div> </form> </div> </div> </div> <script> function login() { $.ajax({ type: "POST",//方法類型 dataType: "json",//服務(wù)器預(yù)期返回類型 url: "/login", // 登錄url data: $("#formLogin").serialize(), success: function (data) { console.log(data) if (data.code == 200) { window.location.href = "/"; } else { alert(data.message); } } }); } </script>
MyAuthenticationService類改造
@Autowired ObjectMapper objectMapper; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { System.out.println("登錄成功后續(xù)處理...."); //redirectStrategy.sendRedirect(request, response, "/"); Map result = new HashMap(); result.put("code", HttpStatus.OK.value());// 設(shè)置響應(yīng)碼 result.put("message", "登錄成功");// 設(shè)置響應(yīng)信息 response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(result)); } @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { System.out.println("登錄失敗后續(xù)處理...."); //redirectStrategy.sendRedirect(request, response, "/toLoginPage"); Map result = new HashMap(); result.put("code", HttpStatus.UNAUTHORIZED.value());// 設(shè)置響應(yīng)碼 result.put("message", exception.getMessage());// 設(shè)置錯誤信息 response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(result)); }
2.3.7 退出登錄
? org.springframework.security.web.authentication.logout.LogoutFilter
? 匹配URL為/logout的請求,實現(xiàn)用戶退出,清除認證信息。
? 只需要發(fā)送請求,請求路徑為/logout即可, 當然這個路徑也可以自行在配置類中自行指定, 同時退出操作也有對應(yīng)的自定義處理LogoutSuccessHandler,退出登錄成功后執(zhí)行,退出的同時如果有remember-me的數(shù)據(jù),同時一并刪除
前端頁面
<a class="button button-little bg-red" href="/logout" rel="external nofollow" > <span class="icon-power-off"></span>退出登錄</a></div>
后臺代碼
package com.boxuegu.service.impl; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.RedirectStrategy; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.stereotype.Service; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; import java.util.Map; /** * 自定義登錄成功,失敗,退出處理類 */ @Service public class MyAuthenticationService implements AuthenticationSuccessHandler, AuthenticationFailureHandler, LogoutSuccessHandler { private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); ................ @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { System.out.println("退出成功后續(xù)處理...."); redirectStrategy.sendRedirect(request, response, "/toLoginPage"); } }
.and().logout().logoutUrl("/logout")//設(shè)置退出url .logoutSuccessHandler(myAuthenticationService)//自定義退出處理
2.4 圖形驗證碼驗證
? 圖形驗證碼一般是防止惡意,人眼看起來都費勁,何況是機器。不少網(wǎng)站為了防止用戶利用機器人自動注冊、登錄、灌水,都采用了驗證碼技術(shù)。所謂驗證碼,就是將一串隨機產(chǎn)生的數(shù)字或符號,生成一幅圖片, 圖片里加上一些干擾, 也有目前需要手動滑動的圖形驗證碼. 這種可以有專門去做的第三方平臺. 比如極驗(https://www.geetest.com/), 那么本次課程講解主要針對圖形驗證碼.
spring security添加驗證碼大致可以分為三個步驟:
- 根據(jù)隨機數(shù)生成驗證碼圖片;
- 將驗證碼圖片顯示到登錄頁面;
- 認證流程中加入驗證碼校驗。
? Spring Security的認證校驗是由UsernamePasswordAuthenticationFilter過濾器完成的,所以我們的驗證碼校驗邏輯應(yīng)該在這個過濾器之前。驗證碼通過后才能到后續(xù)的操作. 流程如下:
代碼實現(xiàn):
自定義驗證碼過濾器ValidateCodeFilter
package com.boxuegu.filter; import com.boxuegu.controller.ValidateCodeController; import com.boxuegu.exception.ValidateCodeException; import com.boxuegu.service.impl.MyAuthenticationService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * 驗證碼驗證filter 需要繼承OncePerRequestFilter確保在一次請求只通過一次filter,而不需要重復(fù)執(zhí)行 */ @Component public class ValidateCodeFilter extends OncePerRequestFilter { @Autowired MyAuthenticationService myAuthenticationService; @Autowired StringRedisTemplate stringRedisTemplate; @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { // 判斷是否是登錄請求,只有登錄請求才去校驗驗證碼 if (httpServletRequest.getRequestURI().equals("/login") && httpServletRequest.getMethod().equalsIgnoreCase("post")) { try { validate(httpServletRequest); } catch (ValidateCodeException e) { myAuthenticationService.onAuthenticationFailure(httpServletRequest, httpServletResponse, e); return; } } //如果不是登錄請求,直接調(diào)用后面的過濾器鏈 filterChain.doFilter(httpServletRequest, httpServletResponse); } private void validate(HttpServletRequest request) throws ServletRequestBindingException { //獲取ip String remoteAddr = request.getRemoteAddr(); //拼接redis的key String redisKey = ValidateCodeController.REDIS_KEY_IMAGE_CODE + "-" + remoteAddr; //從redis中獲取imageCode String redisImageCode = stringRedisTemplate.boundValueOps(redisKey).get(); String imageCode = request.getParameter("imageCode"); if (!StringUtils.hasText(imageCode)) { throw new ValidateCodeException("驗證碼的值不能為空!"); } if (redisImageCode == null) { throw new ValidateCodeException("驗證碼已過期!"); } if (!redisImageCode.equals(imageCode)) { throw new ValidateCodeException("驗證碼不正確!"); } // 從redis中刪除imageCode stringRedisTemplate.delete(redisKey); } }
自定義驗證碼異常類
package com.boxuegu.exception; import org.springframework.security.core.AuthenticationException; /** * 驗證碼異常類 */ public class ValidateCodeException extends AuthenticationException { public ValidateCodeException(String msg) { super(msg); } }
security配置類
@Autowired ValidateCodeFilter validateCodeFilter; /** * http請求處理方法 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { /*http.httpBasic()//開啟httpbasic認證 .and().authorizeRequests(). anyRequest().authenticated();//所有請求都需要登錄認證才能訪問*/ // 加在用戶名密碼過濾器的前面 http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class); http.formLogin()//開啟表單認證 .loginPage("/toLoginPage")//自定義登錄頁面 .loginProcessingUrl("/login")// 登錄處理Url //.usernameParameter().passwordParameter(). 修改自定義表單name值. .successForwardUrl("/")// 登錄成功后跳轉(zhuǎn)路徑 .successHandler(myAuthenticationService)//自定義登錄成功處理 .failureHandler(myAuthenticationService)//自定義登錄失敗處理 .and().logout().logoutUrl("/logout")//設(shè)置退出url .logoutSuccessHandler(myAuthenticationService)//自定義退出處理 .and().authorizeRequests(). antMatchers("/toLoginPage","/code/**").permitAll()//放行登錄頁面與靜態(tài)資源 .anyRequest().authenticated()//所有請求都需要登錄認證才能訪問; .and().rememberMe()//開啟記住我功能 .tokenValiditySeconds(1209600)// token失效時間默認2周 .rememberMeParameter("remember-me")// 自定義表單name值 .tokenRepository(getPersistentTokenRepository());// 設(shè)置tokenRepository // 關(guān)閉csrf防護 http.csrf().disable(); // 允許iframe加載頁面 http.headers().frameOptions().sameOrigin(); }
2.5 session管理
? Spring Security可以與Spring Session庫配合使用,只需要做一些簡單的配置就可以實現(xiàn)一些功能,如(會話過期、一個賬號只能同時在線一個、集群session等)
2.5.1 會話超時
配置session會話超時時間,默認為30分鐘,但是Spring Boot中的會話超時時間至少為60秒
#session設(shè)置 #配置session超時時間 server.servlet.session.timeout=60
當session超時后, 默認跳轉(zhuǎn)到登錄頁面.
自定義設(shè)置session超時后地址
設(shè)置session管理和失效后跳轉(zhuǎn)地址
http.sessionManagement()//設(shè)置session管理 .invalidSessionUrl("/toLoginPage")// session無效后跳轉(zhuǎn)的路徑, 默認是登錄頁面
2.5.2 并發(fā)控制
? 并發(fā)控制即同一個賬號同時在線個數(shù),同一個賬號同時在線個數(shù)如果設(shè)置為1表示,該賬號在同一時間內(nèi)只能有一個有效的登錄,如果同一個賬號又在其它地方登錄,那么就將上次登錄的會話過期,即后面的登錄會踢掉前面的登錄
修改超時時間
#session設(shè)置 #配置session超時時間 server.servlet.session.timeout=600
設(shè)置最大會話數(shù)量
http.sessionManagement().//設(shè)置session管理 invalidSessionUrl("/toLoginPage")// session無效后跳轉(zhuǎn)的路徑, 默認是登錄頁面 .maximumSessions(1)//設(shè)置session最大會話數(shù)量 ,1同一時間只能有一個用戶登錄 .expiredUrl("/toLoginPage");//設(shè)置session過期后跳轉(zhuǎn)路徑
阻止用戶第二次登錄
? sessionManagement也可以配置 maxSessionsPreventsLogin:boolean值,當達到maximumSessions設(shè)置的最大會話個數(shù)時阻止登錄。
http.sessionManagement().//設(shè)置session管理 invalidSessionUrl("/toLoginPage")// session無效后跳轉(zhuǎn)的路徑, 默認是登錄頁面 .maximumSessions(1)//設(shè)置session最大會話數(shù)量 ,1同一時間只能有一個用戶登錄 .maxSessionsPreventsLogin(true)//當達到最大會話個數(shù)時阻止登錄。 .expiredUrl("/toLoginPage");//設(shè)置session過期后跳轉(zhuǎn)路徑
2.5.3 集群session
? 實際場景中一個服務(wù)會至少有兩臺服務(wù)器在提供服務(wù),在服務(wù)器前面會有一個nginx做負載均衡,用戶訪問nginx,nginx再決定去訪問哪一臺服務(wù)器。當一臺服務(wù)宕機了之后,另一臺服務(wù)器也可以繼續(xù)提供服務(wù),保證服務(wù)不中斷。如果我們將session保存在Web容器(比如tomcat)中,如果一個用戶第一次訪問被分配到服務(wù)器1上面需要登錄,當某些訪問突然被分配到服務(wù)器二上,因為服務(wù)器二上沒有用戶在服務(wù)器一上登錄的會話session信息,服務(wù)器二還會再次讓用戶登錄,用戶已經(jīng)登錄了還讓登錄就感覺不正常了。
? 解決這個問題的思路是用戶登錄的會話信息不能再保存到Web服務(wù)器中,而是保存到一個單獨的庫(redis、mongodb、jdbc等)中,所有服務(wù)器都訪問同一個庫,都從同一個庫來獲取用戶的session信息,如用戶在服務(wù)器一上登錄,將會話信息保存到庫中,用戶的下次請求被分配到服務(wù)器二,服務(wù)器二從庫中檢查session是否已經(jīng)存在,如果存在就不用再登錄了,可以直接訪問服務(wù)了。
引用依賴
<!-- 基于redis實現(xiàn)session共享 --> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>
設(shè)置session存儲類型
#使用redis共享session spring.session.store-type=redis
測試
- 使用其中一個服務(wù)去登錄 http://localhost:8080/login
- 使用另一個服務(wù)訪問任意接口 ,則不需要再重新登錄就可以直接訪問
2.6 csrf防護機制
2.6.1 什么是csrf?
? CSRF(Cross-site request forgery),中文名稱:跨站請求偽造
? 你這可以這么理解CSRF攻擊:攻擊者盜用了你的身份,以你的名義發(fā)送惡意請求。CSRF能夠做的事情包括:以你名義發(fā)送郵件,發(fā)消息,盜取你的賬號,甚至于購買商品,虛擬貨幣轉(zhuǎn)賬…造成的問題包括:個人隱私泄露以及財產(chǎn)安全。
? CSRF這種攻擊方式在2000年已經(jīng)被國外的安全人員提出,但在國內(nèi),直到06年才開始被關(guān)注,08年,國內(nèi)外的多個大型社區(qū)和交互網(wǎng)站分別爆出CSRF漏洞,如:NYTimes.com(紐約時報)、Metafilter(一個大型的BLOG網(wǎng)站),YouTube和百度HI…而現(xiàn)在,互聯(lián)網(wǎng)上的許多站點仍對此毫無防備,以至于安全業(yè)界稱CSRF為“沉睡的巨人”。
2.6.2 CSRF的原理
從上圖可以看出,要完成一次CSRF攻擊,受害者必須依次完成三個步驟:
1.登錄受信任網(wǎng)站A,并在本地生成Cookie。
2.在不登出A的情況下,訪問危險網(wǎng)站B。
? 3. 觸發(fā)網(wǎng)站B中的一些元素
2.6.3 CSRF的防御策略
? 在業(yè)界目前防御 CSRF 攻擊主要有三種策略:驗證 HTTP Referer 字段;在請求地址中添加 token 并驗證;在 HTTP 頭中自定義屬性并驗證。
驗證 HTTP Referer 字段
? 根據(jù) HTTP 協(xié)議,在 HTTP 頭中有一個字段叫 Referer,它記錄了該 HTTP 請求的來源地址。在通常情況下,訪問一個安全受限頁面的請求來自于同一個網(wǎng)站,在后臺請求驗證其 Referer 值,如果是以自身安全網(wǎng)站開頭的域名,則說明該請求是是合法的。如果 Referer 是其他網(wǎng)站的話,則有可能是黑客的 CSRF 攻擊,拒絕該請求。
在請求地址中添加 token 并驗證
? CSRF 攻擊之所以能夠成功,是因為黑客可以完全偽造用戶的請求,該請求中所有的用戶驗證信息都是存在于 cookie 中,因此黑客可以在不知道這些驗證信息的情況下直接利用用戶自己的 cookie 來通過安全驗證。要抵御 CSRF,關(guān)鍵在于在請求中放入黑客所不能偽造的信息,并且該信息不存在于 cookie 之中??梢栽?HTTP 請求中以參數(shù)的形式加入一個隨機產(chǎn)生的 token,并在服務(wù)器端建立一個攔截器來驗證這個 token,如果請求中沒有 token 或者 token 內(nèi)容不正確,則認為可能是 CSRF 攻擊而拒絕該請求。
在 HTTP 頭中自定義屬性并驗證
? 這種方法也是使用 token 并進行驗證,和上一種方法不同的是,這里并不是把 token 以參數(shù)的形式置于 HTTP 請求之中,而是把它放到 HTTP 頭中自定義的屬性里。
2.6.4 security中的csrf防御機制
? org.springframework.security.web.csrf.CsrfFilter
csrf又稱跨站請求偽造,SpringSecurity會對所有post請求驗證是否包含系統(tǒng)生成的csrf的token信息,如果不包含,則報錯。起到防止csrf攻擊的效果。(1. 生成token 2.驗證token)
開啟csrf防護
//開啟csrf防護, 可以設(shè)置哪些不需要防護 http.csrf().ignoringAntMatchers("/user/save");
頁面需要添加token值
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
3.第三部分 SpringSecurity授權(quán)
3.1 授權(quán)簡介
? 在第二部分中我們講解的都是用戶認證, 不管是用戶名密碼,還是圖形驗證碼等,最終的目的都是一個: 讓系統(tǒng)知道你到底是誰在訪問你的系統(tǒng), 解決的問題是, 你是誰? 這部分主要講解你能在系統(tǒng)中做什么事情, 針對這個有的叫做: 授權(quán), 有的叫做:鑒權(quán), 還有叫權(quán)限控制. 最終的目的就是你能在系統(tǒng)中能過做什么?
3.1.1 Spring Security 對授權(quán)的定義
? 安全權(quán)限控制問題其實就是控制能否訪問url
3.1.2 Spring Security 授權(quán)原理
在我們應(yīng)用系統(tǒng)里面,如果想要控制用戶權(quán)限,需要有2部分數(shù)據(jù)。
系統(tǒng)配置信息數(shù)據(jù):寫著系統(tǒng)里面有哪些URL,每一個url擁有哪些權(quán)限才允許被訪問。
另一份數(shù)據(jù)就是用戶權(quán)限信息:請求用戶擁有權(quán)限
系統(tǒng)用戶發(fā)送一個請求:系統(tǒng)配置信息和用戶權(quán)限信息作比對,如果比對成功則允許訪問。
當一個系統(tǒng)授權(quán)規(guī)則比較簡單,基本不變時候,系統(tǒng)的權(quán)限配置信息可以寫在我們的代碼里面去的。比如前臺門戶網(wǎng)站等權(quán)限比較單一,可以使用簡單的授權(quán)配置即可完成,如果權(quán)限復(fù)雜, 例如辦公OA, 電商后臺管理系統(tǒng)等就不能使用寫在代碼里面了. 需要RBAC權(quán)限模型設(shè)計.
3.2 Spring Security 授權(quán)
3.2.1 內(nèi)置權(quán)限表達式
? Spring Security 使用Spring EL來支持,主要用于Web訪問和方法安全上, 可以通過表達式來判斷是否具有訪問權(quán)限. 下面是Spring Security常用的內(nèi)置表達式. ExpressionUrlAuthorizationConfigurer定義了所有的表達式
表達式 | 說明 |
---|---|
permitAll | 指定任何人都允許訪問。 |
denyAll | 指定任何人都不允許訪問 |
anonymous | 指定匿名用戶允許訪問。 |
rememberMe | 指定已記住的用戶允許訪問。 |
authenticated | 指定任何經(jīng)過身份驗證的用戶都允許訪問,不包含anonymous |
fullyAuthenticated | 指定由經(jīng)過身份驗證的用戶允許訪問,不包含anonymous和rememberMe |
hasRole(role) | 指定需要特定的角色的用戶允許訪問, 會自動在角色前面插入’ROLE_’ |
hasAnyRole([role1,role2]) | 指定需要任意一個角色的用戶允許訪問, 會自動在角色前面插入’ROLE_’ |
hasAuthority(authority) | 指定需要特定的權(quán)限的用戶允許訪問 |
hasAnyAuthority([authority,authority]) | 指定需要任意一個權(quán)限的用戶允許訪問 |
hasIpAddress(ip) | 指定需要特定的IP地址可以訪問 |
3.2.2 url安全表達式
? 基于web訪問使用表達式保護url請求路徑.
設(shè)置url訪問權(quán)限
// 設(shè)置/user/** 訪問需要ADMIN角色 http.authorizeRequests().antMatchers("/user/**").hasRole("ADMIN"); // 設(shè)置/user/** 訪問需要PRODUCT角色和IP地址為127.0.0.1 .hasAnyRole("PRODUCT,ADMIN") http.authorizeRequests().antMatchers("/product/**") .access("hasAnyRole('ADMIN,PRODUCT') and hasIpAddress('127.0.0.1')"); // 設(shè)置自定義權(quán)限不足信息. http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
MyAccessDeniedHandler自定義權(quán)限不足類
package com.boxuegu.handle; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * 自定義權(quán)限不足信息 */ @Component public class MyAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse resp, AccessDeniedException e) throws IOException, ServletException { resp.setStatus(HttpServletResponse.SC_FORBIDDEN); resp.setContentType("text/html;charset=UTF-8"); resp.getWriter().write("權(quán)限不足,請聯(lián)系管理員!"); } }
設(shè)置用戶對應(yīng)的角色權(quán)限
// 先聲明一個權(quán)限集合, 因為構(gòu)造方法里面不能傳入null Collection<GrantedAuthority> authorities = new ArrayList<>(); if ("admin".equalsIgnoreCase(user.getUsername())) { authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); } else { authorities.add(new SimpleGrantedAuthority("ROLE_PRODUCT")); }
3.2.3 在Web 安全表達式中引用自定義Bean授權(quán)
定義自定義授權(quán)類
package com.boxuegu.service.impl; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import java.util.Collection; /** * 自定義授權(quán)類 */ @Component public class MyAuthorizationService { /** * 檢查用戶是否有對應(yīng)的訪問權(quán)限 * * @param authentication 登錄用戶 * @param request 請求對象 * @return */ public boolean check(Authentication authentication, HttpServletRequest request) { User user = (User) authentication.getPrincipal(); // 獲取用戶所有權(quán)限 Collection<GrantedAuthority> authorities = user.getAuthorities(); // 獲取用戶名 String username = user.getUsername(); // 如果用戶名為admin,則不需要認證 if (username.equalsIgnoreCase("admin")) { return true; } else { // 循環(huán)用戶的權(quán)限, 判斷是否有ROLE_ADMIN權(quán)限, 有返回true for (GrantedAuthority authority : authorities) { String role = authority.getAuthority(); if ("ROLE_ADMIN".equals(role)) { return true; } } } return false; } }
配置類
//使用自定義Bean授權(quán) http.authorizeRequests().antMatchers("/user/**"). access("@myAuthorizationService.check(authentication,request)");
攜帶路徑變量
/** * 檢查用戶是否有對應(yīng)的訪問權(quán)限 * * @param authentication 登錄用戶 * @param request 請求對象 * @param id 參數(shù)ID * @return */ public boolean check(Authentication authentication, HttpServletRequest request, Integer id) { if (id > 10) { return false; } return true; }
//使用自定義Bean授權(quán),并攜帶路徑參數(shù) http.authorizeRequests().antMatchers("/user/delete/{id}"). access("@myAuthorizationService.check(authentication,request,#id)");
3.2.4 Method安全表達式
? 針對方法級別的訪問控制比較復(fù)雜,spring security
提供了4種注解分別是 @PreAuthorize
,@PostAuthorize
,
@PreFilter
,@PostFilter
.
開啟方法級別的注解配置
在security配置類中添加注解
/** * Security配置類 */ @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true)//開啟注解支持 public class SecurityConfiguration extends WebSecurityConfigurerAdapter /** * Security配置類 */ @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true)//開啟注解支持 public class SecurityConfiguration extends WebSecurityConfigurerAdapter
在方法上使用注解
@ProAuthorize : 注解適合進入方法前的權(quán)限驗證
/** * 查詢所有用戶 * * @return */ @RequestMapping("/findAll") @PreAuthorize("hasRole('ADMIN')")//需要ADMIN權(quán)限 public String findAll(Model model) { List<User> userList = userService.list(); model.addAttribute("userList", userList); return "user_list"; } /** * 用戶修改頁面跳轉(zhuǎn) * * @return */ @RequestMapping("/update/{id}") @PreAuthorize("#id<10")//針對參數(shù)權(quán)限限定 id<10可以訪問 public String update(@PathVariable Integer id, Model model) { User user = userService.getById(id); model.addAttribute("user", user); return "user_update"; }
@PostAuthorize: @PostAuthorize在方法執(zhí)行后再進行權(quán)限驗證,適合驗證帶有返回值的權(quán)限,Spring EL
提供返回對象能夠在表達式語言中獲取到返回對象的 returnObject
/** * 根據(jù)ID查詢用戶 * * @return */ @GetMapping("/{id}") @ResponseBody @PostAuthorize("returnObject.username== authentication.principal.username")//判斷查詢用戶信息是否是當前登錄用戶信息.否則沒有權(quán)限 public User getById(@PathVariable Integer id) { User user = userService.getById(id); return user; }
? returnObject : 代表return返回的值
@PreFilter: 可以用來對集合類型的參數(shù)進行過濾, 將不符合條件的元素剔除集合
/** * 商品刪除-多選刪除 * * @return */ @GetMapping("/delByIds") @PreFilter(filterTarget = "ids", value = "filterObject%2==0")//剔除參數(shù)為基數(shù)的值 public String delByIds(@RequestParam(value = "id") List<Integer> ids) { for (Integer id : ids) { System.out.println(id); } return "redirect:/user/findAll"; }
@PostFilter: 可以用來對集合類型的返回值進行過濾, 將不符合條件的元素剔除集合
/** * 查詢所有用戶-返回json數(shù)據(jù) * * @return */ @RequestMapping("/findAllTOJson") @ResponseBody @PostFilter("filterObject.id%2==0")//剔除返回值ID為偶數(shù)的值 public List<User> findAllTOJson() { List<User> userList = userService.list(); return userList; }
3.3 基于數(shù)據(jù)庫的RBAC數(shù)據(jù)模型的權(quán)限控制
? 我們開發(fā)一個系統(tǒng),必然面臨權(quán)限控制的問題,不同的用戶具有不同的訪問、操作、數(shù)據(jù)權(quán)限。形成理論的權(quán)限控制模型有:自主訪問控制(DAC: Discretionary Access Control)、強制訪問控制(MAC: Mandatory Access Control)、基于屬性的權(quán)限驗證(ABAC: Attribute-Based Access Control)等。最常被開發(fā)者使用也是相對易用、通用的就是RBAC權(quán)限模型(Role-Based Access Control)
3.3.1 RBAC權(quán)限模型簡介
RBAC權(quán)限模型(Role-Based Access Control)即:基于角色的權(quán)限控制。模型中有幾個關(guān)鍵的術(shù)語:
- 用戶:系統(tǒng)接口及訪問的操作者
- 權(quán)限:能夠訪問某接口或者做某操作的授權(quán)資格
- 角色:具有一類相同操作權(quán)限的總稱
RBAC權(quán)限模型核心授權(quán)邏輯如下:
某用戶是什么角色?某角色具有什么權(quán)限?通過角色對應(yīng)的權(quán)限推導(dǎo)出用戶的權(quán)限 3.3.2 RBAC的演化進程
用戶與權(quán)限直接關(guān)聯(lián)
? 想到權(quán)限控制,人們最先想到的一定是用戶與權(quán)限直接關(guān)聯(lián)的模式,簡單地說就是:某個用戶具有某些權(quán)限。如圖:
- 張三具有所有權(quán)限他可能是一個超級管理員.
- 李四,王五 具有添加商品和審核商品的權(quán)限有可能是一個普通業(yè)務(wù)員
這種模型能夠清晰的表達用戶與權(quán)限之間的關(guān)系,足夠簡單。但同時也存在問題:
- 現(xiàn)在用戶是張三、李四,王五以后隨著人員增加,每一個用戶都需要重新授權(quán)
- 操作人員的他的權(quán)限發(fā)生變更后,需要對每個一個用戶重新授予新的權(quán)限
用戶與角色關(guān)聯(lián)
這樣只需要維護角色和權(quán)限之間的關(guān)系就可以了. 如果業(yè)務(wù)員的權(quán)限發(fā)生變更, 只需要變動業(yè)務(wù)員角色和權(quán)限之前的關(guān)系進行維護就可以了. 用戶和權(quán)限就分離開來了. 如下圖
3.3.3 基于RBAC設(shè)計權(quán)限表結(jié)構(gòu)
- 一個用戶有一個或多個角色
- 一個角色包含多個用戶
- 一個角色有多種權(quán)限
- 一個權(quán)限屬于多個角色
3.3.4 基于Spring Security 實現(xiàn)RBAC權(quán)限管理
動態(tài)查詢數(shù)據(jù)庫中用戶對應(yīng)的權(quán)限
package com.boxuegu.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.boxuegu.domain.Permission; import org.apache.ibatis.annotations.Select; import java.util.List; public interface PermissionMapper extends BaseMapper<Permission> { /** * 根據(jù)用戶ID查詢權(quán)限 * * @param id * @return */ @Select("SELECT p.* FROM t_permission p,t_role_permission rp,t_role r,t_user_role ur,t_user u " + "WHERE p.id = rp.PID AND rp.RID = r.id AND r.id = ur.RID AND ur.UID = u.id AND u.id =#{id}") List<Permission> findByUserId(Integer id); }
給登錄用戶授權(quán)
// 先聲明一個權(quán)限集合, 因為構(gòu)造方法里面不能傳入null Collection<GrantedAuthority> authorities = new ArrayList<>(); // 查詢用戶對應(yīng)所有權(quán)限 List<Permission> permissions = permissionService.findByUserId(user.getId()); for (Permission permission : permissions) { // 授權(quán) authorities.add(new SimpleGrantedAuthority(permission.getPermissionTag())); }
設(shè)置訪問權(quán)限
// 查詢數(shù)據(jù)庫所有權(quán)限列表 List<Permission> permissions = permissionService.list(); for (Permission permission : permissions) { //添加請求權(quán)限 http.authorizeRequests(). antMatchers(permission.getPermissionUrl()).hasAuthority(permission.getPermissionTag()); }
3.4 基于頁面端標簽的權(quán)限控制
? 在jsp頁面或者thymeleaf模板頁面中我們可以使用spring security提供的權(quán)限標簽來進行權(quán)限控制.要想使用thymeleaf為SpringSecurity提供的標簽屬性,首先需要引入thymeleaf-extras-springsecurity依賴支持。
在pom 文件中的引入springsecurity的標簽依賴thymeleaf-extras-springsecurity5。
<!--添加thymeleaf為SpringSecurity提供的標簽 依賴 --> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> <version>3.0.4.RELEASE</version> </dependency>
在html文件里面申明使用
!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
3.4.1 常用SpringSecurity的標簽屬性介紹
判斷用戶是否已經(jīng)登陸認證,引號內(nèi)的參數(shù)必須是isAuthenticated()。
sec:authorize=“isAuthenticated()”
獲得當前用戶的用戶名,引號內(nèi)的參數(shù)必須是name。
sec:authentication=“name”判斷當前用戶是否擁有指定的權(quán)限。引號內(nèi)的參數(shù)為權(quán)限的名稱。
sec:authorize=“hasRole(‘role’)”
3.4.2 SpringSecurity標簽的使用
<div class="leftnav"> <div class="leftnav-title"> <div sec:authorize="isAuthenticated()"> <span sec:authentication="name"></span> <img src="images/y.jpg" class="radius-circle rotate-hover" height="50" alt=""/></div> </div> <div sec:authorize="hasAuthority('user:findAll')"> <h2><span class="icon-user"></span>系統(tǒng)管理</h2> <ul style="display:block"> <li><a href="/user/findAll" target="right"><span class="icon-caret-right"></span>用戶管理</a></li> <li><a href="javascript:void(0)" onclick="toCors()" target="right"><span class="icon-caret-right"></span>跨域測試</a></li> </ul> </div> <div sec:authorize="hasAuthority('product:findAll')"> <h2><span class="icon-pencil-square-o"></span>數(shù)據(jù)管理</h2> <ul> <li><a href="/product/findAll" target="right"><span class="icon-caret-right"></span>商品管理</a></li> </ul> </div> </div>
簽屬性,首先需要引入thymeleaf-extras-springsecurity依賴支持。
在pom 文件中的引入springsecurity的標簽依賴thymeleaf-extras-springsecurity5。
<!--添加thymeleaf為SpringSecurity提供的標簽 依賴 --> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> <version>3.0.4.RELEASE</version> </dependency>
在html文件里面申明使用
!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
3.4.1 常用SpringSecurity的標簽屬性介紹
判斷用戶是否已經(jīng)登陸認證,引號內(nèi)的參數(shù)必須是isAuthenticated()。
sec:authorize=“isAuthenticated()”
獲得當前用戶的用戶名,引號內(nèi)的參數(shù)必須是name。
sec:authentication=“name”判斷當前用戶是否擁有指定的權(quán)限。引號內(nèi)的參數(shù)為權(quán)限的名稱。
sec:authorize=“hasRole(‘role’)”
3.4.2 SpringSecurity標簽的使用
<div class="leftnav"> <div class="leftnav-title"> <div sec:authorize="isAuthenticated()"> <span sec:authentication="name"></span> <img src="images/y.jpg" class="radius-circle rotate-hover" height="50" alt=""/></div> </div> <div sec:authorize="hasAuthority('user:findAll')"> <h2><span class="icon-user"></span>系統(tǒng)管理</h2> <ul style="display:block"> <li><a href="/user/findAll" target="right"><span class="icon-caret-right"></span>用戶管理</a></li> <li><a href="javascript:void(0)" onclick="toCors()" target="right"><span class="icon-caret-right"></span>跨域測試</a></li> </ul> </div> <div sec:authorize="hasAuthority('product:findAll')"> <h2><span class="icon-pencil-square-o"></span>數(shù)據(jù)管理</h2> <ul> <li><a href="/product/findAll" target="right"><span class="icon-caret-right"></span>商品管理</a></li> </ul> </div> </div>
到此這篇關(guān)于如何使用SpringSecurity的文章就介紹到這了,更多相關(guān)SpringSecurity使用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- springsecurity實現(xiàn)攔截器的使用示例
- SpringSecurity整合JWT的使用示例
- SpringSecurity攔截器鏈的使用詳解
- SpringSecurity實現(xiàn)權(quán)限認證與授權(quán)的使用示例
- SpringSecurity默認登錄頁的使用示例教程
- 使用SpringSecurity+defaultSuccessUrl不跳轉(zhuǎn)指定頁面的問題解決方法
- springsecurity實現(xiàn)用戶登錄認證快速使用示例代碼(前后端分離項目)
- Spring Security 使用 OncePerRequestFilter 過濾器校驗登錄過期、請求日志等操作
- Spring Security使用多種加密方式進行密碼校驗的代碼示例
- 新版SpringSecurity5.x使用與配置詳解
相關(guān)文章
關(guān)于Java繼承中父類和子類構(gòu)造函數(shù)的問題
這篇文章主要介紹了關(guān)于Java繼承中父類和子類構(gòu)造函數(shù)的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-10-10Spark調(diào)優(yōu)多線程并行處理任務(wù)實現(xiàn)方式
這篇文章主要介紹了Spark調(diào)優(yōu)多線程并行處理任務(wù)實現(xiàn)方式,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-08-08springboot配置多數(shù)據(jù)源并集成Druid和mybatis的操作
這篇文章主要介紹了springboot配置多數(shù)據(jù)源并集成Druid和mybatis的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07Spring?boot?自定義?Starter及自動配置的方法
Starter?組件是?Spring?boot?的一個核心特性,Starter組件的出現(xiàn)極大的簡化了項目開發(fā),這篇文章主要介紹了Spring?boot?自定義?Starter?及?自動配置,需要的朋友可以參考下2022-12-12使用System.exit()來優(yōu)雅地終止SpringBoot項目的代碼示例
System.exit() 方法是 Java 中用于退出程序的方法,它接受一個整數(shù)參數(shù),通常被用來指示程序的退出狀態(tài),本文給大家介紹了如何使用System.exit()來優(yōu)雅地終止SpringBoot項目,需要的朋友可以參考下2024-08-08Java框架搭建之Maven、Mybatis、Spring MVC整合搭建(圖文)
這篇文章主要介紹了Java框架搭建之Maven、Mybatis、Spring MVC整合搭建(圖文),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-12-12feign調(diào)用第三方接口,編碼定義GBK,響應(yīng)中文亂碼處理方式
這篇文章主要介紹了feign調(diào)用第三方接口,編碼定義GBK,響應(yīng)中文亂碼處理方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-01-01