SpringBoot集成SpringSecurity安全框架方式
一、CSRF跨站請求偽造攻擊
我們時常會在 QQ 上收到別人發(fā)送的釣魚網(wǎng)站鏈接,只要你在登錄QQ賬號的情況下點(diǎn)擊鏈接,那么不出意外,你的號已經(jīng)在別人手中了。實(shí)際上這一類網(wǎng)站都屬于惡意網(wǎng)站,專門用于盜取他人信息,執(zhí)行非法操作,甚至獲取他人賬戶中的財產(chǎn),非法轉(zhuǎn)賬等。
我們在 JavaWeb 階段已經(jīng)了解了 Session 和 Cookie 的機(jī)制,在一開始的時候,服務(wù)端會給瀏覽器一個名為 JSESSION
的 Cookie 信息作為會話的唯一憑據(jù),只要用戶攜帶此 Cookie 訪問我們的網(wǎng)站,那么我們就可以認(rèn)定此會話屬于哪個瀏覽器。因此,只要此會話的用戶執(zhí)行了登錄操作,那么就可以隨意訪問個人信息等內(nèi)容。
要完成一次CSRF攻擊,受害者必須依次完成兩個步驟:
- 登錄受信任網(wǎng)站A,并在本地生成 Cookie。
- 在不登出A的情況下,訪問危險網(wǎng)站B。
確實(shí)如此,我們無法保證以下情況不會發(fā)生:
- 你不能保證你登錄了一個網(wǎng)站后,不再打開一個web頁面并訪問另外的網(wǎng)站。
- 你不能保證你關(guān)閉瀏覽器了后,你本地的Cookie立刻過期,你上次的會話已經(jīng)結(jié)束。
- 上圖中所謂的攻擊網(wǎng)站,可能是一個存在其他漏洞的可信任的經(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}
- 只有管理員可以訪問,必須登錄
三、認(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)大的安全管理!
記住幾個類:
WebSecurityConfigurerAdapter
:自定義 Security 策略AuthenticationManagerBuilder
:自定義認(rèn)證策略@EnableWebSecurity
:開啟 WebSecurity 模式
Spring Security 的兩個主要目標(biāo)是 “認(rèn)證” 和 “授權(quán)”(訪問控制)。
“認(rèn)證”(Authentication)
- 身份驗(yàn)證是關(guān)于驗(yàn)證您的憑據(jù),如用戶名/用戶ID和密碼,以驗(yàn)證您的身份。
- 身份驗(yàn)證通常通過用戶名和密碼完成,有時與身份驗(yàn)證因素結(jié)合使用。
“授權(quán)” (Authorization)
- 授權(quán)發(fā)生在系統(tǒng)成功驗(yàn)證您的身份后,最終會授予您訪問資源(如信息,文件,數(shù)據(jù)庫,資金,位置,幾乎任何內(nèi)容)的完全權(quán)限。
- 這個概念是通用的,而不是只在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值計算,計算出來的結(jié)果無法還原為原文,如果需要驗(yàn)證是否與此密碼一致,那么需要以同樣的方式加密再比較兩個Hash值是否一致,這樣就很好的保證了用戶密碼的安全性。
此時,我們就可以成功登錄了!
②使用數(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. 然后我們需要創(chuàng)建一個 Service 實(shí)現(xiàn),實(shí)現(xiàn)的是 UserDetailsService
,它支持我們自己返回一個 UserDetails
對象,我們只需直接返回一個包含數(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("登錄失敗,用戶名或密碼錯誤!"); } return User // 這里需要返回 UserDetails,SpringSecurity 會根據(jù)給定的信息進(jìn)行比對 .withUsername(s) .password(password) // 直接從數(shù)據(jù)庫取的密碼 .roles("currentUser") // 用戶角色 .build(); } }
d. 修改一下 Security 配置類:
此時,我們就可以使用數(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") // 某個角色可以訪問的頁面 .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("登錄失敗,用戶名或密碼錯誤!"); 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ā)送給瀏覽器保存,以后登錄帶上這個 cookie,只要通過檢查就可以免登錄了。如果點(diǎn)擊注銷,springsecurity 幫我們自動刪除了這個 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),首先有一個用戶名的輸入框和一個密碼的輸入框,我們需要在其中填寫用戶名和密碼,但是我們發(fā)現(xiàn),除了這兩個輸入框以外,還有一個 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ù)。
- 而這里的登錄表單正好是一個 POST 類型的請求。在默認(rèn)配置下,無論是否登錄,頁面中只要發(fā)起了 PATCH,POST,PUT 和 DELETE 請求 一定會被拒絕,并返回 403 錯誤 (注意,這里是個究極大坑)
- 方案一:我們可以在配置類中加入 http.csrf().disable(); // 關(guān)閉csrf功能,我們采取此方案。
- 方案二:需要在請求的時候加入 csrfToken 才行,也就是"83421936-b84b-44e3-be47-58bb2c14571a"。如果提交的是表單類型的數(shù)據(jù),那么表單中必須包含此 Token 字符串,鍵名稱為 "_csrf";如果是 JSON 數(shù)據(jù)格式發(fā)送的,那么就需要在請求頭中包含此 Token 字符串。
綜上所述,我們最后提交的登錄表單,除了必須的用戶名和密碼,還包含了一個 csrfToken
字符串用于驗(yàn)證,防止攻擊。
②自定義登錄界面
a. 先寫一個登錄頁面: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)用戶未登錄時,跳轉(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)建一個子線程去獲取 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 有三個實(shí)現(xiàn)類:
- GlobalSecurityContextHolderStrategy:全局模式,不常用
- ThreadLocalSecurityContextHolderStrategy:基于ThreadLocal實(shí)現(xiàn),線程內(nèi)可見
- InheritableThreadLocalSecurityContextHolderStrategy:基于InheritableThreadLocal實(shí)現(xiàn),線程和子線程可見
因此,如果上述情況需要在子線程中獲取,那么需要修改 SecurityContextHolder 的存儲策略,在初始化的時候設(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)建一個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ú)占嗎,那每次請求都是一個新的線程,按理說上一次的 SecurityContext 對象應(yīng)該沒了才對啊,為什么再次請求依然能夠繼續(xù)使用上一次 SecurityContext 中的認(rèn)證信息呢?
SecurityContext 的生命周期:請求到來時從 Session 中取出,放入 SecurityContextHolder 中,請求結(jié)束時從 SecurityContextHolder 取出,并放到 Session 中,實(shí)際上就是依靠 Session 來存儲的,一旦會話過期驗(yàn)證信息也跟著消失。
總結(jié)
本文是對SpringSecurity的學(xué)習(xí),學(xué)習(xí)了它的兩大功能:認(rèn)證和授權(quán),以及如何使用數(shù)據(jù)庫進(jìn)行認(rèn)證,如何使用自定義的登錄頁面,最后也學(xué)習(xí)了使用SecurityContext獲取認(rèn)證用戶的信息。
以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
詳解Spring Boot 部署jar和war的區(qū)別
本篇文章主要介紹了詳解Spring Boot 部署jar和war的區(qū)別,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-09-09springMVC的RequestMapping請求不到路徑的解決
這篇文章主要介紹了springMVC的RequestMapping請求不到路徑的解決,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08java 替換docx文件中的字符串方法實(shí)現(xiàn)
這篇文章主要介紹了java 替換docx文件中的字符串方法實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-02-02詳解在Spring?Boot中使用數(shù)據(jù)庫事務(wù)
本篇文章主要介紹了詳解在Spring?Boot中使用數(shù)據(jù)庫事務(wù),具有一定的參考價值,感興趣的小伙伴們可以參考一下<BR>2017-05-05Java讀取Oracle大字段數(shù)據(jù)(CLOB)的2種方法
這篇文章主要介紹了Java讀取Oracle大字段數(shù)據(jù)(CLOB)的2種方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-04-04一鍵清除maven倉庫中下載失敗的jar包的實(shí)現(xiàn)方法
這篇文章主要介紹了一鍵清除maven倉庫中下載失敗的jar包的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07