温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

怎么在SpringBoot和Redis中实现一个Token权限认证功能

发布时间:2021-02-19 14:55:47 来源:亿速云 阅读:509 作者:Leah 栏目:开发技术

怎么在SpringBoot和Redis中实现一个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

关于怎么在SpringBoot和Redis中实现一个Token权限认证功能问题的解答就分享到这里了,希望以上内容可以对大家有一定的帮助,如果你还有很多疑惑没有解开,可以关注亿速云行业资讯频道了解更多相关知识。

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI