SpringBoot和Redis实现Token权限认证的实例讲解

一、引言

登陆权限控制是每个系统都应必备的功能,实现方法也有好多种。下面使用Token认证来实现系统的权限访问。

功能描述:

用户登录成功后,后台返回一个token给调用者,同时自定义一个@AuthToken注解,被该注解标注的API请求都需要进行token效验,效验通过才可以正常访问,实现接口级的鉴权控制。

同时token具有生命周期,在用户持续一段时间不进行操作的话,token则会过期,用户一直操作的话,则不会过期。

二、环境

SpringBoot

Redis(Docke中镜像)

MySQL(Docker中镜像)

三、流程分析

1、流程分析

(1)、客户端登录,输入用户名和密码,后台进行验证,如果验证失败则返回登录失败的提示。

如果验证成功,则生成 token 然后将 username 和 token 双向绑定 (可以根据 username 取出 token 也可以根据 token 取出username)存入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中过期。

四、具体代码实现

1、自定义注解

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

2、登陆控制器

@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("192.168.1.106", 6379);
   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();
 }
}

3、拦截器

@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) throws Exception {
  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.getParameter(httpHeaderName);
   log.info("Get token from request is {} ", token);
   String username = "";
   Jedis jedis = new Jedis("192.168.1.106", 6379);
   if (token != null && token.length() != 0) {
    username = jedis.get(token);
    log.info("Get username from Redis is {}", username);
   }
   if (username != null && !username.trim().equals("")) {
    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);
    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 {
 }
}

4、测试结果

五、小结

登陆权限控制,实际上利用的就是拦截器的拦截功能。因为每一次请求都要通过拦截器,只有拦截器验证通过了,才能访问想要的请求路径,所以在拦截器中做校验Token校验。

想要代码,可以去GitHub上查看。

https://github.com/Hofanking/token-authentication.git

拦截器介绍,可以参考 这篇文章

补充:springboot+spring security+redis实现登录权限管理

笔者负责的电商项目的技术体系是基于SpringBoot,为了实现一套后端能够承载ToB和ToC的业务,需要完善现有的权限管理体系。

在查看Shiro和Spring Security对比后,笔者认为Spring Security更加适合本项目使用,可以总结为以下2点:

1、基于拦截器的权限校验逻辑,可以针对ToB的业务接口来做相关的权限校验,以笔者的项目为例,ToB的接口请求路径以/openshop/api/开头,可以根据接口请求路径配置全局的ToB的拦截器;

2、Spring Security的权限管理模型更简单直观,对权限、角色和用户做了很好的解耦。

以下介绍本项目的实现步骤

一、在项目中添加Spring相关依赖

 <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
   <version>1.5.3.RELEASE</version>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-webmvc</artifactId>
   <version>4.3.8.RELEASE</version>
  </dependency>

二、使用模板模式定义权限管理拦截器抽象类

public abstract class AbstractAuthenticationInterceptor extends HandlerInterceptorAdapter implements InitializingBean {
 @Resource
 private AccessDecisionManager accessDecisionManager;
 @Override
 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  //检查是否登录
  String userId = null;
  try {
   userId = getUserId();
  }catch (Exception e){
   JsonUtil.renderJson(response,403,"{}");
   return false;
  }
  if(StringUtils.isEmpty(userId)){
   JsonUtil.renderJson(response,403,"{}");
   return false;
  }
  //检查权限
  Collection<? extends GrantedAuthority> authorities = getAttributes(userId);
  Collection<ConfigAttribute> configAttributes = getAttributes(request);
  return accessDecisionManager.decide(authorities,configAttributes);
 }
 //获取用户id
 public abstract String getUserId();
 //根据用户id获取用户的角色集合
 public abstract Collection<? extends GrantedAuthority> getAttributes(String userId);
 //查询请求需要的权限
 public abstract Collection<ConfigAttribute> getAttributes(HttpServletRequest request);
}

三、权限管理拦截器实现类 AuthenticationInterceptor

@Component
public class AuthenticationInterceptor extends AbstractAuthenticationInterceptor {
 @Resource
 private SessionManager sessionManager;
 @Resource
 private UserPermissionService customUserService;
 @Override
 public String getUserId() {
  return sessionManager.obtainUserId();
 }
 @Override
 public Collection<? extends GrantedAuthority> getAttributes(String s) {
  return customUserService.getAuthoritiesById(s);
 }
 @Override
 public Collection<ConfigAttribute> getAttributes(HttpServletRequest request) {
  return customUserService.getAttributes(request);
 }
 @Override
 public void afterPropertiesSet() throws Exception {
 }
}

