关于oauth2.0:MaxKey-单点登录认证系统-v353GA-发布

English|中文 概述MaxKey单点登录认证零碎,谐音马克思的钥匙寓意是最大钥匙,是业界当先的IAM身份治理和认证产品,反对OAuth 2.x/OpenID Connect、SAML 2.0、JWT、CAS、SCIM等标准协议,提供平安、规范和凋谢的用户身份治理(IDM)、身份认证(AM)、单点登录(SSO)、RBAC权限治理和资源管理等。 官方网站官网|官网二线 官网QQ:1054466084 邮箱email:support@maxsso.net 代码托管Gitee|GitHub 单点登录(Single Sign On)简称为SSO,用户只须要登录认证核心一次就能够拜访所有相互信任的利用零碎,无需再次登录。 次要性能: 所有利用零碎共享一个身份认证零碎所有利用零碎可能辨认和提取ticket信息产品个性标准协议序号协定反对1.1OAuth 2.0/OpenID Connect高1.2SAML 2.0高1.3JWT高1.4CAS高1.5FormBased中1.6TokenBased(Post/Cookie)中1.7ExtendApi低1.8EXT低登录反对序号登录形式2.1动静验证码 字母/数字/算术2.2双因素认证2.3短信认证 腾讯云短信/阿里云短信/网易云信2.4登录易/Google/Microsoft Authenticator/FreeOTP/反对TOTP或者HOTP2.5Kerberos/SPNEGO/AD域2.6OpenLDAP/ActiveDirectory/规范LDAP服务器2.7社交账号 微信/QQ/微博/钉钉/Google/Facebook/其余2.8扫码登录 企业微信/钉钉/飞书扫码登录提供规范的认证接口以便于其余利用集成SSO,平安的挪动接入,平安的API、第三方认证和互联网认证的整合。提供用户生命周期治理,反对SCIM 2协定;开箱即用的连接器(Connector)实现身份供应同步。简化微软Active Directory域控、规范LDAP服务器机构和账号治理,明码自助服务重置明码。认证多租户性能,反对团体下多企业独立治理或企业下不同部门数据隔离的,升高运维老本。认证核心具备平台无关性、环境多样性,反对Web、手机、挪动设施等, 如Apple iOS,Andriod等,将认证能力从B/S到挪动利用全面笼罩。基于Java EE平台,微服务架构,采纳Spring、MySQL、Tomcat、Redis、MQ等开源技术,扩展性强。开源、平安、自主可控,许可证 Apache 2.0 License &MaxKey版权申明。界面 下载以后版本百度网盘下载,历史版本 版本日期Docker网盘网盘提取码v 3.5.3 GA2022/07/23链接下载mxk9Roadmap序号打算工夫1Java 17+2022Q32Jakarta EE 9+2022Q33Spring Framework 62022Q44Spring Boot 32022Q4版本发行阐明MaxKey v 3.5.3 GA 2022/07/23 *(MAXKEY-220801) formbase用户初始化跳转问题修复     *(MAXKEY-220802) OAuth2 select多选保留问题     *(MAXKEY-220803) OAuth2明码保留不统一修复     *(MAXKEY-220804) HttpSessionListenerAdapter中userinfo空指针异样     *(MAXKEY-220805) 减少LDAP登录认证的参数配置     *(MAXKEY-220806) 组织减少公司、分支机构、部门、组四种类型     *(MAXKEY-220807) REDIS的key对立MXK_结尾     *(MAXKEY-220808) HttpRequestAdapter减少REST办法     *(MAXKEY-220809) 新增利用时减少初始化默认值     *(MAXKEY-220810) Ldap认证配置的判断     *(MAXKEY-220811) 前端refresh_token报错问题,Header可为空,在程序中进行判断     *(MAXKEY-220812) 新增或者编辑图片无奈上传的问题     *(MAXKEY-220813) JWT减少GET形式     *(MAXKEY-220814) 验证码字段调整     *(MAXKEY-220815) 账号治理中利用抉择未显示问题     *(MAXKEY-220816) 删除打jar包时maxkey-web-resources依赖问题     *(MAXKEY-220817) 依赖项援用、更新和降级         log4j                   2.18.0         tomcat                  9.0.64         druid                   1.2.11         druidspringboot         1.2.11 ...

July 22, 2022 · 1 min · jiezi

关于oauth2.0:OAuth20通俗理解

注:浏览之前忘掉受权码、token那一套,只了解这个模型参考起源 注释OAuth2.0提供了一种客户端获取资源的受权模型,这一模型解决了客户端在获取别人资源时不用要求资源持有者提供明确的身份验证信息,而把这个工作交给了一个可信赖的受权者,这个授权人对资源的持有人是可信赖的,对资源的托管方也是可信赖的。因而,OAuth2.0标准定义了四种角色: 首先,客户端的用意很明确,向资源服务器获取资源;这个时候客户端须要出示一个凭证——令牌;令牌由受权服务器颁发,资源服务器在拿到令牌之后本人想方法验证令牌的合法性;因而,客户端的上一步就须要想方法去受权服务器拿到 指标资源 的令牌受权服务器保留了资源所有者的身份信息,因而,客户端须要获得资源所有者的批准,从而向受权服务器拿到受权的凭证,这一步由客户端疏导资源所有者与受权服务器进行身份校验,促使受权服务器给客户端一个受权凭证(这个受权的凭证意味着资源所有者认可客户端的合法性,阐明客户端有获取令牌的资格);客户端在获得这个资格之后,能够释怀地向受权服务器申请资源凭证——令牌;在拿到令牌之后,客户端能够释怀地去资源服务器申请资源所有者指定的资源这么做有个前提:资源持有者认可以后客户端。这个就要靠持有人本人负责验证客户端的合法性了。这么做的益处就是:令牌的解释权在受权服务或资源服务器,资源持有人能够随时告诉两者,使令牌生效。

November 3, 2021 · 1 min · jiezi

关于oauth2.0:MaxKey单点登录认证系统-v240GA-发布

English | 中文 概述MaxKey(马克思的钥匙)单点登录认证零碎(Single Sign On System),寓意是最大钥匙,是业界当先的企业级开源IAM身份治理和身份认证产品,反对OAuth 2.0/OpenID Connect、SAML 2.0、JWT、CAS、SCIM等标准协议,提供简略、规范、平安和凋谢的用户身份治理(IDM)、身份认证(AM)、单点登录(SSO)、RBAC权限治理和资源管理等。 官方网站 官网 | 官网二线 QQ交换群:434469201 邮箱email: maxkeysupport@163.com 代码托管 GitHub | 码云(Gitee) 什么是单点登录(Single Sign On),简称为SSO? 用户只须要登录认证核心一次就能够拜访所有相互信任的利用零碎,无需再次登录。 次要性能: 1) 所有利用零碎共享一个身份认证零碎 2) 所有利用零碎可能辨认和提取ticket信息 产品个性规范认证协定:序号协定反对1.1OAuth 2.0/OpenID Connect高1.2SAML 2.0高1.3JWT高1.4CAS高1.5FormBased中1.6TokenBased(Post/Cookie)中1.7ExtendApi低1.8EXT低登录反对序号登录形式2.1动静验证码 字母/数字/算术2.2双因素认证2.3短信认证 腾讯云短信/阿里云短信/网易云信2.4登录易/Google/Microsoft Authenticator/FreeOTP/反对TOTP或者HOTP2.5Kerberos/SPNEGO/AD域2.6社交账号 微信/QQ/微博/钉钉/Google/Facebook/其余提供规范的认证接口以便于其余利用集成SSO,平安的挪动接入,平安的API、第三方认证和互联网认证的整合。提供用户生命周期治理,反对SCIM 2协定,基于Apache Kafka代理,通过连接器(Connector)实现身份供应同步。认证核心具备平台无关性、环境多样性,反对Web、手机、挪动设施等, 如Apple iOS,Andriod等,将认证能力从B/S到挪动利用全面笼罩。多种认证机制并存,各利用零碎可保留原有认证机制,同时集成认证核心的认证;利用具备高度独立性,不依赖认证核心,又可用应用认证核心的认证,实现单点登录。基于Java平台开发,采纳Spring、MySQL、Tomcat、Apache Kafka、Redis等开源技术,反对微服务,扩展性强。开源、平安、自主可控,许可证 Apache 2.0 License & MaxKey版权申明。界面MaxKey认证 登录界面   主界面   MaxKey治理 拜访报表   用户治理   利用治理   下载以后版本百度网盘下载, 历史版本 版本日期下载地址提取码v 2.4.0 GA2021/01/01链接下载i5xbRoadmap1.MaxKey Cloud(微服务版)-2021年 2.零信赖场景整合 ...

January 5, 2021 · 1 min · jiezi

关于oauth2.0:JustAuth-1159-版发布支持飞书喜马拉雅企业微信网页登录

新增修复并正式启用 飞书 平台的第三方登录AuthToken 类中新增 refreshTokenExpireIn 记录 refresh token 的有效期PR合并 Github #101:反对喜马拉雅登录合并 Github #105:反对企业微信网页受权登录合并 Github #107:增加AuthAlipayRequest网络代理结构器,解决 Github Issue #102批改批改喜马拉雅配置参数,将ClientOsType参数提到 AuthConfig 中AuthChecker 中减少对喜马拉雅平台的校验降级 facebook api 版本到 v9.0,解决 Gitee Issue #I2AR5S!!!留神!!!批改原来的企业微信 Request 类名为 AuthWeChatEnterpriseQrcodeRequest,降级后留神该点留神可能有些开发者对于 JA 集成的四个微信平台不太了解,这儿对立阐明: 依照类名AuthWeChatEnterpriseQrcodeRequest:企业微信二维码登录AuthWeChatEnterpriseWebRequest:企业微信网页登录AuthWeChatOpenRequest:微信开放平台AuthWeChatMpRequest:微信公众平台依照枚举WECHAT_ENTERPRISE:企业微信二维码登录WECHAT_ENTERPRISE_WEB:企业微信网页登录WECHAT_OPEN:微信开放平台WECHAT_MP:微信公众平台对于 Just AuthJustAuth,如你所见,它仅仅是一个第三方受权登录的工具类库,它能够让咱们脱离繁琐的第三方登录SDK,让登录变得So easy! 目前已反对Github、Gitee、微博、钉钉、百度、Coding、腾讯云开发者平台、OSChina、支付宝、QQ、微信、淘宝、Google、Facebook、抖音、领英、小米、微软、今日头条、Teambition、StackOverflow、Pinterest、人人、华为、企业微信、酷家乐、Gitlab、美团、饿了么和推特等第三方平台的受权登录。 Login, so easy! 局部用户 特点废话不多说,就俩字: 全:已集成十多家第三方平台(国内外罕用的根本都已蕴含),依然还在继续扩大中(开发计划)!简:API就是奔着最简略去设计的,尽量让您用起来没有阻碍感!我的项目源码库https://gitee.com/yadong.zhang/JustAuthhttps://github.com/justauth/JustAuth

January 2, 2021 · 1 min · jiezi

关于oauth2.0:OAuth-20

简介第三方认证解决方案。用来获取令牌和应用令牌的协定,自身不解决用户信息。用来受权第三方利用,获取用户数据,而不须要将用户名、明码提供给第三方。 场景第三方客户端拜访用户的资源,须要向用户申请受权。用户批准后,资源给第三方客户端令牌(access token)。令牌的权限范畴、有效期可控。 角色RolesResource Owner: User资源resource的拥有者,受权给第三方利用来拜访。这种受权有限度。Resource Server/Authorization Server: API资源服务器保留用户资源。受权服务器验证用户。Client: Application第三方利用。 协定示意整体流程 The application requests authorization to access service resources from the userIf the user authorized the request, the application receives an authorization grantThe application requests an access token from the authorization server (API) by presenting authentication of its own identity, and the authorization grantIf the application identity is authenticated and the authorization grant is valid, the authorization server (API) issues an access token to the application. Authorization is complete.The application requests the resource from the resource server (API) and presents the access token for authenticationIf the access token is valid, the resource server (API) serves the resource to the application第三方治理在资源零碎(Authorization Server)申请注册,会生成客户端凭证:客户端标识符(client identifier/client ID)、客户端秘钥(client secret)。 ...

November 21, 2020 · 1 min · jiezi

关于oauth2.0:手工方式执行OAuth20流程

后面一篇文章钻研了OAuth2.0标准的工作原理,纸上得来终觉浅,明天就以纯手工的形式跑下整个流程。 本示例以GitHub提供的OAuth2.0受权服务来进行试验,所以你必须先有一个GitHub账号,而且没有被墙,试验的步骤如下: 注册OAuth客户端获取Authorization Code获取Access Token应用Access Token拜访资源注册OAuth客户端先登录GitHub,顺次拜访 Settings->Developer settings->OAuth Apps,点击 New OAuth App注册页面 注册后果 注册次要关注三个参数: Authorization callback URL: 获取Authorization Code后的跳转地址Client ID:注册的客户端的惟一标识Client Secret:注册客户端的Secret信息获取Authorization Code获取Authorization Code为GET申请,能够间接拼装URL后在浏览器间接拜访,地址如下: https://github.com/login/oauth/authorize?client_id=9b4cxxxxxxx60b98cd21&redirect_uri=http://127.0.0.1:8000/oauth拜访后,先跳转到登录界面进行用户认证 认证后,间接进入受权确认页面 用户受权后,跳转到redirect_uri地址http://127.0.0.1:8000/oauth?code=ccxxx92a0cef2c024dd2注:这个地址不肯定须要可拜访,间接跳404也能够,次要是要能获取这个code字段。 获取Access Token应用上述个步骤获取的code,Client ID,Client Secret,因为这个是Post申请,所以须要借助Postman来发送(不过我测试了下,浏览器间接GET拜访也能够,会把返回数据当文件下载)。获取Access Token的地址和参数如下: 地址:https://github.com/login/oauth/access_token参数:client_id=xxx&client_secret=xxx&code=xxxPostman截图如下: 留神:基于设计平安,每个code只能申请一次Access Token,应用后就生效了。 应用Access Token拜访资源以获取用户信息为例,获取用户信息URL: https://api.github.com/user在申请头中减少Authorization字段,值为Bearer access_token_value Postman截图如下: 以上就是简略的通过OAuth2.0协定拜访GitHub资源的过程,这个过程还是比较顺利的。 另外在应用过程中,发现Postman曾经集成了OAuth2.0的受权过程,在申请的Authorization页签中抉择OAuth2.0,软件提供了一个填写Get New Access Token的表单。

September 4, 2020 · 1 min · jiezi

Spring-Security-Oauth2-验证和授权服务开发之MongoDBJWT

前言oauth2规范中具备了四种授权模式,分别如下: ·授权码模式:authorization code ·简化模式:implicit ·密码模式:resource owner password credentials ·客户端模式:client credentials 注:本示例只演示密码模式,感兴趣的同学自己花时间测试另外三种授权模式。 配置mongodb和jwt1、新建Application入口应用类 @SpringBootApplication@RestController@EnableEurekaClient// 该服务将作为OAuth2服务@EnableAuthorizationServer// 注意:不加@EnableResourceServer注解,下面user信息为空@EnableResourceServerpublic class Application { @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @GetMapping("/user") public Map<String, Object> user(OAuth2Authentication user){ Map<String, Object> userInfo = new HashMap<>(); userInfo.put("user", user.getUserAuthentication().getPrincipal()); userInfo.put("authorities", AuthorityUtils.authorityListToSet(user.getUserAuthentication().getAuthorities())); return userInfo; } public static void main(String[] args) { SpringApplication.run(Application.class, args); }}2、新建JWTOAuth2Config类 @Configurationpublic class JWTOAuth2Config extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private ClientDetailsService mongoClientDetailsService; @Autowired private UserDetailsService mongoUserDetailsService; @Autowired private TokenStore tokenStore; @Autowired private DefaultTokenServices tokenServices; // 将JWTTokenStore类中的JwtAccessTokenConverter关联到OAUTH2 @Autowired private JwtAccessTokenConverter jwtAccessTokenConverter; // 自动将JWTTokenEnhancer装配到TokenEnhancer类中 // token增强类,需要添加额外信息内容的就用这个类 @Autowired private TokenEnhancer jwtTokenEnhancer; @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { // /oauth/token // 如果配置支持allowFormAuthenticationForClients的,且url中有client_id和client_secret的会走 // ClientCredentialsTokenEndpointFilter来保护 // 如果没有支持allowFormAuthenticationForClients或者有支持但是url中没有client_id和client_secret的,走basic认证保护 security.tokenKeyAccess("permitAll()") .checkTokenAccess("permitAll()") .allowFormAuthenticationForClients(); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { // Spring Oauth 允许开发人员挂载多个令牌增强器,因此将令牌增强器添加到TokenEnhancerChain类中 // 设置jwt签名和jwt增强器到TokenEnhancerChain TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, jwtAccessTokenConverter)); endpoints.tokenStore(tokenStore) // 在jwt和oauth2服务器之间充当翻译(签名) .accessTokenConverter(jwtAccessTokenConverter) // 令牌增强器类:扩展jwt token .tokenEnhancer(tokenEnhancerChain) .authenticationManager(authenticationManager) .userDetailsService(mongoUserDetailsService); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // 使用mongodb保存客户端信息 clients.withClientDetails(mongoClientDetailsService); }}3、新建JWTTokenEnhancer令牌增强器类 ...

July 6, 2020 · 4 min · jiezi

在Spring-Boot中使用OAuth2保护REST服务

