案例源碼地址:https://gitee.com/gzl_com/spring-security.git
一、SpringSecurity 框架簡介
1.1、概要
Spring Security 是 Spring 家族中的成員。Spring Security 基于 Spring 框架,提供了一套 Web 應(yīng)用安全性的完整解決方案。
安全方面的兩個主要區(qū)域是“認證
”和“授權(quán)
”。在Web 應(yīng)用又稱之為用戶認證
和用戶授權(quán)
兩個部分,這兩點也是 Spring Security 重要核心功能。
1.2、Spring Security到底能干什么?
談?wù)搩?yōu)點的同時,不妨先考慮一下,沒有Spring Security我們難道就無法實現(xiàn)認證和授權(quán)了嗎?
肯定不是的,一般涉及到用戶授權(quán),我們都會分為用戶表、角色表、用戶角色表、菜單表、角色菜單表。
有這五張表,就算沒有Spring Security我們依舊可以完成用戶菜單等控制
。
那他究竟在項目中起到了什么作用呢?
-
和 Spring 無縫整合,在SpringBoot下更加簡便。在SpringBoot的自動裝配下,
可能我們只需要寫一行配置,就能實現(xiàn)一個功能
。 -
對身份驗證和授權(quán)的全面且可擴展的支持,到底有多全面?
(1)他提供了登錄頁和退出頁,假如我們著急做項目不想做登錄頁,只需要引入個依賴添加少量配置就可以快捷的完成一個登錄功能
(2)使用SpringSecurity可以輕松完成接口的權(quán)限管理,假如不用SpringSecurity,那我們想要指定某個接口只允許擁有某個角色才能訪問,這時候我們可能還得用攔截器來做,而且相當麻煩。
(3)這么給你說吧,你做登錄和授權(quán),只要是你能想到的,不管是安全方面,還是控制方面,SpringSecurity都能實現(xiàn),最主要的是通過一個簡單配置就能實現(xiàn)。 -
可以防止會話固定、點擊劫持、跨站點請求偽造等攻擊
1.3、常用術(shù)語
-
認證(Identification): 根據(jù)聲明者持有的特定信息,來確認聲明者的身份
- 例如身份證、用戶名/密碼、手機(包括短信、二維碼、手勢密碼)、電子郵箱、生物特征(虹膜、面部、指紋、語音等)
- 高安全要求的場景下,會使用多種認證方式組合進行身份校驗,即多因素認證
-
授權(quán)(Authorization): 資源所有者委派執(zhí)行者,賦予其指定范圍的權(quán)限,執(zhí)行對資源的操作
- 授權(quán)實體例如銀行卡、門禁卡、鑰匙、證書等
- 例如web服務(wù)的session機制、瀏覽器的cookie機制、授權(quán)令牌token等
-
鑒權(quán)(Authentication): 對一個聲明者的身份權(quán)利的真實性進行鑒別
- 授權(quán)和鑒權(quán)是一個一一對應(yīng)的流程,有授權(quán)才會去鑒權(quán)
- 權(quán)限控制(Access/Permission Control): 權(quán)限控制就是授權(quán)和鑒權(quán)的統(tǒng)稱
-
主體:誰使用系統(tǒng)誰就是主體。
- 可以是使用系統(tǒng)的用戶 或 設(shè)備 或 從其他系統(tǒng)遠程登錄的用戶 等等。
1.4、歷史
Spring Security的前身并非稱呼為Spring Security,而是叫Acegi Security;但這并不意味著它與Spring毫無關(guān)系,它仍然是為Spring提供安全支持的。Acegi Security搭上了Spring的便車,搖身一變成為Spring Security,但即便如此其還是繼承了AcegiSecurity的臃腫繁瑣的配置,學(xué)習成本相對還是十分的高。
直到有一天, Spring Boot橫空出世,提出約定優(yōu)于配置等理念,極大的簡化了繁瑣的配置; SpringSecurity也收益于此,一飛沖天。
1.5、同款產(chǎn)品對比
SpringSecurity 特點:
- SpringSecurity是Spring 技術(shù)棧的組成部分,能和Spring 無縫整合。
- 全面的權(quán)限控制。
- 重量級。
Shiro 特點:
- Apache 旗下的輕量級權(quán)限控制框架。
- Shiro 主張的理念是把復(fù)雜的事情變簡單。針對對
性能
有更高要求的互聯(lián)網(wǎng)應(yīng)用有更好表現(xiàn)。
在 SSM 中整合 Spring Security 都是比較麻煩的操作,所以,Spring Security 雖然功能比 Shiro 強大,但是使用者 反而沒有 Shiro 多(Shiro 雖然功能沒有Spring Security 多,但是對于大部分項目而言,Shiro 也夠用了)。
自從有了 Spring Boot 之后,Spring Boot 對于 Spring Security 提供了自動化配置方案,可以使用更少的配置來使用 Spring Security。
因此,一般來說,常見的安全管理技術(shù)棧的組合是這樣的:
- SSM + Shiro
- Spring Boot/Spring Cloud + Spring Security
以上只是一個推薦的組合而已,如果單純從技術(shù)上來說,無論怎么組合,都是可以運行的。
1.6、模塊劃分

二、SpringSecurity 入門案例
2.1、創(chuàng)建項目
溫馨提示:
這里只是創(chuàng)建項目,如果會的直接跳過,無視就可以!
這里選擇springboot項目,然后創(chuàng)建項目的時候默認選擇的default,但是我用的是手機熱點,是創(chuàng)建不出來的,所以自己添加了地址http://start.springboot.io/然后改為了Custom,說白了默認的是https,我改為了http然后創(chuàng)建成功了,至于為什么創(chuàng)建不成功1、可能是網(wǎng)絡(luò)原因 2、可能是開熱點原因


添加這兩個即可。

2.2、創(chuàng)建配置類
任何框架只要是基于spring 來整合的框架,我們想要改框架配置,有以下方式:
- 實現(xiàn)接口,重寫方法,然后放到容器里面
- 要么就是繼承類,重寫方法,然后放到容器里面
- 要么就是直接new一個框架提供好的類,然后通過@Bean注解 放到容器里面
為什么全是放到容器里面?因為任何框架和spring整合都有一點,他都會檢測容器里面是否存在配置,如果沒有就會創(chuàng)建一個默認配置放到容器里面,如果有則使用容器里面的。
WebSecurityConfigurerAdapter就是Security的核心配置類,一般我們要用Security都會涉及到這個類,一般就是繼承這個類,重寫方法。
package com.gzl.cn.springsecuritydemo1.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;
// @Configuration注解的作用就是放到容器里面
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 表單登錄
.and()
.authorizeRequests() // 認證配置
.anyRequest() // 任何請求
.authenticated(); // 都需要身份驗證
}
}
2.3、啟動測試

