学习笔记(二):Token鉴权实现

上次总结了有关Token鉴权的理论知识,本次内容学习了将jwt、shiro、redis整合,实现token的自动刷新和可控性。

一、为什么要使用jwt+shiro+redis?

在微服务中,我们一般是无状态登录,而传统的session方式,在前后端分离的微服务架构下,如果继续使用,就需要解决跨域sessionId、集群session共享问题等。而通过整合shiro实现的话,则会出现以下问题:

    (1)shiro默认的拦截跳转都是跳转url页面,在前后端分离中,后端并没有权力干涉页面跳转。

    (2)shiro默认使用的登录拦截校验机制恰恰使用的就是session。

这并不是我们期望的,所以想使用shiro,则需要对其进行改造。我们在整合shiro的基础上自定义登录校验,并整合JWT或者oauth2.0等,实现服务端也可无状态登录,即token登录。

但是上述shiro+token整合改造后,还不能实现token的可控性和自动刷新。这样就会导致token在设定时限后过期,用户再次登录需要重新发起请求,这样就拉低了用户的体验感受;其次在token的有效期内,即使用户退出了登录,token依然有效,依然可以使用,这就会有安全风险,因此还需要整合redis来实现可控性操作。

二、AccessToken和RefreshToken

2.1 shiro+jwt 实现无状态鉴权机制

1、首先前端通过登录接口,输入用户名与密码进行登录,如果成功在请求头Header返回一个加密的Authorization,失败则直接返回401未登录等状态码,以后访问均带上这个Authorization即可。

2、鉴权流程主要是重写shiro的入口过滤器BasicHttpAuthenticationFilter,在此基础上进行拦截、token验证授权等操作。

2.2 AccessToken及RefreshToken的说明

1、AccessToken:用于接口传输过程中的用户授权标识,客户端(前端)每次请求都需要携带,出于安全考虑,通常设定的有效时长较短。

2、RefreshToken:与AccessToken为共生关系,一般用于刷新AccessToken,保存于服务端(后端),客户端不可见,有效时长较长。

2.3 Redis中保存RefreshToken信息(实现JWT可控性)

1、登录认证通过后返回AccessToken信息(在AccessToken中保存账号和当前的时间戳),同时在Redis中设置一条以账号为key,Value为当前时间戳(登录时间)的RefreshToken,现在认证时必须满足AccessToken没失效以及Redis存在所对应的RefreshToken,且RefreshToken时间戳和AccessToken信息中的时间戳一致才算认证通过,这样就实现了JWT的可控性。如果重新登录获取了新的AccessToken,旧的AccessToken就认证不了了,因为Redis中存放的RefreshToken时间戳信息只会和最新的AccessToken信息中携带的时间戳一致,这样每个用户就只能使用最新的AccessToken认证。(这样也避免了同一账号多人登录,因为会被挤下去!)

2、Redis的RefreshToken也可以用来判断用户是否在线,如果删除Redis的某个RefreshToken,那这个RefreshToken所对应的AccessToken之后也无法通过认证了,就相当于控制了用户的登录,可以剔除用户。

2.4 根据RefreshToken自动刷新AccessToken

1、AccessToken的默认过期时间是5分钟(配置文件可以配置),RefreshToken过期时间为30分钟(配置文件也可配置),当登录时间过了5分钟,当前AccessToken就会失效,再次带上AccessToken访问JWT会抛出TokenExpiredException异常说明Token过期,开始判断是否要进行AccessToken刷新。首先redis查询RefreshToken是否存在,以及时间戳和过期AccessToken所携带的时间戳是否一致,如果存在且一致,就进行AccessToken刷新。

2、刷新后新的AccessToken过期时间依旧为5分钟,时间戳为当前最新时间戳,同时也设置RefreshToken中的时间戳为当前最新时间戳,刷新过期时间重新为30分钟过期,最终将刷新的AccessToken存放在Response的Header中的Authorization字段返回。

3、同时前端进行获取替换,下次使用新的AccessToken进行访问即可。

三、导入依赖并配置

3.1 数据库文件

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `id` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `username` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `password` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `roles` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `permission` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `sys_user` VALUES ('1', 'admin', '111', 'admin', 'add');
INSERT INTO `sys_user` VALUES ('2', 'zhao', '222', 'user', 'update');
INSERT INTO `sys_user` VALUES ('3', 'wang', '333', 'vip', 'delete');
INSERT INTO `sys_user` VALUES ('4', 'li', '444', 'user', 'add');

SET FOREIGN_KEY_CHECKS = 1;

3.2 pom.xml依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.2.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

3.3 application.yml

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root

spring.redis.host=127.0.0.1
spring.redis.port=6379

mybatis.type-aliases-package=csu.org.jwtshiroredis.domain
mybatis.mapper-location=classpath:mappers/*.xml
mybatis.lazy-initialization=true

四、配置redis和redisUtil的实现

4.1 配置redis

redis的默认序列化是jdk序列化,这样的序列化是以转义字符的形式存储的,不方便查看,因此需要修改,以便查看。

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.StringRedisSerializer;

import java.net.UnknownHostException;

@Configuration
public class RedisConfig {
    //编写我们自己的redisTemplate
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        //我们为了自己开发使用方便,一般使用<String, Object>类型
        RedisTemplate<String, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);

        //序列化配置
        //json序列化
        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);

        //String序列化
        StringRedisSerializer stringRedisSerializer=new StringRedisSerializer();

        //key使用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        //hash的key也使用String序列化
        template.setHashKeySerializer(stringRedisSerializer);
        //value使用json序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //hash的value使用json序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();

        return template;
    }
}

4.2 RedisUtil

编写redisUtil便于我们使用redisTrmplate的api,简化使用过程

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

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

@Component
public final class RedisUtil {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // =============================common============================
    /**
     * 指定缓存失效时间
     * @param key  键
     * @param time 时间(秒)
     */
    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 delta 要增加几(大于0)
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }

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

    public long strLen(String key){
        return redisTemplate.opsForValue().get(key).toString().length();
    }

    /*
     * 追加字符
     * @param key   键
     * @param str   要追加的字符
     * */
    public boolean append(String key,String str){
        try {
            redisTemplate.opsForValue().append(key,str);
            return true;
        }catch (Exception e){
            return false;
        }
    }

    // ================================Map=================================

    /**
     * HashGet
     * @param key  键 不能为null
     * @param item 项 不能为null
     */
    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 对应多个键值
     */
    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)
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }

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

    // ============================set=============================

    /**
     * 根据key获取Set中的所有值
     * @param key 键
     */
    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 键
     */
    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代表所有值
     */
    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 键
     */
    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倒数第二个元素,依次类推
     */
    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 值
     */
    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  时间(秒)
     */
    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 值
     * @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;
        }

    }
}

五、封装token

封装token来替换Shiro原生Token,要实现AuthenticationToken接口

shiro默认supports的是UsernamePasswordToken,而我们现在采用了jwt的方式,因此需要定义一个JwtToken,来完成shiro的supports方法。

import org.apache.shiro.authc.AuthenticationToken;

public class JWTToken implements AuthenticationToken {

    private String token;

    public JWTToken(String token){
        this.token=token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

六、编写JwtUtil

JwtUtil是用来生成token和校验解码token的。

步骤:

1、设置密钥和token的有效时长

2、生成token

3、校验token

4、获取token的信息

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import csu.org.jwtshiroredis.domain.User;

import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JWTUtil {
    //token有效时长 1分钟,L为long型
    private static final long EXPIRE=1*60*1000L;
    //token的密钥
    private static final String SECRET="jwt+shiro";


    public static String createToken(String username,Long current) {
        //token过期时间
        Date date=new Date(current+EXPIRE);

        //jwt的header部分
        Map<String ,Object>map=new HashMap<>();
        map.put("alg","HS256");
        map.put("typ","JWT");

        //使用jwt的api生成token
        String token= null;//签名
        try {
            token = JWT.create()
                    .withHeader(map)
                    .withClaim("username", username)//私有声明
                    .withClaim("current",current)//当前时间截点
                    .withExpiresAt(date)//过期时间
                    .withIssuedAt(new Date())//签发时间
                    .sign(Algorithm.HMAC256(SECRET));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return token;
    }

    //校验token的有效性,1、token的header和payload是否没改过;2、没有过期
    public static boolean verify(String token){
        try {
            //解密
            JWTVerifier verifier=JWT.require(Algorithm.HMAC256(SECRET)).build();
            verifier.verify(token);
            return true;
        }catch (Exception e){
            return false;
        }
    }
    //无需解密也可以获取token的信息
    public static String getUsername(String token){
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    //获取过期时间
    public static Long getExpire(String token){
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("current").asLong();
        }catch (Exception e){
            return null;
        }
    }

    //根据request中的token获取用户账号
    public static String getUserNameByToken(HttpServletRequest request){
       //获取请求头中的token字段内容
       String accessToken = request.getHeader("Authorization");
       String username = getUsername(accessToken);
       if (username.isEmpty()){
           system.out.println("未获取到用户");
       }
       return username;
    }
}

七、编写jwtFilter

这个过滤器是我们的重点,这里我们继承的是shiro内置的BasicHttpAuthenticationFilter,一个可以内置了可以自动登录方法的过滤器。也可以继承AuthenticatingFilter

这个过滤器是要注册到shiro配置里面去的,用来辅助shiro进行过滤处理。所有的请求都会到过滤器进行处理。

这个过滤器类的执行过程是:preHandle==>isAccessAllowed==>isLoginAttempt==>executeLogin

授权成功就会进入onLoginSuccess,否则进入onAccessDenied

在这里要重写几个方法:

    1、isAccessAllowed:是否允许访问。如果带有token,就对token进行检查,否则直接通过。如果请求头不存在token,则返回false并提示“认证失败”信息。(注:根据需求可以修改,若有游客状态,则无需检查token,直接返回true)。

    补充:在检查token时若有问题,那么Realm就会抛出异常,同时异常内容不为空的话则进行刷新token操作。

    2、isLoginAttempt:判断用户是否想要登入。检测header中是否包含Token字段。

    3、executeLogin:本来是先调用createToken来获取token的,在这里进行重写,实现不再自动调用createToken来获取token。然后调用getSubject方法来获取当前用户再调用login方法实现登录。这也解释了自定义jwtToken是因为不再使用shiro默认的UsernamePasswordToken

    4、preHandle:拦截器的前置拦截。在前后端分析项目中,除了跨域全局配置之外,在拦截器中也需要提供跨域支持。这样,拦截器就不会在进入Controller之前就被限制了。

在这里我们需要对token的刷新进行校验,如果符合刷新的条件就会进行刷新。

  • JWTFilter继承BasicHttpAuthenticationFilter
import com.auth0.jwt.exceptions.TokenExpiredException;
import csu.org.jwtshiroredis.shiro.JWTToken;
import csu.org.jwtshiroredis.utils.JWTUtil;
import csu.org.jwtshiroredis.utils.RedisUtil;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

public class JWTFilter extends BasicHttpAuthenticationFilter {


    //是否允许访问,如果带有 token,则对 token 进行检查,否则直接通过
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //判断请求的请求头是否带上 "Token"
        System.out.println("isAccessAllowed");
        if (isLoginAttempt(request, response)){
            //如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
            try {
                executeLogin(request, response);
                return true;

            }catch (Exception e){
                /*
                 *注意这里捕获的异常其实是在Realm抛出的,但是由于executeLogin()方法抛出的异常是从login()来的,
                 * login抛出的异常类型是AuthenticationException,所以要去获取它的子类异常才能获取到我们在Realm抛出的异常类型。
                 * */
                System.out.println("刷新token");
                String msg=e.getMessage();
                Throwable cause = e.getCause();
                if (cause!=null&&cause instanceof TokenExpiredException){
                    //AccessToken过期,尝试去刷新token
                    String result=refreshToken(request, response);
                    if (result.equals("success")){
                        System.out.println("request.equals(\"success\")");
                        return true;
                    }
                    msg=result;
                }
                responseError(response,msg);
            }
        }
        //如果请求头不存在 Token,则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true
        //如果请求头不存在Token,则返回false
        if (!isLoginAttempt(request,response)){
            responseError(response,"用户认证失败,请重新登录!");
            return false;
        }
        return true;
    }

    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req= (HttpServletRequest) request;
        String token=req.getHeader("Authorization");
        return token !=null;
    }
    /*
     * executeLogin实际上就是先调用createToken来获取token,这里我们重写了这个方法,就不会自动去调用createToken来获取token
     * 然后调用getSubject方法来获取当前用户再调用login方法来实现登录
     * 这也解释了我们为什么要自定义jwtToken,因为我们不再使用Shiro默认的UsernamePasswordToken了。
     * */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        System.out.println("executeLogin");
        HttpServletRequest req= (HttpServletRequest) request;
        String token=req.getHeader("Authorization");
        JWTToken jwt=new JWTToken(token);
        //交给自定义的realm对象去登录,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(jwt);
        return true;
    }

    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        System.out.println("preHandle");
        HttpServletRequest req= (HttpServletRequest) request;
        HttpServletResponse res= (HttpServletResponse) response;
        res.setHeader("Access-control-Allow-Origin",req.getHeader("Origin"));
        res.setHeader("Access-control-Allow-Methods","GET,POST,OPTIONS,PUT,DELETE");
        res.setHeader("Access-control-Allow-Headers",req.getHeader("Access-Control-Request-Headers"));
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
            res.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

    /**
     * 将非法请求跳转到 /unauthorized/**
     */
    private void responseError(ServletResponse response, String message) {
        System.out.println("responseError");

        try {
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            //设置编码,否则中文字符在重定向时会变为空字符串
            message = URLEncoder.encode(message, "UTF-8");
            httpServletResponse.sendRedirect("/unauthorized/" + message);
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }


    /*
    * 这里的getBean是因为使用@Autowired无法把RedisUtil注入进来
    * 这样自动去注入当使用的时候是未NULL,是注入不进去了。通俗的来讲是因为拦截器在spring扫描bean之前加载所以注入不进去。
    *
    * 解决的方法:
    * 可以通过已经初始化之后applicationContext容器中去获取需要的bean.
    * */
    public <T> T getBean(Class<T> clazz,HttpServletRequest request){
        WebApplicationContext applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
        return applicationContext.getBean(clazz);
    }

    //刷新token
    private String refreshToken(ServletRequest request,ServletResponse response) {
        System.out.println("refreshToken");

        HttpServletRequest req= (HttpServletRequest) request;
        RedisUtil redisUtil=getBean(RedisUtil.class,req);
        //获取传递过来的accessToken
        String accessToken=req.getHeader("Authorization");
        //获取token里面的用户名
        String username= JWTUtil.getUsername(accessToken);
        System.out.println("username"+username);
        //判断refreshToken是否过期了,过期了那么所含的username的键不存在
        System.out.println("redisUtil.hasKey(username)"+redisUtil.hasKey(username));
        if (redisUtil.hasKey(username)){
            //判断refresh的时间节点和传递过来的accessToken的时间节点是否一致,不一致校验失败
            long current= (long) redisUtil.get(username);
            if (current==JWTUtil.getExpire(accessToken)){
                //获取当前时间节点
                long currentTimeMillis = System.currentTimeMillis();
                //生成刷新的token
                String token=JWTUtil.createToken(username,currentTimeMillis);
                //刷新redis里面的refreshToken,过期时间是30min
                redisUtil.set(username,currentTimeMillis,30*60);
                //再次交给shiro进行认证
                JWTToken jwtToken=new JWTToken(token);
                try {
                    getSubject(request, response).login(jwtToken);
                    // 最后将刷新的AccessToken存放在Response的Header中的Authorization字段返回
                    HttpServletResponse httpServletResponse = (HttpServletResponse) response;
                    httpServletResponse.setHeader("Authorization", token);
                    httpServletResponse.setHeader("Access-Control-Expose-Headers", "Authorization");
                    return "success";
                }catch (Exception e){
                    return e.getMessage();
                }
            }
        }
        return "token认证失效,token过期,重新登陆";
    }
}

八、自定义MyRealm对象

AccountRealm是shiro进行登录或者权限校验的逻辑所在,在这里需要重写3个方法。

  • supports:为了让realm支持jwt的凭证校验
  • doGetAuthorizationInfo:权限校验
  • diGetAuthenticationInfo:登录认证校验

需要注意的点:认证抛出的异常会被jwtFilter的login( )方法获取到的

认证的流程需要梳理一下:

先获取token中信息包含的用户名,判断用户名是否存在,该用户名是否有对应的账号;再验证redis有没有该用户名对应的key,如果有,判断accessToken有没有过期,如果没有则获取对应的value,这就是refreshToken;接着判断该value和accessToken里面的时间戳是否一样,如果一样那么就认证通过。

import com.auth0.jwt.exceptions.TokenExpiredException;
import csu.org.jwtshiroredis.domain.User;
import csu.org.jwtshiroredis.service.UserService;
import csu.org.jwtshiroredis.utils.JWTUtil;
import csu.org.jwtshiroredis.utils.RedisUtil;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class MyRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;
    @Autowired
    private RedisUtil redisUtil;

    //根据token判断此Authenticator是否使用该realm
    //必须重写不然shiro会报错
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }

    /**
     * 只有当需要检测用户权限的时候才会调用此方法,例如@RequiresRoles,@RequiresPermissions之类的
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("授权~~~~~");
        String token=principals.toString();
        String username= JWTUtil.getUsername(token);
        User user=userService.getUser(username);
        SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
        //查询数据库来获取用户的角色
        info.addRole(user.getRoles());
        //查询数据库来获取用户的权限
        info.addStringPermission(user.getPermission());
        return info;
    }


    /**
     * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可,在需要用户认证和鉴权的时候才会调用
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("认证~~~~~~~");
        String jwt= (String) token.getCredentials();
        String username= null;
        try {
            username= JWTUtil.getUsername(jwt);
        }catch (Exception e){
            throw new AuthenticationException("token非法,不是规范的token,可能被篡改了,或者过期了");
        }
        if (username==null){
            throw new AuthenticationException("token中无用户名");
        }
        User user=userService.getUser(username);
        if (user==null){
            throw new AuthenticationException("该用户不存在");
        }
        //开始认证,只要AccessToken没有过期,或者refreshToken的时间节点和AccessToken一致即可
        if (redisUtil.hasKey(username)){
            //判断AccessToken有无过期
            if (!JWTUtil.verify(jwt)){
                throw new TokenExpiredException("token认证失效,token过期,重新登陆");
            }else {
                //判断AccessToken和refreshToken的时间节点是否一致
                long current= (long) redisUtil.get(username);
                if (current==JWTUtil.getExpire(jwt)){
                    return new SimpleAuthenticationInfo(jwt,jwt,"MyRealm");
                }else{
                    throw new AuthenticationException("token已经失效,请重新登录!");
                }
            }
        }else{
            throw new AuthenticationException("token过期或者Token错误!!");
        }
    }
}

九、编写shiroConfig

配置文件的任务主要有:

1、创建defaultWebSecurityManager对象

2、创建ShiroFilterFactoryBean进行过滤拦截,权限和登录

3、关闭session

4、添加注解权限开发

注意:springboot整合jwt与单纯的shiro实现认证有三个不一样的地方。

1、因为不适用Session,所以为了防止会调用getSession( )方法而产生错误,需要关闭session。

2、一些修改,如关闭ShiroDao等。

3、注册JwtFilter到ShiroFilterFactoryBean中。

import csu.org.jwtshiroredis.filter.JWTFilter;
import csu.org.jwtshiroredis.shiro.MyRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {


    @Bean(name = "securityManager")
    public DefaultWebSecurityManager securityManager(MyRealm myRealm){
        DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
        // 设置自定义 realm.
        securityManager.setRealm(myRealm);

        //关闭session
        DefaultSubjectDAO subjectDAO=new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator sessionStorageEvaluator=new DefaultSessionStorageEvaluator();
        sessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
    }

    /**
     * 先走 filter ,然后 filter 如果检测到请求头存在 token,则用 token 去 login,走 Realm 去验证
     */
    @Bean
    public ShiroFilterFactoryBean factory(@Qualifier("securityManager")DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean factoryBean=new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(securityManager);
        // 添加自己的过滤器并且取名为jwt
        Map<String, Filter> filterMap=new LinkedHashMap<>();
        //设置我们自定义的JWT过滤器
        filterMap.put("jwt",new JWTFilter());
        factoryBean.setFilters(filterMap);

        // 设置无权限时跳转的 url;
        factoryBean.setUnauthorizedUrl("/unauthorized/无权限");
        Map<String,String>filterRuleMap=new HashMap<>();
        // 所有请求通过我们自己的JWT Filter
        filterRuleMap.put("/**","jwt");
        // 访问 /unauthorized/** 不通过JWTFilter
        filterRuleMap.put("/unauthorized/**","anon");
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return factoryBean;
    }

    /**
     * 添加注解支持,如果不加的话很有可能注解失效
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){

        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){

        AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
}

十、统一响应结果封装

  • 统一的规范的响应结果封装
import lombok.Data;

import java.io.Serializable;

@Data
public class R<T> implements Serializable {
    
    private static final long serialVersionUID = 1l;
    
    //成功标志
    private Boolean success;
    //返回代码
    private Integer code;
    //返回处理消息
    private String message;
    //返回数据对象 data
    private T data;
    //时间戳
    private long timestamp = System.currentTimeMillis();
    
    public R(){}
    
    public static R<Object> ok(){
        R<Object> r = new R<>();
        r.setSuccess(ResultEnum.SUCCESS.getSuccess());
        r.setCode(ResultEnum.SUCCESS.getCode());
        r.setMessage(ResultEnum.SUCCESS.getMessage());
        return r;
    }

    public static R<Object> ok(String msg){
        R<Object> r = new R<>();
        r.setSuccess(ResultEnum.SUCCESS.getSuccess());
        r.setCode(ResultEnum.SUCCESS.getCode());
        r.setMessage(msg);
        return r;
    }

    public static R<Object> ok(Object data){
        R<Object> r = new R<>();
        r.setSuccess(ResultEnum.SUCCESS.getSuccess());
        r.setCode(ResultEnum.SUCCESS.getCode());
        r.setMessage(ResultEnum.SUCCESS.getMessage());
        r.setData(data);
        return r;
    }
    
    public static<T> R<T> OK(){
        R<T> r = new R<>();
        r.setSuccess(ResultEnum.SUCCESS.getSuccess());
        r.setCode(ResultEnum.SUCCESS.getCode());
        r.setMessage(ResultEnum.SUCCESS.getMessage());
        return r;
    }

    public static<T> R<T> OK(T data){
        R<T> r = new R<>();
        r.setSuccess(ResultEnum.SUCCESS.getSuccess());
        r.setCode(ResultEnum.SUCCESS.getCode());
        r.setMessage(ResultEnum.SUCCESS.getMessage());
        r.setData(data);
        return r;
    }

    public static<T> R<T> OK(String msg,T data){
        R<T> r = new R<>();
        r.setSuccess(ResultEnum.SUCCESS.getSuccess());
        r.setCode(ResultEnum.SUCCESS.getCode());
        r.setMessage(msg);
        r.setData(data);
        return r;
    }

    /**
     * 请求失败
     */
    public static R<Object> error(){
        R<Object> r = new R<>();
        r.setSuccess(ResultEnum.FAILED.getSuccess());
        r.setCode(ResultEnum.FAILED.getCode());
        r.setMessage(ResultEnum.FAILED.getMessage());
        return r;
    }

    public static R<Object> error(String msg){
        R<Object> r = new R<>();
        r.setSuccess(ResultEnum.FAILED.getSuccess());
        r.setCode(ResultEnum.FAILED.getCode());
        r.setMessage(msg);
        return r;
    }

    /**
     * 请求无权限
     */
    public static R<Object> unanthorized(){
        R<Object> r = new R<>();
        r.setSuccess(ResultEnum.UNAUTHORIZED.getSuccess());
        r.setCode(ResultEnum.UNAUTHORIZED.getCode());
        r.setMessage(ResultEnum.UNAUTHORIZED.getMessage());
        return r;
    }
    
    public R<T> setResult(ResultEnum resultEnum){
        R<T> r = new R<>();
        r.setSuccess(resultEnum.getSuccess());
        r.setCode(resultEnum.getCode());
        r.setMessage(resultEnum.getMessage());
        return r;
    }
    
    public R<T> success(Boolean status){
        this.setSuccess(status);
        return this;
    }

    public R<T> code(Integer code){
        this.setCode(code);
        return this;
    }

    public R<T> message(String message){
        this.setMessage(message);
        return this;
    }
}
  • 情况枚举类
