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

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

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

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

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

OAuth2.0服務端的主要流程

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

數據庫的表結構設計

提示:我在下面只介紹一些表的主要字段,這個Demo中使用的完整的表結構能夠參考:gitee.com/zifangsky/O…web

(1)auth_client_detailsredis

接入的第三方客戶端詳情表。這就跟咱們要想使用百度OAuth服務就須要事先在百度開發者中心新建一個應用是一個道理,每一個想要接入OAuth2.0受權服務的第三方客戶端都須要事先在服務端這裏「備案」,因此主要須要如下幾個字段:spring

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

(2)auth_scopesql

用戶信息範圍表。OAuth2.0服務端在受權第三方客戶端訪問用戶的信息的時候,一般會把用戶的信息劃分爲幾個級別,好比用戶的基本信息,用戶密碼、購物記錄等高保密性信息。這樣劃分主要是讓用戶自主選擇把本身哪一種信息受權給第三方客戶端訪問,因此主要須要如下字段:數據庫

  • scope_name:範圍名稱

(3)auth_access_tokenapache

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

  • access_tokenAccess Token字段
  • user_id:代表是哪一個用戶授予的權限
  • client_id:代表授予給哪一個客戶端
  • expires_in:過時時間戳,代表這個Token在哪一天過時
  • scope:代表能夠訪問何種範圍

(4)auth_refresh_token

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

  • refresh_tokenRefresh 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的受權服務端的完整可用源碼能夠參考:gitee.com/zifangsky/O…

(1)客戶端註冊接口:

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

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

參數

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

客戶端註冊接口

(2)受權頁面:

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

受權頁面

實現代碼很簡單,就是在用戶選擇「受權」後,往表auth_client_user插入一條記錄。這裏就很少說了,能夠自行參考一下示例源碼。

(3)獲取Authorization Code:

根據請求的client_idscope生成一個字符串——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

/** * 獲取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類裏面的生成邏輯:

@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 TokenRefresh 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

返回以下

{
	"access_token": "1.6659c9d38f5943f97db334874e5229284cdd1523.2592000.1537600367",
	"refresh_token": "2.b19923a01cf35ccab48ddbd687750408bd1cb763.31536000.1566544316",
	"expires_in": 2592000,
	"scope": "basic"
}
複製代碼
/** * 經過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這個類裏面,具體以下:

@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

返回以下

{
	"access_token": "1.adebb0a4522d5dae9eaf94a5af4fec070c4f3dce.2592000.1537508734",
	"refresh_token": "2.5c58637a2d51e4470d3e1189978e94da8402785e.31536000.1566283826",
	"expires_in": 2592000,
	"scope": "basic"
}
複製代碼
/** * 經過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

返回以下

{
	"mobile": "110",
	"id": 1,
	"email": "admin@zifangsky.cn",
	"username": "admin"
}
複製代碼
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被授予的訪問範圍返回相應的用戶信息:

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);
    }

}
複製代碼

調用的代碼邏輯以下:

@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的第三方客戶端的完整可用源碼能夠參考:gitee.com/zifangsky/O…

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

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回調的時候校驗這個狀態碼。相關代碼以下:

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服務端的接口地址:

own.oauth2.client-id=7Ugj6XWmTDpyYp8M8njG3hqx
own.oauth2.scope=super
own.oauth2.client-secret=tur2rlFfywR9OOP3fB5ZbsLTnNuNabI3
own.oauth2.user-authorization-uri=http://127.0.0.1:7000/oauth2.0/authorize?client_id={0}&response_type=code&scope=super&&status={1}&redirect_uri={2}
own.oauth2.access-token-uri=http://127.0.0.1:7000/oauth2.0/token?client_id={1}&client_secret={2}&grant_type=authorization_code&code={3}&redirect_uri={4}

own.oauth2.resource.userInfoUri=http://127.0.0.1:7000/api/users/getInfo?access_token={1}
複製代碼

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

相關文章
相關標籤/搜索