JAVA和Nginx 教程大全

网站首页 > 精选教程 正文

给女朋友讲某宝是如何设计用户权限管理的(一)

wys521 2024-11-26 12:36:22 精选教程 20 ℃ 0 评论

一、概述

java应用系统设计过程中,用户认证、用户授权、鉴权是绕不过去的话题。

如果这个权限管理的设计,没有做到与业务系统的隔离,拓展性不够强,很容易就会拖后腿。

这个问题应该做过开发的同学都会有所体会。

现在网络上的各种关于权限管理的框架比较主流的有 Apache Shiro,Spring Security,Sa-Token(新兴起的一个优秀框架)。

这里会有同学说,既然已经有这么多的成熟优秀的权限管理框架,为什么还有再给大家介绍这种实现思路。

在本人工作和学习的过程中,经常会使用这些优秀的权限管理框架。

但是,一旦是这些三方框架出现的异常和问题,想要排查,就比较麻烦。要么就是靠着百度大家的经验。要么就是猛扒代码,一点点去排查。

三方框架对于我们使用者来说,就像是一个黑盒。这一点一直让我觉得有点不顺畅。

同学们,谁不想要一个自己知根知底的的权限管理框架呢。

二、框架使用体验

2.1 项目初始化配置

Springboot老三样。

  • 添加pom依赖
  • 修改配置文件
  • 编写组件代码

引入pom依赖:

<!-- 引入鉴权框架 客户端-->
<dependency>
  <groupId>com.lhit.security</groupId>
  <artifactId>security-client</artifactId>
  <version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- 引入鉴权框架 服务端-->
<dependency>
  <groupId>com.lhit.security</groupId>
  <artifactId>security-server</artifactId>
  <version>0.0.1-SNAPSHOT</version>
</dependency>


修改配置文件:

lhit:
  security:
    server-url-of-check-url: /security/check_url                          # 不配置默认就是这个路径
    server-url-of-check-perms-code: /security/check_perms_code            # 不配置默认就是这个路径
    server-url-of-check-static-res-path: /security/check_static_res_path  # 不配置默认就是这个路径
    server-url-of-token-to-authority: /security/token_to_authority        # 不配置默认就是这个路径
    useCloud: false                                                        # 不配置默认为true回去注册中心查找授权中心服务
    token-key: LHTOKEN                                                    # 请求头中token的key
    expire: 9999999

2.2 用户登录

自定义一个凭证类

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UsernamePasswordVerification implements SecurityVoucher {


    // 用户名
    @NotBlank(message = "未上传 username")
    @ApiModelProperty(value = "username")
    private String username;


    @NotBlank(message = "密码不能为空")
    @ApiModelProperty(value = "密码")
    private String password;
}


自定义一个凭证类认证器:

这个认证器很简单 就是默认admin 密码 123456 然后给与了固定的角色和全部的资源。实际应用中应该从数据库中获取到用户的权限 并组织返回的securityAuthority。

@Component
public class DefaultUsernamePasswordVoucherVoucherVerification implements SecurityVoucherVerification<UsernamePasswordVerification> {
    @Override
    public SecurityAuthority verification(UsernamePasswordVerification usernamePasswordVoucher) throws Exception {
        if (usernamePasswordVoucher.getUsername().equals("admin") && usernamePasswordVoucher.getPassword().equals("123456")) {
            SecurityAuthority securityAuthority = new SecurityAuthority();
            securityAuthority.setSecurityUser(new SecurityUser("1", "admin"));
            securityAuthority.setSecurityRoleList(Lists.newArrayList(new SecurityRole(0L,"roleNo","管理员")));
            securityAuthority.setSecurityResList(Lists.newArrayList(SecurityRes.allCodeRes(), SecurityRes.allUrlRes()));
            return securityAuthority;
        }
        throw CommonException.create(ServerResponse.createByError("用户名或密码错误,默认admin-123456"));
    }
}


开放认证接口:

@Slf4j
@Api(tags = "认证接口")
@RestController
@RequestMapping("/security")
public class AuthorizeController {


    @Autowired
    private SecurityServer securityServer;


    @PostMapping("/login/username_password")
    @ApiOperation("系统用户登录")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "body", dataType = "UsernamePasswordVerification", dataTypeClass = UsernamePasswordVerification.class, name = "param", value = "参数")
    })
    public ServerResponse<AuthenticationVo> sysUserLoginByUsernamePassword(@Validated @RequestBody UsernamePasswordVerification param) throws Exception {
        log.info("|-----------------------------------------------|");
        log.info("进入 系统用户登录 接口 : LoginAuthenticationController-sysUserLoginByUsernamePassword ");
        AuthenticationVo authorize = securityServer.authorize(param);
        return ServerResponse.createBySuccess("登录成功", authorize);
    }


    @GetMapping("/logout")
    @ApiOperation("用户退出")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", dataType = "string", name = "LHTOKEN", value = "用户token"),
    })
    public ServerResponse logout(@RequestHeader(value = "LHTOKEN", defaultValue = "") String token) throws Exception {
        log.info("|-----------------------------------------------|");
        log.info("进入 获取当前用户信息 接口 : SysUserController-logout");
        securityServer.tokenDestroy(new TokenParam(token));
        return ServerResponse.createBySuccess("退出成功");
    }


    /**
     * 获取当前用户信息
     */
    @GetMapping("/current_user_prems")
    @ApiOperation("获取当前用户权限信息")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", dataType = "string", name = "LHTOKEN", value = "用户token"),
    })
    @TokenToAuthority // 这个注解将请求同中的token信息转换为securityAuthority参数,到当前方法中。
    public ServerResponse<SecurityAuthority> getUesrPremsInfo(@ApiIgnore SecurityAuthority securityAuthority) throws Exception {
        log.info("|-----------------------------------------------|");
        log.info("进入 获取当前用户信息 接口 : SysUserCurrentUserController-getUesrInfo");
        return ServerResponse.createBySuccess("获取成功", securityAuthority);
    }


}

2.3 权限验证

路由级别鉴权:

不用做其他额外的配置 只需要打上@HasUrl 就会获取到Controller层的当前url地址,并校验用户是否有访问该url的权限。

并将解析后的用户信息放到方法的SecurityAuthority参数中

在第一步用户登录时,默认给了SecurityRes.allUrlRes() ,则配置了 /** 的url访问权限。

/**
 * 删除用户
 */
@DeleteMapping("/delete/{userId}")
@ApiOperation("删除用户")
@ApiImplicitParams({
        @ApiImplicitParam(paramType = "header", dataType = "string", name = "LHTOKEN", value = "用户token"),
        @ApiImplicitParam(paramType = "path", dataType = "Long", dataTypeClass = Long.class, name = "userId", value = "用户id")
})
@HasUrl
public ServerResponse delUser(@PathVariable(value = "userId") Long userId, @ApiIgnore SecurityAuthority securityAuthority) throws Exception {
    log.info("|-----------------------------------------------|");
    log.info("进入 删除用户 接口 : SysUserAdminController-delUser");
    sysUserService.delUser(userId, getCurrentSysUser(securityAuthority));
    return ServerResponse.createBySuccess("删除成功");
}


方法级别鉴权

@Slf4j
@Service("sysUserService")
public class SysUserServiceImpl implements SysUserService {
  /**
  * 删除用户
  */
  @Override
  @Transactional(rollbackFor = Exception.class)
  @HasPermsCode(permsCode = "user:delete") // 该数据会校验springMVC上下文中token是否有访问该资源的权限
  public void delUser(Long userId, SysUser sysUser) throws Exception {
    log.info("开始 删除用户");
    SysUser checkUser = sysUserDao.getById(userId);
    if (checkUser == null) {
      throw CommonException.create(ServerResponse.createByError("用户信息不存在"));
    }
    try {
      // 删除用户
      checkUser.setDelFlag(true);
      checkUser.setUpdateBy(sysUser.getId());
      checkUser.setUpdateTime(new Date());
      sysUserDao.updateById(checkUser);
      log.info("完成 删除用户");
    } catch (Exception e) {
      throw CommonException.create(e, ServerResponse.createByError("删除用户失败,请联系管理员"));
    }
  }    
}


验证用户是否登录

@Slf4j
@Api(tags = "认证接口")
@RestController
@RequestMapping("/security")
public class AuthorizeController {
    /**
     * 获取当前用户信息
     */
    @GetMapping("/current_user_prems")
    @ApiOperation("获取当前用户权限信息")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", dataType = "string", name = "LHTOKEN", value = "用户token"),
    })
    @TokenToAuthority // 这个注解将请求同中的token信息转换为securityAuthority参数,到当前方法中。如果转换失败抛出401异常
    public ServerResponse<SecurityAuthority> getUesrPremsInfo(@ApiIgnore SecurityAuthority securityAuthority) throws Exception {
        log.info("|-----------------------------------------------|");
        log.info("进入 获取当前用户信息 接口 : SysUserCurrentUserController-getUesrInfo");
        return ServerResponse.createBySuccess("获取成功", securityAuthority);
    }


}