import lombok.Getter;
import lombok.ToString;

@Getter
@ToString
public enum ResultEnum {
    SUCCESS(true,200,"操作成功"),
    FAILED(false,300,"操作失败"),
    NO_OPERATOR_AUTH(false,301,"无操作权限"),
    DATA_ERROR(false,302,"数据错误"),
    DATA_NO_EXIST(false,303,"数据不存在"),
    UNAUTHORIZED(false,401,"用户认证失败");

    private final Boolean success;
    private final Integer code;
    private final String message;

    ResultEnum(Boolean success,Integer code,String message){
        this.success=success;
        this.code=code;
        this.message=message;
    }
}

十一、全局异常处理

前后端分离项目一定要注意异常的处理,便于前端人员看懂错误提示。

import com.auth0.jwt.exceptions.TokenExpiredException;
import csu.org.jwtshiroredis.utils.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.ShiroException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.io.IOException;

//捕获全局异常的处理
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandle {

    // 捕捉shiro的异常
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(ShiroException.class)
    public Result handle401(ShiroException e) {
        return Result.fail(401, e.getMessage(), null);
    }

    // 捕捉未登录的异常
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(UnauthenticatedException.class)
    public Result handle401(UnauthenticatedException e) {
        System.out.println(e.getMessage());
        return Result.fail(401, "你还没有登录", null);
    }

    // 捕捉没有相应的权限或者角色的异常
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(UnauthorizedException.class)
    public Result handle401(UnauthorizedException e) {
        System.out.println(e.getMessage());
        return Result.fail(401, "你没有权限访问"+e.getMessage(), null);
    }


    /**
     * @Validated 校验错误异常处理
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result handler(MethodArgumentNotValidException e) throws IOException {
//        log.error("运行时异常:-------------->",e);
        BindingResult bindingResult = e.getBindingResult();
        //这一步是把异常的信息最简化
        ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get();
        return Result.fail(HttpStatus.BAD_REQUEST.value(),objectError.getDefaultMessage(),null);
    }

    /**
     * 处理Assert的异常
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = IllegalArgumentException.class)
    public Result handler(IllegalArgumentException e) throws IOException {
//        log.error("Assert异常:-------------->{}",e.getMessage());
        return Result.fail(400,e.getMessage(),null);
    }


    //运行时错误处理
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = RuntimeException.class)
    public Result handle(RuntimeException e){
        return Result.fail(HttpStatus.BAD_REQUEST.value(),e.getMessage(),null);
    }


    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = TokenExpiredException.class)
    public Result handler(TokenExpiredException e) throws IOException {
        return Result.fail(HttpStatus.BAD_REQUEST.value(),"token已经过期,请重新登录",null);
    }
}

十二、登录和登出实现

12.1 登录

登录需要传递用户名、密码过来,然后到数据库去查找对应的用户,如果找不到就提醒找不到用户,如果用户名密码正确就生成对应的AccessToken,同时把refreshToken保存到redis里面。

  • controller
import csu.org.jwtshiroredis.domain.User;
import csu.org.jwtshiroredis.service.UserService;
import csu.org.jwtshiroredis.utils.JWTUtil;
import csu.org.jwtshiroredis.utils.RedisUtil;
import csu.org.jwtshiroredis.utils.Result;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;

@RestController
public class LoginController {

    @Autowired
    private UserService userService;
    @Autowired
    private RedisUtil redisUtil;

    @PostMapping("/login")
    public Result login(@RequestParam String username, @RequestParam String password){
        User user=userService.getUserByPass(username, password);
        Assert.notNull(user,"用户名或密码错误");
        long currentTimeMillis = System.currentTimeMillis();
        String token= JWTUtil.createToken(user.getUsername(),currentTimeMillis);
        redisUtil.set(username,currentTimeMillis,60*30);
        return Result.succ(200,"登陆成功",token);
    }

    @RequestMapping(path = "/unauthorized/{message}")
    public Result unauthorized(@PathVariable String message) throws UnsupportedEncodingException {
        return Result.fail(message);
    }

    @DeleteMapping("/logout")
    @RequiresAuthentication
    public Result logout(HttpServletRequest request){
        String token=request.getHeader("Authorization");
        String username=JWTUtil.getUsername(token);
        redisUtil.del(username);
        return Result.succ(null);
    }
}
  • service
import com.example.demo.domain.User;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public interface UserService {

    List<SysUser> getUserByPwd(String username, String password);

    /*SysUser getUserByPwd(String username,String password);*/
}
  • serviceImpl
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.demo.domain.SysUser;
import com.example.demo.mapper.UserMapper;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    UserMapper userMapper;

    @Override
    public List<SysUser> getUserByPwd(String username, String password) {
        List<SysUser> list = new ArrayList<>();
        QueryWrapper<SysUser> qw = new QueryWrapper<>();
        if (username!=null&&(!"".equals(username))){
            qw.eq("username",username);
        }
        if (password!=null&&(!"".equals(password))){
            qw.eq("password",password);
        }
        list = userMapper.selectList(qw);
        return list;
    }

    /*@Override
    public SysUser getUserByPwd(String username, String password) {
        return userMapper.selectUserBypwd(username,password);
    }*/
}
  • domain(entity)
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Data;

