OAuth2.0


本篇主要记录和总结工作中用到授权认证的OAuth2.0
如果有人再问你OAuth2.0,你就把这篇文章扔给他

流程原理

使用授权码模式完成OAuth2.0授权的过程需要以下四个步骤:

  • client请求授权服务端,获取Authorization Code;
  • client通过Authorization Code再次请求授权服务端,获取Access Token和Refresh Token;
  • client通过Refresh Token刷新获取Access Token;
  • client通过服务端返回的Access Token调取服务端接口。

数据库的表结构设计

t_partner_interfaces

用于记录后台分配给用户的权限

CREATE TABLE `t_partner_interfaces` (
  `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT,
  `partner_no` varchar(20) NOT NULL DEFAULT '' COMMENT '合作伙伴编号',
  `interfaces` varchar(255) NOT NULL DEFAULT '' COMMENT '接口标志(多个用,分隔)',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='合作伙伴接口表';

t_auth_info

用于记录用户的clientId、clientSecret、redirectUri等

CREATE TABLE `t_auth_info` (
  `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT,
  `partner_no` varchar(20) NOT NULL DEFAULT '' COMMENT '合作伙伴编号',
  `client_id` varchar(64) NOT NULL DEFAULT '' COMMENT '唯一的clientId',
  `client_secret` varchar(64) NOT NULL DEFAULT '' COMMENT '密钥',
  `redirect_uri` varchar(255) NOT NULL DEFAULT '' COMMENT '回调URI(多个用,分隔)',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='授权信息表';

t_auth_token

用户记录授权完成后的授权信息

CREATE TABLE `t_auth_token` (
  `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT,
  `partner_no` varchar(20) NOT NULL DEFAULT '' COMMENT '合作伙伴编号',
  `access_token` varchar(128) NOT NULL DEFAULT '' COMMENT '唯一的Access Token',
  `refresh_token` varchar(128) NOT NULL DEFAULT '' COMMENT '唯一的Refresh Token',
  `access_expires_in` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'accessToken过期时间',
  `refresh_expires_in` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'refreshToken过期时间',
  `scope` varchar(255) NOT NULL DEFAULT '' COMMENT 'accessToken可访问范围',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='令牌表';

授权服务端主要接口的代码实现

获取code接口

@ApiOperation( "授权获取code")
@GetMapping("/authorize")
public ResultData authorize(HttpServletRequest request) {
    try {
        oAuthService.auth(request);
    } catch (IllegalArgumentException | IllegalStateException e) {
        return ResultData.fail(e.getMessage());
    } catch (Exception e) {
        LOGGER.error("Exception 请求授权接口异常", e);
        return ResultData.fail("系统繁忙,请稍候重试");
    }
    return ResultData.succeed();
}
public void auth(HttpServletRequest request) {
    oAuthDomain.auth(request);
}
@Component
@Scope(DefaultListableBeanFactory.SCOPE_PROTOTYPE)
public class OAuthDomain {
    public void auth(HttpServletRequest request) {
        // 客户端ID
        String clientId = request.getParameter("client_id");
        // 权限范围
        String scope = request.getParameter("scope");
        // 回调URL
        String redirectUri = request.getParameter("redirect_uri");
        // state,用于防止CSRF攻击(非必填)
        String state = request.getParameter("state");

        Assert.hasText(clientId, ErrorCodeEnum.INVALID_REQUEST.getErrorDescription());
        Assert.hasText(scope, ErrorCodeEnum.INVALID_REQUEST.getErrorDescription());
        Assert.hasText(redirectUri, ErrorCodeEnum.INVALID_REQUEST.getErrorDescription());

        // 1 校验clientId
        AuthInfoDO authInfoDO = authInfoDAO.getByClientId(clientId);
        Assert.notNull(authInfoDO, ErrorCodeEnum.INVALID_CLIENT.getErrorDescription());

        // 2 校验scope范围
        String partnerNo = authInfoDO.getPartnerNo();
        // 2.1 TODO 校验系统的scope范围
        // 2.2 校验用户的scope范围
        PartnerDO partnerDO = partnerDAO.getByPartnerNo(partnerNo);
        Assert.state(PartnerStatusEnum.OPEN.getStatus().equals(partnerDO.getStatus()), ErrorCodeEnum.INVALID_SCOPE.getErrorDescription());
        PartnerInterfacesDO partnerInterfacesDO = partnerInterfacesDAO.getByPartnerNo(partnerNo);
        Assert.notNull(partnerInterfacesDO, ErrorCodeEnum.INVALID_SCOPE.getErrorDescription());
        String interfaces = partnerInterfacesDO.getInterfaces();
        List interfacesList = Arrays.asList(interfaces.split(","));
        List scopeList = Arrays.asList(scope.split(","));
        Assert.state(interfacesList.containsAll(scopeList), ErrorCodeEnum.INVALID_SCOPE.getErrorDescription());

        // 3 校验redirectUri是否存在
        List redirectUriList = Arrays.asList(authInfoDO.getRedirectUri().split(","));
        Assert.state(redirectUriList.contains(redirectUri), ErrorCodeEnum.INVALID_REDIRECT_URI.getErrorDescription());

        // 4 生成Authorization Code
        String code = createAuthorizationCode(clientId, scope);
        LOGGER.info("客户端ID:" + clientId + "生成的code:" + code);

        // 5 保存Authorization Code, scope到redis
        redisTemplate.opsForValue().set("code:" + partnerNo, code, ExpireEnum.AUTHORIZATION_CODE.getTime(), ExpireEnum.AUTHORIZATION_CODE.getTimeUnit());
        redisTemplate.opsForValue().set("scope:" + partnerNo, scope);

        // 6 设置返回参数
        String params = "?code=" + code;
        params = StringUtils.isNotBlank(state) ? params.concat("&state=").concat(state) : params;

        String uri = redirectUri.concat(params);

        // 7 开子线程,请求重定向地址
        ObjectU.execute(() -> {
            request(uri);
        });
    }
}

code获取Access Token

@ApiOperation( "code获取token")
@GetMapping(value = "/token")
public ResultData<Map<String,Object>> token(HttpServletRequest request){
    try {
        return ResultData.succeed(oAuthService.token(request));
    } catch (IllegalArgumentException | IllegalStateException e) {
        return ResultData.fail(e.getMessage());
    } catch (Exception e) {
        LOGGER.error("Exception 请求授权接口异常", e);
        return ResultData.fail("系统繁忙,请稍候重试");
    }
}
public Map<String,Object> token(HttpServletRequest request) {
    // 1 返回token
    Map<String, Object> result = oAuthDomain.token(request);
    // 2 保存token相关信息
    String clientId = request.getParameter("client_id");
    AuthInfoDO authInfoDO = authInfoDAO.getByClientId(clientId);
    AuthTokenDO authTokenDO = createAuthTokenDO(result, authInfoDO.getPartnerNo(), TokenEnum.ACCESS.getType());
    AuthToken authToken = authTokenFactory.get(authTokenDO);
    authToken.saveOrUpdate();
    return result;
}
public Map<String,Object> token(HttpServletRequest request) {
    Map<String,Object> result = new HashMap<>(8);

    // 授权方式
    String grantType = request.getParameter("grant_type");
    // 前面获取的Authorization Code
    String code = request.getParameter("code");
    // 客户端ID
    String clientId = request.getParameter("client_id");
    // 接入的客户端的密钥
    String clientSecret = request.getParameter("client_secret");

    Assert.hasText(grantType, ErrorCodeEnum.INVALID_REQUEST.getErrorDescription());
    Assert.hasText(code, ErrorCodeEnum.INVALID_REQUEST.getErrorDescription());
    Assert.hasText(clientId, ErrorCodeEnum.INVALID_REQUEST.getErrorDescription());
    Assert.hasText(clientSecret, ErrorCodeEnum.INVALID_REQUEST.getErrorDescription());

    // 1 校验授权方式
    Assert.state(GrantTypeEnum.AUTHORIZATION_CODE.getType().equals(grantType), ErrorCodeEnum.UNSUPPORTED_GRANT_TYPE.getErrorDescription());

    // 2 校验clientId
    AuthInfoDO authInfoDO = authInfoDAO.getByClientId(clientId);
    Assert.notNull(authInfoDO, ErrorCodeEnum.INVALID_CLIENT.getErrorDescription());

    // 3 校验clientSecret
    String localClientSecret = authInfoDO.getClientSecret();
    Assert.state(clientSecret.equals(localClientSecret), ErrorCodeEnum.INVALID_CLIENT.getErrorDescription());

    // 4 校验code
    String partnerNo = authInfoDO.getPartnerNo();
    String authCode = redisTemplate.opsForValue().get("code:" + partnerNo);
    Assert.hasText(authCode, ErrorCodeEnum.INVALID_GRANT.getErrorDescription());
    Assert.state(code.equals(authCode), ErrorCodeEnum.INVALID_GRANT.getErrorDescription());

    // 5 生成accessToken
    String accessToken = createAccessToken(partnerNo, clientId);

    // 6 生成refreshToken
    String refreshToken = createRefreshToken(partnerNo, accessToken);

    String scope = redisTemplate.opsForValue().get("scope:" + partnerNo);
    // 7 返回数据
    result.put("access_token", accessToken);
    result.put("refresh_token", refreshToken);
    result.put("expires_in", ExpireEnum.ACCESS_TOKEN.getTime() * 3600);
    result.put("scope", scope);
    return result;
}

refreshToken刷新获取accessToken

@ApiOperation( "refreshToken获取accessToken")
@GetMapping(value = "/refreshToken")
public ResultData<Map<String,Object>> refreshToken(HttpServletRequest request){
    try {
        return ResultData.succeed(oAuthService.refreshToken(request));
    } catch (IllegalArgumentException | IllegalStateException e) {
        return ResultData.fail(e.getMessage());
    } catch (Exception e) {
        LOGGER.error("Exception 请求授权接口异常", e);
        return ResultData.fail("系统繁忙,请稍候重试");
    }
}
public Map<String, Object> refreshToken(HttpServletRequest request) {
    // 1 返回token
    Map<String, Object> result = oAuthDomain.refreshToken(request);
    // 2 保存token相关信息
    String refreshToken = request.getParameter("refresh_token");
    AuthTokenDO localAuthTokenDO = authTokenDAO.getByRefreshToken(refreshToken);
    AuthTokenDO authTokenDO = createAuthTokenDO(result, localAuthTokenDO.getPartnerNo(), TokenEnum.REFRESH.getType());
    AuthToken authToken = authTokenFactory.get(authTokenDO);
    authToken.saveOrUpdate();
    return result;
}
public Map<String, Object> refreshToken(HttpServletRequest request) {
    Map<String,Object> result = new HashMap<>(8);

    // 获取refreshToken
    String refreshToken = request.getParameter("refresh_token");

    // 1 校验refreshToken是否真实有效
    Assert.hasText(refreshToken, ErrorCodeEnum.INVALID_REQUEST.getErrorDescription());
    AuthTokenDO authTokenDO = authTokenDAO.getByRefreshToken(refreshToken);
    Assert.notNull(authTokenDO, ErrorCodeEnum.INVALID_GRANT.getErrorDescription());

    // 2 校验refreshToken是否过期
    Timestamp refreshExpiresIn = authTokenDO.getRefreshExpiresIn();
    Assert.state(LocalDateTime.now().isBefore(refreshExpiresIn.toLocalDateTime()), ErrorCodeEnum.EXPIRED_TOKEN.getErrorDescription());

    // 3 生成新的Access Token
    String partnerNo = authTokenDO.getPartnerNo();
    AuthInfoDO authInfoDO = authInfoDAO.getByPartnerNo(partnerNo);
    String newAccessToken = createAccessToken(partnerNo, authInfoDO.getClientId());

    // 4 返回数据
    result.put("access_token", newAccessToken);
    result.put("refresh_token", refreshToken);
    result.put("expires_in", ExpireEnum.ACCESS_TOKEN.getTime() * 3600);
    result.put("scope", authTokenDO.getScope());
    return result;
}

枚举

public enum ErrorCodeEnum {
    INVALID_REQUEST("invalid_request","请求缺少某个必需参数,包含一个不支持的参数或参数值,或者格式不正确。"),
    INVALID_CLIENT("invalid_client","请求的client_id或client_secret参数无效。"),
    INVALID_GRANT("invalid_grant","请求的Authorization Code、Access Token、Refresh Token等信息是无效的。"),
    UNSUPPORTED_GRANT_TYPE("unsupported_grant_type","不支持的grant_type。"),
    INVALID_SCOPE("invalid_scope","请求的scope参数是无效的、未知的、格式不正确的,或所请求的权限范围超过了数据拥有者所授予的权限范围。"),
    EXPIRED_TOKEN("expired_token","请求的Access Token或Refresh Token已过期。"),
    REDIRECT_URI_MISMATCH("redirect_uri_mismatch","请求的redirect_uri所在的域名与开发者注册应用时所填写的域名不匹配。"),
    INVALID_REDIRECT_URI("invalid_redirect_uri","请求的回调URL不在白名单中。"),
    UNKNOWN_ERROR("unknown_error","程序发生未知异常,请联系管理员解决。");

    /**
     * 错误码
     */
    private String error;
    /**
     * 错误描述信息
     */
    private String errorDescription;

    ErrorCodeEnum(String error, String errorDescription) {
        this.error = error;
        this.errorDescription = errorDescription;
    }

    public String getError() {
        return error;
    }

    public String getErrorDescription() {
        return errorDescription;
    }
}
/**
 * 过期时间相关枚举
 * @author CFL
 */
public enum ExpireEnum {
    // Authorization Code的有效期为10分钟
    AUTHORIZATION_CODE(10L, TimeUnit.MINUTES),
    // Access Token的有效期为12小时
    ACCESS_TOKEN(12L, TimeUnit.HOURS),
    // Refresh Token的有效期为24小时
    REFRESH_TOKEN(24L,TimeUnit.HOURS)
    ;

    /**
     * 过期时间
     */
    private Long time;
    /**
     * 时间单位
     */
    private TimeUnit timeUnit;

    ExpireEnum(Long time, TimeUnit timeUnit) {
        this.time = time;
        this.timeUnit = timeUnit;
    }

    public Long getTime() {
        return time;
    }

    public TimeUnit getTimeUnit() {
        return timeUnit;
    }
}
/**
 * 授权方式
 */
public enum GrantTypeEnum {
    // 授权码模式
    AUTHORIZATION_CODE("authorization_code");

    private String type;

    GrantTypeEnum(String type) {
        this.type = type;
    }

    public String getType() {
        return type;
    }
}
public enum TokenEnum {
    ACCESS((byte)0, "accessToken"),
    REFRESH((byte)1, "refreshToken");
    private Byte type;
    private String remark;

    TokenEnum(Byte type, String remark) {
        this.type = type;
        this.remark = remark;
    }

    public Byte getType() {
        return type;
    }

    public String getRemark() {
        return remark;
    }
}

拦截器

@Component
public class AuthAccessTokenInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String accessToken = request.getHeader("accessToken");
        String requestURI = request.getRequestURI();

        if (StringUtils.isNotBlank(accessToken)) {
            AuthTokenDO authTokenDO = authTokenDAO.getByAccessToken(accessToken);
            if (Objects.nonNull(authTokenDO)) {
                String scope = authTokenDO.getScope();
                if (StringUtils.isBlank(scope)) {
                    return generateErrorResponse(response, ErrorCodeEnum.INVALID_SCOPE);
                }
                List<String> scopeList = Arrays.asList(scope.split(","));
                String uri = requestURI.substring(requestURI.lastIndexOf("/") + 1);
                if (!scopeList.contains(uri)) {
                    return generateErrorResponse(response, ErrorCodeEnum.INVALID_SCOPE);
                }
                ApiPermissionDO apiPermissionDO = apiPermissionDAO.getByApiSource(uri);
                if (Objects.isNull(apiPermissionDO) || apiPermissionDO.getStatus() == 0) {
                    return generateErrorResponse(response, ErrorCodeEnum.INVALID_SCOPE);
                }
                PartnerDO partnerDO = partnerDAO.getByPartnerNo(authTokenDO.getPartnerNo());
                if (Objects.isNull(partnerDO) || partnerDO.getStatus() == 1) {
                    return generateErrorResponse(response, ErrorCodeEnum.INVALID_SCOPE);
                }
                LocalDateTime accessExpiresIn = authTokenDO.getAccessExpiresIn().toLocalDateTime();
                request.setAttribute("partner_no", authTokenDO.getPartnerNo());
                return LocalDateTime.now().isBefore(accessExpiresIn) || generateErrorResponse(response, ErrorCodeEnum.EXPIRED_TOKEN);
            } else {
                return generateErrorResponse(response, ErrorCodeEnum.INVALID_GRANT);
            }
        } else {
            return generateErrorResponse(response, ErrorCodeEnum.INVALID_REQUEST);
        }
    }

    private boolean generateErrorResponse(HttpServletResponse response, ErrorCodeEnum errorCodeEnum) throws Exception {
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Content-type", "application/json;charset=UTF-8");
        Map<String,String> result = new HashMap<>(2);
        result.put("statusCode", errorCodeEnum.getError());
        result.put("message",errorCodeEnum.getErrorDescription());

        response.getWriter().write(JSON.toJSON(result).toString());
        return false;
    }
}
public class AuthUtil {
    public static String getPartnerNo() {
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (Objects.isNull(servletRequestAttributes)){
            return null;
        }
        HttpServletRequest request = servletRequestAttributes.getRequest();
        String partnerNo = (String) request.getAttribute("partner_no");
        return partnerNo;
    }
}
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Autowired
    private AuthAccessTokenInterceptor authAccessTokenInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authAccessTokenInterceptor)
                .addPathPatterns("/api/**");
    }
}

文章作者: Adbo
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Adbo !
评论
  目录