springboot2.x+redis+token实现鉴权

流程分析:

如何利用redis检验token是否过期 redis的token_redis

如何利用redis检验token是否过期 redis的token_spring_02

1.客户端登录,输入用户名和密码,后台进行验证,如果验证失败则返回登录失败的提示。如果验证成功,则生成token然后将username和token双向绑定(可以根据username取出token也可以根据token取出username)存入redis,同时使用token+username作为key把当前时间戳也存入redis。并且给它们都设置过期时间。
2.每次请求都会走拦截器,如果该接口标注了@AuthToken注解,则要检查http header中的Authorization字段,获取token,然后由于token与username双向绑定,我们可以通过获取的token来尝试从redis中获取username,如果可以获取则说明token正确,反之,说明错误,返回鉴权失败。
3.token可以根据用户使用的情况来动态的调整自己过期时间。我们在生成token的同时也往redis里面存入了创建token时的时间戳,每次请求被拦截器拦截token 验证成功之后,将当前时间与存在redis里面的token生成时刻的时间戳进行比较,当当前时间的距离创建时间快要到达设置的redis过期时间的话,就重新设置token过期时间,将过期时间延长。如果用户在设置的redis过期时间的时间长度内没有进行任何操作(没有发请求),则token会在redis中过期。

这里不用jwt生成token,token由MD5算法加密username+password+时间戳得到

一、依赖(因为没有用JWT所以没有token的依赖)

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.5.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>


<!--redis缓存-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

二、目录结构

如何利用redis检验token是否过期 redis的token_jwt_03


三、自定义注解@AuthToken

package com.jinv.studentinfo.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义@AuthToken
 * @author jinv
 */
@Target({ElementType.METHOD , ElementType.TYPE})//Target说明了Annotation所修饰的对象范围:方法、接口
@Retention(RetentionPolicy.RUNTIME)//注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
public @interface AuthToken {

}

四、token构造
1.TokenGenerator接口(可以用别的实现类实现多种算法生成token)

package com.jinv.studentinfo.token;

import org.springframework.stereotype.Component;

/**
 * token生成器接口
 */
@Component
public interface TokenGenerator {
    public String generate(String ... strings);
}

2.Md5TokenGenerator(使用MD5算法根据username+password+时间戳生成token令牌)

package com.jinv.studentinfo.token;

import com.jinv.studentinfo.utils.RedisUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;

import javax.servlet.http.HttpServletRequest;

/**
 * token生成器
 * @author jinv
 */
@Component
public class Md5TokenGenerator implements TokenGenerator {
    @Autowired
    RedisUtils redisUtils;

    /**
     * token生成器
     * @param strings 参数可以多个
     * @return token token->(s1+s2...+时间戳)MD5加密
     */
    @Override
    public String generate(String... strings) {
        long timeStamp = System.currentTimeMillis();//获取当前时间戳
        StringBuilder tokenMeta = new StringBuilder();
        for (String s: strings){//整合所有字符串参数为一个
            tokenMeta.append(s);
        }
        tokenMeta.append(timeStamp);//加入时间戳
        String token = DigestUtils.md5DigestAsHex(tokenMeta.toString().getBytes());//MD5加密
        return token;
    }

    /**
     * token存入redis缓存
     * @param username  用户名
     * @param password  密码
     * @return token(username+password+时间戳)MD5加密
     */
    public String tokenUtils(String username, String password){
        //设置token
        String token = generate(username,password);
        redisUtils.set(username,token, ConstantKit.TOKEN_EXPIRE_TIME);//五分钟失效缓存
        redisUtils.set(token,username,ConstantKit.TOKEN_EXPIRE_TIME);
        Long currentTime = System.currentTimeMillis();//获取当前时间戳
        redisUtils.set(username+token,currentTime);//设置第一次登陆的时间为token的初始时间
        return token;
    }
}

3.ConstantKit(token有关的全局变量)

package com.jinv.studentinfo.token;

/**
 * token有关全局变量
 */
public final class ConstantKit {
    /**
     * 设置删除标志为真
     */
    public static final Integer DEL_FLAG_TRUE=1;

    /**
     * 设置删除标志为假
     */
    public static final Integer DEL_FLAG_FALSE=0;

    /**
     * redis存储token设置的过期时间
     * 单位:秒(1h)
     */
    public static final Integer TOKEN_EXPIRE_TIME=60*60;

    /**
     * 设置可以重置token过期时间的时间界限
     * 单位:毫秒(30min)
     */
    public static final Integer TOKEN_RESET_TIME=1000*60*30;
}