解释OAuth2技术正如我所说,我们将使用OAuth2协议,因此首先要解释这个协议是如何工作的。OAuth2有一些变体,但我将解释我将在程序中使用的内容,为此,我将给你一个例子,以便你了解我们打算做什么。举个例子,在商店里用信用卡付款。在这种场景下,有三个角色:商店、银行和我们。OAuth2协议中也发生了类似的事情,就像这样:1.客户或买方需要银行提供信用卡。然后,银行将收集我们的信息,核实我们是谁,并根据我们帐户中的资金向我们提供信用卡或者直接拒绝我们。在授予卡的OAuth2协议中,它称为身份验证服务器。2.如果银行给了我们卡,我们可以去商店,即网络服务器,我们提供信用卡。商店不欠我们任何东西,但他们可以通过读卡器向银行询问他们是否可以信任我们以及信用余额(信用余额)。商店是资源服务器。3.商店,根据银行说我们拥有的钱,将允许我们购买。在OAuth2类比中,Web服务器将允许我们访问页面,具体取决于我们的财务状况。如果您没有注意到通常使用身份验证服务器,当您转到网页并被要求注册时,它允许您通过Facebook或Google进行。Facebook或Google成为发行“卡”的“银行”,并会验证您的“信用”是否足够支付这个商品。您可以看到“El Pais”的网站并创建一个帐户。如果我们使用Google或Facebook,这个商店将依赖这些身份验证提供商提供的客户身份信息。在这种情况下,网站唯一需要的是拥有信用卡 - 无论余额如何创建授权服务器现在,让我们看看如何创建银行、商店以及您需要的所有其他内容。首先,在我们的项目中,我们需要具有适当的依赖关系。我们需要启动者:Cloud OAuth2,Security和Web。那么,让我们从定义银行开始; 这就是我们之前说的:  AuthorizationServerConfiguration:我们从 @ Configuration 标签开始,然后使用  @EnableAuthorizationServer 标记告诉Spring激活授权服务器。要定义服务器属性,我们指定我们的类扩展  AuthorizationServerConfigurerAdapter,实现了  AuthorizationServerConfigurerAdapter接口,所以Spring将使用这个类来参数化服务器。我们定义了一个Spring自动提供的AuthenticationManager ,我们将使用它来收集@Autowired标签。我们还定义了一个  TokenStore对象,作为public的功能 。 虽然 AuthenticationManager由Spring提供的,但我们必须自己配置它。我等等解释要如何完成这个配置。TokenStore或者IdentifierStore是身份验证服务器提供的标识符将存储的位置,因此当资源服务器(商店)要求信用卡上的信息时,身份验证服务器就要响应它。在这种情况下,我们使用  InMemoryTokenStore将标识符存储在内存中的类。在实际应用中,我们可以使用JdbcTokenStore将它们保存在数据库中,以便在应用程序发生故障时,客户端不必更新其信用卡。在功能配置中 (ClientDetailsServiceConfigurer clients),我们指定银行的凭证,包括身份验证的管理员,以及提供的服务。因为要访问银行,我们必须为每个提供的服务提供用户名和密码。这是一个非常重要的概念:用户名和密码来自银行,而不是客户。对于银行提供的每项服务,将进行单一认证,但对于不同的服务可能相同。我将详细说明这些内容: clients.inMemory ()指定我们将服务存储在内存中。在“真正的”应用程序中,我们将其保存在数据库,LDAP服务器等中。 withClient ("client")是我们将在银行中识别的用户。在这种情况下,它将被称为“客户端”。将他称为“用户”会不会更好? 要  uthorizedGrantTypes ("password", "authorization_code", "refresh_token", "implicit") ,我们指定配置定义的用户,对服务“ 客户端 ”。在我们的示例中,我们将仅使用密码服务。 authorities ("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT", "USER") 指定所提供服务包含的角色或组。我们也不会在我们的例子中使用它,所以让我们让它暂时运行。 scopes ("read", "write")  是服务的范围 - 我们也不会在我们的应用程序中使用它。 autoApprove (true)-如果您必须自动批准客户的请求,我们会说是,以使应用程序更简单。 secret (passwordEncoder (). encode ("password")) 是客户端的密码。请注意,调用我们稍后定义的编码函数来指定密码将保存在何种类型的加密中。的编码功能进行注释与@Bean标签因为Spring,当我们在HTTP请求中提供密码时,会查找一个  PasswordEncoder对象检查交付密码的有效性。最后,我们有一个函数   configure (AuthorizationServerEndpointsConfigurer endpoints) ,我们定义哪个身份验证控制器和标识符存储应该使用端点。澄清终点是我们将与我们的“银行”联系以请求卡的URL。现在,我们已经创建了我们的身份验证服务器,但是根据引入的凭据,我们仍然需要他知道我们是谁并将我们放在不同的组中的方式。好吧,为此,我们将使用与保护网页相同的类。现在,我们可以检查我们的授权服务器是否有效。让我们看看如何使用优秀的PostMan程序。我们将使用HTTP请求类型POST,表明我们要使用基本验证。在我们的示例中,我们将分别使用“client”和“password”来设置用户和密码,即“银行”的密码。在请求正文和form-url编码格式中,我们将介绍要请求的服务,用户名和密码。'access_token'“ 8279b6f2-013d-464a-b7da-33fe37ca9afb ”是我们的信用卡,是我们必须提供给我们的资源服务器(商店)以查看非公开的页面(资源)的信用卡。创建资源服务器(ResourceServer)现在我们有了信用卡,我们将创建接受该卡的商店。在我们的示例中,我们将使用Spring Boot在相同的程序中创建资源和身份验证服务器,它无需配置任何内容。如果像现实生活中一样,资源服务器在一个地方,而身份验证服务器在另一个地方,我们应该向资源服务器指出哪个是我们的“银行”以及如何与之交谈。但是,我们将把它留给另一个条目。资源服务器的唯一类是  ResourceServerConfiguration:由于身份验证和资源服务器在同一个程序中,我们只需要配置资源服务器的安全性。这是在函数中完成的:一旦我们创建了资源服务器,我们必须只创建服务,这些服务是通过这些行完成的:现在让我们看看验证的工作原理。首先,我们检查我们是否可以在没有任何验证的情况下访问“/ publica”:如果我尝试访问“/ private”页面,则会收到错误“401 unauthorized”,表示我们无权查看该页面,因此我们将使用我们的授权服务器给用户授权。如果我们可以看到我们的私人页面,那么让我们尝试管理员的页面:我们当然没办法看到管理员的界面。因此,我们将要求凭据管理员提供新令牌,但要向用户“管理员”表明身份。返回的令牌是:“ab205ca7-bb54-4d84-a24d-cad4b7aeab57。” 这样就没问题了,我们可以安全地去购物!现在,我们只需要设置商店并拥有产品。本人创业团队产品MadPecker,主要做BUG管理、测试管理、应用分发,有需要的朋友欢迎试用、体验!本文为MadPecker团队产品经理译制,转载请标明出处

September 9, 2019 · 1 min · jiezi

Spring-Cloud-OAuth2-实现自定义返回格式

实现效果 启用授权服务器@Configuration@EnableAuthorizationServerpublic class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter { //...... @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenEnhancer(new TokenEnhancer() { @Override public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) { //在此追加返回的数据 DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) oAuth2AccessToken; CustomUser user = (CustomUser) oAuth2Authentication.getDetails(); Map<String, Object> map = new LinkedHashMap<>(); map.put("nickname", user.getNickname()); map.put("mobile", user.getMobile()); map.put("avatar",user.getAvatar()); token.setAdditionalInformation(map); return oAuth2AccessToken; } }); } }创建响应实体@Data@NoArgsConstructor@AllArgsConstructor@JsonInclude(JsonInclude.Include.NON_NULL)public class Result { //是否成功 private boolean success; //返回码 private int code; //返回信息 private String msg; //返回数据 private Object data; public static Result build(Object data) { return new Result(true, 200, "操作成功",data); } }重写令牌申请接口@RestController@RequestMapping("/oauth")public class OauthController { @Autowired private TokenEndpoint tokenEndpoint; @GetMapping("/token") public Result getAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { return custom(tokenEndpoint.getAccessToken(principal, parameters).getBody()); } @PostMapping("/token") public Result postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { return custom(tokenEndpoint.postAccessToken(principal, parameters).getBody()); } //自定义返回格式 private Result custom(OAuth2AccessToken accessToken) { DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken; Map<String, Object> data = new LinkedHashMap(token.getAdditionalInformation()); data.put("accessToken", token.getValue()); if (token.getRefreshToken() != null) { data.put("refreshToken", token.getRefreshToken().getValue()); } return Result.build(data); }}项目源码https://gitee.com/yugu/demo-o... ...

September 7, 2019 · 1 min · jiezi

RFC6749-Oauth20-阅读笔记

为啥要写这篇最近有facebook好友的接入工作,重读了一遍oauth2.0。相较第一次读,对四种授权模式,主要是Authorization Code, Implicit Grant有了正确的理解。所以有了这篇。 1. 为什么需要Oauth2https://tools.ietf.org/html/r... OAuth addresses these issues by introducing an authorization layer and separating the role of the client from that of the resource owner. In OAuth, the client requests access to resources controlled by the resource owner and hosted by the resource server, and is issued a different set of credentials than those of the resource owner.关键是,OAuth 引入了一个授权层,用来分离两种不同的角色:客户端和资源所有者。从而资源所有者(用户)的credentials,主要是密码,不会暴露给客户端。 2. four roleshttps://tools.ietf.org/html/r... ...

June 23, 2019 · 2 min · jiezi

从0手写springCloud项目二-框架代码详解

写在前面前面一篇将springCloud所需的几个组件搭建起来了,接下来以user模块为例子主要记录一下项目中集成的技术,框架,和使用方式。我想从以下几个地方总结: mybatis-plus3lcn5.0.2liquibaseoauth2others(es,rabbitmq,全局异常处理,feign之间异常处理,logback,mysql,redis)集成mybatis-plus3可能有些同学常用的持久层框架是jpa,但是就我实践而言,mybatisplus好用的不是一丁点,个人建议用mybatisplus...现在plus3的版本支持的还是蛮多的:乐观锁,版本号,代码生成器,分页插件,热加载,通用枚举,自动填充,动态数据源....详见官网(https://mp.baomidou.com),而且3.0集成也比2.x简单了不少,最简单的只需要加一个pom坐标即可,但是如果需要个性化配置,我们还是要写config,当然也不麻烦,很简单的。 pom <!--mybatis-plus--><dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.1.0</version></dependency>yml //可以看到我的各个文件存放的路径分别在哪里 mybatis: type-aliases-package: cn.iamcrawler.crawler_common.domain.goddess mapper-locations: cn/iamcrawler/crawlergoddess/mapper/*Mapper.xml type-handlers-package: cn.iamcrawler.crawlergoddess.mapper.typehandler global-config: refresh-mapper: true 那么接下来继承plus的两个方法就可以了 /** * Created by liuliang on 2019/3/21. */public interface DataUserMapper extends BaseMapper<DataUser> {}@Service@Slf4jpublic class DataUserService extends ServiceImpl<DataUserMapper,DataUser>{}ps:大家可以想一想,spring的controller,service,mapper需要使用注解将其标识为bean才能扫描得到,而这里的mapper却没有添加@Repository注解,这是因为我在注解类上加了一个注解,指定spring启动的时候扫描到这个包下,所以这里就可以不用添加@Repository注解,也可以被spring扫描到啦。主类的注解如下: @SpringBootApplication@EnableEurekaClient@EnableFeignClients@EnableTransactionManagerServer@MapperScan("cn.iamcrawler.crawlergoddess.mapper")//上面说的,就是这个注解!public class CrawlerGoddessApplication { public static void main(String[] args) { SpringApplication.run(CrawlerGoddessApplication.class, args); }}是的,就是这么简单。plus最简单的集成就完成了,接下来继承mapper接口,service接口就可以写sql了。具体详见mybatis-plus官网。 ps:最后还有一个小知识点,其实大家可以看到,我的xml是放在和mapper一起的(默认是放在resource下面,打包成jar的时候路径就是我们常说的classpath),而我们知道,在maven打包jar的时候,是不会扫描这个java下的.xml文件的,为什么我打包jar也可以扫描呢?是因为我在maven里面配置了一个将java下的xml也打包的程序,如下: <build> <finalName>crawler-goddess</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> <!---我上面说的就是下面这个配置哦--> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> <resource> <directory>src/main/resources</directory> </resource> </resources> </build>集成lcn5.0.2微服务集成分布式事务框架已经不是一个新鲜的事情,在这方面,我前面也写过好几个文章介绍过,其中有springboot1.5.9+lcn4.1.0( https://segmentfault.com/a/11... ), 有springboot2.1.3+lcn5.0.2( https://segmentfault.com/a/11... )。大家可以按需阅读并配置,当然这里我这个demo配置的是5.0.2版本.毕竟这个版本比4.1.0强大了不少,并且配置也更加方便。具体大家也可以参照lcn官网,描述的非常详细(http://www.txlcn.org) ...

June 18, 2019 · 1 min · jiezi

laravel前后端分离获取微信授权结合laravelwechat

1、开始之前,请一定仔细阅读微信开发者文档文档中,总共写了几个步骤: 1、通过appId和需要跳转的路由去请求授权2、授权之后跳转路由中返回的code 注:前端只需要知道这两个步骤3、根据code获取access_token4、根据access_token获取用户信息(snsapi_userinfo授权)2、前端发起授权请求。这一步需要前端拼凑路由,并且将页面跳转到拼凑路由,路由规则如:https://open.weixin.qq.com/connect/oauth2/authorize?appid=你的公众appId号&redirect_uri=你的回调路由&response_type=code&scope=你选择的方式&state=STATE#wechat_redirect注 授权方式可选择为snsapi_userinfo或者snsapi_base,差别请看文档跳转之后授权页面如下(开发者工具效果) 3、点击同意之后,会根据你之前拼凑的回调路由返回code,如下:http://test.***.com/index?code=021Azdiu12zdXd05kkju1ZYkiu1AzdiR&state=1 4、将路由中的code直接传递给后端,让后端做获取用户信息的系列的逻辑处理。注:如下是laravel中间件中处理方式,session只用于这次请求,也可以将用户的微信信息放在request中到controller进行逻辑处理,看个人喜好 public function handle($request, Closure $next, $scopes = null) { $wechatCacheKey = 'wechat.oauth_user.default'; if (config("qa.mock_user") == 1){ $user = new SocialiteUser(config('wechat.mock_user')); } else { $code = $request->get("code", ""); if ($code === ""){ $appId = $this->config["app_id"]; return Response::toJson(["aid" => $appId], "请重新获取授权CODE!",10006); } // 开始拉取用户信息 $app = Factory::officialAccount($this->config); $user = $app->oauth->user(); } session([$wechatCacheKey => $user]); } return $next($request); }注:这个例子只是写了授权的逻辑,token相关验证我已经做了剔除 坑点:1、vue的路由会将code拼接在url和#之间,如www.****.com/?code=XXXXX/#/index,这个code需要单独处理

June 14, 2019 · 1 min · jiezi

企业微信登陆

引言用户登陆时,原设计是使用工号加密码进行登陆,只是工号不好记,为了推广,设计了企业微信登陆。 企业微信中可以设置自建应用,其实就是内嵌了一个Chrome,点击左侧的自建应用,会在右侧浏览器显示相关应用,所有工作都放在企业微信中,需实现当前企业微信账号自动登陆系统。 开发的过程很坎坷。让微信折腾的一点脾气都没有。 当时不会调试,因为企业微信中的自建应用要求设置成线上地址,写好了,打包,传给服务器,然后再测试。 五点,觉得还有十分钟就写完了,写完了就去吃饭。 六点、八点,改到九点半,都要改哭了,还是不好使,最后放弃了。 后来邢彦年师兄帮我梳理流程,潘老师教我调试方法,才完成这个功能。 感谢邢彦年师兄和潘老师。 实现文档找文档一定要找对地方,两个API,一个服务端,一个客户端。 最开始我以为是使用客户端的API呢?点进去学了学,企业微信小程序可用的API接口,这个用不了,此应用不是小程序。然后JS-SDK不支持登陆授权。 相关文档在服务端API中的身份认证一节中。 OAuth 2.0当我们用微信登陆某个网站时,会出现类似这样的授权页面。 点击确认登陆,该应用就能获取到用户相关的信息。 用户通过用户名和密码的方式进行微信的使用,第三方应用想获取微信用户信息,不是通过用户名密码,而是微信提供的令牌,这就是OAuth 2.0,既可以让应用获取不敏感的信息,又可以保证账户的安全性。 更多内容可学习阮一峰的博客,写得非常好:OAuth 2.0 的一个简单解释 - 阮一峰的网络日志 登陆流程用户点开应用,实际上是访问当前系统微信授权的入口 微信网页授权地址:https://open.weixin.qq.com/connect/oauth2/authorize 参数说明appid企业的CorpIDredirect_uri授权后的回调地址,需使用urlencode处理response_type回调数据返回类型scope应用授权作用域。企业自建应用固定填写:snsapi_basestate回调时额外的参数,非必选#wechat_redirect终端使用此参数判断是否需要带上身份信息看这个表格也是在无聊,下面是我配置好的微信授权链接,大家只需将相应参数改写即可。注:回调的url一定要使用encodeURIComponent处理! https://open.weixin.qq.com/connect/oauth2/authorize?appid=xxxxxxxx&redirect_uri=https%3A%2F%2Falice.dgitc.net%2Fsetup%2Fwechat&response_type=code&scope=snsapi_base#wechat_redirect用户静默授权成功,跳转到回调地址用户授权成功后,会带着code参数重定向到回调地址。 类似这样: https://alice.dgitc.net/setup/wechat?code=xxxxxx前台的组件就通过路由获取到了code,然后通过code去进一步获取用户信息。 const code = this.route.snapshot.queryParamMap.get('code');this.userService.enterpriseWeChatLogin(code).subscribe(...);后台通过code找微信后台获取用户信息这个是分成两次获取,先获取access_token,再通过access_token和code获取用户信息。 GET请求 https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ID&corpsecret=SECRET这个是获取access_token的地址,获取access_token的过程是重点! 上面传的参数名是corpid和corpsecret,企业id和密钥。 这是企业微信的设计,企业id是一个,标识这个企业,每一个功能模块都有相应的secret。 然后企业id和secret配对,获取到只能访问这个功能模块的一个access_token。 就拿当前Alice系统来举例,自建应用Alice存在secret,通过此secret和corpid获取到access_token,即相当于拿到了受保护API的访问权限。 因为这个access_token是通过Alice应用的secret获取到的,所以再用它访问其他的功能,是不合法的。 access_token有访问频率限制,所以设计了一套缓存方案。 @Overridepublic String getAccessTokenBySecret(String secret) { logger.debug("从缓存中获取令牌"); String access_token = this.cacheService.get(secret); logger.debug("如果获取到了,直接返回"); if (null != access_token) { return access_token; } logger.debug("否则,发起HTTP请求"); logger.debug("获取企业验证信息"); String url = enterpriseInformation.getAccessTokenUrl(); String corpId = enterpriseInformation.getCorpid(); logger.debug("获取认证令牌"); ResponseEntity<EnterpriseAuth> responseEntity = restTemplate.getForEntity(url + "?corpid=" + corpId + "&corpsecret=" + secret, EnterpriseAuth.class); logger.debug("如果请求成功"); if (responseEntity.getStatusCode().is2xxSuccessful()) { logger.debug("获取响应体"); EnterpriseAuth enterpriseAuth = responseEntity.getBody(); Assert.notNull(enterpriseAuth, "微信令牌为空"); logger.debug("如果微信请求成功"); if (enterpriseAuth.successed()) { logger.debug("存储缓存,返回令牌"); access_token = enterpriseAuth.getAccessToken(); this.cacheService.put(secret, access_token, enterpriseAuth.getExpiresIn(), TimeUnit.SECONDS); return access_token; } } logger.debug("请求失败,返回空令牌"); return "";}缓存是通过把Redis工具类包装了一下实现的,很简单。 ...

May 29, 2019 · 2 min · jiezi

基于Spring-Security-Oauth2的SSO单点登录JWT权限控制实践

概 述在前文《基于Spring Security和 JWT的权限系统设计》之中已经讨论过基于 Spring Security和 JWT的权限系统用法和实践,本文则进一步实践一下基于 Spring Security Oauth2实现的多系统单点登录(SSO)和 JWT权限控制功能,毕竟这个需求也还是蛮普遍的。 代码已开源,放在文尾,需要自取理论知识在此之前需要学习和了解一些前置知识包括: Spring Security:基于 Spring实现的 Web系统的认证和权限模块OAuth2:一个关于授权(authorization)的开放网络标准单点登录 (SSO):在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统JWT:在网络应用间传递信息的一种基于 JSON的开放标准((RFC 7519),用于作为JSON对象在不同系统之间进行安全地信息传输。主要使用场景一般是用来在 身份提供者和服务提供者间传递被认证的用户身份信息要完成的目标目标1:设计并实现一个第三方授权中心服务(Server),用于完成用户登录,认证和权限处理目标2:可以在授权中心下挂载任意多个客户端应用(Client)目标3:当用户访问客户端应用的安全页面时,会重定向到授权中心进行身份验证,认证完成后方可访问客户端应用的服务,且多个客户端应用只需要登录一次即可(谓之 “单点登录 SSO”)基于此目标驱动,本文设计三个独立服务,分别是: 一个授权服务中心(codesheep-server)客户端应用1(codesheep-client1)客户端应用2(codesheep-client2)多模块(Multi-Module)项目搭建三个应用通过一个多模块的 Maven项目进行组织,其中项目父 pom中需要加入相关依赖如下: <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.0.8.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>io.spring.platform</groupId> <artifactId>platform-bom</artifactId> <version>Cairo-RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.SR2</version> <type>pom</type> <scope>import</scope> </dependency></dependencies>项目结构如下: 授权认证中心搭建授权认证中心本质就是一个 Spring Boot应用,因此需要完成几个大步骤: pom中添加依赖<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency></dependencies>项目 yml配置文件:server: port: 8085 servlet: context-path: /uac即让授权中心服务启动在本地的 8085端口之上 创建一个带指定权限的模拟用户@Componentpublic class SheepUserDetailsService implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { if( !"codesheep".equals(s) ) throw new UsernameNotFoundException("用户" + s + "不存在" ); return new User( s, passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_NORMAL,ROLE_MEDIUM")); }}这里创建了一个用户名为codesheep,密码 123456的模拟用户,并且赋予了 普通权限(ROLE_NORMAL)和 中等权限(ROLE_MEDIUM) ...

May 7, 2019 · 2 min · jiezi

认识JWT

1.JSON Web Token是什么JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。 2.什么时候你应该用JSON Web Tokens下列场景中使用JSON Web Token是很有用的: Authorization (授权) : 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。Information Exchange (信息交换) : 对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWTs可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。3.JSON Web Token的结构是什么样的 JSON Web Token由三部分组成,它们之间用圆点(.)连接。这三部分分别是: HeaderPayloadSignature因此,一个典型的JWT看起来是这个样子的: xxxxx.yyyyy.zzzzz接下来,具体看一下每一部分: Header header典型的由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。 例如: 然后,用Base64对这个JSON编码就得到JWT的第一部分 Payload JWT的第二部分是payload,它包含声明(要求)。声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型: registered, public 和 private。 Registered claims : 这里有一组预定义的声明,它们不是强制的,但是推荐。比如:iss (issuer), exp (expiration time), sub (subject), aud (audience)等。Public claims : 可以随意定义。Private claims : 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明。下面是一个例子: 对payload进行Base64编码就得到JWT的第二部分 注意,不要在JWT的payload或header中放置敏感信息,除非它们是加密的。 Signature 为了得到签名部分,你必须有编码过的header、编码过的payload、一个秘钥,签名算法是header中指定的那个,然对它们签名即可。 例如: HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)签名是用于验证消息在传递过程中有没有被更改,并且,对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方。 ...

May 3, 2019 · 1 min · jiezi

spring security集成cas

spring security集成cas源码地址在文章末尾,转载请注明出处,谢谢。0.配置本地ssl连接操作记录如下:=====================1.创建证书文件thekeystore ,并导出为thekeystore.crtcd C:\Users\23570\keystoreC:\Users\23570\keystore>keytool -genkey -keyalg RSA -alias thekeystore -keystore thekeystore输入密钥库口令:changeit再次输入新口令:changeit您的名字与姓氏是什么? [Unknown]: localhost您的组织单位名称是什么? [Unknown]: localhost您的组织名称是什么? [Unknown]:您所在的城市或区域名称是什么? [Unknown]:您所在的省/市/自治区名称是什么? [Unknown]:该单位的双字母国家/地区代码是什么? [Unknown]:CN=localhost, OU=localhost, O=Unknown, L=Unknown, ST=Unknown, C=Unknown是否正确? [否]: y输入 <thekeystore> 的密钥口令 (如果和密钥库口令相同, 按回车):Warning:JKS 密钥库使用专用格式。建议使用 “keytool -importkeystore -srckeystore thekeystore -destkeystore thekeystore -deststoretype pkcs12” 迁移到行业标准格式 PKCS12。C:\Users\23570\keystore>keytool -export -alias thekeystore -file thekeystore.crt -keystore thekeystore输入密钥库口令:存储在文件 <thekeystore.crt> 中的证书Warning:JKS 密钥库使用专用格式。建议使用 “keytool -importkeystore -srckeystore thekeystore -destkeystore thekeystore -deststoretype pkcs12” 迁移到行业标准格式 PKCS12。======================2.把证书文件导入到本地证书库中,注意切换JRE相应目录切换为【管理员身份】运行以下命令:C:\Users\23570\keystore>keytool -import -alias thekeystore -storepass changeit -file thekeystore.crt -keystore “C:\Program Files\Java\jdk1.8.0_191\jre\lib\security\cacerts"所有者: CN=localhost, OU=localhost, O=Unknown, L=Unknown, ST=Unknown, C=Unknown发布者: CN=localhost, OU=localhost, O=Unknown, L=Unknown, ST=Unknown, C=Unknown序列号: 657eb9ce有效期为 Fri Mar 29 11:50:08 CST 2019 至 Thu Jun 27 11:50:08 CST 2019证书指纹: MD5: 8D:3C:78:E9:8A:44:77:3F:C2:8B:20:95:C7:6C:91:8F SHA1: 69:F3:46:C4:03:95:E1:D0:E6:9D:8B:72:F4:EB:ED:13:8B:9A:6A:38 SHA256: 79:D1:F8:B2:1B:E3:AF:D4:4F:35:CB:6B:C8:84:3F:85:21:13:0F:96:4A:B5:E5:4C:47:11:44:21:8F:F3:2D:83签名算法名称: SHA256withRSA主体公共密钥算法: 2048 位 RSA 密钥版本: 3扩展:#1: ObjectId: 2.5.29.14 Criticality=falseSubjectKeyIdentifier [KeyIdentifier [0000: B0 38 1D 00 56 65 EE 98 7C 35 58 04 B5 2E C0 A0 .8..Ve…5X…..0010: D5 C2 C5 B5 ….]]是否信任此证书? [否]: y证书已添加到密钥库中=========================3.配置tomcat/conf/server.xml中的ssl连接<Connector port=“8443” protocol=“org.apache.coyote.http11.Http11NioProtocol” maxThreads=“200” SSLEnabled=“true” scheme=“https” secure=“true” clientAuth=“false” sslProtocol=“TLS” keystoreFile=“C:\Users\23570\keystore\thekeystore” keystorePass=“changeit”/> ==========================4.其他命令参考删除JRE中指定别名的证书keytool -delete -alias cas.server.com -keystore “C:\Program Files\Java\jdk1.8.0_191\jre\lib\security\cacerts"查看JRE中指定别名的证书keytool -list -v -keystore “C:\Program Files\Java\jdk1.8.0_191\jre\lib\security\cacerts” -alias cas.server.com 1.cas服务搭建git clone –branch 5.3 https://github.com/apereo/cas-overlay-template.git cas-server注意:这里选用cas server 5.3版本,使用maven构建1.使用数据库账号密码登录cas导入依赖<dependency> <groupId>org.apereo.cas</groupId> <artifactId>cas-server-support-jdbc</artifactId> <version>${cas.version}</version></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version></dependency>配置查询#这里是配置用户表单登录时用户名字段为usernamecas.authn.jdbc.query[0].sql=select password from oauth_account left join oauth_user on oauth_account.user_id=oauth_user.user_id where oauth_user.username=?;cas.authn.jdbc.query[0].fieldPassword=passwordcas.authn.jdbc.query[0].fieldExpired=expiredcas.authn.jdbc.query[0].fieldDisabled=disabledcas.authn.jdbc.query[0].dialect=org.hibernate.dialect.MySQLDialectcas.authn.jdbc.query[0].driverClass=com.mysql.jdbc.Drivercas.authn.jdbc.query[0].url=jdbc:mysql://127.0.0.1:3306/srm-aurora2?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=falsecas.authn.jdbc.query[0].user=rootcas.authn.jdbc.query[0].password=root#默认不加密#cas.authn.jdbc.query[0].passwordEncoder.type=NONE#默认加密策略,通过encodingAlgorithm来指定算法,默认NONE不加密cas.authn.jdbc.query[0].passwordEncoder.type=DEFAULTcas.authn.jdbc.query[0].passwordEncoder.characterEncoding=UTF-8cas.authn.jdbc.query[0].passwordEncoder.encodingAlgorithm=MD5#配置用户表单登录时用户名字段为phonecas.authn.jdbc.query[1].sql=select password from oauth_account left join oauth_user on oauth_account.user_id=oauth_user.user_id where oauth_user.phone=?;cas.authn.jdbc.query[1].fieldPassword=passwordcas.authn.jdbc.query[1].fieldExpired=expiredcas.authn.jdbc.query[1].fieldDisabled=disabledcas.authn.jdbc.query[1].dialect=org.hibernate.dialect.MySQLDialectcas.authn.jdbc.query[1].driverClass=com.mysql.jdbc.Drivercas.authn.jdbc.query[1].url=jdbc:mysql://127.0.0.1:3306/srm-aurora2?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=falsecas.authn.jdbc.query[1].user=rootcas.authn.jdbc.query[1].password=root#默认不加密#cas.authn.jdbc.query[0].passwordEncoder.type=NONE#默认加密策略,通过encodingAlgorithm来指定算法,默认NONE不加密cas.authn.jdbc.query[1].passwordEncoder.type=DEFAULTcas.authn.jdbc.query[1].passwordEncoder.characterEncoding=UTF-8cas.authn.jdbc.query[1].passwordEncoder.encodingAlgorithm=MD5数据库脚本/* Navicat Premium Data Transfer Source Server : localhost Source Server Type : MySQL Source Server Version : 50722 Source Host : localhost:3306 Source Schema : srm-aurora2 Target Server Type : MySQL Target Server Version : 50722 File Encoding : 65001 Date: 19/04/2019 14:40:52*/SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS = 0;– —————————— Table structure for oauth_account– —————————-DROP TABLE IF EXISTS oauth_account;CREATE TABLE oauth_account ( account_id int(255) NOT NULL AUTO_INCREMENT, tenant_id int(255) NULL DEFAULT NULL, user_id int(255) NULL DEFAULT NULL, password varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (account_id) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;– —————————— Records of oauth_account– —————————-INSERT INTO oauth_account VALUES (1, 1, 1, ’e10adc3949ba59abbe56e057f20f883e’);INSERT INTO oauth_account VALUES (2, 2, 2, ’e10adc3949ba59abbe56e057f20f883e’);– —————————— Table structure for oauth_cas_info– —————————-DROP TABLE IF EXISTS oauth_cas_info;CREATE TABLE oauth_cas_info ( cas_id int(255) NOT NULL, tenant_id int(255) NULL DEFAULT NULL, cas_server varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, cas_server_login varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, cas_server_logout varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, cas_service varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, cas_service_logout varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (cas_id) USING BTREE) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;– —————————— Records of oauth_cas_info– —————————-INSERT INTO oauth_cas_info VALUES (1, 2, ‘https://localhost:8443/cas’, ‘https://localhost:8443/cas/login?service=http%3A%2F%2Flocalhost%3A8083%2Flogin%2Fcas’, ‘https://localhost:8443/cas/logout’, ‘http://localhost:8083/login/cas’, ‘https://localhost:8443/cas/logout?service=http://localhost:8083/logout/success’);INSERT INTO oauth_cas_info VALUES (2, 3, ‘https://localhost:9443/sso’, ‘https://localhost:9443/sso/login?service=http%3A%2F%2Flocalhost%3A8083%2Flogin%2Fcas’, ‘https://localhost:9443/sso/logout’, ‘http://localhost:8083/login/cas’, ‘https://localhost:9443/sso/logout?service=http://localhost:8083/logout/success’);– —————————— Table structure for oauth_tenant– —————————-DROP TABLE IF EXISTS oauth_tenant;CREATE TABLE oauth_tenant ( tenant_id int(255) NOT NULL AUTO_INCREMENT, domain varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, name varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, login_provider varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, login_type varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (tenant_id) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;– —————————— Records of oauth_tenant– —————————-INSERT INTO oauth_tenant VALUES (1, ‘http://localhost:8084/’, ‘a租户’, ‘oauth’, ‘form’);INSERT INTO oauth_tenant VALUES (2, ‘http://localhost:8085/’, ‘b租户’, ‘cas’, ‘wechat’);INSERT INTO oauth_tenant VALUES (3, ‘http://localhost:8086/’, ‘c租户’, ‘cas’, ‘form’);– —————————— Table structure for oauth_user– —————————-DROP TABLE IF EXISTS oauth_user;CREATE TABLE oauth_user ( user_id int(255) NOT NULL AUTO_INCREMENT, username varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, phone varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, email varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (user_id) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;– —————————— Records of oauth_user– —————————-INSERT INTO oauth_user VALUES (1, ‘22304’, ‘15797656200’, ‘donglin.ling@hand-china.com’);INSERT INTO oauth_user VALUES (2, ‘admin’, ‘15797656201’, ’ericling666@gmail.com’);SET FOREIGN_KEY_CHECKS = 1;发布cas server,访问:https://localhost:8443/cas/login测试账号和密码,admin:1234562.CAS客户端服务注册这里演示通过json文件注册服务,实际项目中,可以配置成从数据库中注册添加json支持依赖<!–json服务注册–><dependency> <groupId>org.apereo.cas</groupId> <artifactId>cas-server-support-json-service-registry</artifactId> <version>${cas.version}</version></dependency>添加json服务注册文件{ “@class” : “org.apereo.cas.services.RegexRegisteredService”, “serviceId” : “^(https|http|imaps)://.”, “name” : “HTTPS and HTTP and IMAPS”, “id” : 10000001, “description” : “This service definition authorizes all application urls that support HTTPS and HTTP and IMAPS protocols.”, “evaluationOrder” : 10000, “attributeReleasePolicy”: { “@class”: “org.apereo.cas.services.ReturnAllAttributeReleasePolicy” }, “proxyPolicy”: { “@class”: “org.apereo.cas.services.RegexMatchingRegisteredServiceProxyPolicy”, “pattern”: “^(https|http)?://.” }}注意文件目录和文件名格式:目录:resources/services/{xxx}-{id}.jsonxxx表示可以随意配置,后面-{id},这里的id需要和文件中的id一致。作为演示,这个json注册文件,没有限制域名,也就是说所有的服务都可以注册成功。开启json服务注册### 开启json服务注册#cas.serviceRegistry.initFromJson=true以上就是配置json服务注册的过程。3.其它常用配置### 登出后允许跳转到指定页面#cas.logout.followServiceRedirects=true# 设置service ticket的行为# cas.ticket.st.maxLength=20# cas.ticket.st.numberOfUses=1cas.ticket.st.timeToKillInSeconds=120# 设置proxy ticket的行为cas.ticket.pt.timeToKillInSeconds=120# cas.ticket.pt.numberOfUses=1配置说明:配置cas服务登出时,是否跳转到各个子服务的登出页面,默认false【即默认情况下,子服务点击登出,用户统一跳转到cas的登出页面】,子服务登出时访问cas登出端点,并带上service。示例:https://localhost:8443/cas/logout?service=http://localhost:8083/logout/success这样配置,cas注销session之后,会重定向到service。这个字段可以配置,默认是service。配置如下:cas.logout.redirectParameter=service配置service ticket的失效时间,我这里配置这个选项,是为了方便后面debug调试,实际生产中,不必配置这个选项。更多常用配置项,请查看官网链接:https://apereo.github.io/cas/…2.spring security和cas集成1.依赖和其他配置核心依赖<!–security-cas集成–><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-cas</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency>application.yml配置# 我这里是为了方便调试logging.level.org.springframework.security: debuglogging.level.web: debug2.配置登录端点spring security开启表单登陆@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin().loginPage("/login”); }这个配置,会开启用户表单登录,并且配置登录端点为/login配置登录端点响应逻辑@Controllerpublic class LoginEndpointConfig { @Autowired private TenantService tenantService; @Autowired private CasInfoService casInfoService; @GetMapping("/login”) public String loginJump(HttpSession session) { final String SAVED_REQUEST = “SPRING_SECURITY_SAVED_REQUEST”; Object attribute = session.getAttribute(SAVED_REQUEST); if (attribute == null) { //默认跳转到登陆页面 return “login”; } if (attribute instanceof DefaultSavedRequest) { DefaultSavedRequest savedRequest = (DefaultSavedRequest) attribute; List<String> referer = savedRequest.getHeaderValues(“referer”); if (referer.size() == 1) { //有referer请求头 String domain = referer.get(0); Tenant tenant = tenantService.selectByDomain(domain); if (tenant == null) { return “login”; } else { String loginProvider = tenant.getLoginProvider(); switch (loginProvider) { case “cas”: //获取cas地址 CasInfo casInfoByTenantId = casInfoService.getCasInfoByTenantId(tenant.getTenantId()); String casServerLogin = casInfoByTenantId.getCasServerLogin(); session.setAttribute(“casInfoByTenantId”,casInfoByTenantId); return “redirect:” + casServerLogin; case “oauth”: return “login”; default: return “login”; } } } else { return “login”; } } return “login”; }}我这里的登陆逻辑实现了:用户从第三方网站【平台的租户】跳转到这个网站时,根据跳转过来的请求头【referer】获取这个租户的域名,再从数据库中查找这个域名对应的租户信息和登录逻辑。这里的租户信息有一个关键字段是:loginProvider,有两种情况cas,oauthcas:租户有自己的cas单点登录系统,平台需要和租户的cas集成oauth:租户没有cas,使用平台统一的表单登陆具体的登录流程分析,在最后详细介绍,这里不过多讲解。3.配置CAS的ticket校验以及登录响应自定义AuthenticationFilter因为我的需求是,每个租户有自己的cas系统,所以每个cas地址不一样,不可能使用官方的CasAuthenticationFilter 。具体原因是,官方的CasAuthenticationFilter在应用程序启动时,资源匹配器就已经初始化好了,它只会对特定的cas地址发送ticket校验请求。而要做到可配置,就只能自己实现这个逻辑,并且可配置的对相应cas server地址发出ticket校验请求。public class CustomCasAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private final static String endpoint = “/login/cas”; private UserDetailsService userDetailsService; public CustomCasAuthenticationFilter(String defaultFilterProcessesUrl, UserDetailsService userDetailsService) { super(defaultFilterProcessesUrl); this.userDetailsService = userDetailsService; } private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler(); public CustomCasAuthenticationFilter() { super(new AntPathRequestMatcher(endpoint)); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; if (!requiresAuthentication(req, res)) { chain.doFilter(request, response); return; } String ticket = obtainArtifact(req); //开始校验ticket try { CasInfo casInfo = (CasInfo) req.getSession().getAttribute(“casInfoByTenantId”); if (StringUtils.hasText(casInfo.getCasServer())) { //获取当前项目地址 String service; int port = request.getServerPort(); if (port != 80) { service = request.getScheme() + “://” + request.getServerName() + “:” + request.getServerPort() + endpoint; } else { service = request.getScheme() + “://” + request.getServerName() + endpoint; } //开始校验ticket Assertion validateResult = getTicketValidator(casInfo.getCasServer()).validate(ticket, service); //根据校验结果,获取用户详细信息 UserDetails userDetails = null; try { userDetails = userDetailsService.loadUserByUsername(validateResult.getPrincipal().getName()); if (this.logger.isDebugEnabled()) { logger.debug(“userDetailsServiceImpl is loading username:"+validateResult.getPrincipal().getName()); } } catch (UsernameNotFoundException e) { unsuccessfulAuthentication(req, res, e); } //手动封装authentication对象 assert userDetails != null; UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(validateResult.getPrincipal(), ticket, userDetails.getAuthorities()); authentication.setDetails(userDetails); successfulAuthentication(req,res,chain,authentication); } else { unsuccessfulAuthentication(req, res, new BadCredentialsException(“bad credential:ticket校验失败”)); } } catch (TicketValidationException e) { //ticket校验失败 unsuccessfulAuthentication(req, res, new BadCredentialsException(e.getMessage())); }// chain.doFilter(request, response); } /** / public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { return null; } /* * 从HttpServletRequest请求中获取ticket / private String obtainArtifact(HttpServletRequest request) { String artifactParameter = “ticket”; return request.getParameter(artifactParameter); } /* * 获取Cas30ServiceTicketValidator,暂时没有实现代理凭据 / private TicketValidator getTicketValidator(String casServerUrlPrefix) { return new Cas30ServiceTicketValidator(casServerUrlPrefix); } protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { if (this.logger.isDebugEnabled()) { this.logger.debug(“Authentication success. Updating SecurityContextHolder to contain: " + authResult); } SecurityContextHolder.getContext().setAuthentication(authResult); if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); } this.successHandler.onAuthenticationSuccess(request, response, authResult); } protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.clearContext(); if (this.logger.isDebugEnabled()) { this.logger.debug(“Authentication request failed: " + failed.toString(), failed); this.logger.debug(“Updated SecurityContextHolder to contain null Authentication”); this.logger.debug(“Delegating to authentication failure handler " + this.failureHandler); } this.failureHandler.onAuthenticationFailure(request, response, failed); }}2. 把自定义的CustomCasAuthenticationFilter添加到spring security的过滤器链中@Qualifier(“userDetailsServiceImpl”) @Autowired private UserDetailsService userDetailsService;private final static String endpoint = “/login/cas”;@Override protected void configure(HttpSecurity http) throws Exception { http.addFilterAt(new CustomCasAuthenticationFilter(endpoint, userDetailsService), UsernamePasswordAuthenticationFilter.class);}### 4.配置单点登出1. 自定义实现LogoutFilterpublic class CustomLogoutFilter extends GenericFilterBean { private RequestMatcher logoutRequestMatcher; private SimpleUrlLogoutSuccessHandler urlLogoutSuccessHandler; private LogoutHandler logoutHandler = new SecurityContextLogoutHandler(); //获取casInfo信息,依此来判断当前认证用户的cas地址 private CasInfoService casInfoService; public CustomLogoutFilter(String filterProcessesUrl, String logoutSuccessUrl,CasInfoService casInfoService) { this.logoutRequestMatcher = new AntPathRequestMatcher(filterProcessesUrl); this.urlLogoutSuccessHandler=new SimpleUrlLogoutSuccessHandler(); this.urlLogoutSuccessHandler.setDefaultTargetUrl(logoutSuccessUrl); this.casInfoService = casInfoService; } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; if (requiresLogout(request, response)) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (logger.isDebugEnabled()) { logger.debug(“Logging out user ‘” + auth + “’ and transferring to logout destination”); } //本地登出 logoutHandler.logout(request,response,auth); if (auth == null) { urlLogoutSuccessHandler.onLogoutSuccess(request,response, null); }else{ //判断是否通过cas认证,获取cas信息 Object details = auth.getDetails(); if (details == null) { urlLogoutSuccessHandler.onLogoutSuccess(request,response,auth); } if (details instanceof UserDetails) { Integer tenantId = ((UserDetailsVO) details).getTenant().getTenantId(); CasInfo casInfoByTenantId = casInfoService.getCasInfoByTenantId(tenantId); response.sendRedirect(casInfoByTenantId.getCasServiceLogout()); }else{ urlLogoutSuccessHandler.onLogoutSuccess(request,response,auth); } } return; } filterChain.doFilter(request, response); } /* * 当前请求是否为登出请求 */ private boolean requiresLogout(HttpServletRequest request, HttpServletResponse response) { return logoutRequestMatcher.matches(request); }}2. 把CustomLogoutFilter添加到spring security的过滤器链中@Override protected void configure(HttpSecurity http) throws Exception { http.addFilterAt(new CustomLogoutFilter("/logout”, “/logout/success”, casInfoService), LogoutFilter.class);}### 5.流程分析#### 1.表单登陆流程分析目前有5个服务cas server,tenant-a,tenant-b,tenant-c,a2-oauth租户a,b,c就是一个超链接而已,为了模拟三个租户的域名,所以弄了三个租户。这三个域名分别是:&lt;http://localhost:8084/&gt; , &lt;http://localhost:8085/&gt; , &lt;http://localhost:8086/&gt;数据库中,对这3个租户的配置如下:其中b和c租户是配置了cas登录的。cas server发布了两个,都开了SSL链接,分别是:https://localhost:8443/cas ,https://localhost:9443/sso我们先测试表单登录。启动租户a,访问链接http://localhost:8084 ,这个页面只有一个超链接,点击超链接,访问http://localhost:8083/oauth/authorize?client_id=youku&amp;response_type=token&amp;redirect_uri=http://localhost:8081/youku/qq/redirect查看日志://前面经过spring security的一堆过滤器链,都没有匹配到FrameworkEndpointHandlerMapping : Mapped to public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.authorize(java.util.Map<java.lang.String, java.lang.Object>,java.util.Map<java.lang.String, java.lang.String>,org.springframework.web.bind.support.SessionStatus,java.security.Principal)//用户未认证,无法授权,抛出异常,ExceptionTranslationFilter对异常处理,跳转到配置的authentication //entry point,这里的authentication entry point,就是我之前配置的/login端点2019-04-19 16:01:14.608 DEBUG 21568 — [nio-8083-exec-1] o.s.web.servlet.DispatcherServlet : Failed to complete request: org.springframework.security.authentication.InsufficientAuthenticationException: User must be authenticated with Spring Security before authorization can be completed.2019-04-19 16:01:14.611 DEBUG 21568 — [nio-8083-exec-1] o.s.s.w.a.ExceptionTranslationFilter : Authentication exception occurred; redirecting to authentication entry pointorg.springframework.security.authentication.InsufficientAuthenticationException: User must be authenticated with Spring Security before authorization can be completed.可以看到,已经进入到了controller里面。final String SAVED_REQUEST = “SPRING_SECURITY_SAVED_REQUEST”; Object attribute = session.getAttribute(SAVED_REQUEST);这段代码的作用是为了拿到,之前发起的请求。那么这个请求是什么时候被保存的呢?我们知道抛出异常之后,ExceptionTranslationFilter对异常进行处理,检测到用户没有登录,所以才跳转到authentication entry point,所以,猜想应该是这里保存了最开始的请求信息。以下是ExceptionTranslationFilter的核心代码:public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {this.handleSpringSecurityException(request, response, chain, (RuntimeException)ase);}private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException { if (exception instanceof AuthenticationException) { this.logger.debug(“Authentication exception occurred; redirecting to authentication entry point”, exception); this.sendStartAuthentication(request, response, chain, (AuthenticationException)exception); } else if (exception instanceof AccessDeniedException) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (!this.authenticationTrustResolver.isAnonymous(authentication) && !this.authenticationTrustResolver.isRememberMe(authentication)) { this.logger.debug(“Access is denied (user is not anonymous); delegating to AccessDeniedHandler”, exception); this.accessDeniedHandler.handle(request, response, (AccessDeniedException)exception); } else { this.logger.debug(“Access is denied (user is " + (this.authenticationTrustResolver.isAnonymous(authentication) ? “anonymous” : “not fully authenticated”) + “); redirecting to authentication entry point”, exception); this.sendStartAuthentication(request, response, chain, new InsufficientAuthenticationException(this.messages.getMessage(“ExceptionTranslationFilter.insufficientAuthentication”, “Full authentication is required to access this resource”))); } }}这里对异常的处理,其实,核心就只有两个方法:1. this.accessDeniedHandler.handle(request, response, (AccessDeniedException)exception); ,这种情况下,用户已经登陆了,但是权限不够,所以交给accessDeniedHandler进行处理,一般来讲,如果没有进行特殊的配置,会返回一个403错误和异常信息【不再跳转到authentication entry point,因为用户已经登陆了】,这里不深究。2. this.sendStartAuthentication(request, response, chain, (AuthenticationException)exception); ,这个方法核心代码如下:protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException { SecurityContextHolder.getContext().setAuthentication((Authentication)null); //就是在这里保存的这次请求的所有信息,包括请求头,请求路径,参数,cookie等详细信息。所以,后面跳转到/login端点时,我在controller里面可以拿出来。 this.requestCache.saveRequest(request, response); this.logger.debug(“Calling Authentication entry point.”); //这里就是发起用户认证了,根据我的配置,它就会跳转到/login this.authenticationEntryPoint.commence(request, response, reason); }再回到前面的controller登录逻辑,往下走:@GetMapping("/login”)public String loginJump(HttpSession session) {final String SAVED_REQUEST = “SPRING_SECURITY_SAVED_REQUEST”;Object attribute = session.getAttribute(SAVED_REQUEST);// 默认情况下,用户直接访问/login时,没有SAVED_REQUESTif (attribute == null) { //默认跳转到登陆页面 return “login”;}if (attribute instanceof DefaultSavedRequest) { DefaultSavedRequest savedRequest = (DefaultSavedRequest) attribute; List<String> referer = savedRequest.getHeaderValues(“referer”); if (referer.size() == 1) { //有referer请求头 String domain = referer.get(0); //获取到数据库中配置的租户信息 Tenant tenant = tenantService.selectByDomain(domain); if (tenant == null) { return “login”; } else { String loginProvider = tenant.getLoginProvider(); switch (loginProvider) { case “cas”: //获取cas地址 CasInfo casInfoByTenantId = casInfoService.getCasInfoByTenantId(tenant.getTenantId()); String casServerLogin = casInfoByTenantId.getCasServerLogin(); session.setAttribute(“casInfoByTenantId”,casInfoByTenantId); return “redirect:” + casServerLogin; case “oauth”: //因为我在数据库中配置的是oauth,所以,最后响应login视图 return “login”; default: return “login”; } } } else { return “login”; }}return “login”;}用户跳转到登陆页面输入用户名密码,点击登陆,进入UsernamePasswordAuthenticationFilter ,开始尝试认证用户public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals(“POST”)) { throw new AuthenticationServiceException( “Authentication method not supported: " + request.getMethod()); } String username = obtainUsername(request); String password = obtainPassword(request); if (username == null) { username = “”; } if (password == null) { password = “”; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // Allow subclasses to set the “details” property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest);}最终会调用AuthenticationManager接口的authenticate方法,而AuthenticationManager委托一堆的AuthenticationProvider来进行认证。后面的流程,不再赘述,不在本篇文章的讨论范畴。用户认证成功后,调用successfulAuthentication(request, response, chain, authResult); 其实,这个方法里面核心代码就是successHandler.onAuthenticationSuccess(request, response, authResult);AuthenticationSuccessHandler有很多实现类,我们也可以自定义实现AuthenticationSuccessHandler。最常用的实现是,SavedRequestAwareAuthenticationSuccessHandler ,看一下它里面的核心代码:@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { SavedRequest savedRequest = requestCache.getRequest(request, response); if (savedRequest == null) { super.onAuthenticationSuccess(request, response, authentication); return; } String targetUrlParameter = getTargetUrlParameter(); if (isAlwaysUseDefaultTargetUrl() || (targetUrlParameter != null && StringUtils.hasText(request .getParameter(targetUrlParameter)))) { requestCache.removeRequest(request, response); super.onAuthenticationSuccess(request, response, authentication); return; } clearAuthenticationAttributes(request); // Use the DefaultSavedRequest URL String targetUrl = savedRequest.getRedirectUrl(); logger.debug(“Redirecting to DefaultSavedRequest Url: " + targetUrl); getRedirectStrategy().sendRedirect(request, response, targetUrl);}其实,这个方法,就是获取到之前保存的请求信息,然后再重定向到之前的请求。#### 2.CAS登录流程分析这次,我们访问租户b,这个租户,配置了cas登录。访问租户b:<http://localhost:8085/> ,这个页面里,也就是一个超链接,点击超链接,访问http://localhost:8083/oauth/authorize?client_id=iqiyi&response_type=token&redirect_uri=http://localhost:8081/iqiyi/qq/redirect前面的流程还是一样的,经过spring security的过滤器链,都没有匹配到,在最后DispatcherServlet抛出异常,然后ExceptionTranslationFilter对异常处理,跳转到/login端点,然后拿出配置在数据库中的casInfo,跳转到https://localhost:8443/cas/login?service=http%3A%2F%2Flocalhost%3A8083%2Flogin%2Fcas输入用户名密码,cas成功认证用户之后,生成TGT=============================================================WHO: adminWHAT: Supplied credentials: [admin]ACTION: AUTHENTICATION_SUCCESSAPPLICATION: CASWHEN: Fri Apr 19 16:51:01 CST 2019CLIENT IP ADDRESS: 0:0:0:0:0:0:0:1SERVER IP ADDRESS: 0:0:0:0:0:0:0:12019-04-19 16:51:01,300 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - <Audit trail record BEGINWHO: adminWHAT: TGT-GHfz0lUJQE-8fkKJgyv8WXNE5FYLBqb7zfWGfNoKwDZ0AjqA-DESKTOP-GDU9JIIACTION: TICKET_GRANTING_TICKET_CREATEDAPPLICATION: CASWHEN: Fri Apr 19 16:51:01 CST 2019CLIENT IP ADDRESS: 0:0:0:0:0:0:0:1SERVER IP ADDRESS: 0:0:0:0:0:0:0:12019-04-19 16:51:01,307 INFO [org.apereo.cas.DefaultCentralAuthenticationService] - <Granted ticket [ST-35-Mf1v9Z2qVVVKlWeTgyc-Hlzh2xY-DESKTOP-GDU9JII] for service [http://localhost:8083/login/cas] and principal [admin]>2019-04-19 16:51:01,308 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - <Audit trail record BEGINWHO: adminWHAT: ST-35-Mf1v9Z2qVVVKlWeTgyc-Hlzh2xY-DESKTOP-GDU9JII for http://localhost:8083/login/casACTION: SERVICE_TICKET_CREATEDAPPLICATION: CASWHEN: Fri Apr 19 16:51:01 CST 2019CLIENT IP ADDRESS: 0:0:0:0:0:0:0:1SERVER IP ADDRESS: 0:0:0:0:0:0:0:1然后跳转到service地址,也就是localhost:8083/login/cas ,并带上为这个service生成的service ticket,所以最后的请求地址为:http://localhost:8083/login/cas?ticket=ST-35-Mf1v9Z2qVVVKlWeTgyc-Hlzh2xY-DESKTOP-GDU9JII而这个端点/login/cas会被我配置的自定义CustomCasAuthenticationFilter拦截@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; if (!requiresAuthentication(req, res)) { chain.doFilter(request, response); return; } String ticket = obtainArtifact(req); //开始校验ticket try { CasInfo casInfo = (CasInfo) req.getSession().getAttribute(“casInfoByTenantId”); if (StringUtils.hasText(casInfo.getCasServer())) { //获取当前项目地址 String service; int port = request.getServerPort(); if (port != 80) { service = request.getScheme() + “://” + request.getServerName() + “:” + request.getServerPort() + endpoint; } else { service = request.getScheme() + “://” + request.getServerName() + endpoint; } //开始校验ticket Assertion validateResult = getTicketValidator(casInfo.getCasServer()).validate(ticket, service); //根据校验结果,获取用户详细信息 UserDetails userDetails = null; try { userDetails = userDetailsService.loadUserByUsername(validateResult.getPrincipal().getName()); if (this.logger.isDebugEnabled()) { logger.debug(“userDetailsServiceImpl is loading username:"+validateResult.getPrincipal().getName()); } } catch (UsernameNotFoundException e) { unsuccessfulAuthentication(req, res, e); } //手动封装authentication对象 assert userDetails != null; UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(validateResult.getPrincipal(), ticket, userDetails.getAuthorities()); authentication.setDetails(userDetails); successfulAuthentication(req,res,chain,authentication); } else { unsuccessfulAuthentication(req, res, new BadCredentialsException(“bad credential:ticket校验失败”)); } } catch (TicketValidationException e) { //ticket校验失败 unsuccessfulAuthentication(req, res, new BadCredentialsException(e.getMessage())); }// chain.doFilter(request, response);}校验成功之后,我的逻辑是,手动加载用户信息,然后把当前认证信息Authentication放到SecurityContextHolder中。protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { if (this.logger.isDebugEnabled()) { this.logger.debug(“Authentication success. Updating SecurityContextHolder to contain: " + authResult); } SecurityContextHolder.getContext().setAuthentication(authResult); if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); } this.successHandler.onAuthenticationSuccess(request, response, authResult);}protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.clearContext(); if (this.logger.isDebugEnabled()) { this.logger.debug(“Authentication request failed: " + failed.toString(), failed); this.logger.debug(“Updated SecurityContextHolder to contain null Authentication”); this.logger.debug(“Delegating to authentication failure handler " + this.failureHandler); } this.failureHandler.onAuthenticationFailure(request, response, failed);}#### 3.单点登出流程分析用户发送/logout请求,被我自定义的CustomLogoutFilter拦截之后的逻辑是,先从本地登出,然后判断之前是否是从cas认证的,如果是,再获取cas信息,然后把cas也登出了。这里判断登陆用户的认证方式,我想了很久,最后的实现思路如下:之前通过cas登录时,我手动的添加登陆用户的认证方式到Authentication中。代码如下://根据校验结果,获取用户详细信息UserDetails userDetails = null;try {userDetails = userDetailsService.loadUserByUsername(validateResult.getPrincipal().getName());if (this.logger.isDebugEnabled()) { logger.debug(“userDetailsServiceImpl is loading username:"+validateResult.getPrincipal().getName());}} catch (UsernameNotFoundException e) {unsuccessfulAuthentication(req, res, e);}//手动封装authentication对象assert userDetails != null;UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(validateResult.getPrincipal(), ticket, userDetails.getAuthorities());//就是这里做了文章authentication.setDetails(userDetails);successfulAuthentication(req,res,chain,authentication);然后,登出时,拿到这个信息,进行登出操作。因为,我在userdetails中封装了这个信息,所以可以拿到。public class UserDetailsVO implements UserDetails {//userprivate Integer userId;private String username;private String phone;private String email;//tenantprivate Tenant tenant;//accountprivate Integer accountId;private String password;//省略setter和getter} ...

April 19, 2019 · 11 min · jiezi

Spring Boot Security OAuth2 实现支持JWT令牌的授权服务器

标题文字 #### 概要之前的两篇文章,讲述了Spring Security 结合 OAuth2 、JWT 的使用,这一节要求对 OAuth2、JWT 有了解,若不清楚,先移步到下面两篇提前了解下。Spring Boot Security 整合 OAuth2 设计安全API接口服务Spring Boot Security 整合 JWT 实现 无状态的分布式API接口这一篇我们来实现 支持 JWT令牌 的授权服务器。优点使用 OAuth2 是向认证服务器申请令牌,客户端拿这令牌访问资源服务服务器,资源服务器校验了令牌无误后,如果资源的访问用到用户的相关信息,那么资源服务器还需要根据令牌关联查询用户的信息。使用 JWT 是客户端通过用户名、密码 请求服务器获取 JWT,服务器判断用户名和密码无误之后,可以将用户信息和权限信息经过加密成 JWT 的形式返回给客户端。在之后的请求中,客户端携带 JWT 请求需要访问的资源,如果资源的访问用到用户的相关信息,那么就直接从JWT中获取到。所以,如果我们在使用 OAuth2 时结合JWT ,就能节省集中式令牌校验开销,实现无状态授权认证。快速上手项目说明工程名端口作用jwt-authserver8080授权服务器jwt-resourceserver8081资源服务器授权服务器pom.xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId></dependency><dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.3.RELEASE</version></dependency><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.0.10.RELEASE</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>WebSecurityConfig@Configurationpublic class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http. authorizeRequests().antMatchers("/").permitAll(); } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser(“user”).password(“123456”).roles(“USER”); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public PasswordEncoder passwordEncoder() { return new PasswordEncoder() { @Override public String encode(CharSequence charSequence) { return charSequence.toString(); } @Override public boolean matches(CharSequence charSequence, String s) { return Objects.equals(charSequence.toString(),s); } }; }}为了方便,使用内存模式,在内存中创建一个用户 user 密码 123456。OAuth2AuthorizationServer/ * 授权服务器 /@Configuration@EnableAuthorizationServerpublic class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter { /* * 注入AuthenticationManager ,密码模式用到 / @Autowired private AuthenticationManager authenticationManager; /* * 对Jwt签名时,增加一个密钥 * JwtAccessTokenConverter:对Jwt来进行编码以及解码的类 / @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey(“test-secret”); return converter; } /* * 设置token 由Jwt产生,不使用默认的透明令牌 / @Bean public JwtTokenStore jwtTokenStore() { return new JwtTokenStore(accessTokenConverter()); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .authenticationManager(authenticationManager) .tokenStore(jwtTokenStore()) .accessTokenConverter(accessTokenConverter()); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient(“clientapp”) .secret(“123”) .scopes(“read”) //设置支持[密码模式、授权码模式、token刷新] .authorizedGrantTypes( “password”, “authorization_code”, “refresh_token”); }}资源服务器pom.xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId></dependency><dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.3.RELEASE</version></dependency><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.0.10.RELEASE</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>HelloController@RestController("/api")public class HelloController { @PostMapping("/api/hi") public String say(String name) { return “hi , " + name; }}OAuth2ResourceServer/* * 资源服务器 */@Configuration@EnableResourceServerpublic class OAuth2ResourceServer extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated().and() .requestMatchers().antMatchers("/api/**”); }}application.ymlserver: port: 8081security: oauth2: resource: jwt: key-value: test-secret参数说明:security.oauth2.resource.jwt.key-value:设置签名key 保持和授权服务器一致。security.oauth2.resource.jwt:项目启动过程中,检查到配置文件中有security.oauth2.resource.jwt 的配置,就会生成 jwtTokenStore 的 bean,对令牌的校验就会使用 jwtTokenStore 。验证请求令牌curl -X POST –user ‘clientapp:123’ -d ‘grant_type=password&username=user&password=123456’ http://localhost:8080/oauth/token返回JWT令牌{ “access_token”: “eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NTQ0MzExMDgsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiOGM0YWMyOTYtMDQwYS00Y2UzLTg5MTAtMWJmNjZkYTQwOTk3IiwiY2xpZW50X2lkIjoiY2xpZW50YXBwIiwic2NvcGUiOlsicmVhZCJdfQ.YAaSRN0iftmlR6Khz9UxNNEpHHn8zhZwlQrCUCPUmsU”, “token_type”: “bearer”, “refresh_token”: “eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsicmVhZCJdLCJhdGkiOiI4YzRhYzI5Ni0wNDBhLTRjZTMtODkxMC0xYmY2NmRhNDA5OTciLCJleHAiOjE1NTY5Nzk5MDgsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiJdLCJqdGkiOiI0ZjA5M2ZjYS04NmM0LTQxZWUtODcxZS1kZTY2ZjFhOTI0NTAiLCJjbGllbnRfaWQiOiJjbGllbnRhcHAifQ.vvAE2LcqggBv8pxuqU6RKPX65bl7Zl9dfcoIbIQBLf4”, “expires_in”: 43199, “scope”: “read”, “jti”: “8c4ac296-040a-4ce3-8910-1bf66da40997”}携带JWT令牌请求资源curl -X POST -H “authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NTQ0MzExMDgsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiOGM0YWMyOTYtMDQwYS00Y2UzLTg5MTAtMWJmNjZkYTQwOTk3IiwiY2xpZW50X2lkIjoiY2xpZW50YXBwIiwic2NvcGUiOlsicmVhZCJdfQ.YAaSRN0iftmlR6Khz9UxNNEpHHn8zhZwlQrCUCPUmsU” -d ’name=zhangsan’ http://localhost:8081/api/hi返回hi , zhangsan源码https://github.com/gf-huanchu… ...

April 5, 2019 · 2 min · jiezi

OAuth2.0: 接入GitHub登录功能

OAuth网络上关于Oauth 2.0协议的基本内容已经很多了,我就不重复写博客了,对基本概念不理解的同学可以先自行Google。但是我发现实际演示的demo很少,所以写了这个偏实战的博客。本文是以GitHub登录为例来演示的。虽然线上环境肯定要有云服务器,但是可以在本地直接模拟调试的不需要写一行代码就可以演示一个完成的登录流程!!请读者务必手动的实际操作!!其实OAuth认证说白了:有三个角色:GitHub,用户,第三方网站third-side需要就是完成一件事:经用户同意,让third-side安全的从GitHub拿到一个tokenthird-side拿到这个token可以用来标识一个用户,读取用户的基本信息代表用户在GitHub上完成一些操作,比如写一个issue之类的基本流程向GitHub申请注册一个application一个application对应一个项目,我们需要拿到一个client id 和 secret来用于后续的登录认证构造相关的登录链接,引导用户点击登录。这一步需要用到上面的client id用户同意登录后,third-side可以拿到一个code(后面详细解释),通过这个code可以向GitHub拿到用户的token第一步注册App因为我们要使用GitHub作为第三方登录,所以肯定要先到官网上注册一个application。网址: https://github.com/settings/d…点击绿色的按钮就行了,点击后会出现????的页面:上面的内容很多,但是只有两个字段是关键的Application Name: 这个是GitHub 用来标识我们的APP的Authorization callback url:就是上面我特意用红色字体标识的的,很关键Homepage url 这个是展示用的,在我们接下来的登录中用不到,随便写就行了这个Authorization callback url就是在用户确认登录后,GitHub会通过这个url来告知我们的服务器。所以真实情况下这个url应该是由专门的服务器程序来监听的。但是这一步大家可以先跟着我填写,后面细细说。完成注册后,我们就有了这些数据:上面有两个数据很关键cliendt_id这是GitHub用来标识我们的APP的接下来我们需要通过这个字段来构建我们的登录urlclient Secret 这个很关键,等会我们就靠它来认证的,要好好保存。我这个只是演示教程,用完就销毁了,所以直接公开了。构造URL然后我们接下来怎么做?很简单<body> <a href=“https://github.com/login/oauth/authorize?client_id=47878c9a96cfc358eb6e”> login with github</a></body>我们只要在我们的登录页面添加这样的代码,引导用户点击我们的登录按钮就行了。注意到上面的流程了吗? 其实我们已经完成了一半的登录认证流程了,然后我们来分析一下。首先请大家思考一下:从用户点击login with github开始,中间有几次重要的HTTP报文传递?不重要是指中间那些网页中附带的对css js资源的请求,这些不算。与登录认证有关的http请求有几个?3个!!!说清楚这一点,基本就明白了。首先我们构造了这样的html页面<body> <a href=“https://github.com/login/oauth/authorize?client_id=47878c9a96cfc358eb6e”> login with github</a></body>关键是我们构造了一个urlhttps://github.com/login/oauth/authorize:这一部分是固定的,只要是用GitHub登录,就得这样。你可以从官方文档上看到这个链接。如果是微信登录,twitter登录,也是大同小异,具体的url可以在相关的官网上找到。GitHub会监听这个路由,来做出登录处理然后后面是一个查询字符串client_id=47878c9a96cfc358eb6e。它的值就是我们之前申请到的client id。这是个必须存在的字段,用户点击登录后,GitHub就是通过这个字段来确认用户究竟是想登录哪个网站。然后GitHub会返回这样的页面,确认用户是否真的要登录,这就是第一次HTTP请求和响应然后用户点击确认后发生了什么?用户点击确认,就是向GitHub发送一个报文,确认自己确实到登录某网站GitHub收到这个用户的确认消息,然后会返回一个状态码为301的报文这个是第二次HTTP请求和响应。因为浏览器收到301重定向后,会直接前往新的网址了,所以你要是没仔细看的话,可能就忽略了。这个状态码为301的报文,它的Location字段大概长这样:https://github.com/fish56/OAuth?code=a1aee8cacf7560825665>https://github.com/fish56/OAuth这个字段就是我们之前填写的callback url,正常情况下这个应该是我们云服务器的网址。但是这里为了演示方便,我这里就随便填写了我的github地址,没关系的code=a1aee8cacf7560825665:(因为笔者中间调试过,所以现在写的token和gif里面的不一样哈)这个就是我们OAuth登录的一个核心信息。我之前说过,我们OAuth登录的核心目的就是让第三方网站能够安全的拿到用户的token。用户的浏览器收到之前的301的HTTP响应后,就会向我们服务器发起请求,请求的同时服务器就拿到了这个code。服务器就可以通过这个code从github拿到用户的token。这就是第三次http请求。然后又有同学可能会问了,为什么要返回一个code,而不是直接返回一个token呢?答:为了安全,如果token经过用户的手里走一遍,就可能会被其他恶意的人窃取。OAuth协议下,GitHub会返回给用户一个code,然后用户浏览器通过重定向携带这个code来访问我们的服务器,这样服务器就拿到了这个code。服务器拿到这个code 之后,通过结合之前的client secret向GitHub申请token,这样会安全一点。之前我们只做了一半, 接下来我们通过postman来演示一下如何通过code拿到token。登录我们要向GitHub申请用户的token,需要code这个只有在用户同意登录后,服务器才能拿到。client secret + client id向https://github.com/login/oauth/access_token发起POST请求,并且携带上面的三个字段这个url是GitHub规定的,你可以在它的官方文档中找到然后我们在postman中构造这样的请求:哎,可以看到,这我们确实拿到了用户的token:access_token=9094eb58a23093fd593d43eb28c1f06ce7904ed5&scope=&token_type=bearer。只不过真实情况下上面的操作都是由线上的服务器完成的,我这样操作是方便大家的理解。代码实战通过上面的例子我们可以看到,如果只是演示,我们是不需要服务器。接下来我们在本地用代码直接演示下。演示的代码使用node + express + request 写的不过代码很简单,不了解上面的技术栈也可以看得懂流程上和之前演示的一模一样,只是通过代码来完成源代码看这里 GitHub因为我们要启动本地的服务器来监听响应,所以我们首先要修改下我们的callback URL。请大家将这个callback URL自行修改为http://localhost:8099/github/login。这是我们程序的目录结构然后这是我们的node代码:const querystring = require(‘querystring’);const express = require(’express’);const request = require(‘request’);const githubConfig = require(’./oauth.conf’)let app = express();// 做一个路由函数,监听/github/login 的get请求app.get(’/github/login’, async function(req,res){ //read code from url let code = req.query.code // 收到code后,向GitHub请求用户的token request.post(githubConfig.access_token_url, { form:{ client_id: githubConfig.client_ID, client_secret: githubConfig.client_Secret, code: code } },function(error, response, body) { //正常情况下,返回值应该是形如access_token=9094eb58a23093fd59 // 3d43eb28c1f06ce7904ed5&scope=&token_type=bearer // 的字符串,可以通过下面的函数来解析 let result = querystring.parse(body) // 拿到token后,返回结果,表示我们成功了 let access_token = result[“access_token”] if(access_token == undefined){ res.send(result.error_description) } res.send(You are login! you token is ${access_token}) })})// 监听 8999 ,启动程序。注意端口号要和我们之前填写的保持一致app.listen(8099,function(){ console.log(’listening localhost:8099’)})这是oauth.conf.js。注意把相关的字段换成你自己的配置。module.exports = { client_ID: ‘47878c9a96cfc358eb6e’, client_Secret: ‘4813689c043c60dbf3d3a0d8e0a984afc0bf810a’, access_token_url: ‘https://github.com/login/oauth/access_token'}这是实际效果我们的代码做了什么事?只是把我们之前的手动使用postman做的事情自动化了监听http://localhost:8099/github/login收到用户浏览器传递的code之后,我们的服务器向GitHub申请了用户token将token返回给用户,表明登录成功不过其实我上面省略了很多操作。正常情况下,服务器拿到用户的token后,应该:把token保存到数据库通过token从github拿到用户的名称之类的数据不应该把token返回给用户返回给用户一个cookie,使得用户保持登录,并且在数据库中把这个cookie和用户的token对应起来总结好了,上面基本就是一次登录流程。构造登录链接,引导用户登录用户登录后,GitHub会把用户重定向到我们预先设置好的URL,同时携带一个code服务器拿到这个code,向GitHub申请token拿到token后,服务器就可以确认用户成功登录了,然后可以返回一个登录成功的页面 ...

March 26, 2019 · 1 min · jiezi

基于oAuth2的OIDC原理 学习笔记

OAuth2概念OAuth 2.0 规范定义了一个授权(delegation)协议,OAuth2.0 不是认证协议。OAuth解决的大部分问题在于Client和被访问的资源之间的连接上,在用户不存在的情况下,使用这种委托访问。流程上图ABCDE这5个步骤,既是完整的获取访问令牌的一个过程,其中:Client 为第三方应用程序,resource owner 为资源所有者user-agent 为类似浏览器的代理authorization server 授权服务器resource server 资源服务器流程如下:A) Client使用浏览器(User-Agent)访问Authorization server。也就是用浏览器访问一个URL,这个URL是Authorization server提供的,访问的Client需要提供(客户端标识client_id,授权范围scope,本地状态state和redirect_uri)这些参数。B) Authorization server验证Client在(A)中传递的参数信息,如果无误则提供一个页面供Resource owner登陆,登陆成功后选择Client可以访问Resource server的哪些资源以及读写权限。C) 在(B)无误后返回一个授权码(Authorization Code)给Client。D) Client拿着(C)中获得的授权码(Authorization Code)和(客户端标识、redirect_uri等信息)作为参数,请求Authorization server提供的获取access token。E) Authorization server返回access token和可选的refresh token 以及令牌有效时间等信息给Client。F) client拿到access token后就可以去resources server访问resource owner的资源OIDC定义了一种基于OAuth2的用户身份认证OIDC的核心在于在OAuth2的授权流程中,一并提供用户的身份认证信息(ID Token)给到第三方客户端,ID Token使用JWT格式来包装,得益于JWT(JSON Web Token)的自包含性,紧凑性以及防篡改机制,使得ID Token可以安全的传递给第三方客户端程序并且容易被验证。此外还提供了UserInfo的接口,用于获取用户的更完整的信息流程RP发送一个认证请求给OP;OP对EU进行身份认证,然后提供授权;OP把ID Token和Access Token(需要的话)返回给RP;RP使用Access Token发送一个请求UserInfo EndPoint;UserInfo EndPoint返回EU的Claims。术语:EU:End User:一个人类用户。RP:Relying Party ,用来代指OAuth2中的受信任的客户端,身份认证和授权信息的消费方;OP:OpenID Provider,有能力提供EU认证的服务(比如OAuth2中的授权服务),用来为RP提供EU的身份认证信息;ID Token:JWT格式的数据,包含EU身份认证的信息。UserInfo Endpoint:用户信息接口(受OAuth2保护),当RP使用Access Token访问时,返回授权用户的信息,此接口必须使用HTTPSAuthN & AuthZ: AuthN代表authentication AuthZ 代表authrization