四、用户Session信息管理类

集成redis维护用户session信息

@Component
public class SessionManager {
 private static final Logger logger = LoggerFactory.getLogger(SessionManager.class);
 @Autowired
 private RedisUtils redisUtils;
 public SessionManager() {
 }
 public UserInfoDTO obtainUserInfo() {
  UserInfoDTO userInfoDTO = null;
  try {
   String token = this.obtainToken();
   logger.info("=======token=========", token);
   if (StringUtils.isEmpty(token)) {
    LemonException.throwLemonException(AccessAuthCode.sessionExpired.getCode(), AccessAuthCode.sessionExpired.getDesc());
   }
   userInfoDTO = (UserInfoDTO)this.redisUtils.obtain(this.obtainToken(), UserInfoDTO.class);
  } catch (Exception var3) {
   logger.error("obtainUserInfo ex:", var3);
  }
  if (null == userInfoDTO) {
   LemonException.throwLemonException(AccessAuthCode.sessionExpired.getCode(), AccessAuthCode.sessionExpired.getDesc());
  }
  return userInfoDTO;
 }
 public String obtainUserId() {
  return this.obtainUserInfo().getUserId();
 }
 public String obtainToken() {
  HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
  String token = request.getHeader("token");
  return token;
 }
 public UserInfoDTO createSession(UserInfoDTO userInfoDTO, long expired) {
  String token = UUIDUtil.obtainUUID("token.");
  userInfoDTO.setToken(token);
  if (expired == 0L) {
   this.redisUtils.put(token, userInfoDTO);
  } else {
   this.redisUtils.put(token, userInfoDTO, expired);
  }
  return userInfoDTO;
 }
 public void destroySession() {
  String token = this.obtainToken();
  if (StringUtils.isNotBlank(token)) {
   this.redisUtils.remove(token);
  }
 }
}

五、用户权限管理service

@Service
public class UserPermissionService {
 @Resource
 private SysUserDao userDao;
 @Resource
 private SysPermissionDao permissionDao;
 private HashMap<String, Collection<ConfigAttribute>> map =null;
 /**
  * 加载资源,初始化资源变量
  */
 public void loadResourceDefine(){
  map = new HashMap<>();
  Collection<ConfigAttribute> array;
  ConfigAttribute cfg;
  List<SysPermission> permissions = permissionDao.findAll();
  for(SysPermission permission : permissions) {
   array = new ArrayList<>();
   cfg = new SecurityConfig(permission.getName());
   array.add(cfg);
   map.put(permission.getUrl(), array);
  }
 }
/*
*
 * @Author zhangs
 * @Description 获取用户权限列表
 * @Date 18:56 2019/11/11
 **/
 public List<GrantedAuthority> getAuthoritiesById(String userId) {
  SysUserRspDTO user = userDao.findById(userId);
  if (user != null) {
   List<SysPermission> permissions = permissionDao.findByAdminUserId(user.getUserId());
   List<GrantedAuthority> grantedAuthorities = new ArrayList <>();
   for (SysPermission permission : permissions) {
    if (permission != null && permission.getName()!=null) {
     GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getName());
     grantedAuthorities.add(grantedAuthority);
    }
   }
   return grantedAuthorities;
  }
  return null;
 }
 /*
 *
  * @Author zhangs
  * @Description 获取当前请求所需权限
  * @Date 18:57 2019/11/11
  **/
 public Collection<ConfigAttribute> getAttributes(HttpServletRequest request) throws IllegalArgumentException {
  if(map !=null) map.clear();
  loadResourceDefine();
  AntPathRequestMatcher matcher;
  String resUrl;
  for(Iterator<String> iter = map.keySet().iterator(); iter.hasNext(); ) {
   resUrl = iter.next();
   matcher = new AntPathRequestMatcher(resUrl);
   if(matcher.matches(request)) {
    return map.get(resUrl);
   }
  }
  return null;
 }
}

六、权限校验类 AccessDecisionManager

通过查看authorities中的权限列表是否含有configAttributes中所需的权限,判断用户是否具有请求当前资源或者执行当前操作的权限。

@Service
public class AccessDecisionManager {
 public boolean decide(Collection<? extends GrantedAuthority> authorities, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
  if(null== configAttributes || configAttributes.size() <=0) {
   return true;
  }
  ConfigAttribute c;
  String needRole;
  for(Iterator<ConfigAttribute> iter = configAttributes.iterator(); iter.hasNext(); ) {
   c = iter.next();
   needRole = c.getAttribute();
   for(GrantedAuthority ga : authorities) {
    if(needRole.trim().equals(ga.getAuthority())) {
     return true;
    }
   }
  }
  return false;
 }
}

