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> 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权限控制[四]

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,113评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,644评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,340评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,449评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,445评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,166评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,442评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,105评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,601评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,066评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,161评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,792评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,351评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,352评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,584评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,618评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,916评论 2 344

推荐阅读更多精彩内容