Commit 3ab6e756 authored by shengnan hu's avatar shengnan hu
Browse files

init

parents
Pipeline #294 passed with stage
in 2 minutes and 13 seconds
package com.mall4j.cloud.auth.controller;
import com.anji.captcha.model.common.ResponseModel;
import com.anji.captcha.model.vo.CaptchaVO;
import com.anji.captcha.service.CaptchaService;
import com.mall4j.cloud.common.response.ServerResponseEntity;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author FrozenWatermelon
* @date 2020/7/30
*/
@RestController
@RequestMapping("/ua/captcha")
@Tag(name = "验证码")
public class CaptchaController {
private final CaptchaService captchaService;
public CaptchaController(CaptchaService captchaService) {
this.captchaService = captchaService;
}
@PostMapping({ "/get" })
public ServerResponseEntity<ResponseModel> get(@RequestBody CaptchaVO captchaVO) {
return ServerResponseEntity.success(captchaService.get(captchaVO));
}
@PostMapping({ "/check" })
public ServerResponseEntity<ResponseModel> check(@RequestBody CaptchaVO captchaVO) {
return ServerResponseEntity.success(captchaService.check(captchaVO));
}
}
package com.mall4j.cloud.auth.controller;
import com.mall4j.cloud.api.auth.bo.UserInfoInTokenBO;
import com.mall4j.cloud.api.rbac.dto.ClearUserPermissionsCacheDTO;
import com.mall4j.cloud.api.rbac.feign.PermissionFeignClient;
import com.mall4j.cloud.auth.dto.AuthenticationDTO;
import com.mall4j.cloud.auth.manager.TokenStore;
import com.mall4j.cloud.auth.service.AuthAccountService;
import com.mall4j.cloud.common.response.ResponseEnum;
import com.mall4j.cloud.common.response.ServerResponseEntity;
import com.mall4j.cloud.common.security.AuthUserContext;
import com.mall4j.cloud.api.auth.vo.TokenInfoVO;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import jakarta.validation.Valid;
/**
* @author FrozenWatermelon
* @date 2020/6/30
*/
@RestController
@Tag(name = "登录")
public class LoginController {
@Autowired
private TokenStore tokenStore;
@Autowired
private AuthAccountService authAccountService;
@Autowired
private PermissionFeignClient permissionFeignClient;
@Autowired
private PasswordEncoder passwordEncoder;
@PostMapping("/ua/login")
@Operation(summary = "账号密码" , description = "通过账号登录,还要携带用户的类型,也就是用户所在的系统")
public ServerResponseEntity<TokenInfoVO> login(
@Valid @RequestBody AuthenticationDTO authenticationDTO) {
// 这边获取了用户的用户信息,那么根据sessionid对应一个user的原则,我应该要把这个东西存起来,然后校验,那么存到哪里呢?
// redis,redis有天然的自动过期的机制,有key value的形式
ServerResponseEntity<UserInfoInTokenBO> userInfoInTokenResponse = authAccountService
.getUserInfoInTokenByInputUserNameAndPassword(authenticationDTO.getPrincipal(),
authenticationDTO.getCredentials(), authenticationDTO.getSysType());
if (!userInfoInTokenResponse.isSuccess()) {
return ServerResponseEntity.transform(userInfoInTokenResponse);
}
UserInfoInTokenBO data = userInfoInTokenResponse.getData();
ClearUserPermissionsCacheDTO clearUserPermissionsCacheDTO = new ClearUserPermissionsCacheDTO();
clearUserPermissionsCacheDTO.setSysType(data.getSysType());
clearUserPermissionsCacheDTO.setUserId(data.getUserId());
// 将以前的权限清理了,以免权限有缓存
ServerResponseEntity<Void> clearResponseEntity = permissionFeignClient.clearUserPermissionsCache(clearUserPermissionsCacheDTO);
if (!clearResponseEntity.isSuccess()) {
return ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED);
}
// 保存token,返回token数据给前端,这里是最重要的
return ServerResponseEntity.success(tokenStore.storeAndGetVo(data));
}
@PostMapping("/login_out")
@Operation(summary = "退出登陆" , description = "点击退出登陆,清除token,清除菜单缓存")
public ServerResponseEntity<TokenInfoVO> loginOut() {
UserInfoInTokenBO userInfoInToken = AuthUserContext.get();
ClearUserPermissionsCacheDTO clearUserPermissionsCacheDTO = new ClearUserPermissionsCacheDTO();
clearUserPermissionsCacheDTO.setSysType(userInfoInToken.getSysType());
clearUserPermissionsCacheDTO.setUserId(userInfoInToken.getUserId());
// 将以前的权限清理了,以免权限有缓存
permissionFeignClient.clearUserPermissionsCache(clearUserPermissionsCacheDTO);
// 删除该用户在该系统的token
tokenStore.deleteAllToken(userInfoInToken.getSysType().toString(), userInfoInToken.getUid());
return ServerResponseEntity.success();
}
}
package com.mall4j.cloud.auth.controller;
import com.mall4j.cloud.api.auth.bo.UserInfoInTokenBO;
import com.mall4j.cloud.api.auth.vo.TokenInfoVO;
import com.mall4j.cloud.auth.dto.UpdatePasswordDTO;
import com.mall4j.cloud.auth.manager.TokenStore;
import com.mall4j.cloud.auth.model.AuthAccount;
import com.mall4j.cloud.auth.service.AuthAccountService;
import com.mall4j.cloud.common.response.ServerResponseEntity;
import com.mall4j.cloud.common.security.AuthUserContext;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import jakarta.validation.Valid;
/**
* @author FrozenWatermelon
* @date 2021/01/29
*/
@RestController
@Tag(name = "密码")
public class PasswordController {
@Autowired
private TokenStore tokenStore;
@Autowired
private AuthAccountService authAccountService;
@Autowired
private PasswordEncoder passwordEncoder;
@PutMapping("/update_password")
@Operation(summary = "更新密码" , description = "更新当前用户的密码, 更新密码之后要退出登录,清理token")
public ServerResponseEntity<TokenInfoVO> updatePassword(@Valid @RequestBody UpdatePasswordDTO updatePasswordDTO) {
UserInfoInTokenBO userInfoInToken = AuthUserContext.get();
AuthAccount authAccount = authAccountService.getByUserIdAndType(userInfoInToken.getUserId(), userInfoInToken.getSysType());
if (!passwordEncoder.matches(updatePasswordDTO.getOldPassword(), authAccount.getPassword())) {
return ServerResponseEntity.showFailMsg("旧密码不正确");
}
authAccountService.updatePassword(userInfoInToken.getUserId(), userInfoInToken.getSysType(), updatePasswordDTO.getNewPassword());
tokenStore.deleteAllToken(userInfoInToken.getSysType().toString(), userInfoInToken.getUid());
return ServerResponseEntity.success();
}
}
package com.mall4j.cloud.auth.controller;
import com.mall4j.cloud.common.security.bo.TokenInfoBO;
import com.mall4j.cloud.auth.dto.RefreshTokenDTO;
import com.mall4j.cloud.auth.manager.TokenStore;
import com.mall4j.cloud.api.auth.vo.TokenInfoVO;
import com.mall4j.cloud.common.response.ServerResponseEntity;
import io.swagger.v3.oas.annotations.tags.Tag;
import com.mall4j.cloud.common.util.BeanUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import jakarta.validation.Valid;
/**
* @author FrozenWatermelon
* @date 2020/6/30
*/
@RestController
@Tag(name = "token")
public class TokenController {
@Autowired
private TokenStore tokenStore;
@PostMapping("/ua/token/refresh")
public ServerResponseEntity<TokenInfoVO> refreshToken(@Valid @RequestBody RefreshTokenDTO refreshTokenDTO) {
ServerResponseEntity<TokenInfoBO> tokenInfoServerResponseEntity = tokenStore
.refreshToken(refreshTokenDTO.getRefreshToken());
if (!tokenInfoServerResponseEntity.isSuccess()) {
return ServerResponseEntity.transform(tokenInfoServerResponseEntity);
}
return ServerResponseEntity
.success(BeanUtil.map(tokenInfoServerResponseEntity.getData(), TokenInfoVO.class));
}
}
package com.mall4j.cloud.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
/**
* 用于登陆传递账号密码
*
* @author FrozenWatermelon
* @date 2020/7/1
*/
public class AuthenticationDTO {
/**
* 用户名
*/
@NotBlank(message = "principal不能为空")
@Schema(description = "用户名" , requiredMode = Schema.RequiredMode.REQUIRED)
protected String principal;
/**
* 密码
*/
@NotBlank(message = "credentials不能为空")
@Schema(description = "一般用作密码" , requiredMode = Schema.RequiredMode.REQUIRED)
protected String credentials;
/**
* sysType 参考SysTypeEnum
*/
@NotNull(message = "sysType不能为空")
@Schema(description = "系统类型 0.普通用户系统 1.商家端" , requiredMode = Schema.RequiredMode.REQUIRED)
protected Integer sysType;
public String getPrincipal() {
return principal;
}
public void setPrincipal(String principal) {
this.principal = principal;
}
public String getCredentials() {
return credentials;
}
public void setCredentials(String credentials) {
this.credentials = credentials;
}
public Integer getSysType() {
return sysType;
}
public void setSysType(Integer sysType) {
this.sysType = sysType;
}
@Override
public String toString() {
return "AuthenticationDTO{" +
"principal='" + principal + '\'' +
", credentials='" + credentials + '\'' +
", sysType=" + sysType +
'}';
}
}
package com.mall4j.cloud.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* 验证码登陆
*
* @author FrozenWatermelon
* @date 2020/7/1
*/
public class CaptchaAuthenticationDTO extends AuthenticationDTO {
@Schema(description = "验证码" , requiredMode = Schema.RequiredMode.REQUIRED)
private String captchaVerification;
public String getCaptchaVerification() {
return captchaVerification;
}
public void setCaptchaVerification(String captchaVerification) {
this.captchaVerification = captchaVerification;
}
@Override
public String toString() {
return "CaptchaAuthenticationDTO{" + "captchaVerification='" + captchaVerification + '\'' + "} "
+ super.toString();
}
}
package com.mall4j.cloud.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
/**
* 刷新token
*
* @author FrozenWatermelon
* @date 2020/7/1
*/
public class RefreshTokenDTO {
/**
* refreshToken
*/
@NotBlank(message = "refreshToken不能为空")
@Schema(description = "refreshToken" , requiredMode = Schema.RequiredMode.REQUIRED)
private String refreshToken;
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
@Override
public String toString() {
return "RefreshTokenDTO{" + "refreshToken='" + refreshToken + '\'' + '}';
}
}
package com.mall4j.cloud.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
/**
* 更新密码
*
* @author FrozenWatermelon
* @date 2020/09/21
*/
public class UpdatePasswordDTO {
@NotBlank(message = "oldPassword NotBlank")
@Schema(description = "旧密码" , requiredMode = Schema.RequiredMode.REQUIRED)
private String oldPassword;
@NotNull(message = "newPassword NotNull")
@Schema(description = "新密码" , requiredMode = Schema.RequiredMode.REQUIRED)
private String newPassword;
public String getOldPassword() {
return oldPassword;
}
public void setOldPassword(String oldPassword) {
this.oldPassword = oldPassword;
}
public String getNewPassword() {
return newPassword;
}
public void setNewPassword(String newPassword) {
this.newPassword = newPassword;
}
@Override
public String toString() {
return "UpdatePasswordDTO{" +
"oldPassword='" + oldPassword + '\'' +
", newPassword='" + newPassword + '\'' +
'}';
}
}
package com.mall4j.cloud.auth.feign;
import cn.hutool.core.util.StrUtil;
import com.mall4j.cloud.api.auth.bo.UserInfoInTokenBO;
import com.mall4j.cloud.api.auth.constant.SysTypeEnum;
import com.mall4j.cloud.api.auth.dto.AuthAccountDTO;
import com.mall4j.cloud.api.auth.feign.AccountFeignClient;
import com.mall4j.cloud.api.auth.vo.AuthAccountVO;
import com.mall4j.cloud.api.leaf.feign.SegmentFeignClient;
import com.mall4j.cloud.auth.manager.TokenStore;
import com.mall4j.cloud.auth.mapper.AuthAccountMapper;
import com.mall4j.cloud.auth.model.AuthAccount;
import com.mall4j.cloud.common.exception.Mall4cloudException;
import com.mall4j.cloud.common.response.ResponseEnum;
import com.mall4j.cloud.common.response.ServerResponseEntity;
import com.mall4j.cloud.common.security.AuthUserContext;
import com.mall4j.cloud.common.security.bo.AuthAccountInVerifyBO;
import com.mall4j.cloud.common.security.constant.InputUserNameEnum;
import com.mall4j.cloud.api.auth.vo.TokenInfoVO;
import com.mall4j.cloud.common.util.PrincipalUtil;
import com.mall4j.cloud.common.util.BeanUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
import java.util.Objects;
/**
* @author FrozenWatermelon
* @date 2020/9/22
*/
@RestController
public class AccountFeignController implements AccountFeignClient {
@Autowired
private AuthAccountMapper authAccountMapper;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private TokenStore tokenStore;
@Autowired
private SegmentFeignClient segmentFeignClient;
@Override
@Transactional(rollbackFor = Exception.class)
public ServerResponseEntity<Long> save(AuthAccountDTO authAccountDTO) {
ServerResponseEntity<Long> segmentIdResponse = segmentFeignClient.getSegmentId("mall4cloud-auth-account");
if (!segmentIdResponse.isSuccess()) {
throw new Mall4cloudException(ResponseEnum.EXCEPTION);
}
ServerResponseEntity<AuthAccount> verify = verify(authAccountDTO);
if (!verify.isSuccess()) {
return ServerResponseEntity.transform(verify);
}
AuthAccount data = verify.getData();
data.setUid(segmentIdResponse.getData());
authAccountMapper.save(data);
return ServerResponseEntity.success(data.getUid());
}
@Override
@Transactional(rollbackFor = Exception.class)
public ServerResponseEntity<Void> update(AuthAccountDTO authAccountDTO) {
ServerResponseEntity<AuthAccount> verify = verify(authAccountDTO);
if (!verify.isSuccess()) {
return ServerResponseEntity.transform(verify);
}
authAccountMapper.updateAccountInfo(verify.getData());
return ServerResponseEntity.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public ServerResponseEntity<Void> updateAuthAccountStatus(AuthAccountDTO authAccountDTO) {
if (Objects.isNull(authAccountDTO.getStatus())) {
throw new Mall4cloudException(ResponseEnum.EXCEPTION);
}
AuthAccount authAccount = BeanUtil.map(authAccountDTO, AuthAccount.class);
authAccountMapper.updateAccountInfo(authAccount);
return ServerResponseEntity.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public ServerResponseEntity<Void> deleteByUserIdAndSysType(Long userId) {
UserInfoInTokenBO userInfoInTokenBO = AuthUserContext.get();
authAccountMapper.deleteByUserIdAndSysType(userId, userInfoInTokenBO.getSysType());
return ServerResponseEntity.success();
}
@Override
public ServerResponseEntity<AuthAccountVO> getByUserIdAndSysType(Long userId,Integer sysType) {
UserInfoInTokenBO userInfoInTokenBO = AuthUserContext.get();
AuthAccount authAccount = authAccountMapper.getByUserIdAndType(userId, userInfoInTokenBO.getSysType());
return ServerResponseEntity.success(BeanUtil.map(authAccount, AuthAccountVO.class));
}
@Override
public ServerResponseEntity<TokenInfoVO> storeTokenAndGetVo(UserInfoInTokenBO userInfoInTokenBO) {
return ServerResponseEntity.success(tokenStore.storeAndGetVo(userInfoInTokenBO));
}
@Override
public ServerResponseEntity<AuthAccountVO> getByUsernameAndSysType(String username, SysTypeEnum sysType) {
return ServerResponseEntity.success(authAccountMapper.getByUsernameAndSysType(username, sysType.value()));
}
private ServerResponseEntity<AuthAccount> verify(AuthAccountDTO authAccountDTO) {
// 用户名
if (!PrincipalUtil.isUserName(authAccountDTO.getUsername())) {
return ServerResponseEntity.showFailMsg("用户名格式不正确");
}
AuthAccountInVerifyBO userNameBo = authAccountMapper.getAuthAccountInVerifyByInputUserName(InputUserNameEnum.USERNAME.value(), authAccountDTO.getUsername(), authAccountDTO.getSysType());
if (userNameBo != null && !Objects.equals(userNameBo.getUserId(), authAccountDTO.getUserId())) {
return ServerResponseEntity.showFailMsg("用户名已存在,请更换用户名再次尝试");
}
AuthAccount authAccount = BeanUtil.map(authAccountDTO, AuthAccount.class);
if (StrUtil.isNotBlank(authAccount.getPassword())) {
authAccount.setPassword(passwordEncoder.encode(authAccount.getPassword()));
}
return ServerResponseEntity.success(authAccount);
}
@Override
@Transactional(rollbackFor = Exception.class)
public ServerResponseEntity<Void> updateUserInfoByUserIdAndSysType(UserInfoInTokenBO userInfoInTokenBO, Long userId, Integer sysType) {
AuthAccount byUserIdAndType = authAccountMapper.getByUserIdAndType(userId, sysType);
userInfoInTokenBO.setUid(byUserIdAndType.getUid());
tokenStore.updateUserInfoByUidAndAppId(byUserIdAndType.getUid(), sysType.toString(), userInfoInTokenBO);
AuthAccount authAccount = BeanUtil.map(userInfoInTokenBO, AuthAccount.class);
int res = authAccountMapper.updateUserInfoByUserId(authAccount, userId, sysType);
if (res != 1) {
throw new Mall4cloudException("用户信息错误,更新失败");
}
return ServerResponseEntity.success();
}
@Override
public ServerResponseEntity<AuthAccountVO> getMerchantInfoByTenantId(Long tenantId) {
AuthAccountVO authAccountVO = authAccountMapper.getMerchantInfoByTenantId(tenantId);
return ServerResponseEntity.success(authAccountVO);
}
}
package com.mall4j.cloud.auth.feign;
import com.mall4j.cloud.api.auth.bo.UserInfoInTokenBO;
import com.mall4j.cloud.api.auth.feign.TokenFeignClient;
import com.mall4j.cloud.auth.manager.TokenStore;
import com.mall4j.cloud.common.response.ServerResponseEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;
/**
* @author FrozenWatermelon
* @date 2020/7/15
*/
@RestController
public class TokenFeignController implements TokenFeignClient {
private static final Logger logger = LoggerFactory.getLogger(TokenFeignController.class);
@Autowired
private TokenStore tokenStore;
@Override
public ServerResponseEntity<UserInfoInTokenBO> checkToken(String accessToken) {
ServerResponseEntity<UserInfoInTokenBO> userInfoByAccessTokenResponse = tokenStore
.getUserInfoByAccessToken(accessToken, true);
if (!userInfoByAccessTokenResponse.isSuccess()) {
return ServerResponseEntity.transform(userInfoByAccessTokenResponse);
}
return ServerResponseEntity.success(userInfoByAccessTokenResponse.getData());
}
}
package com.mall4j.cloud.auth.manager;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.symmetric.AES;
import com.mall4j.cloud.common.cache.constant.CacheNames;
import com.mall4j.cloud.common.exception.Mall4cloudException;
import com.mall4j.cloud.common.response.ResponseEnum;
import com.mall4j.cloud.common.security.bo.TokenInfoBO;
import com.mall4j.cloud.api.auth.bo.UserInfoInTokenBO;
import com.mall4j.cloud.api.auth.constant.SysTypeEnum;
import com.mall4j.cloud.common.response.ServerResponseEntity;
import com.mall4j.cloud.api.auth.vo.TokenInfoVO;
import com.mall4j.cloud.common.util.PrincipalUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* token管理 1. 登陆返回token 2. 刷新token 3. 清除用户过去token 4. 校验token
*
* @author FrozenWatermelon
* @date 2020/7/2
*/
@Component
@RefreshScope
public class TokenStore {
private static final Logger logger = LoggerFactory.getLogger(TokenStore.class);
private final RedisTemplate<Object, Object> redisTemplate;
private final RedisSerializer<Object> redisSerializer;
private final StringRedisTemplate stringRedisTemplate;
public TokenStore(RedisTemplate<Object, Object> redisTemplate, RedisSerializer<Object> redisSerializer,
StringRedisTemplate stringRedisTemplate) {
this.redisTemplate = redisTemplate;
this.redisSerializer = redisSerializer;
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 将用户的部分信息存储在token中,并返回token信息
* @param userInfoInToken 用户在token中的信息
* @return token信息
*/
public TokenInfoBO storeAccessToken(UserInfoInTokenBO userInfoInToken) {
TokenInfoBO tokenInfoBO = new TokenInfoBO();
String accessToken = IdUtil.simpleUUID();
String refreshToken = IdUtil.simpleUUID();
tokenInfoBO.setUserInfoInToken(userInfoInToken);
tokenInfoBO.setExpiresIn(getExpiresIn(userInfoInToken.getSysType()));
String uidToAccessKeyStr = getUidToAccessKey(getApprovalKey(userInfoInToken));
String accessKeyStr = getAccessKey(accessToken);
String refreshToAccessKeyStr = getRefreshToAccessKey(refreshToken);
// 一个用户会登陆很多次,每次登陆的token都会存在 uid_to_access里面
// 但是每次保存都会更新这个key的时间,而key里面的token有可能会过期,过期就要移除掉
List<String> existsAccessTokens = new ArrayList<>();
// 新的token数据
existsAccessTokens.add(accessToken + StrUtil.COLON + refreshToken);
Long size = redisTemplate.opsForSet().size(uidToAccessKeyStr);
if (size != null && size != 0) {
List<String> tokenInfoBoList = stringRedisTemplate.opsForSet().pop(uidToAccessKeyStr, size);
if (tokenInfoBoList != null) {
for (String accessTokenWithRefreshToken : tokenInfoBoList) {
String[] accessTokenWithRefreshTokenArr = accessTokenWithRefreshToken.split(StrUtil.COLON);
String accessTokenData = accessTokenWithRefreshTokenArr[0];
if (BooleanUtil.isTrue(stringRedisTemplate.hasKey(getAccessKey(accessTokenData)))) {
existsAccessTokens.add(accessTokenWithRefreshToken);
}
}
}
}
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
long expiresIn = tokenInfoBO.getExpiresIn();
byte[] uidKey = uidToAccessKeyStr.getBytes(StandardCharsets.UTF_8);
byte[] refreshKey = refreshToAccessKeyStr.getBytes(StandardCharsets.UTF_8);
byte[] accessKey = accessKeyStr.getBytes(StandardCharsets.UTF_8);
for (String existsAccessToken : existsAccessTokens) {
connection.sAdd(uidKey, existsAccessToken.getBytes(StandardCharsets.UTF_8));
}
// 通过uid + sysType 保存access_token,当需要禁用用户的时候,可以根据uid + sysType 禁用用户
connection.expire(uidKey, expiresIn);
// 通过refresh_token获取用户的access_token从而刷新token
connection.setEx(refreshKey, expiresIn, accessToken.getBytes(StandardCharsets.UTF_8));
// 通过access_token保存用户的租户id,用户id,uid
connection.setEx(accessKey, expiresIn, Objects.requireNonNull(redisSerializer.serialize(userInfoInToken)));
return null;
});
// 返回给前端是加密的token
tokenInfoBO.setAccessToken(encryptToken(accessToken,userInfoInToken.getSysType()));
tokenInfoBO.setRefreshToken(encryptToken(refreshToken,userInfoInToken.getSysType()));
return tokenInfoBO;
}
private int getExpiresIn(int sysType) {
// 3600秒
int expiresIn = 3600;
// 普通用户token过期时间 1小时
if (Objects.equals(sysType, SysTypeEnum.ORDINARY.value())) {
expiresIn = expiresIn * 24 * 30;
}
// 系统管理员的token过期时间 2小时
if (Objects.equals(sysType, SysTypeEnum.MULTISHOP.value()) || Objects.equals(sysType, SysTypeEnum.PLATFORM.value())) {
expiresIn = expiresIn * 24 * 30;
}
return expiresIn;
}
/**
* 根据accessToken 获取用户信息
* @param accessToken accessToken
* @param needDecrypt 是否需要解密
* @return 用户信息
*/
public ServerResponseEntity<UserInfoInTokenBO> getUserInfoByAccessToken(String accessToken, boolean needDecrypt) {
if (StrUtil.isBlank(accessToken)) {
return ServerResponseEntity.showFailMsg("accessToken is blank");
}
String realAccessToken;
if (needDecrypt) {
ServerResponseEntity<String> decryptTokenEntity = decryptToken(accessToken);
if (!decryptTokenEntity.isSuccess()) {
return ServerResponseEntity.transform(decryptTokenEntity);
}
realAccessToken = decryptTokenEntity.getData();
}
else {
realAccessToken = accessToken;
}
UserInfoInTokenBO userInfoInTokenBO = (UserInfoInTokenBO) redisTemplate.opsForValue()
.get(getAccessKey(realAccessToken));
if (userInfoInTokenBO == null) {
return ServerResponseEntity.showFailMsg("accessToken 已过期");
}
return ServerResponseEntity.success(userInfoInTokenBO);
}
/**
* 刷新token,并返回新的token
* @param refreshToken
* @return
*/
public ServerResponseEntity<TokenInfoBO> refreshToken(String refreshToken) {
if (StrUtil.isBlank(refreshToken)) {
return ServerResponseEntity.showFailMsg("refreshToken is blank");
}
ServerResponseEntity<String> decryptTokenEntity = decryptToken(refreshToken);
if (!decryptTokenEntity.isSuccess()) {
return ServerResponseEntity.transform(decryptTokenEntity);
}
String realRefreshToken = decryptTokenEntity.getData();
String accessToken = stringRedisTemplate.opsForValue().get(getRefreshToAccessKey(realRefreshToken));
if (StrUtil.isBlank(accessToken)) {
return ServerResponseEntity.showFailMsg("refreshToken 已过期");
}
ServerResponseEntity<UserInfoInTokenBO> userInfoByAccessTokenEntity = getUserInfoByAccessToken(accessToken,
false);
if (!userInfoByAccessTokenEntity.isSuccess()) {
return ServerResponseEntity.showFailMsg("refreshToken 已过期");
}
UserInfoInTokenBO userInfoInTokenBO = userInfoByAccessTokenEntity.getData();
// 删除旧的refresh_token
stringRedisTemplate.delete(getRefreshToAccessKey(realRefreshToken));
// 删除旧的access_token
stringRedisTemplate.delete(getAccessKey(accessToken));
// 保存一份新的token
TokenInfoBO tokenInfoBO = storeAccessToken(userInfoInTokenBO);
return ServerResponseEntity.success(tokenInfoBO);
}
/**
* 删除全部的token
*/
public void deleteAllToken(String appId, Long uid) {
String uidKey = getUidToAccessKey(getApprovalKey(appId, uid));
Long size = redisTemplate.opsForSet().size(uidKey);
if (size == null || size == 0) {
return;
}
List<String> tokenInfoBoList = stringRedisTemplate.opsForSet().pop(uidKey, size);
if (CollUtil.isEmpty(tokenInfoBoList)) {
return;
}
for (String accessTokenWithRefreshToken : tokenInfoBoList) {
String[] accessTokenWithRefreshTokenArr = accessTokenWithRefreshToken.split(StrUtil.COLON);
String accessToken = accessTokenWithRefreshTokenArr[0];
String refreshToken = accessTokenWithRefreshTokenArr[1];
redisTemplate.delete(getRefreshToAccessKey(refreshToken));
redisTemplate.delete(getAccessKey(accessToken));
}
redisTemplate.delete(uidKey);
}
private static String getApprovalKey(UserInfoInTokenBO userInfoInToken) {
return getApprovalKey(userInfoInToken.getSysType().toString(), userInfoInToken.getUid());
}
private static String getApprovalKey(String appId, Long uid) {
return uid == null? appId : appId + StrUtil.COLON + uid;
}
private String encryptToken(String accessToken,Integer sysType) {
return Base64.encode(accessToken + System.currentTimeMillis() + sysType);
}
private ServerResponseEntity<String> decryptToken(String data) {
String decryptStr;
String decryptToken;
try {
decryptStr = Base64.decodeStr(data);
decryptToken = decryptStr.substring(0,32);
// 创建token的时间,token使用时效性,防止攻击者通过一堆的尝试找到aes的密码,虽然aes是目前几乎最好的加密算法
long createTokenTime = Long.parseLong(decryptStr.substring(32,45));
// 系统类型
int sysType = Integer.parseInt(decryptStr.substring(45));
// token的过期时间
int expiresIn = getExpiresIn(sysType);
long second = 1000L;
if (System.currentTimeMillis() - createTokenTime > expiresIn * second) {
return ServerResponseEntity.showFailMsg("token 格式有误");
}
}
catch (Exception e) {
logger.error(e.getMessage());
return ServerResponseEntity.showFailMsg("token 格式有误");
}
// 防止解密后的token是脚本,从而对redis进行攻击,uuid只能是数字和小写字母
if (!PrincipalUtil.isSimpleChar(decryptToken)) {
return ServerResponseEntity.showFailMsg("token 格式有误");
}
return ServerResponseEntity.success(decryptToken);
}
public String getAccessKey(String accessToken) {
return CacheNames.ACCESS + accessToken;
}
public String getUidToAccessKey(String approvalKey) {
return CacheNames.UID_TO_ACCESS + approvalKey;
}
public String getRefreshToAccessKey(String refreshToken) {
return CacheNames.REFRESH_TO_ACCESS + refreshToken;
}
public TokenInfoVO storeAndGetVo(UserInfoInTokenBO userInfoInToken) {
TokenInfoBO tokenInfoBO = storeAccessToken(userInfoInToken);
TokenInfoVO tokenInfoVO = new TokenInfoVO();
tokenInfoVO.setAccessToken(tokenInfoBO.getAccessToken());
tokenInfoVO.setRefreshToken(tokenInfoBO.getRefreshToken());
tokenInfoVO.setExpiresIn(tokenInfoBO.getExpiresIn());
return tokenInfoVO;
}
public void updateUserInfoByUidAndAppId(Long uid, String appId, UserInfoInTokenBO userInfoInTokenBO) {
if (userInfoInTokenBO == null) {
return;
}
String uidKey = getUidToAccessKey(getApprovalKey(appId, uid));
Set<String> tokenInfoBoList = stringRedisTemplate.opsForSet().members(uidKey);
if (tokenInfoBoList == null || tokenInfoBoList.size() == 0) {
throw new Mall4cloudException(ResponseEnum.UNAUTHORIZED);
}
for (String accessTokenWithRefreshToken : tokenInfoBoList) {
String[] accessTokenWithRefreshTokenArr = accessTokenWithRefreshToken.split(StrUtil.COLON);
String accessKey = this.getAccessKey(accessTokenWithRefreshTokenArr[0]);
UserInfoInTokenBO oldUserInfoInTokenBO = (UserInfoInTokenBO) redisTemplate.opsForValue().get(accessKey);
if (oldUserInfoInTokenBO == null) {
continue;
}
BeanUtils.copyProperties(userInfoInTokenBO, oldUserInfoInTokenBO);
redisTemplate.opsForValue().set(accessKey, Objects.requireNonNull(userInfoInTokenBO),getExpiresIn(userInfoInTokenBO.getSysType()), TimeUnit.SECONDS);
}
}
}
package com.mall4j.cloud.auth.mapper;
import com.mall4j.cloud.api.auth.vo.AuthAccountVO;
import com.mall4j.cloud.auth.model.AuthAccount;
import com.mall4j.cloud.common.security.bo.AuthAccountInVerifyBO;
import org.apache.ibatis.annotations.Param;
/**
* @author FrozenWatermelon
* @date 2020/7/2
*/
public interface AuthAccountMapper {
/**
* 根据输入的用户名及用户名类型获取用户信息
*
* @param inputUserNameType 输入的用户名类型 1.username 2.mobile 3.email
* @param inputUserName 输入的用户名
* @param sysType 系统类型
* @return 用户在token中信息 + 数据库中的密码
*/
AuthAccountInVerifyBO getAuthAccountInVerifyByInputUserName(@Param("inputUserNameType") Integer inputUserNameType,
@Param("inputUserName") String inputUserName, @Param("sysType") Integer sysType);
/**
* 根据用户id 和系统类型获取平台唯一用户
*
* @param userId 用户id
* @param sysType 系统类型
* @return 平台唯一用户
*/
AuthAccount getByUserIdAndType(@Param("userId") Long userId, @Param("sysType") Integer sysType);
/**
* 根据getByUid获取平台唯一用户
*
* @param uid uid
* @return 平台唯一用户
*/
AuthAccount getByUid(@Param("uid") Long uid);
/**
* 更新密码 根据用户id 和系统类型
*
* @param userId 用户id
* @param sysType 系统类型
* @param newPassWord 新密码
*/
void updatePassword(@Param("userId") Long userId, @Param("sysType") Integer sysType, @Param("newPassWord") String newPassWord);
/**
* 保存
*
* @param authAccount
*/
void save(@Param("authAccount") AuthAccount authAccount);
/**
* 更新
*
* @param authAccount authAccount
*/
void updateAccountInfo(@Param("authAccount") AuthAccount authAccount);
/**
* 根据用户id和系统类型删除用户
*
* @param userId 用户id
* @param sysType 系统类型
*/
void deleteByUserIdAndSysType(@Param("userId") Long userId, @Param("sysType") Integer sysType);
/**
* 根据用户名和系统类型获取用户信息
* @param validAccount
* @param systemType
* @return uid
*/
AuthAccount getAccountByInputUserName(@Param("validAccount") String validAccount, @Param("systemType") Integer systemType);
/**
* 根据用户名和系统类型获取用户信息
* @param username
* @param sysType
* @return
*/
AuthAccountVO getByUsernameAndSysType(@Param("userName") String username, @Param("sysType") Integer sysType);
/**
* 根据用户id更新租户id
* @param authAccount
* @param userId
* @param sysType
* @return
*/
int updateUserInfoByUserId(@Param("authAccount") AuthAccount authAccount, @Param("userId") Long userId, @Param("sysType") Integer sysType);
/**
* 根据租户id获取商家信息
* @param tenantId
* @return
*/
AuthAccountVO getMerchantInfoByTenantId(@Param("tenantId") Long tenantId);
}
package com.mall4j.cloud.auth.model;
import com.mall4j.cloud.common.database.annotations.DistributedId;
import com.mall4j.cloud.common.model.BaseModel;
/**
* 统一账户信息
*
* @author FrozenWatermelon
* @date 2020/07/02
*/
public class AuthAccount extends BaseModel {
/**
* 全平台用户唯一id
*/
@DistributedId("mall4cloud-auth-account")
private Long uid;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 创建ip
*/
private String createIp;
/**
* 状态 1:启用 0:禁用 -1:删除
*/
private Integer status;
/**
* 系统类型见SysTypeEnum 0.普通用户系统 1.商家端
*/
private Integer sysType;
/**
* 用户id
*/
private Long userId;
/**
* 所属租户
*/
private Long tenantId;
/**
* 是否是管理员
*/
private Integer isAdmin;
public Long getUid() {
return uid;
}
public void setUid(Long uid) {
this.uid = uid;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getCreateIp() {
return createIp;
}
public void setCreateIp(String createIp) {
this.createIp = createIp;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public Integer getSysType() {
return sysType;
}
public void setSysType(Integer sysType) {
this.sysType = sysType;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public Long getTenantId() {
return tenantId;
}
public void setTenantId(Long tenantId) {
this.tenantId = tenantId;
}
public Integer getIsAdmin() {
return isAdmin;
}
public void setIsAdmin(Integer isAdmin) {
this.isAdmin = isAdmin;
}
@Override
public String toString() {
return "AuthAccount{" +
"uid=" + uid +
", username='" + username + '\'' +
", password='" + password + '\'' +
", createIp='" + createIp + '\'' +
", status=" + status +
", sysType=" + sysType +
", userId=" + userId +
", tenantId=" + tenantId +
", isAdmin=" + isAdmin +
'}';
}
}
package com.mall4j.cloud.auth.service;
import com.mall4j.cloud.api.auth.bo.UserInfoInTokenBO;
import com.mall4j.cloud.auth.model.AuthAccount;
import com.mall4j.cloud.common.response.ServerResponseEntity;
/**
* 统一账户信息
*
* @author FrozenWatermelon
* @date 2020/7/2
*/
public interface AuthAccountService {
/**
* 通过输入的用户名密码,校验并获取部分用户信息
* @param inputUserName 输入的用户名(用户名)
* @param password 密码
* @param sysType 系统类型 @see SysTypeEnum
* @return 用户在token中信息
*/
ServerResponseEntity<UserInfoInTokenBO> getUserInfoInTokenByInputUserNameAndPassword(String inputUserName,
String password, Integer sysType);
/**
* 根据用户id 和系统类型获取平台唯一用户
* @param userId 用户id
* @param sysType 系统类型
* @return 平台唯一用户
*/
AuthAccount getByUserIdAndType(Long userId, Integer sysType);
/**
* 更新密码 根据用户id 和系统类型
* @param userId 用户id
* @param sysType 系统类型
* @param newPassWord 新密码
*/
void updatePassword(Long userId, Integer sysType, String newPassWord);
/**
* 根据getByUid获取平台唯一用户
*
* @param uid uid
* @return 平台唯一用户
*/
AuthAccount getByUid(Long uid);
/**
* 根据用户名获取用户信息
* @param username 用户名
* @param systemType 系统类型
* @return uid
*/
AuthAccount getAccountByInputUserName(String username, Integer systemType);
}
package com.mall4j.cloud.auth.service.impl;
import cn.hutool.core.util.StrUtil;
import com.mall4j.cloud.auth.constant.AuthAccountStatusEnum;
import com.mall4j.cloud.auth.model.AuthAccount;
import com.mall4j.cloud.common.security.bo.AuthAccountInVerifyBO;
import com.mall4j.cloud.api.auth.bo.UserInfoInTokenBO;
import com.mall4j.cloud.common.security.constant.InputUserNameEnum;
import com.mall4j.cloud.auth.mapper.AuthAccountMapper;
import com.mall4j.cloud.auth.service.AuthAccountService;
import com.mall4j.cloud.common.response.ServerResponseEntity;
import com.mall4j.cloud.common.util.PrincipalUtil;
import jakarta.annotation.Resource;
import com.mall4j.cloud.common.util.BeanUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Objects;
/**
* @author FrozenWatermelon
* @date 2020/7/2
*/
@Service
public class AuthAccountServiceImpl implements AuthAccountService {
@Resource
private AuthAccountMapper authAccountMapper;
@Autowired
private PasswordEncoder passwordEncoder;
public static final String USER_NOT_FOUND_SECRET = "USER_NOT_FOUND_SECRET";
private static String userNotFoundEncodedPassword;
@Override
public ServerResponseEntity<UserInfoInTokenBO> getUserInfoInTokenByInputUserNameAndPassword(String inputUserName,
String password, Integer sysType) {
if (StrUtil.isBlank(inputUserName)) {
return ServerResponseEntity.showFailMsg("用户名不能为空");
}
if (StrUtil.isBlank(password)) {
return ServerResponseEntity.showFailMsg("密码不能为空");
}
InputUserNameEnum inputUserNameEnum = null;
// 用户名
if (PrincipalUtil.isUserName(inputUserName)) {
inputUserNameEnum = InputUserNameEnum.USERNAME;
}
if (inputUserNameEnum == null) {
return ServerResponseEntity.showFailMsg("请输入正确的用户名");
}
AuthAccountInVerifyBO authAccountInVerifyBO = authAccountMapper
.getAuthAccountInVerifyByInputUserName(inputUserNameEnum.value(), inputUserName, sysType);
if (authAccountInVerifyBO == null) {
prepareTimingAttackProtection();
// 再次进行运算,防止计时攻击
// 计时攻击(Timing
// attack),通过设备运算的用时来推断出所使用的运算操作,或者通过对比运算的时间推定数据位于哪个存储设备,或者利用通信的时间差进行数据窃取。
mitigateAgainstTimingAttack(password);
return ServerResponseEntity.showFailMsg("用户名或密码不正确");
}
if (Objects.equals(authAccountInVerifyBO.getStatus(), AuthAccountStatusEnum.DISABLE.value())) {
return ServerResponseEntity.showFailMsg("用户已禁用,请联系客服");
}
if (!passwordEncoder.matches(password, authAccountInVerifyBO.getPassword())) {
return ServerResponseEntity.showFailMsg("用户名或密码不正确");
}
return ServerResponseEntity.success(BeanUtil.map(authAccountInVerifyBO, UserInfoInTokenBO.class));
}
@Override
public AuthAccount getByUserIdAndType(Long userId, Integer sysType) {
return authAccountMapper.getByUserIdAndType(userId, sysType);
}
@Override
public void updatePassword(Long userId, Integer sysType, String newPassWord) {
authAccountMapper.updatePassword(userId, sysType, passwordEncoder.encode(newPassWord));
}
@Override
public AuthAccount getByUid(Long uid) {
return authAccountMapper.getByUid(uid);
}
@Override
public AuthAccount getAccountByInputUserName(String mobile, Integer systemType) {
return authAccountMapper.getAccountByInputUserName(mobile,systemType);
}
/**
* 防止计时攻击
*/
private void prepareTimingAttackProtection() {
if (userNotFoundEncodedPassword == null) {
userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_SECRET);
}
}
/**
* 防止计时攻击
*/
private void mitigateAgainstTimingAttack(String presentedPassword) {
if (presentedPassword != null) {
this.passwordEncoder.matches(presentedPassword, userNotFoundEncodedPassword);
}
}
}
com.anji.captcha.service.impl.CaptchaCacheServiceMemImpl
com.mall4j.cloud.auth.adapter.CaptchaCacheServiceRedisImpl
server:
port: 9101
spring:
application:
name: @artifactId@
cloud:
nacos:
discovery:
server-addr: ${NACOS_HOST:192.168.1.46}:${NACOS_PORT:8848}
username: nacos
password: nacos
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
file-extension: yml
namespace: @nacos.namespace@
shared-configs:
- application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
username: ${spring.cloud.nacos.discovery.username}
password: ${spring.cloud.nacos.discovery.password}
profiles:
active: @profiles.active@
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment