认证鉴权与API权限控制在微服务架构中的设计与实现(二)


引言: 本文系《认证鉴权与API权限控制在微服务架构中的设计与实现》系列的第二篇,本文重点讲解用户身份的认证与token发放的具体实现。

1. 系统概览

在上一篇《认证鉴权与API权限控制在微服务架构中的设计与实现(一)》介绍了该项目的背景以及技术调研与最后选型,并且对于最终实现的Endpoint执行结果进行展示。对系统架构虽然有提到,但是并未列出详细流程图。在笔者的应用场景中,Auth系统与网关进行结合。在网关出配置相应的端点信息,如登录系统申请token授权,校验check_token等端点。

下图为网关与Auth系统结合的流程图,网关系统的具体实现细节在后面另写文章介绍。(此处流程图的绘制中,笔者使用极简的语言描述,各位同学轻喷!)
01.png

授权流程图

上图展示了系统登录的简单流程,其中的细节有省略,用户信息的合法性校验实际是调用用户系统。大体流程是这样,客户端请求到达网关之后,根据网关识别的请求登录端点,转发到Auth系统,将用户的信息进行校验。

另一方面是对于一般请求的校验。一些不需要权限的公开接口,在网关处配置好,请求到达网关后,匹配了路径将会直接放行。如果需要对该请求进行校验,会将该请求的相关验证信息截取,以及API权限校验所需的上下文信息(笔者项目对于一些操作进行权限前置验证,下一盘文章会讲到),调用Auth系统,校验成功后进行路由转发。
02.jpg

身份及API权限校验的流程图

这篇文章就重点讲解我们在第一篇文章中提到的用户身份的认证与token发放。这个也主要包含两个方面:
  • 用户合法性的认证
  • 获取到授权的token


2. 配置与类图

2.1 AuthorizationServer主要配置

