这篇文章是我们企业开发实战篇应用spring拦截器的第二篇文章,第一篇《解决跨域问题》,该篇文章我们主要讲解下如何使用拦截器+自定义注解来实现登录鉴权校验的功能,关于拦截器和自定义注解的基础语法下面也会稍带讲解,但不是本文重点,所以讲解的可能不会很细O(∩_∩)O。
我们需要一个Java注解,使用这个注解标记在Controller的类或某个方法上时,就代表着该类下所有方法或某个注解标记的方法需要登录后才可以进行访问。
首先我们需要一个注解,名字定义为@LoginedAuth,然后需要使用拦截器,名字就定为LoginedAuthInterceptor,在调用controller方法前该拦截器根据调用类的基本信息判定是否标记了登录鉴权注解,如果在所在方法或该方法所在类上有标记,则进行登录鉴权逻辑。
那么用户的登录身份信息放在哪里呢?我们约定用户的身份信息通过http请求头参数方式传递,其参数字段名字为X-Token,当然在你的系统中也可以设计为放入cookie+header两种并行的方式。
登录鉴权注解
package com.zhuma.demo.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @desc 已登录权限验证注解
*
* @author zhumaer
* @since 10/17/2017 3:13 PM
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LoginedAuth {
}
说明
注解类上有三个注解
更详细的说明可参考这里
拦截器类
package com.zhuma.demo.interceptors;
import java.lang.reflect.Method;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import com.zhuma.demo.annotations.LoginedAuth;
import com.zhuma.demo.enums.ResultCode;
import com.zhuma.demo.exceptions.BusinessException;
import com.zhuma.demo.helpers.LoginHelper;
import com.zhuma.demo.model.po.User;
import com.zhuma.demo.utils.StringUtil;
/**
* @desc 已登录权限验证拦截器 备注:通过{@link LoginedAuth}配合使用
*
* @author zhumaer
* @since 10/17/2017 3:00 PM
*/
@Component
public class LoginedAuthInterceptor implements HandlerInterceptor {
@Autowired
private UserLoginCacheService userLoginCacheService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
final HandlerMethod handlerMethod = (HandlerMethod) handler;
final Class<?> clazz = handlerMethod.getBeanType();
final Method method = handlerMethod.getMethod();
if (clazz.isAnnotationPresent(LoginedAuth.class) || method.isAnnotationPresent(LoginedAuth.class)) {
//直接获取登录用户(防止请求转发时,第二次查询)
User loginedUser = LoginHelper.getLoginUserFromRequest(request);
if (loginedUser != null) {
return true;
}
//获取登录账号名
String loginAccount = LoginHelper.getLoginAccount(request);
if (StringUtil.isEmpty(loginAccount)) {
throw new BusinessException(ResultCode.USER_NOT_LOGGED_IN);
}
//获取登录人信息
User loginUser = userLoginCacheService.getLoginUser(Long.valueOf(loginAccount));
if (loginUser == null) {
throw new BusinessException(ResultCode.USER_NOT_LOGGED_IN);
}
//登录用户信息放入请求对象,方便后续controller中获取
LoginHelper.addLoginUserToRequest(request, loginUser);
return true;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// nothing to do
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// nothing to do
}
}
说明
这里实现了拦截器HandlerInterceptor接口来定义我们自己的拦截器,鉴定是否登陆的逻辑写在preHandle方法中,也就是在进入controller方法前做一些业务逻辑的处理。
if (clazz.isAnnotationPresent(LoginedAuth.class) || method.isAnnotationPresent(LoginedAuth.class))
上述代码是在判断LoginedAuth注解是标记在所调用的方法上还是在其类上
登录鉴权的主要业务逻辑解释是当发现用户没有做登陆的时候,立即抛出一个自定义的业务异常BusinessException,如果登录则return true继续执行后续代码。
User loginedUser = LoginHelper.getLoginUserFromRequest(request);
if (loginedUser != null) {
return true;
}
说明下为什么会有上面的这个逻辑,是为了防止在spring mvc中程序进行了请求的转发和重定向后,request参数信息仍然会被传递下一个方法中所以没有必要再次去走一遍登录校验逻辑。
这里我们使用了自定义异常来结束程序,由最外层的全局异常捕捉类捕获处理后给用户端返回,我们会在后续中讲解下开发中如何定义异常、如何更好的使用异常、全局异常类该如何处理。
用户登录辅助类
package com.dx.ia.helper;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.zhuma.demo.annotations.LoginedAuth;
import com.zhuma.demo.enums.Time;
import com.zhuma.demo.model.po.User;
import com.zhuma.demo.utils.AESUtil;
import com.zhuma.demo.utils.CookieUtil;
import com.zhuma.demo.utils.StringUtil;
/**
* @desc 用户登录辅助类
*
* @author zhumaer
* @since 10/18/2017 18:31 PM
*/
public class LoginHelper {
private final static String SECRET_KEY = "wo-shi-test-mi-yao";
private final static String USER_TOKEN_NAME = "X-Token";
private final static String LOGIN_USER_KEY = "LOGINED-USER";
/**
* 获取登录账号信息到COOKIE中
*/
public static String getLoginAccountFromCookie(HttpServletRequest request) {
String encodeLoginToken = CookieUtil.getCookieValue(request, USER_TOKEN_NAME, true);
if (StringUtil.isEmpty(encodeLoginToken)) {
return null;
}
return decodeLoginToken(encodeLoginToken);
}
/**
* 获取登录账号信息从请求对象中
*/
public static String getLoginAccount(HttpServletRequest request) {
String loginAccount = request.getHeader(USER_TOKEN_NAME);
if (StringUtil.isNotEmpty(loginAccount)) {
return decodeLoginToken(loginAccount);
}
loginAccount = getLoginAccountFromCookie(request);
if (StringUtil.isNotEmpty(loginAccount)) {
return loginAccount;
}
return null;
}
public static String encodeLoginToken(String loginAccount) {
if (StringUtil.isEmpty(loginAccount)) {
return null;
}
return AESUtil.encrypt(loginAccount, SECRET_KEY);
}
public static String decodeLoginToken(String loginToken) {
if (StringUtil.isEmpty(loginToken)) {
return null;
}
return AESUtil.decrypt(loginToken, SECRET_KEY);
}
/**
* 将用户信息放入请求对象
*/
public static void addLoginUserToRequest(HttpServletRequest request, User user) {
request.setAttribute(LOGIN_USER_KEY, user);
}
/**
* 获取登录用户信息从请求对象 备注:使用该方法时需要在对应controller类或方法上加{@link LoginedAuth}注解
*/
public static User getLoginUserFromRequest(HttpServletRequest request) {
Object userO = request.getAttribute(LOGIN_USER_KEY);
if (userO == null) {
return null;
}
return (User)userO;
}
}
说明
上面用户辅助类的代码是不全的,这里只给大家做个参考。
拦截器配置类(以spring boot为例)
package com.zhuma.demo.configs;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import com.zhuma.demo.constants.Constants;
import com.zhuma.demo.interceptors.LoginedAuthInterceptor;
/**
* @desc WEB配置类
*
* @author zhumaer
* @since 10/18/2017 14:11 PM
*/
@Configuration
public class WebAppConfig extends WebMvcConfigurerAdapter {
private LoginedAuthInterceptor loginedAuthInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
String apiUri = "/" + Constants.API_ROOT + "/**";
//登录拦截
registry.addInterceptor(loginedAuthInterceptor).addPathPatterns(apiUri);
}
}
说明
上述配置后拦截器才会正真的在程序中生效,如果你的程序中有多个拦截器,并且你希望他们按照你的想法顺序调用,可以调整多个registry.addInterceptor方法的代码顺序来控制拦截器的调用顺序。
若上述文字有描述不清楚,或者你有哪里不太懂的可以评论留言或者关注我的公众号,公众号里也会不定期的发送一些关于企业实战相关的文章哈!
源码github地址:https://github.com/zhumaer/zhuma
QQ群号:629446754(欢迎加群)
欢迎关注我们的公众号或加群,等你哦!