添加认证接口.

This commit is contained in:
lijiahang
2023-07-14 18:13:13 +08:00
parent 97b4a27033
commit 0ab9dc57ef
17 changed files with 536 additions and 66 deletions

View File

@@ -44,7 +44,7 @@ public class SystemUserDTO implements Serializable {
private String email;
@Schema(description = "用户状态 0正常 1停用 2锁定")
private Byte status;
private Integer status;
@Schema(description = "最后登录时间")
private Date lastLoginTime;

View File

@@ -0,0 +1,58 @@
package com.orion.ops.module.infra.controller;
import com.orion.lang.define.wrapper.HttpWrapper;
import com.orion.ops.framework.common.annotation.IgnoreLog;
import com.orion.ops.framework.common.annotation.RestWrapper;
import com.orion.ops.module.infra.entity.request.UserLoginRequest;
import com.orion.ops.module.infra.entity.vo.UserLoginVO;
import com.orion.ops.module.infra.service.AuthenticationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import javax.servlet.http.HttpServletRequest;
/**
* 认证服务
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/7/14 11:20
*/
@Tag(name = "infra - 认证服务")
@Slf4j
@Validated
@RestWrapper
@RestController
@RequestMapping("/infra/auth")
@SuppressWarnings({"ELValidationInJSP", "SpringElInspection"})
public class AuthenticationController {
@Resource
private AuthenticationService authenticationService;
@PermitAll
@Operation(summary = "登陆")
@PostMapping("/login")
public UserLoginVO login(@Validated @RequestBody UserLoginRequest request,
HttpServletRequest servletRequest) {
// 验证登陆
String token = authenticationService.login(request, servletRequest);
return UserLoginVO.builder().token(token).build();
}
@IgnoreLog
@PermitAll
@Operation(summary = "登出")
@GetMapping("/logout")
public HttpWrapper<?> logout(HttpServletRequest servletRequest) {
// 登出
authenticationService.logout(servletRequest);
return HttpWrapper.ok();
}
}

View File

@@ -1,12 +1,14 @@
package com.orion.ops.module.infra.convert;
import com.orion.ops.framework.common.security.LoginUser;
import com.orion.ops.module.infra.entity.domain.SystemUserDO;
import com.orion.ops.module.infra.entity.request.SystemUserCreateRequest;
import com.orion.ops.module.infra.entity.request.SystemUserQueryRequest;
import com.orion.ops.module.infra.entity.request.SystemUserUpdateRequest;
import com.orion.ops.module.infra.entity.vo.SystemUserVO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import com.orion.ops.module.infra.entity.domain.*;
import com.orion.ops.module.infra.entity.vo.*;
import com.orion.ops.module.infra.entity.dto.*;
import com.orion.ops.module.infra.entity.request.*;
import com.orion.ops.module.infra.convert.*;
import java.util.List;
/**
@@ -17,9 +19,8 @@ import java.util.List;
* @since 2023-7-13 18:42
*/
@Mapper
@SuppressWarnings("ALL")
public interface SystemUserConvert {
SystemUserConvert MAPPER = Mappers.getMapper(SystemUserConvert.class);
SystemUserDO to(SystemUserCreateRequest request);
@@ -28,8 +29,11 @@ public interface SystemUserConvert {
SystemUserDO to(SystemUserQueryRequest request);
SystemUserVO to(SystemUserDO request);
SystemUserVO to(SystemUserDO domain);
List<SystemUserVO> to(List<SystemUserDO> list);
LoginUser toLoginUser(SystemUserDO domain);
}

View File

@@ -17,14 +17,13 @@ import java.util.List;
* @since 2023-7-13 18:42
*/
@Mapper
@SuppressWarnings("ALL")
public interface SystemUserProviderConvert {
SystemUserProviderConvert MAPPER = Mappers.getMapper(SystemUserProviderConvert.class);
SystemUserDO to(SystemUserDTO dto);
SystemUserDTO to(SystemUserDO dto);
SystemUserDTO to(SystemUserDO domain);
List<SystemUserDO> toDO(List<SystemUserDTO> list);

View File

@@ -1,22 +0,0 @@
package com.orion.ops.module.infra.define;
import com.orion.lang.define.cache.CacheKeyDefine;
import java.util.concurrent.TimeUnit;
/**
* 基建模块缓存 key
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/7/13 21:54
*/
public interface InfraCacheKeyDefine {
CacheKeyDefine USER_INFO = new CacheKeyDefine("user:info:{}", "用户信息", 30, TimeUnit.DAYS);
CacheKeyDefine USER_TOKEN = new CacheKeyDefine("user:token:{}", "用户认证 authenticationToken", 48, TimeUnit.HOURS);
CacheKeyDefine USER_REFRESH = new CacheKeyDefine("user:refresh:{}", "用户认证 refreshToken", 54, TimeUnit.HOURS);
}

View File

@@ -0,0 +1,24 @@
package com.orion.ops.module.infra.define;
import com.orion.lang.define.cache.CacheKeyDefine;
import java.util.concurrent.TimeUnit;
/**
* 用户模块缓存 key
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/7/13 21:54
*/
public interface UserCacheKeyDefine {
CacheKeyDefine USER_INFO = new CacheKeyDefine("user:info:{}", "用户信息 ${id}", 30, TimeUnit.DAYS);
CacheKeyDefine LOGIN_FAILED_COUNT = new CacheKeyDefine("user:failed:{}", "用户登陆失败次数 ${username}", 3, TimeUnit.DAYS);
CacheKeyDefine LOGIN_TOKEN = new CacheKeyDefine("user:token:{}:{}", "用户登陆 token ${id} ${time}", 24, TimeUnit.HOURS);
CacheKeyDefine LOGIN_REFRESH = new CacheKeyDefine("user:refresh:{}:{}", "用户刷新 token ${id} ${time}", 28, TimeUnit.HOURS);
}

View File

@@ -55,7 +55,7 @@ public class SystemUserDO extends BaseDO {
@Schema(description = "用户状态 0正常 1停用 2锁定")
@TableField("status")
private Byte status;
private Integer status;
@Schema(description = "最后登录时间")
@TableField("last_login_time")

View File

@@ -0,0 +1,41 @@
package com.orion.ops.module.infra.entity.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 登陆 token 缓存
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/7/14 16:11
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class LoginTokenDTO {
/**
* 状态
*/
private Integer status;
/**
* 登陆时间
*/
private Long loginTime;
/**
* 登陆 ip
*/
private String ip;
/**
* 登陆地址
*/
private String location;
}

View File

@@ -52,7 +52,7 @@ public class SystemUserCreateRequest implements Serializable {
@NotNull
@Schema(description = "用户状态 0正常 1停用 2锁定")
private Byte status;
private Integer status;
@NotNull
@Schema(description = "最后登录时间")

View File

@@ -43,7 +43,7 @@ public class SystemUserQueryRequest extends PageRequest {
private String email;
@Schema(description = "用户状态 0正常 1停用 2锁定")
private Byte status;
private Integer status;
@Schema(description = "最后登录时间")
private Date lastLoginTime;

View File

@@ -56,7 +56,7 @@ public class SystemUserUpdateRequest implements Serializable {
@NotNull
@Schema(description = "用户状态 0正常 1停用 2锁定")
private Byte status;
private Integer status;
@NotNull
@Schema(description = "最后登录时间")

View File

@@ -44,7 +44,7 @@ public class SystemUserVO implements Serializable {
private String email;
@Schema(description = "用户状态 0正常 1停用 2锁定")
private Byte status;
private Integer status;
@Schema(description = "最后登录时间")
private Date lastLoginTime;

View File

@@ -0,0 +1,26 @@
package com.orion.ops.module.infra.entity.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 用户登陆响应
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/7/14 11:23
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "SystemUserVO", description = "用户 视图响应对象")
public class UserLoginVO {
@Schema(description = "登陆 token")
private String token;
}

View File

@@ -0,0 +1,40 @@
package com.orion.ops.module.infra.enums;
import lombok.Getter;
/**
* 登陆 token 状态
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/7/14 16:15
*/
@Getter
public enum LoginTokenStatusEnum {
/**
* 正常
*/
OK(0),
/**
* 已在其他设备登陆
*/
OTHER_DEVICE(1, "已在其他设备登陆"),
;
LoginTokenStatusEnum(Integer status) {
this(status, null);
}
LoginTokenStatusEnum(Integer status, String message) {
this.status = status;
this.message = message;
}
private final Integer status;
private final String message;
}

View File

@@ -0,0 +1,36 @@
package com.orion.ops.module.infra.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 用户状态枚举
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/7/14 11:35
*/
@Getter
@AllArgsConstructor
public enum UserStatusEnum {
/**
* 0 正常
*/
NORMAL(0),
/**
* 1 停用
*/
DISABLED(1),
/**
* 2 锁定
*/
LOCKED(2),
;
private final Integer status;
}

View File

@@ -1,7 +1,10 @@
package com.orion.ops.module.infra.service;
import com.orion.lang.define.wrapper.Pair;
import com.orion.ops.module.infra.entity.request.UserLoginRequest;
import javax.servlet.http.HttpServletRequest;
/**
* 认证服务
*
@@ -14,16 +17,25 @@ public interface AuthenticationService {
/**
* 登陆
*
* @param request request
* @param request request
* @param servletRequest servletRequest
* @return token
*/
String login(UserLoginRequest request);
String login(UserLoginRequest request, HttpServletRequest servletRequest);
/**
* 登出
*
* @param token token
* @param servletRequest servletRequest
*/
void logout(String token);
void logout(HttpServletRequest servletRequest);
/**
* 获取 token pair
*
* @param loginToken loginToken
* @return pair
*/
Pair<Long, Long> getLoginTokenPair(String loginToken);
}

View File

@@ -1,18 +1,41 @@
package com.orion.ops.module.infra.service.impl;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.orion.lang.constant.StandardHttpHeader;
import com.orion.lang.define.wrapper.Pair;
import com.orion.lang.utils.Exceptions;
import com.orion.lang.utils.Valid;
import com.orion.lang.utils.collect.Lists;
import com.orion.lang.utils.crypto.Signatures;
import com.orion.ops.framework.common.constant.Const;
import com.orion.ops.framework.common.constant.ErrorMessage;
import com.orion.ops.framework.common.crypto.ValueCrypto;
import com.orion.ops.framework.common.security.LoginUser;
import com.orion.ops.framework.common.utils.CryptoUtils;
import com.orion.ops.framework.common.utils.IpUtils;
import com.orion.ops.framework.common.utils.Kits;
import com.orion.ops.framework.redis.core.utils.RedisUtils;
import com.orion.ops.module.infra.convert.SystemUserConvert;
import com.orion.ops.module.infra.dao.SystemUserDAO;
import com.orion.ops.module.infra.define.UserCacheKeyDefine;
import com.orion.ops.module.infra.entity.domain.SystemUserDO;
import com.orion.ops.module.infra.entity.dto.LoginTokenDTO;
import com.orion.ops.module.infra.entity.request.UserLoginRequest;
import com.orion.ops.module.infra.enums.LoginTokenStatusEnum;
import com.orion.ops.module.infra.enums.UserStatusEnum;
import com.orion.ops.module.infra.service.AuthenticationService;
import com.orion.web.servlet.web.Servlets;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 认证服务实现
@@ -24,10 +47,18 @@ import java.util.Optional;
@Service
public class AuthenticationServiceImpl implements AuthenticationService {
/**
* 允许多端登陆
*/
private final boolean allowMultiPlatform = false;
// TODO 想想看 如何配置化
// 允许多端登陆
private final boolean allowMultiDevice = true;
// 允许凭证续签
private final boolean allowRefresh = true;
// 凭证续签最大次数
private final int maxRefreshCount = 5;
// 失败锁定次数
private final int maxFailedLoginCount = 5;
@Resource
private ValueCrypto valueCrypto;
@Resource
private SystemUserDAO systemUserDAO;
@@ -36,32 +67,253 @@ public class AuthenticationServiceImpl implements AuthenticationService {
private RedisTemplate<String, String> redisTemplate;
@Override
public String login(UserLoginRequest request) {
// 检查登陆
LambdaQueryWrapper<SystemUserDO> wrapper = systemUserDAO.wrapper()
.eq(SystemUserDO::getUsername, request.getUsername())
.eq(SystemUserDO::getPassword, Signatures.md5(request.getPassword()));
public String login(UserLoginRequest request, HttpServletRequest servletRequest) {
// 登陆前检查
this.preCheckLogin(request);
// 获取登陆用户
Optional<SystemUserDO> systemUserDO = systemUserDAO.of(wrapper).only().get();
Valid.isTrue(systemUserDO.isPresent(), ErrorMessage.USERNAME_PASSWORD_ERROR);
LambdaQueryWrapper<SystemUserDO> wrapper = systemUserDAO.wrapper()
.eq(SystemUserDO::getUsername, request.getUsername());
SystemUserDO user = systemUserDAO.of(wrapper).only().get();
// 检查密码
boolean passwordCorrect = this.checkPassword(request, user);
Valid.isTrue(passwordCorrect, ErrorMessage.USERNAME_PASSWORD_ERROR);
// 检查用户状态
this.checkUserStatus(user.getStatus());
// 设置上次登录时间
this.setLastLoginTime(user.getId());
// 设置缓存
// 不允许多端登陆删除缓存
// 生成 authenticationToken
// 生成 refreshToken
//
return null;
this.setUserCache(user);
// 删除登陆失败次数缓存
redisTemplate.delete(UserCacheKeyDefine.LOGIN_FAILED_COUNT.format(request.getUsername()));
// 获取登陆 ip
String remoteAddr = Servlets.getRemoteAddr(servletRequest);
String location = IpUtils.getLocation(remoteAddr);
long current = System.currentTimeMillis();
// 不允许多端登陆
if (!false) {
// 无效化其他缓存
this.invalidOtherDeviceToken(user.getId(), current, remoteAddr, location);
}
// 生成 loginToken
return this.generatorLoginToken(user, current, remoteAddr, location);
}
@Override
public void logout(String token) {
public void logout(HttpServletRequest request) {
// 获取登陆 token
String loginToken = Kits.getAuthorization(request.getHeader(StandardHttpHeader.AUTHORIZATION));
if (loginToken == null) {
return;
}
Pair<Long, Long> pair = this.getLoginTokenPair(loginToken);
if (pair == null) {
return;
}
Long id = pair.getKey();
Long current = pair.getValue();
// 删除 loginToken & refreshToken
String loginKey = UserCacheKeyDefine.LOGIN_TOKEN.format(id, current);
String refreshKey = UserCacheKeyDefine.LOGIN_REFRESH.format(id, current);
redisTemplate.delete(Lists.of(loginKey, refreshKey));
}
@Override
public Pair<Long, Long> getLoginTokenPair(String loginToken) {
if (loginToken == null) {
return null;
}
try {
String value = CryptoUtils.decryptBase62(loginToken);
String[] pair = value.split(":");
return Pair.of(Long.valueOf(pair[0]), Long.valueOf(pair[1]));
} catch (Exception e) {
return null;
}
}
/**
* 登陆预检查
*
* @param request request
*/
private void preCheckLogin(UserLoginRequest request) {
// 检查密码长度是否正确 MD5 长度为 32
if (request.getPassword().length() != Const.MD5_LEN) {
throw Exceptions.argument(ErrorMessage.USERNAME_PASSWORD_ERROR);
}
// 检查登陆失败次数
String failedCountKey = UserCacheKeyDefine.LOGIN_FAILED_COUNT.format(request.getUsername());
String failedCount = redisTemplate.opsForValue().get(failedCountKey);
if (failedCount != null && Integer.parseInt(failedCount) >= maxFailedLoginCount) {
throw Exceptions.argument(ErrorMessage.MAX_LOGIN_FAILED);
}
}
/**
* 检查密码
*
* @param request request
* @param user user
* @return 是否正确
*/
private boolean checkPassword(UserLoginRequest request, SystemUserDO user) {
// 密码正确
if (user != null && user.getPassword().equals(Signatures.md5(request.getPassword()))) {
return true;
}
// 刷新登陆失败缓存
String failedCountKey = UserCacheKeyDefine.LOGIN_FAILED_COUNT.format(user.getUsername());
Long failedLoginCount = redisTemplate.opsForValue().increment(failedCountKey);
// 用户不存在
if (user == null) {
return false;
}
// 锁定用户
if (failedLoginCount >= maxFailedLoginCount) {
// 更新用户表
SystemUserDO updateUser = new SystemUserDO();
updateUser.setId(user.getId());
updateUser.setStatus(UserStatusEnum.LOCKED.getStatus());
systemUserDAO.updateById(updateUser);
// 更新缓存
String userInfoKey = UserCacheKeyDefine.USER_INFO.format(user.getId());
String userInfoCache = redisTemplate.opsForValue().get(userInfoKey);
// 缓存不存在
if (userInfoCache == null) {
return false;
}
// 修改缓存状态
LoginUser loginUser = JSON.parseObject(userInfoCache, LoginUser.class);
loginUser.setStatus(UserStatusEnum.LOCKED.getStatus());
redisTemplate.opsForValue().set(userInfoKey, JSON.toJSONString(loginUser),
UserCacheKeyDefine.USER_INFO.getTimeout(),
UserCacheKeyDefine.USER_INFO.getUnit());
}
return false;
}
/**
* 检查用户状态
*
* @param status status
*/
private void checkUserStatus(Integer status) {
if (UserStatusEnum.DISABLED.getStatus().equals(status)) {
// 禁用状态
throw Exceptions.argument(ErrorMessage.USER_DISABLED);
} else if (UserStatusEnum.LOCKED.getStatus().equals(status)) {
// 锁定状态
throw Exceptions.argument(ErrorMessage.USER_LOCKED);
}
}
/**
* 设置最后登录时间
*
* @param id id
*/
private void setLastLoginTime(Long id) {
SystemUserDO update = new SystemUserDO();
update.setId(id);
update.setLastLoginTime(new Date());
systemUserDAO.updateById(update);
}
/**
* 设置用户缓存
*
* @param user user
*/
private void setUserCache(SystemUserDO user) {
String userInfoKey = UserCacheKeyDefine.USER_INFO.format(user.getId());
String userInfoCache = redisTemplate.opsForValue().get(userInfoKey);
// 缓存存在
if (userInfoCache != null) {
return;
}
// 设置缓存
LoginUser loginUser = SystemUserConvert.MAPPER.toLoginUser(user);
// TODO 查询角色
redisTemplate.opsForValue().set(userInfoKey, JSON.toJSONString(loginUser),
UserCacheKeyDefine.USER_INFO.getTimeout(),
UserCacheKeyDefine.USER_INFO.getUnit());
}
/**
* 无效化其他登陆信息
*
* @param id id
* @param loginTime loginTime
* @param remoteAddr remoteAddr
* @param location location
*/
private void invalidOtherDeviceToken(Long id, long loginTime, String remoteAddr, String location) {
String loginKey = UserCacheKeyDefine.LOGIN_TOKEN.format(id, "*");
// 获取登陆信息
Set<String> loginKeyList = RedisUtils.scanKeys(redisTemplate, loginKey, 100);
if (!loginKeyList.isEmpty()) {
// 获取有效登陆信息
List<LoginTokenDTO> loginTokenInfoList = redisTemplate.opsForValue()
.multiGet(loginKeyList)
.stream()
.filter(Objects::nonNull)
.map(s -> JSON.parseObject(s, LoginTokenDTO.class))
.filter(s -> LoginTokenStatusEnum.OK.getStatus().equals(s.getStatus()))
.collect(Collectors.toList());
// 修改登陆信息
for (LoginTokenDTO loginTokenInfo : loginTokenInfoList) {
String deviceLoginKey = UserCacheKeyDefine.LOGIN_TOKEN.format(id, loginTokenInfo.getLoginTime());
loginTokenInfo.setStatus(LoginTokenStatusEnum.OTHER_DEVICE.getStatus());
loginTokenInfo.setLoginTime(loginTime);
loginTokenInfo.setIp(remoteAddr);
loginTokenInfo.setLocation(location);
redisTemplate.opsForValue().set(deviceLoginKey, JSON.toJSONString(loginTokenInfo),
UserCacheKeyDefine.LOGIN_TOKEN.getTimeout(),
UserCacheKeyDefine.LOGIN_TOKEN.getUnit());
}
}
// 删除续签信息
if (allowRefresh) {
String refreshKey = UserCacheKeyDefine.LOGIN_REFRESH.format(id, "*");
Set<String> refreshKeyList = RedisUtils.scanKeys(redisTemplate, refreshKey, 100);
if (!refreshKeyList.isEmpty()) {
redisTemplate.delete(refreshKeyList);
}
}
}
/**
* 生成 loginToken
*
* @param user user
* @param loginTime loginTime
* @param remoteAddr remoteAddr
* @param location location
* @return loginToken
*/
private String generatorLoginToken(SystemUserDO user, long loginTime,
String remoteAddr, String location) {
Long id = user.getId();
// 生成 loginToken
String loginKey = UserCacheKeyDefine.LOGIN_TOKEN.format(id, loginTime);
LoginTokenDTO loginValue = LoginTokenDTO.builder()
.status(LoginTokenStatusEnum.OK.getStatus())
.ip(remoteAddr)
.loginTime(loginTime)
.location(location)
.build();
redisTemplate.opsForValue().set(loginKey, JSON.toJSONString(loginValue),
UserCacheKeyDefine.LOGIN_TOKEN.getTimeout(),
UserCacheKeyDefine.LOGIN_TOKEN.getUnit());
// 生成 refreshToken
if (allowRefresh) {
String refreshKey = UserCacheKeyDefine.LOGIN_REFRESH.format(id, loginTime);
redisTemplate.opsForValue().set(refreshKey, "1",
UserCacheKeyDefine.LOGIN_REFRESH.getTimeout(),
UserCacheKeyDefine.LOGIN_REFRESH.getUnit());
}
// 返回token
return CryptoUtils.encryptBase62(id + ":" + loginTime);
}
}