# scaffold 项目之用户体系
# 用户体系
系统提供两种用户类型,分别满足后台管理、用户 App 场景。
- 后台管理用户,前端访问接口:
/admin-api/**
RESTful API 接口。 - App 用户,前端访问接口:
/app-api/**
RESTful API 接口。
# 表结构
2 种类型的用户采用不同的存储方式(不同表), 后台管理用户的表是 system_users
, app 用户的表则是 member_user
, 授权表中通过 user_type
字段来区分
为什么不统一用户还要区分表
确实可以放在同一张表,在用户表添加一个字段 user_type
来区分,但是在实际的项目中,不同类型的用户往往是不同部门开发维护的。
如果表需要关联多个用户类型,列如上述所说的 system_oauth2_access_token
访问令牌表,可以通过 user_type
字段区分,并且 user_type
对应 UserTypeEnum
全局枚举,代码如下:
| |
| * <p> Project: scaffold - UserTypeEnum </p> |
| * |
| * 全局用户类型枚举 |
| * @author Tz |
| * @date 2024/01/09 23:45 |
| * @version 1.0.0 |
| * @since 1.0.0 |
| */ |
| @AllArgsConstructor |
| @Getter |
| public enum UserTypeEnum implements IntArrayValuable { |
| |
| |
| * 面向 c 端,普通用户 |
| */ |
| MEMBER(1, "会员"), |
| |
| |
| * 面向 b 端,管理后台 |
| */ |
| ADMIN(2, "管理员"); |
| |
| public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(UserTypeEnum::getValue).toArray(); |
| |
| |
| * 类型 |
| */ |
| private final Integer value; |
| |
| * 类型名 |
| */ |
| private final String name; |
| |
| public static UserTypeEnum valueOf(Integer value) { |
| return ArrayUtil.firstMatch(userType -> userType.getValue().equals(value), UserTypeEnum.values()); |
| } |
| |
| @Override |
| public int[] array() { |
| return ARRAYS; |
| } |
| } |
# 如何获取当前登陆用户信息
使用 SecurityFrameworkUtils
提供的如下方法,可以获的当前登陆用户信息
# 获取当前用户信息
| public static LoginUser getLoginUser() |
# 获取当前用户编号(最常用)
| public static Long getLoginUserId() |
# 账号密码登陆
# 管理后台的实现
使用 username
账号 + password
密码进行登陆,由 AuthController
提供 admin-api/system/auth/login
接口。代码如下:
| @PostMapping("/login") |
| @PermitAll |
| @Operation(summary = "使用账号密码登录") |
| @OperateLog(enable = false) |
| public CommonResult<AuthLoginRespVO> login(@RequestBody @Valid AuthLoginReqVO reqVO) { |
| return success(authService.login(reqVO)); |
| } |
| |
| |
| public AuthLoginRespVO login(AuthLoginReqVO reqVO) { |
| |
| validateCaptcha(reqVO); |
| |
| |
| AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword()); |
| |
| |
| if (reqVO.getSocialType() != null) { |
| socialUserService.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(), |
| reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState())); |
| } |
| |
| return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME); |
| } |
# app 端的实现
使用 username
账号 + password
密码进行登陆,由 AppAuthController
提供 /app-api/member/auth/login
接口。代码如下:
| @PostMapping("/login") |
| @Operation(summary = "使用手机 + 密码登录") |
| public CommonResult<AppAuthLoginRespVO> login(@RequestBody @Valid AppAuthLoginReqVO reqVO) { |
| return success(authService.login(reqVO)); |
| } |
| |
| |
| |
| public AppAuthLoginRespVO login(AppAuthLoginReqVO reqVO) { |
| |
| MemberUserDO user = login0(reqVO.getMobile(), reqVO.getPassword()); |
| |
| |
| String openid = null; |
| if (reqVO.getSocialType() != null) { |
| openid = socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(), |
| reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState())); |
| } |
| |
| |
| return createTokenAfterLoginSuccess(user, reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE, openid); |
| } |
# 手机验证码登陆
# 管理后台的实现
- 使用
mobile
手机号获取验证码,由 AuthController
提供 admin-api/system/auth/send-sms-code
接口,获取验证码。代码如下:
| @PostMapping("/send-sms-code") |
| @PermitAll |
| @Operation(summary = "发送手机验证码") |
| @OperateLog(enable = false) |
| public CommonResult<Boolean> sendLoginSmsCode(@RequestBody @Valid AuthSmsSendReqVO reqVO) { |
| authService.sendSmsCode(reqVO); |
| return success(true); |
| } |
| |
| |
| public void sendSmsCode(AuthSmsSendReqVO reqVO) { |
| |
| if (userService.getUserByMobile(reqVO.getMobile()) == null) { |
| throw exception(AUTH_MOBILE_NOT_EXISTS); |
| } |
| |
| smsCodeApi.sendSmsCode(AuthConvert.INSTANCE.convert(reqVO).setCreateIp(getClientIP())); |
| } |
- 使用手机号加
验证码
登陆,由 AuthController
提供 admin-api/system/auth/sms-login
接口登陆。代码如下:
| @PostMapping("/sms-login") |
| @PermitAll |
| @Operation(summary = "使用短信验证码登录") |
| @OperateLog(enable = false) |
| public CommonResult<AuthLoginRespVO> smsLogin(@RequestBody @Valid AuthSmsLoginReqVO reqVO) { |
| return success(authService.smsLogin(reqVO)); |
| } |
| |
| |
| public AuthLoginRespVO smsLogin(AuthSmsLoginReqVO reqVO) { |
| |
| smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.ADMIN_MEMBER_LOGIN.getScene(), getClientIP())); |
| |
| |
| AdminUserDO user = userService.getUserByMobile(reqVO.getMobile()); |
| if (user == null) { |
| throw exception(USER_NOT_EXISTS); |
| } |
| |
| |
| return createTokenAfterLoginSuccess(user.getId(), reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE); |
| } |
# app 端实现
- 使用
mobile
手机号获取验证码,由 AppAuthController
提供 admin-api/system/auth/send-sms-code
接口,获取验证码。代码如下:
| @PostMapping("/send-sms-code") |
| @Operation(summary = "发送手机验证码") |
| public CommonResult<Boolean> sendSmsCode(@RequestBody @Valid AppAuthSmsSendReqVO reqVO) { |
| authService.sendSmsCode(getLoginUserId(), reqVO); |
| return success(true); |
| } |
| |
| |
| public void sendSmsCode(Long userId, AppAuthSmsSendReqVO reqVO) { |
| |
| if (Objects.equals(reqVO.getScene(), SmsSceneEnum.MEMBER_UPDATE_MOBILE.getScene())) { |
| MemberUserDO user = userService.getUserByMobile(reqVO.getMobile()); |
| if (user != null && !Objects.equals(user.getId(), userId)) { |
| throw exception(AUTH_MOBILE_USED); |
| } |
| } |
| |
| if (Objects.equals(reqVO.getScene(), SmsSceneEnum.MEMBER_RESET_PASSWORD.getScene())) { |
| MemberUserDO user= userService.getUserByMobile(reqVO.getMobile()); |
| if (user == null) { |
| throw exception(USER_MOBILE_NOT_EXISTS); |
| } |
| } |
| |
| |
| smsCodeApi.sendSmsCode(AuthConvert.INSTANCE.convert(reqVO).setCreateIp(getClientIP())); |
| } |
- 使用手机号加
验证码
登陆,由 AppAuthController
提供 admin-api/system/auth/sms-login
接口登陆。代码如下:
| @PostMapping("/sms-login") |
| @Operation(summary = "使用手机 + 验证码登录") |
| public CommonResult<AppAuthLoginRespVO> smsLogin(@RequestBody @Valid AppAuthSmsLoginReqVO reqVO, |
| @RequestHeader Integer terminal) { |
| return success(authService.smsLogin(reqVO, terminal)); |
| } |
| |
| |
| |
| @Override |
| @Transactional |
| public AppAuthLoginRespVO smsLogin(AppAuthSmsLoginReqVO reqVO, Integer terminal) { |
| |
| String userIp = getClientIP(); |
| smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.MEMBER_LOGIN.getScene(), userIp)); |
| |
| |
| MemberUserDO user = userService.createUserIfAbsent(reqVO.getMobile(), userIp, terminal); |
| Assert.notNull(user, "获取用户失败,结果为空"); |
| |
| |
| String openid = null; |
| if (reqVO.getSocialType() != null) { |
| openid = socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(), |
| reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState())); |
| } |
| |
| |
| return createTokenAfterLoginSuccess(user, reqVO.getMobile(), LoginLogTypeEnum.LOGIN_SMS, openid); |
| } |
如果用户未注册,会自动使用手机号进行注册会员。所以, admin-api/system/auth/sms-login
也提供了用户的注册功能。
# 三方登陆
详细参考文章
# 管理后台的实现
- 跳转第三方平台,来获取第三方授权码, 由
AuthController
提供 admin-api/system/auth/social-auth-redirect
接口,代码如下:
| @GetMapping("/social-auth-redirect") |
| @PermitAll |
| @Operation(summary = "社交授权的跳转") |
| @Parameters({ |
| @Parameter(name = "type", description = "社交类型", required = true), |
| @Parameter(name = "redirectUri", description = "回调路径") |
| }) |
| public CommonResult<String> socialLogin(@RequestParam("type") Integer type, |
| @RequestParam("redirectUri") String redirectUri) { |
| return success(socialClientService.getAuthorizeUrl( |
| type, UserTypeEnum.ADMIN.getValue(), redirectUri)); |
| } |
| |
| |
| |
| @Override |
| public String getAuthorizeUrl(Integer socialType, Integer userType, String redirectUri) { |
| |
| AuthRequest authRequest = buildAuthRequest(socialType, userType); |
| |
| String authorizeUri = authRequest.authorize(AuthStateUtils.createState()); |
| return HttpUtils.replaceUrlQuery(authorizeUri, "redirect_uri", redirectUri); |
| } |
- 使用
code
第三方授权进行登陆,由 AuthController
提供 admin-api/system/auth/social-login
接口,代码如下:
| @PostMapping("/social-login") |
| @PermitAll |
| @Operation(summary = "社交快捷登录,使用 code 授权码", description = "适合未登录的用户,但是社交账号已绑定用户") |
| @OperateLog(enable = false) |
| public CommonResult<AuthLoginRespVO> socialQuickLogin(@RequestBody @Valid AuthSocialLoginReqVO reqVO) { |
| return success(authService.socialLogin(reqVO)); |
| } |
| |
| |
| @Override |
| public AuthLoginRespVO socialLogin(AuthSocialLoginReqVO reqVO) { |
| |
| SocialUserRespDTO socialUser = socialUserService.getSocialUser(UserTypeEnum.ADMIN.getValue(), reqVO.getType(), |
| reqVO.getCode(), reqVO.getState()); |
| if (socialUser == null) { |
| throw exception(AUTH_THIRD_LOGIN_NOT_BIND); |
| } |
| |
| |
| AdminUserDO user = userService.getUser(socialUser.getUserId()); |
| if (user == null) { |
| throw exception(USER_NOT_EXISTS); |
| } |
| |
| |
| return createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL); |
| } |
- 使用
social
三方授权码 + username
+ password
进行绑定登陆,直接使用 admin-api/system/auth/login
账号密码登陆的接口,区别在于额外添加了 socialType
+ socialCode
+ socialState
参数。
# 用户 App 的实现
- 跳转第三方平台,来获取第三方授权码, 由
AppAuthController
提供 app-api/member/auth/social-auth-redirect
接口,代码如下:
| @GetMapping("/social-auth-redirect") |
| @Operation(summary = "社交授权的跳转") |
| @Parameters({ |
| @Parameter(name = "type", description = "社交类型", required = true), |
| @Parameter(name = "redirectUri", description = "回调路径") |
| }) |
| public CommonResult<String> socialAuthRedirect(@RequestParam("type") Integer type, |
| @RequestParam("redirectUri") String redirectUri) { |
| return CommonResult.success(authService.getSocialAuthorizeUrl(type, redirectUri)); |
| } |
| |
| |
| @Override |
| public String getSocialAuthorizeUrl(Integer type, String redirectUri) { |
| return socialClientApi.getAuthorizeUrl(type, UserTypeEnum.MEMBER.getValue(), redirectUri); |
| } |
- 使用
code
第三方授权进行登陆,由 AppAuthController
提供 app-api/member/auth/social-login
接口,代码如下:
| @PostMapping("/social-login") |
| @Operation(summary = "社交快捷登录,使用 code 授权码", description = "适合未登录的用户,但是社交账号已绑定用户") |
| public CommonResult<AppAuthLoginRespVO> socialLogin(@RequestBody @Valid AppAuthSocialLoginReqVO reqVO) { |
| return success(authService.socialLogin(reqVO)); |
| } |
| |
| |
| @Override |
| public AppAuthLoginRespVO socialLogin(AppAuthSocialLoginReqVO reqVO) { |
| |
| SocialUserRespDTO socialUser = socialUserApi.getSocialUser(UserTypeEnum.MEMBER.getValue(), reqVO.getType(), |
| reqVO.getCode(), reqVO.getState()); |
| if (socialUser == null) { |
| throw exception(AUTH_THIRD_LOGIN_NOT_BIND); |
| } |
| |
| |
| MemberUserDO user = userService.getUser(socialUser.getUserId()); |
| if (user == null) { |
| throw exception(USER_NOT_EXISTS); |
| } |
| |
| |
| return createTokenAfterLoginSuccess(user, user.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL, socialUser.getOpenid()); |
| } |
- 使用
social
三方授权码 + username
+ password
进行绑定登陆,直接使用 admin-api/system/auth/login
账号密码登陆的接口,区别在于额外添加了 socialType
+ socialCode
+ socialState
参数。
# 注册
# 管理后台的实现
管理后台不支持用户在页面上注册,而是通过在 [系统管理 -> 用户管理] 菜单,进行添加新用户,由 UserController
提供 /admin-api/system/user/create
接口。代码如下:
| @PostMapping("/create") |
| @Operation(summary = "新增用户") |
| @PreAuthorize("@ss.hasPermission('system:user:create')") |
| public CommonResult<Long> createUser(@Valid @RequestBody UserCreateReqVO reqVO) { |
| Long id = userService.createUser(reqVO); |
| return success(id); |
| } |
| |
| |
| @Override |
| @Transactional(rollbackFor = Exception.class) |
| public Long createUser(UserCreateReqVO reqVO) { |
| |
| tenantService.handleTenantInfo(tenant -> { |
| long count = userMapper.selectCount(); |
| if (count >= tenant.getAccountCount()) { |
| throw exception(USER_COUNT_MAX, tenant.getAccountCount()); |
| } |
| }); |
| |
| validateUserForCreateOrUpdate(null, reqVO.getUsername(), reqVO.getMobile(), reqVO.getEmail(), |
| reqVO.getDeptId(), reqVO.getPostIds()); |
| |
| AdminUserDO user = UserConvert.INSTANCE.convert(reqVO); |
| |
| user.setStatus(CommonStatusEnum.ENABLE.getStatus()); |
| |
| user.setPassword(encodePassword(reqVO.getPassword())); |
| userMapper.insert(user); |
| |
| if (CollectionUtil.isNotEmpty(user.getPostIds())) { |
| userPostMapper.insertBatch(convertList(user.getPostIds(), |
| postId -> new UserPostDO().setUserId(user.getId()).setPostId(postId))); |
| } |
| return user.getId(); |
| } |
# 用户 App 实现
手机验证码登陆时候,如果用户未注册,会自动使用手机号进行注册会员。所以, admin-api/system/auth/sms-login
也提供了用户的注册功能。
# 用户登出
用户登出功能,统一使用 Spring Security 框架,通过删除用户 token 的方式来实现删除。代码如下:
| |
| @PostMapping("/logout") |
| @PermitAll |
| @Operation(summary = "登出系统") |
| @OperateLog(enable = false) |
| public CommonResult<Boolean> logout(HttpServletRequest request) { |
| String token = SecurityFrameworkUtils.obtainAuthorization(request, |
| securityProperties.getTokenHeader(), securityProperties.getTokenParameter()); |
| if (StrUtil.isNotBlank(token)) { |
| authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType()); |
| } |
| return success(true); |
| } |
| |
| @Override |
| public void logout(String token, Integer logType) { |
| |
| OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.removeAccessToken(token); |
| if (accessTokenDO == null) { |
| return; |
| } |
| |
| createLogoutLog(accessTokenDO.getUserId(), accessTokenDO.getUserType(), logType); |
| } |
| |
| |
| |
| |
| |
| |
| @PostMapping("/logout") |
| @PermitAll |
| @Operation(summary = "登出系统") |
| public CommonResult<Boolean> logout(HttpServletRequest request) { |
| String token = SecurityFrameworkUtils.obtainAuthorization(request, |
| securityProperties.getTokenHeader(), securityProperties.getTokenParameter()); |
| if (StrUtil.isNotBlank(token)) { |
| authService.logout(token); |
| } |
| return success(true); |
| } |
| |
| @Override |
| public void logout(String token) { |
| |
| OAuth2AccessTokenRespDTO accessTokenRespDTO = oauth2TokenApi.removeAccessToken(token); |
| if (accessTokenRespDTO == null) { |
| return; |
| } |
| |
| createLogoutLog(accessTokenRespDTO.getUserId()); |
| } |
# 扩展
- 是否允许用户多个设备登陆的配置
- 多台设备登陆的情况下最多允许多少台设备的配置