image.png用户体系

系统提供了 2 种类型的用户,分别满足对应的管理后台、用户 App 场景。
image.png

  • AdminUser 管理员用户,前端访问 ykkj-ui-admin管理后台,后端访问 /admin-api/** RESTful API 接口。
  • MemberUser 会员用户,前端访问 ykkj-ui-user用户 App,后端访问 /app-api/** RESTful API 接口。

虽然是不同类型的用户,他们访问 RESTful API 接口时,都通过 Token 认证机制,具体可见 《开发指南 —— 功能权限》。

1. 表结构

2 种类型的时候,采用不同数据库的表进行存储,管理员用户对应 system_users表,会员用户对应 member_user表。如下图所示:
image.png
为什么不使用统一的用户表?
确实可以采用这样的方案,新增 type 字段区分用户类型。不同用户类型的信息字段,例如说上图的 dept_id、post_ids 等等,可以增加拓展表,或者就干脆“冗余”在用户表中。
不过实际项目中,不同类型的用户往往是不同的团队维护,并且这也是绝大多团队的实践,所以我们采用了多个用户表的方案。
如果表需要关联多种类型的用户,例如说上述的 system_user_session 表,可以通过 user_type 字段进行区分。并且 user_type 对应 UserTypeEnum(framework/common/enums/UserTypeEnum.java)全局枚举,代码如下:
image.png

2. 如何获取当前登录的用户?

使用 SecurityFrameworkUtils(framework/security/core/util/SecurityFrameworkUtils.java)提供的如下方法,可以获得当前登录用户的信息:

  1. /**
  2. * 【最常用】获得当前用户的编号,从上下文中
  3. *
  4. * @return 用户编号
  5. */
  6. @Nullable
  7. public static Long getLoginUserId() { /** 省略实现 */ }
  8. /**
  9. * 获取当前用户
  10. *
  11. * @return 当前用户
  12. */
  13. @Nullable
  14. public static LoginUser getLoginUser() { /** 省略实现 */ }
  15. /**
  16. * 获得当前用户的角色编号数组
  17. *
  18. * @return 角色编号数组
  19. */
  20. @Nullable
  21. public static Set<Long> getLoginUserRoleIds() { /** 省略实现 */ }

3. 账号密码登录

3.1 管理后台的实现

使用 username 账号 + password 密码进行登录,由 com.ykkj.cdp.module.system.controller.admin.auth.AuthController提供 /admin-api/system/auth/login 接口。代码如下:

  1. @PostMapping("/login")
  2. @ApiOperation("使用账号密码登录")
  3. @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
  4. public CommonResult<AuthLoginRespVO> login(@RequestBody @Valid AuthLoginReqVO reqVO) {
  5. String token = authService.login(reqVO, getClientIP(), getUserAgent());
  6. // 返回结果
  7. return success(AuthLoginRespVO.builder().token(token).build());
  8. }

如果想要关闭登录时的验证码,可修改 infra_config 表的 ykkj.captcha.enable 配置项为 false,并重启后端项目。如下图所示:
image.png

3.2 用户 App 的实现

使用 mobile 手机 + password 密码进行登录,由 com.ykkj.cdp.module.system.controller.app.auth.AppAuthController提供 /app-api/member/auth/login 接口。代码如下:

  1. @PostMapping("/login")
  2. @ApiOperation("使用手机 + 密码登录")
  3. @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
  4. public CommonResult<AppAuthLoginRespVO> login(@RequestBody @Valid AppAuthLoginReqVO reqVO) {
  5. String token = authService.login(reqVO, getClientIP(), getUserAgent());
  6. // 返回结果
  7. return success(AppAuthLoginRespVO.builder().token(token).build());
  8. }

4. 手机验证码登录

4.1 管理后台的实现

① 使用 mobile 手机号获得验证码,由 AuthController提供 /admin-api/system/auth/send-sms-code 接口。代码如下:

  1. @PostMapping("/send-sms-code")
  2. @ApiOperation(value = "发送手机验证码")
  3. @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
  4. public CommonResult<Boolean> sendSmsCode(@RequestBody @Valid AuthSendSmsReqVO reqVO) {
  5. authService.sendSmsCode(getLoginUserId(), reqVO);
  6. return success(true);
  7. }

