OAuth2.0授權服務端從設計到實現

轉自:在此感謝原創作者:https://www.zifangsky.cn/1313.html 博客

OAuth2.0協議入門(一):OAuth2.0協議的基本概念以及使用授權碼模式(authorization code)實現百度賬號登錄https://www.zifangsky.cn/1309.html

OAuth2.0協議入門(二):OAuth2.0授權服務端從設計到實現

https://www.zifangsky.cn/1313.html

數據庫:https://gitee.com/zifangsky/OAuth2.0Demo/blob/master/rbac_db.sql

碼雲地址:https://gitee.com/zifangsky/OAuth2.0Demo/tree/master/ServerDemo

 

一 OAuth2.0授權服務端的設計

上一篇文章中,我介紹了OAuth2.0協議的基本概念以及作爲一個第三方應用在請求授權服務端的時候需要做哪些事情。通過上一篇文章中調用百度OAuth服務的例子我們可以得知,使用授權碼模式完成OAuth2.0授權的過程需要以下三個步驟:

  1. client請求授權服務端,獲取Authorization Code
  2. client通過Authorization Code再次請求授權服務端,獲取Access Token
  3. client通過服務端返回的Access Token獲取用戶的基本信息

因此,OAuth2.0授權服務端的設計也就主要圍繞這幾個接口展開,其主要流程是這樣的:

明白了整個運行流程,那剩下就好辦了。接下來我們需要做的是數據庫的表結構設計。

數據庫的表結構設計

提示:我在下面只介紹一些表的主要字段,這個Demo中使用的完整的表結構可以參考:https://gitee.com/zifangsky/OAuth2.0Demo/blob/master/rbac_db.sql

(1)auth_client_details:

接入的第三方客戶端詳情表。這就跟我們要想使用百度OAuth服務就需要事先在百度開發者中心新建一個應用是一個道理,每個想要接入OAuth2.0授權服務的第三方客戶端都需要事先在服務端這裏“備案”,所以主要需要以下幾個字段:

  • client_id:每個客戶端的client_id是唯一的,通常是一個隨機生成的字符串
  • client_name:客戶端的名稱
  • client_secret:這個祕鑰是客戶端和OAuth2.0服務端共同持有,用於鑑別請求中的身份,通常也是一個隨機生成的字符串

(2)auth_scope:

用戶信息範圍表。OAuth2.0服務端在授權第三方客戶端訪問用戶的信息的時候,通常會把用戶的信息劃分爲幾個級別,比如用戶的基本信息,用戶密碼、購物記錄等高保密性信息。這樣劃分主要是讓用戶自主選擇把自己哪種信息授權給第三方客戶端訪問,所以主要需要以下字段:

  • scope_name:範圍名稱

(3)auth_access_token:

Access Token信息表。這個表主要體現出哪個用戶授予哪個client何種訪問範圍的令牌,以及這個令牌的結束日期是哪天。所以主要需要以下幾個字段:

  • access_token:Access Token字段
  • user_id:表明是哪個用戶授予的權限
  • client_id:表明授予給哪個客戶端
  • expires_in:過期時間戳,表明這個Token在哪一天過期
  • scope:表明可以訪問何種範圍

(4)auth_refresh_token:

Refresh Token信息表。這個表主要用來記錄Refresh Token,在設計表結構的時候需要關聯它對應的auth_access_token表的記錄。所以主要需要以下幾個字段:

  • refresh_token:Refresh Token字段
  • token_id:它對應的auth_access_token表的記錄
  • expires_in:過期時間戳

(5)auth_client_user:

用戶對某個接入客戶端的授權信息表。這個表用於記錄client、scope、用戶之間的關聯關係。所以主要需要以下幾個字段:

  • auth_client_id:授權對應的auth_client_details表的記錄
  • user_id:授權對應的user表的記錄
  • auth_scope_id:授權對應的auth_scope表的記錄

明白了授權的整個流程,以及設計好後面需要用到的表結構,那麼我們最後就剩下具體代碼實現了。

二 OAuth2.0授權服務端主要接口的代碼實現

