最近在着手開發開放平臺的設計選型工作,以下來嘮嘮技術上選型的心路歷程:
相關閱讀:
早期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();
}
}
未開發完:待完善