March 17, 2019 · 1 min · jiezi

Android 原生app获取用户授权访问Autodesk云应用数据

oAuth机制对于网站间的授权管理是很容易实现的,设置好app回调端口,当数据服务提供方拿到其用户授权,则返回授权码发送到回调端口。上一篇文章介绍了如何授权Forge app访问Autodesk 云应用数据,即,获取三条腿的token。但移动端的原生app,授权码返回到发起请求app的某个Activity,可如何得知是哪个Activity并且跳转到此Activity?这时就要用到URL Scheme。iOS和Android都提供了这样的机制。它实现了页面内跳转协议,通过定义自己的scheme协议,非常方便跳转到app中的各个Activity。也让不同app可以唤起其它app的Activity。当然,前提app已经安装到系统。这位大咖对此话题做了专业的讲解,推荐大家先阅读:https://www.jianshu.com/p/7b0…我的同事Bryan搭建了一个框架,演示Android app 集成Forge oAuth,拿到三条腿token,并调用了用户信息(Profile)API,列出登录用户的在Forge数据管理中的基本信息。该框架很好的展示了使用URL Scheme整个过程。https://github.com/dukedhx/oa…我借此学习了有关内容。另外,基于此代码,略微改造,添加流程展示获取Autodesk云应用的Hub,Project,Folder和File,为进一步的综合应用app做一些铺垫。https://github.com/xiaodongli…oAuth相关的几个步骤:1.定义监听oAuth回传的Activity属性 <activity android:name=".MainActivity"> <intent-filter> <action android:name=“android.intent.action.MAIN”/> <category android:name=“android.intent.category.LAUNCHER”/> </intent-filter> <intent-filter> <data android:scheme="@string/FORGE_CALLBACK_SCHEME" android:host="@string/FORGE_CALLBACK_HOST"/> <action android:name=“android.intent.action.VIEW”/> <category android:name=“android.intent.category.DEFAULT”/> <category android:name=“android.intent.category.BROWSABLE”/> </intent-filter> </activity>其中,FORGE_CALLBACK_SCHEME和FORGE_CALLBACK_HOST定义为: <string name=“FORGE_CALLBACK_SCHEME”>lxdapp</string> <string name=“FORGE_CALLBACK_HOST”>mytest3legged</string>同样的, URL Scheme必须和Forge app注册的时候填写的callback URL要一致。 2.登录事件将创建一个Intent.ACTION_VIEW,用以启动网页,URL拼接了Forge app的client id,授权方式 (response_type=code),回传地址和授权的权限范围 跳转到Autodesk登录过程,等待客户授权。public void onLoginClick(View v) { Intent i = new Intent(Intent.ACTION_VIEW); Resources resources = getResources(); i.setData(Uri.parse(getResources().getString(R.string.FORGE_AUTHORIZATION_URL) + “?response_type=code&redirect_uri=” + this.getCallbackUrl() + “&scope=” + resources.getString(R.string.FORGE_SCOPE) + “&client_id=” + resources.getString(R.string.FORGE_CLIENT_ID))); startActivity(i); }3.用户授权后,将对URLScheme地址回传,也就是发起请求的Activity。Activity的OnStart拿到回传信息。由于没有设定android:mimetype, 则任何数据类型都可接收。但我们需要只是授权码,在回传请求的URL参数中。 @Override protected void onStart() { super.onStart(); Intent intent = getIntent(); Uri data = intent == null ? null : intent.getData(); //从回传请求中拿到授权码 String authorizationCode = data == null ? null : data.getQueryParameter(“code”); if (authorizationCode != null && !authorizationCode.isEmpty()) { ……4.接下来的过程就和常规的网页应用类似了,依授权码得到最终的token。5.该样例调用Forge服务采取okhttp3,推荐参考下文的详细讲解:https://www.jianshu.com/p/da4…注:Activity的URLScheme是可能同名的,而且如何保证应用之间跳转和传参的目标正确性,这方面我还在研究中。本例为方便计,将Forge Client Secret也存在了app之中。我想安全的方式是由app服务器端进行secret的管理,当原生app拿到授权码,通过它来向app服务器端发起请求,由app服务器来获取Forge token,再传回原生app。扩展的部分(Hub,Project,Folder和File)只是简单的替换ListView的内容,尚未对页面回退做处理。 ...

