美文网首页
SpringBoot和Redis实现Token权限认证的实例

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

作者: JAVA架构师的圈子 | 来源:发表于2021-03-22 21:58 被阅读0次

    一、引言

    登陆权限控制是每个系统都应必备的功能,实现方法也有好多种。下面使用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 {
    }//加入Java开发交流君样:756584822一起吹水聊天
    

    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();
     }//加入Java开发交流君样:756584822一起吹水聊天
     //测试权限访问
     @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();
         }
        }//加入Java开发交流君样:756584822一起吹水聊天
       }
      }
      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校验。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//加入Java开发交流君样:756584822一起吹水聊天
     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);
     }//加入Java开发交流君样:756584822一起吹水聊天
     @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;
     }//加入Java开发交流君样:756584822一起吹水聊天
     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;
     /**
      * 加载资源,初始化资源变量
      *///加入Java开发交流君样:756584822一起吹水聊天
     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);
      }
     }
    /*//加入Java开发交流君样:756584822一起吹水聊天
    *
     * @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;//加入Java开发交流君样:756584822一起吹水聊天
      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;
        }
       }//加入Java开发交流君样:756584822一起吹水聊天
      }
      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`)
     //加入Java开发交流君样:756584822一起吹水聊天
    ) 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`)
     //加入Java开发交流君样:756584822一起吹水聊天
    ) 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`)
     //加入Java开发交流君样:756584822一起吹水聊天
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    
    image

    最新2020整理收集的一些高频面试题(都整理成文档),有很多干货,包含mysql,netty,spring,线程,spring cloud、jvm、源码、算法等详细讲解,也有详细的学习规划图,面试题整理等,需要获取这些内容的朋友请加Q君样:756584822

    相关文章

      网友评论

          本文标题:SpringBoot和Redis实现Token权限认证的实例

          本文链接:https://www.haomeiwen.com/subject/whwthltx.html