默認的用戶名:user
密碼在項目啟動的時候在控制臺會打印,注意每次啟動的時候密碼都回發(fā)生變化!

輸入用戶名,密碼,這樣表示可以訪問了,404 表示我們沒有這個控制器,但是我們可以訪問了。

2.4、添加訪問成功后的地址
package com.gzl.cn.springsecuritydemo1.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class IndexController {
@GetMapping("index")
@ResponseBody
public String index() {
return "success";
}
}
修改配置文件,改為登錄成功之后跳到這個請求上:
這時候會發(fā)現(xiàn),不登錄是訪問不了這個http://localhost:8080/index請求的,訪問他就會自動跳到登錄頁,原因是我們設(shè)置了所有請求必須登錄認證,我們是入門練習自己并沒有設(shè)置登錄頁,所以他就自動跳到了SpringSecurity提供的默認登錄頁,登錄成功后默認跳轉(zhuǎn)到index請求。

2.5、總結(jié)
通過入門案例我們不難發(fā)現(xiàn),我們只添加了一個簡單的配置,SpringSecurity便已經(jīng)將登錄功能做好了,包括請求攔截、登錄跳轉(zhuǎn)等…這就是SpringSecurity真正的作用,在我們做登錄的時候,基本上我們能想到的功能SpringSecurity都能幫我們輕松實現(xiàn)。
三、spring Security原理
3.1、spring Security過濾器
spring Security采用責任鏈的設(shè)計模式,它有一條很長的過濾器鏈。通過不同的過濾器處理相應(yīng)的業(yè)務(wù)流程,如登錄認證、權(quán)限過濾等。
其中15條過濾器是很常用的,業(yè)務(wù)中經(jīng)常涉及到的,每一個過濾器都有自己的作用,通過了解過濾器也能更清晰的學(xué)習SpringSecurity的功能。
-
org.springframework.security.web.context.SecurityContextPersistenceFilter
:SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存或更新一個SecurityContext,并將SecurityContext給以后的過濾器使用,來為后續(xù)filter建立所需的上下文。SecurityContext中存儲了當前用戶的認證以及權(quán)限信息。 -
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
:此過濾器用于集成SecurityContext到Spring異步執(zhí)行機制中的WebAsyncManager -
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)限。
3.2、ExceptionTranslationFilter
ExceptionTranslationFilter:是個異常過濾器,用來處理在認證授權(quán)過程中拋出的異常
通過這里不難發(fā)現(xiàn),框架同樣也是try-catch方式來進行處理異常。
3.3、UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter :對/login 的 POST 請求做攔截,校驗表單中用戶名,密碼。
通過源碼不難發(fā)現(xiàn):一旦我們使用SpringSecurity來作為認證框架,我們要寫自己的登錄的接口的話,一定要用post請求
,并且用戶名和密碼是固定的,只能用username和password來作為參數(shù)名
,因為這是SpringSecurity默認的(如果執(zhí)意要改,可以通過配置文件進行改)。
3.4、默認生成密碼的源碼探究
1、先追蹤日志是從哪打印出來的
和用戶相關(guān)的自動化配置類在 UserDetailsServiceAutoConfiguration 里邊,在該類的 getOrDeducePassword 方法中,我們看到如下一行日志:
2、getOrDeducePassword()是被誰調(diào)用的?
是被UserDetailsServiceAutoConfiguration類當中注入InMemoryUserDetailsManager的時候調(diào)用的,代碼有點長,我就給復(fù)制出來了,避免看不見。
@Bean
@Lazy
public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties, ObjectProvider<PasswordEncoder> passwordEncoder) {
User user = properties.getUser();
List<String> roles = user.getRoles();
return new InMemoryUserDetailsManager(new UserDetails[]{org.springframework.security.core.userdetails.User.withUsername(user.getName())
.password(this.getOrDeducePassword(user, (PasswordEncoder)passwordEncoder.getIfAvailable()))
.roles(StringUtils.toStringArray(roles)).build()});
}
3、isPasswordGenerated 方法返回 true才打印
isPasswordGenerated實際上是user對象passwordGenerated屬性,而user類是SecurityProperties一個內(nèi)部類對象。
打開SecurityProperties對象不難發(fā)現(xiàn),他里面用到了@ConfigurationProperties注解,這個注解主要作用就是讀取application配置文件當中的值,注入到j(luò)ava對象屬性當中。
下面可以看出password默認是使用的uuid,然后賬號是user,也就是假如application當中配置password了,那他將不再是自動生成的密碼,屬性將被覆蓋。
@ConfigurationProperties注解是通過調(diào)用對象的set方法進行賦值的。也就是我一旦在application當中設(shè)置值,那他將不再打印。
4、得出結(jié)論
可以在配置文件進行配置賬號密碼,如下是配置方式,大家可以去進行試驗,這里我就不再截圖說了。
spring.security.user.name=zhangsan
spring.security.user.password=123456
3.5、UserDetailsService 接口講解
當什么也沒有配置的時候,賬號和密碼是由 Spring Security 定義生成的。而在實際項目中賬號和密碼都是從數(shù)據(jù)庫中查詢出來的。 所以我們要通過自定義認證邏輯。如果需要自定義邏輯時,只需要實現(xiàn) UserDetailsService 接口即可
。
1、為什么要實現(xiàn)UserDetailsService?
回顧剛剛說的UserDetailsServiceAutoConfiguration,首先這個類是springboot給我們默認配置的主體類
,當我們沒有自己的登錄邏輯的時候,默認他就會走這個地方來獲取主體。記住一點這里是獲取主體,而并不是直接進行密碼比較的地方。
正常我們寫登錄可能就是前端傳過來賬號密碼,然后后端收到賬號密碼,直接通過賬號密碼兩個條件去數(shù)據(jù)庫查詢,查到了就登錄成功,查詢不到就失敗。SpringSecurity他不是這樣的,流程如下:
1、前端將賬號和密碼給后端(這里直接以明文舉例)
2、后端通過username獲取用戶信息(獲取不到那證明連這個賬號都沒有)
3、獲取到之后進行密碼比對,看看是否正確(這個過程我們稱之為身份認證)。UserDetailsService接口主要的作用就是流程的第二個步驟
。
一般AutoConfiguration結(jié)尾的類都是springboot的自動裝配類,springboot之所以用任何一個框架都可以開箱即用,就是這些自動裝配類,也就是當我們引用某個框架的時候,springboot內(nèi)部已經(jīng)準備好了這個框架的默認配置,我們根本不需要做任何改動,可能就輕松實現(xiàn)了框架整合,當然默認配置有時候會不滿足我們的需求,這時候我們就需要進行注入自己的配置。
2、springboot是如何知道你有配置了,不再用他提供的默認配置了的呢?
原因是springboot提供的默認配置類用到了以下注解:
-
@Configuration(proxyBeanMethods = false)
:根據(jù)注釋proxyBeanMethods是為了讓使用@Bean注解的方法被代理而實現(xiàn)bean的生命周期的行為。(生命周期就是@scope 屬性)
1.設(shè)置為true,那么直接調(diào)用方法獲取bean,不會創(chuàng)建新的bean,而是會走bean的生命周期的行為。
2.設(shè)置為false, 那么直接調(diào)用方法獲取bean,會創(chuàng)建新的bean,且不會走bean的生命周期的行為。 -
@ConditionalOnClass({AuthenticationManager.class})
:就是說只有在classpath下能找到AuthenticationManager才會構(gòu)建這個bean。 -
@ConditionalOnBean({ObjectPostProcessor.class})
:;它是一種依賴,表示當存在ObjectPostProcessor這個bean,才注冊當前這個bean。 -
@ConditionalOnMissingBean
:當你的bean被注冊之后,如果注冊相同類型的bean,就不會成功,它會保證你的bean只有一個,即你的實例只有一個,當你注冊多個相同的bean時,會出現(xiàn)異常。
通過@ConditionalOnMissingBean注解當中會發(fā)現(xiàn)他有對UserDetailsService 判斷,UserDetailsService 自然也成為了SpringSecurity最主要的接口
。我把UserDetailsService 稱之為了主體業(yè)務(wù)類。容器里面有主體業(yè)務(wù)類的話,就會走容器存在的,如果沒有就會使用SpringSecurity默認的主體業(yè)務(wù)類(也就是我們剛剛所看到的,沒有的話會走SecurityProperties對象當中的內(nèi)部user類對象)。
3、重點了解UserDetailsService接口
UserDetailsService接口只有一個方法,這個方法返回值 UserDetails,UserDetails這個類是系統(tǒng)默認的用戶“主體"
。登錄的時候是會訪問這個方法的,并且會將登錄傳的username以參數(shù)形式傳過來
。我們需要做的就是實現(xiàn)UserDetailsService接口,然后重寫這個方法,并返回一個UserDetails對象。
在這個方法我們一般有如下步驟:
1、根據(jù)username查詢數(shù)據(jù)庫判斷該用戶是否存在。
2、將從數(shù)據(jù)庫查出來的賬號密碼封裝到UserDetails對象當中,作為方法返回值返回。
4、了解UserDetails接口
UserDetails方法:
// 表示獲取登錄用戶所有權(quán)限
Collection<? extends GrantedAuthority> getAuthorities();
// 表示獲取密碼
String getPassword();
// 表示獲取用戶名
String getUsername();
// 表示判斷賬戶是否過期
boolean isAccountNonExpired();
// 表示判斷賬戶是否被鎖定
boolean isAccountNonLocked();
// 表示憑證{密碼}是否過期
boolean isCredentialsNonExpired();
// 表示當前用戶是否可用
boolean isEnabled();
5、了解UserDetails實現(xiàn)類
以后我們只需要使用 User 這個實體類即可!當然也可以實現(xiàn)UserDetails接口自定義一個主體類。
注意這個User類可不是剛剛提到的SecurityProperties內(nèi)部類User了。
6、用法示例
loadUserByUsername方法主要作用就是返回主體信息,至于前端傳的密碼和主題信息的密碼對不對的上,不是這塊來操心的,那是由專門的身份認證來做的。
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
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;
@Service
public class LoginService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws
UsernameNotFoundException {
// 1.根據(jù)username查詢數(shù)據(jù)庫,判斷用戶名是否存在
// 2.將數(shù)據(jù)庫當中查出來的username和pwd封裝到user對象當中返回 第三個參數(shù)表示權(quán)限
return new User(username, pwd,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,"));
}
}
3.6、PasswordEncoder 接口講解
1、了解PasswordEncoder 接口
PasswordEncoder主要負責的就是密碼和 主題信息業(yè)務(wù)類返回的密碼進行比對的時候,所要使用的加密方式。
// 表示把參數(shù)按照特定的解析規(guī)則進行解析
String encode(CharSequence rawPassword);
// 表示驗證從存儲中獲取的編碼密碼與編碼后提交的原始密碼是否匹配。如果密碼匹配,則返回 true;如果不匹配,則返回 false。第一個參數(shù)表示需要被解析的密碼。第二個參數(shù)表示存儲的密碼。
boolean matches(CharSequence rawPassword, String encodedPassword);
// 表示如果解析的密碼能夠再次進行解析且達到更安全的結(jié)果則返回 true,否則返回false。默認返回 false。
default boolean upgradeEncoding(String encodedPassword) {
return false; }
接口實現(xiàn)類:
BCryptPasswordEncoder 是 Spring Security 官方推薦的密碼解析器,平時多使用這個解析器。
BCryptPasswordEncoder 是對 bcrypt 強散列方法的具體實現(xiàn)。是基于 Hash 算法實現(xiàn)的單向加密??梢酝ㄟ^ strength 控制加密強度,默認 10.
單向加密就是通過密文獲取不了明文,bcrypt 加密算法每次加密同樣的內(nèi)容返回的密文是不一樣的,但是對比密文的時候是一樣的,舉例:密碼是123加密后成了a存到了數(shù)據(jù)庫,這時候登錄前端傳的還是123密碼,然后進行加密,加密后的密文會發(fā)現(xiàn)根本不是a,是b,但是a和b兩個密文通過加密算法提供的對比方法,在對比的時候是相等的,這就是這個加密算法的神奇。
2、注冊的時候加密
如下代碼是在注冊賬號的時候使用的,然后通過如下方式對密碼進行加密存放到數(shù)據(jù)庫當中,這樣主體業(yè)務(wù)類從數(shù)據(jù)庫查詢出來的就是加密信息。
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class BCryptTest {
public static void main(String[] args) {
// 創(chuàng)建密碼解析器
BCryptPasswordEncoder bCryptPasswordEncoder = new
BCryptPasswordEncoder();
// 對密碼進行加密
String pwd = bCryptPasswordEncoder.encode("gxs123");
// 打印加密之后的數(shù)據(jù)
System.out.println("加密之后數(shù)據(jù):\t"+pwd);
//判斷原字符加密后和加密之前是否匹配
boolean result = bCryptPasswordEncoder.matches("gxs123", pwd);
// 打印比較結(jié)果
System.out.println("比較結(jié)果:\t"+result);
}
}
運行結(jié)果:
3、認證的時候,加密應(yīng)該如何來用呢?
上面說到了使用BCrypt將密碼加密存到數(shù)據(jù)庫,下面是講登錄時候 密碼如何進行加密 然后和數(shù)據(jù)庫當中的密文進行比較(所謂比較也就是我們所說的真正的認證過程)。
這里最重要的就是WebSecurityConfigurerAdapter接口下的configure方法
,這個方法就是我們要實現(xiàn)的認證邏輯。其實也可以不重寫configure方法,他默認就會去容器里面找PasswordEncoder實現(xiàn)類來作為認證的時候 密碼加密 和數(shù)據(jù)庫比較,以及userDetailsService實現(xiàn)類。
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定義用戶認證邏輯
*/
@Autowired
private UserDetailsService userDetailsService;
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 身份認證接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 告訴SpringSecurity 我們要使用自己定義的userDetailsService來通過username來獲取主體,并且使用了BCryptPasswordEncoder加密進行密碼比較
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}
4、使用BCryptPasswordEncoder加密就徹底安全了嗎?
答案不是,BCrypt只是在數(shù)據(jù)庫層面加密了,那請求層呢?前端直接傳明文安全嗎?所以一般我們會在請求層面使用一個可解密的加密算法,例如對稱加密DES,像我目前的項目就是用的DES對稱加密。
四、SpringSecurity Web 認證方案
4.1、最基礎(chǔ)的認證功能
上面做的入門案例會發(fā)現(xiàn)我們賬號密碼都是通過配置存到內(nèi)存當中的,在實際開發(fā)當中這些肯定是要入庫的。本次案例主要是練習真正的web認證方案。案例持久層用到了mybatis-plus
。
4.1.1、創(chuàng)建項目
引入依賴,這里需要注意的是mybatis-plus在springboot并沒有版本管理,所以我們需要指定mybatis-plus版本,不然就報錯。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--lombok 用來簡化實體類-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
4.1.2、初始化sql
create table users(
id bigint primary key auto_increment,
username varchar(20) unique not null,
password varchar(100)
);
-- 密碼 123456 使用了BCrypt加密
insert into users values(1,'張san','$2a$10$ZglYem2Zs8E4ETbLwaiA4OjXaTZX9w8wJ7x8LZdpGisdtI9VlIfvO');
-- 密碼 123456
insert into users values(2,'李si','$2a$10$ZglYem2Zs8E4ETbLwaiA4OjXaTZX9w8wJ7x8LZdpGisdtI9VlIfvO');
create table role(
id bigint primary key auto_increment,
name varchar(20)
);
insert into role values(1,'管理員');
insert into role values(2,'普通用戶');
create table role_user(
uid bigint,
rid bigint
);
insert into role_user values(1,1);
insert into role_user values(2,2);
create table menu(
id bigint primary key auto_increment,
name varchar(20),
url varchar(100),
parentid bigint,
permission varchar(20)
);
insert into menu values(1,'系統(tǒng)管理','',0,'menu:system');
insert into menu values(2,'用戶管理','',0,'menu:user');
create table role_menu(
mid bigint,
rid bigint
);
insert into role_menu values(1,1);
insert into role_menu values(2,1);
insert into role_menu values(2,2);
4.1.3、添加配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 注入 PasswordEncoder 類到 spring 容器中
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 表單登錄
.defaultSuccessUrl("/index") // 登錄成功之后跳轉(zhuǎn)到哪個 url
.failureForwardUrl("/fail") // 登錄失敗之后跳轉(zhuǎn)到哪個 url
.and()
.authorizeRequests() // 認證配置
.anyRequest() // 任何請求
.authenticated(); // 都需要身份驗證
// 關(guān)閉 csrf
http.csrf().disable();
}
}
4.1.4、添加控制器
提供了兩個接口,一個是登錄成功后跳轉(zhuǎn)的,一個是登錄失敗跳轉(zhuǎn)的。
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class IndexController {
@GetMapping("index")
@ResponseBody
public String index() {
return "success";
}
@PostMapping("fail")
@ResponseBody
public String fail() {
return "fail";
}
}
4.1.5、添加實體類
import lombok.Data;
@Data
public class Users {
private Long id;
private String username;
private String password;
}
4.1.6、添加mapper
在添加之前有一點需要注意,mybatis和mybatis-plus一樣,mapper都需要添加一個掃描注解。
在啟動類添加即可。
@MapperScan("com.gzl.cn.springsecuritywebdemo.mapper")
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gzl.cn.springsecuritywebdemo.entity.Users;
import org.springframework.stereotype.Repository;
@Repository
public interface UsersMapper extends BaseMapper<Users> {
}
4.1.7、添加主體業(yè)務(wù)類
這個類就相當重要了,上面已經(jīng)都講過了,我再簡單絮叨一嘴。
這個類主要做了兩個事:
- 根據(jù)賬號查詢用戶密碼,順便判斷賬戶是否存在。
- 將從數(shù)據(jù)庫查詢出來的賬號密碼,放到User對象當中并返回。
User對象第三個參數(shù)是List,這個list是真正意義上的授權(quán)。在下面案例當中我們會用到,并進行講解,這個案例暫且沒用到。
UserDetailsService接口:主要作用就是返回主體,并且主體當中會攜帶授權(quán)(授權(quán)這個權(quán)可以是菜單權(quán)限,也可以是角色權(quán)限)
。
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.gzl.cn.springsecuritywebdemo.entity.Users;
import com.gzl.cn.springsecuritywebdemo.mapper.UsersMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
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.List;
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
QueryWrapper<Users> wrapper = new QueryWrapper();
wrapper.eq("username", s);
Users users = usersMapper.selectOne(wrapper);
if (users == null) {
throw new UsernameNotFoundException("用戶名不存在!");
}
System.out.println(users);
List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return new User(users.getUsername(), users.getPassword(), auths);
}
}
4.1.8、項目最終結(jié)構(gòu)

