oltu是一个开源的oauth2.0协议的实现,本人在此开源项目的基础上进行修改,实现一个自定义的oauth2.0模块。
关于oltu的使用大家可以看这里:http://oltu.apache.org/
项目可以从这里下载:http://mirror.bit.edu.cn/apache/oltu/org.apache.oltu.oauth2/
项目中我将四种授权方式都做了实现(授权码模式、简化模式、密码模式及客户端模式),但是这里仅以授权码模式为例,服务端采用Jersey框架实现,而客户端采用spring mvc实现。
服务器端实现:为了方便开发,我将oltu的所有源码都拖进了项目中,而不是导入jar,因为很多地方可能在我所开发的项目中不适用,这样可以方便修改和跟踪代码。
其实服务端开发很简单,主要集中在两个比较主要的文件中,一个是请求授权的AuthzEndpoint.java,一个是生成令牌的TokenEndpoint.java,如图所示。
至于资源控制器由于我开发的项目中,资源访问控制是采用过滤器的方式,因此没有用到oltu提供的java类,两个主要类文件的代码修改如下:
AuthzEndpoint.java
/**
* Copyright 2010 Newcastle University
*
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.oltu.oauth2.integration.endpoints;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import org.apache.ibatis.session.SqlSession;
import org.apache.oltu.oauth2.as.issuer.MD5Generator;
import org.apache.oltu.oauth2.as.issuer.OAuthIssuerImpl;
import org.apache.oltu.oauth2.as.request.OAuthAuthzRequest;
import org.apache.oltu.oauth2.as.response.OAuthASResponse;
import org.apache.oltu.oauth2.common.OAuth;
import org.apache.oltu.oauth2.common.error.OAuthError;
import org.apache.oltu.oauth2.common.error.ServerErrorType;
import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
import org.apache.oltu.oauth2.common.message.OAuthResponse;
import org.apache.oltu.oauth2.common.message.types.ResponseType;
import org.apache.oltu.oauth2.integration.utils.Cache;
import org.apache.oltu.oauth2.integration.utils.CacheManager;
import com.cz.bean.App;
import com.cz.bean.Authority;
import com.cz.bean.RefreshToken;
import com.cz.dao.AppMapper;
import com.cz.dao.AuthorityMapper;
import com.cz.dao.RefreshTokenMapper;
import com.cz.util.DbUtil;
/**
*
* client request authorization
*
*/
@Path ( "/authz" )
public class AuthzEndpoint {
SqlSession sqlSession = DbUtil.getSessionFactory().openSession( true );
AppMapper appDao = sqlSession.getMapper(AppMapper. class );
AuthorityMapper authorityDao = sqlSession.getMapper(AuthorityMapper. class );
RefreshTokenMapper refreshTokenDao = sqlSession
.getMapper(RefreshTokenMapper. class );
//登录页面
private static String loginPage;
//错误页面
private static String errorPage;
static {
Properties p = new Properties();
try {
p.load(AuthzEndpoint. class .getClassLoader().getResourceAsStream(
"config.properties" ));
loginPage = p.getProperty( "loginPage" );
errorPage = p.getProperty( "errorPage" );
} catch (Exception e) {
e.printStackTrace();
}
}
public static final String INVALID_CLIENT_DESCRIPTION = "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method)." ;
@GET
public Response authorize( @Context HttpServletRequest request)
throws URISyntaxException, OAuthSystemException {
OAuthAuthzRequest oauthRequest = null ;
OAuthIssuerImpl oauthIssuerImpl = new OAuthIssuerImpl(
new MD5Generator());
try {
oauthRequest = new OAuthAuthzRequest(request);
/*
* 当前登录的用户,模拟一个从session中获取的登录用户
* 该方法未实现,待模块与养老平台整合时,应调用养老平台方法判断用户是否已登录
* 并获得对应用户的userId
*/
String userId = "1" ;
if ( "" .equals(userId) || userId == null ) {
// 用户没有登录就跳转到登录页面
return Response.temporaryRedirect( new URI(loginPage)).build();
}
App app = null ;
if (oauthRequest.getClientId()!= null && ! "" .equals(oauthRequest.getClientId())){
app = appDao.selectByPrimaryKey(oauthRequest.getClientId());
} else {
return Response.temporaryRedirect( new URI(errorPage+ "?error=" +ServerErrorType.CLIENT_ID_IS_NULL)).build();
}
// 根据response_type创建response
String responseType = oauthRequest
.getParam(OAuth.OAUTH_RESPONSE_TYPE);
OAuthASResponse.OAuthAuthorizationResponseBuilder builder = OAuthASResponse
.authorizationResponse(request,
HttpServletResponse.SC_FOUND);
// 检查传入的客户端id是否正确
if (app == null ) {
return Response.temporaryRedirect( new URI(errorPage+ "?error=" +ServerErrorType.UNKOWN_CLIENT_ID)).build();
}
String scope = oauthRequest.getParam(OAuth.OAUTH_SCOPE);
// 授权请求类型
if (responseType.equals(ResponseType.CODE.toString())) {
String code = oauthIssuerImpl.authorizationCode();
builder.setCode(code);
CacheManager.putCache(userId+ "_code" , new Cache( "code" , code,
216000000 , false ));
CacheManager.putCache(userId+ "_scope" , new Cache( "scope" , scope,
216000000 , false ));
}
if (responseType.equals(ResponseType.TOKEN.toString())) {
// 校验client_secret
if (!app.getSecret_key().equals(oauthRequest.getClientSecret())) {
OAuthResponse response =
OAuthASResponse.errorResponse(HttpServletResponse.SC_OK)
.setError(OAuthError.TokenResponse.INVALID_CLIENT).setErrorDescription(INVALID_CLIENT_DESCRIPTION)
.buildJSONMessage();
return Response.status(response.getResponseStatus()).entity(response.getBody()).build();
}
String accessToken = oauthIssuerImpl.accessToken();
builder.setAccessToken(accessToken);
builder.setExpiresIn(3600l);
//判断是否已经授权----待调整是放在authz部分还是token部分
Map<String,Object> aQueryParam = new HashMap<>();
aQueryParam.put( "appKey" ,oauthRequest.getClientId());
aQueryParam.put( "userId" ,Integer.valueOf(userId));
if (authorityDao.findUnique(aQueryParam)== null ){
Authority authority = new Authority();
authority.setApp_key(oauthRequest.getClientId());
authority.setUser_id(Integer.valueOf(userId));
authorityDao.insert(authority);
}
// 存储token,已授权则更新令牌,未授权则新增令牌
Map<String,Object> rQueryParam = new HashMap<>();
rQueryParam.put( "appKey" , oauthRequest.getClientId());
rQueryParam.put( "userId" , Integer.valueOf(userId));
if (refreshTokenDao.findUnique(rQueryParam) != null ) {
Map<String,Object> map = new HashMap<>();
map.put( "accessToken" , accessToken);
map.put( "appKey" , oauthRequest.getClientId());
map.put( "userId" , Integer.valueOf(userId));
map.put( "createTime" , getDate());
map.put( "scope" , scope);
map.put( "authorizationTime" , getDate());
refreshTokenDao.updateAccessToken(map);
} else {
RefreshToken rt = new RefreshToken();
rt.setApp_key(oauthRequest.getClientId());
rt.setUser_id(Integer.valueOf(userId));
rt.setAccess_token(accessToken);
rt.setCreate_time(getDate());
rt.setAuthorization_time(getDate());
rt.setExpire( "3600" );
rt.setScope(scope);
rt.setAuthorization_time(getDate());
refreshTokenDao.insert(rt);
}
}
// 客户端跳转URI
String redirectURI = oauthRequest
.getParam(OAuth.OAUTH_REDIRECT_URI);
final OAuthResponse response = builder.location(redirectURI).setParam( "scope" , scope)
.buildQueryMessage();
String test = response.getLocationUri();
URI url = new URI(response.getLocationUri());
return Response.status(response.getResponseStatus()).location(url)
.build();
} catch (OAuthProblemException e) {
return Response.temporaryRedirect( new URI(errorPage+ "?error=" +ServerErrorType.BAD_RQUEST)).build();
}
}
private String getDate() {
SimpleDateFormat sdf = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" );
return sdf.format(System.currentTimeMillis());
}
}
|
上面的代码是授权码认证的第一步,当用户同意授权之后向服务器请求授权码。你可以使用一下腾讯的授权功能来加深一下体会,因为我所开发的模块也是参考腾讯的授权认证流程来实现的,客户端通过提交请求,访问类似http://192.168.19.75:10087/oauth/authz?client_id=s6BhdRkqt3&client_secret=12345&redirect_uri=http://localhost:8080/redirect.jsp&state=y&response_type=authorization_code的链接来访问上面的程序,参数的含义如下
client_id :客户端id
client_secret:客户端密钥
redirect_uri:回调地址,第三方应用定义的地址
State:状态,服务器将返回一个一模一样的参数。
response_type:授权方式,这里必须是authorization_code,表示授权码 方式。
这个过程结束时,服务器会跳转至第三方应用定义的回调地址并附上授权码,而第三方通过这个回调地址获得授权码并进行相应的处理,而这个过程在oltu的实现中其实就是几行简单的代码:
// 创建response wrapper
OAuthAuthzResponse oar = null ;
oar = OAuthAuthzResponse.oauthCodeAuthzResponse(request);
// 获得授权码
String code = oar.getCode();
|
上面的代码就是oltu客户端接收服务器发回的授权码的代码,其中request是一个HttpServletRequest对象,获得了授权码之后,按照下一步的流程,自然就是向授权服务器请求令牌并附上上一步获得的授权码。服务器获得授权码并进行相应处理的代码如下:
TokenEndpoint.java
/**
* Copyright 2010 Newcastle University
*
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.oltu.oauth2.integration.endpoints;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import org.apache.ibatis.session.SqlSession;
import org.apache.oltu.oauth2.as.issuer.MD5Generator;
import org.apache.oltu.oauth2.as.issuer.OAuthIssuer;
import org.apache.oltu.oauth2.as.issuer.OAuthIssuerImpl;
import org.apache.oltu.oauth2.as.request.OAuthTokenRequest;
import org.apache.oltu.oauth2.as.response.OAuthASResponse;
import org.apache.oltu.oauth2.common.OAuth;
import org.apache.oltu.oauth2.common.error.OAuthError;
import org.apache.oltu.oauth2.common.error.ServerErrorType;
import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
import org.apache.oltu.oauth2.common.message.OAuthResponse;
import org.apache.oltu.oauth2.common.message.types.GrantType;
import org.apache.oltu.oauth2.integration.utils.CacheManager;
import com.cz.bean.App;
import com.cz.bean.Authority;
import com.cz.bean.RefreshToken;
import com.cz.bean.User;
import com.cz.dao.AppMapper;
import com.cz.dao.AuthorityMapper;
import com.cz.dao.RefreshTokenMapper;
import com.cz.dao.UserMapper;
import com.cz.util.DbUtil;
/**
*
* get access token
*
*/
@Path ( "/token" )
public class TokenEndpoint {
SqlSession sqlSession = DbUtil.getSessionFactory().openSession( true );
AppMapper appDao = sqlSession.getMapper(AppMapper. class );
RefreshTokenMapper refreshTokenDao = sqlSession
.getMapper(RefreshTokenMapper. class );
UserMapper dao = sqlSession.getMapper(UserMapper. class );
AuthorityMapper authorityDao = sqlSession.getMapper(AuthorityMapper. class );
// 登录页面
private static String loginPage;
// 错误页面
private static String errorPage;
static {
Properties p = new Properties();
try {
p.load(AuthzEndpoint. class .getClassLoader().getResourceAsStream(
"config.properties" ));
loginPage = p.getProperty( "loginPage" );
errorPage = p.getProperty( "errorPage" );
} catch (Exception e) {
e.printStackTrace();
}
}
public static final String INVALID_CLIENT_DESCRIPTION = "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method)." ;
@SuppressWarnings ({ "unchecked" , "rawtypes" })
@POST
@Consumes ( "application/x-www-form-urlencoded" )
@Produces ( "application/json" )
public Response authorize( @Context HttpServletRequest request)
throws OAuthSystemException, URISyntaxException {
OAuthTokenRequest oauthRequest = null ;
String scope = "" ;
OAuthIssuer oauthIssuerImpl = new OAuthIssuerImpl( new MD5Generator());
try {
oauthRequest = new OAuthTokenRequest(request);
/*
* 当前登录的用户,模拟一个从session中获取的登录用户
* 该方法未实现,待模块与养老平台整合时,应调用养老平台方法判断用户是否已登录
*/
String userId = "1" ;
if ( "" .equals(userId) || userId == null ) {
// 用户没有登录的话就跳转到登录页面
return Response.temporaryRedirect( new URI(loginPage)).build();
}
App app = null ;
if (oauthRequest.getClientId() != null && ! "" .equals(oauthRequest.getClientId())) {
app = appDao.selectByPrimaryKey(oauthRequest.getClientId());
} else {
return Response.temporaryRedirect( new URI(errorPage + "?error=" + ServerErrorType.CLIENT_ID_IS_NULL)).build();
}
// 校验clientid
if (app == null || !app.getApp_key().toString().equals(oauthRequest.getClientId())) {
if (oauthRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals(GrantType.AUTHORIZATION_CODE.toString())){
return Response.temporaryRedirect( new URI(errorPage + "?error=" + ServerErrorType.UNKOWN_CLIENT_ID)).build();
} else {
OAuthResponse response =
OAuthASResponse.errorResponse(HttpServletResponse.SC_OK)
.setError(OAuthError.TokenResponse.INVALID_CLIENT).setErrorDescription(INVALID_CLIENT_DESCRIPTION)
.buildJSONMessage();
return Response.status(response.getResponseStatus()).entity(response.getBody()).build();
}
}
// 校验client_secret
if (!app.getSecret_key().equals(oauthRequest.getClientSecret())) {
if (oauthRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals(GrantType.AUTHORIZATION_CODE.toString())){
return Response.temporaryRedirect( new URI(errorPage + "?error=" + ServerErrorType.UNKOWN_CLIENT_SECRET)).build();
} else {
OAuthResponse response =
OAuthASResponse.errorResponse(HttpServletResponse.SC_OK)
.setError(OAuthError.TokenResponse.INVALID_CLIENT).setErrorDescription(INVALID_CLIENT_DESCRIPTION)
.buildJSONMessage();
return Response.status(response.getResponseStatus()).entity(response.getBody()).build();
}
}
// 校验不同类型的授权方式
if (oauthRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals(GrantType.AUTHORIZATION_CODE.toString())) {
String cacheCode = null ;
if (CacheManager.getCacheInfo(userId + "_code" ).getValue() != null ) {
cacheCode = CacheManager.getCacheInfo(userId + "_code" )
.getValue().toString();
} else {
// 用户没有登录的话就跳转到登录页面
return Response.temporaryRedirect( new URI(loginPage)).build();
}
if (!cacheCode.equals(oauthRequest.getParam(OAuth.OAUTH_CODE))) {
return Response.temporaryRedirect( new URI(errorPage+ "?error=" + ServerErrorType.INVALID_AUTHORIZATION_CODE)).build();
}
if (CacheManager.getCacheInfo(userId+ "_scope" ).getValue()!= null ){
scope = CacheManager.getCacheInfo(userId+ "_scope" ).getValue().toString();
}
} else if (oauthRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals(GrantType.PASSWORD.toString())) {
User user = dao.getById(userId);
if (!user.getPassword().equals(oauthRequest.getPassword())|| !user.getName().equals(oauthRequest.getUsername())) {
OAuthResponse response = OAuthASResponse
.errorResponse(HttpServletResponse.SC_OK)
.setError(OAuthError.TokenResponse.INVALID_CLIENT)
.setErrorDescription( "Invalid username or password." )
.buildJSONMessage();
return Response.status(response.getResponseStatus()).entity(response.getBody()).build();
}
} else if (oauthRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals(
GrantType.CLIENT_CREDENTIALS.toString())) {
// 客户端id以及secret已验证,更多验证规则在这里添加,没有其他验证则程序直接发放令牌
// OAuthResponse response = OAuthASResponse
// .errorResponse(HttpServletResponse.SC_OK)
// .setError(OAuthError.TokenResponse.INVALID_GRANT)
// .setErrorDescription("invalid client")
// .buildJSONMessage();
// return Response.status(response.getResponseStatus()).entity(response.getBody()).build();
} else if (oauthRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals(
GrantType.REFRESH_TOKEN.toString())) {
// 刷新令牌未实现
}
String accessToken = oauthIssuerImpl.accessToken();
String refreshToken = oauthIssuerImpl.refreshToken();
// 构建响应
OAuthResponse response = OAuthASResponse
.tokenResponse(HttpServletResponse.SC_OK)
.setAccessToken(accessToken).setRefreshToken(refreshToken)
.setExpiresIn( "3600" )
.buildJSONMessage();
// 判断是否已经授权----待调整是放在authz部分还是token部分
Map aQueryParam = new HashMap();
aQueryParam.put( "appKey" , oauthRequest.getClientId());
aQueryParam.put( "userId" , Integer.valueOf(userId));
if (authorityDao.findUnique(aQueryParam) == null ) {
Authority authority = new Authority();
authority.setApp_key(oauthRequest.getClientId());
authority.setUser_id(Integer.valueOf(userId));
authorityDao.insert(authority);
}
// String scope = "";
// if(CacheManager.getCacheInfo(userId+"_scope").getValue()!=null){
// scope = CacheManager.getCacheInfo(userId+"_scope").getValue().toString();
// }
// 存储token,已授权则更新令牌,未授权则新增令牌
Map rQueryParam = new HashMap();
rQueryParam.put( "appKey" , oauthRequest.getClientId());
rQueryParam.put( "userId" , Integer.valueOf(userId));
if (refreshTokenDao.findUnique(rQueryParam) != null ) {
Map map = new HashMap();
map.put( "accessToken" , accessToken);
map.put( "appKey" , oauthRequest.getClientId());
map.put( "userId" , Integer.valueOf(userId));
map.put( "createTime" , getDate());
map.put( "scope" , scope);
map.put( "authorizationTime" , getDate());
refreshTokenDao.updateAccessToken(map);
} else {
RefreshToken rt = new RefreshToken();
rt.setApp_key(oauthRequest.getClientId());
rt.setUser_id(Integer.valueOf(userId));
rt.setAccess_token(accessToken);
rt.setRefresh_token(refreshToken);
rt.setCreate_time(getDate());
rt.setAuthorization_time(getDate());
rt.setExpire( "3600" );
rt.setScope(scope);
rt.setAuthorization_time(getDate());
refreshTokenDao.insert(rt);
}
return Response.status(response.getResponseStatus())
.entity(response.getBody()).build();
} catch (OAuthProblemException e) {
System.out.println(e.getDescription());
return Response.temporaryRedirect( new URI(errorPage + "?error=" + ServerErrorType.BAD_RQUEST)).build();
}
}
private String getDate() {
SimpleDateFormat sdf = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" );
return sdf.format(System.currentTimeMillis());
}
}
|
上面的代码,处理了客户端发来的申请令牌请求,并向客户端发放访问令牌,而oltu的客户端则通过如下代码来完成这个请求令牌和解析令牌的过程:
OAuthClient client = new OAuthClient( new URLConnectionClient());
OAuthAccessTokenResponse oauthResponse = null ;
oauthResponse =
client.accessToken(request, OAuth.HttpMethod.POST);
String token = oauthResponse.getRefreshToken();
|
如果你是第一次开发,oauth2.0的认证过程可能会让你觉得头疼,因为你首先需要对这个流程很熟悉,并且同时要看懂了oltu的代码才好理解这个开源的项目到底是怎么实现这个过程的,因此这里我不过多的粘贴代码,因为这并没有什么卵用,还是运行项目和追踪代码比较容易理解它的原理,下面是我实现的项目代码,代码写得比较简陋,不过对于跟我一样的菜鸟,还是能起到一定的帮助的~
服务端:https://git.oschina.net/honganlei/oauth-server.git
服务端授权登录页面:https://git.oschina.net/honganlei/OauthClient.git
第三方接入授权模块的例子:https://git.oschina.net/honganlei/OauthApp.git