oauth2 单点登录_Spring Security Oauth2 单点登录配置及原理深度剖析

单点登录(SingleSignOn,SSO),就是通过用户的一次性鉴别登录。当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。

随着企业各系统越来越多,如办公自动化(OA)系统,财务管理系统,档案管理系统,信息查询系统等,登录问题就变得愈发重要。要记录那么多的用户名和密码,实在不是一件容易的事儿。而为了便于记忆,很多人都在不同的站点使用相同的用户名和密码,虽然这样可以减少负担,但是同时也降低了安全性,而且使用不同的站点同样要进行多次登录。基于以上原因,为用户提供一个畅通的登录通道变得十分重要。

单点登录(SingleSign-On,SSO)是一种帮助用户快捷访问网络中多个站点的安全通信技术。单点登录系统基于一种安全的通信协议,该协议通过多个系统之间的用户身份信息的交换来实现单点登录。使用单点登录系统时,用户只需要登录一次,就可以访问多个系统,不需要记忆多个口令密码。单点登录使用户可以快速访问网络,从而提高工作效率,同时也能帮助提高系统的安全性。

OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是OAUTH的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAUTH是安全的。OAuth是Open Authorization的简写。

虽然OAuth2一开始是用来允许用户授权第三方应用访问其资源的一种协议,并不是用来做单点登录的,但是我们可以用其特性,来变相的实现单点登录,其中就要用到其授权码模式(authorization code),并且,token生成使用JWT。

下面我们建立一套工程,包含授权平台、OA-综合办公平台、CRM-移动营销平台,来模拟单点登录过程,阐述其配置进行,并针对其原理,进行深度剖析。

授权平台

pom

最终的pom依赖如下:

<?xml version="1.0" encoding="UTF-8"?>4.0.0        spring-security-oauth2-sso-sample        org.xbdframework.sample1.0.0-SNAPSHOTorg.xbdframework.sample    sso-auth-server    0.0.1-SNAPSHOTsso-auth-serverDemo project for Spring Bootorg.springframework.boot            spring-boot-starter-thymeleaf        org.springframework.boot            spring-boot-starter-web        org.springframework.boot            spring-boot-starter-security        org.springframework.security.oauth.boot            spring-security-oauth2-autoconfigure        org.springframework.boot            spring-boot-starter-test            testorg.springframework.boot                spring-boot-maven-plugin            

请注意,spring-security-oauth2-autoconfigure依赖必不可少,这是SpringBoot工程,而不是SpringCloud工程。SpringCloud的话,引入oauth2 starter即可。

EnableAuthorizationServer

授权服务器配置如下:

