1.前言
一个正常的授权码模式如下
(A)用户打开客户端以后,客户端要求用户给予授权。
(B)用户同意给予客户端授权。
(C)客户端使用上一步获得的授权,向认证服务器申请令牌。
(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E)客户端使用令牌,向资源服务器申请获取资源。
(F)资源服务器确认令牌无误,同意向客户端开放资源。
下图为一个客户信息请求流向图,客户的访问通过Zuul往后进行动态路由进而访问对应的资源服务,本次要通过要Zuul结合Oauth,实现分布式服务的鉴权控制
2.时序图
本次需要通过使用@EnableOAuth2Sso,实现Zuul服务自动进行执行命令牌模式,下图为对应时序图
3.依赖及配置
各服务作用
ekserver:注册服务中心
zuulserver:动态路由
authserver:登录信息资源服务&鉴权服务
3.1
Nginx要对微服务框架进行配置,所有信息访问流统一往网关访问,通过网关进行反向代理
#定义一个upstream块
upstream nginxtest{
server 127.0.0.1:1112;#指向网关端口
}
server {
listen 8080;
#设置方向代理拦截的路径
location ~ ^/api/? {
proxy_pass http://nginxtest;
}
location ~ ^/oauthserver/? {
proxy_pass http://nginxtest;
}
}
3.2 zuulserver配置
服务配置
security:
oauth2:
client:
#配置自动访问相关路径
clientId: demoApp
clientSecret: demoAppSecret
accessTokenUri: http://localcloudoauth:8080/oauthserver/oauth/token
userAuthorizationUri: http://localcloudoauth:8080/oauthserver/oauth/authorize
preEstablishedRedirectUri: http://localcloudoauth:8080/api/sso #oauth2restemplate发现没有access的时候需要跳转的地方
useCurrentUri: false
resource:
#获取已登录权限的配置
id: resourece
loadBalanced: true
userInfoUri: http://oauthserver/oauthserver/userinfo #获取用户信息的resttemplate地址 注:因此服务名需要使用对应服务的applicationname
preferTokenInfo: false
EnableOAuth2Sso虽然能帮助我们注入SSO服务器的相关配置,但是要实现我们这篇文章的内容我们需要定制自己的配置,我们要对所有/api/**的接口进行鉴权,并且追加我们订制的过滤器。
/**
* 定制我们自己的oauth2客户端拦截器器
* 我们只对/api/**的访问进行鉴权
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.addFilterAfter(createoauth2ClientAuthenticationProcessingFilter(),
AbstractPreAuthenticatedProcessingFilter.class);
http.authorizeRequests().antMatchers("/api/**").
access("isAuthenticated()");
}
自定义OAuth2ClientAuthenticationProcessingFilter通过配置/api/sso这个api接口名来识别并拦截请求,并且建立实际拉取用户信息的UserInfoTokenServices,当遇到來至/api/sso的请求的时候去获取认证信息,并且把认证信息设置到SecurityContext当中
/**
* 避免个性化配置初始BEAN循环
* @return
*/
@Bean
OAuth2ClientAuthenticationProcessingFilter createoauth2ClientAuthenticationProcessingFilter() {
OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(
"/api/sso");
UserInfoTokenServices userInfoTokenServices = new UserInfoTokenServices(resourceServerProperties.getUserInfoUri(),
clientId);
userInfoTokenServices.setRestTemplate(oauth2RestTemplate);
filter.setAuthenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler("http://localcloudoauth:8080/oauthserver/helloworld"));
filter.setTokenServices(userInfoTokenServices);
filter.setRestTemplate(oauth2RestTemplate);
return filter;
}
3.3 oauthserver配置
oauthserver在配置成为认证服务器的同时,同时设置该服务为资源服务器,认证后的用户信息保存在该服务上,虽然使用了内存进行信息的保存,但是因为在在同一个服务中,所以能读取到保存在内存的人员信息。
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private InMemoryTokenStore inMemoryTokenStore;
@Override
public void configure(HttpSecurity http) throws Exception {
http.antMatcher("/userinfo").authorizeRequests().anyRequest().authenticated();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(inMemoryTokenStore);
resources.resourceId("resourece").tokenServices(defaultTokenServices);
}
}
设置资源服务自己的cookiename避免不同服务之间的session使用同一名称导致的某个服务的session失效
@Bean
public CookieSerializer httpSessionIdResolver() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setCookiePath("/oauthserver");
cookieSerializer.setCookieName("resourcesession");
return cookieSerializer;
}
4.展示
因为在配置中开放了home的访问,所以能直接得到结果
当访问http://localcloudoauth:8080/oauthserver/helloworld的时候因为oauthserver服务只开放了home和loginpage的url访问开放因此会自动转向到登录页面
让网关进行sso登录后,我们通过网关访问oauthserver/helloworld
5.源码分析
5.1 AuthorizationEndpoint,如何使sso过程不需要进行人手认可
在命令牌模式当中,需要通过用户手动认可数据,作为SSO登录,只要输入用户名和密码后,SSO服务器就能闭环完成整个命令牌模式的登录,在源码中可知是否对用户提供认证页面通过以下代码来做判断,其中responseTypes
是必定是code的,所以我们只能通过如何修改authorizationRequest.isApproved()来实现自动认证
AuthorizationEndpoint
if (authorizationRequest.isApproved()) //是否需要认证
{
if (responseTypes.contains("token")) {
return getImplicitGrantResponse(authorizationRequest);
}
if (responseTypes.contains("code")) {
return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
(Authentication) principal));
}
}
其中,通过注释我们可知一些系统允许默认已认证,可以通过请求来设置approval的值来自动认可请求
// Some systems may allow for approval decisions to be remembered or approved by default. Check for
// such logic here, and set the approved flag on the authorization request accordingly.
authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
(Authentication) principal);
从代码可知当请求requestedScopes中的scope的和认证服务器中的AutoApprove存储的scope对应的时候就认为该次请求已认可不需要手动认可
ApprovalStoreUserApprovalHandler
ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
for (String scope : requestedScopes) {
if (client.isAutoApprove(scope)) {
approvedScopes.add(scope);
}
}
if (approvedScopes.containsAll(requestedScopes)) {
// gh-877 - if all scopes are auto approved, approvals still need to be added to the approval store.
Set<Approval> approvals = new HashSet<Approval>();
Date expiry = computeExpiry();
for (String approvedScope : approvedScopes) {
approvals.add(new Approval(userAuthentication.getName(), authorizationRequest.getClientId(),
approvedScope, expiry, ApprovalStatus.APPROVED));
}
approvalStore.addApprovals(approvals);
authorizationRequest.setApproved(true);
return authorizationRequest;
}
因此在设置认证服务器的时候需要多给autoApprove添加合适的scope,.autoApprove("all")
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()//数据存在内存中
.withClient("demoApp")//授权服务器id
.secret("demoAppSecret")//授权密码
.authorizedGrantTypes("authorization_code", "password", "refresh_token")//获取模式
.scopes("all")
.resourceIds("rest_api")//资源服务器id
.accessTokenValiditySeconds(1200)//token的存在时间
.autoApprove("all")
.refreshTokenValiditySeconds(50000);//刷新token的token的存在时间
}
5.1 OAuth2ClientAuthenticationProcessingFilter,如何完成命令牌模式
当过滤器发现请求是我们定义的路径之后,就会进行拦截并且往下调用。
OAuth2ClientAuthenticationProcessingFilter
protected boolean requiresAuthentication(HttpServletRequest request,
HttpServletResponse response) {
return requiresAuthenticationRequestMatcher.matches(request);
}
若果之前没有获取acesstoken的话,会抛出UserRedirectRequiredException,并且被OAuth2ClientContextFilter捕获,当发现抛出异常是UserRedirectRequiredException时会进行跳转。
OAuth2RestTemplate
public OAuth2AccessToken getAccessToken() throws UserRedirectRequiredException {
OAuth2AccessToken accessToken = context.getAccessToken();
if (accessToken == null || accessToken.isExpired()) {
try {
accessToken = acquireAccessToken(context);
}
catch (UserRedirectRequiredException e) {
context.setAccessToken(null); // No point hanging onto it now
accessToken = null;
String stateKey = e.getStateKey();
if (stateKey != null) {
Object stateToPreserve = e.getStateToPreserve();
if (stateToPreserve == null) {
stateToPreserve = "NONE";
}
context.setPreservedState(stateKey, stateToPreserve);
}
throw e;
}
}
return accessToken;
}
OAuth2ClientContextFilter
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
UserRedirectRequiredException redirect = (UserRedirectRequiredException) throwableAnalyzer
.getFirstThrowableOfType(
UserRedirectRequiredException.class, causeChain);
if (redirect != null) {
redirectUser(redirect, request, response);
} else {
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
throw new NestedServletException("Unhandled exception", ex);
}
当通过/oauth/authorize访问后,再次访问/api/sso的时候就能通过OAuth2RestTemplate访问/oauth/token获取到acesstoken,最后访问security.resource.userInfoUri获取到用户信息形成,形成授权请求。
结语
值得注意的是,本次我使用的是inMemory方式来保存用户信息和token,所以资源服务器和认证服务器必须在同一个服务中,因此当前实现只有一个资源服务器提供资源,可以通过使用其他存储形式来真正实现多个服务下Spring Security的鉴权控制。
参考: 源码分析@EnableOAuth2Sso在Spring Security OAuth2 SSO单点登录场景下的作用
引用: 理解OAuth 2.0
github:https://github.com/tale2009/spring-security-learning/tree/master/cloudsecurity