Spring Boot + Redis 实现 token 的登录认证-Spring专区论坛-技术-SpringForAll社区

Spring Boot + Redis 实现 token 的登录认证

1.在用户登录后,如果要访问其他路径下的资源的话,我们是否还需要再验证一遍呢?而且我们登陆上系统长时间不操作的话还需不需要再次验证?所以这种情况下就很需要token来实现登录功能。并通过redis(redis是一个key-value存储系统,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set –有序集合)和hash(哈希类型))来存储token信息。

功能描述:用户登录成功后,后台返回一个token给调用者,同时自定义一个@AuthToken注解,被该注解标注的API请求都需要进行token效验,效验通过才可以正常访问,实现接口级的鉴权控制。同时token具有生命周期,在用户持续一段时间不进行操作的话,token则会过期,用户一直操作的话,则不会过期。

2.流程分析

(1)客户端登录,输入用户名和密码,在后端数据库进行验证,如果验证失败则返回登陆失败的提示。如果成功了则生成token,并将token和用户名双向绑定,然后存入redis,同时使用token+username作为key把当前的时间戳存入redis。并设置他们的过期时间。

(2)然后设置拦截器,每一个被@AuthToken注解标注的接口,都要首先检查客户端传过来的Authorization字段,获取token。由于token与username双向绑定,可以通过获取的token来尝试从redis中获取username,如果可以获取则说明token正确,登陆成功;反之,则说明失败。

(3)token可以根据用户的使用时间来动态的调整自己的过期时间。在生成 token 的同时也往 redis 里面存入了创建 token 时的时间戳,每次请求被拦截器拦截 token 验证成功之后,将当前时间与存在 redis 里面的 token 生成时刻的时间戳进行比较,如果当前时间距离创建时间快要到达设置的redis过期时间的话,就重新设置token过期时间,将过期时间延长。如果用户在设置的 redis 过期时间的时间长度内没有进行任何操作(没有发请求),则token会在redis中过期。token过期后则会登陆失败,重新输入用户名和密码。

3.代码实现:

项目具体结构如下:

 

image

首先,先导入依赖:

<dependency>
			<groupId>redis.clients</groupId>
			<artifactId>jedis</artifactId>
			<version>3.0.0</version>
		</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>1.3.2</version>
		</dependency>
 
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
 
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.16.22</version>
		</dependency>
<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.54</version>
		</dependency>

自定义注解的实现:@Target 说明了Annotation所修饰的对象范围,表示注解作用在方法和变量上,{ElementType.METHOD, ElementType.TYPE}表示注解作用在方法、类、接口上

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthToken {
 
}

然后设置拦截器:

/**
 * @author 旺旺米雪饼
 */
@Slf4j
public class AuthorizationInterceptor implements HandlerInterceptor {
 
    //存放鉴权信息的Header名称,默认是Authorization
    private String httpHeaderName = "Authorization";
 
    //鉴权失败后返回的错误信息,默认为401 unauthorized
    private String unauthorizedErrorMessage = "401 unauthorized";
 
    //鉴权失败后返回的HTTP错误码,默认为401
    private int unauthorizedErrorCode = HttpServletResponse.SC_UNAUTHORIZED;
 
    /**
     * 存放登录用户模型Key的Request Key
     */
    public static final String REQUEST_CURRENT_KEY = "REQUEST_CURRENT_KEY";
 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
 