這個Demo的授權服務端的完整可用源碼可以參考:https://gitee.com/zifangsky/OAuth2.0Demo/tree/master/ServerDemo

(1)客戶端註冊接口:

某個第三方客戶端需要事先在服務端這裏“備案”。在這個Demo中我沒有寫具體的頁面,只提供了一個註冊接口,其中client_id和client_secret都是隨機生成的字符串。

接口地址:http://127.0.0.1:7000/oauth2.0/clientRegister

參數:

 

1

{"clientName":"測試客戶端","redirectUri":"http://localhost:7080/login","description":"這是一個測試客戶端服務"}

(2)授權頁面:

如果用戶之前沒有給請求的client授權過,那麼在第一次請求Authorization Code的時候會打開授權頁面,然後用戶手動選擇是否授權:

實現代碼很簡單,就是在用戶選擇“授權”後,往表auth_client_user插入一條記錄。這裏就不多說了,可以自行參考一下示例源碼。

(3)獲取Authorization Code:

根據請求的client_id和scope生成一個字符串——Authorization Code,同時需要將本次請求的授權範圍和所屬的用戶信息保存到Redis中(因爲後面在請求Access Token的時候是從第三方客戶端的後臺直接請求,屬於一個新的會話,所以需要提前存一下用戶信息)。

接口地址:http://127.0.0.1:7000/oauth2.0/authorize?client_id=7Ugj6XWmTDpyYp8M8njG3hqx&scope=basic&response_type=code&state=AB1357&redirect_uri=http://192.168.197.130:7080/login

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

/**

* 獲取Authorization Code

* @author zifangsky

* @date 2018/8/6 17:40

* @since 1.0.0

* @param request HttpServletRequest

* @return org.springframework.web.servlet.ModelAndView

*/

@RequestMapping("/authorize")

public ModelAndView authorize(HttpServletRequest request){

    HttpSession session = request.getSession();

    User user = (User) session.getAttribute(Constants.SESSION_USER);

 

    //客戶端ID

    String clientIdStr = request.getParameter("client_id");

    //權限範圍

    String scopeStr = request.getParameter("scope");

    //回調URL

    String redirectUri = request.getParameter("redirect_uri");

    //status,用於防止CSRF攻擊(非必填)

    String status = request.getParameter("status");

 

    //生成Authorization Code

    String authorizationCode = authorizationService.createAuthorizationCode(clientIdStr, scopeStr, user);

 

    String params = "?code=" + authorizationCode;

    if(StringUtils.isNoneBlank(status)){

        params = params + "&status=" + status;

    }

 

    return new ModelAndView("redirect:" + redirectUri + params);

}

調用的cn/zifangsky/service/impl/AuthorizationServiceImpl.java類裏面的生成邏輯:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

@Override

public String createAuthorizationCode(String clientIdStr, String scopeStr, User user) {

    //1. 拼裝待加密字符串(clientId + scope + 當前精確到毫秒的時間戳)

    String str = clientIdStr + scopeStr + String.valueOf(DateUtils.currentTimeMillis());

 

    //2. SHA1加密

    String encryptedStr = EncryptUtils.sha1Hex(str);

 

    //3.1 保存本次請求的授權範圍

    redisService.setWithExpire(encryptedStr + ":scope", scopeStr, (ExpireEnum.AUTHORIZATION_CODE.getTime()), ExpireEnum.AUTHORIZATION_CODE.getTimeUnit());

    //3.2 保存本次請求所屬的用戶信息

    redisService.setWithExpire(encryptedStr + ":user", user, (ExpireEnum.AUTHORIZATION_CODE.getTime()), ExpireEnum.AUTHORIZATION_CODE.getTimeUnit());

 

    //4. 返回Authorization Code

    return encryptedStr;

}

 

(4)通過Authorization Code獲取Access Token:

在第三方客戶端拿到Authorization Code後,它就可以在後臺調用生成Token的接口,生成Access Token和Refresh Token:

接口地址:http://127.0.0.1:7000/oauth2.0/token?grant_type=authorization_code&code=82ce2bf34f5028d7e8a517ef381f5c87f0139b26&client_id=7Ugj6XWmTDpyYp8M8njG3hqx&client_secret=tur2rlFfywR9OOP3fB5ZbsLTnNuNabI3&redirect_uri=http://192.168.197.130:7080/login

返回如下:

 

1

2

3

4

5

6

{

    "access_token": "1.6659c9d38f5943f97db334874e5229284cdd1523.2592000.1537600367",

    "refresh_token": "2.b19923a01cf35ccab48ddbd687750408bd1cb763.31536000.1566544316",

    "expires_in": 2592000,

    "scope": "basic"

}

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

/**

* 通過Authorization Code獲取Access Token

* @author zifangsky

* @date 2018/8/18 15:11

* @since 1.0.0

* @param request HttpServletRequest

* @return java.util.Map<java.lang.String,java.lang.Object>

*/

@RequestMapping(value = "/token", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)

@ResponseBody

public Map<String,Object> token(HttpServletRequest request){

    Map<String,Object> result = new HashMap<>(8);

 

    //授權方式

    String grantType = request.getParameter("grant_type");

    //前面獲取的Authorization Code

    String code = request.getParameter("code");

    //客戶端ID

    String clientIdStr = request.getParameter("client_id");

    //接入的客戶端的密鑰

    String clientSecret = request.getParameter("client_secret");

    //回調URL

    String redirectUri = request.getParameter("redirect_uri");

 

    //校驗授權方式

    if(!GrantTypeEnum.AUTHORIZATION_CODE.getType().equals(grantType)){

        this.generateErrorResponse(result, ErrorCodeEnum.UNSUPPORTED_GRANT_TYPE);

        return result;

    }

 

    try {

        AuthClientDetails savedClientDetails = authorizationService.selectClientDetailsByClientId(clientIdStr);

        //校驗請求的客戶端祕鑰和已保存的祕鑰是否匹配

        if(!(savedClientDetails != null && savedClientDetails.getClientSecret().equals(clientSecret))){

            this.generateErrorResponse(result, ErrorCodeEnum.INVALID_CLIENT);

            return result;

        }

 

        //校驗回調URL

        if(!savedClientDetails.getRedirectUri().equals(redirectUri)){

            this.generateErrorResponse(result, ErrorCodeEnum.REDIRECT_URI_MISMATCH);

            return result;

        }

 

        //從Redis獲取允許訪問的用戶權限範圍

        String scope = redisService.get(code + ":scope");

        //從Redis獲取對應的用戶信息

        User user = redisService.get(code + ":user");

 

        //如果能夠通過Authorization Code獲取到對應的用戶信息,則說明該Authorization Code有效

        if(StringUtils.isNoneBlank(scope) && user != null){

            //過期時間

            Long expiresIn = DateUtils.dayToSecond(ExpireEnum.ACCESS_TOKEN.getTime());

 

            //生成Access Token

            String accessTokenStr = authorizationService.createAccessToken(user, savedClientDetails, grantType, scope, expiresIn);

            //查詢已經插入到數據庫的Access Token

            AuthAccessToken authAccessToken = authorizationService.selectByAccessToken(accessTokenStr);

            //生成Refresh Token

            String refreshTokenStr = authorizationService.createRefreshToken(user, authAccessToken);

 

            //返回數據

            result.put("access_token", authAccessToken.getAccessToken());

            result.put("refresh_token", refreshTokenStr);

            result.put("expires_in", expiresIn);

            result.put("scope", scope);

            return result;

        }else{

            this.generateErrorResponse(result, ErrorCodeEnum.INVALID_GRANT);

            return result;

        }

    }catch (Exception e){

        this.generateErrorResponse(result, ErrorCodeEnum.UNKNOWN_ERROR);

        return result;

    }

}

生成邏輯同樣在cn/zifangsky/service/impl/AuthorizationServiceImpl.java這個類裏面,具體如下:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

@Override

public String createAccessToken(User user, AuthClientDetails savedClientDetails, String grantType, String scope, Long expiresIn) {

    Date current = new Date();

    //過期的時間戳

    Long expiresAt = DateUtils.nextDaysSecond(ExpireEnum.ACCESS_TOKEN.getTime(), null);

 

    //1. 拼裝待加密字符串(username + clientId + 當前精確到毫秒的時間戳)

    String str = user.getUsername() + savedClientDetails.getClientId() + String.valueOf(DateUtils.currentTimeMillis());

 

    //2. SHA1加密

    String accessTokenStr = "1." + EncryptUtils.sha1Hex(str) + "." + expiresIn + "." + expiresAt;

 

    //3. 保存Access Token

    AuthAccessToken savedAccessToken = authAccessTokenMapper.selectByUserIdClientIdScope(user.getId()

            , savedClientDetails.getId(), scope);

    //如果存在userId + clientId + scope匹配的記錄,則更新原記錄,否則向數據庫中插入新記錄

    if(savedAccessToken != null){

        savedAccessToken.setAccessToken(accessTokenStr);

        savedAccessToken.setExpiresIn(expiresAt);

        savedAccessToken.setUpdateUser(user.getId());

        savedAccessToken.setUpdateTime(current);

        authAccessTokenMapper.updateByPrimaryKeySelective(savedAccessToken);

    }else{

        savedAccessToken = new AuthAccessToken();

        savedAccessToken.setAccessToken(accessTokenStr);

        savedAccessToken.setUserId(user.getId());

        savedAccessToken.setUserName(user.getUsername());

        savedAccessToken.setClientId(savedClientDetails.getId());

        savedAccessToken.setExpiresIn(expiresAt);

        savedAccessToken.setScope(scope);

        savedAccessToken.setGrantType(grantType);

        savedAccessToken.setCreateUser(user.getId());

        savedAccessToken.setUpdateUser(user.getId());

        savedAccessToken.setCreateTime(current);

        savedAccessToken.setUpdateTime(current);

        authAccessTokenMapper.insertSelective(savedAccessToken);

    }

 

    //4. 返回Access Token

    return accessTokenStr;

}

 

@Override

public String createRefreshToken(User user, AuthAccessToken authAccessToken) {

    Date current = new Date();

    //過期時間

    Long expiresIn = DateUtils.dayToSecond(ExpireEnum.REFRESH_TOKEN.getTime());

    //過期的時間戳

    Long expiresAt = DateUtils.nextDaysSecond(ExpireEnum.REFRESH_TOKEN.getTime(), null);

 

    //1. 拼裝待加密字符串(username + accessToken + 當前精確到毫秒的時間戳)

    String str = user.getUsername() + authAccessToken.getAccessToken() + String.valueOf(DateUtils.currentTimeMillis());

 

    //2. SHA1加密

    String refreshTokenStr = "2." + EncryptUtils.sha1Hex(str) + "." + expiresIn + "." + expiresAt;

 

    //3. 保存Refresh Token

    AuthRefreshToken savedRefreshToken = authRefreshTokenMapper.selectByTokenId(authAccessToken.getId());

    //如果存在tokenId匹配的記錄,則更新原記錄,否則向數據庫中插入新記錄

    if(savedRefreshToken != null){

        savedRefreshToken.setRefreshToken(refreshTokenStr);

        savedRefreshToken.setExpiresIn(expiresAt);

        savedRefreshToken.setUpdateUser(user.getId());

        savedRefreshToken.setUpdateTime(current);

        authRefreshTokenMapper.updateByPrimaryKeySelective(savedRefreshToken);

    }else{

        savedRefreshToken = new AuthRefreshToken();

        savedRefreshToken.setTokenId(authAccessToken.getId());

        savedRefreshToken.setRefreshToken(refreshTokenStr);

        savedRefreshToken.setExpiresIn(expiresAt);

        savedRefreshToken.setCreateUser(user.getId());

        savedRefreshToken.setUpdateUser(user.getId());

        savedRefreshToken.setCreateTime(current);

        savedRefreshToken.setUpdateTime(current);

        authRefreshTokenMapper.insertSelective(savedRefreshToken);

    }

 

    //4. 返回Refresh Token

    return refreshTokenStr;

}

 

(5)通過Refresh Token刷新Access Token:

當第三方客戶端的Access Token失效的時候就可以調用這個接口,重新生成一個新的Access Token:

接口地址:http://127.0.0.1:7000/oauth2.0/refreshToken?refresh_token=2.5c58637a2d51e4470d3e1189978e94da8402785e.31536000.1566283826

返回如下

 

1

2

3

4

5

6

{

    "access_token": "1.adebb0a4522d5dae9eaf94a5af4fec070c4f3dce.2592000.1537508734",

    "refresh_token": "2.5c58637a2d51e4470d3e1189978e94da8402785e.31536000.1566283826",

    "expires_in": 2592000,

    "scope": "basic"

}

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

/**

* 通過Refresh Token刷新Access Token

* @author zifangsky

* @date 2018/8/22 11:11

* @since 1.0.0

* @param request HttpServletRequest

* @return java.util.Map<java.lang.String,java.lang.Object>

*/

@RequestMapping(value = "/refreshToken", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)

@ResponseBody

public Map<String,Object> refreshToken(HttpServletRequest request){

    Map<String,Object> result = new HashMap<>(8);

 

    //獲取Refresh Token

    String refreshTokenStr = request.getParameter("refresh_token");

 

    try {

        AuthRefreshToken authRefreshToken = authorizationService.selectByRefreshToken(refreshTokenStr);

 

        if(authRefreshToken != null) {

            Long savedExpiresAt = authRefreshToken.getExpiresIn();

            //過期日期

            LocalDateTime expiresDateTime = DateUtils.ofEpochSecond(savedExpiresAt, null);

            //當前日期

            LocalDateTime nowDateTime = DateUtils.now();

 

            //如果Refresh Token已經失效,則需要重新生成

            if (expiresDateTime.isBefore(nowDateTime)) {

                this.generateErrorResponse(result, ErrorCodeEnum.EXPIRED_TOKEN);

                return result;

            } else {

                //獲取存儲的Access Token

                AuthAccessToken authAccessToken = authorizationService.selectByAccessId(authRefreshToken.getTokenId());

                //獲取對應的客戶端信息

                AuthClientDetails savedClientDetails = authorizationService.selectClientDetailsById(authAccessToken.getClientId());

                //獲取對應的用戶信息

                User user = userService.selectByUserId(authAccessToken.getUserId());

 

                //新的過期時間

                Long expiresIn = DateUtils.dayToSecond(ExpireEnum.ACCESS_TOKEN.getTime());

                //生成新的Access Token

                String newAccessTokenStr = authorizationService.createAccessToken(user, savedClientDetails

                        , authAccessToken.getGrantType(), authAccessToken.getScope(), expiresIn);

 

                //返回數據

                result.put("access_token", newAccessTokenStr);

                result.put("refresh_token", refreshTokenStr);

                result.put("expires_in", expiresIn);

                result.put("scope", authAccessToken.getScope());

                return result;

            }

        }else {

            this.generateErrorResponse(result, ErrorCodeEnum.INVALID_GRANT);

            return result;

        }

    }catch (Exception e){

        this.generateErrorResponse(result, ErrorCodeEnum.UNKNOWN_ERROR);

        return result;

    }

}

 

(6)通過Access Token獲取用戶信息:

在通過Access Token獲取用戶信息的時候,首先需要在攔截器裏校驗請求的Token是否有效,相關代碼邏輯如下:

接口地址:http://127.0.0.1:7000/api/users/getInfo?access_token=1.adebb0a4522d5dae9eaf94a5af4fec070c4f3dce.2592000.1537508734

返回如下

 

1

2

3

4

5

6

{

    "mobile": "110",

    "id": 1,

    "email": "[email protected]",

    "username": "admin"

}

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

package cn.zifangsky.interceptor;

 

import cn.zifangsky.enums.ErrorCodeEnum;

import cn.zifangsky.model.AuthAccessToken;

import cn.zifangsky.service.AuthorizationService;

import cn.zifangsky.utils.DateUtils;

import cn.zifangsky.utils.JsonUtils;

import org.apache.commons.lang3.StringUtils;