4.1.9、測試
http://localhost:8080/login
1、在未登錄前是不能訪問http://localhost:8080/index的。
2、賬號zhangsan 密碼123456,登錄失敗會進入fail請求,成功會進入index請求。
在前后端分離項目當中,一般我們使用SpringSecurity當中不會用到配置請求成功頁面,以及請求失敗頁面,這些都是由前端直接在頁面上控制的。
4.2、基于角色進行訪問控制
通過上面的案例我們發(fā)現(xiàn)他只是做了請求認證,并沒有對權(quán)限進行限制:我們基于上面的案例進行添加權(quán)限限制。
權(quán)限控制一般系統(tǒng)都會分為 角色控制和菜單控制
,菜單控制我們又分為了頁面菜單和按鈕控制
。所謂按鈕級別控制,也就是指定Java某個接口必須具備什么角色,或者具備按鈕權(quán)限,才可以訪問。
在一些比較早的項目當中沒用到SpringSecurity,他們一般是這么做的:
登錄的時候前端訪問后端,拿到這個用戶有哪些權(quán)限,這個權(quán)限包含了菜單權(quán)限,以及按鈕權(quán)限,當不具備某個按鈕權(quán)限的時候,前端直接屏蔽掉。然后攔截器只有一層攔截,就是只針對登錄成功的人就能訪問所有接口,對于普通人來說可能你確實做了這個功能,但是對于程序員來說那只是一個假象。一旦我們登錄成功,我們知道接口名稱,便可以通過接口直接訪問。
SpringSecurity早就已經(jīng)都想到了,我們只需要通過簡單的配置即可避免這樣的問題。
4.2.1、添加實體類
import lombok.Data;
@Data
public class Menu {
private Long id;
private String name;
private String url;
private Long parentId;
private String permission;
}
import lombok.Data;
@Data
public class Role {
private Long id;
private String name;
}
4.2.2、編寫接口與實現(xiàn)類
import com.gzl.cn.springsecuritywebdemo.entity.Menu;
import com.gzl.cn.springsecuritywebdemo.entity.Role;
import java.util.List;
public interface UserInfoMapper {
/**
* 根據(jù)用戶 Id 查詢用戶角色
*
* @param userId
* @return
*/
List<Role> selectRoleByUserId(Long userId);
/**
* 根據(jù)用戶 Id 查詢菜單
*
* @param userId
* @return
*/
List<Menu> selectMenuByUserId(Long userId);
}
需要在 resource/mapper 目錄下自定義 UserInfoMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gzl.cn.springsecuritywebdemo.mapper.UserInfoMapper">
<!--根據(jù)用戶 Id 查詢角色信息-->
<select id="selectRoleByUserId" resultType="com.gzl.cn.springsecuritywebdemo.entity.Role">
SELECT
r.id,
r.NAME
FROM
role r
INNER JOIN role_user ru ON ru.rid = r.id
WHERE
ru.uid = #{0}
</select>
<!--根據(jù)用戶 Id 查詢權(quán)限信息-->
<select id="selectMenuByUserId" resultType="com.gzl.cn.springsecuritywebdemo.entity.Menu">
SELECT
m.id,
m.NAME,
m.url,
m.parentid,
m.permission
FROM
menu m
INNER JOIN role_menu rm ON m.id = rm.mid
INNER JOIN role r ON r.id = rm.rid
INNER JOIN role_user ru ON r.id = ru.rid
WHERE
ru.uid = #{0}
</select>
</mapper>
4.2.3、調(diào)整MyUserDetailsService
這里最主要的改動是,從數(shù)據(jù)庫查詢出來角色和菜單數(shù)據(jù)存到了List<GrantedAuthority>
集合當中,并且放到了user對象屬性當中返回。
然后我們在訪問某個接口的時候,SpringSecurity會去拿返回的主體信息對比,是否具備該接口的權(quán)限,或者是是否具備該角色。
注意:
在實際開發(fā)當中,我們可能涉及不到某些接口必須用哪個角色才能訪問的場景,而只是利用角色來分配菜單,然后給用戶再分配角色。
如果要是這樣的話,我們只需要根據(jù)用戶id來關(guān)聯(lián)查詢角色表,再根據(jù)擁有的角色查詢出來所擁有的菜單權(quán)限即可。就不需要像下面一樣,還查詢出來角色,把角色也放到了List當中。
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.gzl.cn.springsecuritywebdemo.entity.Menu;
import com.gzl.cn.springsecuritywebdemo.entity.Role;
import com.gzl.cn.springsecuritywebdemo.entity.Users;
import com.gzl.cn.springsecuritywebdemo.mapper.UserInfoMapper;
import com.gzl.cn.springsecuritywebdemo.mapper.UsersMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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.List;
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;
@Autowired
private UserInfoMapper userInfoMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
QueryWrapper<Users> wrapper = new QueryWrapper();
wrapper.eq("username", s);
Users users = usersMapper.selectOne(wrapper);
if (users == null) {
throw new UsernameNotFoundException("用戶名不存在!");
}
// 獲取用戶角色、菜單列表
List<Role> roles = userInfoMapper.selectRoleByUserId(users.getId());
List<Menu> menus = userInfoMapper.selectMenuByUserId(users.getId());
// 聲明一個集合List<GrantedAuthority>
List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
// 處理角色
for (Role role:roles){
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_" + role.getName());
grantedAuthorityList.add(simpleGrantedAuthority);
}
// 處理權(quán)限
for (Menu menu:menus){
grantedAuthorityList.add(new SimpleGrantedAuthority(menu.getPermission()));
}
return new User(users.getUsername(), users.getPassword(), grantedAuthorityList);
}
}
注意:這里拼接的ROLE_不可以去掉,去掉會發(fā)現(xiàn),直接失去了這個角色的權(quán)限,原因下面會講。
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_" + role.getName());
4.2.4、添加接口
添加如下兩個接口來測試權(quán)限控制。
@GetMapping("findAll")
@ResponseBody
public String findAll() {
return "findAll";
}
@GetMapping("find")
@ResponseBody
public String find() {
return "find";
}
4.2.5、修改訪問配置類
這塊相當于是我制作規(guī)定,他必須擁有某個角色,或者某個接口的許可,才可以進行訪問這個接口。
這塊的許可就是數(shù)據(jù)庫當中menu菜單當中的permission字段,如果是按鈕級別控制的話,那每個接口都應(yīng)該有一個唯一的permission許可。
// 需要用戶帶有管理員角色才可以訪問/findAll接口
.antMatchers("/findAll").hasRole("管理員")
// 需要用戶具備menu:user這個接口的許可,才可以訪問
.antMatchers("/find").hasAuthority("menu:user")
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 注入 PasswordEncoder 類到 spring 容器中
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表單登錄
http.formLogin()
// 登錄成功之后跳轉(zhuǎn)到哪個 url
.defaultSuccessUrl("/index").permitAll()
// 登錄失敗之后跳轉(zhuǎn)到哪個 url
.failureForwardUrl("/fail").permitAll();
// 身份驗證
http.authorizeRequests()
// 需要用戶帶有管理員權(quán)限
.antMatchers("/findAll").hasRole("管理員")
.antMatchers("/find").hasRole("管理員")
// 需要用戶具備這個接口的權(quán)限
.antMatchers("/find").hasAuthority("menu:user")
// 任何請求都需要認證
.anyRequest().authenticated();
// 關(guān)閉 csrf
http.csrf().disable();
}
}
hasRole方法底層源碼:會發(fā)現(xiàn)他會給我們默認添加一個ROLE_,這也就是我們在上面授權(quán)的時候需要加上ROLE_的原因。
4.2.6、測試
http://localhost:8080/login
1、在未登錄前是不能訪問http://localhost:8080/index的。
2、賬號zhangsan 密碼123456,登錄失敗會進入fail請求,成功會進入index請求。
3、使用賬號:張san 登錄之后findAll接口和find接口是都可以訪問的,因為在數(shù)據(jù)庫當中不管是role還是menu,他具備所有權(quán)限。
4、使用賬號:李si 登錄之后findAll接口他是訪問不了的,直接報403,原因:因為他沒有管理員權(quán)限,我限制了findAll只有管理員能訪問。
5、使用賬號:李si 登錄之后find接口可以訪問,雖然我限制了find接口必須具備管理員權(quán)限才能訪問,但是我還設(shè)置了只要具有menu:user菜單許可即可訪問。也就是兩個判斷條件我滿足了一個就能訪問。
4.3、自定義登錄頁面
接上面案例,我們自定義一個登錄頁面。
4.3.1、添加登錄頁面
這個位置是固定的,假如放到別的目錄是訪問不到的。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/user/login" method="post">
用戶名:<input type="text" name="username">
<br>
密碼:<input type="text" name="password">
<br>
<input type="submit" value="login"/>
</form>
</body>
</html>
4.3.2、修改配置
添加如下配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表單登錄
http.formLogin()
// 修改默認的登錄頁為login.html,他會自動去根路徑static文件夾下尋找login.html
.loginPage("/login.html")
// 設(shè)置登錄接口地址,這個接口不是真實存在的,還是用的security給我們提供的,之所以要有這個配置,是login.html當中form表單提交的地址我們設(shè)置的是這個
.loginProcessingUrl("/user/login")
// 登錄成功之后跳轉(zhuǎn)到哪個 url
.defaultSuccessUrl("/index")
// 登錄失敗之后跳轉(zhuǎn)到哪個 url
.failureForwardUrl("/fail")
// permitAll中文意思是許可所有的:所有的都遵循上面的配置的意思
.permitAll();
// 都需要身份驗證
http.authorizeRequests()
// 該路由不需要身份認證
.antMatchers("/user/login", "/login.html").permitAll()
// 需要用戶帶有管理員權(quán)限
.antMatchers("/findAll").hasRole("管理員")
.antMatchers("/find").hasRole("管理員")
// 需要用戶具備這個接口的權(quán)限
.antMatchers("/find").hasAuthority("menu:user")
// 任何請求都需要認證
.anyRequest().authenticated();
// 關(guān)閉 csrf
http.csrf().disable();
}
4.3.3、測試