        // 如果打上了AuthToken注解则需要验证token
        if (method.getAnnotation(AuthToken.class) != null || handlerMethod.getBeanType().getAnnotation(AuthToken.class) != null) {
 
//            String token = request.getHeader(httpHeaderName);
            String token = request.getParameter(httpHeaderName);
            log.info("Get token from request is {} ", token);
            String username = "";
            Jedis jedis = new Jedis();
            if (token != null && token.length() != 0) {
                username = jedis.get(token);
                log.info("Get username from Redis is {}", username);
            }
            if (username != null && !username.trim().equals("")) {
                //log.info("token birth time is: {}",jedis.get(username+token));
                Long tokeBirthTime = Long.valueOf(jedis.get(token + username));
                log.info("token Birth time is: {}", tokeBirthTime);
                Long diff = System.currentTimeMillis() - tokeBirthTime;
                log.info("token is exist : {} ms", diff);
                //重新设置Redis中的token过期时间
                if (diff > ConstantKit.TOKEN_RESET_TIME) {
                    jedis.expire(username, ConstantKit.TOKEN_EXPIRE_TIME);
                    jedis.expire(token, ConstantKit.TOKEN_EXPIRE_TIME);
                    log.info("Reset expire time success!");
                    Long newBirthTime = System.currentTimeMillis();
                    jedis.set(token + username, newBirthTime.toString());
                }
 
                //用完关闭
                jedis.close();
                request.setAttribute(REQUEST_CURRENT_KEY, username);
                return true;
            } else {
                JSONObject jsonObject = new JSONObject();
 
                PrintWriter out = null;
                try {
                    response.setStatus(unauthorizedErrorCode);
                    response.setContentType(MediaType.APPLICATION_JSON_VALUE);
 
                    jsonObject.put("code", ((HttpServletResponse) response).getStatus());
                    jsonObject.put("message", HttpStatus.UNAUTHORIZED);
                    out = response.getWriter();
                    out.println(jsonObject);
 
                    return false;
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (null != out) {
                        out.flush();
                        out.close();
                    }
                }
 
            }
 
        }
 
        request.setAttribute(REQUEST_CURRENT_KEY, null);
 
        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 {
 
    }
}

在工具类中设置token过期时间等信息:

/**
 * @author 旺旺米雪饼
 */
public class ConstantKit {
    /**
     * 设置删除标志为真
     */
    public static final Integer DEL_FLAG_TRUE = 1;
 
    /**
     * 设置删除标志为假
     */
    public static final Integer DEL_FLAG_FALSE = 0;
 
    /**
     * redis存储token设置的过期时间,10分钟
     */
    public static final Integer TOKEN_EXPIRE_TIME = 60 * 10;
 
    /**
     * 设置可以重置token过期时间的时间界限
     */
    public static final Integer TOKEN_RESET_TIME = 1000 * 100;
 
 
}

并且设计Md5加密后的token值:

/**
 * @author 旺旺米雪饼
 */
@Component
public class Md5TokenGenerator {
 
    public String generate(String... strings) {
        long timestamp = System.currentTimeMillis();
        String tokenMeta = "";
        for (String s : strings) {
            tokenMeta = tokenMeta + s;
        }
        tokenMeta = tokenMeta + timestamp;
        String token = DigestUtils.md5DigestAsHex(tokenMeta.getBytes());
        return token;
    }
}

别忘了在WebConfig中添加拦截器。

最后在controller层中进行验证:

@RestController
public class Welcome {
 
    Logger logger = LoggerFactory.getLogger(Welcome.class);
 
    @Autowired
    Md5TokenGenerator tokenGenerator;
 
    @Autowired
    UserMapper userMapper;
 
    @GetMapping("/welcome")
    public String welcome(){
 
        return "welcome token authentication";
    }
 
 
    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public ResponseTemplate login(String username, String password) {
 
        logger.info("username:"+username+"      password:"+password);
 
        User user = userMapper.getUser(username,password);
 
        logger.info("user:"+user);
 
        JSONObject result = new JSONObject();
        if (user != null) {
 
            Jedis jedis = new Jedis();
            String token = tokenGenerator.generate(username, password);
            jedis.set(username, token);
            //设置key生存时间,当key过期时,它会被自动删除,时间是秒
            jedis.expire(username, ConstantKit.TOKEN_EXPIRE_TIME);
            jedis.set(token, username);
            jedis.expire(token, ConstantKit.TOKEN_EXPIRE_TIME);
            Long currentTime = System.currentTimeMillis();
            jedis.set(token + username, currentTime.toString());
 
            //用完关闭
            jedis.close();
 
            result.put("status", "登录成功");
            result.put("token", token);
        } else {
            result.put("status", "登录失败");
        }
 
        return ResponseTemplate.builder()
                .code(200)
                .message("登录成功")
                .data(result)
                .build();
 
    }
 
    @RequestMapping(value = "test", method = RequestMethod.GET)
    @AuthToken
    public ResponseTemplate test() {
 
        logger.info("已进入test路径");
 
        return ResponseTemplate.builder()
                .code(200)
                .message("Success")
                .data("test url")
                .build();
    }
 
}

来源:https://blog.csdn.net/weixin_43709291/article/details/127415552

 

请登录后发表评论