前言
第一次看到oauth2.0的时候说实话心里是迷茫且惧怕的,自己也有下去小小的了解过,对用户信息反复交互的情况自我感觉掌握度很差。但是在真实完整地去接触学习开发一套涉及完整oauth2.0认证流程之后,我对它又有了新的理解。怎么说呢,由浅入深吧。
oauth2.0认证流程介绍
在真正开始开发之前,我们得先了解oauth2.0整体的运行流程,为什么我们要使用这套流程?我们要用这个流程去做什么内容,这些内容应该怎样去实现。
首先,什么是oauth2.0?
这是本人百度找到的图片,看着蛮简单易懂的
简单介绍一下吧,比如说我们自家公司需要集成第三方网站做登录。
那我们就需要在跳转登录页的时候(不管是token过期还是首次访问的跳转)进行重定向的操作,重定向到第三方网站调用那边的约定接口。
在三方网站做了登录操作之后那边会带着授权code根据我们提供的某个回调地址的参数进行重定向,这个回调地址是我们提供的,也就是我们系统的处理地址。
在拿到授权code之后我们再次调用三方的约定接口(获取accessToken的接口需要code参数)拿到accessToken
然后将accessToken作为参数再次请求获取用户id的约定接口
最后用拿到的用户id调用获取用户信息的接口拿到完整的用户信息完成登录。
以上就是oath2.0的整体流程,说实话看起来可能有点绕,没关系我们在实战中深刻了解一下。
实战开发
开发难点
在了解了上面的oauth2.0流程之后我经过自己实际开发的过程总结出了几个初次开发可能会遇到的问题
1.我们如何能实现访问登录页的时候重定向到第三方登录页?
2.拿到一系列授权code或者token之后怎么去调用第三方的接口获取数据?
3.流程走完之后我们拿到第三方用户数据怎样将其作为已登录用户返回给前端让其跳转到首页?
带着这几个问题我们开始项目实战
实际开发
我们首先要实现的就是登录重定向,一般来说,我们的系统都会去判断当前页面的用户是否是已登录或者过没过期,假如不符合需求就会强制跳转到本系统的登录页。我们就需要利用这一点来进行重定向操作。
大家可以试想一下,我前端判断到用户未登录之后不是跳转到本地登录页,而是给后端一个信息,让后端跳转到自定义的页面是不是就能实现这种操作了呢(为什么不能前端直接跳转,问就是安全性问题)
那就得用上拦截器了,后端和前端约定好重定向的跳转拦截。举个例子
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
/**
* 被拦截的路径
*/
String[] addPathPatterns = {
"/**/oauth2login/**"
};
/**
* 忽略路径
*/
String[] excludePathPatterns = {
"/index",
"/loan/**"
};
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new Oauth2HandlerClient()).addPathPatterns(addPathPatterns);
}
}
这里我拦截的是请求里面包括/oauth2login内容的,前端跳转这个地址就会被后端拦截到,然后我们就可在后端为所欲为了。
我们在另一位置定义好自己的oauth2的实现类(代码仅供参考)
@Service
public class Oauth2HandlerClient extends HandlerInterceptorAdapter {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("拦截器启动");
String path = request.getRequestURI();
String url;
if (path.compareToIgnoreCase("/oauth2login") == 0) {
url = request.getParameter("redirect_uri");
if (StringHelper.isNullOrEmpty(url)) {
String msg = "参数错误. redirect_uri";
_logger.error(msg);
WebUtils.outputResopnse(response, msg);
} else if (!this.IsAuthered(request)) {
this.redirectToSsoServer(request, response, url);
} else {
if (_logger.isDebugEnabled()) {
_logger.debug("Authered : " + request.getRequestURL());
}
response.sendRedirect(url);
}
} else
if (path.compareToIgnoreCase(this.getCallbackPath()) == 0) {
this.handCallbackEx(request, response, this.getCallbackPath());
} else {
if (!this.NeedAuther(path)) {
return true;
}
if (this.IsAuthered(request)) {
if (_logger.isDebugEnabled()) {
_logger.debug("Authered : " + request.getRequestURL());
}
return true;
}
this.redirectToSsoServer(request, response, "");
}
return false;
}
//初始页面重定向
private void redirectToSsoServer(HttpServletRequest request, HttpServletResponse response, String url) throws Exception {
if (StringHelper.isNullOrEmpty(url)) {
url = WebUtils.contactUrlParams(WebUtils.getForwardedUrl(request.getRequestURL().toString()), request.getQueryString());
}
String state = GetAlias(url);
this.userType = request.getParameter("userType");
if (!StringHelper.isNullOrEmpty(userType) && !checkCallBack(userType)) {
String msg = "参数错误. userType";
_logger.error(msg);
WebUtils.outputResopnse(response, msg);
}
String auth2redirect = "";
if ("0".equals(userType)){
auth2redirect = String.format(this.auther2urlForCompany,cqClient_idForCompany,"code",url + this.getCallbackPath(),state);//客户端身份标识、固定值code、回调地址
if (!this.auther2urlForCompany.startsWith("http://") && !this.auther2urlForCompany.startsWith("https://")) {
auth2redirect = WebUtils.getFullPath(request, auth2redirect);
}
}else {
auth2redirect = String.format(this.auther2urlForDept,cqClient_idForDept,"code",url + this.getCallbackPath(),state);//客户端身份标识、客户端密钥、回调地址、固定值code
if (!this.auther2urlForDept.startsWith("http://") && !this.auther2urlForDept.startsWith("https://")) {
auth2redirect = WebUtils.getFullPath(request, auth2redirect);
}
}
if (_logger.isDebugEnabled()) {
_logger.debug("UnAuthered : " + url + "\r\nRedirect : " + auth2redirect);
}
response.sendRedirect(auth2redirect);
}
}
实现类里面的preHandle方法就是拦截之后的请求处理了,记得类继承HandlerInterceptorAdapter
然后我们就可以开始做重定向的工作了,首先拦截器会对当前请求地址进行判断,如果满足要求就会开始重定向定义(redirect_uri参数就是你的回调地址,一般来说就是我们访问我们自己系统首页的ip地址)
然后就进入我们的初始页面重定向方法,注意我在其中是使用了较多变量,特殊原因不能展示出来,但是大概构成意思就是如此。我们将和第三方约定好的重定向地址格式拼接好(比如http://********?**=**&**=**,回调地址必带)然后执行response.sendRedirect(重定向地址)方法就能重定向到对方的第三方登录页了。
下面是第二个问题,我们登录成功拿到code之后怎么调用第三方接口呢?
这就要涉及到第二个知识
url调取接口获取返回数据封装方法
public class SsoUntil{
/**
* url调取接口获取返回数据封装方法
*
* @param strUrl 请求url
* @param requestMethod 请求类型 POST/GET
* @param headerMap 请求头参数集
* @param bodyMap 请求body参数集
*/
public String getURLContent(String strUrl, String requestMethod, HashMap<String,String> headerMap, HashMap<String,String> bodyMap) throws Exception {
StringBuilder strUrlBuilder = new StringBuilder(strUrl);
//遍历bodyMap将参数连接入url
if (null != bodyMap){
for (Map.Entry<String, String> map : bodyMap.entrySet()){
strUrlBuilder.append(strUrlBuilder.toString().contains("?") ? "&" : "?").append(map.getKey()).append("=").append(map.getValue());
}
}
strUrl = strUrlBuilder.toString();
// 将url 以 open方法返回的urlConnection 连接强转为HttpURLConnection连接 (标识一个url所引用的远程对象连接)
// 此时httpConn只是为一个连接对象,待连接中
URL url = new URL(strUrl);
HttpURLConnection httpConn = (HttpURLConnection) url.openConnection();
//设置请求方法
httpConn.setRequestMethod(requestMethod);
httpConn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); //默认
//遍历headerMap将请求头加入请求
if (null != headerMap){
for (Map.Entry<String, String> map : headerMap.entrySet()){
httpConn.setRequestProperty(map.getKey(), (String) map.getValue());
}
}
// 建立连接 (请求未开始,直到connection.getInputStream()方法调用时才发起,以上各个参数设置需在此方法之前进行)
httpConn.connect();
BufferedReader reader = new BufferedReader(new InputStreamReader(httpConn.getInputStream(), StandardCharsets.UTF_8));
String line;
StringBuffer buffer = new StringBuffer();
//读取返回值存入StringBuffer
while ((line = reader.readLine()) != null){
buffer.append(line);
}
reader.close();
httpConn.disconnect();
//返回String类型
return buffer.toString();
}
}
此段代码我就不多做讲解,大家看注释就行了。主要目的就是拿到目标url(接口地址)和拼接的参数,不管是请求头还是请求体的参数都能放到请求里面传递给目标地址。最后以string的形式接收到目标参数返回的数据(一般是字符串类型的json格式)。
有了这个工具类,我们就可以很舒服的进行目标接口调用了!
//获取用户数据
private void handCallbackEx(HttpServletRequest request, HttpServletResponse response, String url) throws Exception {
//初始化调用请求参数
HashMap<String,String> loginIdHeaderMap = new HashMap<>();
HashMap<String,String> tokenBodyMap = new HashMap<>();
HashMap<String,String> userBodyMap = new HashMap<>();
//获取请求地址传入的code
String code = request.getParameter("code");
String state = request.getParameter("state");
SsoUntil ssoUntil = new SsoUntil();
if (!checkCallBack(code)) {
String msg = "参数错误. code";
_logger.error(msg);
WebUtils.outputResopnse(response, msg);
}else {
if ("0".equals(userType)){
tokenBodyMap.put("client_id",cqClient_idForCompany);
tokenBodyMap.put("client_secret",cqClient_secretForCompany);
}else {
tokenBodyMap.put("client_id",cqClient_idForDept);
tokenBodyMap.put("client_secret",cqClient_secretForDept);
}
tokenBodyMap.put("redirect_uri",this.GetAliasUrl(state)+url);
tokenBodyMap.put("code",code);
tokenBodyMap.put("grant_type","authorization_code");
//根据参数获取到accessToken
String Token = ssoUntil.getURLContent(("0".equals(userType) ? auth2urlForCompany : auth2urlForDept) + tokenMethod,"POST",null,tokenBodyMap);
JSONObject jsonObject = JSONObject.parseObject(Token);
String accessToken = jsonObject.getString("access_token");
//检查accessToken
if (checkCallBack(accessToken)){
loginIdHeaderMap.put("Authorization","Bearer "+accessToken);
//根据accessToken获取到loginId
String loginId = ssoUntil.getURLContent(("0".equals(userType) ? auth2urlForCompany : auth2urlForDept) + loginIdMethod, "POST",loginIdHeaderMap,null);
//检查loginId
if (checkCallBack(loginId)){
userBodyMap.put("access_token",accessToken);
userBodyMap.put("loginid",loginId);
}else {
String msg = "参数错误. loginId";
_logger.error(msg);
WebUtils.outputResopnse(response, msg);
}
}else {
String msg = "参数错误. accessToken";
_logger.error(msg);
WebUtils.outputResopnse(response, msg);
}
//根据accessToken与loginId获取到用户信息
String LoginUser = ssoUntil.getURLContent(("0".equals(userType) ? auth2urlForCompany : auth2urlForDept) + getUserIdMethod, "POST",null, userBodyMap);
//解析用户信息
JSONObject userObject = JSONObject.parseObject(LoginUser);
String result = userObject.getString("result");
JSONObject resultObject = JSONObject.parseObject(result);
//存储用户信息
HashMap<String,String> userMap = new HashMap<>();
userMap.put("nickName",resultObject.getString("loginid"));
userMap.put("name",resultObject.getString("displayname"));
userMap.put("sex",resultObject.getString("sex"));
userMap.put("email",resultObject.getString("email"));
userMap.put("Mob1",resultObject.getString("mobile"));
userMap.put("UniqID",resultObject.getString("userguid"));
Person person = this.savePerson(userMap, userType);
LoginResult logInfo = new LoginResult();
String nextUrl = "";
try {
nextUrl = this.GetAliasUrl(state);
logInfo = this._sessions.Login("", "PY", person.getUniqID(), person.getNickName(), person.getName(), "", WebUtils.getRemoteHost(request), 0, null);
} catch (RuntimeException var9) {
String msg = "身份验证失败:";
_logger.error(msg, var9);
logInfo.setCode(-1);
logInfo.setMsg(var9.getMessage());
}
if (logInfo != null && logInfo.getCode() == 0 && !StringHelper.isNullOrEmpty(nextUrl)) {
response.sendRedirect(nextUrl);
} else {
String msg = "身份验证失败! " + logInfo.getMsg();
_logger.info(msg);
msg = this._errTemplate.replace("${Title}", "身份验证失败").replace("${Msg}", logInfo.getMsg());
WebUtils.outputResopnse(response, msg);
}
}
}
注意看我们这个handCallbackEx方法在何处调用的,是在最上方那个拦截器处理方法里面,我们第一次重定向之后根据回调地址回来的请求也会被拦截到,并且执行handCallbackEx处理方法。
在这个方法里面我们做了较多事情,首先用首次拿到的code去获取token。。。。。以此类推,最后拿到用户信息之后存储到我们自己的数据库内作为登录用户的信息。最后将登录用户信息存储到页面cookie让前端读取识别之后跳转到首页了。
最后
因为本人还只是个初登江湖的小实习生,还是个小菜鸡,所以很多地方总结不全面。不足的地方希望大佬多指出。