開放平臺設計選型&代碼編寫

最近在着手開發開放平臺的設計選型工作,以下來嘮嘮技術上選型的心路歷程:
相關閱讀:
早期Oauth2.0解釋:
阮一峯:理解OAuth 2.0
最新Oauth2.0簡單解釋:
阮一峯:OAuth 2.0 的一個簡單解釋
阮一峯:OAuth 2.0 的四種方式
阮一峯:GitHub OAuth 第三方登錄示例教程

選型

那時的我
面臨着兩種抉擇:

  • 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();
        }
    }

未開發完:待完善

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章