javascript
无状态Spring安全性第2部分:无状态身份验证
Spring Stateless Security系列的第二部分是關于以無狀態方式探索身份驗證的方法。 如果您錯過了CSRF的第一部分,可以在這里找到。
因此,在談論身份驗證時,其全部內容就是讓客戶端以可驗證的方式向服務器標識自己。 通常,這始于服務器向客戶端提供挑戰,例如要求填寫用戶名/密碼的請求。 今天,我想著重介紹在通過此類初始(手動)挑戰后會發生什么情況,以及如何處理其他HTTP請求的自動重新身份驗證。
常用方法
基于會話Cookie
我們可能最了解的最常見方法是使用服務器生成的JSESSIONID cookie形式的秘密令牌(會話密鑰)。 這些天的初始設置幾乎沒有用,也許會讓您忘記,您有一個選擇要放在這里。 即使沒有進一步使用此“會話密鑰”來存儲“會話中”的任何其他狀態,該密鑰本身實際上也是狀態 。 即,如果沒有這些密鑰的共享和持久存儲,則成功的身份驗證將無法在服務器重新啟動或請求負載平衡到另一臺服務器后繼續存在。
OAuth2 / API密鑰
每當談論REST API和安全性時; 提到了OAuth2和其他類型的API密鑰。 基本上,它們涉及在HTTP授權標頭中發送自定義令牌/密鑰。 如果使用得當,兩種方法都可以避免客戶端使用標頭來處理Cookie。 這解決了CSRF漏洞和其他Cookie相關問題。 但是,他們無法解決的一件事是服務器需要檢查顯示的身份驗證密鑰,幾乎需要一些持久且可維護的共享存儲來將密鑰鏈接到用戶/授權。
無狀態方法
1. HTTP基礎認證
處理認證的最古老,最粗糙的方式。 只需讓用戶隨每個請求發送其用戶名/密碼。 這聽起來似乎很可怕,但是考慮到上述任何方法也都通過網??絡發送秘密密鑰,這實際上并不是那么安全。 主要是用戶體驗和靈活性,這使得其他方法成為更好的選擇。
2.服務器簽名的令牌
以無狀態方式處理請求中的狀態的一個巧妙小技巧是讓服務器對其“簽名”。 然后可以在每個請求之間在客戶端/服務器之間來回傳輸該請求,并確保它不會受到限制。 這樣,任何用戶標識數據都可以以純文本形式共享,并為其添加特殊的簽名哈希。 考慮到已簽名,服務器可以簡單地驗證簽名哈希是否仍與接收到的內容匹配,而無需保持任何服務器端狀態。
可以用于此目的的通用標準是JSON Web令牌 (JWT),該標準仍在起草中。 對于本博客文章,我想擺脫困境,跳過完全的合規性以及使用附帶的庫的尖叫聲。 從中挑選我們真正需要的東西。 (省略了標頭/變量哈希算法和url-safe base64編碼)
實作
如前所述,我們將使用Spring Security和Spring Boot將自己的實現整合在一起。 沒有任何庫或精美的API會混淆令牌級別上真正發生的事情。 令牌在偽代碼中看起來像這樣:
content = toJSON(user_details) token = BASE64(content) + "." + BASE64(HMAC(content))令牌中的點用作分隔符,因此每個部分都可以分別標識和解碼,因為點字符不是任何base64編碼字符串的一部分。 HMAC代表基于哈希的消息身份驗證代碼,它基本上是使用預定義密鑰從任何數據中生成的哈希。
在實際的Java中,令牌的生成與偽代碼非常相似:
創建令牌
public String createTokenForUser(User user) {byte[] userBytes = toJSON(user);byte[] hash = createHmac(userBytes);final StringBuilder sb = new StringBuilder(170);sb.append(toBase64(userBytes));sb.append(SEPARATOR);sb.append(toBase64(hash));return sb.toString(); }JSON中使用的相關User屬性是id,username,expires和role ,但可以是您真正想要的任何東西。 我標記了杰克遜JSON序列化期間將忽略的User對象的“ password”屬性,因此它不會成為令牌的一部分:
忽略密碼
@JsonIgnore public String getPassword() {return password; }對于現實世界的場景,您可能只想為此使用專用對象。
通過一些輸入驗證來防止/捕獲由于對令牌進行調整而導致的解析錯誤,令牌的解碼會稍微復雜一些:
解碼令牌
public User parseUserFromToken(String token) {final String[] parts = token.split(SEPARATOR_SPLITTER);if (parts.length == 2 && parts[0].length() > 0 && parts[1].length() > 0) {try {final byte[] userBytes = fromBase64(parts[0]);final byte[] hash = fromBase64(parts[1]);boolean validHash = Arrays.equals(createHmac(userBytes), hash);if (validHash) {final User user = fromJSON(userBytes);if (new Date().getTime() < user.getExpires()) {return user;}}} catch (IllegalArgumentException e) {//log tampering attempt here}}return null; }它本質上驗證提供的哈希值是否與內容的新計算哈希值相同。 因為createHmac方法在內部使用未公開的秘密密鑰來計算哈希,所以沒有客戶端能夠調整內容并提供與服務器生成的哈希相同的哈希。 僅在通過此測試后,提供的數據才會被解釋為表示User對象的JSON。
放大Hmac部分,讓我們看一下所涉及的Java。 首先,必須使用一個私鑰對其進行初始化,這是TokenHandler的構造函數的一部分:
HMAC初始化
... private static final String HMAC_ALGO = "HmacSHA256";private final Mac hmac;public TokenHandler(byte[] secretKey) {try {hmac = Mac.getInstance(HMAC_ALGO);hmac.init(new SecretKeySpec(secretKey, HMAC_ALGO));} catch (NoSuchAlgorithmException | InvalidKeyException e) {throw new IllegalStateException("failed to initialize HMAC: " + e.getMessage(), e);} } ...初始化后,可以使用一個方法調用(重新)使用它! (doFinal的JavaDoc讀取“處理給定的字節數組并完成MAC操作。對該方法的調用會將這個Mac對象重置為先前通過調用init(Key)或init(Key,AlgorithmParameterSpec進行初始化)時所處的狀態。 …”)
createHmac
// synchronized to guard internal hmac object private synchronized byte[] createHmac(byte[] content) {return hmac.doFinal(content); }我在這里使用了一些粗略的同步,以防止在Spring Singleton Service中使用時發生沖突。 實際的方法非常快(?0.01ms),因此除非您每臺服務器每秒要發送10k +請求,否則它不會造成任何問題。
說到服務,讓我們一路攀升到完全可運行的基于令牌的身份驗證服務:
令牌認證服務
@Service public class TokenAuthenticationService {private static final String AUTH_HEADER_NAME = "X-AUTH-TOKEN";private static final long TEN_DAYS = 1000 * 60 * 60 * 24 * 10;private final TokenHandler tokenHandler;@Autowiredpublic TokenAuthenticationService(@Value("${token.secret}") String secret) {tokenHandler = new TokenHandler(DatatypeConverter.parseBase64Binary(secret));}public void addAuthentication(HttpServletResponse response, UserAuthentication authentication) {final User user = authentication.getDetails();user.setExpires(System.currentTimeMillis() + TEN_DAYS);response.addHeader(AUTH_HEADER_NAME, tokenHandler.createTokenForUser(user));}public Authentication getAuthentication(HttpServletRequest request) {final String token = request.getHeader(AUTH_HEADER_NAME);if (token != null) {final User user = tokenHandler.parseUserFromToken(token);if (user != null) {return new UserAuthentication(user);}}return null;} } 很簡單,初始化一個私有TokenHandler來完成繁重的工作。 它提供了添加和讀取自定義HTTP令牌標頭的方法。 如您所見,它不使用任何(數據庫驅動的)UserDetailsS??ervice查找用戶詳細信息。 通過令牌提供了讓Spring Security處理進一步的授權檢查所需的所有詳細信息。
最后,我們現在可以將所有這些插件插入到Spring Security中,在Security配置中添加兩個自定義過濾器:
StatelessAuthenticationSecurityConfig內部的安全配置
... @Override protected void configure(HttpSecurity http) throws Exception {http...// custom JSON based authentication by POST of // {"username":"<name>","password":"<password>"} // which sets the token header upon authentication.addFilterBefore(new StatelessLoginFilter("/api/login", ...), UsernamePasswordAuthenticationFilter.class)// custom Token based authentication based on // the header previously given to the client.addFilterBefore(new StatelessAuthenticationFilter(...), UsernamePasswordAuthenticationFilter.class); } ...StatelessLoginFilter在成功認證后添加令牌:
StatelessLoginFilter
... @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,FilterChain chain, Authentication authentication) throws IOException, ServletException {// Lookup the complete User object from the database and create an Authentication for itfinal User authenticatedUser = userDetailsService.loadUserByUsername(authentication.getName());final UserAuthentication userAuthentication = new UserAuthentication(authenticatedUser);// Add the custom token as HTTP header to the responsetokenAuthenticationService.addAuthentication(response, userAuthentication);// Add the authentication to the Security contextSecurityContextHolder.getContext().setAuthentication(userAuthentication); } ...StatelessAuthenticationFilter僅根據標頭設置身份驗證:
StatelessAuthenticationFilter
... @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {SecurityContextHolder.getContext().setAuthentication(tokenAuthenticationService.getAuthentication((HttpServletRequest) req));chain.doFilter(req, res); // always continue } ...請注意,與大多數與Spring Security相關的過濾器不同,無論身份驗證成功如何,我都選擇繼續沿過濾器鏈向下移動。 我想支持觸發Spring的AnonymousAuthenticationFilter以支持匿名身份驗證。 這里最大的區別是過濾器未配置為映射到任何專門用于身份驗證的URL,因此不提供標頭并不是真正的問題。
客戶端實施
客戶端實現同樣非常簡單。 再次,我將其保持為最低限度,以防止在AngularJS詳細信息中丟失身份驗證位。 如果您正在尋找一個更完整地與路由集成的AngularJS JWT示例,則應在此處查看 。 我從中借用了一些攔截器邏輯。
登錄只需存儲令牌(在localStorage中 ):
登錄
$scope.login = function () {var credentials = { username: $scope.username, password: $scope.password };$http.post('/api/login', credentials).success(function (result, status, headers) {$scope.authenticated = true;TokenStorage.store(headers('X-AUTH-TOKEN'));}); };注銷甚至更簡單(無需調用服務器):
登出
$scope.logout = function () {// Just clear the local storageTokenStorage.clear(); $scope.authenticated = false; };要檢查用戶是否“已經登錄”,ng-init =“ init()”可以很好地工作:
在里面
$scope.init = function () {$http.get('/api/users/current').success(function (user) {if(user.username !== 'anonymousUser'){$scope.authenticated = true;$scope.username = user.username;}}); };我選擇使用匿名可訪問的端點來防止觸發401/403。 您也可以解碼令牌本身并檢查到期時間,并相信本地客戶端時間足夠準確。
最后,為了使添加標頭的過程自動化,就像上一個博客條目中那樣,一個簡單的攔截器很好地做到了:
令牌驗證攔截器
factory('TokenAuthInterceptor', function($q, TokenStorage) {return {request: function(config) {var authToken = TokenStorage.retrieve();if (authToken) {config.headers['X-AUTH-TOKEN'] = authToken;}return config;},responseError: function(error) {if (error.status === 401 || error.status === 403) {TokenStorage.clear();}return $q.reject(error);}}; }).config(function($httpProvider) {$httpProvider.interceptors.push('TokenAuthInterceptor'); });假設客戶端不會允許調用需要更高特權的區域,它還會照顧到在收到HTTP 401或403之后自動清除令牌的情況。
令牌存儲
TokenStorage只是對localStorage的包裝服務,我不會打擾您。 將令牌放入localStorage可以防止腳本像保存cookie一樣在保存腳本的腳本源之外讀取腳本。 但是,由于令牌不是實際的Cookie,因此無法指示任何瀏覽器將其自動添加到請求中。 這是至關重要的,因為它可以完全防止任何形式的CSRF攻擊。 因此,您不必實施我以前的博客中提到的任何(無狀態)CSRF保護。
- 您可以在github上找到一個完整的工作示例,其中包含一些不錯的功能。
確保已安裝gradle 2.0,并使用“ gradle build”和“ gradle run”簡單地運行它。 如果要像Eclipse一樣在IDE中使用它,請使用“ gradle eclipse”,只需從IDE內導入并運行它即可(無需服務器)。
翻譯自: https://www.javacodegeeks.com/2014/10/stateless-spring-security-part-2-stateless-authentication.html
總結
以上是生活随笔為你收集整理的无状态Spring安全性第2部分:无状态身份验证的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ps布尔快捷键(ps布尔工具)
- 下一篇: 4个万无一失的技巧让您开始使用JBoss