import java.io.Serializable;

@Data
public class SysUser extends Model<SysUser> implements Serializable {
    private static final long serialVersionUID = 305168670107337418L;

    private String id;
    private String username;
    private String password;
    private String roles;
    private String permission;
}
  • mapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.domain.SysUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface UserMapper extends BaseMapper<SysUser> {

    @Select("select * from sys_user where username=#{username} and password=#{password}")
    SysUser selectUserBypwd(String username,String password);
}

12.2 登出

只需要把redis里面的对应的用户名的refreshToken删除就可以了。

import csu.org.jwtshiroredis.domain.User;
import csu.org.jwtshiroredis.service.UserService;
import csu.org.jwtshiroredis.utils.JWTUtil;
import csu.org.jwtshiroredis.utils.RedisUtil;
import csu.org.jwtshiroredis.utils.Result;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;

@RestController
public class LoginController {

    @Autowired
    private UserService userService;
    @Autowired
    private RedisUtil redisUtil;

    @PostMapping("/login")
    public Result login(@RequestParam String username, @RequestParam String password){
        User user=userService.getUserByPass(username, password);
        Assert.notNull(user,"用户名或密码错误");
        long currentTimeMillis = System.currentTimeMillis();
        String token= JWTUtil.createToken(user.getUsername(),currentTimeMillis);
        redisUtil.set(username,currentTimeMillis,60*30);
        return Result.succ(200,"登陆成功",token);
    }

    @RequestMapping(path = "/unauthorized/{message}")
    public Result unauthorized(@PathVariable String message) throws UnsupportedEncodingException {
        return Result.fail(message);
    }

    @DeleteMapping("/logout")
    @RequiresAuthentication
    public Result logout(HttpServletRequest request){
        String token=request.getHeader("Authorization");
        String username=JWTUtil.getUsername(token);
        redisUtil.del(username);
        return Result.succ(null);
    }
}

十三、测试Controller

import csu.org.jwtshiroredis.service.UserService;
import csu.org.jwtshiroredis.utils.Result;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @RequiresAuthentication
    @GetMapping("/test")
    public Result test(){
        return Result.succ("test");
    }

    @RequiresRoles("admin")
    @GetMapping("/admin")
    public Result admin(){
        return Result.succ("admin");
    }

    @RequiresRoles("vip")
    @PostMapping("/vip")
    public Result vip(){
        return Result.succ("vip");
    }

    @RequiresPermissions("update")
    @PutMapping("/update")
    public Result update(){
        return Result.succ("update");
    }

    @RequiresPermissions("delete")
    @DeleteMapping("/delete")
    public Result delete(){
        return Result.succ("delete");
    }

    @GetMapping("/guest")
    public Result guest(){
        return Result.succ("guest");
    }

}

总结:

1、上述知识点以及代码实现,大部分都是通过学习其他博主的内容,写的很清楚,要特别感谢,在这也放上链接:jwt+shiro+redis实现token的自动刷新和token的可控性_shiro+redis token_csu_zhuzi的博客-CSDN博客

2、以上内容主要是为了自身学习,提升点知识,所以加了部分自己的学习见解。有不足的还请大家指出改正!

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值