Java SpringBoot 如何使用 IdentityServer4 作為驗證學習筆記

BeautifulBoy1301發表於2019-07-16

這邊記錄下如何使用IdentityServer4 作為 Java SpringBoot 的 認證伺服器和令牌頒發伺服器。本人也是新手,所以理解不足的地方請多多指教。另外由於真的很久沒有寫中文了,用詞不太恰當的地方也歡迎新手大佬小夥伴指出,一起進步。

1.什麼是Jwt?

關於什麼是Jwt,包括裡面的引數是什麼,這邊可以參考下面這個連結做一些瞭解:

https://www.cnblogs.com/zjutzz/p/5790180.html

下面這個連結是全英文的,但是對jwt是什麼是比較詳細的,英文好的同學可以上了。

https://tools.ietf.org/html/rfc7519

這個連結主要說了jwt和refernce token不一樣,真的很重要,裡面也有些說的不對,我就被坑了:

https://www.cnblogs.com/Irving/articles/9357539.html

 

2.IdentityServer認證&令牌頒發伺服器準備

關於IdentityServer4怎麼搭建使用,網上已經有太多的教程了,這邊我就不多做別的講解,因為我也是新手。但是我目前自己正在使用的是一個帶UI介面的IdentityServer4和Identity(作為使用者管理的部分)結合的伺服器,很多東西已經幫你搭建好了,對新手可以說是十分友好,省去了探索的步驟。但是不建議新手直接使用,作為自己搭建IdentityServer後還有對IdentitySevrer4一些引數不太理解的地方,可以做進一步的理解。下面是IdentityServer4 UI 的github原始碼連結:

https://github.com/skoruba/IdentityServer4.Admin

  • 這邊要是有人感興趣配置這個IdentityServer4 UI,後期也會記錄下相對的這個事怎麼搭建的。這個IdentityServer上面在配置相關的資訊,比如API,Client,還有使用者資源之後,我們會用到如下端點:
  • http://x.x.x.x:5000/connect/token   請求令牌的埠,需要提供客戶端id,客戶端secret,使用者名稱字,使用者密碼,還有授權方式,這裡我選的grant_type是 password。
  • http://x.x.x.x:5000/connect/introspect  令牌自省端點,很多國內的說法是用於refenrece token,然後還有很多大佬翻譯的官方文件根本就是沒認真翻譯(估計也不知道實際意思)。這個端點實際上可以用於那些沒由相應的包或者library可以用於解析jwt令牌的程式來驗證令牌的合法性。只是注意這邊唯一不同的是,對於renfence token和jwt token 要發給這個端點的引數是不一樣的。對於reference,要發的是 client_id 和 secret。但是對於 jwt token,要發的是 base64編碼在請求頭部的 Api_name 和 Api_secret, 這裡就是為什麼 Api有secret這個引數,但是我們幾乎沒有用到過。
  • http://x.x.x.x:5000/.well-known/openid-configuration/jwks (公鑰開放端點) 用於獲取解析jwt令牌的公鑰開放端點。

 

3.Java SpringBoot

關於如何開啟一個新的專案,這邊就不多說了,網上教程很多,我們直接進入正題,這邊我用的Intellij IDEA。然後註冊了過濾器,然後這邊提供了兩種辦法驗證jwt:

  • 通過自省端點返回驗證結果,使用http://x.x.x.x:5000/connect/introspect 
  • 通過公鑰開放端點本地解析token,使用http://x.x.x.x:5000/.well-known/openid-configuration/jwks

由於沒有客戶端,這邊用postman代替求取token,使用http://x.x.x.x:5000/connect/token,然後給我們的java程式發起請求。

3.1通過自省端點返回驗證結果

這個就十分簡單了,上程式碼,主要是Filter裡面dofiler的部分:

@Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("開始進行過濾請求,由認證伺服器自省端點驗證token");
        boolean authenticated = false;
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        HttpServletResponse rep = (HttpServletResponse) servletResponse;

        //--------------給自省端點傳送請求-------------------------------
        //--------------準備請求資訊----------------------------------------
        //其實一個url請求就是一組組鍵對值,getHeader()方法獲取的是頭部的你想要的
        //鍵名後面的值,由於請求裡面token的keyname是這個,倒是要是要改這裡也要改
        //這裡面header要是沒有token這個就不行,會異常
        boolean authorizationHeaderExist = req.getHeader("Authorization") != null;
        if (!authorizationHeaderExist) {
            rep.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }

        String token = cutToken(req.getHeader("Authorization"));
        //獲取編碼後的ApiSecret和ApiName,在application.propertiesz中
        String apiNameSecret = "Basic " + ApiNameSecretbase64();
        //倒是可以放到配置裡面去,那裡統一改
        String introspectEndpoint = "http://localhost:5000/connect/introspect";


        //-------------創造請求----------------------------------------------
        //protected HttpClient client = new DefaultHttpClient();已過時
        HttpClient client = HttpClientBuilder.create().build();
        HttpPost post = new HttpPost(introspectEndpoint);
        //新增請求頭
        post.setHeader("Authorization", apiNameSecret);
        //新增請求主體(body)
        List<NameValuePair> urlBodys = new ArrayList<NameValuePair>();
        urlBodys.add(new BasicNameValuePair("token", token));
        post.setEntity(new UrlEncodedFormEntity((urlBodys)));
        HttpResponse response = client.execute(post);

        System.out.println("\nSending 'POST' request to URL : " + introspectEndpoint);
        System.out.println("Post parameters : " + post.getEntity());
        System.out.println("Response Code : " +
                response.getStatusLine().getStatusCode());
        //讀取返回reponse的content的資訊,含有決定結果
        BufferedReader rd = new BufferedReader(
                new InputStreamReader(response.getEntity().getContent()));
        //注意StringBuffer不是String
        StringBuffer result = new StringBuffer();
        String line = "";
        while ((line = rd.readLine()) != null) {
            result.append(line);
        }
        //除錯用,列印得到的請求的content
        System.out.println(result.toString());
        //-------------------------------決定authenticated結果---------------------------
        JSONObject jo = new JSONObject(result.toString());
        Boolean active = jo.getBoolean("active");

        if (response.getStatusLine().getStatusCode() == 200&& active==true)
        {
            String role = jo.getString("role");
            authenticated = true;
        }
        //--------------------------------處理authenticated結果,決定是否發出401-----------
        if (authenticated)
        {
            //呼叫該方法後,表示過濾器經過原來的url請求處理方法
            filterChain.doFilter(servletRequest, servletResponse);
        } else {
            rep.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }
    }


    //返回Api名字和secret的編碼,請求頭的一部分
    public String ApiNameSecretbase64()
    {
        String result = api.getapiName()+":"+api.getapiSecret();
        byte[] data=Base64.encodeBase64(result.getBytes());
        return new String(data);
    }
    //處理token字串,去掉Bearer
    public String cutToken(String originToken)
    {
         String[] temp = originToken.split(" ");
         return temp[1];
    }

上面的 ApiNameSecretbase64 的功能是讀取配置中的資訊,返回編碼好的 Api Name 和 Secret, 下面是我application.properties相關的配置,同樣這些配置需要放到IdentityServer那邊,可以是通過記憶體的方式也可以是通過像我一樣使用 UI管理介面直接新增:

#IdentityServer4 配置檔案引數
api.name = Api1
api.secret=secreta

同時啟動java專案和IdentityServer4之後,請求就可以看到結果了,如果沒有帶token就會是無授權,這邊就不放postman的結果了,因為本篇主要側重於程式碼。關於請求這個端點的效果,返回格式參考官方文件,各位自己可以做本地相應的基於回覆的驗證方式。我這邊直接提取了裡面active這個布林量,來確定這個令牌是否合法。

 

