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


這邊記錄下如何使用IdentityServer4 作為 Java SpringBoot 的 認證服務器和令牌頒發服務器。本人也是新手,所以理解不足的地方請多多指教。另外由於真的很久沒有寫中文了,用詞不太恰當的地方也歡迎新手大佬小伙伴指出,一起進步。另外這邊令牌的獲取需要手動使用postman根據令牌端點獲取,然后放在請求頭里面通過postman發給Java的demo,本身這個demo沒有取令牌的功能,請各位注意

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.*;

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM