javascript
项目集成Spring Security
前言
之前寫的 涂涂影院管理系統 這個 demo 是基于 shiro 來鑒權的,項目前后端分離后,顯然集成 Spring Security 更加方便一些,畢竟,都用 Spring 了,權限管理當然 Spring Security.
花了半天時間整理的筆記,希望能對你有所幫助。
Spring Security 一句話概述:一組 filter 過濾器鏈組成的權限認證。
一、加入依賴
環境:項目采用 Spring Initializr 快速構建 Spring Boot ,版本交由 spring-boot-starter-parent 管理。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId> </dependency>在僅僅添加完依賴的情況下,啟動項目看看:
1.1 控制臺打印
控制臺打印了一串密碼,如下圖所示:
訪問一下項目中的某個方法:
http://localhost:7777/tmax/videoCategory/getAll奇怪,怎么自己跳到 /login 路徑下了,而且還讓登陸?
1.2 賬號登錄
在登陸 from 表單里輸入如下:
- 用戶名:user
- 密碼:0839a4ba-c8a3-4aee-8a6e-cd19c1d0b0c1(控制臺打印的)
點擊 Sign in 然后跳轉到了目標地址:
添加 Spring Security 依賴后,實際觸發了兩件事,一時將系統中所有的連接服務都保護起來, 再就是會有默認配置 form 表單認證。
二、基本原理
Spring Security的整個工作流程如下所示:
綠色認證方式可以配置, 橘黃色和藍色的位置不可更改。
Security 有兩種認證方式:
- httpbasic
- formLogin 默認的,如上邊那種方式
同樣,Security 也提供兩種過濾器類:
- UsernamePasswordAuthenticationFilter 表示表單登陸過濾器
- BasicAuthenticationFilter 表示 httpbaic 方式登陸過濾器
圖中橙色的 FilterSecurityInterceptor 是最終的過濾器,它會決定當前的請求可不可以訪問Controller,判斷規則放在這個里面。
當不通過時會把異常拋給在這個過濾器的前面的 ExceptionTranslationFilter 過濾器。
ExceptionTranslationFilter 接收到異常信息時,將跳轉頁面引導用戶進行認證,如上方所示的用戶登陸界面。
三、自定義認證邏輯
實際開發中是不可能使用上方 Spring Security 默認的這種方式的,如何去覆蓋掉 Spring Security 默認的配置呢?
我們以:將默認的 form 認證方式改為 httpbasic 方式為例。
創建SpringSecurity自定義配置類:WebSecurityConfig.java
@Slf4j @Configuration @EnableGlobalMethodSecurity(prePostEnabled=true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();registry.and()表單登錄方式.formLogin().permitAll().and().logout().permitAll().and().authorizeRequests()任何請求.anyRequest()需要身份認證.authenticated().and()關閉跨站請求防護.csrf().disable()前后端分離采用JWT 不需要session.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)} }重新啟動項目,已經看到修改后的 httpbasic 方式認證了。
在這里我們依然采用的默認提供的用戶名 user,以及每次服務器啟動自動生成的 password,那么可不可以自定義認證邏輯呢?比如采用數據庫中的用戶登陸?
答案是肯定的。
自定義用戶認證邏輯需要了解三步:
接下來我們來看一下這三步,然后實現自定義登陸:
3.1 處理用戶信息獲取邏輯
Spring Security 中用戶信息獲取邏輯的獲取邏輯是封裝在一個接口里的:UserDetailService,代碼如下:
public interface UserDetailsService {UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }這個接口中只有一個方法,loadUserByUsername(), 該接收一個 String 類型的 username 參數,然后返回一個 UserDetails 的對象。
那么這個方法到底是干啥的呢?
通過前臺用戶輸入的用戶名,然后去數據庫存儲中獲取對應的用戶信息,然后封裝在 UserDetail 實現類里面。
封裝到 UserDetail 實現類返回以后,Spring Srcurity 會拿著用戶信息去做校驗,如果校驗通過了,就會把用戶放在 session 里面,否則,拋出 UsernameNotFoundException 異常,Spring Security 捕獲后做出相應的提示信息。
想要處理用戶信息獲取邏輯,那么我們就需要自己去實現 UserDetailsService
新建 UserDetailsServiceImpl.java
@Slf4j @Component public class UserDetailsServiceImpl implements UserDetailsService{@Autowiredprivate UserService userService;/*** 從數據庫中獲取用戶信息,返回一個 UserDetails 對象,* @param username* @return* @throws UsernameNotFoundException*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {通過用戶名獲取用戶User user = userService.findByUsername(username);將 user 對象轉化為 UserDetails 對象return new SecurityUserDetails(user);} }SecurityUserDetail.java
public class SecurityUserDetails extends User implements UserDetails {private static final long serialVersionUID = 1L;public SecurityUserDetails(User user) {if(user!=null) {this.setUsername(user.getUsername());this.setPassword(user.getPassword());this.setStatus(user.getStatus());}}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {理想型返回 admin 權限,可自已處理這塊return AuthorityUtils.commaSeparatedStringToAuthorityList("admin");}/*** 賬戶是否過期* @return*/@Overridepublic boolean isAccountNonExpired() {return true;}/*** 是否禁用* @return*/@Overridepublic boolean isAccountNonLocked() {return true;}/*** 密碼是否過期* @return*/@Overridepublic boolean isCredentialsNonExpired() {return true;}/*** 是否啟用* @return*/@Overridepublic boolean isEnabled() {return true;} }至此,處理用戶信息獲取邏輯 部分完成了,主要實現 UserDetailsService 接口的 loadUserByname 方法。
為何會用到 SecurityUserDetail 類進行轉換一下?
其實完全可以直接返回一個 User 對象,但是需要注意的是,如果直接返回 User 對象的話,返回的是 security 包下的 user。
至于為何這樣處理,如果返回的是 security 包下的 user,這樣就失去了使用本地數據庫的意義,下方自定義登陸邏輯詳細說明。
再來登陸試一下:
其中 niceyoo、****** 為數據庫用戶信息,如下圖為成功跳轉:
3.2 處理用戶校驗邏輯
關于用戶的校驗邏輯主要包含兩方面:
前者,已經通過實現 UserDetailsService 的 loadUserByname() 方法實現了,接下來主要看看后者。
用戶密碼是否過期、是否被凍結等等需要實現 UserDetails 接口:
public interface UserDetails extends Serializable {Collection<? extends GrantedAuthority> getAuthorities();授權列表;String getPassword();從數據庫中查詢到的密碼;String getUsername();用戶輸入的用戶名;boolean isAccountNonExpired();當前賬戶是否過期;boolean isAccountNonLocked();賬戶是否被鎖定;boolean isCredentialsNonExpired();賬戶的認證時間是否過期;boolean isEnabled();是賬戶是否有效。 }主要看后四個方法:
1、isAccountNonExpired() 賬戶沒有過期 返回true 表示沒有過期
2、isAccountNonLocked() 賬戶沒有鎖定
3、isCredentialsNonExpired() 密碼是否過期
4、isEnabled() 是否被刪除
如上四個方法,皆可根據實際情況做響應處理。
3.3 處理密碼加密解密
再回到 WebSecurityConfig 自定義配置類。加入:
@Autowired private UserDetailsServiceImpl userDetailsService;@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());//加密 }配置了這個 configure 方法以后,從前端傳遞過來的密碼就會被加密,所以從數據庫查詢到的密碼必須是經過加密的,而這個過程都是在用戶注冊的時候進行加密的。
補充:UserDetailsServiceImpl 為自定義的 UserDetailsService 實現類。
四、個性化認證流程
同樣的在實際的開發中,對于用戶的登錄認證,不可能使用 Spring Security 自帶的方式或者頁面,需要自己定制適用于項目的登錄流程。
Spring Security 支持用戶在配置文件中配置自己的登錄頁面,如果用戶配置了,則采用用戶自己的頁面,否則采用模塊內置的登錄頁面。
WebSecurityConfig 配置類中增加 成功、失敗過濾器。
@Autowired private AuthenticationSuccessHandler successHandler;@Autowired private AuthenticationFailHandler failHandler;@Override protected void configure(HttpSecurity http) throws Exception {ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();registry.and()表單登錄方式.formLogin().permitAll()成功處理類.successHandler(successHandler)失敗.failureHandler(failHandler).and().logout().permitAll().and().authorizeRequests()任何請求.anyRequest()需要身份認證.authenticated().and()關閉跨站請求防護.csrf().disable()前后端分離采用JWT 不需要session.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); }在添加 AuthenticationSuccessHandler、AuthenticationFailHandler 后會幫我們自動導包,但是,既然是個性化認證流程,自然要我們自己去實現~
那我們究竟要實現什么效果呢?
自定義登陸成功處理:
自定義登陸失敗處理:
為何要采用這種返回新式?
用戶登錄成功后,Spring Security 的默認處理方式是跳轉到原來的鏈接上,這也是企業級開發的常見方式,但是有時候采用的是 Ajax 方式發送的請求,往往需要返回 Json 數據,如圖中:登陸成功后,會把 token 返回給前臺,失敗時則返回失敗信息。
AuthenticationSuccessHandler:
Slf4j @Component public class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {String username = ((UserDetails)authentication.getPrincipal()).getUsername();List<GrantedAuthority> authorities = (List<GrantedAuthority>) ((UserDetails)authentication.getPrincipal()).getAuthorities();List<String> list = new ArrayList<>();for(GrantedAuthority g : authorities){list.add(g.getAuthority());}登陸成功生成tokenString token = UUID.randomUUID().toString().replace("-", "");token 需要保存至服務器一份,實現方式:redis or jwt輸出到瀏覽器ResponseUtil.out(response, ResponseUtil.resultMap(true,200,"登錄成功", token));} }SavedRequestAwareAuthenticationSuccessHandle r是 Spring Security 默認的成功處理器,默認方式是跳轉。這里將認證信息作為 Json 數據進行了返回,也可以返回其他數據,這個是根據業務需求來定的,比如,上方代碼在用戶登陸成功后返回來 token,需要注意的是,此 token 需要在服務器備份一份,畢竟要用做下次的身份認證嘛~
AuthenticationFailHandler:
@Component public class AuthenticationFailHandler extends SimpleUrlAuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {## 默認情況下,不管你是用戶名不存在,密碼錯誤,SS 都會報出 Bad credentials 異常信息if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {ResponseUtil.out(response, ResponseUtil.resultMap(false,500,"用戶名或密碼錯誤"));} else if (e instanceof DisabledException) {ResponseUtil.out(response, ResponseUtil.resultMap(false,500,"賬戶被禁用,請聯系管理員"));} else {ResponseUtil.out(response, ResponseUtil.resultMap(false,500,"登錄失敗,其他內部錯誤"));}}}失敗處理器跟成功處理此雷同。
ResponseUtil:
@Slf4j public class ResponseUtil {/*** 使用response輸出JSON* @param response* @param resultMap*/public static void out(HttpServletResponse response, Map<String, Object> resultMap){ServletOutputStream out = null;try {response.setCharacterEncoding("UTF-8");response.setContentType("application/json;charset=UTF-8");out = response.getOutputStream();out.write(new Gson().toJson(resultMap).getBytes());} catch (Exception e) {log.error(e + "輸出JSON出錯");} finally{if(out!=null){try {out.flush();out.close();} catch (IOException e) {e.printStackTrace();}}}} }其中用到 gson 依賴:
<!-- Gson --> <dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId><version>2.8.5</version> </dependency>最后
下一篇將集成 jwt 實現用戶身份認證。
SpringSecurity 整合 JWT:https://www.cnblogs.com/niceyoo/p/10964277.html
總結
以上是生活随笔為你收集整理的项目集成Spring Security的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 面试必备:HashMap、Hashtab
- 下一篇: DDOS高防IP作用,哪些地方需要用到高