Spring Security 實戰乾貨:使用 JWT 認證訪問接口

(轉載)原文連接:https://my.oschina.net/10000000000/blog/3127268

1. 前言

歡迎閱讀Spring Security 實戰乾貨系列。以前我講解了如何編寫一個本身的 Jwt 生成器以及如何在用戶認證經過後返回 Json Web Token 。今天咱們來看看如何在請求中使用 Jwt 訪問鑑權。DEMO 獲取方法在文末。html

2. 經常使用的 Http 認證方式

咱們要在 Http 請求中使用 Jwt 咱們就必須瞭解 常見的 Http 認證方式。java

2.1 HTTP Basic Authentication

HTTP Basic Authentication 又叫基礎認證,它簡單地使用 Base64 算法對用戶名、密碼進行加密,並將加密後的信息放在請求頭 Header 中,本質上仍是明文傳輸用戶名、密碼,並不安全,因此最好在 Https 環境下使用。其認證流程以下:web

basic.png

客戶端發起 GET 請求 服務端響應返回 401 Unauthorized, www-Authenticate 指定認證算法,realm 指定安全域。而後客戶端通常會彈窗提示輸入用戶名稱和密碼,輸入用戶名密碼後放入 Header 再次請求,服務端認證成功後以 200 狀態碼響應客戶端。算法

2.2 HTTP Digest Authentication

爲彌補 BASIC 認證存在的弱點就有了 HTTP Digest Authentication 。它又叫摘要認證。它使用隨機數加上 MD5 算法來對用戶名、密碼進行摘要編碼,流程相似 Http Basic Authentication ,可是更加複雜一些:spring

步驟1:跟基礎認證同樣,只不過返回帶 WWW-Authenticate 首部字段的響應。該字段內包含質問響應方式認證所須要的臨時諮詢碼(隨機數,nonce)。 首部字段 WWW-Authenticate 內必須包含 realm 和 nonce 這兩個字段的信息。客戶端就是依靠向服務器回送這兩個值進行認證的。nonce 是一種每次隨返回的 401 響應生成的任意隨機字符串。該字符串一般推薦由 Base64 編碼的十六進制數的組成形式,但實際內容依賴服務器的具體實現json

步驟2:接收到 401 狀態碼的客戶端,返回的響應中包含 DIGEST 認證必須的首部字段 Authorization 信息。首部字段 Authorization 內必須包含 username、realm、nonce、uri 和 response 的字段信息,其中,realm 和 nonce 就是以前從服務器接收到的響應中的字段。瀏覽器

步驟3:接收到包含首部字段 Authorization 請求的服務器,會確認認證信息的正確性。認證經過後則會返回包含 Request-URI 資源的響應。緩存

而且這時會在首部字段 Authorization-Info 寫入一些認證成功的相關信息。安全

2.3 SSL 客戶端認證

SSL 客戶端認證就是一般咱們說的 HTTPS 。安全級別較高,但須要承擔 CA 證書費用。SSL 認證過程當中涉及到一些重要的概念,數字證書機構的公鑰、證書的私鑰和公鑰、非對稱算法(配合證書的私鑰和公鑰使用)、對稱密鑰、對稱算法(配合對稱密鑰使用)。相對複雜一些這裏不過多講述。服務器

2.4 Form 表單認證

Form 表單的認證方式並非HTTP規範。因此實現方式也呈現多樣化,其實咱們日常的掃碼登陸,手機驗證碼登陸都屬於表單登陸的範疇。表單認證通常都會配合 Cookie,Session 的使用,如今不少 Web 站點都使用此認證方式。用戶在登陸頁中填寫用戶名和密碼,服務端認證經過後會將 sessionId 返回給瀏覽器端,瀏覽器會保存 sessionId 到瀏覽器的 Cookie 中。由於 HTTP 是無狀態的,因此瀏覽器使用 Cookie 來保存 sessionId。下次客戶端會在發送的請求中會攜帶 sessionId 值,服務端發現 sessionId 存在並以此爲索引獲取用戶存在服務端的認證信息進行認證操做。認證過則會提供資源訪問。

