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)這樣的代碼,關於權限控制和異常處理,下一章再詳細來學習。
希望中國早日戰勝疫情,加油!