Shiro OAuth2

2020-12-04 09:46 更新

OAuth2 集成

目前很多開放平臺如新浪微博開放平臺都在使用提供開放 API 接口供開發(fā)者使用,隨之帶來了第三方應(yīng)用要到開放平臺進(jìn)行授權(quán)的問題,OAuth 就是干這個的,OAuth2 是 OAuth 協(xié)議的下一個版本,相比 OAuth1,OAuth2 整個授權(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ù)資源的一個實(shí)體,可以是一個人,那我們稱之為最終用戶;如新浪微博用戶 zhangsan;
資源服務(wù)器(resource server):存儲受保護(hù)資源,客戶端通過 access token 請求資源,資源服務(wù)器響應(yīng)受保護(hù)資源給客戶端;存儲著用戶 zhangsan 的微博等信息。
授權(quán)服務(wù)器(authorization server):成功驗(yàn)證資源擁有者并獲取授權(quán)之后,授權(quán)服務(wù)器頒發(fā)授權(quán)令牌(Access Token)給客戶端。
客戶端(client):如新浪微博客戶端 weico、微格等第三方應(yīng)用,也可以是它自己的官方應(yīng)用;其本身不存儲資源,而是資源擁有者授權(quán)通過后,使用它的授權(quán)(授權(quán)令牌)訪問受保護(hù)資源,然后客戶端把相應(yīng)的數(shù)據(jù)展示出來 / 提交到服務(wù)器?!翱蛻舳恕?術(shù)語不代表任何特定實(shí)現(xiàn)(如應(yīng)用運(yùn)行在一臺服務(wù)器、桌面、手機(jī)或其他設(shè)備)。

  1. 客戶端從資源擁有者那請求授權(quán)。授權(quán)請求可以直接發(fā)給資源擁有者,或間接的通過授權(quán)服務(wù)器這種中介,后者更可取。
  2. 客戶端收到一個授權(quán)許可,代表資源服務(wù)器提供的授權(quán)。
  3. 客戶端使用它自己的私有證書及授權(quán)許可到授權(quán)服務(wù)器驗(yàn)證。
  4. 如果驗(yàn)證成功,則下發(fā)一個訪問令牌。
  5. 客戶端使用訪問令牌向資源服務(wù)器請求受保護(hù)資源。
  6. 資源服務(wù)器會驗(yàn)證訪問令牌的有效性,如果成功則下發(fā)受保護(hù)資源。

更多流程的解釋請參考 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>

其他的請參考 pom.xml。

數(shù)據(jù)字典

用戶 (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

用戶表存儲著認(rèn)證 / 資源服務(wù)器的用戶信息,即資源擁有者;比如用戶名 / 密碼;客戶端表存儲客戶端的的客戶端 id 及客戶端安全 key;在進(jìn)行授權(quán)時使用。

表及數(shù)據(jù) SQL

具體請參考

  • sql/ shiro-schema.sql (表結(jié)構(gòu))
  • sql/ shiro-data.sql (初始數(shù)據(jù))

默認(rèn)用戶名 / 密碼是 admin/123456。

實(shí)體

具體請參考 com.github.zhangkaitao.shiro.chapter17.entity 包下的實(shí)體,此處就不列舉了。

DAO

具體請參考 com.github.zhangkaitao.shiro.chapter17.dao 包下的 DAO 接口及實(shí)現(xiàn)。

Service

具體請參考 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 過期時間
   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ù)控制器

具體請參考 com.github.zhangkaitao.shiro.chapter17.web.controller 包下的 IndexController、LoginController、UserController 和 ClientController,其用于維護(hù)后端的數(shù)據(jù),如用戶及客戶端數(shù)據(jù);即相當(dāng)于后臺管理。

