经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 数据库/运维 » Redis » 查看文章
SpringBoot和Redis实现Token权限认证的实例讲解
来源:jb51  时间:2021/2/18 16:18:15  对本文有异议

一、引言

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

  1. @Target({ElementType.METHOD, ElementType.TYPE})
  2. @Retention(RetentionPolicy.RUNTIME)
  3. public @interface AuthToken {
  4. }

2、登陆控制器

  1. @RestController
  2. public class welcome {
  3. Logger logger = LoggerFactory.getLogger(welcome.class);
  4. @Autowired
  5. Md5TokenGenerator tokenGenerator;
  6. @Autowired
  7. UserMapper userMapper;
  8. @GetMapping("/welcome")
  9. public String welcome(){
  10. return "welcome token authentication";
  11. }
  12. @RequestMapping(value = "/login", method = RequestMethod.GET)
  13. public ResponseTemplate login(String username, String password) {
  14. logger.info("username:"+username+" password:"+password);
  15. User user = userMapper.getUser(username,password);
  16. logger.info("user:"+user);
  17. JSONObject result = new JSONObject();
  18. if (user != null) {
  19. Jedis jedis = new Jedis("192.168.1.106", 6379);
  20. String token = tokenGenerator.generate(username, password);
  21. jedis.set(username, token);
  22. //设置key生存时间,当key过期时,它会被自动删除,时间是秒
  23. jedis.expire(username, ConstantKit.TOKEN_EXPIRE_TIME);
  24. jedis.set(token, username);
  25. jedis.expire(token, ConstantKit.TOKEN_EXPIRE_TIME);
  26. Long currentTime = System.currentTimeMillis();
  27. jedis.set(token + username, currentTime.toString());
  28. //用完关闭
  29. jedis.close();
  30. result.put("status", "登录成功");
  31. result.put("token", token);
  32. } else {
  33. result.put("status", "登录失败");
  34. }
  35. return ResponseTemplate.builder()
  36. .code(200)
  37. .message("登录成功")
  38. .data(result)
  39. .build();
  40. }
  41. //测试权限访问
  42. @RequestMapping(value = "test", method = RequestMethod.GET)
  43. @AuthToken
  44. public ResponseTemplate test() {
  45. logger.info("已进入test路径");
  46. return ResponseTemplate.builder()
  47. .code(200)
  48. .message("Success")
  49. .data("test url")
  50. .build();
  51. }
  52. }

3、拦截器

  1. @Slf4j
  2. public class AuthorizationInterceptor implements HandlerInterceptor {
  3. //存放鉴权信息的Header名称,默认是Authorization
  4. private String httpHeaderName = "Authorization";
  5. //鉴权失败后返回的错误信息,默认为401 unauthorized
  6. private String unauthorizedErrorMessage = "401 unauthorized";
  7. //鉴权失败后返回的HTTP错误码,默认为401
  8. private int unauthorizedErrorCode = HttpServletResponse.SC_UNAUTHORIZED;
  9. //存放登录用户模型Key的Request Key
  10. public static final String REQUEST_CURRENT_KEY = "REQUEST_CURRENT_KEY";
  11. @Override
  12. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  13. if (!(handler instanceof HandlerMethod)) {
  14. return true;
  15. }
  16. HandlerMethod handlerMethod = (HandlerMethod) handler;
  17. Method method = handlerMethod.getMethod();
  18. // 如果打上了AuthToken注解则需要验证token
  19. if (method.getAnnotation(AuthToken.class) != null || handlerMethod.getBeanType().getAnnotation(AuthToken.class) != null) {
  20. String token = request.getParameter(httpHeaderName);
  21. log.info("Get token from request is {} ", token);
  22. String username = "";
  23. Jedis jedis = new Jedis("192.168.1.106", 6379);
  24. if (token != null && token.length() != 0) {
  25. username = jedis.get(token);
  26. log.info("Get username from Redis is {}", username);
  27. }
  28. if (username != null && !username.trim().equals("")) {
  29. Long tokeBirthTime = Long.valueOf(jedis.get(token + username));
  30. log.info("token Birth time is: {}", tokeBirthTime);
  31. Long diff = System.currentTimeMillis() - tokeBirthTime;
  32. log.info("token is exist : {} ms", diff);
  33. if (diff > ConstantKit.TOKEN_RESET_TIME) {
  34. jedis.expire(username, ConstantKit.TOKEN_EXPIRE_TIME);
  35. jedis.expire(token, ConstantKit.TOKEN_EXPIRE_TIME);
  36. log.info("Reset expire time success!");
  37. Long newBirthTime = System.currentTimeMillis();
  38. jedis.set(token + username, newBirthTime.toString());
  39. }
  40. //用完关闭
  41. jedis.close();
  42. request.setAttribute(REQUEST_CURRENT_KEY, username);
  43. return true;
  44. } else {
  45. JSONObject jsonObject = new JSONObject();
  46. PrintWriter out = null;
  47. try {
  48. response.setStatus(unauthorizedErrorCode);
  49. response.setContentType(MediaType.APPLICATION_JSON_VALUE);
  50. jsonObject.put("code", ((HttpServletResponse) response).getStatus());
  51. jsonObject.put("message", HttpStatus.UNAUTHORIZED);
  52. out = response.getWriter();
  53. out.println(jsonObject);
  54. return false;
  55. } catch (Exception e) {
  56. e.printStackTrace();
  57. } finally {
  58. if (null != out) {
  59. out.flush();
  60. out.close();
  61. }
  62. }
  63. }
  64. }
  65. request.setAttribute(REQUEST_CURRENT_KEY, null);
  66. return true;
  67. }
  68. @Override
  69. public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
  70. }
  71. @Override
  72. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
  73. }
  74. }

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相关依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-security</artifactId>
  4. <version>1.5.3.RELEASE</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework</groupId>
  8. <artifactId>spring-webmvc</artifactId>
  9. <version>4.3.8.RELEASE</version>
  10. </dependency>

