目錄
關(guān)于Spring Security框架
Spring Security框架的依賴項(xiàng)
Spring Security框架的典型特征
?關(guān)于Spring Security的配置
關(guān)于默認(rèn)的登錄頁
關(guān)于請(qǐng)求的授權(quán)訪問(訪問控制)
?使用自定義的賬號(hào)登錄
使用數(shù)據(jù)庫中的賬號(hào)登錄
關(guān)于密碼編碼器
使用BCrypt算法
關(guān)于偽造的跨域攻擊
使用前后端分離的登錄
關(guān)于認(rèn)證的標(biāo)準(zhǔn)
未通過認(rèn)證時(shí)拒絕訪問
識(shí)別當(dāng)事人(Principal)
實(shí)現(xiàn)根據(jù)權(quán)限限制訪問
補(bǔ)充解釋(關(guān)于使用resultMap標(biāo)簽):
基于方法的權(quán)限檢查
添加Token?
首先添加Token-JWT的依賴項(xiàng):
生成JWT:
?解析JWT
補(bǔ)充:
在項(xiàng)目中使用JWT識(shí)別用戶的身份
核心流程
驗(yàn)證登錄成功時(shí)響應(yīng)JWT
解析客戶端攜帶的JWT
?我們這次選擇去繼承Spring系列框架提供的OncePerRequestFilter這個(gè)類。
關(guān)于認(rèn)證信息中的當(dāng)事人
處理解析JWT時(shí)的異常
處理復(fù)雜請(qǐng)求的跨域問題
單點(diǎn)登錄
?文章來源地址http://www.zghlxwxcb.cn/news/detail-480051.html
關(guān)于Spring Security框架
Spring Security框架主要解決了認(rèn)證與授權(quán)相關(guān)的問題。 ?
認(rèn)證信息(Authentication):表示用戶的身份信息
認(rèn)證(Authenticate):識(shí)別用戶的身份信息的行為,例如:登錄
授權(quán)(Authorize):授予用戶權(quán)限,使之可以進(jìn)行某些訪問,反之,如果用戶沒有得到必要的授權(quán),將無法進(jìn)行訪問
Spring Security框架的依賴項(xiàng)
在Spring Boot中使用Spring Security時(shí)需要添加spring-boot-starter-security
依賴。 ?
Spring Security框架的典型特征
?當(dāng)添加了spring-boot-starter-security
依賴后,在啟動(dòng)項(xiàng)目時(shí)執(zhí)行一些自動(dòng)配置,具體表現(xiàn)有:
-
所有請(qǐng)求(包括根本不存在的)都是必須要登錄才允許訪問的,如果未登錄,會(huì)自動(dòng)跳轉(zhuǎn)到框架自帶的登錄頁面(1.項(xiàng)目重啟之后需要重新登錄,2.原來想去的頁面會(huì)要求登錄,登錄完成之后回到原來的位置)
- 當(dāng)嘗試登錄時(shí),如果在打開登錄頁面后重啟過服務(wù)器端,則第1次的輸入是無效的 ?
-
默認(rèn)的用戶名是
user
,密碼是在啟動(dòng)項(xiàng)目是控制臺(tái)提示的一段UUID值,每次啟動(dòng)項(xiàng)目時(shí)都不同(同一時(shí)空的唯一性,即同一時(shí)間同一空間的值都不同)-
UUID是通過128位算法(運(yùn)算結(jié)果是128個(gè)bit)運(yùn)算得到的,是一個(gè)隨機(jī)數(shù),在同一時(shí)空是唯一的,通常使用32個(gè)十六進(jìn)制數(shù)來表示,每種平臺(tái)生成UUID的API和表現(xiàn)可能不同,UUID值的種類有2的128次方個(gè),即:3.4028237e+38,也就是340282366920938463463374607431768211456
-
-
當(dāng)?shù)卿洺晒?,?huì)自動(dòng)跳轉(zhuǎn)到此前嘗試訪問的URL
-
當(dāng)?shù)卿洺晒?,可以通過
/logout
退出登錄
-
默認(rèn)不接受普通
POST
請(qǐng)求,如果提交POST
請(qǐng)求,將響應(yīng)403(Forbidden)
?關(guān)于Spring Security的配置
?在項(xiàng)目的根包下創(chuàng)建config.SecurityConfiguration
類,作為Spring Security的配置類,此類需要繼承自WebSecurityConfigurerAdapter
,并重寫void configure(HttpSecurity http)
方法,例如:
@Slf4j
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http); // 不要保留調(diào)用父級(jí)同名方法的代碼,不要保留!不要保留!不要保留!
}
}
做了配置后,此時(shí)重啟工程就不需要登陸了,就算訪問登錄頁面也沒有。
?
?
?寫此配置是為了調(diào)整Spring Security框架的特征的所有表現(xiàn)由自己來設(shè)置。
關(guān)于默認(rèn)的登錄頁
在自定義的配置類中的void configure(HttpSecurity http)
方法中,調(diào)用參數(shù)對(duì)象的formLogin()
方法即可開啟默認(rèn)的登錄表單,如果沒有調(diào)用此方法,則不會(huì)應(yīng)用默認(rèn)的登錄表單,例如:
@Slf4j
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http); // 不要保留調(diào)用父級(jí)同名方法的代碼,不要保留!不要保留!不要保留!
// 如果調(diào)用以下方法,當(dāng)Security認(rèn)為需要通過認(rèn)證,但實(shí)際未通過認(rèn)證時(shí),就會(huì)跳轉(zhuǎn)到登錄頁面
// 如果未調(diào)用以下方法,將會(huì)響應(yīng)403錯(cuò)誤
http.formLogin();
}
}
關(guān)于請(qǐng)求的授權(quán)訪問(訪問控制)
在剛剛添加spring-boot-starter-security
時(shí),所有請(qǐng)求都是需要登錄后才允許訪問的,當(dāng)添加了自定義的配置類且沒有調(diào)用父級(jí)同名方法后,所有請(qǐng)求都是不需要登錄就可以訪問的!
為了實(shí)現(xiàn)一部分需要登錄,一部分不需要登錄就需要做配置類,不然如果是自己做了一個(gè)登錄頁面,訪問登錄頁面還需要登錄就不合適。?
在配置類中的void configure(HttpSecurity http)
方法中,調(diào)用參數(shù)對(duì)象的authorizeRequests()
方法開始配置授權(quán)訪問:
@Override
protected void configure(HttpSecurity http) throws Exception {
// 白名單
// 使用1個(gè)星號(hào),可以通配此層級(jí)的任何資源,例如:/admin/*,可以匹配:/admin/add-new、/admin/list,但不可以匹配:/admin/password/change
// 使用2個(gè)連續(xù)的星可以,可以通配若干層級(jí)的資源,例如:/admin/**,可以匹配:/admin/add-new、/admin/password/change
String[] urls = {
"/doc.html",
"/**/*.css",
"/**/*.js",
"/swagger-resources",
"/v2/api-docs",
};
// 配置授權(quán)訪問
// 注意:以下授權(quán)訪問的配置,是遵循“第一匹配原則”的,即“以最先匹配到的規(guī)則為準(zhǔn)”
// 例如:anyRequest()是匹配任何請(qǐng)求,通常,應(yīng)該配置在最后,表示“除了以上配置過的以外的所有請(qǐng)求”
// 所以,在開發(fā)實(shí)踐中,應(yīng)該將更具體的請(qǐng)求配置在靠前的位置,將更籠統(tǒng)的請(qǐng)求配置在靠后的位置
http.authorizeRequests() // 開始對(duì)請(qǐng)求進(jìn)行授權(quán)
.mvcMatchers(urls) // 匹配某些請(qǐng)求
.permitAll() // 許可,即不需要通過認(rèn)證就可以訪問
.anyRequest() // 任何請(qǐng)求
.authenticated() // 要求已經(jīng)完成認(rèn)證的
;
}
?http.authorizeRequests() // 開始對(duì)請(qǐng)求進(jìn)行授權(quán)?
表示開始對(duì)請(qǐng)求進(jìn)行授權(quán) 。
??.anyRequest() // 任何請(qǐng)求
??.authenticated() // 要求已經(jīng)完成認(rèn)證的
上面兩句需要連起來理解,表示任何請(qǐng)求都要求是已經(jīng)完成認(rèn)證的。加上這兩句就回到了最開始的樣子,所有的請(qǐng)求都需要登錄才能訪問,不登錄訪問不了。
?.mvcMatchers(urls) // 匹配某些請(qǐng)求
?.permitAll() // 許可,即不需要通過認(rèn)證就可以訪問?
這兩句話也是連起來理解的, 理解同上面一樣,上面是任何請(qǐng)求,這里是匹配某些請(qǐng)求,上面的行為是所有都要求認(rèn)證,這里的行為是許可訪問,不需要通過認(rèn)證。(因?yàn)樽裱暗谝黄ヅ湓瓌t”的,即“以最先匹配到的規(guī)則為準(zhǔn)”,所有這兩行代碼要放在最上面才有效)
這里的urls是怎么來的:
首先為了方便頁面正確顯示,勾上禁用緩存。? ?
看下面錯(cuò)誤提示,看到有一堆的200都是login的,為了更好的看到提示信息,把http.formLogin()關(guān)掉。
? ?可以看到大量的403
?從中我們對(duì)這些403進(jìn)行許可(案例訪問的API文檔,給文檔需要的資源進(jìn)行許可,就可以順利訪問API文檔了),urls就是這么來的。
注意:有的比如表面是說的api-docs這樣一個(gè)名字,實(shí)際在配白名單的時(shí)候,看它的url是在一個(gè)v2的文件夾里面,配置為?"/v2/api-docs"。
?
?使用自定義的賬號(hào)登錄
在使用Spring Security框架時(shí),可以自定義組件類,實(shí)現(xiàn)UserDetailsService
接口,則Spring Security就會(huì)基于此類的對(duì)象來處理認(rèn)證!
則在項(xiàng)目的根包下創(chuàng)建security.UserDetailsServiceImpl
,在類上添加@Service
注解使其成為組件類,實(shí)現(xiàn)UserDetailsService
接口:
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
return null;
}
}
(通過loadUserByUsername(String s)這個(gè)方法的名字可以理解為通過用戶名加載用戶,參數(shù)s就是username用戶名,返回UserDetails用戶詳情)
在項(xiàng)目中存在UserDetailsService
接口類型的組件對(duì)象時(shí),嘗試登錄時(shí),Spring Security就會(huì)自動(dòng)使用登錄表單中輸入的用戶名來調(diào)用以上方法, 把輸入的用戶名作為一個(gè)參數(shù),?并得到方法返回的UserDetails
類型的結(jié)果,?此結(jié)果中應(yīng)該包含用戶的相關(guān)信息,例如密碼、賬號(hào)狀態(tài)、權(quán)限等等,接下來,Spring Security框架會(huì)自動(dòng)判斷賬號(hào)的狀態(tài)(例如是否啟用或禁用)、驗(yàn)證密碼(在UserDetails
中的密碼與登錄表單中的密碼是否匹配)等,從而決定此次是否登錄成功!
所以,對(duì)于開發(fā)者而言,在以上方法中只需要完成“根據(jù)用戶名返回匹配的用戶詳情”即可!例如:?
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
log.debug("用戶名:{}", s);
// 假設(shè)正確的用戶名是root,匹配的密碼是1234
if (!"root".equals(s)) {
log.debug("此用戶名沒有匹配的用戶數(shù)據(jù),將返回null");
return null;
}
log.debug("用戶名匹配成功!準(zhǔn)備返回此用戶名匹配的UserDetails類型的對(duì)象");
UserDetails userDetails = User.builder()
.username(s)
.password("1234")
.disabled(false) // 賬號(hào)狀態(tài)是否禁用
.accountLocked(false) // 賬號(hào)狀態(tài)是否鎖定
.accountExpired(false) // 賬號(hào)狀態(tài)是否過期
.credentialsExpired(false) // 賬號(hào)的憑證是否過期
.authorities("這是一個(gè)臨時(shí)使用的山寨的權(quán)限?。?!") // 權(quán)限
.build();
log.debug("即將向Spring Security返回UserDetails類型的對(duì)象:{}", userDetails);
return userDetails;
}
}
?以上代碼中,用User.builder()開啟它的構(gòu)建者模式,?.build()表示構(gòu)建完了。這是一個(gè)鏈?zhǔn)綄懛?,先有個(gè)builder()在執(zhí)行?.build()就可以創(chuàng)建這個(gè)對(duì)象。創(chuàng)建的過程中就傳入例如密碼、賬號(hào)狀態(tài)、權(quán)限等相關(guān)信息。
當(dāng)項(xiàng)目中存在UserDetailsService
類型的對(duì)象后,啟動(dòng)項(xiàng)目時(shí),控制臺(tái)不會(huì)再提示臨時(shí)使用的UUID密碼!并且,user
賬號(hào)也不可用! 用的就是自己配的??.username(s)? .password("1234")這個(gè)。
另外,Spring Security框架認(rèn)為所有的密碼都是必須顯式的經(jīng)過某種算法處理過的,如果使用的密碼是明文(原始密碼例如1234這種),也必須明確的指出!例如,使用沒加密的原始密碼在Security的配置類中添加配置NoOpPasswordEncoder
這種密碼編碼器告訴Security是沒有加密的,不然會(huì)報(bào)錯(cuò): ?
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
?此時(shí)嘗試登錄,輸入用戶名root,密碼1234,登錄成功,輸入錯(cuò)誤的用戶名提示以下為null,這是因?yàn)檫@個(gè)null是在用戶名不對(duì)的時(shí)候我們給它的。
?如果用戶名是輸入的root,密碼故意輸出會(huì)提示:
?如果把禁用打開,輸入正確的用戶名密碼也會(huì)顯示用戶已失效:
使用數(shù)據(jù)庫中的賬號(hào)登錄
需要將UserDetailsServiceImpl
中的實(shí)現(xiàn)改為“根據(jù)用戶名查詢數(shù)據(jù)庫中的用戶信息”!需要執(zhí)行的SQL語句大致是:
select username, password, enable from ams_admin where username=?
?在pojo.vo.AdminLoginInfoVO
類:
@Data
@Accessors(chain = true)
public class AdminLoginInfoVO implements Serializable {
private String username;
private String password;
private Integer enable;
}
?在AdminMapper
接口中添加抽象方法:
AdminLoginInfoVO getLoginInfoByUsername(String username);
?在AdminMapper.xml
中配置SQL:
<!-- AdminLoginInfoVO getLoginInfoByUsername(String username); -->
<select id="getLoginInfoByUsername" resultType="cn.tedu.csmall.passport.pojo.vo.AdminLoginInfoVO">
SELECT
username, password, enable
FROM
ams_admin
WHERE
username=#{username}
</select>
?在AdminMapperTests
中編寫并執(zhí)行測試:
@Test
void getStandardById() {
String username = "root";
Object queryResult = mapper.getLoginInfoByUsername(username);
System.out.println("根據(jù)【username=" + username + "】查詢數(shù)據(jù)完成,結(jié)果:" + queryResult);
}
?然后,在UserDetailsServiceImpl
中調(diào)整原來的實(shí)現(xiàn),改成:
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
log.debug("Spring Security框架自動(dòng)調(diào)用了UserDetailsServiceImpl.loadUserByUsername()方法,用戶名:{}", s);
// 根據(jù)用戶名從數(shù)據(jù)庫中查詢匹配的用戶信息
AdminLoginInfoVO loginInfo = adminMapper.getLoginInfoByUsername(s);
if (loginInfo == null) {
log.debug("此用戶名沒有匹配的用戶數(shù)據(jù),將返回null");
return null;
}
log.debug("用戶名匹配成功!準(zhǔn)備返回此用戶名匹配的UserDetails類型的對(duì)象");
UserDetails userDetails = User.builder()
.username(loginInfo.getUsername())
.password(loginInfo.getPassword())
.disabled(loginInfo.getEnable() == 0) // 賬號(hào)狀態(tài)是否禁用
.accountLocked(false) // 賬號(hào)狀態(tài)是否鎖定
.accountExpired(false) // 賬號(hào)狀態(tài)是否過期
.credentialsExpired(false) // 賬號(hào)的憑證是否過期
.authorities("這是一個(gè)臨時(shí)使用的山寨的權(quán)限?。?!") // 權(quán)限
.build();
log.debug("即將向Spring Security返回UserDetails類型的對(duì)象:{}", userDetails);
return userDetails;
}
為了得到較好的運(yùn)行效果,應(yīng)該在數(shù)據(jù)表中插入一些新的測試數(shù)據(jù),例如:
因?yàn)槟壳芭渲玫拿艽a編碼器是NoOpPasswordEncoder
,所以,本次測試運(yùn)行時(shí),使用的賬號(hào)在數(shù)據(jù)庫的密碼應(yīng)該是明文密碼!
關(guān)于密碼編碼器
?Spring Security定義了PasswordEncoder
接口,可以有多種不同的實(shí)現(xiàn),此接口中的抽象方法主要有:
// 對(duì)原密碼進(jìn)行編碼,返回編碼后的結(jié)果(密文)
String encode(String rawPassword);
// 驗(yàn)證密碼原文(第1個(gè)參數(shù))和密文(第2個(gè)參數(shù))是否匹配
boolean matches(String rawPassword, String encodedPassword);
常見的對(duì)密碼進(jìn)行編碼,實(shí)現(xiàn)“加密”效果所使用的算法主要有:
-
MD(Message Digest)系列:MD2 / MD4 / MD5
-
SHA(Secure Hash Algorithm)系列:SHA-1 / SHA-256 / SHA-384 / SHA-512
-
BCrypt
-
SCrypt
目前,推薦使用的算法是BCrypt
算法!在Spring Security框架中,也提供了BCryptPasswordEncoder
類,其基本使用: ?
public class BCryptTests {
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
@Test
void encode() {
String rawPassword = "123456";
System.out.println("原文:" + rawPassword);
for (int i = 0; i < 5; i++) {
String encodedPassword = passwordEncoder.encode(rawPassword);
System.out.println("密文:" + encodedPassword);
}
}
// 原文:123456
// 密文:$2a$10$YOW67gn1jGQsNd1lWFOktuxGEK3Ai4obSCo6m0o0zP3YA4iTm0QoS
// 密文:$2a$10$AoGlKthb1ZKzTAng5ssX6OUwN8.tC9junqbYhtF0POkr.XdFuoEWy
// 密文:$2a$10$wgBhSmnoFQ.LdvFCLd8lyOSsHuGVIpVYKW8.bW4yt2kBMYqG1G.5u
// 密文:$2a$10$OIiWGSjFH02Vr9khLEQnG.s2rGowkotMV14TThAgJK8KQm.WQq6pm
// 密文:$2a$10$DluGioTO7Zcc0hmwDz8Ld.4Uyp2hIIZ/PcGhFCVd1P3FuSukqJN36
@Test
void matches() {
String rawPassword = "123456";
System.out.println("原文:" + rawPassword);
String encodedPassword = "$2a$10$wgBhSmnoFQ.LdvFCLd8lyOSsHuGVIpVYKW8.bW4yt2kBMYqG1G.5u";
System.out.println("密文:" + encodedPassword);
boolean result = passwordEncoder.matches(rawPassword, encodedPassword);
System.out.println("匹配結(jié)果:" + result);
}
}
關(guān)于BCrypt算法,其典型特征有:
-
使用同樣的原文,每次得到的密文都不相同
-
BCrypt算法在編碼過程中,使用了隨機(jī)的“鹽”(salt)值,所以,每次編碼結(jié)果都不同
-
編碼結(jié)果中保存了這個(gè)隨機(jī)的鹽值,所以,并不影響驗(yàn)證是否匹配
-
-
運(yùn)算效率極為低下,可以非常有效的避免暴力破解
-
可以通過構(gòu)造方法傳入
strength
值,增加強(qiáng)度(默認(rèn)為10
),表示運(yùn)算過程中執(zhí)行2的多少次方的哈希運(yùn)算 -
此特征是MD系列和SHA家庭的算法所不具備的特征
-
另外,SCrypt算法的安全性比BCrypt還要高,但是,執(zhí)行效率比BCrypt更低,通常,由于BCrypt算法已經(jīng)能夠提供足夠的安全強(qiáng)度,所以,目前,使用BCrypt是常見的選擇。
使用BCrypt算法
只需要在Security配置類中將密碼編碼器換成BCryptPasswordEncoder
即可: ?
?接下來,便可以使用數(shù)據(jù)庫中那些密碼是密文的賬號(hào)測試登錄:
(注意:專業(yè)名詞上BCrypt算法以及上文提到的MD等都不是加密算法,加密算法是能加密還能解密的,而這些算法都是單向加密不可逆和還原的。登錄的時(shí)候只能用數(shù)據(jù)庫的密文和傳遞進(jìn)來的密文做匹配,是不能驗(yàn)證原密碼的。)
在以上案例中還不能使用post請(qǐng)求,需要以下:
Spring Security框架設(shè)計(jì)了“防止偽造的跨域攻擊”的防御機(jī)制,所以,默認(rèn)情況下,自定義的POST請(qǐng)求是不可用的,簡單的解決方案就是在Spring Security的配置類中禁用這個(gè)防御機(jī)制即可,例如:
?
?
關(guān)于偽造的跨域攻擊
?偽造的跨域攻擊:此類攻擊原理是利用服務(wù)器端對(duì)客戶端瀏覽器的“信任”來實(shí)現(xiàn)的!目前,主流的瀏覽器都是多選項(xiàng)卡模式的,假設(shè)在第1個(gè)選項(xiàng)卡中登錄了某個(gè)網(wǎng)站,在第2個(gè)選項(xiàng)卡也打開這個(gè)網(wǎng)站的頁面,就會(huì)被當(dāng)作是已經(jīng)登錄的狀態(tài)!基于這種特征,假設(shè)在第1個(gè)選項(xiàng)卡中登錄了某個(gè)網(wǎng)上銀行,在第2個(gè)選項(xiàng)卡中打開了某個(gè)壞人的網(wǎng)站(不是網(wǎng)上銀行的網(wǎng)站),但是,在這個(gè)壞人的網(wǎng)站的頁面中隱藏了一個(gè)使用網(wǎng)上銀行進(jìn)行轉(zhuǎn)賬的請(qǐng)求,這個(gè)請(qǐng)求在壞人的網(wǎng)站的頁面剛剛打開時(shí)就自動(dòng)發(fā)送出去了(自動(dòng)發(fā)送:方法很多,例如將URL設(shè)置為某個(gè)不顯示的<img>
標(biāo)簽的src
值),由于在第1個(gè)選項(xiàng)卡中已經(jīng)登錄了網(wǎng)上銀行,從第2個(gè)選項(xiàng)卡中發(fā)出的請(qǐng)求也會(huì)被視為已經(jīng)登錄網(wǎng)上銀行的狀態(tài),這就實(shí)現(xiàn)了一種攻擊行為!當(dāng)然,以上只是舉例,真正的銀行轉(zhuǎn)賬不會(huì)這么簡單,例如還需要輸入密碼、手機(jī)驗(yàn)證碼等等,但是,這種模式的攻擊行為是確實(shí)存在的,由于使用另一個(gè)網(wǎng)站(壞人的網(wǎng)站)偷偷的實(shí)現(xiàn)的攻擊,所以,稱之為“偽造的跨域攻擊”!
?
典型的防御手段:在Spring Security框架中,默認(rèn)就開啟了對(duì)于“偽造跨域攻擊”的防御機(jī)制,其做法是在所有POST表單中隱藏一個(gè)具有“唯一性”的“隨機(jī)值”,例如UUID值,當(dāng)客戶端提交請(qǐng)求時(shí),必須提交這個(gè)UUID值,如果未提交,則服務(wù)器端將其直接視為攻擊行為,將拒絕處理此請(qǐng)求!以Spring Security默認(rèn)的登錄表單為例:
?
當(dāng)把防御機(jī)制禁用后,這個(gè)數(shù)值也就沒有了。
?提示:此前“如果在打開登錄頁面后重啟過服務(wù)器端,則第1次的輸入是無效的”,也是因?yàn)檫@種防御機(jī)制,當(dāng)打開登錄頁,服務(wù)器端生成了此次使用的UUID,但重啟服務(wù)器后,服務(wù)器不再識(shí)別此前生成的UUID,所以,第1次的輸入是無效的!
?
目前以上已經(jīng)實(shí)現(xiàn)Spring Security框架它默認(rèn)帶來的效果,解決了認(rèn)證和授權(quán)的問題,最主要的用它來處理登錄。但目前還不夠,還需要實(shí)現(xiàn)前后端分離的登錄。
使用前后端分離的登錄
?Spring Security框架自帶了登錄頁面和退出登錄頁面,不是前后端分離的,則不可以與自行開發(fā)的前端項(xiàng)目中的登錄頁面進(jìn)行交互,如果要改為前后端分離的模式,需要:
?
-
不再啟用服務(wù)器端Spring Security框架自帶的登錄頁面和退出登錄頁面
-
在配置類中不再調(diào)用
http.formLogin()
即可
-
?
-
使用控制器接收客戶端的登錄請(qǐng)求
-
自定義Param類,封裝客戶端將提交的用戶名和密碼,在控制器類中添加接收登錄請(qǐng)求的方法
-
?
?
?
-
注意:需要將此請(qǐng)求配置在“白名單”中(不能登錄之后在登錄)
?
使用Service處理登錄的業(yè)務(wù)
-
在接口中聲明抽象方法,并在實(shí)現(xiàn)類中重寫此方法
?
?
?
?
-
具體的驗(yàn)證登錄,仍可以由Spring Security框架來完成,調(diào)用
AuthenticationManager
(認(rèn)證管理器)對(duì)象的authenticate()
方法即可,則Spring Security框架會(huì)自動(dòng)基于調(diào)用方法時(shí)傳入的用戶名來調(diào)用UserDetailsService
接口對(duì)象的loadUserByUsername()
方法,并得到返回的UserDetails
對(duì)象,然后,自動(dòng)判斷賬號(hào)狀態(tài)、對(duì)比密碼等等-
可以在Spring Security的配置類中重寫
authenticationManagerBean()
方法,并在此方法上添加@Bean
注解,則可以在任何所需要的位置自動(dòng)裝配AuthenticationManager
類型的數(shù)據(jù),注意:不要使用authenticationManager()
方法,此方法在某些場景(例如某些測試等)中可能導(dǎo)致死循環(huán),最終內(nèi)存溢出
-
?
?
?調(diào)用AuthenticationManager
(認(rèn)證管理器)對(duì)象的authenticate()
方法,傳入authentication這個(gè)參數(shù)。
?
點(diǎn)開Authentication?發(fā)現(xiàn),也是一個(gè)接口
?
?而Authentication實(shí)現(xiàn)類是
它需要傳入?yún)?shù)? ,?有兩套構(gòu)造方法,第一套第一個(gè)參數(shù)是用戶名,第二個(gè)是密碼。通過傳入的參數(shù)取出用戶名和密碼。
?
?以上:根據(jù)取出的用戶名和密碼創(chuàng)建了用戶認(rèn)證對(duì)象authentication ,用于去調(diào)用認(rèn)證管理器AuthenticationManager的認(rèn)證方法authenticate()。
最后驗(yàn)證登錄成功。
?
完成后,重啟項(xiàng)目,可以通過API文檔的調(diào)試功能來測試登錄,如果使用無法登錄的賬號(hào)信息,會(huì)在服務(wù)器端的控制臺(tái)看到對(duì)應(yīng)的異常: ?
-
用戶名不存在
org.springframework.security.authentication.InternalAuthenticationServiceException: UserDetailsService returned null, which is an interface contract violation
-
密碼錯(cuò)誤
org.springframework.security.authentication.BadCredentialsException: 用戶名或密碼錯(cuò)誤
-
賬號(hào)被禁用
org.springframework.security.authentication.DisabledException: 用戶已失效
可以在全局異常處理器中添加處理以上異常的方法,通常,在處理時(shí),不會(huì)嚴(yán)格區(qū)分“用戶名不存在”和“密碼錯(cuò)誤”這2種錯(cuò)誤,也就是說,無論是這2種錯(cuò)誤中的哪一種,一般提示“用戶名或密碼錯(cuò)誤”即可,以進(jìn)一步保障賬號(hào)安全!
關(guān)于以上用戶名不存在、密碼錯(cuò)誤時(shí)對(duì)應(yīng)的異常,其繼承結(jié)構(gòu)是:
AuthenticationException
-- BadCredentialsException // 密碼錯(cuò)誤
-- AuthenticationServiceException
-- -- InternalAuthenticationServiceException // 用戶名不存在
則可以在處理異常的方法上,在@ExceptionHandler
注解中指定需要處理的2種異常,并且,使用這2種異常公共的父類作為方法的參數(shù),(如果光使用父類作為參數(shù),父類下的其他異常也會(huì)被處理,所以要指定要處理的兩種異常)例如:
// 如果@ExceptionHandler沒有配置參數(shù),則以方法參數(shù)的異常為準(zhǔn),來處理異常
// 如果@ExceptionHandler配置了參數(shù),則只處理此處配置的異常
@ExceptionHandler({
InternalAuthenticationServiceException.class,
BadCredentialsException.class
})
public JsonResult handleAuthenticationException(AuthenticationException e) {
// 暫不關(guān)心方法內(nèi)部的代碼
}
在實(shí)際處理時(shí),需要先在ServiceCode
中添加新的枚舉值,以表示以上錯(cuò)誤的狀態(tài)碼:
?
然后,在全局異常處理器中添加處理異常的方法: ?
// 如果@ExceptionHandler沒有配置參數(shù),則以方法參數(shù)的異常為準(zhǔn),來處理異常
// 如果@ExceptionHandler配置了參數(shù),則只處理此處配置的異常
@ExceptionHandler({
InternalAuthenticationServiceException.class,
BadCredentialsException.class
})
public JsonResult handleAuthenticationException(AuthenticationException e) {
log.warn("程序運(yùn)行過程中出現(xiàn)了AuthenticationException,將統(tǒng)一處理!");
log.warn("異常:", e);
String message = "登錄失敗,用戶名或密碼錯(cuò)誤!";
return JsonResult.fail(ServiceCode.ERR_UNAUTHORIZED, message);
}
@ExceptionHandler
public JsonResult handleDisabledException(DisabledException e) {
log.warn("程序運(yùn)行過程中出現(xiàn)了DisabledException,將統(tǒng)一處理!");
log.warn("異常:", e);
String message = "登錄失敗,賬號(hào)已經(jīng)被禁用!";
return JsonResult.fail(ServiceCode.ERR_UNAUTHORIZED_DISABLE, message);
}
?
以上只能算驗(yàn)證已經(jīng)完成了,還不能算登錄已經(jīng)成功,因?yàn)樵谂袛嘤脩裘兔艽a對(duì)了以后,還需要把相關(guān)的信息比如用戶,把它放進(jìn)例如session里面去,回頭判斷有沒有登錄的標(biāo)準(zhǔn),就是看session有沒有這個(gè)信息,有就是登錄了,沒有就是沒登錄。所以登錄不是判斷用戶名密碼就結(jié)束,還需要把信息留下來,下次在來訪問的時(shí)候才知道你是誰。不僅僅是驗(yàn)證的過程。
關(guān)于認(rèn)證的標(biāo)準(zhǔn)
Spring Security為每個(gè)客戶端分配了一個(gè)SecurityContext
(可稱之為“Security上下文”),并且,會(huì)根據(jù)在SecurityContext
中是否存在認(rèn)證信息來判斷當(dāng)前請(qǐng)求是否已經(jīng)通過認(rèn)證!即:
-
如果在
SecurityContext
中存在有效的認(rèn)證信息,則視為“已通過認(rèn)證” -
如果在
SecurityContext
中沒有有效的認(rèn)證信息,則視為“未通過認(rèn)證”
所以,在驗(yàn)證登錄成功后,需要將認(rèn)證信息存入到SecurityContext
中,否則,所開發(fā)的登錄功能是沒有意義的!
其實(shí)調(diào)用AuthenticationManager
(認(rèn)證管理器)對(duì)象的authenticate()
方法時(shí)是可以接收到一個(gè)返回值的,可以獲取到認(rèn)證結(jié)果。
使用SecurityContextHolder
的getContext()
靜態(tài)方法可以獲取當(dāng)前客戶端對(duì)應(yīng)的SecurityContext
對(duì)象!
?
?打印認(rèn)證方法返回的結(jié)果
?
?以上認(rèn)證方法返回的結(jié)果例如:
UsernamePasswordAuthenticationToken [
Principal=org.springframework.security.core.userdetails.User [
Username=root,
Password=[PROTECTED],
Enabled=true,
AccountNonExpired=true,
credentialsNonExpired=true,
AccountNonLocked=true,
Granted Authorities=[這是一個(gè)臨時(shí)使用的山寨的權(quán)限!??!]
],
Credentials=[PROTECTED],
Authenticated=true,
Details=null,
Granted Authorities=[這是一個(gè)臨時(shí)使用的山寨的權(quán)限?。?!]
]
其實(shí),以上數(shù)據(jù)是基于UserDetailsSerivce
實(shí)現(xiàn)類中loadUserByUsername()
返回的UserDetails
對(duì)象來創(chuàng)建的!
后續(xù)整個(gè)Spring Security在登錄之后每次發(fā)請(qǐng)求的時(shí)候就可以重SecurityContext的到這個(gè)數(shù)據(jù),從而識(shí)別你的身份。
以上算是實(shí)現(xiàn)了一個(gè)登錄的完整功能,但是還有一個(gè)小的問題,比如在登錄了的時(shí)候,服務(wù)端重啟了,此時(shí)登錄的信息就沒了,此時(shí)在沒有登錄信息的時(shí)候去訪問那些必須要登錄的請(qǐng)求。會(huì)得到一個(gè)403錯(cuò)誤。所以以下:
未通過認(rèn)證時(shí)拒絕訪問
當(dāng)未通過認(rèn)證(Spring Security從SecurityContext
中未找到認(rèn)證信息)時(shí),嘗試訪問那些需要授權(quán)的資源(不在白名單中的,需要先登錄才可以訪問的資源),在沒有啟用http.formLogin()
時(shí),默認(rèn)將響應(yīng)403
錯(cuò)誤!
需要在Spring Security的配置類中進(jìn)行處理: ?
首先用http去這個(gè)方法
?這個(gè)方法需要傳進(jìn)去的參數(shù)的類型是AuthenticationEntryPoint,點(diǎn)開后發(fā)現(xiàn)也是一個(gè)接口。
?有兩種方式,可以自己寫個(gè)類去實(shí)現(xiàn),但這個(gè)本身是一次性的使用,因?yàn)檫@個(gè)類只用在配置里,而配置本身是一次性的代碼,所以可以用匿名內(nèi)部類來寫。
?這里我們需要向客戶端去響應(yīng)一個(gè)錯(cuò)誤說你還沒有登錄,那么可以直接用response去響應(yīng),比如通過一個(gè)輸出流-寫出文本-關(guān)流響應(yīng)一個(gè)簡單內(nèi)容:
?但是現(xiàn)在響應(yīng)的內(nèi)容太過簡單,可以響應(yīng)一個(gè)message內(nèi)容進(jìn)去。
?此時(shí)響應(yīng)出現(xiàn)顯示為一堆問號(hào),這是因?yàn)閖ava原始的服務(wù)器端的問題,默認(rèn)使用的是ISO-8859-1這個(gè)編碼格式,這種格式是不支持中文的。
?要在文檔響應(yīng)之前,設(shè)置編碼格式,例如:
此時(shí)顯示就沒有問題了:?
?
?但此時(shí)任然不符合我們的設(shè)計(jì)需求。我們因該響應(yīng)給客戶端的是一個(gè)json結(jié)果,而不是一個(gè)字符串而已,需要更改文檔類型前半截:
?在寫入一個(gè)json格式的字符串(格式可以復(fù)制,手敲累容易出錯(cuò)):
?得到顯示json的結(jié)果:
?現(xiàn)在代碼惡心在需要自己去拼json這個(gè)結(jié)果,最終我們要響應(yīng)的還是和之前成功處理請(qǐng)求和處理異常時(shí)得到是一樣的結(jié)果,它依然是一個(gè)json格式的數(shù)據(jù),只不過我們之前處理請(qǐng)求,處理異常返回JsonResult就可以,為什么返回JsonResult的對(duì)象最終響應(yīng)是一個(gè)json的數(shù)據(jù)是因?yàn)閟pringMVC框架幫我們做了數(shù)據(jù)格式的轉(zhuǎn)換,轉(zhuǎn)換成json格式的字符串。但現(xiàn)在不能轉(zhuǎn),它不在springMVC的范圍之內(nèi)。
?則需要人為創(chuàng)建JSON格式的結(jié)果!可以借助fastjson
工具進(jìn)行處理,這是一款可以實(shí)現(xiàn)對(duì)象與JSON格式字符串相互轉(zhuǎn)換的工具!需要添加依賴:
<fastjson.version>1.2.75</fastjson.version>
<!-- fastjson:實(shí)現(xiàn)對(duì)象與JSON的相互轉(zhuǎn)換 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
?便可以用這個(gè)工具做以下調(diào)整:
?最終:
?
目前,登錄算做好了,對(duì)Security使用難得部分已經(jīng)過去了,下面是一些往后推進(jìn)會(huì)設(shè)計(jì)的問題:
識(shí)別當(dāng)事人(Principal)
當(dāng)事人:當(dāng)前提交請(qǐng)求的客戶端的身份數(shù)據(jù) ?。
當(dāng)事人是一種身份數(shù)據(jù),作用是,比如你登錄一款軟件,這個(gè)軟件得知道你是誰,不然就無法做相關(guān)的操作,例如登錄之后你要修改自己的密碼,首先它得知道你是誰,然后再去改你的密碼。這份表示你到底是誰的這個(gè)數(shù)據(jù)其核心,我們就把它叫做當(dāng)事人。
當(dāng)通過登錄的驗(yàn)證后,AuthenticationManager
的authenticate()
方法返回的Authentication
對(duì)象中,就包含了當(dāng)事人信息!例如: ?
UsernamePasswordAuthenticationToken [
Principal=org.springframework.security.core.userdetails.User [
Username=root,
Password=[PROTECTED],
Enabled=true,
AccountNonExpired=true,
credentialsNonExpired=true,
AccountNonLocked=true,
Granted Authorities=[這是一個(gè)臨時(shí)使用的山寨的權(quán)限?。?!]
],
Credentials=[PROTECTED],
Authenticated=true,
Details=null,
Granted Authorities=[這是一個(gè)臨時(shí)使用的山寨的權(quán)限?。?!]
]
?數(shù)據(jù)里面的Principal這些數(shù)據(jù)就是當(dāng)事人。
由于已經(jīng)將以上認(rèn)證結(jié)果存入到SecurityContext
中,則可以在后續(xù)任何需要識(shí)別當(dāng)事人的場景中,獲取當(dāng)事人信息! ?
Spring Security提供了非常便利的獲取當(dāng)事人的做法,在控制器類中的處理請(qǐng)求的方法的參數(shù)列表中,可以聲明當(dāng)事人類型的參數(shù)(這里的user就是當(dāng)時(shí)返回的userDetails,即可以說它是userDetails類型也可以說是User類型):
并在參數(shù)上添加@AuthenticationPrincipal
注解即可,例如找到管理員的controller: ?
?
上面添加@ApiIgnore是因?yàn)?寫user有一個(gè)問題是API文檔會(huì)以為你這個(gè)是請(qǐng)求參數(shù),會(huì)在API文檔中看到很多參數(shù),調(diào)試?yán)锩嬉矔?huì)有很多輸入框,需要加上這個(gè)注解來忽略。
?
?此時(shí)這個(gè)user是有值的
?它的值就是在登錄成功后返回的當(dāng)事人數(shù)據(jù):?
?也就是這一截:
Principal=org.springframework.security.core.userdetails.User [
Username=root,
Password=[PROTECTED],
Enabled=true,
AccountNonExpired=true,
credentialsNonExpired=true,
AccountNonLocked=true,
Granted Authorities=[這是一個(gè)臨時(shí)使用的山寨的權(quán)限?。?!]
]
就可以通過get拿到當(dāng)時(shí)人的信息:?
?完成以上代碼后,重啟項(xiàng)目,可以在API文檔中使用各個(gè)賬號(hào)嘗試登錄并訪問以上“查詢管理員列表”,可以看到日志中輸出了當(dāng)次登錄的賬號(hào)的用戶名,例如:
?
通過以上做法,雖然可以獲取當(dāng)事人信息,但是,無論是
UserDetails
還是User
類型,可以獲取的數(shù)據(jù)信息較少,且不包含當(dāng)前登錄的用戶的ID,通常并不滿足開發(fā)需求! ?
?需要記?。寒?dāng)前在控制器類中處理請(qǐng)求的方法中注入的當(dāng)事人數(shù)據(jù),就是UserDetailsService
接口的實(shí)現(xiàn)類中返回的數(shù)據(jù)!
?而里面的數(shù)據(jù)來自于loginInfo
?loginInfo是從數(shù)據(jù)庫查出來的
?所以如果需要獲取當(dāng)事人的ID,需要:
?在AdminLoginInfoVO
中添加ID屬性
?修改Mapper層的getLoginInfoByUsername()
,需要查詢管理員ID
?現(xiàn)有的UserDetails
的實(shí)現(xiàn)類User
并不支持ID屬性,需要自定義類實(shí)現(xiàn)UserDetails
接口,或者,自定義類繼承自User
類,在自定義類中擴(kuò)展出所需的各種屬性,例如ID
因?yàn)樗旧斫o了我們user類
?
?點(diǎn)開后發(fā)現(xiàn)user實(shí)現(xiàn)了UserDetails類
?
?所以我們自定義繼承user相對(duì)于也實(shí)現(xiàn)了UserDetails,最終也可以作為這個(gè)方法的返回值。
?
在項(xiàng)目的根包下創(chuàng)建security.AdminDetails
類,繼承自User
類,添加基于父類的構(gòu)造方法,并擴(kuò)展出ID屬性:
?然后只用第二個(gè)多的構(gòu)造方法,第一個(gè)可以去掉,第二個(gè)包含了第一個(gè)所有的參數(shù),還有賬戶啟動(dòng)狀態(tài)等必要的信息。
但同時(shí)也用不完第二個(gè)構(gòu)造方法里面的所有參數(shù),我們需要把自己的構(gòu)造方法中不用的參數(shù)去掉,同時(shí),在調(diào)用父類的構(gòu)造方法的時(shí)候需要這個(gè)參數(shù),我們?cè)诮o個(gè)固定的值傳過去就好了。
?擴(kuò)展出id屬性,并給構(gòu)造參數(shù)加上id傳進(jìn)來給值。回頭還需要被這個(gè)值取出來,但是不能用@Data,因?yàn)長ombok需要在父類也就是user類有一個(gè)默認(rèn)的無參構(gòu)造方法,但是user沒有。所以添加@Getter注解。
?在UserDetailsService
中返回?cái)?shù)據(jù)時(shí),改為返回自定義類的對(duì)象,其中將包含ID等屬性值
?里面的自定義的傳參會(huì)略有不用,之前判斷賬號(hào)是否禁用的==0,因?yàn)楫?dāng)時(shí)方法叫做disabled禁用,而自己的屬性的啟用,就用==1判斷。
?添加權(quán)限用集合,以下:
?最終代碼如下:
?在控制器類中處理請(qǐng)求的方法中,注入的當(dāng)事人類型改為自定義類型
?
以上實(shí)現(xiàn)了可以登錄登錄后也知道你是誰的功能,登錄的效果就差不多了,而Spring Security還有一個(gè)重要的功能就是權(quán)限,我們可以區(qū)分不同的賬戶它有什么操作權(quán)限,使得某些用戶可以做特定的事情。如果要去判端當(dāng)前這個(gè)人有沒有權(quán)限去做這個(gè)事情,第一件事是把現(xiàn)在給的山寨權(quán)限換成數(shù)據(jù)庫里的真實(shí)權(quán)限。
實(shí)現(xiàn)根據(jù)權(quán)限限制訪問
?首先,需要在管理員登錄時(shí),明確此管理員的權(quán)限,則需要在Mapper層實(shí)現(xiàn)“根據(jù)用戶名查詢管理員的登錄信息,且需要包含此管理員對(duì)應(yīng)的各權(quán)限”,需要執(zhí)行的SQL語句大致是:
select
ams_admin.id,
ams_admin.username,
ams_admin.password,
ams_admin.enable,
ams_permission.value
from ams_admin
left join ams_admin_role on ams_admin.id=ams_admin_role.admin_id
left join ams_role_permission on ams_admin_role.role_id=ams_role_permission.role_id
left join ams_permission on ams_role_permission.permission_id=ams_permission.id
where username='root';
然后,修改現(xiàn)有的查詢功能,需要先在AdminLoginInfoVO
類中添加新的屬性,用于存放“權(quán)限列表”:
?
?
?然后,調(diào)整AdminMapper.xml
中的配置:
<!-- AdminLoginInfoVO getLoginInfoByUsername(String username); -->
<select id="getLoginInfoByUsername" resultMap="LoginInfoResultMap">
SELECT
ams_admin.id,
ams_admin.username,
ams_admin.password,
ams_admin.enable,
ams_permission.value
FROM ams_admin
LEFT JOIN ams_admin_role ON ams_admin.id=ams_admin_role.admin_id
LEFT JOIN ams_role_permission ON ams_admin_role.role_id=ams_role_permission.role_id
LEFT JOIN ams_permission ON ams_role_permission.permission_id=ams_permission.id
WHERE
username=#{username}
</select>
<!-- resultMap標(biāo)簽:指導(dǎo)MyBatis封裝查詢結(jié)果 -->
<!-- resultMap標(biāo)簽的id屬性:自定義名稱,也是select標(biāo)簽上使用resultMap屬性的值 -->
<!-- resultMap標(biāo)簽的type屬性:封裝查詢結(jié)果的類型的全限定名 -->
<resultMap id="LoginInfoResultMap"
type="cn.tedu.csmall.passport.pojo.vo.AdminLoginInfoVO">
<!-- id標(biāo)簽:配置主鍵的列與屬性的對(duì)應(yīng)關(guān)系 -->
<!-- result標(biāo)簽:配置普通的列與屬性的對(duì)應(yīng)關(guān)系 -->
<!-- collection標(biāo)簽:配置List集合類型的屬性與查詢結(jié)果中的數(shù)據(jù)的對(duì)應(yīng)關(guān)系 -->
<!-- collection標(biāo)簽的ofType屬性:集合中的元素類型,取值為類型的全限定名 -->
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
<result column="enable" property="enable"/>
<collection property="permissions" ofType="String">
<!-- constructor標(biāo)簽:通過構(gòu)造方法來創(chuàng)建對(duì)象 -->
<constructor>
<!-- arg標(biāo)簽:配置構(gòu)造方法的參數(shù),如果構(gòu)造方法有多個(gè)參數(shù),依次使用多個(gè)此標(biāo)簽 -->
<arg column="value"></arg>
</constructor>
</collection>
</resultMap>
補(bǔ)充解釋(關(guān)于使用resultMap標(biāo)簽):
[SpringBoot]xml文件里寫SQL用resultMap標(biāo)簽_萬物更新_的博客-CSDN博客
?配置完成后,可以通過測試進(jìn)行檢驗(yàn),查詢結(jié)果例如:
根據(jù)【username=super_admin】查詢數(shù)據(jù)完成,結(jié)果:
AdminLoginInfoVO(
id=2,
username=super_admin,
password=$2a$10$N.ZOn9G6/YLFixAOPMg/h.z7pCu6v2XyFDtC4q.jeeGm/TEZyj15C,
enable=1,
permissions=[/pms/product/read, /pms/product/add-new, /pms/product/delete, /pms/product/update, /pms/brand/read, /pms/brand/add-new, /pms/brand/delete, /pms/brand/update, /pms/category/read, /pms/category/add-new, /pms/category/delete, /pms/category/update, /pms/picture/read, /pms/picture/add-new, /pms/picture/delete, /pms/picture/update, /pms/album/read, /pms/album/add-new, /pms/album/delete, /pms/album/update]
)
?
?
基于方法的權(quán)限檢查
以上loginInfo已經(jīng)有真實(shí)的權(quán)限信息,從中g(shù)et出真實(shí)權(quán)限,遍歷加到權(quán)限集合里面去,加進(jìn)去后,返回的userDetails就有真正的權(quán)限信息了。
當(dāng)有了真實(shí)的權(quán)限以后,接下來就可以對(duì)所有的訪問加上權(quán)限的限制,就是某些人可以干什么,某些人不可以干什么。要實(shí)現(xiàn)這樣的效果需要做兩件事情。
第一件事情,找到配置類, 開啟權(quán)限的檢查機(jī)制
?接下來就可以做訪問什么需要什么權(quán)限,例如必須具有管理員權(quán)限的值的人,才可以查看權(quán)限列表。
?
加上下面這個(gè)注解后就表示你不光要登錄,你的認(rèn)證信息的權(quán)限列表里面必須要包含hasAuthority里面的這個(gè)值,才能夠做這次的訪問,如果不包含這個(gè)權(quán)限,就訪問不了。
提示:以上使用@PreAuthorize
注解檢查權(quán)限時(shí),此注解可以添加在任何方法上!例如Controller中的方法,或Service中的方法等等,由于當(dāng)前項(xiàng)目中,客戶端的請(qǐng)求第一時(shí)間都是交給了Controller,所以,更適合在Controller方法上檢查權(quán)限!
?
當(dāng)訪問不包含所需的權(quán)限時(shí),?Spring Security給了我們以下這個(gè)異常:
?有異常在全局異常處理器里面處理異常:
?
在ServiceCode
中添加新的業(yè)務(wù)狀態(tài)碼表示“無此權(quán)限”:
[異常]401和403的區(qū)分_萬物更新_的博客-CSDN博客
?
然后,在全局異常處理器中添加處理以上異常的方法: ?
?
以上權(quán)限做好以后,還需要給它添加Token功能,這樣每次客服端在訪問過一次之后,都不用在繼續(xù)登陸。
[java]關(guān)于Session&關(guān)于Token&關(guān)于JWT_萬物更新_的博客-CSDN博客?
添加Token?
首先添加Token-JWT的依賴項(xiàng):
父項(xiàng)目添加版本管理:
?父項(xiàng)目添加依賴:
?子項(xiàng)目添加依賴:
?添加好依賴以后做兩個(gè)測試,一個(gè)生成JWT的測試,一個(gè)解析JWT 的測試:
生成JWT:
// 不太簡單的、難以預(yù)測的字符串
String secretKey = "jhdSfkkjKJ3831HdsDkdfSA9jklJD749Fhsa34fdsKf08dfjFhkdfs";
@Test
void generate() {
Map<String, Object> claims = new HashMap<>();
claims.put("id", 9527);
claims.put("name", "張三");
String jwt = Jwts.builder()
// Header
.setHeaderParam("alg", "HS256")
.setHeaderParam("typ", "JWT")
// Payload
.setClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + 3 * 60 * 1000))//設(shè)置有效期,防止一直用.
// Verify Signature
.signWith(SignatureAlgorithm.HS256, secretKey)
// 生成
.compact();
System.out.println(jwt);
}
備注:
?基于它的做法,我們可以自己傳進(jìn)去一個(gè)值:
?
?解析JWT
// 不太簡單的、難以預(yù)測的字符串
String secretKey = "jhdSfkkjKJ3831HdsDkdfSA9jklJD749Fhsa34fdsKf08dfjFhkdfs";
@Test
void parse() {
String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoi5byg5LiJIiwiaWQiOjk1MjcsImV4cCI6MTY4NDkwODUwMn0.tBo7YKRqQv6TG2cf5jeu7nNjUim5X8H6pKLF1LrYuKI";
Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
Long id = claims.get("id", Long.class);
String name = claims.get("name", String.class);
System.out.println("id = " + id);
System.out.println("name = " + name);
}
備注:?
點(diǎn)進(jìn)Claims可以看到本質(zhì)是一個(gè)map
?獲取往里面放的值,直接給的是object,因?yàn)閙ap的value被定義死了是object,取出也是object
?但在這里Claims在原有的map之上,get方法是有擴(kuò)展的,傳入的第二個(gè)參數(shù)就是你的目標(biāo)類型是什么,這樣傳進(jìn)去是什么類型得到的就是什么類型。
?
以下是會(huì)這塊會(huì)出現(xiàn)的異常,列舉出來,回頭需要全局處理。
如果嘗試解析的JWT已經(jīng)過期,會(huì)出現(xiàn)異常:
io.jsonwebtoken.ExpiredJwtException: JWT expired at 2023-05-24T12:02:38Z. Current time: 2023-05-24T14:04:35Z, a difference of 7317175 milliseconds. Allowed clock skew: 0 milliseconds.
?如果解析JWT時(shí)使用的secretKey有誤,會(huì)出現(xiàn)異常:
io.jsonwebtoken.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.
如果解析JWT的數(shù)據(jù)格式錯(cuò)誤,會(huì)出現(xiàn)異常:
io.jsonwebtoken.MalformedJwtException: JWT strings must contain exactly 2 period characters. Found: 1
補(bǔ)充:
注意:在不知曉secretKey的情況下,也可以解析出JWT中的數(shù)據(jù)(例如將JWT數(shù)據(jù)粘貼到官網(wǎng)),只不過驗(yàn)證簽名是失敗的,所以,不要在JWT中存放敏感信息(比如密碼,手機(jī)號(hào)碼,身份證號(hào)碼等)!
?
?驗(yàn)證簽名是失敗的就是說就算你知道里面的數(shù)據(jù),但是我會(huì)告訴你不可信,比如id是9527但是也不要相信id就是9527,因?yàn)樗苡锌赡苁且粋€(gè)偽造的JWT,因?yàn)轵?yàn)證簽名失敗了。所以JWT的secretKey的價(jià)值是防止被偽造,而不是防止被解析出來,它不能做到這一點(diǎn)。
?
經(jīng)過上面的測試,接下來就要在項(xiàng)目中使用JWT識(shí)別用戶的身份了
在項(xiàng)目中使用JWT識(shí)別用戶的身份
核心流程
?在項(xiàng)目中使用JWT識(shí)別用戶的身份,至少需要:
-
當(dāng)驗(yàn)證登錄成功時(shí),生成JWT數(shù)據(jù),并響應(yīng)到客戶端去,是“賣票”的過程
-
當(dāng)驗(yàn)證登錄成功后,不再需要(沒有必要)
-
當(dāng)驗(yàn)證登錄成功時(shí),生成JWT數(shù)據(jù),并響應(yīng)到客戶端去,是“賣票”的過程
-
當(dāng)驗(yàn)證登錄成功后,不再需要(沒有必要)將認(rèn)證結(jié)果存入到
SecurityContext
中 ,之前是這樣的:
-
-
?
-
當(dāng)客戶端提交請(qǐng)求時(shí),需要獲取客戶端攜帶的JWT數(shù)據(jù),并嘗試解析,解析成功后,再將相關(guān)信息存入到
SecurityContext
中去,(因?yàn)橹拔覀冋fSecurity去檢驗(yàn)這個(gè)賬號(hào)或者說這次客戶端的訪問到底是不是一個(gè)已認(rèn)證的狀態(tài),就只是去看SecurityContext里面有沒有東西,所以一旦解析成功之后,還是要把相關(guān)信息往SecurityContext里面放,然后就沒了,后續(xù)說他有沒有登錄啊,有沒有權(quán)限啊不是這里管的事,是Security去做后續(xù)的處理
)是“檢票”的過程-
可以調(diào)整Spring Security使用Session的策略,改為不使用Session,則不會(huì)將
SecurityContext
存入到Session中(不存在Session里面的好處是它就只作用在這一次請(qǐng)求中,這次請(qǐng)求結(jié)束了SecurityContext就沒了,當(dāng)下次在過來的時(shí)候就又有了,結(jié)束了又沒了。。。所以SecurityContext里面的認(rèn)證信息只作用于當(dāng)次那一次而已,在沒有調(diào)整之前是基于session的,意味著如果session的有效期是15分鐘,那你把認(rèn)證信息存上下文里面,那這個(gè)上下文的有效時(shí)間就是15分鐘,15分鐘之內(nèi)一直存在這個(gè)數(shù)據(jù)了,如果有效期是30分鐘,那就會(huì)存在30分鐘,在這個(gè)30分鐘里面肯定是會(huì)有浪費(fèi)的時(shí)間的,內(nèi)存里面存這個(gè)信息就會(huì)浪費(fèi)了,并且在你重新來訪之后時(shí)間又會(huì)重新調(diào)整為30分鐘,所以會(huì)有很長時(shí)間的浪費(fèi)
-
?
驗(yàn)證登錄成功時(shí)響應(yīng)JWT
需要調(diào)整的代碼大致包括:
-
在
IAdminService
中,將login()
方法的返回值類型改為String
類型,重寫的方法作同樣的修改
?
-
在
AdminServiceImpl
中,驗(yàn)證登錄成功后,生成此管理員的信息對(duì)應(yīng)的JWT(把上文測試?yán)锩嫔蒍WT的代碼拿過來做修改),并返回
?
?
-
在
AdminController
中,處理登錄時(shí),調(diào)用Service方法時(shí)獲取返回的JWT,并響應(yīng)到客戶端去
?
解析客戶端攜帶的JWT
客戶端提交若干種不同的請(qǐng)求時(shí),可能都會(huì)攜帶JWT,對(duì)應(yīng)的,在服務(wù)器,處理若干種不同的請(qǐng)求時(shí),也都需要嘗試接收并解析JWT,則應(yīng)該使用過濾器(Filter)組件進(jìn)行處理!
[web]關(guān)于過濾器Filter_萬物更新_的博客-CSDN博客
其實(shí),Spring Security框架也使用了許多不同的過濾器來解決各種問題,為了保證解析JWT是有效的,解析JWT的代碼必須運(yùn)行在Spring Security的某些過濾器之前,則接收、解析JWT的代碼也必須定義在過濾器中!
提示:過濾器(Filter)是Java服務(wù)器端應(yīng)用程序的核心組件之一,它是最早接收到請(qǐng)求的組件!過濾器可以對(duì)請(qǐng)求選擇“阻止”或“放行”!同一個(gè)項(xiàng)目中,允許存在若干個(gè)過濾器,形成“過濾器鏈(Filter Chain)”,任何請(qǐng)求必須被所有過濾器都“放行”,才會(huì)被控制器或其它組件所處理!
?按照之前的方法,實(shí)現(xiàn)javax.servlet的過濾器接口,讓后重寫doFilter方法。
?但是重寫方法需要對(duì)類型進(jìn)行強(qiáng)轉(zhuǎn),比較麻煩,不太好用。?
?
?我們這次選擇去繼承Spring系列框架提供的OncePerRequestFilter這個(gè)類。
?
?這個(gè)類是一個(gè)抽象類,這個(gè)類繼承自GenericFilterBean這個(gè)類。
而GenericFilterBean這個(gè)類實(shí)現(xiàn)了Filter這個(gè)接口,?所以繼承OncePerRequestFilter這個(gè)類也算是實(shí)現(xiàn)了過濾器接口的。
繼承spring這個(gè)框架提供的OncePerRequestFilter這個(gè)類已經(jīng)幫我們做了強(qiáng)轉(zhuǎn)了,就不用我們自己強(qiáng)轉(zhuǎn)了。
?
?所以在項(xiàng)目的根包下創(chuàng)建filter.JwtAuthorizationFilter
類,繼承自OncePerRequestFilter
類,并添加@Component
注解:
@Slf4j
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.debug("JwtAuthorizationFilter開始執(zhí)行……");
// 放行
filterChain.doFilter(request, response);
}
}
添加@Component
注解把它標(biāo)記成組件是因?yàn)橥ㄟ^注入,把解析JWT的代碼必須運(yùn)行在Spring Security的某些過濾器之前。
?到此可以測試通過API登錄請(qǐng)求常看第一步過濾器有沒有生效:
關(guān)于攜帶JWT,根據(jù)業(yè)內(nèi)慣用的做法,客戶端會(huì)將JWT放在請(qǐng)求頭(Request Header)中的Authorization屬性中,在Knife4j的API文檔中,可以:
?
?
關(guān)于過濾器的初步實(shí)現(xiàn):
/**
* JWT過濾器,解決的問題:接收J(rèn)WT,解析JWT,將解析得到的數(shù)據(jù)創(chuàng)建為認(rèn)證信息并存入到SecurityContext
*/
@Slf4j
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.debug("JwtAuthorizationFilter開始執(zhí)行……");
// 根據(jù)業(yè)內(nèi)慣用的做法,客戶端會(huì)將JWT放在請(qǐng)求頭(Request Header)中的Authorization屬性中
String jwt = request.getHeader("Authorization");
log.debug("客戶端攜帶的JWT:{}", jwt);
// 判斷客戶端是否攜帶了有效的JWT
if (!StringUtils.hasText(jwt)) {
// 如果JWT無效,則放行,并reture
filterChain.doFilter(request, response);
return;
}
// TODO 當(dāng)前類和AdminServiceImpl中都聲明了同樣的secretKey變量,是不合理的
// TODO 解析JWT過程中可能出現(xiàn)異常,需要處理
// 嘗試解析JWT
String secretKey = "jhdSfkkjKJ3831HdsDkdfSA9jklJD749Fhsa34fdsKf08dfjFhkdfs";
Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
Long id = claims.get("id", Long.class);
String username = claims.get("username", String.class);
System.out.println("id = " + id);
System.out.println("username = " + username);
// TODO 需要考慮使用什么數(shù)據(jù)作為當(dāng)事人
// TODO 需要使用真實(shí)的權(quán)限
// 創(chuàng)建認(rèn)證信息
Object principal = username; //當(dāng)事人 可以是任何類型,暫時(shí)使用用戶名
Object credentials = null; //憑證 本次不需要
Collection<GrantedAuthority> authorities = new ArrayList<>();//權(quán)限
authorities.add(new SimpleGrantedAuthority("山寨權(quán)限"));
Authentication authentication = new UsernamePasswordAuthenticationToken(
principal, credentials, authorities);
// 將認(rèn)證信息存入到SecurityContext中
SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.setAuthentication(authentication);
// 放行
filterChain.doFilter(request, response);
}
}
因?yàn)樯厦娲a中當(dāng)事人是username,此時(shí)參數(shù)再用AdminDetails 聲明是不對(duì)的,此處暫時(shí)去掉。
?
需要注意:由于Spring Security的SecurityContext
默認(rèn)是基于Session的,所以,當(dāng)攜帶JWT成功登錄訪問過后,在SecurityContext
中就已經(jīng)有了認(rèn)證信息,并且,在Session的有效期內(nèi),即使后續(xù)不攜帶JWT,Spring Security也能基于Session找到SecurityContext
并讀取到認(rèn)證信息,并不在需要登錄就能訪問的,這可能與設(shè)計(jì)初衷并不相符!
可以將Spring Security使用(創(chuàng)建)Session的策略改為“完全不使用Session”,需要在Spring Security的配置類中添加配置:
?
備注:
1.用StringUtils.hasText的方法
?用StringUtils.hasText的方法可以同時(shí)判斷,不能為空,不能為null,和包含文本。
?包含文本即不是空白就是包含文本:
?2.關(guān)于?Object credentials = null本此不需要憑證,因?yàn)橹皯{證的表現(xiàn)是密碼,而放在上下文里的認(rèn)證信息作用是回頭框架來識(shí)別出你是誰,有什么權(quán)限,這個(gè)過程是不需要使用密碼的。
?
關(guān)于認(rèn)證信息中的當(dāng)事人
pring Security框架并不介意你使用什么類型作為認(rèn)證信息(Authentication
)中的當(dāng)事人(principal
)!
在項(xiàng)目中,到底使用什么類型作為當(dāng)事人,可以自行考慮,主要考慮的因素就是:當(dāng)你需要注入當(dāng)事人數(shù)據(jù)的時(shí)候,你希望能夠得到哪些數(shù)據(jù)!
在項(xiàng)目的根包下創(chuàng)建security.LoginPrincipal
作為自定義的當(dāng)事人類型:
?
并且,在解析JWT成功后,在過濾去使用此類型作為當(dāng)事人來創(chuàng)建認(rèn)證信息:
?后續(xù),在Controller中,就可以通過@AuthenticationPrincipal
來注入自定義的當(dāng)事人數(shù)據(jù),例如:
?
接著處理一個(gè)小問題,因?yàn)樵谏珊徒馕鯦WT的時(shí)候?qū)π枰玫絪ecretKey這個(gè)值,并且這個(gè)值相同,如果不相同就會(huì)簽名失敗,所以一個(gè)完全相同的代碼寫兩遍是不合理的,有兩種解決方案,第一種是專門寫一個(gè)類去調(diào)取,第二個(gè)是寫在application.yml文件里面,它們的區(qū)別是在application.yml里面需要讀取在應(yīng)用,有一個(gè)讀取的過程,在類里面是直接應(yīng)用的,從執(zhí)行效率來說肯定是在類里面更快一些,但由于這個(gè)值需要甲方來定(為了防止偽造相關(guān)問題),所以必須寫在application.yml里面。
?
?
?
?
?
?
?
?關(guān)于secretKey必須有4位以上,否則都會(huì)被視為空值報(bào)錯(cuò)
?
以上權(quán)限還是一個(gè)假的權(quán)限,需要換成真的權(quán)限,目前我們就用把權(quán)限放在JWT中,然后再從JWT中取出權(quán)限的方式。(以替換在數(shù)據(jù)庫里查的方式,因?yàn)閺臄?shù)據(jù)庫里查數(shù)據(jù)是一個(gè)效率低下的方式,其一需要連接,傳遞SQL,然后準(zhǔn)備,準(zhǔn)備好了編譯執(zhí)行,執(zhí)行好了在給個(gè)結(jié)果一個(gè)過程。其二,數(shù)據(jù)庫里面的數(shù)據(jù)存在硬盤里面,硬盤是一個(gè)存儲(chǔ)效率非常低效的硬件。同時(shí)這段代碼只要有客戶端來訪就會(huì)執(zhí)行這段代碼,發(fā)生的非常高頻率,所以不能選擇連接數(shù)據(jù)庫這么低效的做法)?
?把集合放進(jìn)JWT里面。
?
?從JWT取出權(quán)限列表
?這樣的取出方式看似語法沒有問題,但會(huì)出現(xiàn)類型轉(zhuǎn)換錯(cuò)誤。
?因?yàn)樵谶@一步,它獲取出來的是LinkedHashMap,但是LinkedHashMap不能強(qiáng)制轉(zhuǎn)其他類型,為什么獲取的是LinkedHashMap類型呢,因?yàn)锳PI不知道你要獲取什么類型,給你處理為了LinkedHashMap。因?yàn)槭羌霞臃盒鸵矝]有辦法向獲取id一樣在后面第二個(gè)參數(shù)加上Long.class來指定返回的類型。
?
?所以這里需要換一個(gè)做法,在生成JWT的時(shí)候不往里面放集合里,改為放Json,
?可以放Json是因?yàn)槲覀冇刑砑觙astjson的依賴,實(shí)現(xiàn)對(duì)象和Json相互轉(zhuǎn)換的依賴。
?在從JWT 取出權(quán)限的時(shí)候也取出Json字符串,然后用fastjson轉(zhuǎn)成集合
?
以上就實(shí)現(xiàn)真實(shí)權(quán)限的功能了。
?注意:此方式也不是最優(yōu)解決方案。
?
接下來處理解析JWT時(shí)可能出現(xiàn)的異常,往常我們是在全局異常處理器處理的,但是在這里不行,因?yàn)榻馕鯦WT是在過濾器里面做的,全局異常處理器只能處理controller拋出的異常。
處理解析JWT時(shí)的異常
由于解析JWT是在過濾器組件中執(zhí)行的,而過濾器是最早處理請(qǐng)求的組件,此時(shí),控制器(Controller)還沒有開始處理這次的請(qǐng)求,則全局異常處理器也無法處理解析JWT時(shí)出現(xiàn)的異常(全局異常處理器只能處理控制器拋出的異常)!這里使用最原始的try...catch處理
首先,在ServiceCode
中補(bǔ)充新的狀態(tài)碼:
ERR_JWT_EXPIRED(60000),
ERR_JWT_MALFORMED(60100),
ERR_JWT_SIGNATURE(60200),
然后,在JwtAuthorizationFilter
中,使用try...catch
包裹嘗試解析JWT的代碼:
// 嘗試解析JWT
response.setContentType("application/json; charset=utf-8");
Claims claims = null;
try {
claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
} catch (MalformedJwtException e) {
String message = "非法訪問!";
log.warn("程序運(yùn)行過程中出現(xiàn)了MalformedJwtException,將向客戶端響應(yīng)錯(cuò)誤信息!");
log.warn("錯(cuò)誤信息:{}", message);
JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_JWT_MALFORMED, message);
String jsonString = JSON.toJSONString(jsonResult);
PrintWriter printWriter = response.getWriter();
printWriter.println(jsonString);
printWriter.close();
return;
} catch (SignatureException e) {
String message = "非法訪問!";
log.warn("程序運(yùn)行過程中出現(xiàn)了SignatureException,將向客戶端響應(yīng)錯(cuò)誤信息!");
log.warn("錯(cuò)誤信息:{}", message);
JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_JWT_SIGNATURE, message);
String jsonString = JSON.toJSONString(jsonResult);
PrintWriter printWriter = response.getWriter();
printWriter.println(jsonString);
printWriter.close();
return;
} catch (ExpiredJwtException e) {
String message = "您的登錄信息已經(jīng)過期,請(qǐng)重新登錄!";
log.warn("程序運(yùn)行過程中出現(xiàn)了ExpiredJwtException,將向客戶端響應(yīng)錯(cuò)誤信息!");
log.warn("錯(cuò)誤信息:{}", message);
JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_JWT_EXPIRED, message);
String jsonString = JSON.toJSONString(jsonResult);
PrintWriter printWriter = response.getWriter();
printWriter.println(jsonString);
printWriter.close();
return;
} catch (Throwable e) {
String message = "服務(wù)器忙,請(qǐng)稍后再試!【在開發(fā)過程中,如果看到此提示,應(yīng)該檢查服務(wù)器端的控制臺(tái),分析異常,并在解析JWT的過濾器中補(bǔ)充處理對(duì)應(yīng)異常的代碼塊】";
log.warn("程序運(yùn)行過程中出現(xiàn)了Throwable,將向客戶端響應(yīng)錯(cuò)誤信息!");
log.warn("異常:", e);
JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_UNKNOWN, message);
String jsonString = JSON.toJSONString(jsonResult);
PrintWriter printWriter = response.getWriter();
printWriter.println(jsonString);
printWriter.close();
return;
}
注意:
以上代碼中只有response.setContentType("application/json; charset=utf-8");這串代碼可以提到最上面給每一個(gè)catch復(fù)用。?PrintWriter printWriter = response.getWriter();是不可以的:
把printWriter 放在上面會(huì)導(dǎo)致本該在正常成功訪問的時(shí)候會(huì)報(bào)狀態(tài)異常錯(cuò)誤,說getWriter在本次調(diào)用中已經(jīng)被占用了。原因是我們服務(wù)端向客戶端響應(yīng)就是用printWriter 來響應(yīng)的,然后你在上圖中拿到了getWriter輸出流,控制器那邊就拿不到響應(yīng)成功的輸出流了,以至于控制器沒有辦法去響應(yīng)。
?
?
?以上JWT就差不多了,以下在和前端結(jié)合的時(shí)候還需要實(shí)現(xiàn)的一些功能。
處理復(fù)雜請(qǐng)求的跨域問題
當(dāng)客戶端提交請(qǐng)求時(shí),在請(qǐng)求頭中配置了特定的屬性(例如Authorization,帶了JWT的時(shí)候
),則這個(gè)請(qǐng)求會(huì)被視為“復(fù)雜請(qǐng)求”:
對(duì)于復(fù)雜請(qǐng)求,瀏覽器會(huì)先對(duì)服務(wù)器端發(fā)送OPTIONS
類型的請(qǐng)求(也是和get,post一樣的請(qǐng)求方式,OPTIONS請(qǐng)求的目的是試一下服務(wù)器是不是好的,是不是可以接受
),以執(zhí)行預(yù)檢(PreFlight),如果預(yù)檢通過,才會(huì)執(zhí)行本應(yīng)該發(fā)送的請(qǐng)求。
然后會(huì)看到它的請(qǐng)求就需要給它配置白名單已通過。
?在Spring Security的配置類中,可以在配置對(duì)請(qǐng)求授權(quán)時(shí),將所有OPTIONS
類型的請(qǐng)求全部直接許可,例如:
?或者,調(diào)用參數(shù)對(duì)象的cors()
方法也可以,例如:
提示:對(duì)于復(fù)雜請(qǐng)求的預(yù)檢,是瀏覽器的行為,并且,當(dāng)某個(gè)請(qǐng)求通過預(yù)檢后,瀏覽器會(huì)緩存此結(jié)果,后續(xù)再次發(fā)出此請(qǐng)求時(shí),不會(huì)再次執(zhí)行預(yù)檢。
實(shí)現(xiàn)單點(diǎn)登錄,以下
?
單點(diǎn)登錄
SSO(Single Sign On):單點(diǎn)登錄,表示在集群或分布式系統(tǒng)中,客戶端只需要在某1個(gè)服務(wù)器上完成登錄的驗(yàn)證,后續(xù),無論訪問哪個(gè)服務(wù)器,都不需要再次重新登錄!常見的實(shí)現(xiàn)手段主要有:共享Session,使用Token。 ?
?目前,如果希望客戶端在csmall-passport
中登錄后,在csmall-product
中也能夠被識(shí)別身份、權(quán)限,需要:
-
復(fù)制依賴項(xiàng):
spring-boot-starter-security
、jjwt
、fastjson
-
復(fù)制
LoginPrincipal
?
-
復(fù)制
ServiceCode
,覆蓋此前的文件
-
復(fù)制
application-dev.yml
中的自定義的配置
-
復(fù)制
JwtAuthorizationFilter
?
-
復(fù)制
SecurityConfiguration,并更改導(dǎo)包
-
刪除
PasswordEncoder
的@Bean
方法 -
刪除
AuthenticationManager
的@Bean
方法 -
刪除“白名單”中管理員登錄的URL地址
-
完成后,在csmall-product
項(xiàng)目中,也可以通過@AuthenticationPrincipal
來注入當(dāng)事人數(shù)據(jù),也可以使用@PreAuthorize
來配置訪問權(quán)限,這些都是通的。
文章來源:http://www.zghlxwxcb.cn/news/detail-480051.html
?
到了這里,關(guān)于[SpringBoot]Spring Security框架的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!