咱們在Spring Security 實戰乾貨:登陸後返回 JWT Token 一文其實也是經過 Form 提交來獲取 Jwt 其實 Jwt 跟 sessionId 一樣的做用,只不過 Jwt 自然攜帶了用戶的一些信息,而 sessionId 須要去進一步獲取用戶信息。

2.5 Json Web Token 的認證方式 Bearer Authentication

咱們經過表單認證獲取 Json Web Token ,那麼如何使用它呢? 一般咱們會把 Jwt 做爲令牌使用 Bearer Authentication 方式使用。Bearer Authentication 是一種基於令牌的 HTTP 身份驗證方案,用戶向服務器請求訪問受限資源時,會攜帶一個 Token 做爲憑證,檢驗經過則能夠訪問特定的資源。最初是在 RFC 6750 中做爲 OAuth 2.0 的一部分,但有時也能夠單獨使用。 咱們在使用 Bear Token 的方法是在請求頭的 Authorization 字段中放入 Bearer <token> 的格式的加密串(Json Web Token)。請注意 Bearer 前綴與 Token 之間有一個空字符位,與基自己份驗證相似,Bearer Authentication 只能在HTTPS(SSL)上使用。

3. Spring Security 中實現接口 Jwt 認證

接下來咱們是咱們該系列的重頭戲 ———— 接口的 Jwt 認證。

3.1 定義 Json Web Token 過濾器

不管上面提到的哪一種認證方式,咱們均可以使用 Spring Security 中的 Filter 來處理。 Spring Security 默認的基礎配置沒有提供對 Bearer Authentication 處理的過濾器, 可是提供了處理 Basic Authentication 的過濾器:

org.springframework.security.web.authentication.www.BasicAuthenticationFilter

BasicAuthenticationFilter 繼承了 OncePerRequestFilter 。因此咱們也模仿 BasicAuthenticationFilter 來實現本身的 JwtAuthenticationFilter 。 完整代碼以下:

