登录验证过程,PC与APP开放登录接口(支持WEB与SDK方式)

1.需求场景 登录,是一个系统的第一步功能。登录成功后,才能进入系统,使用系统功能。在某些场景下,其它系统需要接入到本系统中。或者通过接口的方式进行登录,以及通过接口的方式来取数据。
另外,系统配套移动APP端,变得越来越常见。因此,支持移动端的登录,也变动同样重要。本文介绍登录这块的功能,以及常规验证机制。
2.实现原理 2.1.登录提交 客户端(不管是WEB网页还是APP端)每次登录,服务端都需要记录登录日志。日志内容包括登录账号、是否登录成功,以及客户端的详细配置信息。因此提交到服务端的数据,是越详细越好。
必填的数据,是账号和密码。选填的信息,比如分辨率,需要客户端传递过去(服务端无法获取)。其他信息(比如浏览器、操作系统)则可以从来源信息中提取。
登录验证的流程,如下图所示。
登录验证过程,PC与APP开放登录接口(支持WEB与SDK方式)
文章图片

登录过程详细说明如下:

  1. 提交账号和密码信息到服务端。post和get方式均可。可连同分辨率一并提交。
  2. 根据账号,查询用户信息。如不存在,则提示账号不存在。
  3. 判断密码是否匹配。提交的密码,会被2次MD5加密,之后与数据库中密码进行比对。
  4. 判断账号状态。如果被禁用,返回错误信息。
  5. 生成随机token。将新的token、以及token生成时间,更新到数据库中。后期可验证token过期。
  6. 服务端创建用户session信息。如果是web方式登录,服务端会创建用户session;APP端登录,则不会创建。
  7. 返回token和用户角色id到客户端。方便客户端根据角色,进行不同的处理。为了安全,一般不会返回用户id给客户端。
  8. 客户端后续提交请求,都必须带上返回的token和登录账号。服务端根据登录账号和token,来判断并验证用户。
2.2.登录日志 登录日志记录的信息包括:登录账号、姓名昵称、登录结果、备注说明、登录方式、登录时间、登录IP、操作系统、浏览器、分辨率、浏览器头。
  • 登录账号。当前提交的账号。
  • 姓名昵称。如果登录成功,则会记录对应账号的姓名昵称;登录失败,则为空。
  • 登录结果。记录登录成功还是失败。
  • 备注说明。如果登录成功,则不说明;登录失败,则会记录失败原因。比如密码错误、账号不存在、账号被禁用。
  • 登录方式。分三种:PC端、安卓端、苹果端。调用了哪个接口,就记录是哪种方式。完全由接口决定,如果是PC端调用了安卓端的接口,那就记录的是安卓端。
  • 登录时间。当前登录时间。
  • 登录IP。记录登录客户端的广域网IP地址,而不是局域网IP地址。
  • 操作系统。记录操作系统名称,比如Windows 7、Windows 10、Mac OS X等。
  • 浏览器。记录浏览器名称和版本号,比如Chrome 65、Internet Explorer 11等。
  • 分辨率。由于服务端无法获取分辨率,因此屏幕的宽度和高度,由客户端传递过来。
  • 浏览器头。国内的浏览器种类繁多,目前提取浏览器名称的准确性还不高。因此先直接把浏览器头部信息记录下来,以后精确化浏览器版本的识别。
3.登录接口 3.1.WEB接口 3.1.1.登录
如果通过web链接方式请求,或者APP方式。可使用下面的接口格式。
接口地址:http://xxx.com/login.do?action=login
接口参数说明:需提交下面剩余的5个参数。post和get方式均可。
参数 含义 说明
action 请求类型 必填。登录固定为login
username 登录账号 必填。
password 登录密码 必填。服务端会2次md5加密
loginType 登录类型 0:web网页。不填写,则默认0
1:安卓端登录
2:苹果端登录
screenWidth 分辨率宽度 整数。当前屏幕像素宽度,非必填。
screenHeight 分辨率高度 整数。当前屏幕像素高度。非必填。
服务端以JSON格式数据返回。
1、成功返回样例。
{ "success": true, "info": "登录成功", "data": { "token": "02E191DD4CA543F58E7DF3B55E746046", "roleId": 8 } }

格式说明:
参数 含义 说明
success 是否成功 bool类型。值为true或false
info 描述说明 如果是登录失败,会写明失败原因
data 数据内容 所有的返回附带数据,都会放入data中。
token 随机码 每次登录成功后,都会重新返回一个新的。32位随机字符串
roleId 角色id 当前用户的角色id。前端有可能根据不同角色,转向不同页面。
2、失败返回样例。
{ "success": false, "info": "账号不存在" }

3.1.2.退出
除了登录请求,后续的其他所有请求,都是需要带上登录时返回的token,所以退出接口也不例外。
退出请求链接:http://xxx.com/login.do?action=logout&username=xxx&token=xxx&loginType=1
接口参数说明:get方式请求。
参数 含义 说明
action 请求动作 退出登录,赋值为logout。
username 登录账号 为了安全,所有接口请求,都不以用户id来定位身份。
token 随机码 登录时返回。每次登录后,会重新生成新的token。
loginType 登录类型 需要与登录时传递的值相同。否则无法退出。
1、成功返回样例。
{ "success": true, "info": "退出成功" }

2、失败返回样例。
{ "success": false, "info": "账号验证失败" }

注意:在实际应用场景中,不管服务端是否返回成功,客户端都需要正常退出。总不能因为服务端返回失败,不让客户端退出。只是失败后,可以给予用户提示,这样根据用户反馈,去排除解决问题。
3.1.3.token验证
如果需要验证token和username是否匹配,则可以使用token验证接口。
退出请求链接:http://xxx.com/login.do?action=token&username=xxx&token=xxx&loginType=1
接口参数说明:get方式请求。
参数 含义 说明
action 请求动作 验证token,action赋值为token。
username 登录账号
token 随机码 需要验证的token字符串。
loginType 登录类型 非必填。移动端和PC端不同。
1、成功返回样例。
{ "success": true, "info": "用户token验证成功" }

2、失败返回样例。
{ "success": false, "info": "token不正确" }


3.1.4.获取用户信息
如果需要获取当前登录用户更多详细信息,以便进行下一步的处理,可以通过下面的接口。
请求链接:http://xxx.com/login.do?action=getUser&username=xxx&token=xxx&loginType=1
返回当前用户的详细信息。
成功返回数据样板如下。
{ "data": { "departmentName": "公司总部", "userState": true, "nickName": "大路", "roleId": 4, "sex": true, "departmentId": 1, "roleName": "超级管理员", "dutyId": 0, "superAdmin": true, "username": "lilu" }, "success": true, "info": "获取用户信息成功" }

失败返回信息
{ "success": false, "info": "token不正确" }

3.1.5.修改密码
当前用户需要修改自己密码时,可调用下面的接口。
请求链接:http://xxx.com/login.do?action=modifyPassword&username=xxx&token=xxx&loginType=1
接口参数说明。get和post方式均可。
参数 含义 说明
action 修改密码 固定为modifyPassword
loginType 登录类型 0:web网页。不填写,则默认0
1:安卓端登录
2:苹果端登录
oldPassword 旧密码 必填。无需加密
newPassword 新密码 必填。无需加密
newPassword2 确认新密 必填。无需加密
返回修改结果。
成功样例:
{ "success": true, "info": "密码修改成功" }

失败样例:
{ "success": false, "info": "原密码不正确" }


3.2.SDK接口 如果要二次开发,将登录验证功能集成在代码中,则需要使用SDK接口。SDK接口,只需要调用封装好的方法即可。
登录退出相关的方法,都封装在LoginService类中,使用静态方法,直接调用。
3.2.1.登录
登录SDK有3种重载方式。
  1. 传递servlet。最简单的方式,直接把request对象传递进去,自动获取参数值进行处理。
  2. 传递账号密码。提取账号密码后,根据账号密码进行验证。此方法的登录日志记录,没有客户端信息。
  3. 传递账号密码,以及日志对象。记录登录日志时,会比较详细。
方法格式如下:
//验证用户登录,传递request对象。返回结果实体,用户信息存储在data属性中。 public static NoCodeResult userLogin(HttpServletRequest request) { NoCodeResult noCodeResult = new NoCodeResult(); //…………代码省略………… return noCodeResult; }//验证用户登录,传递登录账号、密码、登录方式。返回结果实体,用户信息存储在data属性中。 public static NoCodeResult userLogin(String userName, String password, int loginType) { NoCodeResult noCodeResult = new NoCodeResult(); //…………代码省略………… return noCodeResult; }//验证用户登录,传递账号、密码、日志实体(便于记录登录日志)。返回结果实体,用户信息存储在data属性中。 public static NoCodeResult userLogin(String userName, String password, UserLoginLog loginLog) { NoCodeResult noCodeResult = new NoCodeResult(); //…………代码省略………… return noCodeResult; }

第一种方式,样例代码如下。在servlet页面中进行处理。
调用LoginService.userLogin方法。返回一个NoCodeResult结果对象。从结果对象中取值。如果登录成功,会将用户实体写入到NoCodeResult对象的data属性中。从data属性中取值,转换为NoCodeUser对象,即可以取到登录用户信息。
//用户账号登录 private void login(HttpServletRequest request, HttpServletResponse response) throws IOException { //调用登录方法。取得返回结果。自动完成登录日志写入、更新token、获取用户信息等操作。 NoCodeResult noCodeResult = LoginService.userLogin(request); if (noCodeResult == null) { ExceptionUtil.printlnFailure(response, "未知错误"); return; } //失败,返回失败原因 if (!noCodeResult.isSuccess()) { ExceptionUtil.printlnFailure(response, noCodeResult.getInfo()); return; } //获取用户对象实体 Object obj = noCodeResult.getData(); if (obj == null) { ExceptionUtil.printlnFailure(response, "获取用户信息失败"); return; } //成功后,做其他动作。创建session,写入登录日志,都已经做了。 // 目前暂时不需要做其他动作。 //返回结果转换为实体对象 NoCodeUser noCodeUser = (NoCodeUser) obj; //构造json对象返回 JSONObject jsonLogin = new JSONObject(); JSONObject jsonData = https://www.it610.com/article/new JSONObject(); jsonLogin.put("success", true); jsonLogin.put("info", noCodeResult.getInfo()); jsonLogin.put("data", jsonData); if (noCodeUser.getLoginType() == 0) { jsonData.put("token", noCodeUser.getToken()); } else { jsonData.put("appToken", noCodeUser.getAppToken()); } jsonData.put("roleId", noCodeUser.getRoleId()); //登录成功后,返回结果 response.getWriter().println(jsonLogin.toJSONString()); }

第二种方式,样例代码如下。将username、token、loginType传递给方法即可。该方法也会自动记录登录日志,只是登录日志的客户端信息没有。比如浏览器信息、登录IP等信息获取不到。
//用户账号登录 private void login(HttpServletRequest request, HttpServletResponse response) throws IOException { String username = request.getParameter("username"); String token = request.getParameter("token"); int loginType = StringUtil.convertToInt(request.getParameter("loginType")); //调用登录方法。取得返回结果。自动完成登录日志写入、更新token、获取用户信息等操作。 NoCodeResult noCodeResult = LoginService.userLogin(username, token, loginType); if (noCodeResult == null) { ExceptionUtil.printlnFailure(response, "未知错误"); return; } //失败,返回失败原因 if (!noCodeResult.isSuccess()) { ExceptionUtil.printlnFailure(response, noCodeResult.getInfo()); return; } }

第三种方式,样例代码如下。需要自己创建登录日志对象,并且赋值。将日志对象传递进去,这样写登录日志时会比较详细。
//用户账号登录 private void login(HttpServletRequest request, HttpServletResponse response) throws IOException { String username = request.getParameter("username"); String token = request.getParameter("token"); int loginType = StringUtil.convertToInt(request.getParameter("loginType")); int screenWidth = StringUtil.convertToInt(request.getParameter("screenWidth")); int screenHeight = StringUtil.convertToInt(request.getParameter("screenHeight")); //获取浏览器信息 String ua = request.getHeader("User-Agent"); //转成UserAgent对象 UserAgent userAgent = UserAgent.parseUserAgentString(ua); //系统名称 String systemName = userAgent.getOperatingSystem().getName(); //浏览器名称 String browserName = userAgent.getBrowser().getName(); //登录日志 UserLoginLog loginLog = new UserLoginLog(); loginLog.setUserName(username); loginLog.setSuccess(false); //会根据真实登录结果进行修改 loginLog.setRemark("未知原因"); //会根据真实登录结果进行修改 loginLog.setLoginType(loginType); loginLog.setIpWan(WebServerUtil.getClientIpAddr(request)); loginLog.setBrowser(browserName); loginLog.setoS(systemName); loginLog.setScreenWidth(screenWidth); loginLog.setScreenHeight(screenHeight); loginLog.setUserAgent(ua); //调用登录方法。取得返回结果。自动完成登录日志写入、更新token、获取用户信息等操作。 NoCodeResult noCodeResult = LoginService.userLogin(username, token, loginLog); if (noCodeResult == null) { ExceptionUtil.printlnFailure(response, "未知错误"); return; } //失败,返回失败原因 if (!noCodeResult.isSuccess()) { ExceptionUtil.printlnFailure(response, noCodeResult.getInfo()); return; } }

