以前寫的 塗塗影院管理系統 這個 demo 是基於 shiro 來鑑權的,項目先後端分離後,顯然集成 Spring Security 更加方便一些,畢竟,都用 Spring 了,權限管理固然 Spring Security.html
花了半天時間整理的筆記,但願能對你有所幫助。前端
Spring Security 一句話概述:一組 filter 過濾器鏈組成的權限認證。java
環境:項目採用 Spring Initializr 快速構建 Spring Boot ,版本交由 spring-boot-starter-parent 管理。redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
在僅僅添加完依賴的狀況下,啓動項目看看:spring
控制檯打印了一串密碼,以下圖所示:數據庫
訪問一下項目中的某個方法:json
http://localhost:7777/tmax/videoCategory/getAll
奇怪,怎麼本身跳到 /login 路徑下了,並且還讓登錄?後端
在登錄 from 表單裏輸入以下:瀏覽器
點擊 Sign in 而後跳轉到了目標地址:ruby
添加 Spring Security 依賴後,實際觸發了兩件事,一時將系統中全部的鏈接服務都保護起來, 再就是會有默認配置 form 表單認證。
Spring Security的整個工做流程以下所示:
綠色認證方式能夠配置, 橘黃色和藍色的位置不可更改。
Security 有兩種認證方式:
一樣,Security 也提供兩種過濾器類:
圖中橙色的 FilterSecurityInterceptor 是最終的過濾器,它會決定當前的請求可不能夠訪問Controller,判斷規則放在這個裏面。
當不經過時會把異常拋給在這個過濾器的前面的 ExceptionTranslationFilter 過濾器。
ExceptionTranslationFilter 接收到異常信息時,將跳轉頁面引導用戶進行認證,如上方所示的用戶登錄界面。
實際開發中是不可能使用上方 Spring Security 默認的這種方式的,如何去覆蓋掉 Spring Security 默認的配置呢?
咱們以:將默認的 form 認證方式改成 httpbasic 方式爲例。
建立SpringSecurity自定義配置類:WebSecurityConfig.java
@Slf4j
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected 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,那麼可不能夠自定義認證邏輯呢?好比採用數據庫中的用戶登錄?
答案是確定的。
接下來咱們來看一下這三步,而後實現自定義登錄:
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{
@Autowired
private UserService userService;
/**
* 從數據庫中獲取用戶信息,返回一個 UserDetails 對象,
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public 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());
}
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
理想型返回 admin 權限,可自已處理這塊
return AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
}
/**
* 帳戶是否過時
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 是否禁用
* @return
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 密碼是否過時
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 是否啓用
* @return
*/
@Override
public boolean isEnabled() {
return true;
}
}
至此,處理用戶信息獲取邏輯 部分完成了,主要實現 UserDetailsService 接口的 loadUserByname 方法。
爲什麼會用到 SecurityUserDetail 類進行轉換一下?
其實徹底能夠直接返回一個 User 對象,可是須要注意的是,若是直接返回 User 對象的話,返回的是 security 包下的 user。
至於爲什麼這樣處理,若是返回的是 security 包下的 user,這樣就失去了使用本地數據庫的意義,下方自定義登錄邏輯詳細說明。
再來登錄試一下:
其中 niceyoo、 爲數據庫用戶信息,以下圖爲成功跳轉:
關於用戶的校驗邏輯主要包含兩方面:
前者,已經經過實現 UserDetailsService 的 loadUserByname() 方法實現了,接下來主要看看後者。
用戶密碼是否過時、是否被凍結等等須要實現 UserDetails 接口:
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();受權列表;
String getPassword();從數據庫中查詢到的密碼;
String getUsername();用戶輸入的用戶名;
boolean isAccountNonExpired();當前帳戶是否過時;
boolean isAccountNonLocked();帳戶是否被鎖定;
boolean isCredentialsNonExpired();帳戶的認證時間是否過時;
boolean isEnabled();是帳戶是否有效。
}
主要看後四個方法:
一、isAccountNonExpired() 帳戶沒有過時 返回true 表示沒有過時
二、isAccountNonLocked() 帳戶沒有鎖定
三、isCredentialsNonExpired() 密碼是否過時
四、isEnabled() 是否被刪除
如上四個方法,皆可根據實際狀況作響應處理。
再回到 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 {
@Override
public 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());
}
登錄成功生成token
String 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 {
@Override
public 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 實現用戶身份認證。