spring security与oauth2集成实现对多服务系统的认证与授权
系统架构流程
服务模块可以有多个,认证和授权是做在一起的单独一个模块。
原本想写关于spring security的源码阅读的文章,但是一方面考虑时间问题,另一方面出于实用的目的,这里打算记录一下相关启动加载流程,请求认证和授权的流程。
概要如下:
1、启动流程概述
2、token获取流程分析
3、请求的认证与授权流程分析
一、启动流程概述
此处参考这里。
启动流程的关注点主要涉及配置信息的加载和过滤器链的构建。
spring security框架核心就是这个过滤器链!它是分析流程始和调试代码终要关心的重点。
首先看启动入口类org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration,方法setFilterChainProxySecurityConfigurer中有
for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) { webSecurity.apply(webSecurityConfigurer); }
会将org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter类型的配置(security相关)、org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter类型配置(资源服务器相关)、org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter类型配置(授权服务器相关)都装配到org.springframework.security.config.annotation.web.builders.WebSecurity对象中。
继续关注方法springSecurityFilterChain,
1 public Filter springSecurityFilterChain() throws Exception { 2 boolean hasConfigurers = webSecurityConfigurers != null 3 && !webSecurityConfigurers.isEmpty(); 4 if (!hasConfigurers) { 5 WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor 6 .postProcess(new WebSecurityConfigurerAdapter() { 7 }); 8 webSecurity.apply(adapter); 9 } 10 return webSecurity.build(); 11 }
跟进第10行,进入org.springframework.security.config.annotation.AbstractSecurityBuilder#build,
1 public final O build() throws Exception { 2 if (this.building.compareAndSet(false, true)) { 3 this.object = doBuild(); 4 return this.object; 5 } 6 throw new AlreadyBuiltException("This object has already been built"); 7 }
继续进入doBuild()方法,
1 protected final O doBuild() throws Exception { 2 synchronized (configurers) { 3 buildState = BuildState.INITIALIZING; 4 5 beforeInit(); 6 init(); 7 8 buildState = BuildState.CONFIGURING; 9 10 beforeConfigure(); 11 configure(); 12 13 buildState = BuildState.BUILDING; 14 15 O result = performBuild(); 16 17 buildState = BuildState.BUILT; 18 19 return result; 20 } 21 }
由于spring security的配置一般都是继承org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter,这里针对它来看。重点看上面的第6行init(),第11行configure(),第15行performBuild()。init()方法会触发org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter#init方法,
1 public void init(final WebSecurity web) throws Exception { 2 final HttpSecurity http = getHttp(); 3 web.addSecurityFilterChainBuilder(http).postBuildAction(new Runnable() { 4 public void run() { 5 FilterSecurityInterceptor securityInterceptor = http 6 .getSharedObject(FilterSecurityInterceptor.class); 7 web.securityInterceptor(securityInterceptor); 8 } 9 }); 10 }
接着看getHttp()方法,其中含有的如下代码会构建默认过滤器链,
1 http 2 .csrf().and() 3 .addFilter(new WebAsyncManagerIntegrationFilter()) 4 .exceptionHandling().and() 5 .headers().and() 6 .sessionManagement().and() 7 .securityContext().and() 8 .requestCache().and() 9 .anonymous().and() 10 .servletApi().and() 11 .apply(new DefaultLoginPageConfigurer<>()).and() 12 .logout();
可通过debug查看每一步对应构建出来的过滤器,如下图:
同时getHttp()方法最后有
1 configure(http)
这里会执行org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter类型的配置(security相关)、org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter类型配置(资源服务器相关)、org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter类型配置(授权服务器相关)许多configure方法。
到此大致解释了配置信息的加载流程和默认过滤器链的创建。
二、token获取流程分析
本文是使用oauth2的密码授权模式(Password Grant Type),简言之,首次认证时用户传递过来用户名密码,认证服务器会返回一个token,其后用户请求访问资源时带上这个token,即可访问其有权限的资源。详细参考:https://developer.okta.com/blog/2018/06/29/what-is-the-oauth2-password-grant 。
所以本文中所指单点登录就是获取这个token的过程。
token获取的链接是:/oauth/token ,对应代码在spring-security-oauth2 jar包的 org.springframework.security.oauth2.provider.endpoint.TokenEndpoint#postAccessToken ;
如果想了解整个流程走过的过滤器,可以在org.springframework.security.web.FilterChainProxy中断点跟踪执行到的过滤器链中的过滤器,具体可以在org.springframework.security.web.FilterChainProxy.VirtualFilterChain#doFilter方法内断点。
这里我主要想了解token的生成,所以直接在postAccessToken方法内断点,一步步走,直到如下这行代码,
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
这里就是请求授予token的地方,一步步断点跟进去,最后回到方法org.springframework.security.oauth2.provider.CompositeTokenGranter#grant,
1 public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) { 2 for (TokenGranter granter : tokenGranters) { 3 OAuth2AccessToken grant = granter.grant(grantType, tokenRequest); 4 if (grant!=null) { 5 return grant; 6 } 7 } 8 return null; 9 }
这里的tokenRequest见下图,
从字面就看大概看出这对应oauth2的5种grant types,grant types参考https://oauth.net/2/grant-types/ ,我们密码模式对应上图中的最后一个Granter,方法org.springframework.security.oauth2.provider.CompositeTokenGranter#grant的第三行不同Granter都是根据grant type来选择适用Granter,如果不匹配直接返回null。进入最后一个Granter的grant方法如下,
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) { return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest)); }
最后进入getOAuth2Authentication(client, tokenRequest),方法内有如下代码,
userAuth = authenticationManager.authenticate(userAuth);
上面authenticationManager是一个org.springframework.security.authentication.ProviderManager,封装了若干org.springframework.security.authentication.AuthenticationProvider,provider进行认证并返回非空Authentication则结束继续认证,我们项目中使用的是org.springframework.security.authentication.dao.DaoAuthenticationProvider,她会做各种check(比如配置文件中配置的Checker),用户密码校验等;
接着进入org.springframework.security.oauth2.provider.token.DefaultTokenServices#createAccessToken(org.springframework.security.oauth2.provider.OAuth2Authentication)方法,
1 @Transactional 2 public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException { 3 4 OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication); 5 OAuth2RefreshToken refreshToken = null; 6 if (existingAccessToken != null) { 7 if (existingAccessToken.isExpired()) { 8 if (existingAccessToken.getRefreshToken() != null) { 9 refreshToken = existingAccessToken.getRefreshToken(); 10 // The token store could remove the refresh token when the 11 // access token is removed, but we want to 12 // be sure... 13 tokenStore.removeRefreshToken(refreshToken); 14 } 15 tokenStore.removeAccessToken(existingAccessToken); 16 } 17 else { 18 // Re-store the access token in case the authentication has changed 19 tokenStore.storeAccessToken(existingAccessToken, authentication); 20 return existingAccessToken; 21 } 22 } 23 24 // Only create a new refresh token if there wasn't an existing one 25 // associated with an expired access token. 26 // Clients might be holding existing refresh tokens, so we re-use it in 27 // the case that the old access token 28 // expired. 29 if (refreshToken == null) { 30 refreshToken = createRefreshToken(authentication); 31 } 32 // But the refresh token itself might need to be re-issued if it has 33 // expired. 34 else if (refreshToken instanceof ExpiringOAuth2RefreshToken) { 35 ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken; 36 if (System.currentTimeMillis() > expiring.getExpiration().getTime()) { 37 refreshToken = createRefreshToken(authentication); 38 } 39 } 40 41 OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken); 42 tokenStore.storeAccessToken(accessToken, authentication); 43 // In case it was modified 44 refreshToken = accessToken.getRefreshToken(); 45 if (refreshToken != null) { 46 tokenStore.storeRefreshToken(refreshToken, authentication); 47 } 48 return accessToken; 49 50 }
我们项目使用redis存储token的,如果token存在且未过期,20行直接返回,后面的代码则是关于refreshToken是否过期,过期刷新,accessToken和refreshToken创建的过程,默认都是UUID.randomUUID().toString()生成一个UUID,都有过期时间机制,最后存入
tokenStore,对于我们的项目tokenStore就是redis。
到此token获取流程分析完毕。
三、请求的认证与授权流程分析
客户请求某个子应用时,需要先到认证服务器去认证,接着到授权服务器去检测权限,那么如何办到的呢? 假设服务应用为A,我们这里认证和授权的两个服务是做在一期的,姑且称之为B;
认证与授权的流程简图如下,
图1
图2
图3
图2和图3分别是服务模块和认证授权模块中的过滤器链;抓住过滤器链方才能够掌握整个流程。
posted on 2019-08-27 19:23 mylittlecabin 阅读(4060) 评论(0) 编辑 收藏 举报