方法执行后,返回数据为一个结果实体,定义如下。如果登录成功,则会将用户实体对象NoCodeUser存入data属性中。
//返回结果实体 public class NoCodeResult { private boolean success; // 执行成功或失败 private String info; // 结果说明 private boolean isExcepted; //是否异常 private String exceptionTitle; //异常信息标题 private Exception exception; //异常实体 private Object data; // 结果内容。可以创建对象。 }

用户实体NoCodeUser定义如下。字段含义后面都有说明。
public class NoCodeUser { private int id; //用户id private boolean userState; //用户状态 private boolean superAdmin; //是否超级管理员 private boolean sex; //用户性别 private String userName; //登录账号 private String nickName; //姓名昵称 private String password; //登录密码 private int loginType; //登录方式。0为PC网页端登录,1为安卓,2为苹果 private String token; //token值。WEB方式登录更新此token值 private String appToken; //移动端token。APP登录取此token值 private String phoneNumber; //手机号码 private int dutyId; //职务id private String dutyName; //职务名称 private int roleId; //角色id private String roleName; //角色名称 private int departmentId; //部门id private String departmentName; //部门名称 private String authority; //角色权限 private Date lastLoginTime; //上次登录时间。注意:是上次登录时间,不是本次登录时间。本次时间就是现在。 private String lastLoginIP; //上次登录IP。注意:是上次登录IP,不是本次登录IP。本次IP可取到。 private int loginCount; //登录次数 private String remark; //备注说明。 }

3.2.2.退出
退出方法有2种重载:传递用户id和用户账号。
//用户账号退出。传递账号和登录方式。 public static NoCodeResult userLogout(String userName, int loginType) { NoCodeResult noCodeResult = new NoCodeResult(); //…………代码省略………… return noCodeResult; }//用户账号退出。传递账号id和登录方式 public static NoCodeResult userLogout(int userId, int loginType) { NoCodeResult noCodeResult = new NoCodeResult(); //…………代码省略………… return noCodeResult; }

调用样例代码如下。在退出之前,需要进行账号验证。账号验证可以调用验证方法。
//退出登录。清理session、cookie、sign private void logout(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { String username = request.getParameter("username"); String token = request.getParameter("token"); int loginType = StringUtil.convertToInt(request.getParameter("loginType")); //验证账号和token NoCodeResult noCodeResult = LoginService.validateUserToken(username, token, loginType); if (noCodeResult == null) { ExceptionUtil.printlnFailure(response, "未知错误"); return; } //失败,返回失败原因 if (!noCodeResult.isSuccess()) { ExceptionUtil.printlnFailure(response, noCodeResult.getInfo()); return; } int userId = StringUtil.convertToInt(noCodeResult.getData()); if (userId <= 0) { ExceptionUtil.printlnFailure(response, "获取用户id失败"); return; } //调用退出 noCodeResult = LoginService.userLogout(userId, loginType); if (noCodeResult == null) { return; } //返回结果 if (!noCodeResult.isSuccess()) { ExceptionUtil.printlnFailure(response, noCodeResult.getInfo()); } else { ExceptionUtil.printlnSuccess(response, noCodeResult.getInfo()); } }

3.3.3.账号验证
相比web接口方式,SDK方式多了token验证。将username和token传递给验证方法,返回验证结果。
方法格式定义如下,有2种重载。
第2种是对第一种的再次封装,针对servlet方式使用更简洁。直接传到request和response对象进去,如果验证不通过,会通过response输出错误信息。如果验证通过,则直接返回用户id值。
//验证账号和token是否匹配 public static NoCodeResult validateUserToken(String username, String token, int loginType) { NoCodeResult noCodeResult = new NoCodeResult(); //…………代码省略………… return noCodeResult; }//验证账号和token是否匹配,会自动输出错误信息,成功后返回用户id public static int validateUserToken(HttpServletRequest request, HttpServletResponse response) throws IOException { String username = request.getParameter("username"); String token = request.getParameter("token"); int loginType = StringUtil.convertToInt(request.getParameter("loginType")); //验证账号和token NoCodeResult noCodeResult = LoginService.validateUserToken(username, token, loginType); if (noCodeResult == null) { ExceptionUtil.printlnFailure(response, "未知错误"); return 0; } //失败,返回失败原因 if (!noCodeResult.isSuccess()) { ExceptionUtil.printlnFailure(response, noCodeResult.getInfo()); return 0; } int userId = StringUtil.convertToInt(noCodeResult.getData()); if (userId <= 0) { ExceptionUtil.printlnFailure(response, "获取用户id失败"); return 0; } //返回用户id return userId; }

将验证结果存储在NoCodeResult对象实体中。如果验证成功,则将用户主键Id存储在data属性中。
一般情况下,对账号和token的验证,都是放在过滤器中统一执行。如果不方便过滤,那么就在功能页面前面统一调用。
调用执行,样例代码见上面的退出功能。
3.3.4.修改密码
修改密码是通用的功能,也提供接口方法。
方法定义如下,有2种重载。需要传递用户id或者账号、旧密码、新密码。修改密码会验证旧密码是否匹配。因为token本地化存储后,存在一定的安全风险。某些敏感操作,还是需要进一步的验证用户合法性。
注意:密码都是未加密前的值。
//当前用户修改密码 public static NoCodeResult modifyPassword(String username, String oldPassword, String newPassword) { NoCodeResult noCodeResult = new NoCodeResult(); //…………代码省略………… return noCodeResult; }//当前用户修改密码 public static NoCodeResult modifyPassword(int userId, String oldPassword, String newPassword) { NoCodeResult noCodeResult = new NoCodeResult(); //…………代码省略………… return noCodeResult; }

调用样例如下。返回结果仍然存储在NoCodeResult中。如果失败,有可能是原密码不正确等等。从失败原因中读取。
//当前用户修改密码 private void modifyPassword(HttpServletRequest request, HttpServletResponse response) throws IOException { String oldPassword = request.getParameter("oldPassword"); String newPassword = request.getParameter("newPassword"); String newPassword2 = request.getParameter("newPassword2"); if (StringUtil.isEmpty(oldPassword) || oldPassword.length() > 20) { ExceptionUtil.printlnFailure(response, "旧密码长度为1到20位"); return; } if (StringUtil.isEmpty(newPassword) || newPassword.length() > 20) { ExceptionUtil.printlnFailure(response, "新密码长度为1到20位"); return; } if (StringUtil.isEmpty(newPassword2) || newPassword2.length() > 20) { ExceptionUtil.printlnFailure(response, "确认新密码长度为1到20位"); return; } if (!newPassword.equals(newPassword2)) { ExceptionUtil.printlnFailure(response, "两次输入新密码不一样"); return; } //使用另一个验证方法,直接返回用户id,并且会输出错误信息 int userId = LoginService.validateUserToken(request, response); //用户id没有取到,已报错,返回。 if (userId <= 0) { return; } //修改密码 NoCodeResult noCodeResult = LoginService.modifyPassword(userId, oldPassword, newPassword); if (noCodeResult == null) { ExceptionUtil.printlnFailure(response, "未知错误"); return; } //失败,返回失败原因 if (!noCodeResult.isSuccess()) { ExceptionUtil.printlnFailure(response, noCodeResult.getInfo()); return; } ExceptionUtil.printlnSuccess(response, "密码修改成功"); }


4.前端样例 前端分为WEB网页前端,和APP客户端。前端需要处理的信息,主要是对登录返回的token进行本地化存储,提交请求时带上参数。退出登录后,清空本地token。
4.1.WEB前端 参见链接:编写独立的登录页(替换框架自带登录页)
4.2.APP前端
5.框架优势 框架自带的方法,一方面可以减少开发量,另一方面会和系统贴合的更紧密。登录和退出的功能,都做的相对比较完善。
  • 登录功能会自动完成账号验证(密码、状态)、token更新、登录日志记录、session创建等工作。
  • 退出也会清空token、注销session。
  • 验证token通过后,返回用户id。通过username和token,避免用户id泄露。
【登录验证过程,PC与APP开放登录接口(支持WEB与SDK方式)】如果自己去写登录退出功能,也要切记同样处理这些工作。

    推荐阅读