授權(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)請求
      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)) {//登錄失敗時跳轉(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) {
      //出錯處理
      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 訪問授權(quán)頁面;
  2. 該控制器首先檢查 clientId 是否正確;如果錯誤將返回相應(yīng)的錯誤信息;
  3. 然后判斷用戶是否登錄了,如果沒有登錄首先到登錄頁面登錄;
  4. 登錄成功后生成相應(yīng)的 auth code 即授權(quán)碼,然后重定向到客戶端地址,如 http://localhost:9080/chapter17-client/oauth2-login?code=52b1832f5dff68122f4f00ae995da0ed;在重定向到的地址中會帶上 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請求
      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("錯誤的授權(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)建錯誤響應(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. 該控制器會驗(yàn)證 client_id、client_secret、auth code 的正確性,如果錯誤會返回相應(yīng)的錯誤;
  3. 如果驗(yàn)證通過會生成并返回相應(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資源請求
      OAuthAccessResourceRequest oauthRequest = 
            new OAuthAccessResourceRequest(request, ParameterStyle.QUERY);
      //獲取Access Token
      String accessToken = oauthRequest.getAccessToken();
      //驗(yàn)證Access Token
      if (!oAuthService.checkAccessToken(accessToken)) {
        // 如果不存在/過期了,返回未驗(yàn)證錯誤,需重新驗(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è)置了錯誤碼
      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. 該控制器會驗(yàn)證 access token 的有效性;如果無效了將返回相應(yīng)的錯誤,客戶端再重新進(jìn)行授權(quán);
  3. 如果有效,則返回當(dāng)前登錄用戶的用戶名。

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 都是匿名可訪問的。

其他源碼請直接下載文檔查看。

服務(wù)器維護(hù)

訪問 localhost:8080/chapter17-server/,登錄后進(jìn)行客戶端管理和用戶管理。
客戶端管理就是進(jìn)行客戶端的注冊,如新浪微博的第三方應(yīng)用就需要到新浪微博開發(fā)平臺進(jìn)行注冊;用戶管理就是進(jìn)行如新浪微博用戶的管理。

對于授權(quán)服務(wù)和資源服務(wù)的實(shí)現(xiàn)可以參考新浪微博開發(fā)平臺的實(shí)現(xiàn):

客戶端

客戶端流程:如果需要登錄首先跳到 oauth2 服務(wù)端進(jìn)行登錄授權(quán),成功后服務(wù)端返回 auth code,然后客戶端使用 auth code 去服務(wù)器端換取 access token,最好根據(jù) access token 獲取用戶信息進(jìn)行客戶端的登錄綁定。這個可以參照如很多網(wǎng)站的新浪微博登錄功能,或其他的第三方帳號登錄功能。

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>

其他的請參考 pom.xml。

OAuth2Token

類似于 UsernamePasswordToken 和 CasToken;用于存儲 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)證,首先會判斷 url 中是否有 code(服務(wù)端返回的 auth code),如果沒有則重定向到服務(wù)端進(jìn)行登錄并授權(quán),然后返回 auth code;接著 OAuth2AuthenticationFilter 會用 auth code 創(chuàng)建 OAuth2Token,然后提交給 Subject.login 進(jìn)行登錄;接著 OAuth2Realm 會根據(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ù)端返回了錯誤
            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 { //登錄失敗時重定向到失敗頁面
                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ù)端申請的 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&amp;response_type=code&amp;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&amp;response_type=code&amp;redirect_uri=http://localhost:9080/chapter17-client/oauth2-login";其會自動設(shè)置到所有的 AccessControlFilter,如 oAuth2AuthenticationFilter;另外 /oauth2-login = oauth2Authc 表示 /oauth2-login 地址使用 oauth2Authc 攔截器攔截并進(jìn)行 oauth2 客戶端授權(quán)。

測試

1、首先訪問 http://localhost:9080/chapter17-client/,然后點(diǎn)擊登錄按鈕進(jìn)行登錄,會跳到如下頁面:

2、輸入用戶名進(jìn)行登錄并授權(quán);

3、如果登錄成功,服務(wù)端會重定向到客戶端,即之前客戶端提供的地址 http://localhost:9080/chapter17-client/oauth2-login?code=473d56015bcf576f2ca03eac1a5bcc11,并帶著 auth code 過去;

4、客戶端的 OAuth2AuthenticationFilter 會收集此 auth code,并創(chuàng)建 OAuth2Token 提交給 Subject 進(jìn)行客戶端登錄;

5、客戶端的 Subject 會委托給 OAuth2Realm 進(jìn)行身份驗(yàn)證;此時 OAuth2Realm 會根據(jù) auth code 換取 access token,再根據(jù) access token 獲取受保護(hù)的用戶信息;然后進(jìn)行客戶端登錄。

到此 OAuth2 的集成就完成了,此處的服務(wù)端和客戶端相對比較簡單,沒有進(jìn)行一些異常檢測,請參考如新浪微博進(jìn)行相應(yīng) API 及異常錯誤碼的設(shè)計(jì)。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號