import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

 

import javax.annotation.Resource;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.time.LocalDateTime;

import java.util.HashMap;

import java.util.Map;

 

/**

* 用於校驗Access Token是否爲空以及Access Token是否已經失效

*

* @author zifangsky

* @date 2018/8/22

* @since 1.0.0

*/

public class AuthAccessTokenInterceptor extends HandlerInterceptorAdapter{

    @Resource(name = "authorizationServiceImpl")

    private AuthorizationService authorizationService;

 

    /**

     * 檢查Access Token是否已經失效

     */

    @Override

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String accessToken = request.getParameter("access_token");

 

        if(StringUtils.isNoneBlank(accessToken)){

            //查詢數據庫中的Access Token

            AuthAccessToken authAccessToken = authorizationService.selectByAccessToken(accessToken);

 

            if(authAccessToken != null){

                Long savedExpiresAt = authAccessToken.getExpiresIn();

                //過期日期

                LocalDateTime expiresDateTime = DateUtils.ofEpochSecond(savedExpiresAt, null);

                //當前日期

                LocalDateTime nowDateTime = DateUtils.now();

 

                //如果Access Token已經失效,則返回錯誤提示

                return expiresDateTime.isAfter(nowDateTime) || this.generateErrorResponse(response, ErrorCodeEnum.EXPIRED_TOKEN);

            }else{

                return this.generateErrorResponse(response, ErrorCodeEnum.INVALID_GRANT);

            }

        }else{

            return this.generateErrorResponse(response, ErrorCodeEnum.INVALID_REQUEST);

        }

    }

    

    /**

     * 組裝錯誤請求的返回

     */

    private boolean generateErrorResponse(HttpServletResponse response,ErrorCodeEnum errorCodeEnum) throws Exception {

        response.setCharacterEncoding("UTF-8");

        response.setHeader("Content-type", "application/json;charset=UTF-8");

        Map<String,String> result = new HashMap<>(2);

        result.put("error", errorCodeEnum.getError());

        result.put("error_description",errorCodeEnum.getErrorDescription());

 

        response.getWriter().write(JsonUtils.toJson(result));

        return false;

    }

 

}

然後再根據這個Access Token被授予的訪問範圍返回相應的用戶信息:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

package cn.zifangsky.controller;

 

import cn.zifangsky.enums.ErrorCodeEnum;

import cn.zifangsky.model.AuthAccessToken;

import cn.zifangsky.model.User;

import cn.zifangsky.service.AuthorizationService;

import cn.zifangsky.service.UserService;

import cn.zifangsky.utils.JsonUtils;

import org.springframework.http.MediaType;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

 

import javax.annotation.Resource;

import javax.servlet.http.HttpServletRequest;

import java.util.HashMap;

import java.util.Map;

 

/**

* 通過Access Token訪問的API服務

*

* @author zifangsky

* @date 2018/8/22

* @since 1.0.0

*/

@RestController

@RequestMapping("/api")

public class ApiController {

 

    @Resource(name = "authorizationServiceImpl")

    private AuthorizationService authorizationService;

 

    @Resource(name = "userServiceImpl")

    private UserService userService;

 

    @RequestMapping(value = "/users/getInfo", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)

    public String getUserInfo(HttpServletRequest request){

        String accessToken = request.getParameter("access_token");

        //查詢數據庫中的Access Token

        AuthAccessToken authAccessToken = authorizationService.selectByAccessToken(accessToken);

 

        if(authAccessToken != null){

            User user = userService.selectUserInfoByScope(authAccessToken.getUserId(), authAccessToken.getScope());

            return JsonUtils.toJson(user);

        }else{

            return this.generateErrorResponse(ErrorCodeEnum.INVALID_GRANT);

        }

    }

 

    /**

     * 組裝錯誤請求的返回

     */

    private String generateErrorResponse(ErrorCodeEnum errorCodeEnum) {

        Map<String,Object> result = new HashMap<>(2);

        result.put("error", errorCodeEnum.getError());

        result.put("error_description",errorCodeEnum.getErrorDescription());

 

        return JsonUtils.toJson(result);

    }

 

}

調用的代碼邏輯如下:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

@Override

public User selectUserInfoByScope(Integer userId, String scope) {

    User user = userMapper.selectByPrimaryKey(userId);

 

    //如果是基礎權限,則部分信息不返回

    if(ScopeEnum.BASIC.getCode().equalsIgnoreCase(scope)){

        user.setPassword(null);

        user.setCreateTime(null);

        user.setUpdateTime(null);

        user.setStatus(null);

    }

 

    return user;

}

 

三 接入OAuth2.0授權的第三方客戶端的代碼邏輯

這個Demo的第三方客戶端的完整可用源碼可以參考:https://gitee.com/zifangsky/OAuth2.0Demo/tree/master/ClientDemo

其實,對於接入的第三方客戶端來說,後臺的代碼邏輯跟我上篇文章中接入百度OAuth服務的代碼邏輯是差不多的。示例如下:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

package cn.zifangsky.controller;

 

import cn.zifangsky.common.Constants;

import cn.zifangsky.model.AuthorizationResponse;

import cn.zifangsky.model.User;

import cn.zifangsky.utils.EncryptUtils;

import org.apache.commons.lang3.StringUtils;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.client.RestTemplate;

import org.springframework.web.servlet.ModelAndView;

 

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpSession;

import java.text.MessageFormat;

 

/**

* 登錄

* @author zifangsky

* @date 2018/7/9

* @since 1.0.0

*/

@Controller

public class LoginController {

 

    @Autowired

    private RestTemplate restTemplate;

 

    @Value("${own.oauth2.client-id}")

    private String clientId;

 

    @Value("${own.oauth2.scope}")

    private String scope;

 

    @Value("${own.oauth2.client-secret}")

    private String clientSecret;

 

    @Value("${own.oauth2.user-authorization-uri}")

    private String authorizationUri;

 

    @Value("${own.oauth2.access-token-uri}")

    private String accessTokenUri;

 

    @Value("${own.oauth2.resource.userInfoUri}")

    private String userInfoUri;

 

    /**

     * 登錄驗證(實際登錄調用認證服務器)

     * @author zifangsky

     * @date 2018/7/25 16:42

     * @since 1.0.0

     * @param request HttpServletRequest

     * @return org.springframework.web.servlet.ModelAndView

     */

    @RequestMapping("/login")

    public ModelAndView login(HttpServletRequest request){

        //當前系統登錄成功之後的回調URL

        String redirectUrl = request.getParameter("redirectUrl");

        //當前系統請求認證服務器成功之後返回的Authorization Code

        String code = request.getParameter("code");

 

        //最後重定向的URL

        String resultUrl = "redirect:";

        HttpSession session = request.getSession();

        //當前請求路徑

        String currentUrl = request.getRequestURL().toString();

 

        //code爲空,則說明當前請求不是認證服務器的回調請求,則重定向URL到認證服務器登錄

        if(StringUtils.isBlank(code)){

            //如果存在回調URL,則將這個URL添加到session

            if(StringUtils.isNoneBlank(redirectUrl)){

                session.setAttribute(Constants.SESSION_LOGIN_REDIRECT_URL,redirectUrl);

            }

 

            //生成隨機的狀態碼,用於防止CSRF攻擊

            String status = EncryptUtils.getRandomStr1(6);

            session.setAttribute(Constants.SESSION_AUTH_CODE_STATUS, status);

            //拼裝請求Authorization Code的地址

            resultUrl += MessageFormat.format(authorizationUri,clientId,status,currentUrl);

        }else{

            //2. 通過Authorization Code獲取Access Token

            AuthorizationResponse response = restTemplate.getForObject(accessTokenUri, AuthorizationResponse.class

                    ,clientId,clientSecret,code,currentUrl);

 

            //如果正常返回

            if(StringUtils.isNoneBlank(response.getAccess_token())){

                System.out.println(response);

 

                //2.1 將Access Token存到session

                session.setAttribute(Constants.SESSION_ACCESS_TOKEN,response.getAccess_token());

 

                //2.2 再次查詢用戶基礎信息,並將用戶ID存到session

                User user = restTemplate.getForObject(userInfoUri, User.class

                        ,response.getAccess_token());

 

                if(StringUtils.isNoneBlank(user.getUsername())){

                    session.setAttribute(Constants.SESSION_USER,user);

                }

            }

 

            //3. 從session中獲取回調URL,並返回

            redirectUrl = (String) session.getAttribute(Constants.SESSION_LOGIN_REDIRECT_URL);

            session.removeAttribute("redirectUrl");

            if(StringUtils.isNoneBlank(redirectUrl)){

                resultUrl += redirectUrl;

            }else{

                resultUrl += "/user/userIndex";

            }

        }

 

        return new ModelAndView(resultUrl);

    }

 

}

