选型

那时的我
面临着两种抉择:

  • Spring Security
  • Shiro + Oltu

起初,由于Oltu已经不进行维护

Oltu Last Published: 2016-07-04

开放开发平台架构设计 开放平台如何设计_开放开发平台架构设计


本帅毅然决然加入了spring全家桶的行列。

但是由于spring-security的高度封装,导致他的拓展性不足。

具体表现在:

  • 数据库字段不能够很灵活的添加删除以及自定义
  • 对接口的高度封装导致不能够很灵活的处理一些业务逻辑

这个时候的我头也不回的扭向了Shiro+Oltu,真香!所有鉴权的逻辑皆以Controller的形式自定义完成,同时数据库字段也完全可以自定义,模块化的设计灵活方便。
当然最终一句话让我下定了决心使用Oltu的方案:

OAuth2.0已经是个很成熟的协议,况且Oltu也年代久远,只要Oauth2.0不变动,Oltu自然也不需要有所更新

代码

建议授权服务器与资源服务器解耦分开部署
下面就来体会下Oltu的简便之处:

Oauth(授权服务器:授权码模式)

1.登陆授权获取code

接口

@ApiOperation(value = "授权(需要用户登陆)",
            nickname = "authorize",
            notes = "仅支持response_type=code类型",
            tags = "Oauth")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "response_type", value = "response_type", required = true, paramType = "query", example = "code"),
            @ApiImplicitParam(name = "client_id", value = "client_id", required = true, paramType = "query", example = "test"),
            @ApiImplicitParam(name = "redirect_uri", value = "redirect_uri", required = true, paramType = "query", example = "https://www.baidu.com/s?wd=123"),
            @ApiImplicitParam(name = "scope", value = "scope", required = true, paramType = "query", example = "all"),
            @ApiImplicitParam(name = "state", value = "state", required = false, paramType = "query", example = "hello")
    })
    @GetMapping("/authorize")
    Object authorize(HttpServletRequest request, HttpServletResponse response, Model model) throws URISyntaxException, OAuthSystemException;

逻辑

@Override
    public Object authorize(HttpServletRequest request, Model model) throws URISyntaxException, OAuthSystemException {
        try {
            //构建 OAuth 授权请求
            OAuthAuthzRequest oauthRequest = new OAuthAuthzRequest(request);
            String clientId = oauthRequest.getClientId();
            String redirectURI = oauthRequest.getRedirectURI();
            //检查传入的客户端 id 是否正确
            ClientDetails clientDetails = oauthManager.getClientDetailByClientId(clientId);
            boolean isExist = clientDetails != null;
            if (!isExist) {
                OAuthResponse response = OAuthASResponse
                        .errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                        .setError(OAuthError.TokenResponse.INVALID_CLIENT)
                        .setErrorDescription(Constants.INVALID_CLIENT_DESCRIPTION)
                        .buildJSONMessage();
                return new ResponseEntity<>(response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
            }
            Subject subject = SecurityUtils.getSubject();
            //检查传入的重定向uri是否正确
            if (!redirectURI.equals(clientDetails.getRedirectUri())) {
                OAuthResponse response = OAuthASResponse
                        .errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                        .setError(OAuthError.TokenResponse.INVALID_CLIENT)
                        .setErrorDescription(Constants.MISMATHC_REDIRECT_URI)
                        .buildJSONMessage();
                return new ResponseEntity<>(response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
            }
            //如果用户没有登录,跳转到登陆页面
            if (!subject.isAuthenticated()) {
                /*if(!login(subject, request)) {//登录失败时跳转到登陆页面
                    model.addAttribute("client",
                            oauthManager.getClientDetailByClientId(oauthRequest.getClientId()));
                    return "oauth2login";
                }*/
            }
            /*String username = (String)subject.getPrincipal();*/
            String username = "写死的";
            //生成授权码
            String authorizationCode = null;
            //responseType 目前仅支持 CODE,另外还有 TOKEN
            String responseType = oauthRequest.getParam(OAuth.OAUTH_RESPONSE_TYPE);
            if (responseType.equals(ResponseType.CODE.toString())) {
                OAuthIssuerImpl oauthIssuerImpl = new OAuthIssuerImpl(new MD5Generator());
                authorizationCode = oauthIssuerImpl.authorizationCode();
                oauthManager.saveOrUpdateOauthCode(clientId, username, authorizationCode);
            }
            //进行 OAuth 响应构建
            OAuthASResponse.OAuthAuthorizationResponseBuilder builder
                    = OAuthASResponse.authorizationResponse(request, HttpServletResponse.SC_FOUND);
            //设置授权码
            builder.setCode(authorizationCode);
            //构建响应
            final OAuthResponse response = builder
                    .location(oauthRequest.getParam(OAuth.OAUTH_REDIRECT_URI))
                    .buildQueryMessage();
            //根据 OAuthResponse 返回 ResponseEntity 响应
            HttpHeaders headers = new HttpHeaders();
            headers.setLocation(new URI(response.getLocationUri()));
            return new ResponseEntity(headers, HttpStatus.valueOf(response.getResponseStatus()));
        } catch (OAuthProblemException e) {
            log.error(e.getMessage(), e);
            //出错处理
            String redirectUri = e.getRedirectUri();
            if (OAuthUtils.isEmpty(redirectUri)) {
                //告诉客户端没有传入 redirectUri 直接报错
                return new ResponseEntity<>("OAuth callback url needs to be provided by client!!!", HttpStatus.NOT_FOUND);
            }
            //返回错误消息(如?error=)
            final OAuthResponse response = OAuthASResponse
                    .errorResponse(HttpServletResponse.SC_FOUND)
                    .error(e)
                    .location(redirectUri)
                    .buildQueryMessage();
            HttpHeaders headers = new HttpHeaders();
            headers.setLocation(new URI(response.getLocationUri()));
            return new ResponseEntity(headers, HttpStatus.valueOf(response.getResponseStatus()));
        }
2.根据code获取token

接口

@ApiOperation(value = "令牌",
            nickname = "token",
            notes = "1.根据授权码获取令牌</br>2.根据refresh_token刷新token",
            tags = "Oauth")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "grant_type", value = "授予类型", required = true, paramType = "query", allowableValues = "authorization_code,refresh_token"),
            @ApiImplicitParam(name = "client_id", value = "客户端id", required = true, paramType = "query", example = "test"),
            @ApiImplicitParam(name = "client_secret", value = "客户端密钥", required = true, paramType = "query", example = "test"),
            @ApiImplicitParam(name = "redirect_uri", value = "重定向uri", required = true, paramType = "query", example = "https://www.baidu.com/s?wd=123"),
            @ApiImplicitParam(name = "code", value = "授权码", required = true, paramType = "query"),
            @ApiImplicitParam(name = "content-type", value = "content-type", required = true, paramType = "header", allowableValues = "application/x-www-form-urlencoded")
    })
    @PostMapping(value = "/token", produces = "application/json")
    @ResponseBody
    HttpEntity token(HttpServletRequest request, HttpServletResponse response) throws OAuthSystemException;