② 使用 mobile 手机 + code 验证码进行登录,由 AppAuthController提供 /admin-api/system/auth/sms-login 接口。代码如下:

  1. @PostMapping("/sms-login")
  2. @ApiOperation("使用短信验证码登录")
  3. @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
  4. public CommonResult<AuthLoginRespVO> smsLogin(@RequestBody @Valid AuthSmsLoginReqVO reqVO) {
  5. String token = authService.smsLogin(reqVO, getClientIP(), getUserAgent());
  6. // 返回结果
  7. return success(AuthLoginRespVO.builder().token(token).build());
  8. }

4.2 用户 App 的实现

① 使用 mobile 手机号获得验证码,由 AppAuthController提供 /app-api/member/auth/send-sms-code 接口。代码如下:

  1. @PostMapping("/send-sms-code")
  2. @ApiOperation(value = "发送手机验证码")
  3. @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
  4. public CommonResult<Boolean> sendSmsCode(@RequestBody @Valid AppAuthSendSmsReqVO reqVO) {
  5. authService.sendSmsCode(getLoginUserId(), reqVO);
  6. return success(true);
  7. }

② 使用 mobile 手机 + code 验证码进行登录,由 AppAuthController提供 /app-api/member/auth/sms-login 接口。代码如下:

  1. @PostMapping("/sms-login")
  2. @ApiOperation("使用手机 + 验证码登录")
  3. @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
  4. public CommonResult<AppAuthLoginRespVO> smsLogin(@RequestBody @Valid AppAuthSmsLoginReqVO reqVO) {
  5. String token = authService.smsLogin(reqVO, getClientIP(), getUserAgent());
  6. // 返回结果
  7. return success(AppAuthLoginRespVO.builder().token(token).build());
  8. }

如果用户未注册,会自动使用手机号进行注册会员用户。所以,/app-api/member/user/sms-login 接口也提供了用户注册的功能

5. 三方登录

系统对接国内多个第三方平台,实现三方登录的功能。例如说:

  • 管理后台:企业微信、阿里钉钉
  • 用户 App:微信公众号、微信小程序

友情提示:为了表述方便,本文主要使用管理后台的三方登录作为示例。
用户 App 也是支持该功能,你可以自己去体验一下。

5.1. 表结构

image.png
① 三方登录完成时,系统会将三方用户存储到 system_social_user表中,通过 type标记对应的第三方平台。
② 【未】关联本系统 User 的三方用户,需要在三方登录完成后,使用账号密码进行「绑定登录」,成功后记录到 system_social_user_bind表中。
【已】关联本系统 User 的三方用户,在三方登录完成后,直接进入系统,即「快捷登录」。

5.2. 绑定登录

① 使用浏览器访问 http://127.0.0.1:1024/login地址,点击 [钉钉] 或者 [企业微信] 进行三方登录。此时,会调用 /admin-api/system/auth/social-auth-redirect接口,获得第三方平台的登录地址,并进行跳转。
image.png
然后,使用 [钉钉] 或者 [企业微信] 进行扫码,完成三方登录。
② 三方登录成功后,跳转回 http://127.0.0.1:1024/social-login地址。此时,会调用 /admin-api/system/auth/quick-login接口,尝试「快捷登录」。由于该三方用户【未】关联管理后台的 AdminUser 用户,所以会看到 “未绑定账号,需要进行绑定” 报错。
image.png
③ 输入账号密码,点击 [提交] 按钮,进行「绑定登录」。此时,会调用 /admin-api/system/auth/bind-login接口。成功后,即可进入系统的首页。
image.png

5.3. 快捷登录

退出系统,再进行一次三方登录的流程。
【相同】① 使用浏览器访问 http://127.0.0.1:1024/login地址,点击 [钉钉] 或者 [企业微信] 进行三方登录。此时,会调用 /admin-api/system/auth/social-auth-redirect接口,获得第三方平台的登录地址,并进行跳转。
image.png
【不同】② 三方登录成功后,跳转回 http://127.0.0.1:1024/social-login地址。此时,会调用 /admin-api/system/auth/quick-login接口,尝试「快捷登录」。由于该三方用户【已】关联管理后台的 AdminUser 用户,所以直接进入系统的首页。
image.png

