Spring-Security-OAuth2服务器搭建之资源服务器搭建[三]

本章任务,搭建OAuth2的资源服务器,什么是认证授权服务器什么是资源服务器,请参考链接,如果是谷歌浏览器,右键点翻译即可了解大概。资源服务器与认证授权服务器可以是一个服务器,也可以独立使用。
现在我所讲述的是独立使用,独立使用的一个好处在于,你的认证授权服务器是一个,但是你的资源服务器却可以是多个,这就形成了一对多的关系。就我个人理解来说(未有具体实战,如有错误请指出),而且如果你的情况是:分布式应用,那就更好办了。比如,对于当前架构,使用redis作为存储AccessToken的介质,你可以使用主从redis多节点存储AccessToken,部署多个认证授权服务器。这样认证授权服务器与资源服务器就形成了多对多的关系。
好了废话少说,开始:code show time now!

获取AccessToken接口

/**
 * @Desc 认证登录接口(获取AccessToken)
 */
@RestController
@RequestMapping("/api/oauth2")
public class Oauth2Controller {

    private static final Logger log = LoggerFactory.getLogger(Oauth2Controller.class);

    /**
     * OAuth2的密码授权模式
     */
    @RequestMapping(value = "/passwordMode",method = RequestMethod.POST)
    public Object accessToken(@RequestParam(value = "client_id") String client_id,
                              @RequestParam(value = "client_secret") String client_secret,
                              @RequestParam(value = "grant_type") String grant_type,
                              @RequestParam(value = "username") String username,
                              @RequestParam(value = "password") String password
                                     ){
        //补足:对dm5加密后的密码不足32位加零补齐
        String fill = "";
        if (password.length() < 32) {//下面的details的password的长度必须32位,所以非32位则,需要补足位数
            int len = 32 - password.length();
            fill = String.format("%0" + len + "d", 0);
        }
        //创建一个包含需要请求的资源实体以及认证信息集合的对象
        ResourceOwnerPasswordResourceDetails details = new ResourceOwnerPasswordResourceDetails();
        //设置请求认证授权的服务器的地址
        details.setAccessTokenUri(ApplicationSupport.getParamVal("oauth.token"));
        //下面都是认证信息:所拥有的权限,认证的客户端,具体的用户
        details.setScope(Arrays.asList("read", "write"));
        details.setClientId(client_id);
        details.setClientSecret(client_secret);
        details.setUsername(username);
        details.setPassword(fill + password);

        ResourceOwnerPasswordAccessTokenProvider provider = new ResourceOwnerPasswordAccessTokenProvider();
        OAuth2AccessToken accessToken = null;
        try {
            //获取AccessToken
            // 1、(内部流程简介:根据上述信息,将构造一个前文一中的请求头为 "Basic Base64(username:password)" 的http请求
            //2、之后将向认证授权服务器的 oauth/oauth_token 端点发送请求,试图获取AccessToken
            accessToken = provider.obtainAccessToken(details, new DefaultAccessTokenRequest());
        } catch (NullPointerException e) {
            log.error("授权失败原因:{}", e.getMessage());
            return "用户不存在";
        }catch (Exception e){
            log.error("授权失败原因:{}", e.getMessage());
            return "创建token失败";
        }
        return accessToken;
    }

    /**
     * Oauth2的受信任的客户端授权模式
     */
    @RequestMapping(value = "/clientMode",method = RequestMethod.POST)
    public Object getToken(@RequestParam(value = "client_id") String client_id,
                                   @RequestParam(value = "client_secret") String client_secret,
                                   @RequestParam(value = "grant_type") String grant_type
                                   ){
        //创建一个包含需要请求的资源实体以及认证信息集合的对象
        ClientCredentialsResourceDetails clientCredentials = new ClientCredentialsResourceDetails();
        clientCredentials.setAccessTokenUri(ApplicationSupport.getParamVal("oauth.token"));
        //下面都是认证信息:所拥有的权限,认证的客户端
        clientCredentials.setScope(Arrays.asList("read", "write"));
        clientCredentials.setClientId(client_id);
        clientCredentials.setClientSecret(client_secret);
        clientCredentials.setGrantType(grant_type);
        ClientCredentialsAccessTokenProvider provider = new ClientCredentialsAccessTokenProvider();
        OAuth2AccessToken accessToken = null;
        try {
            accessToken = provider.obtainAccessToken(clientCredentials, new DefaultAccessTokenRequest());
        } catch (Exception e) {
            e.printStackTrace();
            return "获取AccessToken失败";
        }
        return accessToken;
    }

}

检测AccessToken有效性的拦截器Oauth2Interceptor

/**
 * 对AccessToken进行检测,当出现AccessToken失效或者非法时,将直接返回401,未授权错误
 */
public class Oauth2Interceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler) throws Exception {
        String accessToken = request.getParameter("access_token");
        OAuth2AccessToken oauth2AccessToken = Oauth2Utils.checkTokenInOauth2Client(accessToken);
        if (oauth2AccessToken==null){//非法的Token值
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            ResponseUtils.responseData(response,"非法的Token!");
            return false;
        }else if (oauth2AccessToken.isExpired()){//token失效
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            ResponseUtils.responseData(response,"Token失效,请重新登录!");
            return false;
        }
         return true;
    }
}

注册拦截器InterceptorRegisterConfiguration

@Configuration
@EnableWebMvc //开启spring mvc的相关默认配置
public class InterceptorRegisterConfiguration extends WebMvcConfigurerAdapter {
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new Oauth2Interceptor())
        .excludePathPatterns("/api/oauth2/**");//添加Oauth2Interceptor,除了/api/oauth2/**下的接口都需要进行 AccessToken 的校验
    }
}