February 25, 2019 · 1 min · jiezi

Forge oAuth访问Autodesk云应用数据 (三条腿token)

大多数朋友刚开始接触Autodesk Forge,源于‘网页浏览和管理模型’的需求,即,使用Forge的模型转换服务和前端Forge Viewer。模型的来源通常是开发者利用Forge数据管理服务创建bucket,上传模型。而这种方式意味着其应用程序(app)是该模型(数据)拥有者。但在很多场景,模型源并不是app所有,而是存储和管理在某些其它云应用中,例如BOX,DropBox,百度盘,阿里云盘,或者Autodesk的BIM 360, Fusion 360。这些云应用的用户拥有其数据的权限。Forge的app 要对这些模型访问或操作的话,则要其它云应用的用户进行授权,拿到授权后,才能访问或操作数据(取决于授权的范围)。这就是通行的oAuth2.0。在我们平常使用支付验证的时候,也是这个机制。推荐阅读这篇文章,对oAuth做了精炼的讲解:https://www.jianshu.com/p/639…简言之,典型的流程是,app发起一个请求,让用户在第三方云应用登录和授权,给予app一定范围的数据权限(此过程全部导向第三方,app无法干预和定制),当用户完成授权过程后,第三方云应用将会给app传回一个授权码(code)。借此,app继续调用第三方的云服务API,获取到最终的token。因为有三步走 (发起,授权,获取token),这也就是为何经常把这样的token叫做三条腿token。前面提到的创建bucket,自行上传模型相关的token,叫做两条腿token。Forge全球资料有个教材演示如何一步步的搭建app,访问Autodesk云应用数据。http://learnforge.autodesk.io…本文对其中授权认证过程稍微再讲解一下:1.首先,Autodesk的云应用也提供了oAuth机制,底层是Forge的身份认证服务(大家经常用它来获取token)。例如app访问和操作BIM 360客户的数据,发起请求的过程,还要有一个回调端口监听来自Forge认证服务传回的授权码。而回调端口必须在创建app的时候设置好。如果您的app只有两条腿token的需要,此项随便给一个scheme://host 形式的字串即可。2.下载教材提供的完整代码工程 (以Node.js为例):https://github.com/Autodesk-Forge/learn.forge.viewhubmodels/tree/nodejs 打开工程,定位到config.js,填写对应的Client ID, Client Secret 和Callback URL (一般设置为环境变量)。3.此工程定义好的发起授权请求的端口在oauth.js中。它其实是拼接了一个URL,传回客户端,让客户端访问。此URL带有app的client id,授权方式 (response_type=code),回传地址和授权的权限范围。当发起请求,Forge认证服务会和app注册是对应的信息最比对,因此,必须保证client id和回传地址和app注册信息一致。router.get(’/oauth/url’, (req, res) => { const url = ‘https://developer.api.autodesk.com’ + ‘/authentication/v1/authorize?response_type=code’ + ‘&client_id=’ + config.credentials.client_id + ‘&redirect_uri=’ + config.credentials.callback_url + ‘&scope=’ + config.scopes.internal.join(’ ‘); res.end(url);});此工程定义好的回传端口在oauth.js中,router.get(’/callback/oauth’, async (req, res, next) => { //从Autodesk认证传回的授权码,用于获取最终的token const { code } = req.query; const oauth = new OAuth(req.session); try { await oauth.setCode(code); res.redirect(’/’); } catch(err) { next(err); }});客户端拿到请求地址启动访问,则会弹出Autodesk标准的登录对话框客户输入账号密码后,Autodesk授权机制会询问,某app需要如下数据权限,是否允许。客户允许后,授权码会发到回调函数接着,代码调用Forge的API,得到token。token有两个,一个是执行其他API操作的token(和两条腿的token类似,JWT),有效期也是60分钟。而另外一个是refresh token(用户授权令牌),用于保留用户授权,有效期14天。所以,当用户授权一次后,并不用每次都有授权过程,在refresh token有效期间,可以用此再调用Forge API获取新的访问token。 注:三条腿token,同样要注意高权限和低权限的token,样例代码中有演示。目前,Forge的app只允许设置一个回调函数端口。而有些其它云应用例如Azure的app,允许多个。Forge可以授予app代表某用户,使用两条腿token的方式进行数据访问,在以后文章中再介绍。BIM 360的数据访问,还需要一个特殊步骤,详情见 BIM 360 开发账户授权。oAuth2.0的机制适用于其它云存储,参考这篇博客 ...

