拦截器一般用的还是比较多的,刚开始只知道在springmvc.xml文件中,配置拦截器和配置放行的方法,在拦截器中使用session判断当前用户是不是登录状态,也就是session是否在有效期内。
这次接触到一个新的项目,是一个APP的后台,在controller的方法里面使用了注解@LoginRequired
,刚开始接接手这个项目的时候,还以为是用的某个框架里面的,此注解用在方法上,表示这个方法需要登录状态才能访问,后来在翻项目的源码的时候看到是使用的自定义的注解加拦截器实现的登录拦截。
感觉还挺好用的,这里记录一下
首先是新建一个注解接口文件:LoginRequired.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created by wangH on 2017/10/24.
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
}
注解:
@Target 用于描述注解的使用范围(即:被描述的注解可以用在什么地方),其取值可以是枚举类型ElementType的值:
取值 | 描述 |
---|---|
CONSTRUCTOR | 用于描述构造器(领盒饭) |
FIELD | 用于描述域(领盒饭) |
LOCAL_VARIABLE | 用于描述构造器(领盒饭) |
METHOD | 用于描述方法 |
PACKAGE | 用于描述包(领盒饭) |
PARAMETER | 用于描述参数 |
TYPE | 用于描述类或接口(领盒饭) |
这里使用的是METHOD证明此注解用于方法上。
@Retention 用于描述注解的生命周期(即:被描述的注解在什么范围内有效),其取值有:
取值 | 描述 |
---|---|
SOURCE | 在源文件中有效(即源文件保留,领盒饭) |
CLASS | 在 class 文件中有效(即 class 保留,领盒饭) |
RUNTIME | 在运行时有效(即运行时保留) |
另外的两个注解:
@Documented 在默认的情况下javadoc命令不会将我们的annotation生成再doc中去的,所以使用该标记就是告诉jdk让它也将annotation生成到doc中去
@Inherited 比如有一个类A,在他上面有一个标记annotation,那么A的子类B是否不用再次标记annotation就可以继承得到呢,答案是肯定的。
记录一些题外话:
可以看到在LoginRequired 上面的两个注解后面都有自己的属性值,当然也可以在自定义注解中设置对应的值,其中注解后面的值可以是三种类型的:
1:基本串类型
public @interface UserdefinedAnnotation {
intvalue();
String name();
String address();
}
使用:
@UserdefinedAnnotation(value=123,name="wangwenjun",address="火星")
public static void main(String[] args) {
System.out.println("hello");
}
}
如果一个annotation中只有一个属性名字叫value,我没在使用的时候可以给出属性名也可以省略。
public @interface UserdefinedAnnotation {
int value();
}
也可以写成如下的形式
Java代码
@UserdefinedAnnotation(123) 等同于@UserdefinedAnnotation(value=123)
public static void main(String[] args) {
System.out.println("hello");
}
2:数组类型 我们在自定义annotation中定义一个数组类型的属性,代码如下:
public @interface UserdefinedAnnotation {
int[] value();
}
使用:
public class UseAnnotation {
@UserdefinedAnnotation({123})
public static void main(String[] args) {
System.out.println("hello");
}
}
注意1:其中123外面的大括号是可以被省略的,因为只有一个元素,如果里面有一个以上的元素的话,花括号是不能被省略的哦。比如{123,234}。
注意2:其中属性名value我们在使用的时候进行了省略,那是因为他叫value,如果是其他名字我们就不可以进行省略了必须是@UserdefinedAnnotation(属性名={123,234})这样的格式。
3:枚举类型
public enum DateEnum {
Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday
}
然后在定义一个annotation
package com.wangwenjun.annatation.userdefined;
public @interface UserdefinedAnnotation {
DateEnum week();
}
使用:
public class UseAnnotation {
@UserdefinedAnnotation(week=DateEnum.Sunday)
public static void main(String[] args) {
System.out.println("hello");
}
}
4.默认值
public @interface UserdefinedAnnotation {
String name() default "zhangsan";
}
使用:
public class UseAnnotation {
@UserdefinedAnnotation()
public static void main(String[] args) {
System.out.println("hello");
}
}
注意 :
Annotation是不可以继承其他接口的,这一点是需要进行注意,这也是annotation的一个规定吧。
Annotation也是存在包结构的,在使用的时候直接进行导入即可。
Annotation类型的类型只支持原声数据类型,枚举类型和Class类型的一维数组,其他的类型或者用户自定义的类都是不可以作为annotation的类型。
注解参数值介绍参考:https://www.cnblogs.com/shipengzhi/articles/2716004.html
好了 下面回归正题
自定义的注解有了,下面就是拦截器MemberloginInterceptor.java的内容了,拦截器中还包括token的验证:
由于项目使用的JeeSite快速开发平台,在这里的拦截器还继承了BaseService ,这个可以去掉,直接实现HandlerInterceptor 接口即可。
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.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.thinkgem.jeesite.common.service.BaseService;
import com.thinkgem.jeesite.modules.ejt.entity.AppTokenRecord;
import com.thinkgem.jeesite.modules.ejt.service.AppTokenRecordService;
import com.thinkgem.jeesite.modules.ejt.util.JsonUtil;
import com.thinkgem.jeesite.modules.ejt.util.SignUtil;
public class MemberloginInterceptor extends BaseService implements HandlerInterceptor {
@Autowired
private AppTokenRecordService appTokenRecordService;
/**
* Handler执行之前调用这个方法
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
response.setHeader("Access-Control-Allow-Origin", "*");
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
LoginRequired methodAnnotation = method.getAnnotation(LoginRequired.class);
// 判断请求的接口方法上是否含有自定义注解LoginRequired,如果没有,返回true,继续进行,如果有,则判断登录,验证签名和token
if (methodAnnotation != null) {
// controller类中的方法添加自定义注解后 请求才能进来
String data=request.getParameter("data"); //用户登录的数据
String sign=request.getParameter("sign"); // 签名
JSONObject obj=JSON.parseObject(data);
String memberId=(String)obj.get("memberId");
AppTokenRecord findByMemberId = appTokenRecordService.findByMemberId(memberId);
String token=findByMemberId.getAppToken();
//验签,获取传递的参数,
boolean checkSign = SignUtil.checkSign(data,sign,token);
if(checkSign){
int aaa=appTokenRecordService.checkToken(data,token);
//验证token有效性
if(aaa==1){
response.setCharacterEncoding("UTF-8");
response.getWriter().write(JSON.toJSONString(JsonUtil.getJson(800, null, "token 不正确")));
return false;
}else if(aaa==2){
return true;
}else{
response.setCharacterEncoding("UTF-8");
response.getWriter().write(JSON.toJSONString(JsonUtil.getJson(900, null, "token 过期")));
return false;
}
}else{
response.setCharacterEncoding("UTF-8");
response.getWriter().write(JSON.toJSONString(JsonUtil.getJson(700, null, "签名不一致")));
return false;
}
}
return true;
}
/**
* Handler执行之后,ModelAndView返回之前调用这个方法
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) throws Exception {
String name = request.getServletPath().toString();
// System.out.println("========"+name+"===>LoginInterceptor postHandle");
}
/**
* Handler执行完成之后调用这个方法
*/
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception exc)
throws Exception {
String name = request.getServletPath().toString();
// System.out.println("========"+name+"===>LoginInterceptor afterCompletion");
}
}
其中添加了拦截器之后,记得在springmvc.xml中配置拦截器,不然拦截器是没用的,其中拦截器是可以配置多个的,执行顺序按照在配置文件的先后顺序执行:
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**" />
<bean
class="自定义注解MemberloginInterceptor的位置">
</bean>
</mvc:interceptor>
</mvc:interceptors>
这样在controller类中的方法上面,添加自定义的注解@LoginRequired
,即可进入拦截器中的验证签名和token的方法里面。
类似下面的使用:
// 根据当前用户查询订单
@LoginRequired
@RequestMapping("/selectOrder")
@ResponseBody
public JSONObject selectOrder(HttpServletRequest request, String data) {
JSONObject json = JSON.parseObject(data);
String memberId = json.getString("memberId");
}
到这里自定义注解的使用算是结束了。
接下来是token的验证部分,实现使用@LoginRequired
注解的方法需要在登录状态才能访问:
这里单独创建了一个token表,用来存用户的token和有效时间
CREATE TABLE `app_token_record` (
`member_id` int(11) NOT NULL,
`app_token` varchar(64) DEFAULT NULL,
`active_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`member_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
在用户注册账号的时候,将用户的token保存下来,并设置一个有效截止时间,在用户进行操作的时候,将APP端传来的token跟数据库里用户的token比较,token正确并且token有效,即可通过验证。
在注册成功,用户数据写入数据库之后,开始初始化用户的token:
public String initializeToken(String id) {
// 获取当前时间 毫秒
Long time=System.currentTimeMillis();
// 设置token有效期为30天
String d = format.format(time+(long)1000*60*60*30*24);
Date date=null;
try {
date=format.parse(d);
} catch (ParseException e) {
e.printStackTrace();
}
//初始化token
AppTokenRecord appTokenRecord=new AppTokenRecord();
appTokenRecord.setMemberId(id);
appTokenRecord.setAppToken(TokenUtil.generateToken(id));
appTokenRecord.setActiveTime(date);
int insertSelective = insertSelective(appTokenRecord);
System.out.println(insertSelective);
return appTokenRecord.getAppToken();
}
token生成工具TokenUtil.java:
import java.security.MessageDigest;
public class TokenUtil {
private static final String YAN = "ejtMRf1$789aadf89jkds//*-+'[]eu;389785*^&%$%";// 加盐
/**
* 产生一个token
*/
public static String generateToken (String id){
return MD5(System.currentTimeMillis()+YAN+id);
}
/**
*
* @Title: MD5
* @Description: 加密
* @param @param s
* @param @return 参数
* @return String 返回类型
*/
public final static String MD5(String s) {
try {
byte[] btInput = s.getBytes();
// 获得MD5摘要算法的 MessageDigest 对象
MessageDigest mdInst = MessageDigest.getInstance("MD5");
// 使用指定的字节更新摘要
mdInst.update(btInput);
// 获得密文
return byte2hex(mdInst.digest());
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将字节数组转换成16进制字符串
* @param b
* @return
*/
private static String byte2hex(byte[] b) {
StringBuilder sbDes = new StringBuilder();
String tmp = null;
for (int i = 0; i < b.length; i++) {
tmp = (Integer.toHexString(b[i] & 0xFF));
if (tmp.length() == 1) {
sbDes.append("0");
}
sbDes.append(tmp);
}
return sbDes.toString();
}
public static void main(String[] args) {
System.out.println("token=" +MD5(System.currentTimeMillis()+YAN+"s"+1));
}
}
初始化token之后,需要将用户的id和token返回给APP,这样APP在登录之后,就可以拿到这个token来进行请求,后台进行验证即可。
注册接口:
public JSONObject memberlogin1(HttpServletRequest request,HttpServletResponse response,String phone,String InviteCode,String pwd){
//查询该手机号有没有验证码如果没有直接返回
List<AppSmsRecord> selectByPhoneNum = appSmsRecordService.selectByPhoneNumber(phone, "10");
if(selectByPhoneNum.isEmpty()||!selectByPhoneNum.get(0).getStatus().equals("1")){
return JsonUtil.getJson(30, null, "非法注册");
}
ShopMember member1=shopMemberService.findByMemberPhone(phone);
if(member1!=null){
return JsonUtil.getJson(30, null, "非法注册");
}
//将参数字符串转成json
ShopMember encapsulationMember = shopMemberService.encapsulationMember(phone,pwd,InviteCode);
Integer result=shopMemberService.insertSelective(encapsulationMember);
if(result > 0){
ShopMember member=shopMemberService.findByMemberPhone(phone);
//初始化用户token信息
String initializeToken = appTokenRecordService.initializeToken(member.getMemberId());
JSONObject json = new JSONObject();
json.put("token", initializeToken);
json.put("memberId", member.getMemberId());
String str=String.valueOf(json);
byte[] aa=null;
try {
aa=RSAUtils.encryptByPublicKey(str,ejtConfig.PUBLIC_KEY2);
} catch (Exception e1) {
e1.printStackTrace();
}
String base64 = Base64.encodeBase64String(aa);
System.out.println(base64);
return JsonUtil.getJson(10, base64, "注册成功");
}
return JsonUtil.getJson(20, null, "插入会员信息失败");
}
在注册成功之后,将用户的id和token先进行RSA加密之后,返回给APP端的。关于RSA加密的使用,可以参考这篇博客:【RSA加密】初探RSA并简单使用
下面是拦截器中验证token的部分SignUtil.java:
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import org.apache.commons.codec.digest.DigestUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
public class SignUtil {
/**
* @Description: 初始化签名
* @param characterEncoding
* @param parameters
* @param key
* @return String
*/
public static String createSign(String characterEncoding,SortedMap<Object,Object> parameters,String key){
StringBuffer sb = new StringBuffer();
StringBuffer sbkey = new StringBuffer();
Set<Entry<Object, Object>> es = parameters.entrySet(); //所有参与传参的参数按照accsii排序(升序)
Iterator<Entry<Object, Object>> it = es.iterator();
while(it.hasNext()) {
Map.Entry entry = (Map.Entry)it.next();
String k = (String)entry.getKey();
Object v = entry.getValue();
//空值不传递,不参与签名组串
if(null != v && !"".equals(v)) {
sb.append(k + "=" + v + "&");
sbkey.append(k + "=" + v + "&");
}
}
//System.out.println("字符串:"+sb.toString());
sbkey=sbkey.append("accessKey="+key);
System.out.println("字符串:"+sbkey.toString());
//MD5加密,结果转换为大写字符
String sign = MD5Util.MD5Encode(sbkey.toString(), characterEncoding).toUpperCase();
System.out.println("MD5加密值:"+sign);
return sign;
}
/**
* 验证签名
* @param data
* @param token
* @return
*/
public static boolean checkSign(String data,String sign, String token){
JSONObject obj=JSON.parseObject(data);
//json转换成map
SortedMap<Object, Object> params = JSONObject.parseObject(obj.toJSONString(), new TypeReference<TreeMap<Object, Object>>(){});
String characterEncoding = "UTF-8";
//指定字符集UTF-8
String mySign = createSign(characterEncoding,params,token);
return mySign.equals(sign);
}
}
最后是验证token的方法
/**
* 1:token 不正确
* 2:token 有效
* 3:token 过有效期
* 验证token
* @param token
* @param request
* @return
*/
public int checkToken(String data, String token) {
JSONObject obj=JSON.parseObject(data);
String memberId=(String)obj.get("memberId");
AppTokenRecord findByMemberId = findByMemberId(memberId);
if(!findByMemberId.getAppToken().equals(token)){
return 1;
}else{
if(DateUtil.compareDate(findByMemberId.getActiveTime() ,new Date())==1){
return 2;
}else{
return 3;
}
}
}
/**
* 退出登录,更改token有效期
* @param memberId
*/
public void refreshInitializeToken1(String memberId) {
//更新token
AppTokenRecord appTokenRecord=new AppTokenRecord();
//时间设置30天
appTokenRecord.setActiveTime(new Date());
appTokenRecord.setMemberId(memberId);
updateByPrimaryKeySelective(appTokenRecord);
}