4.4、自定義403頁面
接上面案例,我們自定義一個403頁面。
4.4.1、添加403頁面

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>沒有權(quán)限</title>
</head>
<body>
<h1>沒有權(quán)限</h1>
</body>
</html>
4.4.2、添加配置
// 設(shè)置沒有權(quán)限訪問跳轉(zhuǎn)自定義頁面
http.exceptionHandling().accessDeniedPage("/unauth.html");
4.4.3、測試
李si 登錄之后findAll接口他是訪問不了的,直接報403,原因:因為他沒有管理員權(quán)限,我限制了findAll只有管理員能訪問。

4.5、用戶注銷
4.5.1、添加登錄首頁

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
登錄成功<br>
<a href="/logout">退出</a>
</body>
</html>
4.5.2、添加配置
// 退出,這里的/logout的請求是和前端的接口約定,是security給我們提供的,退出成功后跳轉(zhuǎn)登錄頁/login.html
http.logout().logoutUrl("/logout").logoutSuccessUrl("/login.html").permitAll();
4.5.3、測試
登錄成功之后,在成功頁面點擊退出再去訪問其他controller不能進行訪問的
4.6、基于數(shù)據(jù)庫的自動登錄
自動登錄也可以叫做記住我,正常情況下我們登錄后 關(guān)閉所有的網(wǎng)頁,這時候就需要重新登錄,假如我們沒有關(guān)閉瀏覽器,但是服務(wù)升級發(fā)生了重啟,重啟過后也是需要重新登錄的,為了解決這兩個問題,我們需要將這些實例化到db當中。
4.6.1、創(chuàng)建表
CREATE TABLE `persistent_logins` (
`username` VARCHAR ( 64 ) NOT NULL,
`series` VARCHAR ( 64 ) NOT NULL,
`token` VARCHAR ( 64 ) NOT NULL,
`last_used` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY ( `series` )
) ENGINE = INNODB DEFAULT CHARSET = utf8;
4.6.2、編寫配置類
@Autowired
private DataSource dataSource;
@Autowired
private MyUserDetailsService myUserDetailsService;
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}
// 注入 PasswordEncoder 類到 spring 容器中
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 設(shè)置記住我
http.rememberMe()
.tokenRepository(persistentTokenRepository())
// 設(shè)置有效時長180秒,默認 2 周時間。
.tokenValiditySeconds(180)
.userDetailsService(myUserDetailsService);
4.6.3、登錄頁面添加記住我復(fù)選框
此處:name 屬性值必須位 remember-me.不能改為其他值
<input type="checkbox" name="remember-me">自動登錄
4.6.4、測試
使用張san賬號,登錄成功之后,關(guān)閉瀏覽器再次訪問 http://localhost:8090/findAll,發(fā)現(xiàn)依然可以使用!
4.6.5、原理
流程如下:
在用戶發(fā)送認證請求之后,或調(diào)用我們之前說過的usernamePasswordAuthenticationFilter這個過濾器,認證成功之后會調(diào)用一個服務(wù)負責針對每一用戶生成一個Token,然后將token寫入Cookie與數(shù)據(jù)庫中
。當用戶再次請求的時候會經(jīng)過過濾器鏈中的RemeberMeAuthenticationFilter,通過名字我們也知道這個了過濾器的作用便是讀取Cookie中的Token,然后Service會到數(shù)據(jù)庫里查Token是否有記錄,如果有記錄會調(diào)用UserDetailsService然后根據(jù)用戶名與密碼進行認證。
1.登錄成功會訪問rememberMeServices的loginSuccess方法。
2.rememberMeServices的實現(xiàn)類loginSuccess實際上是調(diào)用的AbstractRememberMeServices類的loginSuccess方法。而loginSuccess方法會調(diào)用onLoginSuccess方法。
3.onLoginSuccess實際是個抽象方法,也就是得需要繼續(xù)尋找他的實現(xiàn)方法,如下:這個方法的作用就是生成token然后放到cookie當中。
4. 上面的createNewToken方法不僅僅是創(chuàng)建一個token,他還將token保存到了數(shù)據(jù)庫??吹竭@里應(yīng)該就明白了為什么上面要將PersistentTokenRepository注入到容器當中,原因就是這塊用到了。
JdbcTokenRepositoryImpl是PersistentTokenRepository的實現(xiàn)類。
5. 上面是存儲數(shù)據(jù)的過程,下面講自動認證的過程。訪問請求會經(jīng)過這個過濾器:當他發(fā)現(xiàn)沒有登錄主體信息的時候會訪問autoLogin方法。
6. 自動登錄認證過程
五、注解使用
注意:這里的每一個講解,還是基于上面的案例進行講解的。
通過上面案例不難發(fā)現(xiàn),我想控制一個接口只有某個角色可以訪問 或者是 具備這個接口的訪問權(quán)限才可以訪問,還需要在配置文件配置一下。
// 需要用戶帶有管理員角色才可以訪問/findAll接口
.antMatchers("/findAll").hasRole("管理員")
假如針對性的接口越來越多,那配置文件會變得越來越臃腫,SpringSecurity也是考慮到了這一點,給我們提供了一些專門控制權(quán)限的注解,這樣我們就可以在方法或者類上添加個注解就可以完全可以替代掉在配置文件配置。
在測試使用注解之前,我們需要先把在配置文件配置的控制給去掉。
5.1、@Secured
判斷是否具有角色,另外需要注意的是這里匹配的字符串需要添加前綴“ROLE_“
。
使用該注解 需要先開啟該注解!如下:在啟動類添加即可。
@EnableGlobalMethodSecurity(securedEnabled=true)
在控制器方法上添加注解:這樣就代表管理員和普通用戶角色都可以訪問這個控制器的接口。
當然也可以在方法上加,只控制某個方法。
5.2、@PreAuthorize
這個注解在開發(fā)當中經(jīng)常會遇到!
使用注解先要開啟注解功能!在啟動類添加即可。
@EnableGlobalMethodSecurity(prePostEnabled = true)
@PreAuthorize:注解適合進入方法前的權(quán)限驗證, 可以將登錄用戶的 roles/permissions 參數(shù)傳到方法中。
hasAnyAuthority方法實際上就是從當前登錄者的User對象當中獲取
Set<GrantedAuthority> authorities
屬性,然后判斷set列表當中是否有該權(quán)限。這個注解當中的方法是可以自己定義的
。
@RequestMapping("/find")
@ResponseBody
//@PreAuthorize("hasRole('ROLE_管理員')")
@PreAuthorize("hasAnyAuthority('menu:user')")
public String preAuthorize(){
System.out.println("preAuthorize");
return "preAuthorize";
}
@PreAuthorize功能說白了就是可以替換以下這段代碼:
.antMatchers("/find").hasAuthority("menu:user")
5.3、@PostAuthorize
先開啟注解功能:
@EnableGlobalMethodSecurity(prePostEnabled = true)
@PostAuthorize 注解使用并不多,在方法執(zhí)行后再進行權(quán)限驗證,適合驗證帶有返回值
的權(quán)限。
拿李si登錄進行驗證,李si是沒有menu:system這個權(quán)限的。登錄之后會發(fā)現(xiàn)實際是403,但是system卻輸出值了,代表方法被執(zhí)行了,但是沒有權(quán)限導(dǎo)致沒return返回。
5.4、@PostFilter
@PostFilter :權(quán)限驗證之后對數(shù)據(jù)進行過濾 留下用戶名是 admin1 的數(shù)據(jù)
表達式中的 filterObject 引用的是方法返回值 List 中的某一個元素
@RequestMapping("getAll")
@PreAuthorize("hasRole('ROLE_管理員')")
@PostFilter("filterObject.username == 'admin1'")
@ResponseBody
public List<UserInfo> getAllUser(){
ArrayList<UserInfo> list = new ArrayList<>();
list.add(new UserInfo(1l,"admin1","6666"));
list.add(new UserInfo(2l,"admin2","888"));
return list;
}
5.5、@PreFilter
@PreFilter: 進入控制器之前對數(shù)據(jù)進行過濾
以下示例:過濾掉參數(shù)list當中對象id屬性除2=0的。
@PreFilter:進入控制器之前對數(shù)據(jù)進行過濾
@RequestMapping("getTestPreFilter")
@PreAuthorize("hasRole('ROLE_管理員')")
@PreFilter(value = "filterObject.id%2==0")
@ResponseBody
public List<UserInfo> getTestPreFilter(@RequestBody List<UserInfo>
list) {
list.forEach(t -> {
System.out.println(t.getId() + "\t" + t.getUsername());
});
return list;
}
六、常用配置
@Override
protected void configure(HttpSecurity http) throws Exception {
// 設(shè)置沒有權(quán)限訪問跳轉(zhuǎn)自定義頁面
http.exceptionHandling().accessDeniedPage("/unauth.html");
// 退出,這里的/logout的請求是和前端的接口約定,是security給我們提供的,退出成功后跳轉(zhuǎn)登錄頁/login.html
http.logout().logoutUrl("/logout").logoutSuccessUrl("/login.html").permitAll();
// 設(shè)置記住我
http.rememberMe()
.tokenRepository(persistentTokenRepository())
// 設(shè)置有效時長180秒
.tokenValiditySeconds(180)
.userDetailsService(myUserDetailsService);
// 表單登錄相關(guān)的配置
http.formLogin()
// 前端登錄表單用戶名別名, 從參數(shù)user中獲取username參數(shù)取值
.usernameParameter("user")
// 前端登錄表單密碼別名, 從參數(shù)passwd中獲取password參數(shù)取值
.passwordParameter("passwd")
// 當http請求的url是/login時,進行我們自定義的登錄邏輯
.loginProcessingUrl("/login")
// 自定義登錄的前端控制器
.loginPage("/showLogin")
// 登錄成功之后跳轉(zhuǎn)到哪個 url
.defaultSuccessUrl("/index")
// 登錄失敗之后跳轉(zhuǎn)到哪個 url
.failureForwardUrl("/fail")
// 所有的都許可,就是遵循上面的配置的意思
.permitAll()
// 設(shè)置登錄成功的跳轉(zhuǎn)鏈接
// .successForwardUrl("/home");
// 通過successHandler處理器進行登錄成功之后的邏輯處理
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("登錄成功,頁面即將跳轉(zhuǎn)...");
response.sendRedirect("/home");
}
})
// 設(shè)置登錄失敗的跳轉(zhuǎn)鏈接
// .failureForwardUrl("/errPage");
// 通過failureHandler處理器進行登錄失敗之后的邏輯處理
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
e.printStackTrace();
System.out.println("登錄失敗,頁面即將跳轉(zhuǎn)到默認失敗頁...");
response.sendRedirect("/errPage");
}
});
/**
* http請求是否要登錄認證配置
*/
http.authorizeRequests()
// 允許登錄頁面匿名訪問
.antMatchers("/showLogin", "/errPage").anonymous()
// 所有的靜態(tài)資源允許匿名訪問
.antMatchers(
"/css/**",
"/js/**",
"/images/**",
"/fonts/**",
"/favicon.ico"
).anonymous()
.antMatchers(
"/**/*.js",
"/profile/**"
).permitAll()
// 需要用戶帶有管理員權(quán)限
.antMatchers("/findAll").hasRole("管理員")
// 需要用戶帶有管理員權(quán)限或者平臺維護管理員任意一個角色即可訪問
.antMatchers("/findAll").hasAnyRole("管理員,平臺維護管理員")
// 需要用戶具備這個接口的權(quán)限
.antMatchers("/find").hasAuthority("menu:user")
// 需要用戶具備menu:user或者menu:user1任意一個即可訪問
.antMatchers("/find").hasAnyAuthority("menu:user,menu:user1")
// 除上面外的所有請求全部需要鑒權(quán)認證
.anyRequest().authenticated();
// 關(guān)閉 csrf,默認是開啟的
http.csrf().disable();
}
1、authorizeRequests() 配置路徑攔截,表明路徑訪問所對應(yīng)的權(quán)限,角色,認證信息。
2、formLogin()對應(yīng)表單認證相關(guān)的配置
這兩個之間可以用and進行連接,也可以分開寫。
antMatchers().anonymous() 、antMatchers().permitAll() 區(qū)別?
-
anonymous()
:匿名訪問,僅允許匿名用戶訪問,如果登錄認證后,帶有token信息再去請求,這個anonymous()關(guān)聯(lián)的資源就不能被訪問(就相當于登陸之后不允許訪問,只允許匿名的用戶) -
permitAll()
登錄能訪問,不登錄也能訪問,一般用于靜態(tài)資源js等
七、總結(jié)
總的來說這一篇只能算是入門篇,SpringSecurity內(nèi)容太多了,本篇文章已經(jīng)篇幅很長了,所以計劃把剩余部分再整理幾篇文章。項目當中使用SpringSecurity的其實還是很多的,最起碼自我接手的項目基本都用這個來做認證和授權(quán)。
通過這篇文章能掌握以下知識點:文章來源:http://www.zghlxwxcb.cn/news/detail-673594.html
- SpringSecurity配置從哪里找(項目當中繼承
WebSecurityConfigurerAdapter
的類) - SpringSecurity 認證流程:前端傳username和password,然后后端會先去通過 username來查詢主體,查詢出來主體之后,再根據(jù)配置的加密方式,將前端傳的密碼進行加密,然后和數(shù)據(jù)庫當中的密碼進行比較。
- SpringSecurity 加密怎么用
- SpringSecurity 認證配置:
WebSecurityConfigurerAdapter
配置類當中,重寫configure
方法是可以顯示的去配置加密方式的,還有返回主體的類,如果不配置的話,默認就會去容器當中找PasswordEncoder
的實現(xiàn)類來作為加密方式,還有UserDetailsService
的實現(xiàn)類來作為返回主體類 - SpringSecurity 提供的注解
- 當我們接手SpringSecurity項目的時候,首先要確認項目是否是按鈕級別控制:如果是的話,看看他是通過注解來控制的,還是在配置文件當中控制的。了解了這些當我們要新增接口的時候就會避免一些尷尬。當要新增接口的時候,如果沒配置 然后還想測試的接口的話,可以先在WebSecurityConfigurerAdapter配置類當中過濾掉接口認證,方便來做本地調(diào)試。過濾的時候在
antMatchers("接口").permitAll()
過濾即可。
當項目使用的是SpringSecurity的時候,我們有了以上這些知識掌握,最起碼不用慌了,因為基本的流程我們掌握了。文章來源地址http://www.zghlxwxcb.cn/news/detail-673594.html
到了這里,關(guān)于學(xué)習SpringSecurity這一篇就夠了的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!