February 24, 2019 · 1 min · jiezi

Spring Security OAuth 个性化token

个性化Token 目的默认通过调用 /oauth/token 返回的报文格式包含以下参数{ “access_token”: “e6669cdf-b6cd-43fe-af5c-f91a65041382”, “token_type”: “bearer”, “refresh_token”: “da91294d-446c-4a89-bdcf-88aee15a75e8”, “expires_in”: 43199, “scope”: “server”}并没包含用户的业务信息比如用户信息、租户信息等。扩展生成包含业务信息(如下),避免系统多次调用,直接可以通过认证接口获取到用户信息等,大大提高系统性能{ “access_token”:“a6f3b6d6-93e6-4eb8-a97d-3ae72240a7b0”, “token_type”:“bearer”, “refresh_token”:“710ab162-a482-41cd-8bad-26456af38e4f”, “expires_in”:42396, “scope”:“server”, “tenant_id”:1, “license”:“made by pigx”, “dept_id”:1, “user_id”:1, “username”:“admin”}密码模式生成Token 源码解析 主页参考红框部分ResourceOwnerPasswordTokenGranter (密码模式)根据用户的请求信息,进行认证得到当前用户上下文信息protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) { Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters()); String username = parameters.get(“username”); String password = parameters.get(“password”); // Protect from downstream leaks of password parameters.remove(“password”); Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password); ((AbstractAuthenticationToken) userAuth).setDetails(parameters); userAuth = authenticationManager.authenticate(userAuth); OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest); return new OAuth2Authentication(storedOAuth2Request, userAuth);}然后调用AbstractTokenGranter.getAccessToken() 获取OAuth2AccessTokenprotected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) { return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));}默认使用DefaultTokenServices来获取tokenpublic OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException { … 一系列判断 ,合法性、是否过期等判断 OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken); tokenStore.storeAccessToken(accessToken, authentication); // In case it was modified refreshToken = accessToken.getRefreshToken(); if (refreshToken != null) { tokenStore.storeRefreshToken(refreshToken, authentication); } return accessToken;}createAccessToken 核心逻辑// 默认刷新token 的有效期private int refreshTokenValiditySeconds = 60 * 60 * 24 * 30; // default 30 days.// 默认token 的有效期private int accessTokenValiditySeconds = 60 * 60 * 12; // default 12 hours.private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) { DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(uuid); token.setExpiration(Date) token.setRefreshToken(refreshToken); token.setScope(authentication.getOAuth2Request().getScope()); return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;}如上代码,在拼装好token对象后会调用认证服务器配置TokenEnhancer( 增强器) 来对默认的token进行增强。TokenEnhancer.enhance 通过上下文中的用户信息来个性化Tokenpublic OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { final Map<String, Object> additionalInfo = new HashMap<>(8); PigxUser pigxUser = (PigxUser) authentication.getUserAuthentication().getPrincipal(); additionalInfo.put(“user_id”, pigxUser.getId()); additionalInfo.put(“username”, pigxUser.getUsername()); additionalInfo.put(“dept_id”, pigxUser.getDeptId()); additionalInfo.put(“tenant_id”, pigxUser.getTenantId()); additionalInfo.put(“license”, SecurityConstants.PIGX_LICENSE); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo); return accessToken;}基于pig 看下最终的实现效果Pig 基于Spring Cloud、oAuth2.0开发基于Vue前后分离的开发平台,支持账号、短信、SSO等多种登录,提供配套视频开发教程。 https://gitee.com/log4j/pig ...

