1. 程式人生 > >一起來學SpringCloud之

一起來學SpringCloud之

上一篇已經講了微服務元件中的 路由閘道器(Zuul),但是未介紹服務認證相關,本章主要講解基於Spring SecurityJJWT 實現 JWT(JSON Web Token)為介面做授權處理…

- JWT

JWT(JSON Web Token), 是為了在網路應用環境間傳遞宣告而執行的一種基於JSON的開放標準((RFC 7519).該token被設計為緊湊且安全的,特別適用於分散式站點的單點登入(SSO)場景。JWT的宣告一般被用來在身份提供者和服務提供者間傳遞被認證的使用者身份資訊,以便於從資源伺服器獲取資源,也可以增加一些額外的其它業務邏輯所必須的宣告資訊,該token也可直接被用於認證,也可被加密。

- JWT與其它的區別

通常情況下,把API直接暴露出去是風險很大的,不說別的,直接被機器攻擊就喝一壺的。那麼一般來說,對API要劃分出一定的許可權級別,然後做一個使用者的鑑權,依據鑑權結果給予使用者開放對應的API。目前,比較主流的方案有幾種:

OAuth

OAuth(開放授權)是一個開放的授權標準,允許使用者讓第三方應用訪問該使用者在某一服務上儲存的私密的資源(如照片,視訊),而無需將使用者名稱和密碼提供給第三方應用。

OAuth 允許使用者提供一個令牌,而不是使用者名稱和密碼來訪問他們存放在特定服務提供者的資料。每一個令牌授權一個特定的第三方系統(例如,視訊編輯網站)在特定的時段(例如,接下來的2小時內)內訪問特定的資源(例如僅僅是某一相簿中的視訊)。這樣,OAuth讓使用者可以授權第三方網站訪問他們儲存在另外服務提供者的某些特定資訊,而非所有內容

Cookie/Session Auth

Cookie認證機制就是為一次請求認證在服務端建立一個Session物件,同時在客戶端的瀏覽器端建立了一個Cookie物件;通過客戶端帶上來Cookie物件來與伺服器端的session物件匹配來實現狀態管理的。預設的,當我們關閉瀏覽器的時候,cookie會被刪除。但可以通過修改cookie 的expire time使cookie在一定時間內有效,基於session方式認證勢必會對伺服器造成一定的壓力(記憶體儲存),不易於擴充套件(需要處理分散式session),跨站請求偽造的攻擊(CSRF)

- JWT的優點

1.相比於session,它無需儲存在伺服器,不佔用伺服器記憶體開銷。

2.無狀態、可拓展性強:比如有3臺機器(A、B、C)組成伺服器叢集,若session存在機器A上,session只能儲存在其中一臺伺服器,此時你便不能訪問機器B、C,因為B、C上沒有存放該Session,而使用token就能夠驗證使用者請求合法性,並且我再加幾臺機器也沒事,所以可拓展性好就是這個意思。

3.前後端分離,支援跨域訪問。

- JWT的組成

{ "iss": "JWT Builder", 
  "iat": 1416797419, 
  "exp": 1448333419, 
  "aud": "www.battcn.com", 
  "sub": "[email protected]", 
  "GivenName": "Levin", 
  "Surname": "Levin", 
  "Email": "[email protected]", 
  "Role": [ "ADMIN", "MEMBER" ] 
}
  • iss: 該JWT的簽發者,是否使用是可選的;
  • sub: 該JWT所面向的使用者,是否使用是可選的;
  • aud: 接收該JWT的一方,是否使用是可選的;
  • exp(expires): 什麼時候過期,這裡是一個Unix時間戳,是否使用是可選的;
  • iat(issued at): 在什麼時候簽發的(UNIX時間),是否使用是可選的;
  • nbf (Not Before):如果當前時間在nbf裡的時間之前,則Token不被接受;一般都會留一些餘地,比如幾分鐘;,是否使用是可選的;

JWT生成器

一個JWT實際上就是一個字串,它由三部分組成,頭部、載荷、簽名(上圖依次排序)

- 認證

互動圖

- 登陸認證

  • 客戶端傳送 POST 請求到伺服器,提交登入處理的Controller層
  • 呼叫認證服務進行使用者名稱密碼認證,如果認證通過,返回完整的使用者資訊及對應許可權資訊
  • 利用 JJWT 對使用者、許可權資訊、祕鑰構建Token
  • 返回構建好的Token

構建結果

- 請求認證

  • 客戶端向伺服器請求,服務端讀取請求頭資訊(request.header)獲取Token
  • 如果找到Token資訊,則根據配置檔案中的簽名加密祕鑰,呼叫JJWT Lib對Token資訊進行解密和解碼;
  • 完成解碼並驗證簽名通過後,對Token中的exp、nbf、aud等資訊進行驗證;
  • 全部通過後,根據獲取的使用者的角色許可權資訊,進行對請求的資源的許可權邏輯判斷;
  • 如果許可權邏輯判斷通過則通過Response物件返回;否則則返回HTTP 401;

無效Token

無效Token

有效Token

有效Token請求

- JWT的缺點

有優點就會有缺點,是否適用應該考慮清楚,而不是技術跟風

  • token過大容易佔用更多的空間
  • token中不應該儲存敏感資訊
  • JWT不是 session ,勿將token當session
  • 無法作廢已頒佈的令牌,因為所有的認證資訊都在JWT中,由於在服務端沒有狀態,即使你知道了某個JWT被盜取了,你也沒有辦法將其作廢。在JWT過期之前(你絕對應該設定過期時間),你無能為力。
  • 類似快取,由於無法作廢已頒佈的令牌,在其過期前,你只能忍受”過期”的資料(自己放出去的token,含著淚也要用到底)。

- 程式碼(片段)

TokenProperties 與 application.yml資源的key對映,方便使用

@Configuration
@ConfigurationProperties(prefix = "battcn.security.token")
public class TokenProperties {
    /**
     * {@link com.battcn.security.model.token.Token} token的過期時間
     */
    private Integer expirationTime;

    /**
     * 發行人
     */
    private String issuer;

    /**
     * 使用的簽名KEY {@link com.battcn.security.model.token.Token}.
     */
    private String signingKey;

    /**
     * {@link com.battcn.security.model.token.Token} 重新整理過期時間
     */
    private Integer refreshExpTime;

    // get set ...
}

Token生成的類

@Component
public class TokenFactory {

    private final TokenProperties properties;

    @Autowired
    public TokenFactory(TokenProperties properties) {
        this.properties = properties;
    }

    /**
     * 利用JJWT 生成 Token
     * @param context
     * @return
     */
    public AccessToken createAccessToken(UserContext context) {
        Optional.ofNullable(context.getUsername()).orElseThrow(()-> new IllegalArgumentException("Cannot create Token without username"));
        Optional.ofNullable(context.getAuthorities()).orElseThrow(()-> new IllegalArgumentException("User doesn't have any privileges"));
        Claims claims = Jwts.claims().setSubject(context.getUsername());
        claims.put("scopes", context.getAuthorities().stream().map(Object::toString).collect(toList()));
        LocalDateTime currentTime = LocalDateTime.now();
        String token = Jwts.builder()
          .setClaims(claims)
          .setIssuer(properties.getIssuer())
          .setIssuedAt(Date.from(currentTime.atZone(ZoneId.systemDefault()).toInstant()))
          .setExpiration(Date.from(currentTime
              .plusMinutes(properties.getExpirationTime())
              .atZone(ZoneId.systemDefault()).toInstant()))
          .signWith(SignatureAlgorithm.HS512, properties.getSigningKey())
        .compact();
        return new AccessToken(token, claims);
    }

    /**
     * 生成 重新整理 RefreshToken
     * @param userContext
     * @return
     */
    public Token createRefreshToken(UserContext userContext) {
        if (StringUtils.isBlank(userContext.getUsername())) {
            throw new IllegalArgumentException("Cannot create Token without username");
        }
        LocalDateTime currentTime = LocalDateTime.now();
        Claims claims = Jwts.claims().setSubject(userContext.getUsername());
        claims.put("scopes", Arrays.asList(Scopes.REFRESH_TOKEN.authority()));
        String token = Jwts.builder()
          .setClaims(claims)
          .setIssuer(properties.getIssuer())
          .setId(UUID.randomUUID().toString())
          .setIssuedAt(Date.from(currentTime.atZone(ZoneId.systemDefault()).toInstant()))
          .setExpiration(Date.from(currentTime
              .plusMinutes(properties.getRefreshExpTime())
              .atZone(ZoneId.systemDefault()).toInstant()))
          .signWith(SignatureAlgorithm.HS512, properties.getSigningKey())
        .compact();

        return new AccessToken(token, claims);
    }
}

配置檔案,含token過期時間,祕鑰,可自行擴充套件

battcn:
  security:
    token:
      expiration-time: 10 # 分鐘 1440
      refresh-exp-time: 30 # 分鐘 2880
      issuer: http://blog.battcn.com
      signing-key: battcn

WebSecurityConfig 是 Spring Security 關鍵配置,在Securrty中基本上可以通過定義過濾器去實現我們想要的功能.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    public static final String TOKEN_HEADER_PARAM = "X-Authorization";
    public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login";
    public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**";
    public static final String MANAGE_TOKEN_BASED_AUTH_ENTRY_POINT = "/manage/**";
    public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token";

    @Autowired private RestAuthenticationEntryPoint authenticationEntryPoint;
    @Autowired private AuthenticationSuccessHandler successHandler;
    @Autowired private AuthenticationFailureHandler failureHandler;
    @Autowired private LoginAuthenticationProvider loginAuthenticationProvider;
    @Autowired private TokenAuthenticationProvider tokenAuthenticationProvider;

    @Autowired private TokenExtractor tokenExtractor;

    @Autowired private AuthenticationManager authenticationManager;

    protected LoginProcessingFilter buildLoginProcessingFilter() throws Exception {
        LoginProcessingFilter filter = new LoginProcessingFilter(FORM_BASED_LOGIN_ENTRY_POINT, successHandler, failureHandler);
        filter.setAuthenticationManager(this.authenticationManager);
        return filter;
    }

    protected TokenAuthenticationProcessingFilter buildTokenAuthenticationProcessingFilter() throws Exception {
        List<String> list = Lists.newArrayList(TOKEN_BASED_AUTH_ENTRY_POINT,MANAGE_TOKEN_BASED_AUTH_ENTRY_POINT);
        SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(list);
        TokenAuthenticationProcessingFilter filter = new TokenAuthenticationProcessingFilter(failureHandler, tokenExtractor, matcher);
        filter.setAuthenticationManager(this.authenticationManager);
        return filter;
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(loginAuthenticationProvider);
        auth.authenticationProvider(tokenAuthenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
        .csrf().disable() // 因為使用的是JWT,因此這裡可以關閉csrf了
        .exceptionHandling()
        .authenticationEntryPoint(this.authenticationEntryPoint)
        .and()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
            .authorizeRequests()
                .antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point
                .antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point
        .and()
            .authorizeRequests()
                .antMatchers(TOKEN_BASED_AUTH_ENTRY_POINT).authenticated() // Protected API End-points
                .antMatchers(MANAGE_TOKEN_BASED_AUTH_ENTRY_POINT).hasAnyRole(RoleEnum.ADMIN.name())
        .and()
            .addFilterBefore(buildLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
            .addFilterBefore(buildTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

- 說點什麼

由於JWT程式碼做了簡單封裝,包含內容較多,所以文章裡只貼主要片段,需要完整程式碼可以直接從下面GIT獲取

如有問題請及時與我聯絡

  • 個人QQ:1837307557
  • Spring Cloud中國社群①:415028731
  • Spring For All 社群⑤:157525002