美文网首页Spring Cloud技术干货Java学习笔记
Spring-Security-OAuth2服务器搭建之资源服务

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

作者: 彳亍路 | 来源:发表于2017-07-10 20:37 被阅读960次

    本章任务,搭建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> T getBean(Class<T> 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如下图:


    密码授权模式

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


    受信任的客户端模式
    无AccessToken访问测试接口如下图:
    无AccessToken

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


    使用AccessToken

    大功告成

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

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

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

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

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

    相关文章

      网友评论

      • leewaiho:感谢博主解决了我诸多的疑惑
      • 0416119a816f:那key用什么呢。。。
        彳亍路:根据业务定啊,如果你是用户登录,那么可以用唯一的用户名为key,value存储正确的AccessToken,访问的时候,将取出的token进行比较,值比较,失效时间判断等,看是否有无权限访问接口或者目标url
      • 0416119a816f:后面意思是说。为了节省认证开销,资源服务器可以自己缓存带时效的AccessToken嘛。
        彳亍路:@艾尔欧唯伊 看业务需求了,如果你每一个请求,都会携带一个比如userId字段值,那么用userId作为key也是可以的
        0416119a816f: @筱眞 真棒。。token当key。。。
        彳亍路:@艾尔欧唯伊 恩,像这样,redisService.set(RedisKeyPre.OAUTH2_TOKEN+token,token,accessToken.getExpiresIn());

      本文标题:Spring-Security-OAuth2服务器搭建之资源服务

      本文链接:https://www.haomeiwen.com/subject/bmyehxtx.html