Spring-Security-Oauth2学习笔记(2):自定义token返回格式

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,结果如下:
Spring-Security-Oauth2学习笔记(2):自定义token返回格式_第1张图片
可以看到返回格式已经是我们要求的格式了,但是这里还有个问题,我们上一章还有个功能是使用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,结果如下:
Spring-Security-Oauth2学习笔记(2):自定义token返回格式_第2张图片
可以看到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当然也是我定制过的异常,比如密码错误会显示如下信息:
Spring-Security-Oauth2学习笔记(2):自定义token返回格式_第3张图片
关于自定义异常处理,将在下一章详细说明,敬请期待。

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);
    }

现在我们再来测试一下:
Spring-Security-Oauth2学习笔记(2):自定义token返回格式_第4张图片
可以看到在data里面新增了两个信息。至此,自定义token工作已全部完成。

4 结语

我不知道这种用法是不是最合理的用法,但是只要能满足需求,管它那么多呢?
在上一章的2.4里,我们有一个@EnableGlobalMethodSecurity(prePostEnabled = true)这样的代码,关于权限控制和异常处理,下一章再详细来学习。
希望中国早日战胜疫情,加油!

你可能感兴趣的:(Spring-Security-Oauth2学习笔记(2):自定义token返回格式)