逻辑

@Override
    public ResponseEntity<String> token(HttpServletRequest request) throws OAuthSystemException {
        try {
            //构建 OAuth 请求
            OAuthTokenRequest oauthRequest = new OAuthTokenRequest(request);
            String clientId = oauthRequest.getClientId();
            String clientSecret = oauthRequest.getClientSecret();
            String authCode = oauthRequest.getParam(OAuth.OAUTH_CODE);
            String oauthGrantType = oauthRequest.getParam(OAuth.OAUTH_GRANT_TYPE);
            String refreshToken = oauthRequest.getParam(OAuth.OAUTH_REFRESH_TOKEN);
            //检查提交的客户端 id 是否正确
            ClientDetails clientDetails = oauthManager.getClientDetailByClientId(clientId);
            if (clientDetails == null) {
                OAuthResponse response = OAuthASResponse
                        .errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                        .setError(OAuthError.TokenResponse.INVALID_CLIENT)
                        .setErrorDescription(Constants.INVALID_CLIENT_DESCRIPTION)
                        .buildJSONMessage();
                return new ResponseEntity<>(response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
            }
            // 检查客户端安全 KEY 是否正确
            if (!clientSecret.equals(clientDetails.getClientSecret())) {
                OAuthResponse response = OAuthASResponse
                        .errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
                        .setError(OAuthError.TokenResponse.UNAUTHORIZED_CLIENT)
                        .setErrorDescription(Constants.INVALID_CLIENT_DESCRIPTION)
                        .buildJSONMessage();
                return new ResponseEntity<>(response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
            }

            // 检查验证类型,此处只检查 AUTHORIZATION_CODE 类型,其他的还有 PASSWORD 或 REFRESH_TOKEN
            String userUUID;
            if (oauthGrantType.equals(GrantType.AUTHORIZATION_CODE.toString())) {
                OauthCode oauthCode = oauthManager.getOauthByClientIdAndCode(clientId, authCode);
                if (oauthCode == null) {
                    OAuthResponse response = OAuthASResponse
                            .errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                            .setErrorDescription("错误的授权码")
                            .buildJSONMessage();
                    return new ResponseEntity<>(response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
                }
                //删除原有令牌
                userUUID = oauthCode.getUserUUID();
                AccessToken accessToken = oauthManager.getAccessToken(clientId, userUUID);
                if (accessToken != null) {
                    oauthManager.deleteAccessToken(accessToken);
                }
                //删除一次性auth_code
                oauthManager.deleteOauthCode(clientId,authCode);
            } else if (oauthGrantType.equals(GrantType.REFRESH_TOKEN.toString())) {
                //删除原有令牌
                AccessToken accessToken = oauthManager.getAccessTokenByRefreshToken(clientId, refreshToken);
                if (accessToken != null) {
                    userUUID = accessToken.getUserUUID();
                    oauthManager.deleteAccessToken(accessToken);
                } else {
                    //不存在的refreshToken
                    OAuthResponse response = OAuthASResponse
                            .errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                            .setErrorDescription("不存在的refreshToken")
                            .buildJSONMessage();
                    return new ResponseEntity<>(response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
                }
            } else {
                OAuthResponse response = OAuthASResponse
                        .errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                        .setErrorDescription(OAuthError.CodeResponse.UNSUPPORTED_RESPONSE_TYPE)
                        .setErrorDescription("不支持的响应类型")
                        .buildJSONMessage();
                return new ResponseEntity<>(response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
            }
            //生成最新AccessToken并保存
            OAuthIssuer oauthIssuerImpl = new OAuthIssuerImpl(new MD5Generator());
            final String token = oauthIssuerImpl.accessToken();
            AccessToken newAccessToken = new AccessToken();
            newAccessToken.setToken(token);
            newAccessToken.setClientId(clientId);
            newAccessToken.setUserUUID(userUUID);
            /*if (clientDetails.supportRefreshToken()) {

            }*/
            String newRefreshToken = oauthIssuerImpl.refreshToken();
            newAccessToken.setRefreshToken(newRefreshToken);
            oauthManager.saveAccessToken(newAccessToken);
            //生成 OAuth 响应
            OAuthResponse response = OAuthASResponse
                    .tokenResponse(HttpServletResponse.SC_OK)
                    .setTokenType(TokenType.BEARER.toString())
                    .setAccessToken(token)
                    .setExpiresIn(Integer.toString(Constants.ACCESS_TOKEN_VALIDITY_SECONDS))
                    .setRefreshToken(newRefreshToken)
                    .buildJSONMessage();
            //根据 OAuthResponse 生成 ResponseEntity
            return new ResponseEntity<>(response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
        } catch (OAuthProblemException e) {
            log.error(e.getMessage(), e);
            //构建错误响应
            OAuthResponse res = OAuthASResponse
                    .errorResponse(HttpServletResponse.SC_BAD_REQUEST).error(e)
                    .buildJSONMessage();
            return new ResponseEntity<>(res.getBody(), HttpStatus.valueOf(res.getResponseStatus()));
        }
    }

Resource(资源服务器)

请求头:token:Bearer ${access_token}
注意一定要加Bearer!!!注意一定要加Bearer!!!注意一定要加Bearer!!!

在所有请求之前做如下token鉴权逻辑的判断

String getToken() {
        try {
            OAuthAccessResourceRequest oauthRequest =
                    new OAuthAccessResourceRequest(request, ParameterStyle.HEADER);
            //获取 Access Token
            String accessToken = oauthRequest.getAccessToken();
            //验证 Access Token
            AccessToken accessTokenByToken = oauthManager.getAccessTokenByToken(accessToken);
            if (accessTokenByToken == null) {
                // 如果不存在/过期了,返回未验证错误,需重新验证
                throw OAuthProblemException.error(OAuthError.ResourceResponse.INVALID_TOKEN, "无效的token");
            }
            //返回用户名
            String userUUID = accessTokenByToken.getUserUUID();
            UserLink userLink = userManager.getUserLink(userUUID);
            //获取数据库缓存的协作系统token
            UserToken userToken = userManager.getUserToken(userUUID);
            if (userToken == null) {
                String token = rishiqingApiManager.getToken(userLink.getCorpId(), userLink.getStaffId());
                userToken = new UserToken();
                userToken.setUserId(userLink.getUserId());
                userToken.setUserUUID(userUUID);
                userToken.setRishiqingToken(token);
                userManager.saveUserToken(userToken);
                return token;
            } else {
                LocalDateTime gmtModified = userToken.getGmtModified();
                long l = Duration.between(gmtModified, LocalDateTime.now()).toDays();
                if (l > 15) {
                    String token = rishiqingApiManager.getToken(userLink.getCorpId(), userLink.getStaffId());
                    userToken.setRishiqingToken(token);
                    userManager.updateUserToken(userToken);
                    return token;
                } else {
                    return userToken.getRishiqingToken();
                }
            }
        } catch (OAuthSystemException | OAuthProblemException e) {
            log.error(e.getMessage(), e);
            throw new TokenException();
        }
    }

未开发完:待完善