二、使用模板模式定义权限管理拦截器抽象类

  1. public abstract class AbstractAuthenticationInterceptor extends HandlerInterceptorAdapter implements InitializingBean {
  2. @Resource
  3. private AccessDecisionManager accessDecisionManager;
  4. @Override
  5. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  6. //检查是否登录
  7. String userId = null;
  8. try {
  9. userId = getUserId();
  10. }catch (Exception e){
  11. JsonUtil.renderJson(response,403,"{}");
  12. return false;
  13. }
  14. if(StringUtils.isEmpty(userId)){
  15. JsonUtil.renderJson(response,403,"{}");
  16. return false;
  17. }
  18. //检查权限
  19. Collection<? extends GrantedAuthority> authorities = getAttributes(userId);
  20. Collection<ConfigAttribute> configAttributes = getAttributes(request);
  21. return accessDecisionManager.decide(authorities,configAttributes);
  22. }
  23. //获取用户id
  24. public abstract String getUserId();
  25. //根据用户id获取用户的角色集合
  26. public abstract Collection<? extends GrantedAuthority> getAttributes(String userId);
  27. //查询请求需要的权限
  28. public abstract Collection<ConfigAttribute> getAttributes(HttpServletRequest request);
  29. }

三、权限管理拦截器实现类 AuthenticationInterceptor

  1. @Component
  2. public class AuthenticationInterceptor extends AbstractAuthenticationInterceptor {
  3. @Resource
  4. private SessionManager sessionManager;
  5. @Resource
  6. private UserPermissionService customUserService;
  7. @Override
  8. public String getUserId() {
  9. return sessionManager.obtainUserId();
  10. }
  11. @Override
  12. public Collection<? extends GrantedAuthority> getAttributes(String s) {
  13. return customUserService.getAuthoritiesById(s);
  14. }
  15. @Override
  16. public Collection<ConfigAttribute> getAttributes(HttpServletRequest request) {
  17. return customUserService.getAttributes(request);
  18. }
  19. @Override
  20. public void afterPropertiesSet() throws Exception {
  21. }
  22. }

四、用户Session信息管理类