需要注意的是,我這裏添加了一個狀態碼,用於防止OAuth2.0授權登錄過程中的CSRF攻擊。因此,需要新添加一個攔截器,用於在請求完Authorization Code回調的時候校驗這個狀態碼。相關代碼如下:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

package cn.zifangsky.interceptor;

 

import cn.zifangsky.common.Constants;

import cn.zifangsky.enums.ErrorCodeEnum;

import cn.zifangsky.utils.JsonUtils;

import org.apache.commons.lang3.StringUtils;

import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

 

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import javax.servlet.http.HttpSession;

import java.util.HashMap;

import java.util.Map;

 

/**

* 用於校驗OAuth2.0登錄中的狀態碼

*

* @author zifangsky

* @date 2018/8/23

* @since 1.0.0

*/

public class AuthInterceptor extends HandlerInterceptorAdapter{

 

    @Override

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        HttpSession session = request.getSession();

 

        //當前系統請求認證服務器成功之後返回的Authorization Code

        String code = request.getParameter("code");

        //原樣返回的狀態碼

        String resultStatus = request.getParameter("status");

 

        //code不爲空,則說明當前請求是從認證服務器返回的回調請求

        if(StringUtils.isNoneBlank(code)){

            //從session獲取保存的狀態碼

            String savedStatus = (String) session.getAttribute(Constants.SESSION_AUTH_CODE_STATUS);

            //1. 校驗狀態碼是否匹配

            if(savedStatus != null && resultStatus != null && savedStatus.equals(resultStatus)){

                return true;

            }else{

                response.setCharacterEncoding("UTF-8");

                response.setHeader("Content-type", "application/json;charset=UTF-8");

                Map<String,String> result = new HashMap<>(2);

                result.put("error", ErrorCodeEnum.INVALID_STATUS.getError());

                result.put("error_description",ErrorCodeEnum.INVALID_STATUS.getErrorDescription());

 

                response.getWriter().write(JsonUtils.toJson(result));

                return false;

            }

        }else{

            return true;

        }

    }

}

另外,實際上面代碼中使用到的一些配置就是我們OAuth2.0服務端的接口地址:

 

1

2

3

4

5

6

7

own.oauth2.client-id=7Ugj6XWmTDpyYp8M8njG3hqx

own.oauth2.scope=super

own.oauth2.client-secret=tur2rlFfywR9OOP3fB5ZbsLTnNuNabI3

own.oauth2.user-authorization-uri=http://10.0.5.22:7000/oauth2.0/authorize?client_id={0}&response_type=code&scope=super&&status={1}&redirect_uri={2}

own.oauth2.access-token-uri=http://10.0.5.22:7000/oauth2.0/token?client_id={1}&client_secret={2}&grant_type=authorization_code&code={3}&redirect_uri={4}

 

own.oauth2.resource.userInfoUri=http://10.0.5.22:7000/api/users/getInfo?access_token={1}

 

特別提示:在測試代碼的時候,最好將授權服務端和客戶端分別運行於兩個不同服務器上面,不然域名都是localhost會被瀏覽器判斷爲同一個網站。

好了,本篇文章到此結束,感興趣的同學可以參考示例源碼自己手動嘗試下。另外,我將在下篇文章中介紹一下OAuth2.0與單點登錄(SSO)之間的區別與聯繫,敬請期待。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章