上一章我们搭建好了授权认证服务器,也能成功返回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,拿到结果再进行封装。我觉得这种方法应该是可行的,于是有了下文的具体实现,现在放上干货。
与前端约定格式如下:
{
"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当然也是我定制过的异常,比如密码错误会显示如下信息:
关于自定义异常处理,将在下一章详细说明,敬请期待。
我们在请求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工作已全部完成。
我不知道这种用法是不是最合理的用法,但是只要能满足需求,管它那么多呢?
在上一章的2.4里,我们有一个@EnableGlobalMethodSecurity(prePostEnabled = true)这样的代码,关于权限控制和异常处理,下一章再详细来学习。
希望中国早日战胜疫情,加油!