欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2023-01-13 16:50:27
一、引言登陆权限控制是每个系统都应必备的功能,实现方法也有好多种。下面使用token认证来实现系统的权限访问。功能描述:用户登录成功后,后台返回一个token给调用者,同时自定义一个@authtoke...

一、引言

登陆权限控制是每个系统都应必备的功能,实现方法也有好多种。下面使用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、测试结果

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

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

五、小结

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

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