【SpringBoot+Redis】实现多端登录+token自动续期和定期刷新+自定义注解和拦截器实现鉴权(角色
- 人工智能
- 2025-08-20 22:54:02

目录 前言思路1、登录、token相关2、鉴权相关 实现一、登录1、先定义一个Component组件2、登录、退出 二、鉴权、token相关1、自定义注解2、拦截器鉴权、token续期和定期刷新3、新增/更新角色时,更新redis中角色对应的权限4、更新菜单权限标识时,更新redis中对应的权限标识 最后源码 前言
前面写了几篇鉴权框架SaToken的使用的文章,用这个框架我们很容易就实现了多端登录、单点登录、鉴权等这些功能(详情可以看看这两篇链接)
【SaToken使用】springboot+redis+satoken权限认证
【SaToken使用】SpringBoot整合SaToken(一)token自动续期+token定期刷新+注解鉴权
然后又写了如果项目中没有权限这些东西,不使用SaToken,如何实现多端登录、防重登录、token续期等需求的文章:
SpringBoot+Redis 防止用户重复登录
现在我闲来无事,我想在多端登录、防重登录、token续期这些基础上再实现注解鉴权,而且不使用SaToken,我们自定义注解+拦截器去实现。在实现的过程中,我尝试参考SaToken的源码,边阅读边自己去实现,奈何自己能力有限,啃源码的过程还是挺吃力的。。。那些具体实现要一层一层找过去,大大增加了阅读难度。。。不说了,再说下去要被自己菜哭了😭我们还是说正事吧。
思路 1、登录、token相关首先,我按照SaToken的方式,将登录相关、退出、token相关的这些操作,全部抽出来,放到一个自定义的@Component组件中,在这里实现具体过程,其他地方我们不用关心实现步骤,只需要直接调用这里面的相关方法就行。
登录时在redis中,我们需要存三个东西:一个是token,一个是登录用户信息,还有一个是token的创建时间(用于定期刷新token)。
2、鉴权相关一般权限设计都是用角色进行关联的,我们需要考虑每次鉴权时:用户对应的角色——>角色对应的权限,这两个东西从哪里拿?
如果从数据库拿,请求数据库是否过于频繁?
如果从缓存中拿,那么每次修改角色或菜单的权限标识时,就要更新对应的缓存,操作是否过于麻烦?
上面这两个大概就是两种获取方式的缺点,然后综合考虑之下,我选择了第二种方式,从缓存中拿角色和权限,虽然更新麻烦一点,但不用每校验一次就查一遍数据库,可以减轻数据库的压力。
既然有了思路和解决方式,那就开干!
实现 一、登录 1、先定义一个Component组件用于实现登录和token相关的操作
import cn.hutool.crypto.digest.DigestUtil; import cn.hutool.extra.spring.SpringUtil; import com mon.base.BaseConstant; import com mon.vo.ExceptionVo; import com.entity.sys.SysRoleMenu; import com.entity.sys.SysUser; import com.service.sys.SysRoleMenuService; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import java.util.*; import java.util.stream.Collectors; /** * 登录、token相关组件工具类 */ @Component public class LoginUtil { private RedisUtil redisUtil = SpringUtil.getBean(RedisUtil.class); private SysRoleMenuService sysRoleMenuService = SpringUtil.getBean(SysRoleMenuService.class); private long tokeTime = 3600; /** * 登录 * @param user 登录的用户信息 */ public String login(SysUser user){ return login(user,null,tokeTime); } /** * 登录(指定登录类型) * @param user 登录的用户信息 * @param loginType 登录类型 */ public String login(SysUser user,String loginType){ return login(user,loginType,tokeTime); } /** * 登录(指定登录类型和有效时长) * @param user 登录的用户信息 * @param loginType 登录类型 * @param time token有效期 */ public String login(SysUser user,String loginType,long time){ String str = DigestUtil.md5Hex(String.valueOf(user.getId())); // userId加密 //获取所有包含当前用户id加密后的字符串开头的key redisUtil.del(redisUtil.allKey(BaseConstant.cachePrefix + str + "*")); // 将旧的key删除 if (loginType != null) str = loginType + "_" + str; String token = str + EncryptionUtil.generateRandom(32,false); // token组成为 用户id加密字符串+随机32为字符串 Map<String,Object> map = new HashMap<>(); map.put("token",token); map.put("createTime",System.currentTimeMillis()); map.put("user",user); redisUtil.hmset(BaseConstant.cachePrefix+token,map,time); return token; } /** * 退出(根据token退出) */ public void logout(String token){ redisUtil.del(token); } /** * 获取当前登录用户的token */ public String getToken(HttpServletRequest request){ String token = request.getHeader(BaseConstant.tokenHeader); if (token == null || token.length() == 0 || !redisUtil.hasKey(BaseConstant.cachePrefix+token)){ throw new ExceptionVo(1003,"用户未登录"); } return token; } /** * 获取当前用户的剩余有效时长 */ public long getTimeOut(String token){ return redisUtil.getExpire(BaseConstant.cachePrefix+token); } /** * 获取当前登录信息 */ public Map<String,Object> getLogin(String token){ return redisUtil.hmget(BaseConstant.cachePrefix+token); } /** * 获取当前用户 */ public SysUser getLoginUser(String token){ return (SysUser) redisUtil.hget(BaseConstant.cachePrefix+token,"user"); } /** * 获取当前用户token的创建时间 */ public long getCreateTime(String token){ return (Long) redisUtil.hget(BaseConstant.cachePrefix+token,"createTime"); } /** * 获取当前用户的所有权限集合 */ public List<String> getPermList(String token,List<String> roleList){ if (roleList == null){ roleList = getRoleList(token); } List<String> permList = new ArrayList<>(); if (redisUtil.hasKey(BaseConstant.rolePermList)) { // 如果redis中有所有角色对应的所有权限,直接从redis中拿 Map<String, Object> map = redisUtil.hmget(BaseConstant.rolePermList); for (String role : roleList) { permList.addAll((List<String>) map.get(role)); } }else { // redis没有,则去数据库查询,并将查询到的数据设置到redis中 List<SysRoleMenu> allRolePerm = sysRoleMenuService.listAll(); Map<String, List<SysRoleMenu>> group = allRolePerm.stream().collect(Collectors.groupingBy(SysRoleMenu::getRoleId)); Map<String, List<String>> map = new HashMap<>(); for (String key : group.keySet()) { List<String> perms = group.get(key).stream().map(SysRoleMenu::getMenuId).collect(Collectors.toList()); map.put(key,perms); if (roleList.contains(key)){ permList.addAll(perms); } } redisUtil.hmset(BaseConstant.rolePermList,map); } return permList; } /** * 获取当前用户的所有角色集合 */ public List<String> getRoleList(String token){ String[] roleIds = getLoginUser(token).getRoleIds(); return Arrays.asList(roleIds); } /** * 刷新过期时间 */ public void updateTimeOut(String token){ updateTimeOut(token,tokeTime); } /** * 刷新过期时间(指定过期时间) */ public void updateTimeOut(String token,long time){ redisUtil.hmset(BaseConstant.cachePrefix+token,getLogin(token),time); } /** * 更新用户信息 */ public void updateUser(String token,SysUser user){ redisUtil.hset(BaseConstant.cachePrefix+token,"user",user); } } 2、登录、退出登录
@Resource private LoginUtil loginUtil; /** 登录 */ @PostMapping("/login") public ResultUtil login(String userName, String password, String code,HttpServletRequest request){ try { code = code.toUpperCase(); Object verCode = redisUtil.get(BaseConstant.verCode+code); if (Objects.isNull(verCode)) { return ResultUtil.error("验证码已失效,请重新输入"); } redisUtil.del(BaseConstant.verCode+code); password = RSAUtil.decrypt(password); //密码私钥解密 SysSafe safe = sysSafeService.list().get(0); SysUser user = passwordErrorNum(userName, password,safe);// 校验用户名密码是否正确 int i = safe.getIdleTimeSetting(); //如果系统闲置时间为0,设置token和session永不过期 String token = ""; if (i==0){ token = loginUtil.login(user,null,-1); }else { token = loginUtil.login(user); } return ResultUtil.success(token); } catch (ExceptionVo e) { return ResultUtil.error(e.getCode(),e.getMessage()); }catch (Exception e) { e.printStackTrace(); return ResultUtil.error(BaseConstant.UNKNOWN_EXCEPTION); } }退出
/** 退出登录 */ @DeleteMapping("/logout") public ResultUtil logout(HttpServletRequest request){ String token = request.getHeader(BaseConstant.tokenHeader); //根据token退出登录 loginUtil.logout(token); return ResultUtil.success("退出登录成功"); }获取当前登录用户信息
/** * 获取当前登录用户信息 */ @GetMapping("/getLoginUser") public ResultUtil getLoginUserInfo() { Map<String,Object> map = new HashMap<>(); SysUser user = getLoginUser(); map.put("user",user); //查询角色和权限 map.put("permissions",loginUtil.getPermList(token(),Arrays.asList(user.getRoleIds()))); return ResultUtil.success(map); }公共的controller类
import com mon.util.LoginUtil; import com.entity.sys.SysUser; import org.springframework.beans.factory.annotation.Autowired; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; public abstract class BaseController<S extends BaseService, E extends BaseEntity>{ @Autowired(required=false) protected S service; @Resource protected LoginUtil loginUtil; @Resource protected HttpServletRequest httpServletRequest; public String token(){ return service.token(); } /** * 获取当前登录的用户 */ public SysUser getLoginUser(){ return service.getLoginUser(); } public String loginId(){ return service.loginId(); } }公共的service类
import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com mon.util.LoginUtil; import com mon.util.RedisUtil; import com.entity.sys.SysUser; import com.entity.sys.query.SysQuery; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.List; import java.util.Map; public abstract class BaseService<M extends BaseMapper<E>, E extends BaseEntity> extends ServiceImpl<M, E>{ @Resource protected RedisUtil redisUtil; @Resource protected LoginUtil loginUtil; @Resource protected HttpServletRequest httpServletRequest; /** * 获取当前token */ public String token(){ return loginUtil.getToken(httpServletRequest); } /** * 获取当前登录的用户 */ public SysUser getLoginUser(){ return loginUtil.getLoginUser(token()); } /** * 获取当前登录用户的id */ public String loginId(){ return getLoginUser().getId(); } /** * 更新redis中的权限标识 */ public void updatePerm(String oldPerm,String newPerm){ Map<String, Object> map = redisUtil.hmget(BaseConstant.rolePermList); for (String key : map.keySet()) { List<String> permList = (List<String>) map.get(key); int index = permList.indexOf(oldPerm); // 获取旧权限标识的索引 if (index != -1){ permList.set(index,newPerm); // 更新新标识 } } redisUtil.hmset(BaseConstant.rolePermList,map); } /** * 删除redis中的角色与权限关联 */ public void deleteRolePerm(String roleId){ redisUtil.hdel(BaseConstant.rolePermList,roleId); } // ......省略其他代码 }上面我们就实现了多端登录功能,token自动续期和定期刷新是在拦截器中实现的,和鉴权的拦截器是同一个,我们放到后面说。
二、鉴权、token相关 1、自定义注解 import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 权限校验 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface CheckPermission { /** 拥有指定权限 */ String[] perm() default {}; /** 拥有指定角色 */ String[] role() default {}; /** * 校验类型: * 0: 只校验权限,多个权限需要同时满足; * 1: 只校验权限,多个权限满足一个即可; * 2: 只校验角色,多个角色需要同时满足; * 3: 只校验角色,多个角色满足一个即可; * 4: 校验权限和角色,需要权限和角色同时满足; * 5: 校验权限和角色,权限和角色任意满足一个; * @return */ int type() default 0; } 2、拦截器鉴权、token续期和定期刷新 import cn.hutool.extra.spring.SpringUtil; import com mon.anno.CheckPermission; import com mon.base.BaseConstant; import com mon.util.LoginUtil; import com mon.vo.ExceptionVo; import com.entity.sys.SysUser; import com.entity.sys.SysUserRole; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.regex.Pattern; import java.util.stream.Collectors; /** * 自定义拦截器,实现注解鉴权和token验证 */ public class CustomInterceptor implements HandlerInterceptor { private LoginUtil loginUtil = SpringUtil.getBean(LoginUtil.class); /** 设置响应头部信息 */ private void setHeader(HttpServletRequest request,HttpServletResponse response){ response.setHeader( "Set-Cookie" , "cookiename=httponlyTest;Path=/;Domain=domainvalue;Max-Age=seconds;HTTPOnly"); response.setHeader( "Content-Security-Policy" , "default-src 'self'; script-src 'self'; frame-ancestors 'self'"); response.setHeader("Access-Control-Allow-Origin", (request).getHeader("Origin")); response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Referrer-Policy","no-referrer"); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); } /** token有效期刷新和定期刷新 */ private void refreshToken(HttpServletResponse response,String tokenValue){ //判断token的创建时间是否大于2小时,如果是的话则需要刷新token long time = System.currentTimeMillis() - loginUtil.getCreateTime(tokenValue); long hour = time/1000/(60 * 60); if (hour>2){ // TODO 获取当前用户信息,重新登录,生成新的token,将新token设置到响应头中 SysUser user = loginUtil.getLoginUser(tokenValue); // TODO 这里重新登录(会把旧登录删除),生成新的token String newToken = loginUtil.login(user); response.setHeader(BaseConstant.tokenHeader, newToken); } long tokenTimeout = loginUtil.getTimeOut(tokenValue);// 获取过期时间 if (tokenTimeout != -1){ // token没过期,过期时间不是-1的时候,每次请求都刷新过期时间 loginUtil.updateTimeOut(tokenValue); } } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){ setHeader(request,response); // 获取当前token(获取的是请求头的token),同时会校验token是否过期,过期了会直接抛出异常,所以这里不用做额外处理。 String tokenValue = loginUtil.getToken(request); if (!checkPermisson(handler,tokenValue)){ // 鉴权 throw new ExceptionVo(-1,"没有操作权限"); } refreshToken(response,tokenValue); return true; } /** 校验注解权限或角色 */ private boolean checkPermisson(Object handler,String tokenValue){ boolean flag = true; if (handler instanceof HandlerMethod){ // 获得请求的方法 Method method = ((HandlerMethod) handler).getMethod(); // 获得该方法上面的注解,如果没有注解,直接返回true,通过 CheckPermission annotation = method.getAnnotation(CheckPermission.class); if (annotation != null) { String[] perm = annotation.perm(); // 注解上指定的权限标识 String[] role = annotation.role(); // 注解上指定的角色 int type = annotation.type(); // 校验的类别方式 SysUser user = loginUtil.getLoginUser(tokenValue); //得到当前登录人的权限和角色 List<String> roleList = user.getRoleList().stream().map(SysUserRole.UserRoleVo::getRoleKey).collect(Collectors.toList()); List<String> permList = loginUtil.getPermList(tokenValue, Arrays.asList(user.getRoleIds())); if(type == 0){ // 0: 只校验权限,多个权限需要同时满足; flag = hasElement(permList, Arrays.asList(perm),true); }else if(type == 1){ // 1: 只校验权限,多个权限满足一个即可; flag = hasElement(permList, Arrays.asList(perm),false); }else if(type == 2){ // 2: 只校验角色,多个角色需要同时满足; flag = hasElement(roleList, Arrays.asList(role),true); }else if(type == 3){ // 3: 只校验角色,多个角色满足一个即可; flag = hasElement(roleList, Arrays.asList(role),false); }else if(type == 4){ // 4: 校验权限和角色,需要权限和角色同时满足; flag = hasElement(permList, Arrays.asList(perm),true); if (flag){ // 满足权限时再校验角色,否则直接返回false flag = hasElement(roleList, Arrays.asList(role),true); } }else if(type == 5){ // 5: 校验权限和角色,权限和角色任意满足一个; flag = hasElement(permList, Arrays.asList(perm),true); if (!flag){ // 不满足权限时再校验角色,否则直接返回true flag = hasElement(roleList, Arrays.asList(role),true); } } } } return flag; } /** * 在list中判断指定元素是否存在 * @param list1 拥有的权限列表 * @param list2 给定的权限列表 * @param isAnd 是否要全部满足, true 全部满足,false 满足一个即可 */ public static boolean hasElement(List<String> list1,List<String> list2,boolean isAnd){ boolean flag = true; if (list1 != null && list1.size() != 0 && list2 != null && list2.size() != 0 && list1.size() >= list2.size()) { if (list1.contains(list2)) { return flag; } else { for (String element : list2) { boolean b = hasElement(list1,element); if (isAnd){ if (!b){ // 同时满足,当有一个不满足时,直接跳出,返回false flag = false; break; } }else { if (b){ // 满足一个即可,当有一个满足时,直接跳出,返回true flag = true; break; } flag = false; } } } } else { return false; } return flag; } /** * 在list中判断指定元素是否存在 * @param list 拥有的权限列表 * @param element 给定的权限 */ public static boolean hasElement(List<String> list,String element){ if (list != null && list.size() != 0) { if (list.contains(element)) { return true; } else { Iterator it = list.iterator(); String patt; do { if (!it.hasNext()) { return false; } patt = (String) it.next(); } while(!hasElement(patt, element)); return true; } } else { return false; } } /** * 判断两个元素是否匹配 * @param value1 拥有的 * @param value2 给定的需要匹配的元素 */ private static boolean hasElement(String value1,String value2){ if (value1 == null && value2 == null) { return true; } else if (value1 != null && value2 != null) { return value1.indexOf("*") == -1 ? value1.equals(value2) : Pattern.matches(value1.replaceAll("\\*", ".*"), value2); } else { return false; } } }测试:
3、新增/更新角色时,更新redis中角色对应的权限SysMenuService
/** * 更新redis中的角色与权限关联 */ public void updateRolePerm(String roleId){ SysQuery query = new SysQuery(); query.setId(roleId); List<SysMenu> menuList = selectPermsByRoleId(query); List<String> permList = menuList.stream().map(SysMenu::getPerms).collect(Collectors.toList()); redisUtil.hset(BaseConstant.rolePermList,roleId,permList); }SysRoleController
/** * 新增/修改角色 */ @PostMapping("/insert") @CheckPermission("system:role:edit") public ResultUtil insert(@RequestBody SysRole role) { // ......省略其他代码 sysMenuService.updateRolePerm(role.getId()); // 更新缓存中角色与菜单权限关联 return ResultUtil.success(); } 4、更新菜单权限标识时,更新redis中对应的权限标识SysMenuController
/** * 修改菜单 */ @PostMapping("/update") @CheckPermission("system:menu:update") public ResultUtil update(@RequestBody SysMenu menu){ SysMenu m = service.getById(menu.getId());// 根据菜单id获取更新前的菜单 // ......省略其他代码 if (!m.getPerms().equals(menu.getPerms())){ // 如果更改了权限标识,在redis缓存中也需要更改 service.updatePerm(m.getPerms(),menu.getPerms()); } return ResultUtil.success(); } 最后 源码都看到最后了,不给博主一个赞再走嘛(づ ̄ 3 ̄)づ
【SpringBoot+Redis】实现多端登录+token自动续期和定期刷新+自定义注解和拦截器实现鉴权(角色由讯客互联人工智能栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“【SpringBoot+Redis】实现多端登录+token自动续期和定期刷新+自定义注解和拦截器实现鉴权(角色”