Spring Security是什么?
Spring Security 提供了基於javaEE的企業應有個你軟件全面的安全服務。這里特別強調支持使用SPring框架構件的項目,Spring框架是企業軟件開發javaEE方案的領導者。如果你還沒有使用Spring來開發企業應用程序,我們熱忱的鼓勵你仔細的看一看。熟悉Spring特別是一來注入原理兩幫助你更快更方便的使用Spring Security。中文文檔地址: https://www.springcloud.cc/spring-security-zhcn.html
人們使用Spring Secruity的原因有很多,單大部分都發現了javaEE的Servlet規范或EJB規范中的安全功能缺乏典型企業應用場景所需的深度。提到這些規范,重要的是要認識到他們在WAR或EAR級別無法移植。因此如果你更換服務器環境,這里有典型的大量工作去重新配置你的應用程序員安全到新的目標環境。使用Spring Security 解決了這些問題,也為你提供許多其他有用的,可定制的安全功能。正如你可能知道的兩個應用程序的兩個主要區域是“認證”和“授權”(或者訪問控制)。這兩個主要區域是Spring Security 的兩個目標。“認證”,是建立一個他聲明的主題的過程(一個“主體”一般是指用戶,設備或一些可以在你的應用程序中執行動作的其他系統)。“授權”指確定一個主體是否允許在你的應用程序執行一個動作的過程。為了抵達需要授權的店,主體的身份已經有認證過程建立。這個概念是通用的而不只在Spring Security中。在身份驗證層,Spring Security 的支持多種認證模式。這些驗證絕大多數都是要么由第三方提供,或由相關的標准組織,如互聯網工程任務組開發。另外Spring Security 提供自己的一組認證功能。具體而言,Spring Security 目前支持所有這些技術集成的身份驗證:
-
HTTP BASIC 認證頭 (基於 IETF RFC-based 標准)
-
HTTP Digest 認證頭 ( IETF RFC-based 標准)
-
HTTP X.509 客戶端證書交換 ( IETF RFC-based 標准)
-
LDAP (一個非常常見的方法來跨平台認證需要, 尤其是在大型環境)
-
Form-based authentication (用於簡單的用戶界面)
-
OpenID 認證
-
Authentication based on pre-established request headers (such as Computer Associates Siteminder) 根據預先建立的請求有進行驗證
-
JA-SIG Central Authentication Service (CAS,一個開源的SSO系統 )
-
............
很多獨立軟件供應商,因為靈活的身份驗證模式二選擇Spring Security。這樣做允許他們快速的集成到他們的終端客戶需求的解決方案而不用進行大量工程或者改變客戶的環境。如果上面的驗證機制不符合你的需求,Spring Security 是一個開放的平台,要實現你 自己的驗證機制檢查。Spring Security 的許多企業用戶需要與不遵循任何安全標准的“遺留”系統集成,Spring Security可以很好的與這類系統集成。無論何種身份驗證機制,Spring Security提供一套的授權功能。這里有三個主要的熱點區域,授權web請求、授權方法是否可以被調用和授權訪問單個域對象的實例。為了幫助讓你分別了解這些差異,認識在Servlet規范網絡模式安全的授權功能,EJB容器管理的安全性和文件系統的安全。
SpringBoot 集成 Security 實現自定義登錄:
那么接下去我將使用springboot 2.0.1 版本集成spring security。本文中部介紹默認的配置下的使用方式,直接自定義實現。
1.導入依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.相關配置:在Security 中給我們提供了一個配置適配器,供開發者來配置自定義的實現。配置全局屬性,這個類就是 WebSecurityConfigurerAdapter,我們只需要集成該類,實現對應方法即可:
@Configuration @EnableWebSecurity// 開啟Security
@EnableGlobalMethodSecurity(prePostEnabled = true)//開啟Spring方法級安全
public class SecurityConfig extends WebSecurityConfigurerAdapter { // Secutiry 處理鏈 // SecurityContextPersistenceFilter // --> UsernamePasswordAuthenticationFilter // --> BasicAuthenticationFilter // --> ExceptionTranslationFilter // --> FilterSecurityInterceptor
@Autowired private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler; @Autowired private MyAuthenticationProvider myAuthenticationProvider; @Autowired private MyAuthenctiationFailureHandler myAuthenctiationFailureHandler; @Autowired private MyUserDetailService myUserDetailService;//密碼加密
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } // 自定義認證配置
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(myAuthenticationProvider); } @Override protected void configure(HttpSecurity http) throws Exception { //關閉Security功能 // http.csrf().disable() // .authorizeRequests() // .anyRequest().permitAll() // .and().logout().permitAll();
http.authorizeRequests() .antMatchers("/wuzz/test4","/code/*").permitAll() //不需要保護的資源,可以多個
.antMatchers("/wuzz/**").authenticated()// 需要認證得資源,可以多個
.and() .formLogin().loginPage("http://localhost:8080/#/login")//自定義登陸地址
.loginProcessingUrl("/authentication/form") //登錄處理地址
.successHandler(myAuthenticationSuccessHandler) // 登陸成功處理器
.failureHandler(myAuthenctiationFailureHandler) // 登陸失敗處理器
.permitAll() .and() .userDetailsService(myUserDetailService)//設置userDetailsService,處理用戶信息 ; http.headers().cacheControl(); //禁用緩存
http.csrf().disable(); //禁用csrf校驗
}
//忽略的uri // @Override // public void configure(WebSecurity web) throws Exception { // web.ignoring() // .antMatchers( "/api/**", "/resources/**", "/static/**", "/public/**", "/webui/**", "/h2-console/**" // , "/configuration/**", "/swagger-resources/**", "/api-docs", "/api-docs/**", "/v2/api-docs/**" // , "/**/*.css", "/**/*.js","/**/*.ftl", "/**/*.png ", "/**/*.jpg", "/**/*.gif ", "/**/*.svg", "/**/*.ico", "/**/*.ttf", "/**/*.woff"); // } }
例如上面的配置,我們就完成了基本的自定義登錄的實現流程。可能有些小伙伴還不清理里面的配置是什么意思,那么我們接下去來說明一下關鍵的幾個接口。
- AuthenticationSuccessHandler :成功處理器,用於登陸成功后的處理。
- AuthenticationFailureHandler:失敗處理器,用於登陸失敗的處理。
- AuthenticationProvider :認證器,服務用戶登錄的認證。
- UserDetailsService:用戶信息的構建。
以上接口就是自定義登錄流程關鍵的四個。我們只需要實現他們指定的方法,編寫自己的業務邏輯,就可以實現自定義登錄認證流程。默認情況下,認證地址 loginPage 又默認的實現。loginProcessingUrl 是/login。默認用戶為user、密碼在啟動日志中由打印。但是在當下前后端分離盛行的情況下。我們都需要自定義登陸頁面。同手我們也希望登陸成功后能返回一些用戶信息的JSON串等等。所以我們這里還需要搞個頁面。但是頁面的請求一定要采用表單的方式登錄,不然會獲取不到信息導致登陸失敗。由於我這里重點介紹是Security。前端用的是 Vue,有興趣的同學可以參考 https://www.cnblogs.com/wuzhenzhao/category/1697454.html。裝好后需要安裝一些插件,配置網絡等等可以自行百度。下面是前端頁面的代碼
<template xmlns:v-on="http://www.w3.org/1999/xhtml">
<div id="app">
<div class="login-page">
<section class="login-contain">
<header>
<h1>后台管理系統</h1>
<p>management system</p>
</header>
<div class="form-content">
<ul>
<li>
<div class="form-group">
<label class="control-label">管理員賬號:</label>
<input type="text" placeholder="管理員賬號..." class="form-control form-underlined" v-model="username"/>
</div>
</li>
<li>
<div class="form-group">
<label class="control-label">管理員密碼:</label>
<input type="password" placeholder="管理員密碼..." class="form-control form-underlined" id="adminPwd" v-model="password"/>
</div>
</li>
<li>
<div class="form-group">
<label class="control-label">驗證碼</label>
<input type="text" placeholder="驗證碼..." class="form-control form-underlined" id="imageCode" v-model="imageCode"/>
<img src="http://localhost:8889/code/image">
</div>
</li>
<li>
<label class="check-box">
<input type="checkbox" name="remember" v-model="checked"/>
<span>記住賬號密碼</span>
</label>
</li>
<li>
<button class="btn btn-lg btn-block" id="entry" v-on:click="login">立即登錄</button>
</li>
<li>
<p class="btm-info">©Copyright 2017-2020 <a href="#" target="_blank" title="DeathGhost">wuzz</a></p>
<address class="btm-info">浙江省杭州市</address>
</li>
</ul>
</div>
</section>
<div class="mask"></div>
<div class="dialog">
<div class="dialog-hd">
<strong class="lt-title">標題</strong>
<a class="rt-operate icon-remove JclosePanel" title="關閉"></a>
</div>
<div class="dialog-bd">
<!--start::-->
<p>這里是基礎彈窗,可以定義文本信息,HTML信息這里是基礎彈窗,可以定義文本信息,HTML信息。</p>
<!--end::-->
</div>
<div class="dialog-ft">
<button class="btn btn-info JyesBtn">確認</button>
<button class="btn btn-secondary JnoBtn">關閉</button>
</div>
</div>
</div>
</div>
</template>
<script> import '../other/javascript/login.js' import qs from 'qs' export default { name: 'Login', data () { return { checked: false, msg: 'Welcome to Your Vue.js Home', username: '', password: '', imageCode: '' } }, methods: { login: function () { this.$axios({ method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, // 在請求之前對data傳參進行格式轉換
transformRequest: [function (data) { data = qs.stringify(data) return data }], url: '/api/authentication/form', data: { 'username': this.username, 'password': this.password, 'imageCode': this.imageCode, 'remember-me': this.checked } }).then(function (res) { alert(JSON.stringify(res.data)) }).catch(function (err) { alert(err) }) } } } </script>
對於代碼中驗證碼部分以及記住我部分會在后面的章節中說明,這里小伙伴們自己可以先注釋掉,避免出現其他錯誤。成果啟動后頁面如下:
3.AuthenticationSuccessHandler 實現:
@Configuration("myAuthenticationSuccessHandler") public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private Logger logger = LoggerFactory.getLogger(getClass()); @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { logger.info("登錄成功"); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(JSONObject.toJSONString(authentication)); } }
實現非常簡單,就是將認證信息通過JSon字符串的形式發送給前端,可以自定義發送內容。
4.AuthenticationFailureHandler 實現:
@Configuration("myAuthenctiationFailureHandler") public class MyAuthenctiationFailureHandler implements AuthenticationFailureHandler { private Logger logger = LoggerFactory.getLogger(getClass()); @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { logger.info("登錄失敗"); // 錯誤碼設置,這里先注釋掉。登陸失敗由前端處理 // response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(JSONObject.toJSONString(exception.getLocalizedMessage())); } }
這里的實現跟成功處理器一樣,登陸失敗后信息會被封裝到 AuthenticationException 類中。返回信息即可。
5.AuthenticationProvider 實現:
@Configuration("myAuthenticationProvider") public class MyAuthenticationProvider implements AuthenticationProvider { @Autowired private MyUserDetailService myUserDetailService; @Autowired private PasswordEncoder passwordEncoder; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String userName = authentication.getName();// 這個獲取表單輸入中的用戶名
String password = (String) authentication.getCredentials(); UserDetails userDetails = myUserDetailService.loadUserByUsername(userName); String encodePassword = passwordEncoder.encode(password); if (!passwordEncoder.matches(password,encodePassword)) { throw new UsernameNotFoundException("用戶名或者密碼不正確"); } Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities(); return new UsernamePasswordAuthenticationToken(userDetails, encodePassword, authorities); } @Override public boolean supports(Class<?> aClass) { return true; } }
這個類的實現就是通過 UserDetailService 返回的用戶信息進行跟表單中獲取的用戶信息進行比對,比如密碼。等等。
6.UserDetailsService 實現:
@Configuration("myUserDetailService") public class MyUserDetailService implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 根據用戶名查找用戶信息 //根據查找到的用戶信息判斷用戶是否被凍結
String password = passwordEncoder.encode("123456"); System.out.println("數據庫密碼是:" + password); return new User(username, password, true, true, true, true, AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); } }
這個實現主要是構造用戶信息。但是這里沒有連接數據庫,只有admin、密碼123456才能認證成功。
通過以上這些配置就可以實現自定義的登錄流程。
Spring Security 認證原理:
其核心就是一組過濾器鏈,項目啟動后將會自動配置。最核心的就是 Basic Authentication Filter 用來認證用戶的身份,一個在spring security中一種過濾器處理一種認證方式
其中綠色部分的每一種過濾器代表着一種認證方式,主要工作檢查當前請求有沒有關於用戶信息,如果當前的沒有,就會跳入到下一個綠色的過濾器中,請求成功會打標記。綠色認證方式可以配置,比如短信認證,微信。比如如果我們不配置BasicAuthenticationFilter的話,那么它就不會生效。
我們主要來看一下上述這些攔截器的 doFilter 方法的主要邏輯。
SecurityContextPersistenceFilter#doFilter:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; if (request.getAttribute(FILTER_APPLIED) != null) { // ensure that filter is only applied once per request // 確保每次請求只調用過一次
chain.doFilter(request, response); return; } final boolean debug = logger.isDebugEnabled(); request.setAttribute(FILTER_APPLIED, Boolean.TRUE); if (forceEagerSessionCreation) { HttpSession session = request.getSession(); if (debug && session.isNew()) { logger.debug("Eagerly created session: " + session.getId()); } } // 將 request/response 對象交給HttpRequestResponseHolder維持
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); //通過SecurityContextRepository接口的實現類裝載SecurityContext實例 //HttpSessionSecurityContextRepository將產生SecurityContext實例的任務交給SecurityContextHolder.createEmptyContext() //SecurityContextHolder再根據策略模式的不同, //把任務再交給相應策略類完成SecurityContext的創建 //如果沒有配置策略名稱,則默認為 //ThreadLocalSecurityContextHolderStrategy, //該類直接通過new SecurityContextImpl()創建實例
SecurityContext contextBeforeChainExecution = repo.loadContext(holder); try { //將產生的SecurityContext再通過SecurityContextHolder->ThreadLocalSecurityContextHolderStrategy設置到ThreadLocal
SecurityContextHolder.setContext(contextBeforeChainExecution); //繼續把請求流向下一個過濾器執行
chain.doFilter(holder.getRequest(), holder.getResponse()); } finally {//先從SecurityContextHolder獲取SecurityContext實例
SecurityContext contextAfterChainExecution = SecurityContextHolder .getContext(); // Crucial removal of SecurityContextHolder contents - do this before anything // else. //關鍵性地除去SecurityContextHolder內容 - 在任何事情之前執行此操作 //再把SecurityContext實例從SecurityContextHolder中清空 //若沒有清空,會受到服務器的線程池機制的影響 SecurityContextHolder.clearContext(); //將SecurityContext實例持久化到session中
repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse()); request.removeAttribute(FILTER_APPLIED); if (debug) { logger.debug("SecurityContextHolder now cleared, as request processing completed"); } } }
通過了上面的攔截器后,隨后的一個是登陸用戶密碼驗證過濾器 UsernamePasswordAuthenticationFilter ,這個可謂是非常重要的一個過濾器了,他的攔截方法定義在父類中:AbstractAuthenticationProcessingFilter#doFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; //判斷form-login標簽是否包含login-processing-url屬性 //如果沒有采用默認的url:j_spring_security_check //如果攔截的url不需要認證,直接跳過
if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } Authentication authResult; //。。。。
try { // 子類完成
authResult = attemptAuthentication(request, response); if (authResult == null) { // return immediately as subclass has indicated that it hasn't completed // authentication
return; } //session策略處理認證信息
sessionStrategy.onAuthentication(authResult, request, response); } // 異常處理 // Authentication success
if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } //認證成功處理 //1.向SecurityContext中設置Authentication認證信息 //2.如果有remember me服務,則查找請求參數中是否包含_spring_security_remember_me,如果該參數值為true、yes、on、1則執行remember me功能:添加cookie、入庫。為下次請求時自動登錄做准備 //3.發布認證成功事件 //4.執行跳轉
successfulAuthentication(request, response, chain, authResult); }
UsernamePasswordAuthenticationFilter #attemptAuthentication
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } // 獲取表單輸入的賬號密碼
String username = obtainUsername(request); String password = obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); // 構造token
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // Allow subclasses to set the "details" property
setDetails(request, authRequest); // 調用認證管理器進行認證
return this.getAuthenticationManager().authenticate(authRequest); }
這里默認是走 org.springframework.security.authentication.ProviderManager 認證:
public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; Authentication result = null; boolean debug = logger.isDebugEnabled(); // 獲取provisers集合 for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) {//判斷是否支持當前類型 continue; } if (debug) { logger.debug("Authentication attempt using "
+ provider.getClass().getName()); } try {
// 默認獲取到DaoAuthenticationProvider 進行認證 result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } //.......
// 如果拋出異常,則result為null,且如果沒有自定義的認證類,會直接返回異常信息
} if (result == null && parent != null) { // Allow the parent to try.
try {//調用parent認證,其實這里就會調到我們自定義的provider result = parent.authenticate(authentication); } catch (ProviderNotFoundException e) { // ignore as we will throw below if no other exception occurred prior to // calling parent and the parent // may throw ProviderNotFound even though a provider in the child already // handled the request
} catch (AuthenticationException e) { lastException = e; } } if (result != null) { if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { // Authentication is complete. Remove credentials and other secret data // from authentication
((CredentialsContainer) result).eraseCredentials(); } eventPublisher.publishAuthenticationSuccess(result); return result; } // Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) { lastException = new ProviderNotFoundException(messages.getMessage( "ProviderManager.providerNotFound", new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}")); } prepareException(lastException, authentication); throw lastException; }
以上這個方法進入會獲取到一個 DaoAuthenticationProvider ,調用的是父類 AbstractUserDetailsAuthenticationProvider 的 authenticate 方法
public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, messages.getMessage( "AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported")); // Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try {//調用自定義的 UserDetailsService獲取用戶信息
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } //。。。。。。異常捕獲
} try {//前置檢查,檢查的是userDetail 里面的 前3個boolean類型的屬性
preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } //。。。。。。異常捕獲 // 后置檢查,檢查剩下來的那個參數
postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } // 創建成功的認證信息,即登陸成功
return createSuccessAuthentication(principalToReturn, authentication, user); }
然后回到 AbstractAuthenticationProcessingFilter 類的 doFilter 方法,現在是認證成功了 ,會走 successfulAuthentication(request, response, chain, authResult);
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { if (logger.isDebugEnabled()) { logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult); } // 設置認證信息 SecurityContextHolder.getContext().setAuthentication(authResult); // 主要是處理是否啟用記住我功能 rememberMeServices.loginSuccess(request, response, authResult); // Fire event
if (this.eventPublisher != null) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( authResult, this.getClass())); } //調用自定義的成功處理器 successHandler.onAuthenticationSuccess(request, response, authResult); }
這樣就完成了認證流程。