1. 修改在线会话的实现

2. 接入到会员管理 OAuth2.0
This commit is contained in:
YunaiV
2022-05-10 23:20:15 +08:00
parent 6ed624861d
commit 5cf68961e1
29 changed files with 368 additions and 162 deletions

View File

@ -19,7 +19,7 @@ tenant-id: {{appTenentId}}
}
### 请求 /sms-login 接口 => 成功
POST {{appApi}}/member/sms-login
POST {{appApi}}/member/auth/sms-login
Content-Type: application/json
tenant-id: {{appTenentId}}
@ -29,7 +29,12 @@ tenant-id: {{appTenentId}}
}
### 请求 /logout 接口 => 成功
POST {{appApi}}/member/logout
POST {{appApi}}/member/auth/logout
Content-Type: application/json
Authorization: Bearer c1b76bdaf2c146c581caa4d7fd81ee66
tenant-id: {{appTenentId}}
### 请求 /auth/refresh-token 接口 => 成功
POST {{appApi}}/member/auth/refresh-token?refreshToken=bc43d929094849a28b3a69f6e6940d70
Content-Type: application/json
tenant-id: {{appTenentId}}

View File

@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.member.controller.app.auth;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
import cn.iocoder.yudao.framework.security.config.SecurityProperties;
import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
@ -20,8 +21,6 @@ import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getUserAgent;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Api(tags = "用户 APP - 认证")
@ -40,8 +39,7 @@ public class AppAuthController {
@PostMapping("/login")
@ApiOperation("使用手机 + 密码登录")
public CommonResult<AppAuthLoginRespVO> login(@RequestBody @Valid AppAuthLoginReqVO reqVO) {
String token = authService.login(reqVO, getClientIP(), getUserAgent());
return success(AppAuthLoginRespVO.builder().token(token).build());
return success(authService.login(reqVO));
}
@PostMapping("/logout")
@ -54,12 +52,20 @@ public class AppAuthController {
return success(true);
}
@PostMapping("/refresh-token")
@ApiOperation("刷新令牌")
@ApiImplicitParam(name = "refreshToken", value = "刷新令牌", required = true, dataTypeClass = String.class)
@OperateLog(enable = false) // 避免 Post 请求被记录操作日志
public CommonResult<AppAuthLoginRespVO> refreshToken(@RequestParam("refreshToken") String refreshToken) {
return success(authService.refreshToken(refreshToken));
}
// ========== 短信登录相关 ==========
@PostMapping("/sms-login")
@ApiOperation("使用手机 + 验证码登录")
public CommonResult<AppAuthLoginRespVO> smsLogin(@RequestBody @Valid AppAuthSmsLoginReqVO reqVO) {
String token = authService.smsLogin(reqVO, getClientIP(), getUserAgent());
// 返回结果
return success(AppAuthLoginRespVO.builder().token(token).build());
return success(authService.smsLogin(reqVO));
}
@PostMapping("/send-sms-code")
@ -100,16 +106,14 @@ public class AppAuthController {
@PostMapping("/social-quick-login")
@ApiOperation(value = "社交快捷登录,使用 code 授权码", notes = "适合未登录的用户,但是社交账号已绑定用户")
public CommonResult<AppAuthLoginRespVO> socialLogin(@RequestBody @Valid AppAuthSocialQuickLoginReqVO reqVO) {
String token = authService.socialQuickLogin(reqVO, getClientIP(), getUserAgent());
return success(AppAuthLoginRespVO.builder().token(token).build());
public CommonResult<AppAuthLoginRespVO> socialQuickLogin(@RequestBody @Valid AppAuthSocialQuickLoginReqVO reqVO) {
return success(authService.socialQuickLogin(reqVO));
}
@PostMapping("/social-bind-login")
@ApiOperation(value = "社交绑定登录,使用 手机号 + 手机验证码", notes = "适合未登录的用户,进行登录 + 绑定")
public CommonResult<AppAuthLoginRespVO> socialLogin2(@RequestBody @Valid AppAuthSocialBindLoginReqVO reqVO) {
String token = authService.socialBindLogin(reqVO, getClientIP(), getUserAgent());
return success(AppAuthLoginRespVO.builder().token(token).build());
public CommonResult<AppAuthLoginRespVO> socialBindLogin(@RequestBody @Valid AppAuthSocialBindLoginReqVO reqVO) {
return success(authService.socialBindLogin(reqVO));
}
}

View File

@ -7,14 +7,25 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@ApiModel("用户 APP - 手机密码登录 Response VO")
import java.util.Date;
@ApiModel("用户 APP - 登录 Response VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppAuthLoginRespVO {
@ApiModelProperty(value = "token", required = true, example = "yudaoyuanma")
private String token;
@ApiModelProperty(value = "用户编号", required = true, example = "1024")
private Long userId;
@ApiModelProperty(value = "访问令牌", required = true, example = "happy")
private String accessToken;
@ApiModelProperty(value = "刷新令牌", required = true, example = "nice")
private String refreshToken;
@ApiModelProperty(value = "过期时间", required = true)
private Date expiresTime;
}

View File

@ -1,9 +1,8 @@
package cn.iocoder.yudao.module.member.convert.auth;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.module.member.controller.app.auth.vo.*;
import cn.iocoder.yudao.module.member.controller.app.social.vo.AppSocialUserUnbindReqVO;
import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenRespDTO;
import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO;
@ -17,8 +16,6 @@ public interface AuthConvert {
AuthConvert INSTANCE = Mappers.getMapper(AuthConvert.class);
LoginUser convert(MemberUserDO bean);
SocialUserBindReqDTO convert(Long userId, Integer userType, AppAuthSocialBindLoginReqVO reqVO);
SocialUserBindReqDTO convert(Long userId, Integer userType, AppAuthSocialQuickLoginReqVO reqVO);
SocialUserUnbindReqDTO convert(Long userId, Integer userType, AppSocialUserUnbindReqVO reqVO);
@ -27,4 +24,6 @@ public interface AuthConvert {
SmsCodeUseReqDTO convert(AppAuthResetPasswordReqVO reqVO, SmsSceneEnum scene, String usedIp);
SmsCodeUseReqDTO convert(AppAuthSmsLoginReqVO reqVO, Integer scene, String usedIp);
AppAuthLoginRespVO convert(OAuth2AccessTokenRespDTO bean);
}

View File

@ -17,11 +17,9 @@ public interface MemberAuthService {
* 手机 + 密码登录
*
* @param reqVO 登录信息
* @param userIp 用户 IP
* @param userAgent 用户 UA
* @return 身份令牌,使用 JWT 方式
* @return 登录结果
*/
String login(@Valid AppAuthLoginReqVO reqVO, String userIp, String userAgent);
AppAuthLoginRespVO login(@Valid AppAuthLoginReqVO reqVO);
/**
* 基于 token 退出登录
@ -34,31 +32,25 @@ public interface MemberAuthService {
* 手机 + 验证码登陆
*
* @param reqVO 登陆信息
* @param userIp 用户 IP
* @param userAgent 用户 UA
* @return 身份令牌,使用 JWT 方式
* @return 登录结果
*/
String smsLogin(@Valid AppAuthSmsLoginReqVO reqVO, String userIp, String userAgent);
AppAuthLoginRespVO smsLogin(@Valid AppAuthSmsLoginReqVO reqVO);
/**
* 社交登录,使用 code 授权码
*
* @param reqVO 登录信息
* @param userIp 用户 IP
* @param userAgent 用户 UA
* @return 身份令牌,使用 JWT 方式
* @return 登录结果
*/
String socialQuickLogin(@Valid AppAuthSocialQuickLoginReqVO reqVO, String userIp, String userAgent);
AppAuthLoginRespVO socialQuickLogin(@Valid AppAuthSocialQuickLoginReqVO reqVO);
/**
* 社交登录,使用 手机号 + 手机验证码
*
* @param reqVO 登录信息
* @param userIp 用户 IP
* @param userAgent 用户 UA
* @return 身份令牌,使用 JWT 方式
* @return 登录结果
*/
String socialBindLogin(@Valid AppAuthSocialBindLoginReqVO reqVO, String userIp, String userAgent);
AppAuthLoginRespVO socialBindLogin(@Valid AppAuthSocialBindLoginReqVO reqVO);
/**
* 获得社交认证 URL
@ -90,4 +82,11 @@ public interface MemberAuthService {
*/
void sendSmsCode(Long userId, AppAuthSmsSendReqVO reqVO);
/**
* 刷新访问令牌
*
* @param refreshToken 刷新令牌
* @return 登录结果
*/
AppAuthLoginRespVO refreshToken(String refreshToken);
}

View File

@ -6,17 +6,19 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.module.member.controller.app.auth.vo.*;
import cn.iocoder.yudao.module.member.convert.auth.AuthConvert;
import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
import cn.iocoder.yudao.module.member.dal.mysql.user.MemberUserMapper;
import cn.iocoder.yudao.module.member.service.user.MemberUserService;
import cn.iocoder.yudao.module.system.api.auth.UserSessionApi;
import cn.iocoder.yudao.module.system.api.auth.OAuth2TokenApi;
import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenCreateReqDTO;
import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenRespDTO;
import cn.iocoder.yudao.module.system.api.logger.LoginLogApi;
import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO;
import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
import cn.iocoder.yudao.module.system.api.social.SocialUserApi;
import cn.iocoder.yudao.module.system.enums.auth.OAuth2ClientIdEnum;
import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum;
import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum;
import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
@ -49,9 +51,9 @@ public class MemberAuthServiceImpl implements MemberAuthService {
@Resource
private LoginLogApi loginLogApi;
@Resource
private UserSessionApi userSessionApi;
@Resource
private SocialUserApi socialUserApi;
@Resource
private OAuth2TokenApi oauth2TokenApi;
@Resource
private PasswordEncoder passwordEncoder;
@ -59,35 +61,31 @@ public class MemberAuthServiceImpl implements MemberAuthService {
private MemberUserMapper userMapper;
@Override
public String login(AppAuthLoginReqVO reqVO, String userIp, String userAgent) {
public AppAuthLoginRespVO login(AppAuthLoginReqVO reqVO) {
// 使用手机 + 密码,进行登录。
LoginUser loginUser = login0(reqVO.getMobile(), reqVO.getPassword());
MemberUserDO user = login0(reqVO.getMobile(), reqVO.getPassword());
// 缓存登录用户到 Redis 中,返回 Token 令牌
return createUserSessionAfterLoginSuccess(loginUser, reqVO.getMobile(),
LoginLogTypeEnum.LOGIN_USERNAME, userIp, userAgent);
// 创建 Token 令牌,记录登录日志
return createTokenAfterLoginSuccess(user, reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE);
}
@Override
@Transactional
public String smsLogin(AppAuthSmsLoginReqVO reqVO, String userIp, String userAgent) {
public AppAuthLoginRespVO smsLogin(AppAuthSmsLoginReqVO reqVO) {
// 校验验证码
String userIp = getClientIP();
smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.MEMBER_LOGIN.getScene(), userIp));
// 获得获得注册用户
MemberUserDO user = userService.createUserIfAbsent(reqVO.getMobile(), userIp);
Assert.notNull(user, "获取用户失败,结果为空");
// 执行登陆
LoginUser loginUser = buildLoginUser(user);
// 缓存登录用户到 Redis 中,返回 Token 令牌
return createUserSessionAfterLoginSuccess(loginUser, reqVO.getMobile(),
LoginLogTypeEnum.LOGIN_SMS, userIp, userAgent);
// 创建 Token 令牌,记录登录日志
return createTokenAfterLoginSuccess(user, reqVO.getMobile(), LoginLogTypeEnum.LOGIN_SMS);
}
@Override
public String socialQuickLogin(AppAuthSocialQuickLoginReqVO reqVO, String userIp, String userAgent) {
public AppAuthLoginRespVO socialQuickLogin(AppAuthSocialQuickLoginReqVO reqVO) {
// 使用 code 授权码,进行登录。然后,获得到绑定的用户编号
Long userId = socialUserApi.getBindUserId(UserTypeEnum.MEMBER.getValue(), reqVO.getType(),
reqVO.getCode(), reqVO.getState());
@ -101,33 +99,30 @@ public class MemberAuthServiceImpl implements MemberAuthService {
throw exception(USER_NOT_EXISTS);
}
// 创建 LoginUser 对象
LoginUser loginUser = buildLoginUser(user);
// 缓存登录用户到 Redis 中,返回 Token 令牌
return createUserSessionAfterLoginSuccess(loginUser, null,
LoginLogTypeEnum.LOGIN_SOCIAL, userIp, userAgent);
// 创建 Token 令牌,记录登录日志
return createTokenAfterLoginSuccess(user, null, LoginLogTypeEnum.LOGIN_SOCIAL);
}
@Override
public String socialBindLogin(AppAuthSocialBindLoginReqVO reqVO, String userIp, String userAgent) {
public AppAuthLoginRespVO socialBindLogin(AppAuthSocialBindLoginReqVO reqVO) {
// 使用手机号、手机验证码登录
AppAuthSmsLoginReqVO loginReqVO = AppAuthSmsLoginReqVO.builder()
.mobile(reqVO.getMobile()).code(reqVO.getSmsCode()).build();
String token = this.smsLogin(loginReqVO, userIp, userAgent);
LoginUser loginUser = userSessionApi.getLoginUser(token);
AppAuthLoginRespVO token = smsLogin(loginReqVO);
// 绑定社交用户
socialUserApi.bindSocialUser(AuthConvert.INSTANCE.convert(loginUser.getId(), getUserType().getValue(), reqVO));
socialUserApi.bindSocialUser(AuthConvert.INSTANCE.convert(token.getUserId(), getUserType().getValue(), reqVO));
return token;
}
private String createUserSessionAfterLoginSuccess(LoginUser loginUser, String mobile,
LoginLogTypeEnum logType, String userIp, String userAgent) {
private AppAuthLoginRespVO createTokenAfterLoginSuccess(MemberUserDO user, String mobile, LoginLogTypeEnum logType) {
// 插入登陆日志
createLoginLog(loginUser.getId(), mobile, logType, LoginResultEnum.SUCCESS);
// 缓存登录用户到 Redis 中,返回 Token 令牌
return userSessionApi.createUserSession(loginUser, userIp, userAgent);
createLoginLog(user.getId(), mobile, logType, LoginResultEnum.SUCCESS);
// 创建 Token 令牌
OAuth2AccessTokenRespDTO accessTokenRespDTO = oauth2TokenApi.createAccessToken(new OAuth2AccessTokenCreateReqDTO()
.setUserId(user.getId()).setUserType(getUserType().getValue()).setClientId(OAuth2ClientIdEnum.DEFAULT.getId()));
// 构建返回结果
return AuthConvert.INSTANCE.convert(accessTokenRespDTO);
}
@Override
@ -135,7 +130,7 @@ public class MemberAuthServiceImpl implements MemberAuthService {
return socialUserApi.getAuthorizeUrl(type, redirectUri);
}
private LoginUser login0(String mobile, String password) {
private MemberUserDO login0(String mobile, String password) {
final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_MOBILE;
// 校验账号是否存在
MemberUserDO user = userService.getUserByMobile(mobile);
@ -152,9 +147,7 @@ public class MemberAuthServiceImpl implements MemberAuthService {
createLoginLog(user.getId(), mobile, logTypeEnum, LoginResultEnum.USER_DISABLED);
throw exception(AUTH_LOGIN_USER_DISABLED);
}
// 构建 User 对象
return buildLoginUser(user);
return user;
}
private void createLoginLog(Long userId, String mobile, LoginLogTypeEnum logType, LoginResultEnum loginResult) {
@ -177,15 +170,13 @@ public class MemberAuthServiceImpl implements MemberAuthService {
@Override
public void logout(String token) {
// 查询用户信息
LoginUser loginUser = userSessionApi.getLoginUser(token);
if (loginUser == null) {
// 删除访问令牌
OAuth2AccessTokenRespDTO accessTokenRespDTO = oauth2TokenApi.removeAccessToken(token);
if (accessTokenRespDTO == null) {
return;
}
// 删除 session
userSessionApi.deleteUserSession(token);
// 记录登出日志
createLogoutLog(loginUser.getId());
// 删除成功,则记录登出日志
createLogoutLog(accessTokenRespDTO.getUserId());
}
@Override
@ -219,6 +210,12 @@ public class MemberAuthServiceImpl implements MemberAuthService {
smsCodeApi.sendSmsCode(AuthConvert.INSTANCE.convert(reqVO).setCreateIp(getClientIP()));
}
@Override
public AppAuthLoginRespVO refreshToken(String refreshToken) {
OAuth2AccessTokenRespDTO accessTokenDO = oauth2TokenApi.refreshAccessToken(refreshToken, OAuth2ClientIdEnum.DEFAULT.getId());
return AuthConvert.INSTANCE.convert(accessTokenDO);
}
/**
* 校验旧密码
*
@ -260,10 +257,6 @@ public class MemberAuthServiceImpl implements MemberAuthService {
loginLogApi.createLoginLog(reqDTO);
}
private LoginUser buildLoginUser(MemberUserDO user) {
return AuthConvert.INSTANCE.convert(user).setUserType(getUserType().getValue());
}
private String getMobile(Long userId) {
if (userId == null) {
return null;

View File

@ -9,7 +9,7 @@ import cn.iocoder.yudao.module.member.controller.app.auth.vo.AppAuthUpdatePasswo
import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
import cn.iocoder.yudao.module.member.dal.mysql.user.MemberUserMapper;
import cn.iocoder.yudao.module.member.service.user.MemberUserService;
import cn.iocoder.yudao.module.system.api.auth.UserSessionApi;
import cn.iocoder.yudao.module.system.api.auth.OAuth2TokenApi;
import cn.iocoder.yudao.module.system.api.logger.LoginLogApi;
import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
import cn.iocoder.yudao.module.system.api.social.SocialUserApi;
@ -46,7 +46,7 @@ public class MemberAuthServiceTest extends BaseDbAndRedisUnitTest {
@MockBean
private LoginLogApi loginLogApi;
@MockBean
private UserSessionApi userSessionApi;
private OAuth2TokenApi oauth2TokenApi;
@MockBean
private SocialUserApi socialUserApi;
@MockBean