使用OAuth2的SSO分析

参考:https://github.com/spring-guides/tut-spring-security-and-angular-js/blob/master/oauth2-vanilla/README.adoc 

1.浏览器向UI服务器点击触发要求安全认证 
2.跳转到授权服务器获取授权许可码 
3.从授权服务器带授权许可码跳回来 
4.UI服务器向授权服务器获取AccessToken 
5.返回AccessToken到UI服务器 
6.发出/resource请求到UI服务器 
7.UI服务器将/resource请求转发到Resource服务器 
8.Resource服务器要求安全验证,于是直接从授权服务器获取认证授权信息进行判断后(最后会响应给UI服务器,UI服务器再响应给浏览中器)

一.先创建OAuth2授权服务器 
1.使用spring Initializrt生成初始项目,选使用spring boot 1.3.3生成maven项目,根据需要填写group,artifact,依赖选Web和Security两块,点生成按钮即可. 
2.加入OAuth2依赖到pom.xml

<dependency>
   <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
</dependency>

修改主类(这里同时也作为资源服务器)

@SpringBootApplication
@RestController
@EnableAuthorizationServer
@EnableResourceServer
public class AuthserverApplication {

    @RequestMapping("/user")
    public Principal user(Principal user) {
        return user;
    }

    public static void main(String[] args) {
        SpringApplication.run(AuthserverApplication.class, args);
    }

}

同时修改servlet容器的port,contextPath,注册一个测试用户与客户端,加入配置:application.properties

server.port: 9999
server.contextPath: /uaa
security.user.password: password
security.sessions: if-required
security.oauth2.client.clientId: acme
security.oauth2.client.clientSecret: acmesecret
security.oauth2.client.authorized-grant-types: authorization_code,refresh_token,password
security.oauth2.client.scope: openid

基于spring boot的security的session创建策略默认是STATELESS,至于几个选项意义,可看

org.springframework.security.config.http.SessionCreationPolicy

启动授权服务器后,可测试了: 

a.打开浏览器输入地址

http://localhost:9999/uaa/oauth/authorize?response_type=code&client_id=acme&redirect_uri=http://example.com

发出请求,然后根据以上配置,输入用户名/密码,点同意,获取返回的授权许可码 

b.在Linux的bash或mac的terminal输入 

[root@dev ~]#curl acme:acmesecret@192.168.1.115:9999/uaa/oauth/token \
-d grant_type=authorization_code -d client_id=acme \
-d redirect_uri=http://example.com -d code=fjRdsL 

回车获取access token,其中fjRdsL替换上步获取的授权许可码.返回结果类似如下: 

{
  "access_token": "8eded27d-b849-4473-8b2d-49ae49e17943",
  "token_type": "bearer",
  "refresh_token": "5e9af75c-c442-433f-81ba-996eb2c00f53",
  "expires_in": 43199,
  "scope": "openid"
}

 

从返回结果复制access_token,继续: 

[root@dev ~]# TOKEN=8eded27d-b849-4473-8b2d-49ae49e17943
[root@dev ~]# curl -H “Authorization: Bearer $TOKEN” 192.168.1.115:9999/uaa/user 

其中上面的8eded27d-b849-4473-8b2d-49ae49e17943是access_token,根据实际情况替换,第二个命令返回结果类似如下: 

{
  "details": {
    "remoteAddress": "192.168.1.194",
    "sessionId": null,
    "tokenValue": "8eded27d-b849-4473-8b2d-49ae49e17943",
    "tokenType": "Bearer",
    "decodedDetails": null
  },
  "authorities": [
    {
      "authority": "ROLE_USER"
    }
  ],
  "authenticated": true,
  "userAuthentication": {
    "details": {
      "remoteAddress": "0:0:0:0:0:0:0:1",
      "sessionId": "3943F6861E0FE31C29568542730342F6"
    },
    "authorities": [
      {
        "authority": "ROLE_USER"
      }
    ],
    "authenticated": true,
    "principal": {
      "password": null,
      "username": "user",
      "authorities": [
        {
          "authority": "ROLE_USER"
        }
      ],
      "accountNonExpired": true,
      "accountNonLocked": true,
      "credentialsNonExpired": true,
      "enabled": true
    },
    "credentials": null,
    "name": "user"
  },
  "oauth2Request": {
    "clientId": "acme",
    "scope": [
      "openid"
    ],
    "requestParameters": {
      "response_type": "code",
      "redirect_uri": "http://example.com",
      "code": "QzbdLe",
      "grant_type": "authorization_code",
      "client_id": "acme"
    },
    "resourceIds": [],
    "authorities": [
      {
        "authority": "ROLE_USER"
      }
    ],
    "approved": true,
    "refresh": false,
    "redirectUri": "http://example.com",
    "responseTypes": [
      "code"
    ],
    "extensions": {},
    "grantType": "authorization_code",
    "refreshTokenRequest": null
  },
  "credentials": "",
  "principal": {
    "password": null,
    "username": "user",
    "authorities": [
      {
        "authority": "ROLE_USER"
      }
    ],
    "accountNonExpired": true,
    "accountNonLocked": true,
    "credentialsNonExpired": true,
    "enabled": true
  },
  "clientOnly": false,
  "name": "user"
}

