• HZERO微服务平台06: 代码分析之token生成、校验、获取信息、传递


    概述

    功能介绍: 认证服务

    oauth2是开放的标准协议, spring security oauth提供了实现, 授权中心(authorization server)用@EnableAuthorizationServer及相关配置实现, 资源服务(resource server)用@EnableResourceServer及相关配置实现;
    授权中心提供授权(/oauth/authorize)、获取token(/oauth/token)等接口, 资源服务实现对token的校验、信息提取; hzero的oauth服务即是授权中心也是资源服务;

    资料:
    OAuth2.0的RFC文档: RFC 6749 - The OAuth 2.0 Authorization Framework

    spring官方开发文档: OAuth 2 Developers Guide

    调用过程阅读方法: 如何以纯文本方式简单快速记录java代码的调用过程

    从oauth服务获取token过程

    调试技巧: 在最内层的方法上打断点, 看调用堆栈;

    post /oauth/token
    TokenEndpoint#postAccessToken
    OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);  //责任链模式, 每种授权模式对应一个granter
    AbstractTokenGranter#grant
    ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
    AbstractTokenGranter#getAccessToken
    return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
        AbstractTokenGranter#getOAuth2Authentication //这个方法会被子类granter覆写
        return new OAuth2Authentication(storedOAuth2Request, null); 
    DefaultTokenServices#createAccessToken(OAuth2Authentication)  //hzero修改版
    DefaultTokenServices#createAccessToken(OAuth2Authentication , OAuth2RefreshToken) //hzero修改版
    return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
    

    "hzero修改版": hzero直接把spring的某些代码保留包名、类名复制到了项目里, 相当于直接替换了源码, 一种不太好的hack方法;
    AbstractTokenGranter的子类AuthorizationCodeTokenGranter、ImplicitTokenGranter、ClientCredentialsTokenGranter、ResourceOwnerPasswordTokenGranter分别对应四种授权模式, 可以增加新的Granter, 优雅的实现新的认证方式.

    oauth服务校验token的过程

    OAuth2AuthenticationProcessingFilter#doFilter
    Authentication authResult = authenticationManager.authenticate(authentication);
    OAuth2AuthenticationManager#authenticate
    OAuth2Authentication auth = tokenServices.loadAuthentication(token);
    DefaultTokenServices#loadAuthentication
    OAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue);
    CustomRedisTokenStore#readAccessToken //从redis里读取、反序列化
    

    如果快过期, 自动延长有效时间;

    DefaultTokenServices#loadAuthentication

    //如果快过期, 自动增加有效时间;
    if (accessToken.getExpiresIn() < 3600) {
        Long deltaMs = 4 * 3600 * 1000L; //4小时, 单位是毫秒;
        ((DefaultOAuth2AccessToken) accessToken).setExpiration(new Date(System.currentTimeMillis() + deltaMs));
        tokenStore.storeAccessToken(accessToken, result);
    }
    

    服务间token的传递过程

    前端使用oauth2流程获取token, 之后的请求必须携带token, token在传递示意图:

    1. 前端获取的uuid格式的token(相当于sessionId), 传递给网关;
    2. 网关使用uuid token获取用户信息, 把用户信息转换jwt token, 并添加到jwt_tokenheader里, 传递到后端服务; 如果获取用户信息失败, 直接返回401(认证失败);
    3. 后端服务从jwt_token里解析、获取用户信息;

    gateway获取用户信息(principal)的过程

    重点:

    • gateway把uuid转换为jwt是在AddJwtFilter
    • 用户信息最终是oauth服务从CustomRedisTokenStore里读取的;

    gateway服务:
    从gateway调用非public的任意接口时:

    GetUserDetailsFilter#run
    CustomUserDetailsWithResult result = this.getUserDetailsService.getUserDetails(accessToken);
    GetUserDetailsServiceImpl#getUserDetails //调用oauth服务的/oauth/api/user
    

    注意: oauth/api/user接口是within接口, 直接从网关调用会报错: error.permission.withinForbidden

    <oauth><status>PERMISSION_WITH_IN</status><code>error.permission.withinForbidden</code><message>No access to within interface</message></oauth>
    

    oauth服务:

    // oauth/api/user
    OauthController#user
    return principal;
    

    principal来自SecurityContext, SecurityContext来自OAuth2AuthenticationProcessingFilter:

    OAuth2AuthenticationProcessingFilter#doFilter
    Authentication authResult = authenticationManager.authenticate(authentication);
        OAuth2AuthenticationManager#authenticate
        OAuth2Authentication auth = tokenServices.loadAuthentication(token);
    SecurityContextHolder.getContext().setAuthentication(authResult);
    

    关于additionInfo字段:

    • DefaultTokenServices#loadAuthentication的返回结果包含additionInfo, 但序列化的之后不包含, 因为spring添加了ignore注解;
    • principal序列化把additionInfo字段里信息, 放到了和client_id同级的位置;

    oauth服务创建用户信息(principal)的过程

    principal来自Object SecurityContext.getAuthentication().getPrincipal(), Object具体是什么类型需要看AuthenticationToken设置了什么值;

    client_credentials模式

    principal是CustomClientDetails类型:

    ...
    ClientCredentialsTokenGranter#grant
    AbstractTokenGranter#grant
    ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
        CustomClientDetailsService#loadClientByClientId
        clientDetailsWrapper.warp(clientDetails, client.getId(), client.getOrganizationId());  //角色、租户等信息来自这里
    return getAccessToken(client, tokenRequest);
    AbstractTokenGranter#getAccessToken
    return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
    ClientCredentialsTokenGranter#getOAuth2Authentication //hzero修改版
    return new ClientOAuth2Authentication(storedOAuth2Request, new ClientAuthenticationToken(client)); //new ClientAuthenticationToken(client)的入参client是principal, 是CustomClientDetails
    

    password模式

    principal是CustomUserDetails类型:

    ...
    ResourceOwnerPasswordTokenGranter#getOAuth2Authentication
    userAuth = authenticationManager.authenticate(userAuth);
        ProviderManager#authenticate
        AbstractUserDetailsAuthenticationProvider#authenticate
        user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
            CustomAuthenticationProvider#retrieveUser
            return getUserDetailsService().loadUserByUsername(username);
            CustomUserDetailsService#loadUserByUsername
        return createSuccessAuthentication(principalToReturn, authentication, user);
    return new OAuth2Authentication(storedOAuth2Request, userAuth);
    

    业务服务从jwt_token获取用户信息的过程

    调试思路: 给JwtTokenExtractor打断点, 看调用堆栈;

    业务服务里hzero没有用spring oauth的@EnableResourceServer, 自定义了JwtTokenFilter, 相当于OAuth2AuthenticationProcessingFilter的功能:

    JwtTokenFilter#doFilter
    Authentication authentication = this.tokenExtractor.extract(httpRequest);
    Authentication authResult = this.authenticate(authentication);
        JwtTokenFilter#authenticate
        this.tokenServices.loadAuthentication(token); 
    SecurityContextHolder.getContext().setAuthentication(authResult);
    

    使用方法: 封装好的方法:
    DetailsHelper.getUserDetails()

  • 相关阅读:
    HTML5之Canvas影片广场
    iOS英语—》中国本土化,如调用专辑,摄像头的变化“cancel”,“photos”至“撤消”,“摄像头”
    约瑟夫问题解决
    2014在百度之星资格赛的第二个问题Disk Schedule
    2015第30周二
    2015第30周一
    2015第29周日
    2015第29周六Spring
    2015第29周五AOP
    2015第29周四
  • 原文地址:https://www.cnblogs.com/QIAOXINGXING001/p/15571803.html
Copyright © 2020-2023  润新知