认证鉴权与API权限控制在微服务架构中的设计与实现:受权码模式

引言: 以前系列文章《认证鉴权与API权限控制在微服务架构中的设计与实现》,前面文章已经将认证鉴权与API权限控制的流程和主要细节讲解完。因为有些同窗想了解下受权码模式,本文特意补充讲解。html

受权码类型介绍

受权码类型(authorization code)经过重定向的方式让资源全部者直接与受权服务器进行交互来进行受权,避免了资源全部者信息泄漏给客户端,是功能最完整、流程最严密的受权类型,可是须要客户端必须能与资源全部者的代理(一般是Web浏览器)进行交互,和可从受权服务器中接受请求(重定向给予受权码),受权流程以下:java

+----------+
 | Resource |
 |   Owner  |
 |          |
 +----------+
      ^
      |
     (B)
 +----|-----+          Client Identifier      +---------------+
 |         -+----(A)-- & Redirection URI ---->|               |
 |  User-   |                                 | Authorization |
 |  Agent  -+----(B)-- User authenticates --->|     Server    |
 |          |                                 |               |
 |         -+----(C)-- Authorization Code ---<|               |
 +-|----|---+                                 +---------------+
   |    |                                         ^      v
  (A)  (C)                                        |      |
   |    |                                         |      |
   ^    v                                         |      |
 +---------+                                      |      |
 |         |>---(D)-- Authorization Code ---------'      |
 |  Client |          & Redirection URI                  |
 |         |                                             |
 |         |<---(E)----- Access Token -------------------'
 +---------+       (w/ Optional Refresh Token)
复制代码
  1. 客户端引导资源全部者的用户代理到受权服务器的endpoint,通常经过重定向的方式。客户端提交的信息应包含客户端标识(client identifier)、请求范围(requested scope)、本地状态(local state)和用于返回受权码的重定向地址(redirection URI)
  2. 受权服务器认证资源全部者(经过用户代理),并确认资源全部者容许仍是拒绝客户端的访问请求
  3. 若是资源全部者授予客户端访问权限,受权服务器经过重定向用户代理的方式回调客户端提供的重定向地址,并在重定向地址中添加受权码和客户端先前提供的任何本地状态
  4. 客户端携带上一步得到的受权码向受权服务器请求访问令牌。在这一步中受权码和客户端都要被受权服务器进行认证。客户端须要提交用于获取受权码的重定向地址
  5. 受权服务器对客户端进行身份验证,和认证受权码,确保接收到的重定向地址与第三步中用于的获取受权码的重定向地址相匹配。若是有效,返回访问令牌,可能会有刷新令牌(Refresh Token)

快速入门

Spring-Securiy 配置

因为受权码模式须要登陆用户给请求access_token的客户端受权,因此auth-server须要添加Spring-Security的相关配置用于引导用户进行登陆。spring

在原来的基础上,进行Spring-Securiy相关配置,容许用户进行表单登陆:数据库

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    CustomLogoutHandler customLogoutHandler;


    @Override
    protected void configure(HttpSecurity http) throws Exception {


        http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .and()
                .requestMatchers().antMatchers("/**")
                .and().authorizeRequests()
                .antMatchers("/**").permitAll()
                .anyRequest().authenticated()
                .and().formLogin()
                .permitAll()
                .and().logout()
                .logoutUrl("/logout")
                .clearAuthentication(true)
                .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
                .addLogoutHandler(customLogoutHandler);

    }

}

复制代码

同时须要把ResourceServerConfig中的资源服务器中的对于登出端口的处理迁移到WebSecurityConfig中,注释掉ResourceServerConfigHttpSecurity配置:json

public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

// @Override
// public void configure(HttpSecurity http) throws Exception {
// http.csrf().disable()
// .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// .and()
// .requestMatchers().antMatchers("/**")
// .and().authorizeRequests()
// .antMatchers("/**").permitAll()
// .anyRequest().authenticated()
// .and().logout()
// .logoutUrl("/logout")
// .clearAuthentication(true)
// .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
// .addLogoutHandler(customLogoutHandler());
//
// //http.antMatcher("/api/**").addFilterAt(customSecurityFilter(), FilterSecurityInterceptor.class);
//
// }

 /* @Bean public CustomSecurityFilter customSecurityFilter() { return new CustomSecurityFilter(); } */
.....
}

复制代码

AuthenticationProvider

因为用户表单登陆的认证过程可能有所不一样,为此再添加一个CustomSecurityAuthenticationProvider,基本上与CustomAuthenticationProvider一致,只是忽略对client客户端的认证和处理。api