从结果来看,使用access token访问资源一切正常,说明授权服务器没问题.

二.再看分离的资源服务器(改动也不少) 

不再使用Spring Session从Redis抽取认证授权信息,而是使用ResourceServerTokenServices向授权服务器发送请求获取认证授权信息.
因些没用到Spring Session时可移除,同时application.properties
配置
security.oauth2.resource.userInfoUri

security.oauth2.resource.tokenInfoUri
中的一个,
主类修改如下:

@SpringBootApplication
@RestController
@EnableResourceServer
public class ResourceApplication {
    @RequestMapping("/")
    public Message home() {
        return new Message("Hello World");
    }
    public static void main(String[] args) {
        SpringApplication.run(ResourceApplication.class, args);
    }
}

最后运行主类的main方法,开始测试(授权服务器前面启动了,access_token也得到了),于是在使用curl命令: 

[root@dev ~]# curl -H “Authorization: Bearer $TOKEN” 192.168.1.115:9000 

返回结果类似如下: 

{
  "id": "03af8be3-2fc3-4d75-acf7-c484d9cf32b1",
  "content": "Hello World"
} 

可借鉴的经验,我在windows上开发,启动资源服务器,然后资源服务器有配置

server.address: 127.0.0.1

,这里限制容器只能是本机访问,
如果使用局域网IP是不可以访问的,比如你在别人的机器或在一台虚拟的linux上使用curl都是不是访问的,注释这行配置,这限制就解除. 

跟踪下获取认证授权的信息过程: 
1.userInfoRestTemplate Bean的声明在

org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerTokenServicesConfiguration.
UserInfoRestTemplateConfiguration#userInfoRestTemplate 

2.使用前面配置的userInfoUri和上面的userInfoRestTemplate Bean在org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerTokenServicesConfiguration.
RemoteTokenServicesConfiguration.
UserInfoTokenServicesConfiguration#userInfoTokenServices
创建UserInfoTokenServices Bean. 

3.在org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer#configure添加了org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter 

4.当使用curl -H “Authorization: Bearer $TOKEN” 192.168.1.115:9000发出请求时,直到被OAuth2AuthenticationProcessingFilter拦截器处理, 
org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter
#doFilter{ 
Authentication authentication = tokenExtractor.extract(request);//抽取Token 
Authentication authResult = authenticationManager.authenticate(authentication);//还原解码认证授权信息 

org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationManager
#authenticate{ 
OAuth2Authentication auth = tokenServices.loadAuthentication(token);//这里的tokenServices就是上面的UserInfoTokenServices Bean,就在这里向授权服务器发出请求. 

三.UI服务器作为SSO的客户端. 

1.同样UI服务器不需要Spring Session,认证如我们所期望的,交给授权服务器,所以使用Spring Security OAuth2依赖替换Spring Session和Redis依赖
2.当然UI服务器还是API网关的角色,所以不要移除@EnableZuulProxy
在UI服务器主类加上@EnableOAuth2Sso,这个注解会帮我们完成跳转到授权服务器,当然要些配置application.yml