集成redis维护用户session信息

  1. @Component
  2. public class SessionManager {
  3. private static final Logger logger = LoggerFactory.getLogger(SessionManager.class);
  4. @Autowired
  5. private RedisUtils redisUtils;
  6. public SessionManager() {
  7. }
  8. public UserInfoDTO obtainUserInfo() {
  9. UserInfoDTO userInfoDTO = null;
  10. try {
  11. String token = this.obtainToken();
  12. logger.info("=======token=========", token);
  13. if (StringUtils.isEmpty(token)) {
  14. LemonException.throwLemonException(AccessAuthCode.sessionExpired.getCode(), AccessAuthCode.sessionExpired.getDesc());
  15. }
  16. userInfoDTO = (UserInfoDTO)this.redisUtils.obtain(this.obtainToken(), UserInfoDTO.class);
  17. } catch (Exception var3) {
  18. logger.error("obtainUserInfo ex:", var3);
  19. }
  20. if (null == userInfoDTO) {
  21. LemonException.throwLemonException(AccessAuthCode.sessionExpired.getCode(), AccessAuthCode.sessionExpired.getDesc());
  22. }
  23. return userInfoDTO;
  24. }
  25. public String obtainUserId() {
  26. return this.obtainUserInfo().getUserId();
  27. }
  28. public String obtainToken() {
  29. HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
  30. String token = request.getHeader("token");
  31. return token;
  32. }
  33. public UserInfoDTO createSession(UserInfoDTO userInfoDTO, long expired) {
  34. String token = UUIDUtil.obtainUUID("token.");
  35. userInfoDTO.setToken(token);
  36. if (expired == 0L) {
  37. this.redisUtils.put(token, userInfoDTO);
  38. } else {
  39. this.redisUtils.put(token, userInfoDTO, expired);
  40. }
  41. return userInfoDTO;
  42. }
  43. public void destroySession() {
  44. String token = this.obtainToken();
  45. if (StringUtils.isNotBlank(token)) {
  46. this.redisUtils.remove(token);
  47. }
  48. }
  49. }

五、用户权限管理service

  1. @Service
  2. public class UserPermissionService {
  3. @Resource
  4. private SysUserDao userDao;
  5. @Resource
  6. private SysPermissionDao permissionDao;
  7. private HashMap<String, Collection<ConfigAttribute>> map =null;
  8. /**
  9. * 加载资源,初始化资源变量
  10. */
  11. public void loadResourceDefine(){
  12. map = new HashMap<>();
  13. Collection<ConfigAttribute> array;
  14. ConfigAttribute cfg;
  15. List<SysPermission> permissions = permissionDao.findAll();
  16. for(SysPermission permission : permissions) {
  17. array = new ArrayList<>();
  18. cfg = new SecurityConfig(permission.getName());
  19. array.add(cfg);
  20. map.put(permission.getUrl(), array);
  21. }
  22. }
  23. /*
  24. *
  25. * @Author zhangs
  26. * @Description 获取用户权限列表
  27. * @Date 18:56 2019/11/11
  28. **/
  29. public List<GrantedAuthority> getAuthoritiesById(String userId) {
  30. SysUserRspDTO user = userDao.findById(userId);
  31. if (user != null) {
  32. List<SysPermission> permissions = permissionDao.findByAdminUserId(user.getUserId());
  33. List<GrantedAuthority> grantedAuthorities = new ArrayList <>();
  34. for (SysPermission permission : permissions) {
  35. if (permission != null && permission.getName()!=null) {
  36. GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getName());
  37. grantedAuthorities.add(grantedAuthority);
  38. }
  39. }
  40. return grantedAuthorities;
  41. }
  42. return null;
  43. }
  44. /*
  45. *
  46. * @Author zhangs
  47. * @Description 获取当前请求所需权限
  48. * @Date 18:57 2019/11/11
  49. **/
  50. public Collection<ConfigAttribute> getAttributes(HttpServletRequest request) throws IllegalArgumentException {
  51. if(map !=null) map.clear();
  52. loadResourceDefine();
  53. AntPathRequestMatcher matcher;
  54. String resUrl;
  55. for(Iterator<String> iter = map.keySet().iterator(); iter.hasNext(); ) {
  56. resUrl = iter.next();
  57. matcher = new AntPathRequestMatcher(resUrl);
  58. if(matcher.matches(request)) {
  59. return map.get(resUrl);
  60. }
  61. }
  62. return null;
  63. }
  64. }

六、权限校验类 AccessDecisionManager

