选型
那时的我
面临着两种抉择:
- 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();
}
}
未开发完:待完善