@Component
public class CustomSecurityAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private UserClient userClient;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password;

        Map map;

        password = (String) authentication.getCredentials();
        //若是你是调用user服务,这边不用注掉
        //map = userClient.checkUsernameAndPassword(getUserServicePostObject(username, password, type));
        map = checkUsernameAndPassword(getUserServicePostObject(username, password));


        String userId = (String) map.get("userId");
        if (StringUtils.isBlank(userId)) {
            String errorCode = (String) map.get("code");
            throw new BadCredentialsException(errorCode);
        }
        CustomUserDetails customUserDetails = buildCustomUserDetails(username, password, userId);
        return new CustomAuthenticationToken(customUserDetails);
    }

    private CustomUserDetails buildCustomUserDetails(String username, String password, String userId) {
        CustomUserDetails customUserDetails = new CustomUserDetails.CustomUserDetailsBuilder()
                .withUserId(userId)
                .withPassword(password)
                .withUsername(username)
                .withClientId("for Security")
                .build();
        return customUserDetails;
    }

    private Map<String, String> getUserServicePostObject(String username, String password) {
        Map<String, String> requestParam = new HashMap<String, String>();
        requestParam.put("userName", username);
        requestParam.put("password", password);
        return requestParam;
    }

    //模拟调用user服务的方法
    private Map checkUsernameAndPassword(Map map) {

        //checkUsernameAndPassword
        Map ret = new HashMap();
        ret.put("userId", UUID.randomUUID().toString());

        return ret;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }

}
复制代码

AuthenticationManagerConfig添加CustomSecurityAuthenticationProvider配置:浏览器

@Configuration
public class AuthenticationManagerConfig extends GlobalAuthenticationConfigurerAdapter {

    @Autowired
    CustomAuthenticationProvider customAuthenticationProvider;
    @Autowired
    CustomSecurityAuthenticationProvider securityAuthenticationProvider;

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(customAuthenticationProvider)
                .authenticationProvider(securityAuthenticationProvider);
    }

}
复制代码

保证数据库中的请求客户端存在受权码的请求受权和具有回调地址,回调地址是用来接受受权码的。服务器

测试使用

启动服务,浏览器访问地址http://localhost:9091/oauth/authorize?response_type=code&client_id=frontend& scope=all&redirect_uri=http://localhost:8080微信

重定向到登陆界面,引导用户登陆:session

登陆成功,受权客户端获取受权码。

受权以后,从回调地址中获取到受权码:

http://localhost:8080/?code=7OglOJ
复制代码

携带受权码获取对应的token:

源码详解

AuthorizationServerTokenServices是受权服务器中进行token操做的接口,提供了如下的三个接口:

public interface AuthorizationServerTokenServices {

	// 生成与OAuth2认证绑定的access_token
	OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;

	// 根据refresh_token刷新access_token
	OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest) throws AuthenticationException;

	// 获取OAuth2认证的access_token,若是access_token存在的话
	OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);

}

复制代码

请注意,生成的token都是与受权的用户进行绑定的。

AuthorizationServerTokenServices接口的默认实现是DefaultTokenServices,注意token经过TokenStore进行保存管理。

生成token:

//DefaultTokenServices
@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
	// 从TokenStore获取access_token
	OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
	OAuth2RefreshToken refreshToken = null;
	if (existingAccessToken != null) {
		if (existingAccessToken.isExpired()) {
			// 若是access_token已经存在可是过时了
			// 删除对应的access_token和refresh_token
			if (existingAccessToken.getRefreshToken() != null) {
				refreshToken = existingAccessToken.getRefreshToken();
			  	tokenStore.removeRefreshToken(refreshToken);
			}
			tokenStore.removeAccessToken(existingAccessToken);
		}
		else {
			// 若是access_token已经存在而且没有过时
			// 从新保存一下防止authentication改变,而且返回该access_token
			tokenStore.storeAccessToken(existingAccessToken, authentication);
			return existingAccessToken;
		}
	}

	// 只有当refresh_token为null时,才从新建立一个新的refresh_token
	// 这样可使持有过时access_token的客户端能够根据之前拿到refresh_token拿到从新建立的access_token
	// 由于建立的access_token须要绑定refresh_token
	if (refreshToken == null) {
		refreshToken = createRefreshToken(authentication);
	}else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
	 	// 若是refresh_token也有期限而且过时,从新建立
		ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
		if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
			refreshToken = createRefreshToken(authentication);
		}
	}
	// 绑定受权用户和refresh_token建立新的access_token
	OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
	// 将access_token与受权用户对应保存
	tokenStore.storeAccessToken(accessToken, authentication);
	// In case it was modified
	refreshToken = accessToken.getRefreshToken();
	if (refreshToken != null) {
		// 将refresh_token与受权用户对应保存
		tokenStore.storeRefreshToken(refreshToken, authentication);
	}
	return accessToken;
}
复制代码

须要注意到,在建立token的过程当中,会根据该受权用户去查询是否存在未过时的access_token,有就直接返回,没有的话才会从新建立新的access_token,同时也应该注意到是先建立refresh_token,再去建立access_token,这是为了防止持有过时的access_token可以经过refresh_token从新得到access_token,由于先后建立access_token绑定了同一个refresh_token。

DefaultTokenServices中刷新token的refreshAccessToken()以及获取token的getAccessToken()方法就留给读者们本身去查看,在此不介绍。

小结

本文主要讲了受权码模式,在受权码模式须要用户登陆以后进行受权才获取获取受权码,再携带受权码去向TokenEndpoint请求访问令牌,固然也能够在请求中设置response_token=token经过隐式类型直接获取到access_token。这里须要注意一个问题,在到达AuthorizationEndpoint端点时,并无对客户端进行验证,可是必需要通过用户认证的请求才能被接受。

订阅最新文章,欢迎关注个人公众号

微信公众号

推荐阅读

系列文章:认证鉴权与API权限控制在微服务架构中的设计与实现

参考

spring-security

相关文章
相关标签/搜索