提示:以下是本篇文章正文內(nèi)容,Java 系列學(xué)習(xí)將會持續(xù)更新
一、CSRF跨站請求偽造攻擊
我們時(shí)常會在 QQ 上收到別人發(fā)送的釣魚網(wǎng)站鏈接,只要你在登錄QQ賬號的情況下點(diǎn)擊鏈接,那么不出意外,你的號已經(jīng)在別人手中了。實(shí)際上這一類網(wǎng)站都屬于惡意網(wǎng)站,專門用于盜取他人信息,執(zhí)行非法操作,甚至獲取他人賬戶中的財(cái)產(chǎn),非法轉(zhuǎn)賬等。
我們在 JavaWeb 階段已經(jīng)了解了 Session 和 Cookie 的機(jī)制,在一開始的時(shí)候,服務(wù)端會給瀏覽器一個(gè)名為 JSESSION
的 Cookie 信息作為會話的唯一憑據(jù),只要用戶攜帶此 Cookie 訪問我們的網(wǎng)站,那么我們就可以認(rèn)定此會話屬于哪個(gè)瀏覽器。因此,只要此會話的用戶執(zhí)行了登錄操作,那么就可以隨意訪問個(gè)人信息等內(nèi)容。
要完成一次CSRF攻擊,受害者必須依次完成兩個(gè)步驟:
- 登錄受信任網(wǎng)站A,并在本地生成 Cookie。
- 在不登出A的情況下,訪問危險(xiǎn)網(wǎng)站B。
確實(shí)如此,我們無法保證以下情況不會發(fā)生:
- 你不能保證你登錄了一個(gè)網(wǎng)站后,不再打開一個(gè)web頁面并訪問另外的網(wǎng)站。
- 你不能保證你關(guān)閉瀏覽器了后,你本地的Cookie立刻過期,你上次的會話已經(jīng)結(jié)束。
- 上圖中所謂的攻擊網(wǎng)站,可能是一個(gè)存在其他漏洞的可信任的經(jīng)常被人訪問的網(wǎng)站。
顯然,我們之前編寫的圖書管理系統(tǒng)就存在這樣的安全漏洞,而SpringSecurity
就很好地解決了這樣的問題。
二、項(xiàng)目準(zhǔn)備
我們還是基于之前的 SpringBoot 項(xiàng)目 - 圖書管理系統(tǒng)進(jìn)行改造,需要實(shí)現(xiàn)以下:
-
http://localhost:8080/index.html
- 任何人都可以訪問,不需要登錄 -
http://localhost:8080/book/{bid}
- 任何人都可以訪問,不需要登錄 -
http://localhost:8080/user/{bid}
- 只有用戶可以訪問,必須登錄 -
http://localhost:8080/borrow/{uid}
- 只有管理員可以訪問,必須登錄
回到目錄…文章來源:http://www.zghlxwxcb.cn/news/detail-733990.html
三、認(rèn)識 SpringSecurity
Spring Security 是針對Spring項(xiàng)目的安全框架,也是Spring Boot底層安全模塊默認(rèn)的技術(shù)選型,他可以實(shí)現(xiàn)強(qiáng)大的Web安全控制,對于安全控制,我們僅需要引入 spring-boot-starter-security 模塊,進(jìn)行少量的配置,即可實(shí)現(xiàn)強(qiáng)大的安全管理!
記住幾個(gè)類:
-
WebSecurityConfigurerAdapter
:自定義 Security 策略 -
AuthenticationManagerBuilder
:自定義認(rèn)證策略 -
@EnableWebSecurity
:開啟 WebSecurity 模式
Spring Security 的兩個(gè)主要目標(biāo)是 “認(rèn)證” 和 “授權(quán)”(訪問控制)。
-
“認(rèn)證”(Authentication)
身份驗(yàn)證是關(guān)于驗(yàn)證您的憑據(jù),如用戶名/用戶ID和密碼,以驗(yàn)證您的身份。
身份驗(yàn)證通常通過用戶名和密碼完成,有時(shí)與身份驗(yàn)證因素結(jié)合使用。 -
“授權(quán)” (Authorization)
授權(quán)發(fā)生在系統(tǒng)成功驗(yàn)證您的身份后,最終會授予您訪問資源(如信息,文件,數(shù)據(jù)庫,資金,位置,幾乎任何內(nèi)容)的完全權(quán)限。
這個(gè)概念是通用的,而不是只在Spring Security 中存在。
參考官網(wǎng):https://spring.io/projects/spring-security
相關(guān)幫助文檔:https://docs.spring.io/spring-security/site/docs/3.0.7.RELEASE/reference
①先引入 SpringSecurity 依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
②實(shí)現(xiàn) SpringSecurity 配置類: 我們可以在配置類中認(rèn)證和授權(quán)
@EnableWebSecurity // 開啟WebSecurity模式
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
回到目錄…
3.1 認(rèn)證
??①直接認(rèn)證
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();// 必須加密,使用SpringSecurity提供的BCryptPasswordEncoder
// 在內(nèi)存中定義認(rèn)證用戶
auth.inMemoryAuthentication().passwordEncoder(encoder)
.withUser("aaa").password(encoder.encode("666")).roles("currentUser")
.and()
.withUser("bbb").password(encoder.encode("666")).roles("currentUser")
.and()
.withUser("root").password(encoder.encode("123456")).roles("admin");
}
SpringSecurity 的密碼校驗(yàn)并不是直接使用原文進(jìn)行比較,而是使用加密算法將密碼進(jìn)行加密(更準(zhǔn)確地說應(yīng)該進(jìn)行Hash處理,此過程是不可逆的,無法解密),最后將用戶提供的密碼以同樣的方式加密后與密文進(jìn)行比較。
對于我們來說,用戶提供的密碼屬于隱私信息,直接明文存儲并不好,而且如果數(shù)據(jù)庫內(nèi)容被竊取,那么所有用戶的密碼將全部泄露,這是我們不希望看到的結(jié)果,我們需要一種既能隱藏用戶密碼也能完成認(rèn)證的機(jī)制,而Hash處理就是一種很好的解決方案,通過將用戶的密碼進(jìn)行Hash值計(jì)算,計(jì)算出來的結(jié)果無法還原為原文,如果需要驗(yàn)證是否與此密碼一致,那么需要以同樣的方式加密再比較兩個(gè)Hash值是否一致,這樣就很好的保證了用戶密碼的安全性。
此時(shí),我們就可以成功登錄了!
回到目錄…
??②使用數(shù)據(jù)庫認(rèn)證
a. 首先,我們必須保證數(shù)據(jù)庫中的 user.password
是通過 BCryptPasswordEncoder
加密過的,否則驗(yàn)證不通過。我們可以將加密后的密碼插入到數(shù)據(jù)庫中:
@Test
public void toEncoder() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
System.out.println(encoder.encode("123456"));
}
b. 編寫 UserMapper 中獲取用戶密碼的 SQL
@Select("select password from user where name = #{name}")
String getPasswordByUsername(String name);
c. 然后我們需要?jiǎng)?chuàng)建一個(gè) Service 實(shí)現(xiàn),實(shí)現(xiàn)的是 UserDetailsService
,它支持我們自己返回一個(gè) UserDetails
對象,我們只需直接返回一個(gè)包含數(shù)據(jù)庫中的用戶名、密碼等信息的 UserDetails 即可, SpringSecurity
會自動進(jìn)行比對。
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
String password = userMapper.getPasswordByUsername(s); //從數(shù)據(jù)庫根據(jù)用戶名獲取密碼
if(password == null) {
throw new UsernameNotFoundException("登錄失敗,用戶名或密碼錯(cuò)誤!");
}
return User // 這里需要返回 UserDetails,SpringSecurity 會根據(jù)給定的信息進(jìn)行比對
.withUsername(s)
.password(password) // 直接從數(shù)據(jù)庫取的密碼
.roles("currentUser") // 用戶角色
.build();
}
}
d. 修改一下 Security 配置類:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 從數(shù)據(jù)庫中認(rèn)證
auth
.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
}
此時(shí),我們就可以使用數(shù)據(jù)庫信息登錄成功了!
回到目錄…
3.2 授權(quán)
??①基于角色授權(quán)
@Override
protected void configure(HttpSecurity http) throws Exception {
// 定制請求的授權(quán)規(guī)則
http.authorizeRequests()
.antMatchers("/index.html", "/book/*").permitAll() // 所有人都可以訪問
.antMatchers("/user/*").hasRole("currentUser") // 某個(gè)角色可以訪問的頁面
.antMatchers("/borrow/*").hasRole("admin");
}
??②基于權(quán)限的授權(quán)
基于權(quán)限的授權(quán)與角色類似,需要以 hasAnyAuthority
或 hasAuthority
進(jìn)行判斷:
.anyRequest().hasAnyAuthority("page:index")
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
String password = mapper.getPasswordByUsername(s);
if(password == null)
throw new UsernameNotFoundException("登錄失敗,用戶名或密碼錯(cuò)誤!");
return User
.withUsername(user.getUsername())
.password(user.getPassword())
.authorities("page:index") // 權(quán)限
.build();
}
??③使用注解判斷權(quán)限
我們可以直接在需要添加權(quán)限驗(yàn)證的請求映射上添加注解:
@PreAuthorize("hasRole('currentUser')") //判斷是否為 currentUser 角色,只有此角色才可以訪問
@RequestMapping("/hello")
public String index(){
return "hello,world";
}
通過添加 @PreAuthorize
注解,在執(zhí)行之前判斷判斷權(quán)限,如果沒有對應(yīng)的權(quán)限或是對應(yīng)的角色,將無法訪問頁面。
同樣的還有 @PostAuthorize
注解,但是它是在方法執(zhí)行之后再進(jìn)行攔截:
@PostAuthorize("hasRole('currentUser')")
@RequestMapping("/test")
public String index(){
System.out.println("先執(zhí)行,再攔截);
return "test";
}
回到目錄…
3.3 “記住我”
<input type="checkbox" name="remember"> 記住我
@Override
protected void configure(HttpSecurity http) throws Exception {
// ........................
http.rememberMe() // 記住我
.rememberMeParameter("remember"); // 自定義頁面的參數(shù)!
}
登錄成功后,將 cookie 發(fā)送給瀏覽器保存,以后登錄帶上這個(gè) cookie,只要通過檢查就可以免登錄了。如果點(diǎn)擊注銷,springsecurity 幫我們自動刪除了這個(gè) cookie。
3.4 登錄和注銷
??①原生登錄界面
首先我們要了解一下 SpringSecurity 是如何進(jìn)行登陸驗(yàn)證的,我們可以觀察一下默認(rèn)的登陸界面中,表單內(nèi)有哪些內(nèi)容:
<div class="container">
<form class="form-signin" method="post" action="/book_manager/login">
<h2 class="form-signin-heading">Please sign in</h2>
<p>
<label for="username" class="sr-only">Username</label>
<input type="text" id="username" name="username" class="form-control" placeholder="Username" required="" autofocus="">
</p>
<p>
<label for="password" class="sr-only">Password</label>
<input type="password" id="password" name="password" class="form-control" placeholder="Password" required="">
</p>
<input name="_csrf" type="hidden" value="83421936-b84b-44e3-be47-58bb2c14571a">
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</form>
</div>
我們發(fā)現(xiàn),首先有一個(gè)用戶名的輸入框和一個(gè)密碼的輸入框,我們需要在其中填寫用戶名和密碼,但是我們發(fā)現(xiàn),除了這兩個(gè)輸入框以外,還有一個(gè) input
標(biāo)簽,它是隱藏的,并且它存儲了一串類似于 Hash
值的東西,名稱為 "_csrf"
,其實(shí)看名字就知道,這玩意八成都是為了防止 CSRF 攻擊而存在的。
- 從 Spring Security 4.0 開始,默認(rèn)情況下會啟用 CSRF 保護(hù),以防止 CSRF 攻擊應(yīng)用程序,Spring Security CSRF 會針對 PATCH,POST,PUT 和 DELETE 方法的請求(不僅僅只是登錄請求,這里指的是任何請求路徑)進(jìn)行防護(hù)。
- 而這里的登錄表單正好是一個(gè)
POST
類型的請求。在默認(rèn)配置下,無論是否登錄,頁面中只要發(fā)起了 PATCH,POST,PUT 和 DELETE 請求 一定會被拒絕,并返回 403 錯(cuò)誤 (注意,這里是個(gè)究極大坑) -
方案一:我們可以在配置類中加入
http.csrf().disable(); // 關(guān)閉csrf功能
,我們采取此方案。 -
方案二:需要在請求的時(shí)候加入
csrfToken
才行,也就是"83421936-b84b-44e3-be47-58bb2c14571a"
。如果提交的是表單類型的數(shù)據(jù),那么表單中必須包含此 Token 字符串,鍵名稱為"_csrf"
;如果是 JSON 數(shù)據(jù)格式發(fā)送的,那么就需要在請求頭中包含此 Token 字符串。
綜上所述,我們最后提交的登錄表單,除了必須的用戶名和密碼,還包含了一個(gè) csrfToken
字符串用于驗(yàn)證,防止攻擊。
回到目錄…
??②自定義登錄界面
a. 先寫一個(gè)登錄頁面:index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登錄</title>
</head>
<body>
<h1>用戶登錄</h1>
<form action="/doLogin" method="post">
用戶名: <input type="text" name="username"><br>
密碼: <input type="password" name="password"><br>
<input type="checkbox" name="remember"> 記住我<br>
<button>登錄</button>
</form>
</body>
</html>
b. 編寫 LoginController
登錄相關(guān)的接口:
@Controller
public class LoginController {
@GetMapping("/success") // 登錄成功后跳轉(zhuǎn)的頁面
public String loginSuccess() {
return "redirect:/user/1";
}
@GetMapping("/failure") // 登錄失敗后跳轉(zhuǎn)的頁面
public String loginFailure() {
return "redirect:/index.html";
}
}
c. 在配置類中設(shè)置:
@Override
protected void configure(HttpSecurity http) throws Exception {
// .............................
http.csrf().disable();
http
.formLogin()
.loginPage("/index.html") // 當(dāng)用戶未登錄時(shí),跳轉(zhuǎn)到該自定義登錄頁面
.loginProcessingUrl("/doLogin") // form表單提交地址(POST),不需要在控制層寫 /doLogin
.defaultSuccessUrl("/success") // 登錄成功后的頁面
.failureUrl("/failure"); // 登錄失敗后的頁面
}
重啟服務(wù)器就可以發(fā)現(xiàn),使用了我們的自定義登錄頁面:
??注銷
注銷接口:http://localhost:8080/logout
,同樣可以自定義注銷頁面,這里就不做演示了。
http.logout().logoutSuccessUrl("/index.html");
回到目錄…
3.4 SecurityContext
用戶登錄之后,怎么獲取當(dāng)前已經(jīng)登錄用戶的信息呢?
方法一:通過使用 SecurityContextHolder
就可以很方便地得到 SecurityContext
對象了,我們可以直接使用 SecurityContext 對象來獲取當(dāng)前的認(rèn)證信息:
@RequestMapping("/index")
public String index(){
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
// org.springframework.security.core.userdetails.User
User user = (User) authentication.getPrincipal();
System.out.println(user.getUsername());
System.out.println(user.getAuthorities());
return "index";
}
方法二:除了這種方式以外,我們還可以直接通過 @SessionAttribute
從 Session 中獲?。?/p>
@RequestMapping("/index")
public String index(@SessionAttribute("SPRING_SECURITY_CONTEXT") SecurityContext context){
Authentication authentication = context.getAuthentication();
User user = (User) authentication.getPrincipal();
System.out.println(user.getUsername());
System.out.println(user.getAuthorities());
return "index";
}
注意:SecurityContextHolder 默認(rèn)的存儲策略是 MODE_THREADLOCAL
,它是基于 ThreadLocal 實(shí)現(xiàn)的,getContext()
方法本質(zhì)上調(diào)用的是對應(yīng)的存儲策略實(shí)現(xiàn)的方法。如果我們這樣編寫,那么在默認(rèn)情況下是無法獲取到認(rèn)證信息的:
@RequestMapping("/index")
public String index(){
new Thread(() -> { //創(chuàng)建一個(gè)子線程去獲取
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
User user = (User) authentication.getPrincipal(); // 失敗,無法獲取認(rèn)證信息
System.out.println(user.getUsername());
System.out.println(user.getAuthorities());
});
return "index";
}
SecurityContextHolderStrategy 有三個(gè)實(shí)現(xiàn)類:
- GlobalSecurityContextHolderStrategy:全局模式,不常用
- ThreadLocalSecurityContextHolderStrategy:基于ThreadLocal實(shí)現(xiàn),線程內(nèi)可見
- InheritableThreadLocalSecurityContextHolderStrategy:基于InheritableThreadLocal實(shí)現(xiàn),線程和子線程可見
因此,如果上述情況需要在子線程中獲取,那么需要修改 SecurityContextHolder 的存儲策略,在初始化的時(shí)候設(shè)置:
@PostConstruct
public void init(){
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}
這樣在子線程中也可以獲取認(rèn)證信息了。
因?yàn)橛脩舻尿?yàn)證信息是基于 SecurityContext 進(jìn)行判斷的,我們可以直接修改 SecurityContext 的內(nèi)容,來手動為用戶進(jìn)行登錄:
@RequestMapping("/auth")
@ResponseBody
public String auth(){
// 獲取SecurityContext對象(當(dāng)前會話肯定是沒有登陸的)
SecurityContext context = SecurityContextHolder.getContext();
// 手動創(chuàng)建一個(gè)UsernamePasswordAuthenticationToken對象,也就是用戶的認(rèn)證信息,角色需要添加ROLE_前綴,權(quán)限直接寫
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Test", null,
AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_user"));
context.setAuthentication(token); // 手動為SecurityContext設(shè)定認(rèn)證信息
return "Login success!";
}
在未登錄的情況下,訪問此地址將直接進(jìn)行手動登錄,再次訪問 /index
頁面,可以直接訪問,說明手動設(shè)置認(rèn)證信息成功。
疑惑:SecurityContext 這玩意不是默認(rèn)線程獨(dú)占嗎,那每次請求都是一個(gè)新的線程,按理說上一次的 SecurityContext 對象應(yīng)該沒了才對啊,為什么再次請求依然能夠繼續(xù)使用上一次 SecurityContext 中的認(rèn)證信息呢?
SecurityContext 的生命周期:請求到來時(shí)從 Session 中取出,放入 SecurityContextHolder 中,請求結(jié)束時(shí)從 SecurityContextHolder 取出,并放到 Session 中,實(shí)際上就是依靠 Session 來存儲的,一旦會話過期驗(yàn)證信息也跟著消失。
回到目錄…
總結(jié):
提示:這里對文章進(jìn)行總結(jié):
本文是對SpringSecurity的學(xué)習(xí),學(xué)習(xí)了它的兩大功能:認(rèn)證和授權(quán),以及如何使用數(shù)據(jù)庫進(jìn)行認(rèn)證,如何使用自定義的登錄頁面,最后也學(xué)習(xí)了使用SecurityContext獲取認(rèn)證用戶的信息。之后的學(xué)習(xí)內(nèi)容將持續(xù)更新?。?!文章來源地址http://www.zghlxwxcb.cn/news/detail-733990.html
到了這里,關(guān)于SpringBoot集成 SpringSecurity安全框架的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!