5.4. 绑定与解绑

访问 http://127.0.0.1:1024/user/profile地址,选择 [社交信息] 选项,可以三方用户的绑定与解绑。

5.5. 配置文件

在 application-{env}.yaml配置文件中,对应 justauth 配置项,填写你的第三方平台的配置信息。
image.png
系统使用 justauth-spring-boot-starter JustAuth组件,想要对接其它第三方平台,只需要新增对应的配置信息即可。
疑问:ykkj-spring-boot-starter-biz-social 技术组件的作用是什么?
ykkj-spring-boot-starter-biz-social对 JustAuth 进行二次封装,提供微信小程序的集成。

5.6. 第三方平台的申请

注意,如果第三方平台如果需要配置具体的授信地址,需要添加 /social-login 用于三方登录回调页、/user/profile 用于三方用户的绑定与解绑。

5.7 管理后台的实现

① 跳转第三方平台,来获得三方授权码,由 AuthController提供 /admin-api/system/auth/social-auth-redirect 接口。代码如下:

@GetMapping("/social-auth-redirect")
@ApiOperation("社交授权的跳转")
@ApiImplicitParams({
    @ApiImplicitParam(name = "type", value = "社交类型", required = true, dataTypeClass = Integer.class),
    @ApiImplicitParam(name = "redirectUri", value = "回调路径", dataTypeClass = String.class)
})
public CommonResult<String> socialAuthRedirect(@RequestParam("type") Integer type,
                                               @RequestParam("redirectUri") String redirectUri) {
    return CommonResult.success(socialUserService.getAuthorizeUrl(type, redirectUri));
}

② 使用 code 三方授权码进行快登录,由 AuthController提供 /admin-api/system/auth/social-quick-login 接口。代码如下:

@PostMapping("/social-quick-login")
@ApiOperation("社交快捷登录,使用 code 授权码")
@OperateLog(enable = false) // 避免 Post 请求被记录操作日志
public CommonResult<AuthLoginRespVO> socialQuickLogin(@RequestBody @Valid AuthSocialQuickLoginReqVO reqVO) {
    String token = authService.socialLogin(reqVO, getClientIP(), getUserAgent());
    // 返回结果
    return success(AuthLoginRespVO.builder().token(token).build());
}

③ 使用 code 三方授权码 + username + password 进行绑定登录,由 AuthController提供 /admin-api/system/auth/social-bind-login 接口。代码如下:

@PostMapping("/social-bind-login")
@ApiOperation("社交绑定登录,使用 code 授权码 + 账号密码")
@OperateLog(enable = false) // 避免 Post 请求被记录操作日志
public CommonResult<AuthLoginRespVO> socialBindLogin(@RequestBody @Valid AuthSocialBindLoginReqVO reqVO) {
    String token = authService.socialBindLogin(reqVO, getClientIP(), getUserAgent());
    // 返回结果
    return success(AuthLoginRespVO.builder().token(token).build());
}

6. 注册

6.1 管理后台的实现

管理后台暂不支持用户注册,而是通过在 [系统管理 -> 用户管理] 菜单,进行添加用户,由 UserController提供 /admin-api/system/user/create 接口。代码如下:

@PostMapping("/create")
@ApiOperation("新增用户")
@PreAuthorize("@ss.hasPermission('system:user:create')")
public CommonResult<Long> createUser(@Valid @RequestBody UserCreateReqVO reqVO) {
    Long id = userService.createUser(reqVO);
    return success(id);
}

6.2 用户 App 的实现

手机验证码登录时,如果用户未注册,会自动使用手机号进行注册会员用户。所以,/app-api/system/user/sms-login 接口也提供了用户注册的功能

7. 用户登出

用户登出的功能,统一使用 Spring Security 框架,通过删除用户 Token 的方式来实现。代码如下:

image.png
image.png
差别在于使用的 API 接口不同,管理员用户使用 /admin-api/system/logout,会员用户使用 /app-api/member/logout。