3.2通過請求公鑰本地解析返回驗證token

廢話不多說先上程式碼:

 @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException
    {
        System.out.println("開始進行過濾請求,由認證伺服器jwk公鑰解析驗證token");
        boolean authenticated = false;
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        HttpServletResponse rep = (HttpServletResponse) servletResponse;
        boolean authorizationHeaderExist = req.getHeader("Authorization") != null;
        if (!authorizationHeaderExist) {
            rep.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }
        String jwkEndpoint = "http://localhost:5000/.well-known/openid-configuration/jwks";
        //String token = cutToken(((HttpServletRequest) servletRequest).getHeader("Authorization"));
        String token = cutToken(req.getHeader("Authorization"));


        //------------解析------------------------------------------------------
        //com.nimbusds JWT解析包,這個包目前沒有找到原始碼,
        //https://connect2id.com/products/nimbus-jose-jwt/examples/validating-jwt-access-tokens
        //建立解析處理物件
        ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor();
        //提供公鑰地址來獲取
        JWKSource keySource = new RemoteJWKSet(new URL(jwkEndpoint));
        //提供解析演算法,演算法型別要寫對,伺服器用的是什麼就是什麼,目前是RSA256演算法
        JWSAlgorithm expectedJWSAlg = JWSAlgorithm.RS256;
        //填寫 RSA 公鑰來源從提供公鑰地址獲取那邊得到
        JWSKeySelector keySelector = new JWSVerificationKeySelector(expectedJWSAlg, keySource);
        if(keySelector==null)
        {
            rep.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            System.out.println("無法獲取公鑰。");
            return;
        }
        //設定第一步建立的解析處理物件
        jwtProcessor.setJWSKeySelector(keySelector);
        //處理收到的token(令牌),錯誤則返回物件
        SecurityContext ctx = null;
        JWTClaimsSet claimsSet = null;
        try {
            claimsSet = jwtProcessor.process(token, ctx);
            authenticated = true;
        } catch (ParseException e) {
            rep.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            e.printStackTrace();
            return;
        } catch (BadJOSEException e) {
            rep.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            e.printStackTrace();
            return;
        } catch (JOSEException e) {
            rep.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            e.printStackTrace();
            return;
        }
        //除錯用,列印出來
        System.out.println(claimsSet.toJSONObject());
        //失敗返回無授權
        if(claimsSet==null) {
            rep.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }
        //解碼裡面具體內容,尤其角色,雖然這裡不需要,順利取出
        JSONObject jo = new JSONObject(claimsSet.toJSONObject());
        String role = jo.getString("role");
        //試一下過期的token,刪除使用者的可以不試試
        //--------------------------------處理authenticated結果,決定是否發出401-----------
        if (authenticated)
        {
            //呼叫該方法後,表示過濾器經過原來的url請求處理方法
            filterChain.doFilter(servletRequest, servletResponse);
        } else {
            rep.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }
    }


    //幫助類
    public String cutToken(String originToken)
    {
        String[] temp = originToken.split(" ");
        return temp[1];
    }

這邊主要是用到了一個包,用法連結如下,英文好的同學可以直接研究這個連結:

https://connect2id.com/products/nimbus-jose-jwt/examples/validating-jwt-access-tokens

這個包需要import的東西還要maven依賴如下:


        <!-- https://mvnrepository.com/artifact/com.nimbusds/nimbus-jose-jwt -->
        <dependency>
            <groupId>com.nimbusds</groupId>
            <artifactId>nimbus-jose-jwt</artifactId>
            <version>7.3</version>
        </dependency>

 

import com.nimbusds.jose.*;
import com.nimbusds.jose.jwk.source.*;
import com.nimbusds.jwt.*;
import com.nimbusds.jose.proc.JWSKeySelector;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jwt.proc.*;

 差不多是這樣了,還有不是很清楚的地方直接看原始碼。

相關文章