目錄貼: 跟我學(xué)Shiro目錄貼
目前很多開放平臺(tái)如新浪微博開放平臺(tái)都在使用提供開放API接口供開發(fā)者使用,隨之帶來了第三方應(yīng)用要到開放平臺(tái)進(jìn)行授權(quán)的問題,OAuth就是干這個(gè)的,OAuth2是OAuth協(xié)議的下一個(gè)版本,相比OAuth1,OAuth2整個(gè)授權(quán)流程更簡單安全了,但不兼容OAuth1,具體可以到OAuth2官網(wǎng)http://oauth.net/2/查看,OAuth2協(xié)議規(guī)范可以參考http://tools.ietf.org/html/rfc6749。目前有好多參考實(shí)現(xiàn)供選擇,可以到其官網(wǎng)查看下載。
本文使用Apache Oltu,其之前的名字叫Apache Amber ,是Java版的參考實(shí)現(xiàn)。使用文檔可參考https://cwiki.apache.org/confluence/display/OLTU/Documentation。
OAuth角色
資源擁有者(resource owner):能授權(quán)訪問受保護(hù)資源的一個(gè)實(shí)體,可以是一個(gè)人,那我們稱之為最終用戶;如新浪微博用戶zhangsan;
資源服務(wù)器(resource server):存儲(chǔ)受保護(hù)資源,客戶端通過access token請(qǐng)求資源,資源服務(wù)器響應(yīng)受保護(hù)資源給客戶端;存儲(chǔ)著用戶zhangsan的微博等信息。
授權(quán)服務(wù)器(authorization server):成功驗(yàn)證資源擁有者并獲取授權(quán)之后,授權(quán)服務(wù)器頒發(fā)授權(quán)令牌(Access Token)給客戶端。
客戶端(client):如新浪微博客戶端weico、微格等第三方應(yīng)用,也可以是它自己的官方應(yīng)用;其本身不存儲(chǔ)資源,而是資源擁有者授權(quán)通過后,使用它的授權(quán)(授權(quán)令牌)訪問受保護(hù)資源,然后客戶端把相應(yīng)的數(shù)據(jù)展示出來/提交到服務(wù)器?!翱蛻舳恕毙g(shù)語不代表任何特定實(shí)現(xiàn)(如應(yīng)用運(yùn)行在一臺(tái)服務(wù)器、桌面、手機(jī)或其他設(shè)備)。
OAuth2協(xié)議流程
1、客戶端從資源擁有者那請(qǐng)求授權(quán)。授權(quán)請(qǐng)求可以直接發(fā)給資源擁有者,或間接的通過授權(quán)服務(wù)器這種中介,后者更可取。
2、客戶端收到一個(gè)授權(quán)許可,代表資源服務(wù)器提供的授權(quán)。
3、客戶端使用它自己的私有證書及授權(quán)許可到授權(quán)服務(wù)器驗(yàn)證。
4、如果驗(yàn)證成功,則下發(fā)一個(gè)訪問令牌。
5、客戶端使用訪問令牌向資源服務(wù)器請(qǐng)求受保護(hù)資源。
6、資源服務(wù)器會(huì)驗(yàn)證訪問令牌的有效性,如果成功則下發(fā)受保護(hù)資源。
更多流程的解釋請(qǐng)參考OAuth2的協(xié)議規(guī)范http://tools.ietf.org/html/rfc6749。
服務(wù)器端
本文把授權(quán)服務(wù)器和資源服務(wù)器整合在一起實(shí)現(xiàn)。
POM依賴
此處我們使用apache oltu oauth2服務(wù)端實(shí)現(xiàn),需要引入authzserver(授權(quán)服務(wù)器依賴)和resourceserver(資源服務(wù)器依賴)。
- <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>
其他的請(qǐng)參考pom.xml。
數(shù)據(jù)字典
用戶(oauth2_user)
名稱
|
類型
|
長度
|
描述
|
id
|
bigint
|
10
|
編號(hào) 主鍵
|
username
|
varchar
|
100
|
用戶名
|
password
|
varchar
|
100
|
密碼
|
salt
|
varchar
|
50
|
鹽
|
客戶端(oauth2_client)
名稱
|
類型
|
長度
|
描述
|
id
|
bigint
|
10
|
編號(hào) 主鍵
|
client_name
|
varchar
|
100
|
客戶端名稱
|
client_id
|
varchar
|
100
|
客戶端id
|
client_secret
|
varchar
|
100
|
客戶端安全key
|
用戶表存儲(chǔ)著認(rèn)證/資源服務(wù)器的用戶信息,即資源擁有者;比如用戶名/密碼;客戶端表存儲(chǔ)客戶端的的客戶端id及客戶端安全key;在進(jìn)行授權(quán)時(shí)使用。
表及數(shù)據(jù)SQL
具體請(qǐng)參考
sql/ shiro-schema.sql (表結(jié)構(gòu))
sql/ shiro-data.sql (初始數(shù)據(jù))
默認(rèn)用戶名/密碼是admin/123456。
實(shí)體
具體請(qǐng)參考com.github.zhangkaitao.shiro.chapter17.entity包下的實(shí)體,此處就不列舉了。
DAO
具體請(qǐng)參考com.github.zhangkaitao.shiro.chapter17.dao包下的DAO接口及實(shí)現(xiàn)。
Service
具體請(qǐng)參考com.github.zhangkaitao.shiro.chapter17.service包下的Service接口及實(shí)現(xiàn)。以下是出了基本CRUD之外的關(guān)鍵接口:
- public interface UserService {
- public User createUser(User user);// 創(chuàng)建用戶
- public User updateUser(User user);// 更新用戶
- public void deleteUser(Long userId);// 刪除用戶
- public void changePassword(Long userId, String newPassword); //修改密碼
- User findOne(Long userId);// 根據(jù)id查找用戶
- List<User> findAll();// 得到所有用戶
- public User findByUsername(String username);// 根據(jù)用戶名查找用戶
- }
- public interface ClientService {
- public Client createClient(Client client);// 創(chuàng)建客戶端
- public Client updateClient(Client client);// 更新客戶端
- public void deleteClient(Long clientId);// 刪除客戶端
- Client findOne(Long clientId);// 根據(jù)id查找客戶端
- List<Client> findAll();// 查找所有
- Client findByClientId(String clientId);// 根據(jù)客戶端id查找客戶端
- Client findByClientSecret(String clientSecret);//根據(jù)客戶端安全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); // 驗(yàn)證auth code是否有效
- boolean checkAccessToken(String accessToken); // 驗(yàn)證access token是否有效
- String getUsernameByAuthCode(String authCode);// 根據(jù)auth code獲取用戶名
- String getUsernameByAccessToken(String accessToken);// 根據(jù)access token獲取用戶名
- long getExpireIn();//auth code / access token 過期時(shí)間
- public boolean checkClientId(String clientId);// 檢查客戶端id是否存在
- public boolean checkClientSecret(String clientSecret);// 堅(jiān)持客戶端安全KEY是否存在
- }
此處通過OAuthService實(shí)現(xiàn)進(jìn)行auth code和access token的維護(hù)。
后端數(shù)據(jù)維護(hù)控制器
具體請(qǐng)參考com.github.zhangkaitao.shiro.chapter17.web.controller包下的IndexController、LoginController、UserController和ClientController,其用于維護(hù)后端的數(shù)據(jù),如用戶及客戶端數(shù)據(jù);即相當(dāng)于后臺(tái)管理。
授權(quán)控制器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 {
- //構(gòu)建OAuth 授權(quán)請(qǐng)求
- 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();
- //如果用戶沒有登錄,跳轉(zhuǎn)到登陸頁面
- if(!subject.isAuthenticated()) {
- if(!login(subject, request)) {//登錄失敗時(shí)跳轉(zhuǎn)到登陸頁面
- model.addAttribute("client",
- clientService.findByClientId(oauthRequest.getClientId()));
- return "oauth2login";
- }
- }
-
- String username = (String)subject.getPrincipal();
- //生成授權(quán)碼
- 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);
- }
- //進(jìn)行OAuth響應(yīng)構(gòu)建
- OAuthASResponse.OAuthAuthorizationResponseBuilder builder =
- OAuthASResponse.authorizationResponse(request,
- HttpServletResponse.SC_FOUND);
- //設(shè)置授權(quán)碼
- builder.setCode(authorizationCode);
- //得到到客戶端重定向地址
- String redirectURI = oauthRequest.getParam(OAuth.OAUTH_REDIRECT_URI);
-
- //構(gòu)建響應(yīng)
- final OAuthResponse response = builder.location(redirectURI).buildQueryMessage();
- //根據(jù)OAuthResponse返回ResponseEntity響應(yīng)
- HttpHeaders headers = new HttpHeaders();
- headers.setLocation(new URI(response.getLocationUri()));
- return new ResponseEntity(headers, HttpStatus.valueOf(response.getResponseStatus()));
- } catch (OAuthProblemException e) {
- //出錯(cuò)處理
- String redirectUri = e.getRedirectUri();
- if (OAuthUtils.isEmpty(redirectUri)) {
- //告訴客戶端沒有傳入redirectUri直接報(bào)錯(cuò)
- return new ResponseEntity(
- "OAuth callback url needs to be provided by client!!!", HttpStatus.NOT_FOUND);
- }
- //返回錯(cuò)誤消息(如?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訪問授權(quán)頁面;
2、該控制器首先檢查clientId是否正確;如果錯(cuò)誤將返回相應(yīng)的錯(cuò)誤信息;
3、然后判斷用戶是否登錄了,如果沒有登錄首先到登錄頁面登錄;
4、登錄成功后生成相應(yīng)的auth code即授權(quán)碼,然后重定向到客戶端地址,如http://localhost:9080/chapter17-client/oauth2-login?code=52b1832f5dff68122f4f00ae995da0ed;在重定向到的地址中會(huì)帶上code參數(shù)(授權(quán)碼),接著客戶端可以根據(jù)授權(quán)碼去換取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 {
- //構(gòu)建OAuth請(qǐng)求
- 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);
- // 檢查驗(yàn)證類型,此處只檢查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("錯(cuò)誤的授權(quán)碼")
- .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響應(yīng)
- OAuthResponse response = OAuthASResponse
- .tokenResponse(HttpServletResponse.SC_OK)
- .setAccessToken(accessToken)
- .setExpiresIn(String.valueOf(oAuthService.getExpireIn()))
- .buildJSONMessage();
-
- //根據(jù)OAuthResponse生成ResponseEntity
- return new ResponseEntity(
- response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
- } catch (OAuthProblemException e) {
- //構(gòu)建錯(cuò)誤響應(yīng)
- 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提交如下數(shù)據(jù):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、該控制器會(huì)驗(yàn)證client_id、client_secret、auth code的正確性,如果錯(cuò)誤會(huì)返回相應(yīng)的錯(cuò)誤;
3、如果驗(yàn)證通過會(huì)生成并返回相應(yīng)的訪問令牌access token。
資源控制器UserInfoController
- @RestController
- public class UserInfoController {
- @Autowired
- private OAuthService oAuthService;
-
- @RequestMapping("/userInfo")
- public HttpEntity userInfo(HttpServletRequest request) throws OAuthSystemException {
- try {
- //構(gòu)建OAuth資源請(qǐng)求
- OAuthAccessResourceRequest oauthRequest =
- new OAuthAccessResourceRequest(request, ParameterStyle.QUERY);
- //獲取Access Token
- String accessToken = oauthRequest.getAccessToken();
-
- //驗(yàn)證Access Token
- if (!oAuthService.checkAccessToken(accessToken)) {
- // 如果不存在/過期了,返回未驗(yàn)證錯(cuò)誤,需重新驗(yàn)證
- 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) {
- //檢查是否設(shè)置了錯(cuò)誤碼
- 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進(jìn)行訪問;
2、該控制器會(huì)驗(yàn)證access token的有效性;如果無效了將返回相應(yīng)的錯(cuò)誤,客戶端再重新進(jìn)行授權(quán);
3、如果有效,則返回當(dāng)前登錄用戶的用戶名。
Spring配置文件
具體請(qǐng)參考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>
對(duì)于oauth2的幾個(gè)地址/authorize、/accessToken、/userInfo都是匿名可訪問的。
其他源碼請(qǐng)直接下載文檔查看。
服務(wù)器維護(hù)
訪問localhost:8080/chapter17-server/,登錄后進(jìn)行客戶端管理和用戶管理。
客戶端管理就是進(jìn)行客戶端的注冊(cè),如新浪微博的第三方應(yīng)用就需要到新浪微博開發(fā)平臺(tái)進(jìn)行注冊(cè);用戶管理就是進(jìn)行如新浪微博用戶的管理。
對(duì)于授權(quán)服務(wù)和資源服務(wù)的實(shí)現(xiàn)可以參考新浪微博開發(fā)平臺(tái)的實(shí)現(xiàn):
http://open.weibo.com/wiki/授權(quán)機(jī)制說明
http://open.weibo.com/wiki/微博API
客戶端
客戶端流程:如果需要登錄首先跳到oauth2服務(wù)端進(jìn)行登錄授權(quán),成功后服務(wù)端返回auth code,然后客戶端使用auth code去服務(wù)器端換取access token,最好根據(jù)access token獲取用戶信息進(jìn)行客戶端的登錄綁定。這個(gè)可以參照如很多網(wǎng)站的新浪微博登錄功能,或其他的第三方賬號(hào)登錄功能。
POM依賴
此處我們使用apache oltu oauth2客戶端實(shí)現(xiàn)。
- <dependency>
- <groupId>org.apache.oltu.oauth2</groupId>
- <artifactId>org.apache.oltu.oauth2.client</artifactId>
- <version>0.31</version>
- </dependency>
其他的請(qǐng)參考pom.xml。
OAuth2Token
類似于UsernamePasswordToken和CasToken;用于存儲(chǔ)oauth2服務(wù)端返回的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客戶端的身份驗(yàn)證控制;如果當(dāng)前用戶還沒有身份驗(yàn)證,首先會(huì)判斷url中是否有code(服務(wù)端返回的auth code),如果沒有則重定向到服務(wù)端進(jìn)行登錄并授權(quán),然后返回auth code;接著OAuth2AuthenticationFilter會(huì)用auth code創(chuàng)建OAuth2Token,然后提交給Subject.login進(jìn)行登錄;接著OAuth2Realm會(huì)根據(jù)OAuth2Token進(jìn)行相應(yīng)的登錄邏輯。
- public class OAuth2AuthenticationFilter extends AuthenticatingFilter {
- //oauth2 authc code參數(shù)名
- private String authcCodeParam = "code";
- //客戶端id
- private String clientId;
- //服務(wù)器端登錄成功/失敗后重定向到的客戶端地址
- private String redirectUrl;
- //oauth2服務(wù)器響應(yīng)類型
- 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)) {//如果服務(wù)端返回了錯(cuò)誤
- 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))) {
- //如果用戶沒有身份驗(yàn)證,且沒有auth code,則重定向到服務(wù)端授權(quán)
- saveRequestAndRedirectToLogin(request, response);
- return false;
- }
- }
- //執(zhí)行父類里的登錄邏輯,調(diào)用Subject.login登錄
- return executeLogin(request, response);
- }
-
- //登錄成功后的回調(diào)方法 重定向到成功頁面
- protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
- issueSuccessRedirect(request, response);
- return false;
- }
-
- //登錄失敗后的回調(diào)
- protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException ae, ServletRequest request,
- ServletResponse response) {
- Subject subject = getSubject(request, response);
- if (subject.isAuthenticated() || subject.isRemembered()) {
- try { //如果身份驗(yàn)證成功了 則也重定向到成功頁面
- issueSuccessRedirect(request, response);
- } catch (Exception e) {
- e.printStackTrace();
- }
- } else {
- try { //登錄失敗時(shí)重定向到失敗頁面
- WebUtils.issueRedirect(request, response, failureUrl);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- return false;
- }
- }
該攔截器的作用:
1、首先判斷有沒有服務(wù)端返回的error參數(shù),如果有則直接重定向到失敗頁面;
2、接著如果用戶還沒有身份驗(yàn)證,判斷是否有auth code參數(shù)(即是不是服務(wù)端授權(quán)之后返回的),如果沒有則重定向到服務(wù)端進(jìn)行授權(quán);
3、否則調(diào)用executeLogin進(jìn)行登錄,通過auth code創(chuàng)建OAuth2Token提交給Subject進(jìn)行登錄;
4、登錄成功將回調(diào)onLoginSuccess方法重定向到成功頁面;
5、登錄失敗則回調(diào)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;再根據(jù)access token去獲取用戶信息(用戶名),然后根據(jù)此信息創(chuàng)建AuthenticationInfo;如果需要AuthorizationInfo信息,可以根據(jù)此處獲取的用戶名再根據(jù)自己的業(yè)務(wù)規(guī)則去獲取。
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需要配置在服務(wù)端申請(qǐng)的clientId和clientSecret;及用于根據(jù)auth code換取access token的accessTokenUrl地址;及用于根據(jù)access token換取用戶信息(受保護(hù)資源)的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用于攔截服務(wù)端重定向回來的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>
此處設(shè)置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";其會(huì)自動(dòng)設(shè)置到所有的AccessControlFilter,如oAuth2AuthenticationFilter;另外/oauth2-login = oauth2Authc表示/oauth2-login地址使用oauth2Authc攔截器攔截并進(jìn)行oauth2客戶端授權(quán)。
測試
1、首先訪問http://localhost:9080/chapter17-client/,然后點(diǎn)擊登錄按鈕進(jìn)行登錄,會(huì)跳到如下頁面:
2、輸入用戶名進(jìn)行登錄并授權(quán);
3、如果登錄成功,服務(wù)端會(huì)重定向到客戶端,即之前客戶端提供的地址http://localhost:9080/chapter17-client/oauth2-login?code=473d56015bcf576f2ca03eac1a5bcc11,并帶著auth code過去;
4、客戶端的OAuth2AuthenticationFilter會(huì)收集此auth code,并創(chuàng)建OAuth2Token提交給Subject進(jìn)行客戶端登錄;
5、客戶端的Subject會(huì)委托給OAuth2Realm進(jìn)行身份驗(yàn)證;此時(shí)OAuth2Realm會(huì)根據(jù)auth code換取access token,再根據(jù)access token獲取受保護(hù)的用戶信息;然后進(jìn)行客戶端登錄。
到此OAuth2的集成就完成了,此處的服務(wù)端和客戶端相對(duì)比較簡單,沒有進(jìn)行一些異常檢測,請(qǐng)參考如新浪微博進(jìn)行相應(yīng)API及異常錯(cuò)誤碼的設(shè)計(jì)。