七、配置拦截规则

@Configuration
public class WebAppConfigurer extends WebMvcConfigurerAdapter {
 @Resource
 private AbstractAuthenticationInterceptor authenticationInterceptor;
 @Override
 public void addInterceptors(InterceptorRegistry registry) {
  // 多个拦截器组成一个拦截器链
  // addPathPatterns 用于添加拦截规则
  // excludePathPatterns 用户排除拦截
  //对来自/openshop/api/** 这个链接来的请求进行拦截
  registry.addInterceptor(authenticationInterceptor).addPathPatterns("/openshop/api/**");
  super.addInterceptors(registry);
 }
}

八 相关表说明

用户表 sys_user

CREATE TABLE `sys_user` (
 `user_id` varchar(64) NOT NULL COMMENT '用户ID',
 `username` varchar(255) DEFAULT NULL COMMENT '登录账号',
 `first_login` datetime(6) NOT NULL COMMENT '首次登录时间',
 `last_login` datetime(6) NOT NULL COMMENT '上次登录时间',
 `pay_pwd` varchar(100) DEFAULT NULL COMMENT '支付密码',
 `chant_id` varchar(64) NOT NULL DEFAULT '-1' COMMENT '关联商户id',
 `create_time` datetime DEFAULT NULL COMMENT '创建时间',
 `modify_time` datetime DEFAULT NULL COMMENT '修改时间',
 PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

角色表 sys_role

CREATE TABLE `sys_role` (
 `role_id` int(11) NOT NULL AUTO_INCREMENT,
 `name` varchar(255) DEFAULT NULL,
 `create_time` datetime DEFAULT NULL COMMENT '创建时间',
 `modify_time` datetime DEFAULT NULL COMMENT '修改时间',
 PRIMARY KEY (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

用户角色关联表 sys_role_user

CREATE TABLE `sys_role_user` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `sys_user_id` varchar(64) DEFAULT NULL,
 `sys_role_id` int(11) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

权限表 sys_premission

CREATE TABLE `sys_permission` (
 `permission_id` int(11) NOT NULL,
 `name` varchar(255) DEFAULT NULL COMMENT '权限名称',
 `description` varchar(255) DEFAULT NULL COMMENT '权限描述',
 `url` varchar(255) DEFAULT NULL COMMENT '资源url',
 `check_pwd` int(2) NOT NULL DEFAULT '1' COMMENT '是否检查支付密码:0不需要 1 需要',
 `check_sms` int(2) NOT NULL DEFAULT '1' COMMENT '是否校验短信验证码:0不需要 1 需要',
 `create_time` datetime DEFAULT NULL COMMENT '创建时间',
 `modify_time` datetime DEFAULT NULL COMMENT '修改时间',
 PRIMARY KEY (`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

角色权限关联表 sys_permission_role

CREATE TABLE `sys_permission_role` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `role_id` int(11) DEFAULT NULL,
 `permission_id` int(11) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。如有错误或未考虑完全的地方,望不吝赐教。

(0)

相关推荐

  • SpringBoot框架集成token实现登录校验功能

    简介 公司新项目,需要做移动端(Android和IOS),登录模块,两个移动端人员提出用token来校验登录状态,一脸懵懵的,没做过,对于token的基本定义都模棱两可,然后查资料查查查,最终OK完成,写篇博客记录一下 思路: 1.基于session登录 基于session的登录(有回话状态),用户携带账号密码发送请求向服务器,服务器进行判断,成功后将用户信息放入session,用户发送请求判断session中是否有用户信息,有的话放行,没有的话进行拦截,但是考虑到时App产品,牵扯到要判断用户

  • Springboot+SpringSecurity+JWT实现用户登录和权限认证示例

    如今,互联网项目对于安全的要求越来越严格,这就是对后端开发提出了更多的要求,目前比较成熟的几种大家比较熟悉的模式,像RBAC 基于角色权限的验证,shiro框架专门用于处理权限方面的,另一个比较流行的后端框架是Spring-Security,该框架提供了一整套比较成熟,也很完整的机制用于处理各类场景下的可以基于权限,资源路径,以及授权方面的解决方案,部分模块支持定制化,而且在和oauth2.0进行了很好的无缝连接,在移动互联网的授权认证方面有很强的优势,具体的使用大家可以结合自己的业务场景进行选

  • 基于redis实现token验证用户是否登陆

    基于项目需求, 我们要实现一个基于redis实现token登录验证,该如何实现呢: 后端实现: 1.引入redis相关的依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupI

  • SpringBoot和Redis实现Token权限认证的实例讲解

    一.引言 登陆权限控制是每个系统都应必备的功能,实现方法也有好多种.下面使用Token认证来实现系统的权限访问. 功能描述: 用户登录成功后,后台返回一个token给调用者,同时自定义一个@AuthToken注解,被该注解标注的API请求都需要进行token效验,效验通过才可以正常访问,实现接口级的鉴权控制. 同时token具有生命周期,在用户持续一段时间不进行操作的话,token则会过期,用户一直操作的话,则不会过期. 二.环境 SpringBoot Redis(Docke中镜像) MySQL

  • redis在java中的使用(实例讲解)

    1.首先下载jar包放到你的工程中 2.练习 package com.jianyuan.redisTest; import java.util.Iterator; import java.util.List; import java.util.Set; import redis.clients.jedis.Jedis; public class RedisTest { public static void main(String[] args) { //连接本地的Redis服务 Jedis je

  • 基于Django框架的权限组件rbac实例讲解

    1.基于rbac的权限管理 RBAC(Role-Based Access Control,基于角色的访问控制),就是用户通过角色与权限进行关联.简单地说,一个用户拥有若干角色,一个角色拥有若干权限.这样,就构造成"用户-角色-权限"的授权模型.在这种模型中,用户与角色之间,角色与权限之间都是多对多的关系. 简单的模型图示如下: 2.Rbac组件的基本目录结构: 3.按照写的流程,来讲解rbac组件中的各个部分,以及功能, 3.1 models数据库表设计(models.py). 为了在

  • Redis解决库存超卖问题实例讲解

    商品和订单服务间使用MQ 商品服务的库存变化时,通过 MQ 通知订单服务库存变化. 原始的同步流程 查询商品信息 (调用商品服务) 计算总价(生成订单详情) 商品服务扣库存(调用商品服务) 订单入库( 生成订单) // 原始的MySQL同步流程 // 判断此代金券是否加入抢购 SeckillVouchers seckillVouchers = seckillVouchersMapper.selectVoucher(voucherId); AssertUtil.isTrue(seckillVouc

  • ThinkPHP权限认证Auth实例详解

    本文以实例代码的形式深入剖析了ThinkPHP权限认证Auth的实现原理与方法,具体步骤如下: mysql数据库部分sql代码: -- ---------------------------- -- Table structure for think_auth_group -- ---------------------------- DROP TABLE IF EXISTS `think_auth_group`; CREATE TABLE `think_auth_group` ( `id` m

  • springboot+jwt实现token登陆权限认证的实现

    一 前言 此篇文章的内容也是学习不久,终于到周末有时间码一篇文章分享知识追寻者的粉丝们,学完本篇文章,读者将对token类的登陆认证流程有个全面的了解,可以动态搭建自己的登陆认证过程:对小项目而已是个轻量级的认证机制,符合开发需求: 二 jwt实现登陆认证流程 用户使用账号和面发出post请求 服务器接受到请求后使用私钥创建一个jwt,这边会生成token 服务器返回这个jwt给浏览器 浏览器需要将带有token的jwt放入请求头 每次手到客户端请求,服务器验证该jwt的token 验证成功返回

  • 基于SpringBoot整合oauth2实现token认证

    这篇文章主要介绍了基于SpringBoot整合oauth2实现token 认证,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 session和token的区别: session是空间换时间,而token是时间换空间.session占用空间,但是可以管理过期时间,token管理部了过期时间,但是不占用空间. sessionId失效问题和token内包含. session基于cookie,app请求并没有cookie . token更加安全(每次请

  • SpringBoot集成Spring security JWT实现接口权限认证

    1.添加依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjw

  • django restframework使用redis实现token认证

    目录 一.前言 二.详解 1. 前期准备 2. 配置redis 3. 将token写入redis 3.1 原来的登录代码 3.2 重写后的登录代码 3.3 登录后redis存储的用户记录 4. 重写认证token方法 4.1 源码分析 4.2 进行重写 4.3 加入认证配置 4.4 效果展示 三.总结 一.前言 restframework有自己很方便的一套认证.权限体系:官方文档(tokenauthentication) 官方文档的token 是基于数据库中的authtoken_token表来做

随机推荐