February 18, 2019 · 2 min · jiezi

微信小程序如何调用后台service的简单记录

写在前头,本篇文章简单记录一下,在前后端分离的微信小程序应用中,前端访问后台service的实现思路,没有过多涉及技术实现方面。先上一张官网的图片,它很清楚的讲明了要在后台service为每一个小程序用户提供登录信息需要做哪些事情。用户拿到了后台给的登录凭据,访问后台service。静默授权与非静默授权先来讲讲这两个概念,静默授权,顾名思义,这授权动作对于用户来说是感知不到的,小程序端悄咪咪得就做了。因此,没有知会用户的授权方式拿到的信息也是不太重要的。但是,静默授权是开始重要的一步,因为它为接下来的动作提供了code。调用静默授权可用小程序提供的API:wx.login()。再来讲讲非静默授权,顾名思义,它的授权是要有明显动静的,而且它是要得到用户认可才可以执行。API:wx.getUserInfo(),这个接口要不要用可根据小程序的实际需求来。后台获取openId小程序提供了一个接口code2session,拿着我们静默授权获取到的临时登录凭证code再调用一下这个接口就能获取到openId了。注意,这一步是在后台服务器做的。openId是在当前小程序中对用户唯一性的标识。基于OAuth2.0生成token在后台,可以用SpringSecurity的OAuth2.0这一个工具,用openId来生成前端请求后端数据的附带校验信息token。具体是如何实现的笔者也没有深入了解……获取到了这个token后,前端可将其存入webStorage中,每一次调用后台service的时候,就可以利用请求的拦截器在config参数中加入token。后端就可以确定发送请求的用户身份,保证了系统的安全性。总结:以上大致描绘了小程序后台自定义登录态的开发思路。

January 30, 2019 · 1 min · jiezi

Authlib强大OAuth框架入门

基于Python的Authlib是集所有主流WebAPI权限认证协议的客户端、服务端、底层实现、高层架构于一身的强大工具库。参考官方:Authlib: Python AuthenticationAuthlib几乎是能将RFC所有相关的API认证协议都包括进来了,甚至从协议的底层实现、高层架构,从客户端到服务端都能实现的,当之无愧称为Monolithic project 的一个项目。目前Authlib支持的Authentication协议有:OAuth 1.0OAuth 2.0JWTmany more…安装:$ pip install AuthlibOauth2.0客户端验证参考官方:Authlib - Client Guide - OAuth 2 SessionAuthlib中用来登录验证OAuth2.0的对象叫Session,其中包括了所有相关的验证所需方法函数。from authlib.client import OAuth2Session# 生成session,用来之后的创建URL、获取token、刷新token等所有动作session = OAuth2Session( client_id=‘Your Client ID’, client_secret=‘Your Client Secret’, scope=‘user:email’, # 这个授权范围根据每个API有所不同 redirect_uri=‘https://MyWebsite.com/callback')# 生成URLuri, state = session.create_authorization_url( ‘https://目标网址的授权入口/authorize’)print(uri)# 「手动」打开浏览器访问URI,登录API帐户,点击授权,然后获取最终的URL# 或者自己有服务器的话,可以自己接收#。。。# 将callback返回的URL复制下来,因为其中包含授权codeauthorization_response = ‘https://MyWebsite.com/callback?code=42..e9&state=d..t'# 用获得的Code去访问access_token入口tokens = session.fetch_access_token( access_token_url=‘https://目标网址的授权入口/api/access_token’, authorization_response=authorization_response)print(tokens) #返回字典格式: {‘access_token’: ’e..ad’, ’token_type’: ‘bearer’, ‘scope’: ‘user:email’}# 过期后,刷新token。需重建session对象:session = OAuth2Session( client_id=‘Your Client ID’, client_secret=‘Your Client Secret’, scope=‘user:email’, state=state, redirect_uri=‘https://MyWebsite.com/callback')new_tokens = session.refresh_token( access_token_url, refresh_token=tokens[‘refresh_token’])print(’[Refreshed tokens]:’, new_tokens)设置的callback或redirect_url不存在怎么获取Code?在调试过程中,如果我们向上面一样手动去打开浏览器复制URL,再复制回应过来的URL是很麻烦的。Oauth2的逻辑就是:要求各种客户自己在自己的浏览器里登录帐户,然后给你的App授权。所以这一步redirect_url是躲不过的。但是我们测试过程中,还没来得及专门建一个服务器或网页来接收这个callback回调怎么办呢?有办法!方法一:直接截取我们可以在第一次登录并授权后,复制cookies,然后在测试中直接使用requests带着cookies登录信息去访问,就不再需要手动打开浏览器了:raw_cookies = """ 这里是你复制过来的cookies “““cookies = dict([line.split("=”, 1) for line in raw_cookies.strip().split(”; “)])try: r = requests.get(uri, cookies=cookies, allow_redirects=True)except requests.exceptions.ConnectionError as e: print( ‘[Final URL]: ‘, e.request.url ) authorization_response = e.request.url由于你最开始设置的callback是公网上的某个网址,应该是不存在的(只要你没有设置的话)。所以,这里去request的时候,肯定会报错,且是ConnectionError。所以我们可以将最终报错的URL获取到,这个里面就包含了我们想要的Code码。方法二:更改本地hosts如果本地已经搭建了测试服务器,比如Nginx或Flask,这种方法更简单。比如在供应商中设定的redirect_url为http://example.com/callback,那么只需简单编辑hosts:# /etc/hosts127.0.0.1 example.com那么,只要本机设置了Nginx或Flask等服务器,只需要获取127.0.0.1/callback即可得到需要的内容。 ...

January 25, 2019 · 1 min · jiezi

Spring Cloud OAuth2 资源服务器CheckToken 源码解析

CheckToken的目的 当用户携带token 请求资源服务器的资源时, OAuth2AuthenticationProcessingFilter 拦截token,进行token 和userdetails 过程,把无状态的token 转化成用户信息。 ## 详解OAuth2AuthenticationManager.authenticate(),filter执行判断的入口当用户携带token 去请求微服务模块,被资源服务器拦截调用RemoteTokenServices.loadAuthentication ,执行所谓的check-token过程。源码如下 CheckToken 处理逻辑很简单,就是调用redisTokenStore 查询token的合法性,及其返回用户的部分信息 (username )继续看 返回给 RemoteTokenServices.loadAuthentication 最后一句tokenConverter.extractAuthentication 解析组装服务端返回的信息最重要的 userTokenConverter.extractAuthentication(map);最重要的一步,是否判断是否有userDetailsService实现,如果有 的话去查根据 返回的username 查询一次全部的用户信息,没有实现直接返回username,这也是很多时候问的为什么只能查询到username 也就是 EnablePigxResourceServer.details true 和false 的区别。那根据的你问题,继续看 UerDetailsServiceImpl.loadUserByUsername 根据用户名去换取用户全部信息。关于pig基于Spring Cloud、oAuth2.0开发基于Vue前后分离的开发平台,支持账号、短信、SSO等多种登录,提供配套视频开发教程。 https://gitee.com/log4j/pig

January 25, 2019 · 1 min · jiezi

SaaS化实践——如何用一个微信公众号登录多个不同的域名

背景SaaS 作为一种服务,需要为不同的客户定制不同的域名以满足客户定制化的需求。而微信登录时需要填写一个回调地址,单一的回调地址是难以处理多客户域名的业务需求的,经过不同的 SaaS 项目的实践,总结出了下面的方式。微信登录的核心代码依然采用 psa 这个库 https://github.com/python-soc…。微信说明阅读微信公众平台文档,可以看到,当同一个微信公众号需要在多个服务间使用时,微信的建议是提供一台中控服务器,防止access_token的重复刷新,这个坑确实踩到过。oauth 2.0https://tools.ietf.org/html/r…核心概念、表结构中控机中控机为同一引导用户登录的微信登录服务器,其中此机器做的为 oauth 2.0 截图部分的 A、B,引导用户授权,微信回调到此中控机,拿到code。中控机通过state参数,解除customerid,根据customer配置找到回调地址。回调是将state,code带去回调的客户域名。customercustomer表需要记录微信的appid,appsecret,这样即使客户需要定制自己的微信公众号,中控机也可以saas化。redirecturl由于微信的state参数长度有限,因此提供一张redirecturl表记录回调地址,登录时只需要将redirecturl_id带入state中即可。redirecturl记录的为回调客户域名+psa compelate地址的完整路由。statestate为oauth 2.0中允许的回调参数,state的构成为: 客户id,回调地址id,其他需要回调参数核心流程核心代码中控机通过customer获取对应的appid,secret。微信回调到cherrypick后,拿着code,state跳转到对应的客户域名。def _auth(request, backend): cid = request.GET[‘cid’] # TODO: DoesNotExist customer = Customer.objects.get(id=cid) appid, appsecret = customer.get_key_and_secret() log.info(’login cid:%s, key:%s’, cid, appid) def get_key_and_secret(): return appid, appsecret request.backend.get_key_and_secret = get_key_and_secret return do_auth(request.backend)@never_cache@psa(‘our_social:cherrypick’)def auth(request, backend, key=’’): return _auth(request, backend) @never_cache@psa()def cherrypick(request, backend): code = request.GET.get(‘code’, ‘’) state = request.GET.get(‘state’, ‘’) redirect_url_id = state.split(’,’)[0] redirect_url = RedirectURL.objects.get(id=redirect_url_id).url redirect_url = ‘{}?code={}&state={}’.format(redirect_url, code, state) log.info(‘cherrypick, redirect_url: %s, state: %s’, redirect_url, state) return redirect(redirect_url) SaaS 服务器处理 oauth 2.0 C、D之后的步骤@psa(‘our_social:complete’)def complete(request, backend, *args, **kwargs): “““Authentication complete view””” logout(request) state = request.GET.get(‘state’, ‘’) …… state解析出cid等参数 customer = Customer.objects.get(id=cid) appid, appsecret = product.get_key_and_secret() request._customer = customer 覆盖backend的方法 def get_key_and_secret(): log.info(’login complete use appid: %s %s’, appid, state) request.backend.get_key_and_secret = get_key_and_secret return do_complete(request.backend, _do_login, request.user, redirect_name=REDIRECT_FIELD_NAME, request=request, *args, **kwargs) ...

January 15, 2019 · 1 min · jiezi

轻松理解OAuth2.0协议(多图)

前言最近有一个简单的需求需要在一个微信公众号外部的H5页面中使用微信登录(接入),同时还要在内部使用第三方支付接口完成支付(不使用微信支付).无奈队友不给力只好自己研究了一下具体的流程,不出意料的是这两个接入商提供的SDK不同另外微观操作不同,但是基本流程是相似的.理解了其中的规则觉得很有意思,边萌生出了使用简单的几张图片来解释OAuth2.0协议的想法.ps:素材是使用note8+autodesk画的,合成使用的是Photoshop,对于我这种手残真的累死了,下次能写字就不发图了( _ ).注意:本文章不是介绍微信以及第三方支付接入具体的实现的文章.什么是OAuth2.0协议(它能干什么)OAuth(开放授权)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。那什么是开放授权,最直接的例子就是在登录一些网站的时候使用第三方账户进行登录,相信这些场景相信大家都再熟悉不过了.为什么使用OAuth2.0协议举一个简单的例子.我们正在维护一个网上商城,当用户点击购买的时候,我们想借用第三方支付来向我们账户里打钱,而不用花巨额的力气再去开发一套支付系统.现在用户点击付钱了你会哪个选项来提供支付功能呢?选项A:<a href=“www.xxx.com/pay/account/123456">点击支付</a>这个方法不错,几乎0成本,可是它有如下的缺陷(使用这种你是认真的吗?):你不知道用户是否支付成功从而无法确定订单是否完成你不知道用户输入的金额你不知道是谁向你支付,买的什么东西没有这种可爱的用户(大概吧)选项B: 直接和用户索要第三方支付的账户和密码,然后由后台进行代理购买.提出这个方案的人简直是天才,他彻底的解决了无法确认是谁支付的问题,但是有如下的不足:传输用户的帐号密码不安全由上面引发的种种不安全你不安全(重要)选项C: 使用AUTH2.0协议.用户,第三方平台和我(服务商)的想法????用户使用第三方账户登录一个新的网站,对于用户来说无非就是有如下的需求:懒的注册想利用这种第三方登入的便利性(例如:使用QQ登入百度网盘的用户免费获取2T空间)第三方平台那么对于第三方就拿腾讯来说吧,假如我有一个博客,要使用QQ登入,对于腾讯来说如何做才能保证用户的安全呢?首先确认要求接入的服务商提供者的信息资历等(必要的时候可以查水表????)然后给要求接入的服务商一个唯一凭证,标明服务商身份服务商(我)我们要做的就是将这两者进行连接起来,而具体的方式如下:正文一般来说你会得到如下的两个参数:appid 代表你的应用唯一IDappsecret 对应的密钥这个部分每家平台都不一样,具体如何获取你的APPID请参考对应平台的指南.注意:第三方平台给你的不一定是APPID,我的意思不是连名字都完全一样,有的平台给的参数多有的给的少,总之都是用于验明身份的.用户不一定第一次授权就是登录操作,这里我们以登录为例.在这个流程中服务器(我)接受到了用户想要第三方登录的请求,我们使用之前获取的APPID(不同平台叫法和参数可能不同),然后拼接为成第三方平台指定的url然后直接重定向到这个url.例如在这个例子中我们的地址可能长这个样子:www.xxx.com/oauth2.0/authorize?appid=123456&redirect=www.sss.com/login参数:appid 我们的应用对于第三方平台的唯一idredirect 用户同意授权后被重定向的地址,一般来说都是本应用的首页或者登录页面,在本例中就是www.sss.com/login这个地址.其他参数 根据第三方平会有不同的额外参数.然后将用户重定向到这个url中,此时用户会跳转到www.xxx.com.注意:重定向是一个很重要的参数,当用户同意授权后第三方服务器将会重定向到这个地址.这个时候用户被重定向到了www.xxx.com上,页面提示用户是否要向www.sss.com进行授权.如果用户同意了授权,那么第三方服务器会重定向url到我们指定的url上,在本例中就是www.sss.com/login并且带上一个code参数.在这个例子中这个url看起来是这个样子的:www.sss.com/login?code=xxxxx此时我们的www.sss.com/login接受到了一个含有code的请求,我们知道这个是一个第三方登录授权后的请求.我们再次拼接一个url(不同平台地址规则不同),但是一般来说这个请求会有如下的参数:code 用户授权后重定向带回来的codeappid 应用唯一idappsecret 应用对应的密钥在这个例子中我们请求服务器的url可能是这个样子的:www.xxx.com/oauth2/access_token?appid=xxxx&secert=xxxx&code=xxxx如果一切顺利在这个阶段我们就可以获取第三方平台响应的一个accesstoken,这个accesstoken代表着用户对于这个应用的授权.除此以外你还会获取到用户的基本信息例如用户的唯一id之类的.后续的请求用户的信息需要使用accesstoken进行请求.现在我们来完成用户的登录这个流程.利用accesstoken我们向服务器获取了用户的名字,显示在了我们的应用中.后续的资源获取就是这个模式(不同平台资源获取地址以及方式有可能稍有不同).在用户的资源请求中对于敏感的操作,都不会在前端中完成,都是代交由服务器进行资源获取.注意用户的会话持久依然是由你来做的,当用户首次授权登录后你就应该记住用户,而不是每次登录时候都进行授权.accesstoken一般都会有过期时间,使用一段时间后失效,所以在某个环节你还可以得到一个refreshtoken用于刷新accesstoken.参考&引用http://www.ruanyifeng.com/blo… https://www.cnblogs.com/flash… https://blog.csdn.net/hel_wor… https://blog.csdn.net/wang839…

December 24, 2018 · 1 min · jiezi

spring OAuth快速入门