通过查看authorities中的权限列表是否含有configAttributes中所需的权限,判断用户是否具有请求当前资源或者执行当前操作的权限。

  1. @Service
  2. public class AccessDecisionManager {
  3. public boolean decide(Collection<? extends GrantedAuthority> authorities, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
  4. if(null== configAttributes || configAttributes.size() <=0) {
  5. return true;
  6. }
  7. ConfigAttribute c;
  8. String needRole;
  9. for(Iterator<ConfigAttribute> iter = configAttributes.iterator(); iter.hasNext(); ) {
  10. c = iter.next();
  11. needRole = c.getAttribute();
  12. for(GrantedAuthority ga : authorities) {
  13. if(needRole.trim().equals(ga.getAuthority())) {
  14. return true;
  15. }
  16. }
  17. }
  18. return false;
  19. }
  20. }

七、配置拦截规则

  1. @Configuration
  2. public class WebAppConfigurer extends WebMvcConfigurerAdapter {
  3. @Resource
  4. private AbstractAuthenticationInterceptor authenticationInterceptor;
  5. @Override
  6. public void addInterceptors(InterceptorRegistry registry) {
  7. // 多个拦截器组成一个拦截器链
  8. // addPathPatterns 用于添加拦截规则
  9. // excludePathPatterns 用户排除拦截
  10. //对来自/openshop/api/** 这个链接来的请求进行拦截
  11. registry.addInterceptor(authenticationInterceptor).addPathPatterns("/openshop/api/**");
  12. super.addInterceptors(registry);
  13. }
  14. }

八 相关表说明

用户表 sys_user

  1. CREATE TABLE `sys_user` (
  2. `user_id` varchar(64) NOT NULL COMMENT '用户ID',
  3. `username` varchar(255) DEFAULT NULL COMMENT '登录账号',
  4. `first_login` datetime(6) NOT NULL COMMENT '首次登录时间',
  5. `last_login` datetime(6) NOT NULL COMMENT '上次登录时间',
  6. `pay_pwd` varchar(100) DEFAULT NULL COMMENT '支付密码',
  7. `chant_id` varchar(64) NOT NULL DEFAULT '-1' COMMENT '关联商户id',
  8. `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  9. `modify_time` datetime DEFAULT NULL COMMENT '修改时间',
  10. PRIMARY KEY (`user_id`)
  11. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

角色表 sys_role

  1. CREATE TABLE `sys_role` (
  2. `role_id` int(11) NOT NULL AUTO_INCREMENT,
  3. `name` varchar(255) DEFAULT NULL,
  4. `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  5. `modify_time` datetime DEFAULT NULL COMMENT '修改时间',
  6. PRIMARY KEY (`role_id`)
  7. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

用户角色关联表 sys_role_user

  1. CREATE TABLE `sys_role_user` (
  2. `id` int(11) NOT NULL AUTO_INCREMENT,
  3. `sys_user_id` varchar(64) DEFAULT NULL,
  4. `sys_role_id` int(11) DEFAULT NULL,
  5. PRIMARY KEY (`id`)
  6. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

权限表 sys_premission

  1. CREATE TABLE `sys_permission` (
  2. `permission_id` int(11) NOT NULL,
  3. `name` varchar(255) DEFAULT NULL COMMENT '权限名称',
  4. `description` varchar(255) DEFAULT NULL COMMENT '权限描述',
  5. `url` varchar(255) DEFAULT NULL COMMENT '资源url',
  6. `check_pwd` int(2) NOT NULL DEFAULT '1' COMMENT '是否检查支付密码:0不需要 1 需要',
  7. `check_sms` int(2) NOT NULL DEFAULT '1' COMMENT '是否校验短信验证码:0不需要 1 需要',
  8. `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  9. `modify_time` datetime DEFAULT NULL COMMENT '修改时间',
  10. PRIMARY KEY (`permission_id`)
  11. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

角色权限关联表 sys_permission_role

  1. CREATE TABLE `sys_permission_role` (
  2. `id` int(11) NOT NULL AUTO_INCREMENT,
  3. `role_id` int(11) DEFAULT NULL,
  4. `permission_id` int(11) DEFAULT NULL,
  5. PRIMARY KEY (`id`)
  6. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

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

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号