首先抛出一个问题,什么情况下才需要登录操作,其实登录操作在很多的管理系统,后台系统中都会涉及到的一个看似简单,但是又特别重要的操作
在之前我总觉得登录应该是一个很简单的操作,验证数据库?然后通过.但是这样做的一个简单的判断,能完成登录操作,但是?我能不能绕过你的登录呢?答案是可以的.我最开始可以不调用你的登录接口,我直接调用你的后台其他接口,就能实现绕过验证,进行操作你的管理系统.这样你的登录操作对我来说,形同虚设.
答案是有的,这里就引出了一个概念:拦截
,拦截是什么?拦截简单点说就是做一个拦截操作,不满足我的条件时,你无法访问我设计的一些接口.所以这里,我们需要了解一下拦截器: Interceptor
这里的拦截器讲得是mvc中的拦截器,后面小例子会用一个springboot项目作为演示.这里用到的拦截器类似于servlet中的Filter过滤器.用于拦截用户的请求,进行一些权限验证,登录等操作
首先定义一个拦截器,有四种方法:
这里我们通过第二种方式来实现我们登录操作:
public class LoginInterceptor extends HandlerInterceptorAdapter
我们来分析一下HandlerInterceptorAdapter
package org.springframework.web.servlet.handler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;
import org.springframework.web.servlet.AsyncHandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
public abstract class HandlerInterceptorAdapter implements AsyncHandlerInterceptor {
public HandlerInterceptorAdapter() {
}
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
}
在这个源码中我们可以看到HandlerInterceptorAdapter实现了AsyncHandlerInterceptor
接口的三个方法,分别是preHandle
postHandle
afterCompletion
,
首先分析第一个方法:
preHandle()
:预处理回调方法,若方法返回值为true,请求继续(调用下一个拦截器或处理器方法);若方法返回值为false,请求处理流程中断,不会继续调用其他的拦截器或处理器方法,此时需要通过response产生响应;
简单点说就是:在每次调用需要拦截器验证的接口时,会预先调用这个preHandler方法
用一个小例子来说明:
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
Anonymous anonymous = (handler instanceof HandlerMethod) ?
((HandlerMethod) handler).getMethodAnnotation(Anonymous.class) : null;
if (null != anonymous && anonymous.value()) {
return true;
}
String token = cookieUtil.getValue(request, CookieUtil.TOKEN);
RequestAdmin.Admin admin = adminManageService.identify(token);
if(null == admin) {
throw new BizException(SystemCode.NEED_LOGIN);
}
//延长cookie
cookieUtil.setToken(request, response, token);
RequestAdmin.put(admin);
return true;
}
说明:这里实现登录用了一个Anonymous,我们可以理解把这个理解为访客模式,也就是说在执行某个接口的时候,可以不需要验证,直接通过拦截器.当然,这里需要在方法上加上一个自定义的anonymous注解,接着我们看下面,通过一个cookie工具类或者cookie的值,工具类具体代码如下:
@Configuration
public class CookieUtil {
@Value("${manage.cookie.domain}")
private String domain;
@Value("${manage.cookie.maxAge}")
private Integer maxAge;
public static final String TOKEN = "token";
public void setToken(HttpServletRequest request, HttpServletResponse response, String cookieValue) {
setCookie(request, response, TOKEN, cookieValue, "/", maxAge);
}
public void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, String path, int maxAge) {
try {
Cookie cookie = new Cookie(cookieName, URLEncoder.encode(cookieValue, "utf-8"));
cookie.setMaxAge(maxAge);
cookie.setPath(path);
cookie.setDomain(domain);
cookie.setHttpOnly(true);
response.addCookie(cookie);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
/**
* 获取cookie值
*
* @param request
* @param name
* @return
*/
public String getValue(HttpServletRequest request, String name) {
String value = null;
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (name.equals(cookie.getName())) {
try {
value = URLDecoder.decode(cookie.getValue(), "utf-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e.getMessage());
}
break;
}
}
}
return value;
}
}
通过调用这个方法,从浏览器请求中获取到浏览器携带过来的cookie,然后将这个cookie通过设置的cookie前缀拿出来,这里是key-value结构.获取到这个cookie信息之后我们调用方式去识别这个cookie信息是否正确,为什么要这一步呢?,因为浏览器的cookie是多种多样的,我们要识别这个携带过来的cookie是不是我们在登录时设置的那个,而不是cookie有值就可以了.同样这里也通过这个方法获取到了我们放进入的用户信息
getValue方法:
public String getValue(HttpServletRequest request, String name) {
String value = null;
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (name.equals(cookie.getName())) {
try {
value = URLDecoder.decode(cookie.getValue(), "utf-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e.getMessage());
}
break;
}
}
}
return value;
}
这里拿到请求带过来的cookie,进行一个遍历操作,然后获取key相同的那个,也就是name相同的,拿着这个cookie进行解码操作value = URLDecoder.decode(cookie.getValue(), "utf-8");
,解码完成后退出循环,这样就保证拿到了解码后的cookie值.
接着我们拿到了从cookie解码后的值设置为token,这里因为项目后面需要记录这个登陆者的信息,所以我们要用一个ThreadLocal存储这个登录人的信息,也就是代码中调用的identify方法,这个方法中:
/**
* 添加全局请求辅助类
*
* @param token 用户信息
* @return admin
*/
@Override
public RequestAdmin.Admin identify(String token) {
if (StringUtils.isBlank(token)) {
return null;
}
Admin admin = cacheClient.get(CachePrefix.TOKEN, token);
if (null == admin) {
return null;
}
cacheClient.increment(CachePrefix.TOKEN, token, 60 * 60L);
RequestAdmin.Admin requestAdmin = new RequestAdmin.Admin();
requestAdmin.setId(admin.getId());
if (StringUtils.isNotBlank(admin.getUserName())) {
requestAdmin.setUserName(admin.getUserName());
}
if (StringUtils.isNotBlank(admin.getPassword())) {
requestAdmin.setPassword(admin.getPassword());
}
return requestAdmin;
}
上面代码的操作就是获取到一个全局都能调用的信息:比如说我要在其他接口上用到这个登录者的信息,那我不需要调用数据库去查询,只要设置这个全局请求类,后面调用他就可以了
做完上面的操作后,然后延长cookie的时间,也就是说每次请求都会延长cookie的时间.
这就是preHandler方法做的事,如果验证通过的则可以进入下一步了
postHandle()
:后处理回调方法,实现处理器的后处理(但在渲染视图之前),此时可以通过modelAndView对模型数据进行处理或对视图进行处理;(解释:对接口中的数据进行处理,这个时候并没有会回调参数给前端页面上),我做的这个springboot项目是前后分离的,所以在这个方法中就没有做操作
afterCompletion()
:整个请求处理完毕回调方法,即在视图渲染完毕时调用;
这里的话其实一般就是在调用接口完成后进行一个全局信息的清理.
上面步骤中,只是说到了拦截器起到的作用,但是比没有解释上面的cookie,token,redis怎么设置的,在拦截器中只是做了一个判断是否存在.
回到正题:怎么实现登录操作:
在上面我们一直解释拦截器怎么去保证后面接口的一个安全性.接下俩我们说说登录的接口中应该做什么?
让我们看一段实际项目的代码:
public Boolean login(String userName, String password, HttpServletRequest request, HttpServletResponse response) {
if (StringUtils.isBlank(userName) || StringUtils.isBlank(password)) {
log.error("AdminManageServiceImpl --- login 登录账号信息异常 userName = {},password = {}",userName,password);
throw new BizException(BizErrorCode.ACCOUNT_ERROR);
}
Admin admin = adminService.getAdmin(userName);
if (null == admin) {
log.error("AdminManageServiceImpl --- login 获取admin信息异常 userName = {}",userName);
throw new BizException(BizErrorCode.ACCOUNT_NOT_EXIST);
}
if (!admin.getPassword().equals(MD5.encrypt(password))) {
log.error("AdminManageServiceImpl --- login 获取admin信息异常 password = {}",password);
throw new BizException(BizErrorCode.ACCOUNT_PASSWORD_ERROR);
}
//存储token信息
String token = EncryptUtils.md5(UUID.randomUUID() + admin.getId().toString() + System.currentTimeMillis());
AdminToken adminToken = adminTokenService.getAdminToken(admin.getAdminId());
if (null == adminToken) {
AdminToken tokenInfo = new AdminToken();
tokenInfo.setAdminId(admin.getAdminId());
tokenInfo.setToken(token);
boolean b = adminTokenService.saveAdminToken(tokenInfo);
System.out.println(b);
} else {
//删除在redis上的,然后把新的设置进入
cacheClient.delete(CachePrefix.TOKEN, adminToken.getToken());
adminToken.setToken(token);
adminTokenService.updateAdminToken(adminToken);
}
//设计token缓存信息
cacheClient.set(CachePrefix.TOKEN, token, admin, 60 * 60L);
if (null == admin.getUserName()){
log.info("AdminManageServiceImpl --- login 获取admin中的用户名失败 userName = {}",userName);
throw new BizException(BizErrorCode.ACCOUNT_ERROR);
}
String token1 = getToken(userName);
cookieUtil.setToken(request,response,token1);
return admin.getUserName() != null;
}
解释一下这段代码:
首先我们获取到前端输入的用户名和密码之后,首先要做的就是对这个用户名和密码进行非空验证,验证通过之后在进行一个查询数据库中用户表信息的操作,同时也要对查询来的用户对象进行非空判断,为什么要做一步呢?,很多人在想,如果说我都能查出来的,那为什么还需要这一步验证呢?
解释:由于我们查询条件是按照用户名进行查询,不排除查数据库中有用户名但是没有密码,或者说含有逻辑删除(逻辑删除:这个mybatisPlus中做的一个功能,执行删除操作时,并不会真正的删除这条信息,只是改变该条信息的状态位),这就是异常处理,一个完整程序必须要做的.
接着我们设定一条存储信息,也就是设置一个新的token,然后我们通过登录者的id去查询token表中对应的token(旧的token).如果说根据id没有查到token时,就将存储这个新的token.
如果这个token不是空的,也就是会出现多个人登录同一个账号的情况下,就删除redis中对应储存的token,然后更新数据库中的这个旧的token.
接着设计redis中新的一个这个token,并设置过期时间,并通过cookie工具类去设置cookie信息,并在这个cookie信息中设置最大过期时间.
这样我们在redis中设置了过期时间,同时在cookie中设置了过期时间.
@Override
public Boolean loginOut() {
AdminToken token = adminTokenService.getAdminToken(RequestAdmin.getId());
cacheClient.delete(CachePrefix.TOKEN,token.getToken());
boolean status = adminTokenService.removeById(RequestAdmin.getId());
if (!status){
log.info("AdminManageServiceImpl --- loginOut 获取token失败 token = {}",token);
throw new BizException(BizErrorCode.LOGOUT_ERROR);
}
return true;
}
很简单,在我们登出的时候,删除redis中的token记录,同时删掉数据库中的token.
以上就是实现登录操作的思路,如果有问题可以下方留言,文章有不足之处,还请指正.