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
以上为个人经验,希望能给大家一个参考,也希望大家多多支持。如有错误或未考虑完全的地方,望不吝赐教。
上一篇: JS中promise化微信小程序api