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

1.需求场景

登录,是一个系统的第一步功能。登录成功后,才能进入系统,使用系统功能。在某些场景下,其它系统需要接入到本系统中。或者通过接口的方式进行登录,以及通过接口的方式来取数据。

另外,系统配套移动APP端,变得越来越常见。因此,支持移动端的登录,也变动同样重要。本文介绍登录这块的功能,以及常规验证机制。

2.实现原理

2.1.登录提交

客户端(不管是WEB网页还是APP端)每次登录,服务端都需要记录登录日志。日志内容包括登录账号、是否登录成功,以及客户端的详细配置信息。因此提交到服务端的数据,是越详细越好。

必填的数据,是账号和密码。选填的信息,比如分辨率,需要客户端传递过去(服务端无法获取)。其他信息(比如浏览器、操作系统)则可以从来源信息中提取。

登录验证的流程,如下图所示。

登录验证过程,PC与APP开放登录接口(支持WEB与SDK方式)_第1张图片

登录过程详细说明如下:

  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 = 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泄露。

如果自己去写登录退出功能,也要切记同样处理这些工作。

你可能感兴趣的:(太极开发框架)