如何阅读前几节简单介绍了OAuth,以及spring secuirty OAuth的一些基本构成。文中有一些关联知识点的引用,如果要深入研究,可以点进去详细了解一下,这样可以加深立即。如果你只想快速浏览OAuth server的搭建可以跳过前面几节,直接看下面的代码实例。什么是OAuth2OAuth(开放授权)是一个开放标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容。Oauth2 授权方式authorization_code(授权码类型,使用三方qq登录使用该方式)implicit(隐式授权类型)password(资源所有者即用户密码类型,典型应用是微服务里使用uaa服务器进行登录认证)client_credentials(客户端凭据【客户端ID以及Key】类型,典型应用是微服务内部系统与系统之间调用客服端授权)refresh_token(通过以上授权获得的刷新令牌来获取新的令牌)。关于OAuth不同授权的具体流程可以参考[理解OAuth 2.0-阮一峰](http://www.ruanyifeng.com/blo…。spring Security OAuth2spring security OAuth 是对OAuth2协议的一个实现。是在spring security的基础上发展而来,之前是spring security的一个子项目,现在已经独立出来。详细见官网.spring OAuth使用类似spring secuirty的机制来实现OAuth2 .根据OAuth协议规定,一个完整的授权服务器应当包含:授权服务器和资源服务器。授权服务器负责认证和授权,资源服务器负责根据用户提供的授权凭证(比如给以token)。这里有一篇官方文档介绍了spring OAuth的使用.[OAuth 2 Developers Guide](http://projects.spring.io/spr…授权服务一个授权服务大致几个模块:client管理、授权接口、用户认证、令牌管理。client管理主要用来管理和区分不同的client,我们可以通过配置认证client链接是否合法,能为该client提供哪些授权服务,个性化定制client允许的行为。在spring secruity OAuth2中,可以对client进行如下属性配置:clientId:(必须的)用来标识客户的Id。secret:(需要值得信任的客户端)客户端安全码,如果有的话。scope:用来限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围。authorizedGrantTypes:此客户端可以使用的授权类型,默认为空。authorities:此客户端可以使用的权限(基于Spring Security authorities)。授权接口是授权服务对外提供的http入口。在spring中授权端口如下:spring security OAuth授端口/oauth/authorize:授权端点。对应AuthorizationEndpoint类/oauth/token:令牌端点。对应TokenEndpoint类/oauth/confirm_access:用户确认授权提交端点。对应WhitelabelApprovalEndpoint类/oauth/error:授权服务错误信息端点。对应WhitelabelApprovalEndpoint类/oauth/check_token:用于资源服务访问的令牌解析端点。对应CheckTokenEndpoint类/oauth/token_key:提供公有密匙的端点,如果你使用JWT令牌的话。对应TokenKeyEndpoint类授权是使用 AuthorizationEndpoint 这个端点来进行控制的,你能够使用 AuthorizationServerEndpointsConfigurer 这个对象的实例来进行配置 ,如果你不进行设置的话,默认是除了资源所有者密码(password)授权类型以外,支持其余所有标准授权类型的(RFC6749),我们来看一下这个配置对象有哪些属性可以设置吧,如下列表:authenticationManager:认证管理器,当你选择了资源所有者密码(password)授权类型的时候,请设置这个属性注入一个 AuthenticationManager 对象。userDetailsService:如果啊,你设置了这个属性的话,那说明你有一个自己的 UserDetailsService 接口的实现,或者你可以把这个东西设置到全局域上面去(例如 GlobalAuthenticationManagerConfigurer 这个配置对象),当你设置了这个之后,那么 “refresh_token” 即刷新令牌授权类型模式的流程中就会包含一个检查,用来确保这个账号是否仍然有效,假如说你禁用了这个账户的话。authorizationCodeServices:这个属性是用来设置授权码服务的(即 AuthorizationCodeServices 的实例对象),主要用于 “authorization_code” 授权码类型模式。implicitGrantService:这个属性用于设置隐式授权模式,用来管理隐式授权模式的状态。tokenGranter:这个属性就很牛B了,当你设置了这个东西(即 TokenGranter 接口实现),那么授权将会交由你来完全掌控,并且会忽略掉上面的这几个属性,这个属性一般是用作拓展用途的,即标准的四种授权模式已经满足不了你的需求的时候,才会考虑使用这个用户认证,例如在使用password授权模式时,需要在获取令牌之前先校验用户提供的凭证是否合法,合法的凭证是用户获取授权令牌的前提,spring secuiurty OAuth使用了spring security的认证服务,在令牌获取端口AuthenticationManager进行授权,这个会在后面的授权端口中提到。令牌管理服务负责令牌是生成、校验等操作。令牌主要有两种解决方案:使用随机算法生成唯一标示与用户授权关联,然后保存起来供校验时查询,为了方便资源服务方便验证令牌,这种方案常常是授权服务和资源服务共存的,如果不共存,那么资源服务的tokenStore服务于授权服务tokenStore要做到数据互通,spring 的解决方案是提供/oauth/check_token接口来完成。第二种是授权服务器使用某种算法生成字符串,资源服务器使用约定好的算法对令牌进行解析校验,以验证 他的合法性。在spring security OAuth2中提供了InMemoryTokenStore、JdbcTokenStore、JwtTokenStore。前面两者需要将令牌存在起来,最后一个JwtTokenStore是jwt令牌TokenStore的实现,他不存储令牌,只根据一定的规则和秘钥验证令牌的合法性。jwt令牌分为三段:头部信息、playload、签名。每段使用base64编码,每段之间使用".“连接。资源服务一个资源服务(可以和授权服务在同一个应用中,当然也可以分离开成为两个不同的应用程序)提供一些受token令牌保护的资源,Spring OAuth提供者是通过Spring Security authentication filter来验证过滤器来实现的保护(OAuth2AuthenticationProcessingFilter),你可以通过 @EnableResourceServer 注解到一个 @Configuration 配置类上来标记应用是一个资源服务器,你可以通过配置 ResourceServerConfigurer 配置对象来进行资源服务器的一些自定义配置(可以选择继承自 ResourceServerConfigurerAdapter 然后覆写其中的方法,参数就是这个对象的实例),下面是一些可以配置的属性:tokenServices:ResourceServerTokenServices 类的实例,用来实现令牌服务。resourceId:这个资源服务的ID,这个属性是可选的,但是推荐设置并在授权服务中进行验证。其他的拓展属性例如 tokenExtractor 令牌提取器用来提取请求中的令牌,也就说,你可以自定义提如何在请求中提取令牌。请求匹配器,用来设置需要进行保护的资源路径,默认的情况下是受保护资源服务的全部路径。受保护资源的访问规则,默认的规则是简单的身份验证(plain authenticated)。其他的自定义权限保护规则通过 HttpSecurity 来进行配置。@EnableResourceServer 注解自动增加了一个类型为 OAuth2AuthenticationProcessingFilter 的过滤器链。ResourceServerTokenServices 是组成授权服务的另一半,如果你的授权服务和资源服务在同一个应用程序上的话,你可以使用 DefaultTokenServices ,这样的话,你就不用考虑关于实现所有必要的接口的一致性问题,这通常是很困难的。如果你的资源服务器是分离开的,那么你就必须要确保能够有匹配授权服务提供的 ResourceServerTokenServices,它知道如何对令牌进行解码。在授权服务器上,你通常可以使用 DefaultTokenServices 并且选择一些主要的表达式通过 TokenStore(后端存储或者本地编码)。RemoteTokenServices 可以作为一个替代,它将允许资源服务器通过HTTP请求来解码令牌(也就是授权服务的 /oauth/check_token 端点)。如果你的资源服务没有太大的访问量的话,那么使用RemoteTokenServices 将会很方便(所有受保护的资源请求都将请求一次授权服务用以检验token值),或者你可以通过缓存来保存每一个token验证的结果。spring Security OAuth 配置示例关于spring OAuth的更多配置细节可以参考官方文档:OAuth2 Autoconfig.spring Secuirty OAuth2配置比较简单,关键还是要理解OAuth2协议,以及他的使用场景。添加依赖<dependencies> <!– … other dependency elements … –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.1.RELEASE</version> </dependency></dependencies>授权服务配置授权服务配置主要需要完成以下几个配置:ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService),客户端详情信息在这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。AuthorizationServerSecurityConfigurer:用来配置令牌端点(Token Endpoint)的安全约束.AuthorizationServerEndpointsConfigurer:用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)我们可以继承AuthorizationServerConfigurerAdapter并且覆写其中的三个configure方法来进行配置。下面是简单的配置demo,配置来源于官方文档。@Componentpublic class CustomAuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter { AuthenticationManager authenticationManager; public CustomAuthorizationServerConfigurer( AuthenticationConfiguration authenticationConfiguration ) throws Exception { this.authenticationManager = authenticationConfiguration.getAuthenticationManager(); } @Override public void configure( ClientDetailsServiceConfigurer clients ) throws Exception { //下面配置了一个简单的客服端 clients.inMemory()// 使用内存保存客服端信息,建议编写自己的clientService来配置客服端 .withClient(“client”)//客服端的clientid .authorizedGrantTypes(“password”,“client_credentials”,“refresh_token”)//支持的授权方式 .secret(“secret”)//该客服端访问时的密码 .scopes(“all”);//scope范围 } @Override public void configure( AuthorizationServerEndpointsConfigurer endpoints ) throws Exception { endpoints.authenticationManager(authenticationManager); endpoints.tokenStore(tokenStore()); } @Bean public TokenStore tokenStore() { return new InMemoryTokenStore(); } }以上完成了一个简单的授权服务配置,我们的配置重点放在了client的配置上,由于我们为配置资源拥有者(用户),我们无法使用password授权,应为用户信不存在,我们无论输入用户名和密码都不正确。因此我暂且只能使用,client_credentials,这个仅仅是client授权,不牵扯到用户信息。使用下面命令curl client:secret@localhost:8080/oauth/token -d grant_type=client_credentials一切顺利的话我们可以得到token相关信息。如果要使用其他比如password等用户授权,那么需要在授权服务器配置用户相关信息,并把相关的userDetailService配置到AuthorizationServerEndpointsConfigurer对象上,上面有提到endponint配置相关规则。资源服务配置资源服务的作用前面已经介绍过了,资源服务主要关注两个事情,对token进行鉴权,根据具体权限包含资源。由于token可能是其他服务器生成,因此要做到ResourceServerTokenServices与授权服务的tokenService相匹配。下面是简单的配置. @EnableResourceServer public static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { // 注意这个TokenStore需要和之前配置的一致 private final TokenStore tokenStore; public ResourceServerConfiguration(TokenStore tokenStore) { this.tokenStore = tokenStore; } // 资源服务器安全规则配置 @Override public void configure(HttpSecurity http) throws Exception { http .exceptionHandling() .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)) .and() .csrf() .disable() .headers() .frameOptions() .disable() .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/api/register”).permitAll() .antMatchers("/api/activate").permitAll() .antMatchers("/api/authenticate").permitAll() .antMatchers("/api/account/reset-password/init").permitAll() .antMatchers("/api/account/reset-password/finish").permitAll() .antMatchers("/api/").authenticated() // 配置scope权限访问// .antMatchers("/api/").access("#oauth2.hasScope(‘read’) or (!#oauth2.isOAuth() and hasRole(‘ROLE_USER’))"); } @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.resourceId(“jhipster-uaa”) .tokenStore(tokenStore); } }上面是资源服务和授权服务在一台机器上时的配置。由于共享tokenStore,因此资源服务在验证token的时候不会出任何问题。@Configuration@EnableResourceServer@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)public class SecurityConfiguration extends ResourceServerConfigurerAdapter { private final OAuth2Properties oAuth2Properties; public SecurityConfiguration(OAuth2Properties oAuth2Properties) { this.oAuth2Properties = oAuth2Properties; } // 主要配置资源的保护规则 @Override public void configure(HttpSecurity http) throws Exception { http .csrf() .disable() .headers() .frameOptions() .disable() .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/api/").authenticated() .antMatchers("/management/health").permitAll() .antMatchers("/management/info").permitAll() .antMatchers("/management/").hasAuthority(AuthoritiesConstants.ADMIN); } // 配置tokenstore,资源服务器在进行token验证的时候需要, @Bean public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) { return new JwtTokenStore(jwtAccessTokenConverter); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter(OAuth2SignatureVerifierClient signatureVerifierClient) { return new OAuth2JwtAccessTokenConverter(oAuth2Properties, signatureVerifierClient); }}上面是采用jwt,资源/授权服务分离的配置,以上配置无法和上面的授权服务配置配合使用。若要配置使用,需要修改授权服务的TokenStore为JwtTokenStore,并且在OAuth2JwtAccessTokenConverter中需访问授权服务器/token/access_key获取RSA公钥,该公钥会在token验证时用到。 ...

December 14, 2018 · 2 min · jiezi

PHP下的Oauth2.0尝试 - OpenID Connect

OpenID ConnectOpenID Connect简介OpenID Connect是基于OAuth 2.0规范族的可互操作的身份验证协议。它使用简单的REST / JSON消息流来实现,和之前任何一种身份认证协议相比,开发者可以轻松集成。OpenID Connect允许开发者验证跨网站和应用的用户,而无需拥有和管理密码文件。OpenID Connect允许所有类型的客户,包括基于浏览器的JavaScript和本机移动应用程序,启动登录流动和接收可验证断言对登录用户的身份。OpenID的历史是什么?OpenID Connect是OpenID的第三代技术。首先是原始的OpenID,它不是商业应用,但让行业领导者思考什么是可能的。OpenID 2.0设计更为完善,提供良好的安全性保证。然而,其自身存在一些设计上的局限性,最致命的是其中依赖方必须是网页,但不能是本机应用程序;此外它还要依赖XML,这些都会导致一些应用问题。OpenID Connect的目标是让更多的开发者使用,并扩大其使用范围。幸运的是,这个目标并不遥远,现在有很好的商业和开源库来帮助实现身份验证机制。OIDC基础简要而言,OIDC是一种安全机制,用于应用连接到身份认证服务器(Identity Service)获取用户信息,并将这些信息以安全可靠的方法返回给应用。在最初,因为OpenID1/2经常和OAuth协议(一种授权协议)一起提及,所以二者经常被搞混。OpenID是Authentication,即认证,对用户的身份进行认证,判断其身份是否有效,也就是让网站知道“你是你所声称的那个用户”;OAuth是Authorization,即授权,在已知用户身份合法的情况下,经用户授权来允许某些操作,也就是让网站知道“你能被允许做那些事情”。由此可知,授权要在认证之后进行,只有确定用户身份只有才能授权。(身份验证)+ OAuth 2.0 = OpenID ConnectOpenID Connect是“认证”和“授权”的结合,因为其基于OAuth协议,所以OpenID-Connect协议中也包含了client_id、client_secret还有redirect_uri等字段标识。这些信息被保存在“身份认证服务器”,以确保特定的客户端收到的信息只来自于合法的应用平台。这样做是目的是为了防止client_id泄露而造成的恶意网站发起的OIDC流程。在OAuth中,这些授权被称为scope。OpenID-Connect也有自己特殊的scope–openid ,它必须在第一次请求“身份鉴别服务器”(Identity Provider,简称IDP)时发送过去。OpenID Connect 实现我们的本代码实现建立在PHP下的Oauth2.0尝试 - 授权码授权(Authorization Code Grant) 文章的代码基础上调整证书# 生成私钥 private key$ openssl genrsa -out privkey.pem 2048# 私钥生成公钥 public key$ openssl rsa -in privkey.pem -pubout -out pubkey.pem调整serverprivate function server(){ $pdo = new \PDO(‘mysql:host=ip;dbname=oauth_test’, “user”, “123456”); $storage = new \OAuth2\Storage\Pdo($pdo); $config = [ ‘use_openid_connect’ => true, //openid 必须设置 ‘issuer’ => ‘sxx.qkl.local’ ]; $server = new \OAuth2\Server($storage, $config); // 第二个参数,必须设置值为public_key $server->addStorage($this->getKeyStorage(), ‘public_key’); // 添加 Authorization Code 授予类型 $server->addGrantType(new \OAuth2\GrantType\AuthorizationCode($storage)); // 添加 Client Credentials 授予类型 一般三方应用都是直接通过client_id & client_secret直接请求获取access_token $server->addGrantType(new \OAuth2\GrantType\ClientCredentials($storage)); return $server;}private function getKeyStorage(){ $rootCache = dirname(APP_PATH) . “/cert/oauth/”; $publicKey = file_get_contents($rootCache.‘pubkey.pem’); $privateKey = file_get_contents($rootCache.‘privkey.pem’); // create storage $keyStorage = new \OAuth2\Storage\Memory(array(‘keys’ => array( ‘public_key’ => $publicKey, ‘private_key’ => $privateKey, ))); return $keyStorage;}授权public function authorize(){ // scope增加openid // 该页面请求地址类似: // http://sxx.qkl.local/v2/oauth/authorize?response_type=code&client_id=testclient&state=xyz&redirect_uri=http://sxx.qkl.local/v2/oauth/cb&scope=basic%20get_user_info%20upload_pic%20openid //获取server对象 $server = $this->server(); $request = \OAuth2\Request::createFromGlobals(); $response = new \OAuth2\Response(); // 验证 authorize request // 这里会验证client_id,redirect_uri等参数和client是否有scope if (!$server->validateAuthorizeRequest($request, $response)) { $response->send(); die; } // 显示授权登录页面 if (empty($_POST)) { //获取client类型的storage //不过这里我们在server里设置了storage,其实都是一样的storage->pdo.mysql $pdo = $server->getStorage(‘client’); //获取oauth_clients表的对应的client应用的数据 $clientInfo = $pdo->getClientDetails($request->query(‘client_id’)); $this->assign(‘clientInfo’, $clientInfo); $this->display(‘authorize’); die(); } $is_authorized = true; // 当然这部分常规是基于自己现有的帐号系统验证 if (!$uid = $this->checkLogin($request)) { $is_authorized = false; } // 这里是授权获取code,并拼接Location地址返回相应 // Location的地址类似:http://sxx.qkl.local/v2/oauth/cb?code=69d78ea06b5ee41acbb9dfb90500823c8ac0241d&state=xyz $server->handleAuthorizeRequest($request, $response, $is_authorized, $uid); if ($is_authorized) { // 这里会创建Location跳转,你可以直接获取相关的跳转url,用于debug $parts = parse_url($response->getHttpHeader(‘Location’)); var_dump($parts); parse_str($parts[‘query’], $query); // 拉取oauth_authorization_codes记录的信息,包含id_token $code = $server->getStorage(‘authorization_code’) ->getAuthorizationCode($query[‘code’]); var_dump($code); }// $response->send();}curl获取# 使用 HTTP Basic Authentication$ curl -u testclient:123456 http://sxx.qkl.local/v2/oauth/token -d ‘grant_type=client_credentials’# 使用 POST Body 请求$ curl http://sxx.qkl.local/v2/oauth/token -d ‘grant_type=client_credentials&client_id=testclient&client_secret=123456’postman获取access_token总结access_token 用于授权id_token(通常为JWT) 用于认证通常我们首先,需要使用id_token登录然后,你会得到一个access_token最后,使用access_token来访问授权相关接口。 ...

September 28, 2018 · 2 min · jiezi