调用 oauth2_OAuth2.0协议入门(二)

本文详细介绍了OAuth2.0授权服务端的设计,包括数据库表结构设计和主要接口的代码实现,如客户端注册、授权页面、获取Authorization Code、Access Token、Refresh Token以及通过Access Token获取用户信息。此外,还提到了第三方客户端的代码逻辑和防止CSRF攻击的措施。
摘要由CSDN通过智能技术生成
39e1dfb9377d48545e1f4097c1d9a66a.png

一OAuth2.0授权服务端的设计

在上一篇文章中,我介绍了OAuth2.0协议的基本概念以及作为一个第三方应用在请求授权服务端的时候需要做哪些事情。通过上一篇文章中调用百度OAuth服务的例子我们可以得知,使用授权码模式完成OAuth2.0授权的过程需要以下三个步骤:

  1. client请求授权服务端,获取Authorization Code
  2. client通过Authorization Code再次请求授权服务端,获取Access Token
  3. client通过服务端返回的Access Token获取用户的基本信息

因此,OAuth2.0授权服务端的设计也就主要围绕这几个接口展开,其主要流程是这样的:

32e7b4729868c3ad77edf75c16ee0e0f.png

明白了整个运行流程,那剩下就好办了。接下来我们需要做的是数据库的表结构设计。

数据库的表结构设计

提示:我在下面只介绍一些表的主要字段,这个Demo中使用的完整的表结构可以参考:gitee.com/zifangsky/O…

(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的授权服务端的完整可用源码可以参考:gitee.com/zifangsky/O…

(1)客户端注册接口:

某个第三方客户端需要事先在服务端这里“备案”。在这个Demo中我没有写具体的页面,只提供了一个注册接口,其中client_id和client_secret都是随机生成的字符串。

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

参数

{"clientName":"测试客户端","redirectUri":"http://localhost:7080/login","description":"这是一个测试客户端服务"}
3bd091bb204088306359ac0bb1467ff1.png

(2)授权页面:

如果用户之前没有给请求的client授权过,那么在第一次请求Authorization Code的时候会打开授权页面,然后用户手动选择是否授权:

295c90e1cff83699279254da1b141869.png

实现代码很简单,就是在用户选择“授权”后,往表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

/** * 获取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);//客户端IDString clientIdStr = request.getParameter("client_id");//权限范围String scopeStr = request.getParameter("scope");//回调URLString redirectUri = request.getParameter("redirect_uri");//status,用于防止CSRF攻击(非必填)String status = request.getParameter("status");//生成Authorization CodeString 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类里面的生成逻辑:

@Overridepublic 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 Codereturn 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

返回如下

{"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 */@RequestMapping(value = "/token", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)@ResponseBodypublic Map token(HttpServletRequest request){Map result = new HashMap<>(8);//授权方式String grantType = request.getParameter("grant_type");//前面获取的Authorization CodeString code = request.getParameter("code");//客户端IDString clientIdStr = request.getParameter("client_id");//接入的客户端的密钥String clientSecret = request.getParameter("client_secret");//回调URLString 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;}//校验回调URLif(!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 TokenString accessTokenStr = authorizationService.createAccessToken(user, savedClientDetails, grantType, scope, expiresIn);//查询已经插入到数据库的Access TokenAuthAccessToken authAccessToken = authorizationService.selectByAccessToken(accessTokenStr);//生成Refresh TokenString 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这个类里面,具体如下:

@Overridepublic 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 TokenAuthAccessToken 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 Tokenreturn accessTokenStr;}@Overridepublic 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 TokenAuthRefreshToken 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 Tokenreturn 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 */@RequestMapping(value = "/refreshToken", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)@ResponseBodypublic Map refreshToken(HttpServletRequest request){Map result = new HashMap<>(8);//获取Refresh TokenString 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 TokenAuthAccessToken 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 TokenString 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 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 result = new HashMap<>(2);        result.put("error", errorCodeEnum.getError());        result.put("error_description",errorCodeEnum.getErrorDescription());        return JsonUtils.toJson(result);    }}

调用的代码逻辑如下:

@Overridepublic 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 */@Controllerpublic 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 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=7Ugj6XWmTDpyYp8M8njG3hqxown.oauth2.scope=superown.oauth2.client-secret=tur2rlFfywR9OOP3fB5ZbsLTnNuNabI3own.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)之间的区别与联系,敬请期待。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值