Spring-Security-Oauth2學習筆記(2):自定義token返回格式

https://blog.csdn.net/qq_26121913/article/details/104296595

1 前言
上一章我們搭建好了授權認證服務器,也能成功返回token了,oauth2默認的格式如下:

{
    "access_token": "86d17c41-4de9-470a-a637-0acb7237099d",
    "token_type": "bearer",
    "refresh_token": "fb255411-9535-47ad-9f7e-40b21fac42cf",
    "expires_in": 43199,
    "scope": "all"
}



但是一般前後端交互時有關於API數據格式的約定,現在我想以如下格式來返回token:

{
    "code": 0,
    "msg": "登錄成功!",
    "data": {
        "access_token": "86d17c41-4de9-470a-a637-0acb7237099d",
        "token_type": "bearer",
        "refresh_token": "fb255411-9535-47ad-9f7e-40b21fac42cf",
        "expires_in": 43097,
        "scope": "all"
    }
}


期間我也參考了很多其它資料,比如https://blog.csdn.net/u013905744/article/details/100637224,這篇文章使用切面編程的方式將/oauth/token接口返回的數據重新封裝,這個方法我覺得有點麻煩,所以沒有試過。另外還有https://segmentfault.com/a/1190000020317220?utm_source=tag-newest,這篇文章使用的方法是重寫/oauth/token接口,直接調用TokenEndpoint的postAccessToken方法獲取 token然後重新包裝,這個方法我試過了,可以用,但是有點問題,用這種方法時異常處理有點問題,具體就不多說了。
在查資料的過程中,也看到別人發的問題貼子,需求跟我一樣,但是下面的回答沒有具體的代碼,只是有一段話,說可以後端轉發請求到/oauth/token,拿到結果再進行封裝。我覺得這種方法應該是可行的,於是有了下文的具體實現,現在放上乾貨。

2 自定義token返回格式
與前端約定格式如下:

{
    "code":0,//表示成功
    "msg":"",//信息
    "data":{//數據
    }
}


因此我們寫一個ResponseResult 工具類來返回數據,代碼如下:

package com.example.oauth2.util;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ResponseResult {
    //返回狀態碼
    private int code;

    //返回信息
    private String msg;

    //返回數據
    private Object data;

    public ResponseResult(Object data){
        this.code=0;
        this.msg="操作成功!";
        this.data=data;
    }

    public ResponseResult(String msg,Object data){
        this.code=0;
        this.msg=msg;
        this.data=data;
    }

    public ResponseResult(int code,String msg){
        this.code=code;
        this.msg=msg;
        this.data=null;
    }
}


核心代碼來了,具體是寫一個/oauth/login,在該接口中使用RestTemplate請求/oauth/token接口,拿到token後使用ResponseResult封裝再返回。代碼如下:

package com.example.oauth2.controller;

import com.example.oauth2.util.ResponseResult;
import org.springframework.http.client.support.BasicAuthenticationInterceptor;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.Map;

@RestController
@RequestMapping("/oauth")
public class OauthController {
    @PostMapping("/login")
    public ResponseResult login(@RequestParam Map<String,Object> map){
        MultiValueMap<String,Object> paramsMap=new LinkedMultiValueMap<>();
        paramsMap.set("username",map.get("username"));
        paramsMap.set("password",map.get("password"));
        paramsMap.set("grant_type",map.get("grant_type"));
        RestTemplate restTemplate=new RestTemplate();
        restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(map.get("client_id").toString(),map.get("client_secret").toString()));
        OAuth2AccessToken token=restTemplate.postForObject("http://localhost:8080/oauth/token",paramsMap,OAuth2AccessToken.class);
        return new ResponseResult("登錄成功!",token);
    }
}


其中,restTemplate.getInterceptors()非常重要,如果不加這行代碼,使用restTemplate時會報401錯誤。下面我們就來測試一下,打開 postman,與上一章測試一樣,只不過/oauth/token改爲/oauth/login,結果如下:

可以看到返回格式已經是我們要求的格式了,但是這裏還有個問題,我們上一章還有個功能是使用refresh_token刷新access_token,所以還需要把/oauth/login改造一下,先判斷grant_type是以什麼方式來請求token,再進行請求。代碼如下:

    @PostMapping("/login")
    public ResponseResult login(@RequestParam Map<String,Object> map){
        MultiValueMap<String,Object> paramsMap=new LinkedMultiValueMap<>();
        if(map.get("grant_type").equals("password")){
            paramsMap.set("username",map.get("username"));
            paramsMap.set("password",map.get("password"));
        }else if(map.get("grant_type").equals("refresh_token")){
            paramsMap.set("refresh_token",map.get("refresh_token"));
        }
        paramsMap.set("grant_type",map.get("grant_type"));
        RestTemplate restTemplate=new RestTemplate();
        restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(map.get("client_id").toString(),map.get("client_secret").toString()));
        OAuth2AccessToken token=restTemplate.postForObject("http://localhost:8080/oauth/token",paramsMap,OAuth2AccessToken.class);
        return new ResponseResult("登錄成功!",token);
    }



現在我們再測試一下refresh_token,結果如下:

可以看到refresh_token也可以刷新access_token,並且同樣返回的我們自定義的格式。當然,這裏我要說明一下,我現在使用的方式是將client_id和client_secret放在參數裏進行請求,上一章中也說了,還可以使用Basic憑證來請求,具體改法就是先從請求頭裏獲取Authorization,看看是否以"Basic "開頭,如果有就把它放到restTemplate的請求頭裏,如果沒有,再把參數裏的client_id和client_secret通過restTemplate.getInterceptors()的方式添加進去。另外,請求不可能每次都成功,如果出現異常,在返回的token裏面把異常拿出來就行了,具體改法如下:

        if(token.getValue()==null){
            return new ResponseResult(Integer.parseInt(token.getAdditionalInformation().get("code").toString()),token.getAdditionalInformation().get("msg").toString());
        }else{
            return new ResponseResult("登錄成功!",token);
        }


token裏的code和msg當然也是我定製過的異常,比如密碼錯誤會顯示如下信息:

關於自定義異常處理,將在下一章詳細說明,敬請期待。

3 添加額外的token信息
我們在請求token後,前端如果有需求,比如說要將用戶信息顯示在頁面上,那麼請求token的時候能不能給它添加一些額外參數呢?答案是肯定的,也比較簡單,只需要實現TokenEnhancer接口就可以了,具體代碼如下:

package com.example.oauth2.service.impl;

import com.example.oauth2.entity.Account;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.stereotype.Service;

import java.util.LinkedHashMap;
import java.util.Map;

@Service
public class TokenEnhancerImpl implements TokenEnhancer {
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
        Account account=(Account)oAuth2Authentication.getPrincipal();
        DefaultOAuth2AccessToken token=(DefaultOAuth2AccessToken)oAuth2AccessToken;
        Map<String,Object> map=new LinkedHashMap<>();
        map.put("username",account.getUsername());
        map.put("nickname",account.getNickname());
        token.setAdditionalInformation(map);
        return oAuth2AccessToken;
    }
}


然後在AuthorizationServerConfig的public void configure(AuthorizationServerEndpointsConfigurer endpoints)方法裏增加最後一行代碼: 

   @Autowired
    @Qualifier("tokenEnhancerImpl")
    private TokenEnhancer tokenEnhancer;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(new InMemoryTokenStore())
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService)
                .tokenEnhancer(tokenEnhancer);
    }


現在我們再來測試一下:

可以看到在data裏面新增了兩個信息。至此,自定義token工作已全部完成。

4 結語
我不知道這種用法是不是最合理的用法,但是只要能滿足需求,管它那麼多呢?
在上一章的2.4裏,我們有一個@EnableGlobalMethodSecurity(prePostEnabled = true)這樣的代碼,關於權限控制和異常處理,下一章再詳細來學習。
希望中國早日戰勝疫情,加油!
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章