?
目錄貼: ?跟我學Shiro目錄貼
?
?
目前很多開放平臺如新浪微博開放平臺都在使用提供開放 API 接口供開發者使用,隨之帶來了第三方應用要到開放平臺進行授權的問題, OAuth 就是干這個的, OAuth2 是 OAuth 協議的下一個版本,相比 OAuth1 , OAuth2 整個授權流程更簡單安全了,但不兼容 OAuth1 ,具體可以到 OAuth2 官網 http://oauth.net/2/ 查看, OAuth2 協議規范可以參考 http://tools.ietf.org/html/rfc6749 。目前有好多參考實現供選擇,可以到其官網查看下載。
?
本文使用 Apache Oltu ,其之前的名字叫 Apache Amber? ,是 Java 版的參考實現。使用文檔可參考 https://cwiki.apache.org/confluence/display/OLTU/Documentation 。
?
OAuth 角色
資源擁有者( resource owner ) :能授權訪問受保護資源的一個實體,可以是一個人,那我們稱之為最終用戶;如新浪微博用戶 zhangsan ;
資源服務器( resource server ) :存儲受保護資源,客戶端通過 access token 請求資源,資源服務器響應受保護資源給客戶端;存儲著用戶 zhangsan 的微博等信息。
授權服務器( authorization server ) :成功驗證資源擁有者并獲取授權之后,授權服務器頒發授權令牌( Access Token )給客戶端。
客戶端( client ) :如新浪微博客戶端 weico 、微格等第三方應用,也可以是它自己的官方應用;其本身不存儲資源,而是資源擁有者授權通過后,使用它的授權(授權令牌)訪問受保護資源,然后客戶端把相應的數據展示出來 / 提交到服務器。“客戶端”術語不代表任何特定實現(如應用運行在一臺服務器、桌面、手機或其他設備)。 ?
?
OAuth2 協議流程
1 、客戶端從資源擁有者那請求授權。授權請求可以直接發給資源擁有者,或間接的通過授權服務器這種中介, 后者更可取 。
2 、客戶端收到一個授權許可,代表資源服務器提供的授權。
3 、客戶端使用它自己的私有證書及授權許可到授權服務器驗證。
4 、如果驗證成功,則下發一個訪問令牌。
5 、客戶端使用訪問令牌向資源服務器請求受保護資源。
6 、資源服務器會驗證訪問令牌的有效性,如果成功則下發受保護資源。
?
更多流程的解釋請參考 OAuth2 的協議規范 http://tools.ietf.org/html/rfc6749 。
?
服務器端
本文把授權服務器和資源服務器整合在一起實現。
?
POM 依賴
此處我們使用 apache oltu oauth2 服務端實現,需要引入 authzserver (授權服務器依賴)和 resourceserver (資源服務器依賴)。?
<dependency>
<groupId>org.apache.oltu.oauth2</groupId>
<artifactId>org.apache.oltu.oauth2.authzserver</artifactId>
<version>0.31</version>
</dependency>
<dependency>
<groupId>org.apache.oltu.oauth2</groupId>
<artifactId>org.apache.oltu.oauth2.resourceserver</artifactId>
<version>0.31</version>
</dependency>?
其他的請參考 pom.xml 。
?
數據字典
用戶 (oauth2_user)
|
名稱 |
類型 |
長度 |
描述 |
|
id |
bigint |
10 |
編號 ? 主鍵 |
|
username |
varchar |
100 |
用戶名 |
|
password |
varchar |
100 |
密碼 |
|
salt |
varchar |
50 |
鹽 |
客戶端 (oauth2_client)
|
名稱 |
類型 |
長度 |
描述 |
|
id |
bigint |
10 |
編號 ? 主鍵 |
|
client_name |
varchar |
100 |
客戶端名稱 |
|
client_id |
varchar |
100 |
客戶端 id |
|
client_secret |
varchar |
100 |
客戶端安全 key |
?
用戶表存儲著認證 / 資源服務器的用戶信息,即資源擁有者;比如用戶名 / 密碼;客戶端表存儲客戶端的的客戶端 id 及客戶端安全 key ;在進行授權時使用。
?
表及數據 SQL
具體請參考
sql/ shiro-schema.sql? (表結構)
sql/ shiro-data.sql?? (初始數據)
?
默認用戶名 / 密碼是 admin/123456 。
?
實體
具體請參考 com.github.zhangkaitao.shiro.chapter17.entity 包下的實體,此處就不列舉了。
?
DAO
具體請參考 com.github.zhangkaitao.shiro.chapter17.dao 包下的 DAO 接口及實現。
?
Service
具體請參考 com.github.zhangkaitao.shiro.chapter17.service 包下的 Service 接口及實現。以下是出了基本 CRUD 之外的關鍵接口:?
public interface UserService {
public User createUser(User user);// 創建用戶
public User updateUser(User user);// 更新用戶
public void deleteUser(Long userId);// 刪除用戶
public void changePassword(Long userId, String newPassword); //修改密碼
User findOne(Long userId);// 根據id查找用戶
List<User> findAll();// 得到所有用戶
public User findByUsername(String username);// 根據用戶名查找用戶
}
public interface ClientService {
public Client createClient(Client client);// 創建客戶端
public Client updateClient(Client client);// 更新客戶端
public void deleteClient(Long clientId);// 刪除客戶端
Client findOne(Long clientId);// 根據id查找客戶端
List<Client> findAll();// 查找所有
Client findByClientId(String clientId);// 根據客戶端id查找客戶端
Client findByClientSecret(String clientSecret);//根據客戶端安全KEY查找客戶端
}
public interface OAuthService {
public void addAuthCode(String authCode, String username);// 添加 auth code
public void addAccessToken(String accessToken, String username); // 添加 access token
boolean checkAuthCode(String authCode); // 驗證auth code是否有效
boolean checkAccessToken(String accessToken); // 驗證access token是否有效
String getUsernameByAuthCode(String authCode);// 根據auth code獲取用戶名
String getUsernameByAccessToken(String accessToken);// 根據access token獲取用戶名
long getExpireIn();//auth code / access token 過期時間
public boolean checkClientId(String clientId);// 檢查客戶端id是否存在
public boolean checkClientSecret(String clientSecret);// 堅持客戶端安全KEY是否存在
}?
此處通過 OAuthService 實現進行 auth code 和 access token 的維護。
?
后端數據維護控制器
具體請參考 com.github.zhangkaitao.shiro.chapter17.web.controller 包下的 IndexController 、 LoginController 、 UserController 和 ClientController ,其用于維護后端的數據,如用戶及客戶端數據;即相當于后臺管理。
?
授權控制器 AuthorizeController? ?????
@Controller
public class AuthorizeController {
@Autowired
private OAuthService oAuthService;
@Autowired
private ClientService clientService;
@RequestMapping("/authorize")
public Object authorize(Model model, HttpServletRequest request)
throws URISyntaxException, OAuthSystemException {
try {
//構建OAuth 授權請求
OAuthAuthzRequest oauthRequest = new OAuthAuthzRequest(request);
//檢查傳入的客戶端id是否正確
if (!oAuthService.checkClientId(oauthRequest.getClientId())) {
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();
//如果用戶沒有登錄,跳轉到登陸頁面
if(!subject.isAuthenticated()) {
if(!login(subject, request)) {//登錄失敗時跳轉到登陸頁面
model.addAttribute("client",
clientService.findByClientId(oauthRequest.getClientId()));
return "oauth2login";
}
}
String username = (String)subject.getPrincipal();
//生成授權碼
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();
oAuthService.addAuthCode(authorizationCode, username);
}
//進行OAuth響應構建
OAuthASResponse.OAuthAuthorizationResponseBuilder builder =
OAuthASResponse.authorizationResponse(request,
HttpServletResponse.SC_FOUND);
//設置授權碼
builder.setCode(authorizationCode);
//得到到客戶端重定向地址
String redirectURI = oauthRequest.getParam(OAuth.OAUTH_REDIRECT_URI);
//構建響應
final OAuthResponse response = builder.location(redirectURI).buildQueryMessage();
//根據OAuthResponse返回ResponseEntity響應
HttpHeaders headers = new HttpHeaders();
headers.setLocation(new URI(response.getLocationUri()));
return new ResponseEntity(headers, HttpStatus.valueOf(response.getResponseStatus()));
} catch (OAuthProblemException 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()));
}
}
private boolean login(Subject subject, HttpServletRequest request) {
if("get".equalsIgnoreCase(request.getMethod())) {
return false;
}
String username = request.getParameter("username");
String password = request.getParameter("password");
if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
return false;
}
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
subject.login(token);
return true;
} catch (Exception e) {
request.setAttribute("error", "登錄失敗:" + e.getClass().getName());
return false;
}
}
}?
如上代碼的作用:
1 、首先通過如 http://localhost:8080/chapter17-server/authorize
?client_id=c1ebe466-1cdc-4bd3-ab69-77c3561b9dee&response_type=code&redirect_uri=http://localhost:9080/chapter17-client/oauth2-login 訪問授權頁面;
2 、該控制器首先檢查 clientId 是否正確;如果錯誤將返回相應的錯誤信息;
3 、然后判斷用戶是否登錄了,如果沒有登錄首先到登錄頁面登錄;
4 、登錄成功后生成相應的 auth code 即授權碼,然后重定向到客戶端地址,如 http://localhost:9080/chapter17-client/oauth2-login?code=52b1832f5dff68122f4f00ae995da0ed ;在重定向到的地址中會帶上 code 參數(授權碼),接著客戶端可以根據授權碼去換取 access token 。
?
訪問令牌控制器 AccessTokenController? ?
@RestController
public class AccessTokenController {
@Autowired
private OAuthService oAuthService;
@Autowired
private UserService userService;
@RequestMapping("/accessToken")
public HttpEntity token(HttpServletRequest request)
throws URISyntaxException, OAuthSystemException {
try {
//構建OAuth請求
OAuthTokenRequest oauthRequest = new OAuthTokenRequest(request);
//檢查提交的客戶端id是否正確
if (!oAuthService.checkClientId(oauthRequest.getClientId())) {
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 (!oAuthService.checkClientSecret(oauthRequest.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()));
}
String authCode = oauthRequest.getParam(OAuth.OAUTH_CODE);
// 檢查驗證類型,此處只檢查AUTHORIZATION_CODE類型,其他的還有PASSWORD或REFRESH_TOKEN
if (oauthRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals(
GrantType.AUTHORIZATION_CODE.toString())) {
if (!oAuthService.checkAuthCode(authCode)) {
OAuthResponse response = OAuthASResponse
.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
.setError(OAuthError.TokenResponse.INVALID_GRANT)
.setErrorDescription("錯誤的授權碼")
.buildJSONMessage();
return new ResponseEntity(
response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
}
}
//生成Access Token
OAuthIssuer oauthIssuerImpl = new OAuthIssuerImpl(new MD5Generator());
final String accessToken = oauthIssuerImpl.accessToken();
oAuthService.addAccessToken(accessToken,
oAuthService.getUsernameByAuthCode(authCode));
//生成OAuth響應
OAuthResponse response = OAuthASResponse
.tokenResponse(HttpServletResponse.SC_OK)
.setAccessToken(accessToken)
.setExpiresIn(String.valueOf(oAuthService.getExpireIn()))
.buildJSONMessage();
//根據OAuthResponse生成ResponseEntity
return new ResponseEntity(
response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
} catch (OAuthProblemException e) {
//構建錯誤響應
OAuthResponse res = OAuthASResponse
.errorResponse(HttpServletResponse.SC_BAD_REQUEST).error(e)
.buildJSONMessage();
return new ResponseEntity(res.getBody(), HttpStatus.valueOf(res.getResponseStatus()));
}
}
}?
如上代碼的作用:
1 、首先通過如 http://localhost:8080/chapter17-server/accessToken , POST 提交如下數據: client_id= c1ebe466-1cdc-4bd3-ab69-77c3561b9dee& client_secret= d8346ea2-6017-43ed-ad68-19c0f971738b&grant_type=authorization_code&code=828beda907066d058584f37bcfd597b6&redirect_uri=http://localhost:9080/chapter17-client/oauth2-login 訪問;
2 、該控制器會驗證 client_id 、 client_secret 、 auth code 的正確性,如果錯誤會返回相應的錯誤;
3 、如果驗證通過會生成并返回相應的訪問令牌 access token 。
?
資源控制器 UserInfoController? ?
@RestController
public class UserInfoController {
@Autowired
private OAuthService oAuthService;
@RequestMapping("/userInfo")
public HttpEntity userInfo(HttpServletRequest request) throws OAuthSystemException {
try {
//構建OAuth資源請求
OAuthAccessResourceRequest oauthRequest =
new OAuthAccessResourceRequest(request, ParameterStyle.QUERY);
//獲取Access Token
String accessToken = oauthRequest.getAccessToken();
//驗證Access Token
if (!oAuthService.checkAccessToken(accessToken)) {
// 如果不存在/過期了,返回未驗證錯誤,需重新驗證
OAuthResponse oauthResponse = OAuthRSResponse
.errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
.setRealm(Constants.RESOURCE_SERVER_NAME)
.setError(OAuthError.ResourceResponse.INVALID_TOKEN)
.buildHeaderMessage();
HttpHeaders headers = new HttpHeaders();
headers.add(OAuth.HeaderType.WWW_AUTHENTICATE,
oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE));
return new ResponseEntity(headers, HttpStatus.UNAUTHORIZED);
}
//返回用戶名
String username = oAuthService.getUsernameByAccessToken(accessToken);
return new ResponseEntity(username, HttpStatus.OK);
} catch (OAuthProblemException e) {
//檢查是否設置了錯誤碼
String errorCode = e.getError();
if (OAuthUtils.isEmpty(errorCode)) {
OAuthResponse oauthResponse = OAuthRSResponse
.errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
.setRealm(Constants.RESOURCE_SERVER_NAME)
.buildHeaderMessage();
HttpHeaders headers = new HttpHeaders();
headers.add(OAuth.HeaderType.WWW_AUTHENTICATE,
oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE));
return new ResponseEntity(headers, HttpStatus.UNAUTHORIZED);
}
OAuthResponse oauthResponse = OAuthRSResponse
.errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
.setRealm(Constants.RESOURCE_SERVER_NAME)
.setError(e.getError())
.setErrorDescription(e.getDescription())
.setErrorUri(e.getUri())
.buildHeaderMessage();
HttpHeaders headers = new HttpHeaders();
headers.add(OAuth.HeaderType.WWW_AUTHENTICATE, 、
oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE));
return new ResponseEntity(HttpStatus.BAD_REQUEST);
}
}
}?
如上代碼的作用:
1 、首先通過如 http://localhost:8080/chapter17-server/userInfo? access_token=828beda907066d058584f37bcfd597b6 進行訪問;
2 、該控制器會驗證 access token 的有效性;如果無效了將返回相應的錯誤,客戶端再重新進行授權;
3 、如果有效,則返回當前登錄用戶的用戶名。
?
Spring 配置文件
具體請參考resources/spring*.xml,此處只列舉spring-config-shiro.xml中的shiroFilter的filterChainDefinitions屬性:??
<property name="filterChainDefinitions">
<value>
/ = anon
/login = authc
/logout = logout
/authorize=anon
/accessToken=anon
/userInfo=anon
/** = user
</value>
</property>?
對于 oauth2 的幾個地址 /authorize 、 /accessToken 、 /userInfo 都是匿名可訪問的。
?
其他源碼請直接下載文檔查看。
?
服務器維護
訪問 localhost:8080/chapter17-server/ ,登錄后進行客戶端管理和用戶管理。
客戶端管理就是進行客戶端的注冊,如新浪微博的第三方應用就需要到新浪微博開發平臺進行注冊;用戶管理就是進行如新浪微博用戶的管理。
?
對于授權服務和資源服務的實現可以參考新浪微博開發平臺的實現:
http://open.weibo.com/wiki/授權機制說明 ?
http://open.weibo.com/wiki/ 微博 API ?
?
客戶端
客戶端流程:如果需要登錄首先跳到 oauth2 服務端進行登錄授權,成功后服務端返回 auth code ,然后客戶端使用 auth code 去服務器端換取 access token ,最好根據 access token 獲取用戶信息進行客戶端的登錄綁定。這個可以參照如很多網站的新浪微博登錄功能,或其他的第三方帳號登錄功能。
POM 依賴
此處我們使用apache oltu oauth2客戶端實現。? ???
<dependency>
<groupId>org.apache.oltu.oauth2</groupId>
<artifactId>org.apache.oltu.oauth2.client</artifactId>
<version>0.31</version>
</dependency>?
其他的請參考 pom.xml 。
?
OAuth2Token
類似于UsernamePasswordToken和CasToken;用于存儲oauth2服務端返回的auth code。??
public class OAuth2Token implements AuthenticationToken {
private String authCode;
private String principal;
public OAuth2Token(String authCode) {
this.authCode = authCode;
}
//省略getter/setter
}?
??
OAuth2AuthenticationFilter
該filter的作用類似于FormAuthenticationFilter用于oauth2客戶端的身份驗證控制;如果當前用戶還沒有身份驗證,首先會判斷url中是否有code(服務端返回的auth code),如果沒有則重定向到服務端進行登錄并授權,然后返回auth code;接著OAuth2AuthenticationFilter會用auth code創建OAuth2Token,然后提交給Subject.login進行登錄;接著OAuth2Realm會根據OAuth2Token進行相應的登錄邏輯。? ?
public class OAuth2AuthenticationFilter extends AuthenticatingFilter {
//oauth2 authc code參數名
private String authcCodeParam = "code";
//客戶端id
private String clientId;
//服務器端登錄成功/失敗后重定向到的客戶端地址
private String redirectUrl;
//oauth2服務器響應類型
private String responseType = "code";
private String failureUrl;
//省略setter
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String code = httpRequest.getParameter(authcCodeParam);
return new OAuth2Token(code);
}
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return false;
}
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
String error = request.getParameter("error");
String errorDescription = request.getParameter("error_description");
if(!StringUtils.isEmpty(error)) {//如果服務端返回了錯誤
WebUtils.issueRedirect(request, response, failureUrl + "?error=" + error + "error_description=" + errorDescription);
return false;
}
Subject subject = getSubject(request, response);
if(!subject.isAuthenticated()) {
if(StringUtils.isEmpty(request.getParameter(authcCodeParam))) {
//如果用戶沒有身份驗證,且沒有auth code,則重定向到服務端授權
saveRequestAndRedirectToLogin(request, response);
return false;
}
}
//執行父類里的登錄邏輯,調用Subject.login登錄
return executeLogin(request, response);
}
//登錄成功后的回調方法 重定向到成功頁面
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
issueSuccessRedirect(request, response);
return false;
}
//登錄失敗后的回調
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException ae, ServletRequest request,
ServletResponse response) {
Subject subject = getSubject(request, response);
if (subject.isAuthenticated() || subject.isRemembered()) {
try { //如果身份驗證成功了 則也重定向到成功頁面
issueSuccessRedirect(request, response);
} catch (Exception e) {
e.printStackTrace();
}
} else {
try { //登錄失敗時重定向到失敗頁面
WebUtils.issueRedirect(request, response, failureUrl);
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
}?
該攔截器的作用:
1 、首先判斷有沒有服務端返回的 error 參數,如果有則直接重定向到失敗頁面;
2 、接著如果用戶還沒有身份驗證,判斷是否有 auth code 參數(即是不是服務端授權之后返回的),如果沒有則重定向到服務端進行授權;
3 、否則調用 executeLogin 進行登錄,通過 auth code 創建 OAuth2Token 提交給 Subject 進行登錄;
4 、登錄成功將回調 onLoginSuccess 方法重定向到成功頁面;
5 、登錄失敗則回調 onLoginFailure 重定向到失敗頁面。
?
OAuth2Realm ??
public class OAuth2Realm extends AuthorizingRealm {
private String clientId;
private String clientSecret;
private String accessTokenUrl;
private String userInfoUrl;
private String redirectUrl;
//省略setter
public boolean supports(AuthenticationToken token) {
return token instanceof OAuth2Token; //表示此Realm只支持OAuth2Token類型
}
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
return authorizationInfo;
}
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
OAuth2Token oAuth2Token = (OAuth2Token) token;
String code = oAuth2Token.getAuthCode(); //獲取 auth code
String username = extractUsername(code); // 提取用戶名
SimpleAuthenticationInfo authenticationInfo =
new SimpleAuthenticationInfo(username, code, getName());
return authenticationInfo;
}
private String extractUsername(String code) {
try {
OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient());
OAuthClientRequest accessTokenRequest = OAuthClientRequest
.tokenLocation(accessTokenUrl)
.setGrantType(GrantType.AUTHORIZATION_CODE)
.setClientId(clientId).setClientSecret(clientSecret)
.setCode(code).setRedirectURI(redirectUrl)
.buildQueryMessage();
//獲取access token
OAuthAccessTokenResponse oAuthResponse =
oAuthClient.accessToken(accessTokenRequest, OAuth.HttpMethod.POST);
String accessToken = oAuthResponse.getAccessToken();
Long expiresIn = oAuthResponse.getExpiresIn();
//獲取user info
OAuthClientRequest userInfoRequest =
new OAuthBearerClientRequest(userInfoUrl)
.setAccessToken(accessToken).buildQueryMessage();
OAuthResourceResponse resourceResponse = oAuthClient.resource(
userInfoRequest, OAuth.HttpMethod.GET, OAuthResourceResponse.class);
String username = resourceResponse.getBody();
return username;
} catch (Exception e) {
throw new OAuth2AuthenticationException(e);
}
}
}
此 Realm 首先只支持 OAuth2Token 類型的 Token ;然后通過傳入的 auth code 去換取 access token ;再根據 access token 去獲取用戶信息(用戶名),然后根據此信息創建 AuthenticationInfo ;如果需要 AuthorizationInfo 信息,可以根據此處獲取的用戶名再根據自己的業務規則去獲取。
?
Spring shiro 配置( spring-config-shiro.xml )? ?
<bean id="oAuth2Realm"
class="com.github.zhangkaitao.shiro.chapter18.oauth2.OAuth2Realm">
<property name="cachingEnabled" value="true"/>
<property name="authenticationCachingEnabled" value="true"/>
<property name="authenticationCacheName" value="authenticationCache"/>
<property name="authorizationCachingEnabled" value="true"/>
<property name="authorizationCacheName" value="authorizationCache"/>
<property name="clientId" value="c1ebe466-1cdc-4bd3-ab69-77c3561b9dee"/>
<property name="clientSecret" value="d8346ea2-6017-43ed-ad68-19c0f971738b"/>
<property name="accessTokenUrl"
value="http://localhost:8080/chapter17-server/accessToken"/>
<property name="userInfoUrl" value="http://localhost:8080/chapter17-server/userInfo"/>
<property name="redirectUrl" value="http://localhost:9080/chapter17-client/oauth2-login"/>
</bean>?
此 OAuth2Realm 需要配置在服務端申請的 clientId 和 clientSecret ;及用于根據 auth code 換取 access token 的 accessTokenUrl 地址;及用于根據 access token 換取用戶信息(受保護資源)的 userInfoUrl 地址。 ?
?
<bean id="oAuth2AuthenticationFilter"
class="com.github.zhangkaitao.shiro.chapter18.oauth2.OAuth2AuthenticationFilter">
<property name="authcCodeParam" value="code"/>
<property name="failureUrl" value="/oauth2Failure.jsp"/>
</bean>?
此OAuth2AuthenticationFilter用于攔截服務端重定向回來的auth code。 ?
?
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="http://localhost:8080/chapter17-server/authorize?client_id=c1ebe466-1cdc-4bd3-ab69-77c3561b9dee&response_type=code&redirect_uri=http://localhost:9080/chapter17-client/oauth2-login"/>
<property name="successUrl" value="/"/>
<property name="filters">
<util:map>
<entry key="oauth2Authc" value-ref="oAuth2AuthenticationFilter"/>
</util:map>
</property>
<property name="filterChainDefinitions">
<value>
/ = anon
/oauth2Failure.jsp = anon
/oauth2-login = oauth2Authc
/logout = logout
/** = user
</value>
</property>
</bean>
此處設置 loginUrl 為 http://localhost:8080/chapter17-server/authorize
?client_id=c1ebe466-1cdc-4bd3-ab69-77c3561b9dee&response_type=code&redirect_uri=http://localhost:9080/chapter17-client/oauth2-login" ;其會自動設置到所有的 AccessControlFilter ,如 oAuth2AuthenticationFilter ;另外 /oauth2-login = oauth2Authc 表示 /oauth2-login 地址使用 oauth2Authc 攔截器攔截并進行 oauth2 客戶端授權。
?
測試
1 、首先訪問 http://localhost:9080/chapter17-client/ ,然后點擊登錄按鈕進行登錄,會跳到如下頁面:?
?
2 、輸入用戶名進行登錄并授權;
3 、如果登錄成功,服務端會重定向到客戶端,即之前客戶端提供的地址 http://localhost:9080/chapter17-client/oauth2-login?code=473d56015bcf576f2ca03eac1a5bcc11 ,并帶著 auth code 過去;
4 、客戶端的 OAuth2AuthenticationFilter 會收集此 auth code ,并創建 OAuth2Token 提交給 Subject 進行客戶端登錄;
5 、客戶端的 Subject 會委托給 OAuth2Realm 進行身份驗證;此時 OAuth2Realm 會根據 auth code 換取 access token ,再根據 access token 獲取受保護的用戶信息;然后進行客戶端登錄。
?
到此 OAuth2 的集成就完成了,此處的服務端和客戶端相對比較簡單,沒有進行一些異常檢測,請參考如新浪微博進行相應 API 及異常錯誤碼的設計。???
? ??
?
?
示例源代碼: https://github.com/zhangkaitao/shiro-example ;可加群 231889722?探討Spring/Shiro技術。
? ? ? ??
??
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061
微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元