关于AuthorizationServer和ResourceServer的配置在上一篇文章已经列出。AuthorizationServer主要是继承了AuthorizationServerConfigurerAdapter,覆写了其实现接口的三个方法:
//对应于配置AuthorizationServer安全认证的相关信息,创建ClientCredentialsTokenEndpointFilter核心过滤器
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { 
}
//配置OAuth2的客户端相关信息
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
}
//配置身份认证器,配置认证方式,TokenStore,TokenGranter,OAuth2RequestFactory
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {


2.2 主要Authentication类的类图

03.png

AuthorizationServer UML类图

主要的验证方法authenticate(Authentication authentication)在接口AuthenticationManager中,其实现类有ProviderManager,有上图可以看出ProviderManager又依赖于AuthenticationProvider接口,其定义了一个List<AuthenticationProvider>全局变量。笔者这边实现了该接口的实现类CustomAuthenticationProvider。自定义一个provider,并在GlobalAuthenticationConfigurerAdapter中配置好改自定义的校验provider,覆写configure()方法。
@Configuration
public class AuthenticationManagerConfig extends GlobalAuthenticationConfigurerAdapter {
@Autowired
CustomAuthenticationProvider customAuthenticationProvider;
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(customAuthenticationProvider);//使用自定义的AuthenticationProvider
}


AuthenticationManagerBuilder是用来创建AuthenticationManager,允许自定义提供多种方式的AuthenticationProvider,比如LDAP、基于JDBC等等。

3. 认证与授权token

下面讲解认证与授权token主要的类与接口。

3.1 自定义的验证类CustomAuthenticationProvider

CustomAuthenticationProvider中定义了验证方法的具体实现。其具体实现如下所示。
//主要的自定义验证方法
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
   String username = authentication.getName();
   String password = (String) authentication.getCredentials();
   Map data = (Map) authentication.getDetails();
   String clientId = (String) data.get("client");
   Assert.hasText(clientId,"clientId must have value" );
   String type = (String) data.get("type");
   //通过调用user服务,校验用户信息
   Map map = userClient.checkUsernameAndPassword(getUserServicePostObject(username, password, type));
    //校验返回的信息,不正确则抛出异常,授权失败
   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, clientId);
   return new CustomAuthenticationToken(customUserDetails);
}
//构造一个CustomUserDetails,简单,略去
private CustomUserDetails buildCustomUserDetails(String username, String password, String userId, String clientId) {
}
//构造一个请求userService的map,内容略
private Map<String, String> getUserServicePostObject(String username, String password, String type) {


authenticate()最后返回构造的自定义CustomAuthenticationToken,在CustomAuthenticationToken中,将boolean authenticated设为true,user信息验证成功。这边传入的参数CustomUserDetails与token生成有关,作为payload中的信息,下面会讲到。
//继承抽象类AbstractAuthenticationToken
public class CustomAuthenticationToken extends AbstractAuthenticationToken {
private CustomUserDetails userDetails;
public CustomAuthenticationToken(CustomUserDetails userDetails) {
    super(null);
    this.userDetails = userDetails;
    super.setAuthenticated(true);
}
...


而AbstractAuthenticationToken实现了接口Authentication和CredentialsContainer,里面的具体信息读者可以自己看下源码。

3.2 关于JWT

用户信息校验完成之后,下一步则是要对该用户进行授权。在讲具体的授权之前,先补充下关于JWT Token的相关知识点。


Json web token(JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519)。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
从上面的描述可知JWT的定义,这边读者可以对比下token的认证和传统的session认证的区别。推荐一篇文章《什么是 JWT – JSON WEB TOKEN》,笔者这边就不详细扩展讲了,只是简单介绍下其构成。

JWT包含三部分:header头部、payload信息、signature签名。下面以上一篇生成好的access_token为例介绍。

header

JWT的头部承载两部分信息,一是声明类型,这里是JWT;二是声明加密的算法 通常直接使用HMAC SHA256。第一部分一般固定为:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

playload

存放的有效信息,这些有效信息包含三个部分、标准中注册的声明、公共的声明、私有的声明。这边笔者额外添加的信息为X-KEETS-UserId和X-KEETS-ClientId。读者可根据实际项目需要进行定制。最后playload经过base64编码后的结果为:
eyJYLUtFRVRTLVVzZXJJZCI6ImQ2NDQ4YzI0LTNjNGMtNGI4MC04MzcyLWMyZDYxODY4ZjhjNiIsImV4cCI6MTUwODQ0Nzc1NiwidXNlcl9uYW1lIjoia2VldHMiLCJqdGkiOiJiYWQ3MmIxOS1kOWYzLTQ5MDItYWZmYS0wNDMwZTdkYjc5ZWQiLCJjbGllbnRfaWQiOiJmcm9udGVuZCIsInNjb3BlIjpbImFsbCJdfQ

signature

JWT的第三部分是一个签证信息,这个签证信息由三部分组成:header(base64后的)、payload(base64后的)、secret。

关于secret,细心的读者可能会发现之前的配置里面有具体设置。前两部分连接组成的字符串,通过header中声明的加密方式进行加盐secret组合加密,然后就构成了JWT的第三部分。第三部分结果为:
5ZNVN8TLavgpWy8KZQKArcbj7ItJLLaY1zBRaAgMjdo

至于具体应用方法,可以参见第一篇文章中构建的/logout端点。

3.3 自定义的AuthorizationTokenServices

现在到了为用户创建token,这边主要与自定义的接口AuthorizationServerTokenServices有关。AuthorizationServerTokenServices主要有如下三个方法:
//创建token
OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;
//刷新token
OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest)
    throws AuthenticationException;
//获取token
OAuth2AccessToken getAccessToken(OAuth2Authentication authentication); 

由于篇幅限制,笔者这边仅对createAccessToken()的实现方法进行分析,其他的方法实现,读者可以下关注笔者的GitHub项目。
public class CustomAuthorizationTokenServices implements AuthorizationServerTokenServices, ConsumerTokenServices {
...

public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
    //通过TokenStore,获取现存的AccessToken
    OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
    OAuth2RefreshToken refreshToken;
    //移除已有的AccessToken和refreshToken
    if (existingAccessToken != null) {
        if (existingAccessToken.getRefreshToken() != null) {
            refreshToken = existingAccessToken.getRefreshToken();
            // The token store could remove the refresh token when the
                // access token is removed, but we want to be sure
            tokenStore.removeRefreshToken(refreshToken);
        }
        tokenStore.removeAccessToken(existingAccessToken);
    }
    //recreate a refreshToken
    refreshToken = createRefreshToken(authentication);
    OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
    if (accessToken != null) {
        tokenStore.storeAccessToken(accessToken, authentication);
    }
    refreshToken = accessToken.getRefreshToken();
    if (refreshToken != null) {
        tokenStore.storeRefreshToken(refreshToken, authentication);
    }
    return accessToken;
}
...


这边具体的实现在上面有注释,基本没有改写多少,读者此处可以参阅源码。createAccessToken()还调用了两个私有方法,分别创建accessToken和refreshToken。创建accessToken,需要基于refreshToken。

此处可以自定义设置token的时效长度,accessToken创建实现如下:
private int refreshTokenValiditySeconds = 60 * 60 * 24 * 30; // default 30 days.
private int accessTokenValiditySeconds = 60 * 60 * 12; // default 12 hours.

private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
//对应tokenId,存储的标识
  DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
  int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
  if (validitySeconds > 0) {
      token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
  }
  token.setRefreshToken(refreshToken);
  //scope对应作用范围
  token.setScope(authentication.getOAuth2Request().getScope());
//上一节介绍的自定义TokenEnhancer,这边使用
  return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;


既然提到TokenEnhancer,这边简单贴一下代码。
public class CustomTokenEnhancer extends JwtAccessTokenConverter {
private static final String TOKEN_SEG_USER_ID = "X-KEETS-UserId";
private static final String TOKEN_SEG_CLIENT = "X-KEETS-ClientId";
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken,
                                 OAuth2Authentication authentication) {
    CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
    Map<String, Object> info = new HashMap<>();
    //从自定义的userDetails中取出UserId
    info.put(TOKEN_SEG_USER_ID, userDetails.getUserId());
    DefaultOAuth2AccessToken customAccessToken = new DefaultOAuth2AccessToken(accessToken);
    customAccessToken.setAdditionalInformation(info);
    OAuth2AccessToken enhancedToken = super.enhance(customAccessToken, authentication);
    //设置ClientId
    enhancedToken.getAdditionalInformation().put(TOKEN_SEG_CLIENT, userDetails.getClientId());
    return enhancedToken;
}


自此,用户身份校验与发放授权token结束。最终成功返回的结果为:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJYLUtFRVRTLVVzZXJJZCI6ImQ2NDQ4YzI0LTNjNGMtNGI4MC04MzcyLWMyZDYxODY4ZjhjNiIsImV4cCI6MTUwODQ0Nzc1NiwidXNlcl9uYW1lIjoia2VldHMiLCJqdGkiOiJiYWQ3MmIxOS1kOWYzLTQ5MDItYWZmYS0wNDMwZTdkYjc5ZWQiLCJjbGllbnRfaWQiOiJmcm9udGVuZCIsInNjb3BlIjpbImFsbCJdfQ.5ZNVN8TLavgpWy8KZQKArcbj7ItJLLaY1zBRaAgMjdo",   
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJYLUtFRVRTLVVzZXJJZCI6ImQ2NDQ4YzI0LTNjNGMtNGI4MC04MzcyLWMyZDYxODY4ZjhjNiIsInVzZXJfbmFtZSI6ImtlZXRzIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6ImJhZDcyYjE5LWQ5ZjMtNDkwMi1hZmZhLTA0MzBlN2RiNzllZCIsImV4cCI6MTUxMDk5NjU1NiwianRpIjoiYWE0MWY1MjctODE3YS00N2UyLWFhOTgtZjNlMDZmNmY0NTZlIiwiY2xpZW50X2lkIjoiZnJvbnRlbmQifQ.mICT1-lxOAqOU9M-Ud7wZBb4tTux6OQWouQJ2nn1DeE",
"expires_in": 43195,
"scope": "all",
"X-KEETS-UserId": "d6448c24-3c4c-4b80-8372-c2d61868f8c6",
"jti": "bad72b19-d9f3-4902-affa-0430e7db79ed",
"X-KEETS-ClientId": "frontend"


4. 总结

本文开头给出了Auth系统概述,画出了简要的登录和校验的流程图,方便读者能对系统的实现有个大概的了解。然后主要讲解了用户身份的认证与token发放的具体实现。对于其中主要的类和接口进行了分析与讲解。下一篇文章主要讲解token的鉴定和API级别的上下文权限校验。

本文的源码地址:


参考



原文链接:http://blueskykong.com/2017/10/22/security2/

0 个评论

要回复文章请先登录注册