zuul:
  routes:
    resource:
      path: /resource/**
      url: http://localhost:9000
    user:
      path: /user/**
      url: http://localhost:9999/uaa/user

这里将”/user”请求代理到授权服务器 

3.继续修改UI主类继承WebSecurityConfigurerAdapter,重写org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
#configure(org.springframework.security.config.annotation.web.builders.HttpSecurity) 
目的是为了修改@EnableOAuth2Sso引起的默认Filter链,默认是org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2SsoDefaultConfiguration
#configure,
这个类上面有@Conditional(NeedsWebSecurityCondition.class)意思应该是,没有WebSecurityConfigurerAdapter才会去执行这个config,
因为继承了这个类,所以此config不再执行. 

4.作为oauth2的客户端,application.yml下面这几项是少不了的

security:
  oauth2:
    client:
      accessTokenUri: http://localhost:9999/uaa/oauth/token
      userAuthorizationUri: http://localhost:9999/uaa/oauth/authorize
      clientId: acme
      clientSecret: acmesecret
    resource:
      userInfoUri: http://localhost:9999/uaa/user

最后一项,因为也作为资源服务器,所以也加上吧

spring:
  aop:
    proxy-target-class: true

spring aop默认一般都是使用jdk生成代理,前提是要有接口,cglib生成代理,目标类不能是final类,这是最基本的条件.
估计是那些restTemplate没有实现接口,所以不得不在这里使用cglib生成代理. 

5.其它的前端微小改变,这里不赘述.把授权服务器,分离的资源服务器和这个UI服务器都启动.准备测试:http://localhost:8080/login 
a.经过security的拦截链接中的
org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter.doFilter拦截,
触发了attemptAuthentication方法

    public OAuth2AccessToken getAccessToken() throws UserRedirectRequiredException {
        OAuth2AccessToken accessToken = context.getAccessToken();
        if (accessToken == null || accessToken.isExpired()) {
            try {
                accessToken = acquireAccessToken(context);
            } catch (UserRedirectRequiredException e) {
                context.setAccessToken(null); // No point hanging onto it now
                accessToken = null;
                String stateKey = e.getStateKey();
                if (stateKey != null) {
                    Object stateToPreserve = e.getStateToPreserve();
                    if (stateToPreserve == null) {
                        stateToPreserve = "NONE";
                    }
                    context.setPreservedState(stateKey, stateToPreserve);
                }
                throw e;
            }
        }
        return accessToken;
    }

acquireAccessToken(context)去获取token的时候触发抛异常.
在org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider
#getRedirectForAuthorization处理发送的url,
最后这个UserRedirectRequiredException往上抛,
一直往上抛到org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter#doFilter

    catch (Exception ex) {
        // Try to extract a SpringSecurityException from the stacktrace
        Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
        UserRedirectRequiredException redirect = (UserRedirectRequiredException) throwableAnalyzer
                .getFirstThrowableOfType(
                        UserRedirectRequiredException.class, causeChain);
        if (redirect != null) {
            redirectUser(redirect, request, response);
        } else {
            if (ex instanceof ServletException) {
                throw (ServletException) ex;
            }
            if (ex instanceof RuntimeException) {
                throw (RuntimeException) ex;
            }
            throw new NestedServletException("Unhandled exception", ex);
        }
    }

终于看到redirectUser(redirect, request, response);进行跳转到授权服务器去了.

授权服务器跳回到UI服务器原来的地址(带回来授权许可码),再次被OAuth2ClientAuthenticationProcessingFilter拦截发送获取accessToken,
经org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport
#retrieveToken提交POST请求,获取到返回原来发请求处得到OAuth2AccessToken对象. 

在org.springframework.security.oauth2.client.OAuth2RestTemplate#acquireAccessToken使用oauth2Context.setAccessToken(accessToken);
对token进行保存.有了accessToken,就可以从授权服务器获取用户信息了.

最后,当用户点logout的时候,授权服务器根本没有退出(销毁认证授权信息)

http://blog.csdn.net/xiejx618/article/details/51039653

 

时间: 2024-11-05 12:19:15

使用OAuth2的SSO分析的相关文章

使用JWT的OAuth2的SSO分析

参考:https://github.com/spring-guides/tut-spring-security-and-angular-js/blob/master/oauth2/README.adoc http://jwt.io/introduction/ 本文在<使用OAuth2的SSO分析>文章的基础上扩展,使用jwt可减少了向认证服务器的请求,但jwt比swt(Simple Web Tokens)要长不少,还要依赖公钥解密. 1.浏览器向UI服务器点击触发要求安全认证 2.跳转到授权服

浅谈谁都能看懂的单点登录(SSO)实现方式(附源码)_实用技巧

SSO的基本概念 SSO英文全称Single Sign On(单点登录).SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统.它包括可以将这次主要的登录映射到其他应用中用于同一个用户的登录的机制.它是目前比较流行的企业业务整合的解决方案之一.(本段内容来自百度百科) 今天这篇文章将介绍SSO的一种实现方式,代码超简单,仅用来验证我的思路是否可行,具体细节请大家来完善! 二级域名的单点登录 什么是二级域名呢?例如: site1.domain.com site2.domai

深入实践Spring Boot导读

Preface?前 言 Spring Boot作为Java编程语言的一个全新开发框架,在国内外才刚刚兴起,还未得到普及使用.相比于以往的一些开发框架,Spring Boot不但使用更加简单,而且功能更加丰富,性能更加稳定而健壮.使用Spring Boot开发框架,不仅能提高开发速度,增强生产效率,从某种意义上,可以说是解放了程序员的劳动,而且一种新技术的使用,更能增强系统的稳定性和扩展系统的性能指标.本书就是本着提高开发效率,增强系统性能,促进新技术的普及使用这一目的而写的. Spring Bo

深入实践Spring

深入实践Spring Boot 陈韶健 著 图书在版编目(CIP)数据 深入实践Spring Boot / 陈韶健著. -北京:机械工业出版社,2016.10 ISBN 978-7-111-55088-4 I. 深- II. 陈- III. JAVA语言-程序设计 IV. TP312 中国版本图书馆CIP数据核字(2016)第244089号 深入实践Spring Boot 出版发行:机械工业出版社(北京市西城区百万庄大街22号 邮政编码:100037) 责任编辑:李 艺 责任校对:殷 虹 印 刷

SSO单点登录系列2:cas客户端和cas服务端交互原理动画图解,cas协议终极分析

这次的收获是把PPT也深入研究了一番... 上图:一会上原理分析:(本篇不涵盖cas代理认证模式,代理目前还没用到.) 文中所有资料下载地址:在文章中最下方. 1)PPT流程图: 一.用户第一次访问web1应用. ps:上图少画了一条线,那一条线,应该再返回来一条,然后再到server端,画少了一步...谢谢提醒.而且,重定向肯定是从浏览器过去的.我写的不严谨,画的比较通俗了...因该像下面这张图一样就ok了!!PPT自己下载下来修改吧,我就不改了. 二.用户第一次访问web2应用. 困扰了好久

PHP中SSO Cookie登录分析和实现_php实例

什么是SSO? 单点登录SSO(Single Sign-On)是身份管理中的一部分.SSO的一种较为通俗的定义是:SSO是指访问同一服务器不同应用中的受保护资源的同一用户,只需要登录一次,即通过一个应用中的安全验证后,再访问其他应用中的受保护资源时,不再需要重新登录验证 SSO的用途: 目前的企业应用环境中,往往有很多的应用系统,淘宝.天猫.爱淘宝等等产品和如办公自动化(OA)系统,财务管理系统,档案管理系统,信息查询系统等等.这些应用系统服务于企业的信息化建设,为企业带来了很好的效益.但是,用

分析单点登录(流程图与数据安全)

去年公司也曾经做过一个单点登录模块,两个站点,同事是基于cookie和session来实现的,在那个模块中并没有单独的用户认证中心,每个子站都有自己的登录系统,在判断用户是否登录时,首先是通过判断cookie是否存在来判断用户登录与否.如果cookie值存在则写入session,保存登录票据. 一般基于cookie的程序,在某种程度上来说最大的问题就是安全,因为它是以文件形式存储在客户端的.所以一般非常重要的信息,例如用户登录信息,用户银行卡信息都不会采用cookie来存储.尽管cookie可以

针对分析单点登录(流程图与数据安全)提出的问题及解决方案

上一篇本人根据园友的文章:[原创]单点登陆(SSO)组件的设计与实现,根据自己的理解进行一次总结性的分析:分析单点登录(流程图与数据安全). 当时根据SSO的流程做下了个人分析,也得到不少园友的评论及帮助,可是觉的可惜的是,没有一位园友提出我画的流程图中存在的问题.这也可能是大家没有看明白我的图(本人不才). 当时自认为在流程上以及业务逻辑上都没有太大的问题,至到有次和朋友谈到了SSO,当时我非常自豪的说出了SSO的流程及思路,他也非常认可,可是他的一个问题把我难倒了. 他的问题是这样的: 如果

Android 使用新浪微博SSO授权

  新浪微博SSO授权,很早就做好了,只是一直没有时间整理博客,今天加班,晚上闲暇之时便想到整理一下.由于整个七月份很忙,加班很多.前段时间把腾讯微博的SSO认证整理好了.想在七月份翻篇之前再写点东西.好了,不多说废话了,下面就说说关于新浪微博SSO认证的内容. 新浪微博比较简单,而且很方便使用.由于在腾讯微博中我有讲到SSO认证的过程,这里主要是看看新浪微博demo中的MainActivity,这个类中告诉我们如何进行新浪微博的授权,按照这个范例来做就可以的.下面是这个类的源码 package