package org.xbdframework.sample.sso.authserver.confg;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.oauth2.config.annotation.builders.InMemoryClientDetailsServiceBuilder;import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;import org.springframework.security.oauth2.provider.ClientDetailsService;import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;@EnableAuthorizationServer@Configurationpublic class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {    @Autowired    private PasswordEncoder passwordEncoder;    @Override    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {        security.allowFormAuthenticationForClients()                .tokenKeyAccess("isAuthenticated()");    }    @Override    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {        clients.withClientDetails(a1());    }    @Bean    public ClientDetailsService a1() throws Exception {        return new InMemoryClientDetailsServiceBuilder()                // client oa application                .withClient("oa")                .secret(passwordEncoder.encode("oa_secret"))                .scopes("all")                .authorizedGrantTypes("authorization_code", "refresh_token")                .redirectUris("http://localhost:8080/oa/login", "http://www.baidu.com")                .accessTokenValiditySeconds(7200)                .autoApprove(true)                .and()                // client crm application                .withClient("crm")                .secret(passwordEncoder.encode("crm_secret"))                .scopes("all")                .authorizedGrantTypes("authorization_code", "refresh_token")                .redirectUris("http://localhost:8090/crm/login")                .accessTokenValiditySeconds(7200)                .autoApprove(true)                .and()                .build();    }    @Override    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {        endpoints.accessTokenConverter(jwtAccessTokenConverter())                .tokenStore(jwtTokenStore());    }    @Bean    public JwtTokenStore jwtTokenStore() {        return new JwtTokenStore(jwtAccessTokenConverter());    }    @Bean    public JwtAccessTokenConverter jwtAccessTokenConverter() {        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();        jwtAccessTokenConverter.setSigningKey("123456");        return jwtAccessTokenConverter;    }}

WebSecurityConfiguration

Spring Security 配置如下:

package org.xbdframework.sample.sso.authserver.confg;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.builders.WebSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import org.springframework.security.core.userdetails.User;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.provisioning.InMemoryUserDetailsManager;import java.util.ArrayList;import java.util.Collection;import java.util.List;@Configurationpublic class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {    @Override    protected void configure(AuthenticationManagerBuilder auth) throws Exception {        auth.userDetailsService(userDetailsServiceBean()).passwordEncoder(passwordEncoder());    }    @Override    public void configure(WebSecurity web) throws Exception {        web.ignoring().antMatchers("/assets/**", "/css/**", "/images/**");    }    @Override    protected void configure(HttpSecurity http) throws Exception {        http.formLogin()                .loginPage("/login")                .and()                .authorizeRequests()                .antMatchers("/login").permitAll()                .anyRequest()                .authenticated()                .and().csrf().disable().cors();    }    @Bean    @Override    public UserDetailsService userDetailsServiceBean() {        Collection users = buildUsers();        return new InMemoryUserDetailsManager(users);    }    private Collection buildUsers() {        String password = passwordEncoder().encode("123456");        List users = new ArrayList<>();        UserDetails user_admin = User.withUsername("admin").password(password).authorities("ADMIN", "USER").build();        UserDetails user_user1 = User.withUsername("user 1").password(password).authorities("USER").build();        users.add(user_admin);        users.add(user_user1);        return users;    }    @Bean    public PasswordEncoder passwordEncoder() {        return new BCryptPasswordEncoder();    }    @Bean    @Override    public AuthenticationManager authenticationManagerBean() throws Exception {        return super.authenticationManagerBean();    }}

至于后续的创建登录页面、首页等,比较简单,不再赘述,想查看具体代码请参考文末的工程链接,下载后自行查看。

OA-综合办公平台

pom

最终的pom依赖如下:

<?xml version="1.0" encoding="UTF-8"?>4.0.0        spring-security-oauth2-sso-sample        org.xbdframework.sample1.0.0-SNAPSHOTorg.xbdframework.sample    sso-oa    0.0.1-SNAPSHOTsso-oaDemo project for Spring Bootorg.springframework.boot            spring-boot-starter-oauth2-client        org.springframework.boot            spring-boot-starter-security        org.springframework.security.oauth.boot            spring-security-oauth2-autoconfigure        org.springframework.boot            spring-boot-starter-thymeleaf        org.thymeleaf.extras            thymeleaf-extras-springsecurity5        org.springframework.boot            spring-boot-starter-web        org.springframework.boot            spring-boot-starter-test            testorg.springframework.boot                spring-boot-maven-plugin            

WebSecurityConfiguration

Spring Security配置如下:

package org.xbdframework.sample.sso.oa.config;import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;import org.springframework.context.annotation.Configuration;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.builders.WebSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;@EnableOAuth2Sso@Configurationpublic class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {    @Override    public void configure(WebSecurity web) throws Exception {        super.configure(web);    }    @Override    protected void configure(HttpSecurity http) throws Exception {        http.logout()                .and()                .authorizeRequests()                .anyRequest().authenticated()                .and()                .csrf().disable();    }}

特别注意,一定不要忘记@EnableOAuth2Sso注解,这是单点登录相关自动化配置的入口。

CRM-移动营销平台

相关配置同OA平台,不再赘述。

单点登录过程

系统都建立好以后,我们依次启动授权系统(8888)、OA平台(8080)、CRM平台(8090)。访问OA平台http://localhost:8080/oa/system/profile。此时,浏览器会重定向到授权系统登录页面,需要登录。

f550d24ab51547428b3e45b5ad380c65

此时,我们查看Network标签,查看其访问路径,即可看到确实是先跳转到了商品系统的登录页面(http://localhost:8080/oa/login),然后又重定向到授权系统的授权链接,由于未登录,所以最后又重定向到授权系统的登录界面,如图:

8273d2a839eb4131ad957c2e8c97c03b

使用admin/123456进行登录,成功的跳转到了OA平台的profile页面。

1b80b768fcfc443a88f3bce35680ed3c

可以看到,admin用户已成功登录。此时,再次查看Network标签。

15193bc4799b4641bef6a4ad38eecc71

在成功登录之后,授权系统重定向到OA平台配置的回调地址(http://localhost:8080/oa/login),与此同时,携带了两个参数code和state。最最重要的一个,便是code(state参数是防止CSRF攻击而设置的,此处不谈)。客户端可根据此code,访问授权系统token接口(/oauth/token),申请token。申请成功后,重定向到OA平台配置的回调地址(http://localhost:8080/oa/login)。

然后,我们点击“CRM-移动营销平台”(亦可直接在浏览器输入地址访问,效果是一样的。CRM平台的访问地址为:http://localhost:8090/crm/sysmtem/profile),此时,我们并不需要登录,即可直接访问该页面。

d47d972331074e5dac7912c8f051f4c7

再次查看Network标签。

86ebd9a3b8614985bc591cae30a58d8e

可以看到,与第一次访问OA平台相同,浏览器先重定向到CRM平台的登录页面,然后又重定向到授权系统的授权链接,最后直接就重新重定向到CRM平台的登录页面,而不同的是,此次访问并不需要再次重定向到授权系统进行登录,而是成功访问授权系统的授权接口,并携带着code重定向到CRM平台的回调路径。然后框架依据此code,再次访问授权的token接口(/oauth/token),顺利拿到了token,可正常访问受保护的资源。

为什么访问第二次无需登录,就直接拿到了第一次登录的用户信息了呢,它是怎么拿到的,而且不至于发生错乱呢?

这还是归功于Spring Security。Spring Security第一个Filter,便是SecurityContextPersistenceFilter。作何用处呢?从字面意思理解,便是安全上下文持久化Filter,即存储已认证成功的用户信息。如遇该用户请求后续访问,则可直接取出并使用。

f5afef9c475140158143e49842e8852a

其重点就在于HttpSessionSecurityContextRepository类的loadContext。

bff8bf9ba89e4e41907238cf1211203e

再来看一下readSecurityContextFromSession方法是如何获取SecurityContext的。

1c4f2dd1547f4f668c94023b71affcc8

Spring Security在认证成功后,会向Session中写入一些属性,而key值即为SPRING_SECURITY_CONTEXT。后续可根据Request请求中的Session,获取此信息,其中有登录用户、权限等信息。

基于同一浏览器访问同一网站,其JSESSIONID固定。所以当访问OA平台时,浏览器会重定向到授权平台,此时会生成一个JSESSIONID,以标识当前登录用户,然后再重定向回OA平台回调地址;当再访问CRM平台时,一样会重定向到授权平台,此时授权平台根据此前生成的唯一JSESSIONID,可直接获取上一次登录用户的信息。这一节作者也是查找了好久,才找到这里,真是不容易!

217e591610b2406894e2ae080177a353
4404a02380134b2fb9e4803f4da67e4c

如图所示,OA平台、CRM平台,在访问授权平台时,为同一个JSESSIONID。

当用户访问服务器的时候会为每一个用户开启一个session,浏览器正是基于JSESSIONID,来判断这个SESSION到底属于哪个用户。即JSESSIONID就是用来判断当前用户对应于哪个SESSION。换句话说,服务器识别SESSION的方法是通过JSESSIONID来告诉服务器该客户端的SESSION在内存的什么地方。

客户端

我们来分析一下客户端是如何触发一系列请求的。

在前文中说过一个注解非常重要,就是@EnableOAuth2Sso。依托此注解,框架自动注册了OAuth2ClientAuthenticationProcessingFilter实例。从名字即能看出作何用处。其中,重要逻辑如下:

988706f5fa5c49f6b14ab7821f136c1a

第二部分没什么说的,重点在于第一部分,从OAuth2RestTemplate中获取token。

c24935cbc4b147bb82e29b455462c488

可以看到,框架先从OAuth2ClientContext中获取缓存的token,如没有,再调用acquireAccessToken方法进行获取。如果发生UserRedirectRequiredException异常,则抛出。记着这里抛出的这个异常,就是由于此,才会触发后续一系列的授权、登录等重定向。

先来说一下acquireAccessToken方法。

c9e85275b3af4e2e9970c1dfb3a1a1e3

其它的都不重要,重点一就在于accessTokenProvider.obtainAccessToken这一句话,而accessTokenProvider不是别的,正是AuthorizationCodeAccessTokenProvider。重点二,一旦获取了token,即缓存到OAuth2ClientContext中。因此,后续请求可直接从OAuth2ClientContext中获取token,就是这个原因。

70f08efed91d45f49d0c7f033aaa4b7e

在第一次访问OA平台时时,由于没有登录,也没有申请授权,所以没有code、没有state。因此,框架会生成一个UserRedirectRequiredException并返回,进而被OAuth2RestTemplate的getAccessToken方法捕获并抛出。

那么,抛出的异常框架怎么处理的,触发了重定向呢?

答案就是OAuth2ClientContextFilter,在后续filter过程中,会触发重定向,逻辑如下:

a1158d5873464aa19da4928ca5985269

而重定向返回回调url之后,跟访问profile页面跳转到客户端应用自己的登录页面一样,都是/login,而刚好被OAuth2ClientAuthenticationProcessingFilter所拦截,其拦截路径,就是/login。不同的是,前者是后者一系列操作后的后续操作,即访问profile页面跳转到客户端应用自己的登录页面,被OAuth2ClientAuthenticationProcessingFilter拦截,进而发生UserRedirectRequiredException异常,重定向到授权服务申请授权,申请成功后又重定向到登录页面,进而成功根据code获取到token。

下面再说明一下AccessTokenRequest对象和OAuth2ClientContext对象,这两个bean的声明如下:

42e45bdadc7a435abcc42df30cc80078

可以看到,AccessTokenRequest对象scope为request,针对每个HTTP的request请求有效,也就是说,在一次HTTP请求中,每个Bean定义对应一个实例。而OAuth2ClientContext对象的scope为session,针对每个HTTP的Session有效,即在一个HTTP Session中,每个Bean定义对应一个实例。这样,便把不同请求、不同用户给区分开了。

UML时序图

整个授权过程的时序图如下:

d7cebdeac6f8447a8dd7f63bce1ce950

最后,再次感叹一下,Spring Security OAuth2框架真的厉害!

源码

附上源码,以供参考,欢迎star、fork!

github

https://github.com/liuminglei/spring-security-oauth2-sso-sample.git

gitee

https://gitee.com/xbd521/spring-security-oauth2-sso-sample.git

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值