4、redisUtils工具类

package com.jinv.studentinfo.utils;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

/**
 * Redis工具类
 * 操作redis数据库
 * 底层封装了RedisTemplate<String,Object>
 */
@Component
public class RedisUtils {
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    /**
     * 指定缓存失效时间
     * @param key 键
     * @param time 时间(秒)
     * @return
     */
    public boolean expire(String key,long time){
        try {
            if(time>0){
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key){
        return redisTemplate.getExpire(key,TimeUnit.SECONDS);
    }

    /**
     * 判断key是否存在
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key){
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String ... key){
        if(key!=null&&key.length>0){
            if(key.length==1){
                redisTemplate.delete(key[0]);
            }else{
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }

    //============================String=============================
    /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
    public Object get(String key){
        return key==null?null:redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     * @param key 键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key,Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }

    /**
     * 普通缓存放入并设置时间
     * @param key 键
     * @param value 值
     * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key,Object value,long time){
        try {
            if(time>0){
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            }else{
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 递增
     * @param key 键
     * @param by 要增加几(大于0)
     * @return
     */
    public long incr(String key, long delta){
        if(delta<0){
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 递减
     * @param key 键
     * @param by 要减少几(小于0)
     * @return
     */
    public long decr(String key, long delta){
        if(delta<0){
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }

    //================================Map=================================
    /**
     * HashGet
     * @param key 键 不能为null
     * @param item 项 不能为null
     * @return 值
     */
    public Object hget(String key,String item){
        return redisTemplate.opsForHash().get(key, item);
    }

    /**
     * 获取hashKey对应的所有键值
     * @param key 键
     * @return 对应的多个键值
     */
    public Map<Object,Object> hmget(String key){
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * HashSet
     * @param key 键
     * @param map 对应多个键值
     * @return true 成功 false 失败
     */
    public boolean hmset(String key, Map<String,Object> map){
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * HashSet 并设置时间
     * @param key 键
     * @param map 对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String,Object> map, long time){
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if(time>0){
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     * @param key 键
     * @param item 项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key,String item,Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     * @param key 键
     * @param item 项
     * @param value 值
     * @param time 时间(秒)  注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key,String item,Object value,long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if(time>0){
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除hash表中的值
     * @param key 键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item){
        redisTemplate.opsForHash().delete(key,item);
    }

    /**
     * 判断hash表中是否有该项的值
     * @param key 键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item){
        return redisTemplate.opsForHash().hasKey(key, item);
    }

    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     * @param key 键
     * @param item 项
     * @param by 要增加几(大于0)
     * @return
     */
    public double hincr(String key, String item,double by){
        return redisTemplate.opsForHash().increment(key, item, by);
    }

    /**
     * hash递减
     * @param key 键
     * @param item 项
     * @param by 要减少记(小于0)
     * @return
     */
    public double hdecr(String key, String item,double by){
        return redisTemplate.opsForHash().increment(key, item,-by);
    }

    //============================set=============================
    /**
     * 根据key获取Set中的所有值
     * @param key 键
     * @return
     */
    public Set<Object> sGet(String key){
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 根据value从一个set中查询,是否存在
     * @param key 键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key,Object value){
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将数据放入set缓存
     * @param key 键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object...values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 将set数据放入缓存
     * @param key 键
     * @param time 时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key,long time,Object...values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if(time>0) expire(key, time);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 获取set缓存的长度
     * @param key 键
     * @return
     */
    public long sGetSetSize(String key){
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 移除值为value的
     * @param key 键
     * @param values 值 可以是多个
     * @return 移除的个数
     */
    public long setRemove(String key, Object ...values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    //===============================list=================================

    /**
     * 获取list缓存的内容
     * @param key 键
     * @param start 开始
     * @param end 结束  0 到 -1代表所有值
     * @return
     */
    public List<Object> lGet(String key,long start, long end){
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 获取list缓存的长度
     * @param key 键
     * @return
     */
    public long lGetListSize(String key){
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 通过索引 获取list中的值
     * @param key 键
     * @param index 索引  index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     * @return
     */
    public Object lGetIndex(String key,long index){
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @param time 时间(秒)
     * @return
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @param time 时间(秒)
     * @return
     */
    public boolean lSet(String key, Object value, long time) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0) expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @param time 时间(秒)
     * @return
     */
    public boolean lSet(String key, List<Object> value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @param time 时间(秒)
     * @return
     */
    public boolean lSet(String key, List<Object> value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0) expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据索引修改list中的某条数据
     * @param key 键
     * @param index 索引
     * @param value 值
     * @return
     */
    public boolean lUpdateIndex(String key, long index,Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 移除N个值为value
     * @param key 键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */
    public long lRemove(String key,long count,Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
}

六、(最重要的)AuthorizantionInterceptor拦截器
拦截器定义每个有@AuthToken注解的方法在被调用前,都要根据验证token
如何验证token:获取请求头header中的Authorization中的token,去redis缓存中查询,有则允许,无则返回到登录页面
并且根据redis缓存里查询token的失效时间,判断是否需要重新定义失效时间(加时)

package com.jinv.studentinfo.interception;

import com.alibaba.fastjson.JSONObject;
import com.jinv.studentinfo.annotation.AuthToken;
import com.jinv.studentinfo.token.ConstantKit;
import com.jinv.studentinfo.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.lang.reflect.Method;

/**
 * 鉴权拦截器
 * 使用token鉴权
 */
@Slf4j
public class AuthorizationInterceptor implements HandlerInterceptor {

    @Autowired
    private RedisUtils redisUtils;

    //存放鉴权信息的Header名称,默认是Authorization
    private String httpHeaderName = "Authorization";

    //鉴权失败后返回的错误信息,默认是401 unauthorized
    private String unauthorizedErrorMessage = "401 unathorized";

    //鉴权失败后返回的HTTP错误码,默认为401
    private int unauthorizedErrorCode = HttpServletResponse.SC_UNAUTHORIZED;

    //存放登录用户模型Key的Request key
    public static final String REQUEST_CURRENT_KEY = "REUQEST_CURRENT_KEY";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        if (!(handler instanceof HandlerMethod)){
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        //如果有@AuthoToken注解则需要验证token:
        if (method.getAnnotation(AuthToken.class)!=null || handlerMethod.getBeanType().getAnnotation(AuthToken.class)!=null){
            //1.获取请求头的token
            String token = request.getHeader(httpHeaderName);
            log.info("token is ()",token);
            String username = "";
            //根据token获取redis缓存的username
            if (token != null && token.length()!=0){//用token校验
                username=redisUtils.get(token).toString();//根据token获取redis缓存中的username
                log.info("username is: {}",username);
                //2.根据token+username获取token的初始时间
                if (username !=null && !username.trim().equals("")){//校验成功
                    Long tokenBirthTime = Long.valueOf((String) redisUtils.get(token+username));//获取token初始时间
                    log.info("token Birth time is: {}",tokenBirthTime);
                    Long diff = System.currentTimeMillis()-tokenBirthTime;//时间差——token存在了多久
                    log.info("token is exist: {}",diff);
                    //3.重置缓存失效时间
                    if (diff> ConstantKit.TOKEN_RESET_TIME){
                        redisUtils.expire(username, ConstantKit.TOKEN_EXPIRE_TIME);
                        redisUtils.expire(token, ConstantKit.TOKEN_EXPIRE_TIME);
                        log.info("Reset expire time success!");
                        Long newBirthTime = System.currentTimeMillis();
                        redisUtils.set(token+username,newBirthTime.toString());//重置token初始时间
                    }
                    //4/设置请求头
                    request.setAttribute(REQUEST_CURRENT_KEY, username);
                    return true;
                }
            } else {//验证失败要重新登录
                response.sendRedirect("/studentinfo");
                return false;
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

七、配置类
1.RedisConfig(redis配置类)
**

package com.jinv.studentinfo.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
/**
 * Redis配置类
 */
@Configuration
@EnableCaching//启用缓存,这个注解很重要
//继承CachingConfigurerSupport,为了自定义key的策略,可以不继承
public class RedisConfig extends CachingConfigurerSupport {
    @Value("${spring.cache.redis.time-to-live}")
    private Duration timeToLive = Duration.ZERO;
    @Bean(name = "redisTemplate")
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(mapper);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        //使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(stringRedisSerializer);
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置序列化(解决乱码的问题)
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(timeToLive)
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
}

2、WebApplicationConfig(添加token拦截器)

package com.jinv.studentinfo.config;
import com.jinv.studentinfo.interception.AuthorizationInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
 * 拦截器
 */
@Configuration
public class WebAppConfiguration implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthorizationInterceptor()).addPathPatterns("/**");//拦截所有
    }
}

八、封装返回类
ResultVo

package com.jinv.studentinfo.vo;

import lombok.Data;

import java.io.Serializable;

/**
 * 封装返回类
 * @Author: jinv
 * @CreateTime: 2020-05-15 18:08
 * @Description:
 */
@Data
public class ResultVo<T> implements Serializable {
    private static final long serialVersionUID=1L;

    //响应码
    private int code;
    //返回消息
    private String msg;
    //数据
    private T data;

    //成功
    public ResultVo(){
        this.code=0;
        this.msg="成功";
    }

    //成功返回数据
    public ResultVo(T data){
        this();
        this.data=data;
    }

    //成功,返回消息
    public ResultVo(String msg){
        this();
        this.msg=msg;
    }

    //失败,返回错误码和失败消息
    public ResultVo(int code,String msg){
        this.code=code;
        this.msg=msg;
    }

    //失败,返回固定500错误码和异常消息
    public ResultVo(Throwable e){
        this.code=500;
        this.msg=e.getMessage();
    }

    //失败,返回自定义的错误码和异常消息(使用场景,ExceptionHandler隐藏异常细节,直接返回500和未知异常,请联系管理员)
    public ResultVo(int code,Throwable e){
        this.code=code;
        this.msg=e.getMessage();
    }

}

九、Web层
LoginController——第一次登陆成功要把username+password传给token的相关构造类,生成token并且存入redis缓存中,设置失效时间等

package com.jinv.studentinfo.web;

import com.alibaba.fastjson.JSON;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.jinv.studentinfo.annotation.AuthToken;
import com.jinv.studentinfo.domain.Student;
import com.jinv.studentinfo.domain.Teacher;
import com.jinv.studentinfo.service.StudentService;
import com.jinv.studentinfo.service.TeacherService;
import com.jinv.studentinfo.token.Md5TokenGenerator;
import com.jinv.studentinfo.utils.KaptchaUtils;
import com.jinv.studentinfo.utils.RedisUtils;
import com.jinv.studentinfo.vo.ResultVo;
import org.apache.ibatis.annotations.Param;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

@Controller
public class LoginController {
    @Resource
    private DefaultKaptcha kaptchaProducer;//验证码生成器

    @Autowired
    private Md5TokenGenerator tokenGenerator;//token生成器

    @Autowired
    private RedisUtils redisUtils;//redis工具类

    @Autowired
    private StudentService studentService;

    @Autowired
    private TeacherService teacherService;

    private ResultVo resultVo;

    @GetMapping("")//去登陆页面
    public String index(){
        return "login";
    }

    /**
     * 检查登录状态
     * @param request
     * @return resultVo:已登录:code:0,msg:成功 未登录:返回登录页面
     */
    @GetMapping("/checkLogin")
    @AuthToken
    @ResponseBody
    public String checkLogin(HttpServletRequest request){
        Long studentId = Long.valueOf(redisUtils.get(request.getHeader("Authorization")).toString());
        Student student = studentService.findById(studentId);
        Map<String,Student> map = new HashMap<>();
        map.put("student",student);
        resultVo = new ResultVo(0,map);
        return JSON.toJSONString(resultVo);
    }


    /**
     * 登录校验表单以及验证码
     * @param @RequestBody 请求体Map<String,String>
     * @param request   获取客户端ip
     * @return resultVo  code=0管理员验证成功,code=1学生验证成功,
     *                      code=2教师验证成功,code=401账号密码错误,
     *                      code=402验证码错误, data——>token令牌
     */
    @PostMapping("/login")
    @ResponseBody
    public String login(
                    @RequestBody Map map,
                    HttpServletRequest request){
        Long username =  Long.valueOf(map.get("username").toString());
        String password = (String) map.get("password");
        String validateCode = (String) map.get("validateCode");
        String judgement = (String) map.get("judgement");
        //获取客户端IP
        String IP = request.getRemoteAddr();
        //获取redis中的验证码
        String rightCode = redisUtils.hget("validateCode",IP).toString();
        //验证验证码
        if (validateCode.trim().equals(rightCode)){//验证成功
            if (judgement.equals("0")){//0学生登录
                Student student = studentService.findByIdAndPsd(username, password);
                if (student!=null){//验证成功
                    //设置值token缓存
                    String token = tokenGenerator.tokenUtils(String.valueOf(username), password);
                    Map<String,String> data = new HashMap<>();
                    data.put("token",token);
                    resultVo  = new ResultVo(1,data);//学生验证成功
                }else {
                    resultVo = new ResultVo(401,"账号或密码错误");
                }
            }else{//judgement=1老师登录
                Teacher teacher = teacherService.findByIdAndPsd(username,password);
                if (teacher!=null){//验证成功
                    //设置值token缓存
                    String token = tokenGenerator.tokenUtils(String.valueOf(username), password);
                    Map<String,String> data = new HashMap<>();
                    data.put("token",token);
                    resultVo  = new ResultVo(2,data);//教师验证成功
                }else {
                    resultVo = new ResultVo(401,"账号或密码错误");
                }
            }
        }else {
            resultVo = new ResultVo(402,"验证码错误");
        }

        return JSON.toJSONString(resultVo);
    }


    /**
     * 登录验证码图片
     */
    @GetMapping("/loginValidateCode")
    public void loginValidateCode(HttpServletRequest request,HttpServletResponse response) throws Exception {
        KaptchaUtils.validateCode(request,response,kaptchaProducer,redisUtils);
    }


}

十、前端
1、login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
    <title>登录页面</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" /><!--自适应-->
    <link rel="stylesheet" href="../static/css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}" />
    <link rel="stylesheet" href="../static/css/bootstrap-responsive.min.css" th:href="@{/css/bootstrap-responsive.min.css}" />
    <link rel="stylesheet" href="../static/css/matrix-login.css" th:href="@{/css/matrix-login.css}" />
    <link rel="stylesheet" href="../static/font-awesome/css/font-awesome.css" th:href="@{/font-awesome/css/font-awesome.css}" />
    <link rel="stylesheet" href="../static/css/me.css" th:href="@{/css/me.css}" />
    <style>
        .error{
            color: #b96027;
        }
    </style>
</head>
<body>
<div id="loginbox">
    <div id="loginform" class="form-vertical">
        <input type="hidden" name="judgement" id="judgement">
        <div class="control-group normal_text"> <h3><img src="../static/img/logo.png" th:src="@{/img/logo.png}"  alt="Logo" /></h3></div>
        <div class="control-group">
            <div class="text-center">
                <h3 id="errorMsg" class="error" hidden>用户名或密码错误</h3>
            </div>
            <div class="controls">
                <div class="text-center">
                    <h3 id="usernameError" class="error" hidden>用户名不能为空</h3>
                </div>
                <div class="main_input_box">
                    <span class="add-on bg_lg"><i class="icon-user"></i></span>
                    <input type="text" id="username" name="username" class="form-control" required  placeholder="用户名。。。" />
                </div>
            </div>
        </div>
        <div class="control-group">
            <div class="controls">
                <div class="text-center">
                    <h3 id="passwordError" class="error" hidden>密码不能为空</h3>
                </div>
                <div class="main_input_box">
                    <span class="add-on bg_ly"><i class="icon-lock"></i></span>
                    <input type="password" id="password" name="password" class="form-control" required  placeholder="密码。。。" />
                </div>
            </div>
        </div>
        <div class="control-group">
            <div class="controls" style="text-align: center">
                <img src="/loginValidateCode" th:src="@{/loginValidateCode}" id="loginValidateCode" name="loginValidateCode"
                     onclick="uploadLoginValidateCode()" class="m-margin-bottom" alt="">
                <input type="text" id="validateCode" name="validateCode" class="form-control" required  placeholder="验证码。。。">
                <div class="text-center">
                    <h3 id="validateCodeError" class="error" hidden>验证码错误</h3>
                </div>
            </div>
        </div>
        <div class="form-actions" style="text-align: center">
            <button type="submit" id="tea-log-btn" name="submit" class="btn btn-success" >教师登录</button>

            <button type="submit" id="stu-log-btn" name="submit" class="btn btn-warning" >学生登录</button>
        </div>
    </div>
</div>

<script src="../static/js/jquery.min.js" th:src="@{/js/jquery.min.js}"></script>
<script src="../static/js/jquery.validate.js" th:src="@{/js/jquery.validate.js}"></script>
<script src="../static/js/messages_zh.js" th:src="@{/js/messages_zh.js}"></script><!--验证表单-->
<script src="../static/js/matrix.login.js" th:src="@{/js/matrix.login.js}"></script>
<script src="../static/js/me/login.js" th:src="@{/js/me/login.js}"></script>
</body>

</html>

2、login.js
重点:使用localStorage存储接口返回的token,后面的请求都要设置ajax的header的key:Authorization, value:[token]

//跟新验证码图片
function uploadLoginValidateCode() {
    /*路径一定要加上项目的跟路径*/
    $("#loginValidateCode").attr("src","/studentinfo/loginValidateCode?random="+new Date().getMilliseconds());//加一段随机数更换图片
}

$("#tea-log-btn").click(function () {
    var valid = yanzheng();
    if (valid==true){
        $("#judgement").val("1");
        login();
    }
});

$("#stu-log-btn").click(function () {
    var valid = yanzheng();
    if (valid==true){
        $("#judgement").val("0");
        login();
    }
});

/*验证表单*/
function yanzheng() {
    var username = $("#username").val();
    var password = $("#password").val();
    var validateCode = $("#validateCode").val();
    if (username.trim()!=null&&username.trim()!=""){
        if (password.trim()!=null&&password.trim()!=""){
            if (validateCode.trim().length==4){
                return true;
            }else {
                $("#validateCodeError").show();
                return false;
            }
        }else {
            $("#passwordError").show();
            return false;
        }
    }else {
        $("#usernameError").show();
        return false;
    }
}



//登录
function login() {
    var validateCode = $("#validateCode").val();
    var adata = {
        "username":$("#username").val(),
        "password":$("#password").val(),
        "validateCode":validateCode,
        "judgement":$("#judgement").val()
    };                                      //定义JSON字符串格式的数据
    if (validateCode.trim()!=null && validateCode.trim()!="" && validateCode.trim().length==4){//校验验证码不为空且为4位数字
        $.ajax({
            type: "post",
            async: false,                   //默认为true异步,false表示同步,异步不需要等待,同步需要等待回调方法做完
            contentType : "application/json;charset=UTF-8",//上传内容格式为json结构
            dataType: "json",
            data : JSON.stringify(adata),
            url: "/studentinfo/login",
            success: function (data) {
                console.log(data);
                // var result = eval("("+data1+")");
                var code = data["code"];
                var token = data["data"]["token"];
                var msg = data["msg"];
                console.log(code);
                console.log(token);
                console.log(msg);
                if (code==1){
                    console.log("学生验证成功");
                    if (saveToken(token)==true){
                        location.href="/studentinfo/student/index";//跳转到学生端首页
                    }
                }
                if (code==2){
                    console.log("教师验证成功");
                    // location.href="/studentinfo/student/index?token="+token;//将token拼接进去
                }


                else if (code==402) {
                    $("#validateCodeError").show();
                    uploadLoginValidateCode();
                }
                },
            error: function(XMLHttpRequest,textStatus){
                alert("服务器错误!状态码:"+XMLHttpRequest.status);
                // 状态
                console.log(XMLHttpRequest.readyState);
                // 错误信息
                console.log(textStatus);
            }
        });
    }else{
        $("#validateCodeError").show();
    }
}

//存储token到localStorage中,并且发送ajax请求
function saveToken(token) {
    if (! window.localStorage){//浏览器不支持localstorage,localstorage是html5的新属性,并且在IE8以上的IE版本
        alert("浏览器不支持localstorage,localstorage是html5的新属性");
        return false;
    }else {//浏览器支持localStorage
        var storage=window.localStorage;
        //写入token
        localStorage.setItem("token",token);
        console.log("已经存入localStorage");
        console.log(localStorage.getItem("token"));//也可以这样子取值:localStorage.getItem("token")
        return true;
    }
}

重点:3.客户端还需要一个用于检测登录状态的ajax,并且该方法是window.onload的即页面加载完就要自动发送ajax请求检测登录状态,并不是每次调用接口都往ajax里面加入token
fragment.js

//加载页面并且检验登录状态
window.onload=function(){//页面加载完马上执行
    var token = localStorage.getItem("token");
    console.log("加载出来token");
    console.log(token);
    $.ajax({
        type: "get",
        url: "/studentinfo/checkLogin",
        // data: mydata,
        contentType: "application/json",
        dataType: "json",
        beforeSend: function(request) {
            request.setRequestHeader("Authorization", token);//将token加入请求头
        },
        success(data){
            console.log(data);
            // var result = eval("("+data+")");
            var code = data["code"];
            var msg = data["msg"];
            var studentName = data["data"]["student"]["studentName"];
            if (code==0){//已登录
                $("#studentname").html(studentName);
                $("#myname").html(studentName);
            }else {//未登录
                location.href="/studentinfo";//跳转到登录页面
            }
        },
        error(e){
            console.log(e);
        }
    });
};

这里还是有很多未完善的地方,后期会把项目源码贴出来github地址填坑。。。。还在开发中。。。。