工具类Oauth2Utils、ResponseUtils、ApplicationSupport

/**
 * 获取Spring容器管理的Bean对象,应用中配置参数
 **/
/**
 * 工具类
 */
public class Oauth2Utils {
    private static final Logger LOGGER = LoggerFactory.getLogger(Oauth2Utils.class);
    //检查AccessToken有效性的url(认证授权服务器的url地址)
    private static String checkTokenUrl ;
    static {
        checkTokenUrl = ApplicationSupport.getParamVal("oauth.check_token");
    }

/**
     * 客户端申请校验
     * @param tokenValue
     * @return
     */
    public static OAuth2AccessToken checkTokenInOauth2Client(String tokenValue){
        if (StringUtils.isEmpty(tokenValue)) {
            return null;
        }
        try {
            RestTemplate restTemplate = new RestTemplate();
            OAuth2AccessToken oAuth2AccessToken = restTemplate.getForObject(checkTokenUrl+"?token="+tokenValue, OAuth2AccessToken.class);
            return oAuth2AccessToken;
        }catch (Exception e){
            LOGGER.error("checkTokenInOauth2Client failure:",e);
            return null;
        }
    }
}
@Component
public class ApplicationSupport implements DisposableBean, ApplicationContextAware {

    private static ApplicationContext applicationContext;
    // 获取配置文件参数值
    public static String getParamVal(String paramKey){
        return applicationContext.getEnvironment().getProperty(paramKey);
    }

    // 获取bean对象
    public static Object getBean(String name) {
        Assert.hasText(name);
        return applicationContext.getBean(name);
    }

    public static  T getBean(Class clazz) {
        return applicationContext.getBean(clazz);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
   public void destroy() throws Exception {
        applicationContext = null;
    }

}

一个需要合法的AccessToken的后端接口

/**
 * 测试接口
 */
@RestController
@RequestMapping("/api")
public class TestController {
    @RequestMapping("/test")
    public String test(){
        return "success";
    }
}

配置文件application.yml

security:
    basic:
        enabled: false # 是否开启基本的鉴权,默认为true。 true:所有的接口默认都需要被验证,将导致 拦截器[对于 excludePathPatterns()方法失效]
server:
  context-path: /oauth2-client
  port: 8051
---
spring:
  application:
      name: oauth2-client
  datasource: #数据源的配置
    url: jdbc:mysql://127.0.0.1:3306/redis-oauth2?useUnicode=true&characterEncoding=UTF-8
    username: root
    password: 123456
  jpa: #jpa的支持:hibernate的相关配置
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
    database: MYSQL
    openInView: true
    show_sql: true
    generate-ddl: true #(false)
    hibernate:
        ddl-auto: update #(none)

oauth: #oauth2-server认证授权服务器的url配置,在获取AccessToken以及检测AccessToken中会用到
  token: http://127.0.0.1:8050/oauth2-server/oauth/token
  check_token: http://localhost:8050/oauth2-server/oauth/check_token #检查AccessToken有效性的url(认证授权服务器的url地址),获取 AccessToken 对象

启动类

@SpringBootApplication
@EnableOAuth2Client 
public class Oauth2ClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(Oauth2ClientApplication.class, args);
    }
}

上面就是一个完整的OAuth2服务器中资源服务器的整个代码,是不是很简单。当然,代码中尚有可以根据环境调整的部分,比如,在拦截器中,我们可以直接用key(tokenValue)从redis中获取AccessToken的完整信息,然后判断是否存在,是否失效,这也是一种策略。其次,在获取AccessToken的value值之后,你也可以自定义一个key-value存储当前认证登陆用户的有用信息到redis中(可以与认证授权服务器使用同一个库,也可以不同),并设置失效时间,然后在拦截器中先再redis中校验,最后如有必要再去认证服务器中校验。此处,我是直接发送http请求去远端校验的。

测试流程

1、先启动认证授权服务器oauth-server
2、后启动资源服务器
3、向资源服务器发送获取AccessToken的请求
4、使用获取的AccessToken向测试接口发送请求

测试结果

密码认证模式获取AccessToken如下图:


Spring-Security-OAuth2服务器搭建之资源服务器搭建[三]_第1张图片
密码授权模式

受信任的客户端模式获取AccessToken如下图:


Spring-Security-OAuth2服务器搭建之资源服务器搭建[三]_第2张图片
受信任的客户端模式

无AccessToken访问测试接口如下图:
Spring-Security-OAuth2服务器搭建之资源服务器搭建[三]_第3张图片
无AccessToken

使用AccessToken访问测试接口如下图:


Spring-Security-OAuth2服务器搭建之资源服务器搭建[三]_第4张图片
使用AccessToken

大功告成

现在,Spring-Security-OAuth2的认证授权服务器和资源服务器到此全部搭建完毕。
基于此资源服务器可以为移动端(Android和iOS)、网页端提供数据接口。上述系列文章,如有错误,请诸多指教!

Spring-Security-OAuth2服务器之搭建认证授权服务器[一]

Spring-Security-OAuth2服务器搭建之AccessToken的检测[二]

Spring-Security-OAuth2服务器搭建之资源服务器搭建[三]

Spring-Security-OAuth2资源服务器及SpringSecurity权限控制[四]

你可能感兴趣的:(Spring-Security-OAuth2服务器搭建之资源服务器搭建[三])