package cn.felord.spring.security.filter; import cn.felord.spring.security.exception.SimpleAuthenticationEntryPoint; import cn.felord.spring.security.jwt.JwtTokenGenerator; import cn.felord.spring.security.jwt.JwtTokenPair; import cn.felord.spring.security.jwt.JwtTokenStorage; import cn.hutool.json.JSONArray; import cn.hutool.json.JSONObject; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.CredentialsExpiredException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.User; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; import java.util.Objects; /**  * jwt 認證攔截器 用於攔截 請求 提取jwt 認證  *  * @author dax  * @since 2019/11/7 23:02  */ @Slf4j public class JwtAuthenticationFilter extends OncePerRequestFilter {     private static final String AUTHENTICATION_PREFIX = "Bearer ";     /**      * 認證若是失敗由該端點進行響應      */     private AuthenticationEntryPoint authenticationEntryPoint = new SimpleAuthenticationEntryPoint();     private JwtTokenGenerator jwtTokenGenerator;     private JwtTokenStorage jwtTokenStorage;     public JwtAuthenticationFilter(JwtTokenGenerator jwtTokenGenerator, JwtTokenStorage jwtTokenStorage) {         this.jwtTokenGenerator = jwtTokenGenerator;         this.jwtTokenStorage = jwtTokenStorage;     }     @Override     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {         // 若是已經經過認證         if (SecurityContextHolder.getContext().getAuthentication() != null) {             chain.doFilter(request, response);             return;         }         // 獲取 header 解析出 jwt 並進行認證 無token 直接進入下一個過濾器  由於  SecurityContext 的緣故 若是無權限並不會放行         String header = request.getHeader(HttpHeaders.AUTHORIZATION);         if (StringUtils.hasText(header) && header.startsWith(AUTHENTICATION_PREFIX)) {             String jwtToken = header.replace(AUTHENTICATION_PREFIX, "");             if (StringUtils.hasText(jwtToken)) {                 try {                     authenticationTokenHandle(jwtToken, request);                 } catch (AuthenticationException e) {                     authenticationEntryPoint.commence(request, response, e);                 }             } else {                 // 帶安全頭 沒有帶token                 authenticationEntryPoint.commence(request, response, new AuthenticationCredentialsNotFoundException("token is not found"));             }         }         chain.doFilter(request, response);     }     /**      * 具體的認證方法  匿名訪問不要攜帶token      * 有些邏輯本身補充 這裏只作基本功能的實現      *      * @param jwtToken jwt token      * @param request  request      */     private void authenticationTokenHandle(String jwtToken, HttpServletRequest request) throws AuthenticationException {         // 根據個人實現 有效token纔會被解析出來         JSONObject jsonObject = jwtTokenGenerator.decodeAndVerify(jwtToken);         if (Objects.nonNull(jsonObject)) {             String username = jsonObject.getStr("aud");             // 從緩存獲取 token             JwtTokenPair jwtTokenPair = jwtTokenStorage.get(username);             if (Objects.isNull(jwtTokenPair)) {                 if (log.isDebugEnabled()) {                     log.debug("token : {}  is  not in cache", jwtToken);                 }                 // 緩存中不存在就算 失敗了                 throw new CredentialsExpiredException("token is not in cache");             }             String accessToken = jwtTokenPair.getAccessToken();             if (jwtToken.equals(accessToken)) {                   // 解析 權限集合  這裏                 JSONArray jsonArray = jsonObject.getJSONArray("roles");                 String roles = jsonArray.toString();                 List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(roles);                 User user = new User(username, "[PROTECTED]", authorities);                 // 構建用戶認證token                 UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(user, null, authorities);                 usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));                 // 放入安全上下文中                 SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);             } else {                 // token 不匹配                 if (log.isDebugEnabled()){                     log.debug("token : {}  is  not in matched", jwtToken);                 }                 throw new BadCredentialsException("token is not matched");             }         } else {             if (log.isDebugEnabled()) {                 log.debug("token : {}  is  invalid", jwtToken);             }             throw new BadCredentialsException("token is invalid");         }     } }

具體看代碼註釋部分,邏輯有些地方根據你業務進行調整。匿名訪問必然是不能帶 Token 的!

3.2 配置 JwtAuthenticationFilter

首先將過濾器 JwtAuthenticationFilter 注入 Spring IoC 容器 ,而後必定要將 JwtAuthenticationFilter 順序置於 UsernamePasswordAuthenticationFilter 以前:

       @Override         protected void configure(HttpSecurity http) throws Exception {             http.csrf().disable()                     .cors()                     .and()                     // session 生成策略用無狀態策略                     .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)                     .and()                     .exceptionHandling().accessDeniedHandler(new SimpleAccessDeniedHandler()).authenticationEntryPoint(new SimpleAuthenticationEntryPoint())                     .and()                     .authorizeRequests().anyRequest().authenticated()                     .and()                     .addFilterBefore(preLoginFilter, UsernamePasswordAuthenticationFilter.class)                     // jwt 必須配置於 UsernamePasswordAuthenticationFilter 以前                     .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)                     // 登陸  成功後返回jwt token  失敗後返回 錯誤信息                     .formLogin().loginProcessingUrl(LOGIN_PROCESSING_URL).successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler)                     .and().logout().addLogoutHandler(new CustomLogoutHandler()).logoutSuccessHandler(new CustomLogoutSuccessHandler());         }

4. 使用 Jwt 進行請求驗證

編寫一個受限接口 ,咱們這裏是 http://localhost:8080/foo/test 。直接請求會被 401 。 咱們經過下圖方式獲取 Token :

而後在 Postman 中使用 Jwt :

最終會認證成功並訪問到資源。

5. 刷新 Jwt Token

咱們在 Spring Security 實戰乾貨:手把手教你實現JWT Token 中已經實現了 Json Web Token 都是成對出現的邏輯。accessToken 用來接口請求, refreshToken 用來刷新 accessToken 。咱們能夠一樣定義一個 Filter 可參照 上面的 JwtAuthenticationFilter 。只不過 此次請求攜帶的是 refreshToken,咱們在過濾器中攔截 URI跟咱們定義的刷新端點進行匹配。一樣驗證 Token ,經過後像登陸成功同樣返回 Token 對便可。這裏再也不進行代碼演示。

原文連接:https://my.oschina.net/10000000000/blog/3127268

6. 最後

若是文章對您有幫助,關注一下⬇️再走吧!

公衆號.png

相關文章
相關標籤/搜索