登录,是一个系统的第一步功能。登录成功后,才能进入系统,使用系统功能。在某些场景下,其它系统需要接入到本系统中。或者通过接口的方式进行登录,以及通过接口的方式来取数据。
另外,系统配套移动APP端,变得越来越常见。因此,支持移动端的登录,也变动同样重要。本文介绍登录这块的功能,以及常规验证机制。
客户端(不管是WEB网页还是APP端)每次登录,服务端都需要记录登录日志。日志内容包括登录账号、是否登录成功,以及客户端的详细配置信息。因此提交到服务端的数据,是越详细越好。
必填的数据,是账号和密码。选填的信息,比如分辨率,需要客户端传递过去(服务端无法获取)。其他信息(比如浏览器、操作系统)则可以从来源信息中提取。
登录验证的流程,如下图所示。
登录过程详细说明如下:
登录日志记录的信息包括:登录账号、姓名昵称、登录结果、备注说明、登录方式、登录时间、登录IP、操作系统、浏览器、分辨率、浏览器头。
如果通过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": "账号不存在"
}
除了登录请求,后续的其他所有请求,都是需要带上登录时返回的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": "账号验证失败"
}
注意:在实际应用场景中,不管服务端是否返回成功,客户端都需要正常退出。总不能因为服务端返回失败,不让客户端退出。只是失败后,可以给予用户提示,这样根据用户反馈,去排除解决问题。
如果需要验证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不正确"
}
如果需要获取当前登录用户更多详细信息,以便进行下一步的处理,可以通过下面的接口。
请求链接: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不正确"
}
当前用户需要修改自己密码时,可调用下面的接口。
请求链接: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": "原密码不正确"
}
如果要二次开发,将登录验证功能集成在代码中,则需要使用SDK接口。SDK接口,只需要调用封装好的方法即可。
登录退出相关的方法,都封装在LoginService类中,使用静态方法,直接调用。
登录SDK有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; //备注说明。
}
退出方法有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());
}
}
相比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的验证,都是放在过滤器中统一执行。如果不方便过滤,那么就在功能页面前面统一调用。
调用执行,样例代码见上面的退出功能。
修改密码是通用的功能,也提供接口方法。
方法定义如下,有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, "密码修改成功");
}
前端分为WEB网页前端,和APP客户端。前端需要处理的信息,主要是对登录返回的token进行本地化存储,提交请求时带上参数。退出登录后,清空本地token。
参见链接:编写独立的登录页(替换框架自带登录页)
框架自带的方法,一方面可以减少开发量,另一方面会和系统贴合的更紧密。登录和退出的功能,都做的相对比较完善。
如果自己去写登录退出功能,也要切记同样处理这些工作。