三、时间地点人物

想要描述一个事情,都是将时间地点人物介绍完,才能吧事情描述清楚。

介绍这个设计思路也需要介绍前提:

  • 什么时候用这个框架
  • 框架能提供哪些能力
  • 框架应该有哪些抽象组件

3.1 什么时候用这个框架

显然,如果系统需要提供用户认证、用户授权、用户鉴权的时候,就需要有一个权限管理的模块。

整个流程应该是:

用户认证 --> 颁发token(用户授权) --> 用户鉴权 --> token回收

3.2 框架要提供哪些能力

  • 首先可以对系统用户进行认证。
  • 可以将生成用户口令给客户端,并可以管理该口令。
  • 用户携带口令访问资源是可以判断用户是否有权限来访问这个资源。


以上能力老生常谈就是最基础的权限管理。

3.3 框架应该有哪些抽象组件

这个问题是面向对象开发的java程序员必须要好好思考的问题,就是当你接到一个需求时,如何以面向对象的思维来分析和设计程序来完成需求。

3.3.1 用户认证

用户认证,最最常见的场景就是用户名密码登录。


在这个场景中可能存在:
用户名+密码、用户名+密码+验证码、手机号+验证码、邮箱+验证码 ...... 这么多的登录方式。

而通常来验证这些登录信息是否合法,一般都是要去数据库中读取用户的注册信息来完成认证。

这个场景下可以抽象出来的类有:

1. 凭证类:用户名+密码、用户名+密码+验证码、手机号+验证码、邮箱+验证码 ......

2. 凭证类验证器:用来验证用户上传的凭证是否是合法的。

3.3.2 用户授权

当用户完成认证凭证验证后,服务器应该返回一个用户的口令(token),给用户使用。

并且用户的token应该可以关联并携带出用户绑定的所有资源权限,和角色、部门、岗位等等信息。

用户的资源又分为:

静态资源:

菜单、按钮等静态资源

文档、图片等静态资源

动态资源:

对某种资源的CURD权限:如 是否可以对 sys_user表数据进行CURD。


这个场景下可以抽象出来的类:

  • Token生成器:用来生成用户token.
  • 用户权限描述类:存放用户基本信息、用户拥有的资源权限、用户的角色信息、用 户的其他信息(例如:岗位、部门等)
  • 角色:典型的RBAC设计,角色代表权限的集合。
  • 岗位:从另外一种维度给用户打标签,来区分用户的权限。
  • 部门:从另外一种维度给用户打标签,来区分用户的权限。


其中的岗位和部门,有些权限管理框架中没有,有的或许有一个,这里不纠结这个问题,无论是部门还是岗位,其实都是提供了一种权限判断的维度,类型给用户打上一种标签。

3.3.3 token管理

生成用户token后,所有的token需要管理起来。可以用来统计和维护。

所以需要将上一步获取到的用户权限描述类的信息与token建立一种映射关系。从而可以通过token获取到用户的各种信息。

这个场景可以抽象出来的类:

Token管理类:用来管理所有生成的token。并建立用户信息与token的关联关系。

3.3.4 用户鉴权

当用户通过用户认证和用户授权后,就获取到了他的token口令。

每次用户来访问服务资源时,都需要携带token,当服务器收到请求后,需要通过token获取到用户的所有的权限信息,来判断用户是否可以访问当前资源。

这个场景似乎没有可以抽离出来的类,而是我们要找到一种用户鉴权的方案。

这里,根据以往的经验,基于Spring的AOP切面编程应该是对使用者最友好的方式

所以这里总结下我们需要鉴权的类型:

  • 用户身份验证:token是否对应着一个有效的用户身份。就是用户是不是已经登录。
  • URL基本的鉴权:对于java开发来说就是Controller层开放的接口释放可访问。
  • 方法级别鉴权:对于java开发者来说,就对应着某个类的某个方法该用户是否可以访问,这里可以参考shiro的授权码 user:delete表示是否可以删除用户。
  • 静态资源鉴权:用户是否可以访问系统中的静态资源。比如 一张图片的下载地址。

四、小结

上面铺垫了那么些,其实只是想让大家能跟笔者有一个相同的认知。

  • 用户认证:就是用户登录。
  • 用户授权:就是为用户颁发一个可以表达他拥有的角色和自有的口令。
  • 用户鉴权:就是判断用户有没有权限来访问当前的资源。

先梳理下上面总结出来的类。

凭证类、凭证类验证器、token生成器、token管理器。

以及,基于AOP实现的用户鉴权方案。

大致思路:


未完待续。。。

敬请关注下一篇

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表