refactor: 多租户开发和代码规范调整
This commit is contained in:
@@ -2,38 +2,45 @@ package com.youlai.boot.security.filter;
|
||||
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.common.constant.SecurityConstants;
|
||||
import com.youlai.boot.core.web.ResultCode;
|
||||
import com.youlai.boot.core.web.WebResponseHelper;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletInputStream;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import org.springframework.web.util.ContentCachingRequestWrapper;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 图形验证码校验过滤器
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2022/10/1
|
||||
*/
|
||||
public class CaptchaValidationFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final RequestMatcher LOGIN_PATH_REQUEST_MATCHER = PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST,SecurityConstants.LOGIN_PATH);
|
||||
private static final RequestMatcher LOGIN_PATH_REQUEST_MATCHER = PathPatternRequestMatcher.withDefaults()
|
||||
.matcher(HttpMethod.POST, SecurityConstants.LOGIN_PATH);
|
||||
|
||||
public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode";
|
||||
public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey";
|
||||
public static final String CAPTCHA_ID_PARAM_NAME = "captchaId";
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
private final CodeGenerator codeGenerator;
|
||||
|
||||
public CaptchaValidationFilter(RedisTemplate<String, Object> redisTemplate, CodeGenerator codeGenerator) {
|
||||
@@ -41,37 +48,111 @@ public class CaptchaValidationFilter extends OncePerRequestFilter {
|
||||
this.codeGenerator = codeGenerator;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
|
||||
// 检验登录接口的验证码
|
||||
if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) {
|
||||
// 请求中的验证码
|
||||
String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME);
|
||||
// TODO 兼容没有验证码的版本(线上请移除这个判断)
|
||||
if (StrUtil.isBlank(captchaCode)) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
// 缓存中的验证码
|
||||
String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME);
|
||||
String cacheVerifyCode = (String) redisTemplate.opsForValue().get(
|
||||
StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey)
|
||||
);
|
||||
if (cacheVerifyCode == null) {
|
||||
WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED);
|
||||
} else {
|
||||
// 验证码比对
|
||||
if (codeGenerator.verify(cacheVerifyCode, captchaCode)) {
|
||||
chain.doFilter(request, response);
|
||||
} else {
|
||||
WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 非登录接口放行
|
||||
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
// 非登录接口直接放行
|
||||
if (!LOGIN_PATH_REQUEST_MATCHER.matches(request)) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 仅支持 JSON 登录
|
||||
String contentType = request.getContentType();
|
||||
if (contentType == null || !contentType.contains(MediaType.APPLICATION_JSON_VALUE)) {
|
||||
WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
// 包装请求,确保下游还能读取 body
|
||||
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
|
||||
|
||||
byte[] bodyBytes = StreamUtils.copyToByteArray(requestWrapper.getInputStream());
|
||||
String body = new String(bodyBytes, StandardCharsets.UTF_8);
|
||||
String captchaCode = null;
|
||||
String captchaId = null;
|
||||
|
||||
if (StrUtil.isNotBlank(body)) {
|
||||
JSONObject jsonObject = JSONUtil.parseObj(body);
|
||||
captchaCode = jsonObject.getStr(CAPTCHA_CODE_PARAM_NAME);
|
||||
captchaId = jsonObject.getStr(CAPTCHA_ID_PARAM_NAME);
|
||||
}
|
||||
|
||||
if (StrUtil.isBlank(captchaCode) || StrUtil.isBlank(captchaId)) {
|
||||
WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
String cacheVerifyCode = (String) redisTemplate.opsForValue().get(
|
||||
StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaId)
|
||||
);
|
||||
if (cacheVerifyCode == null) {
|
||||
WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED);
|
||||
return;
|
||||
}
|
||||
|
||||
if (codeGenerator.verify(cacheVerifyCode, captchaCode)) {
|
||||
HttpServletRequest repeatableRequest = new RepeatableReadRequestWrapper(requestWrapper, bodyBytes);
|
||||
chain.doFilter(repeatableRequest, response);
|
||||
} else {
|
||||
WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple wrapper to allow repeated reads of the request body after we've parsed it here.
|
||||
*/
|
||||
private static class RepeatableReadRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
private final byte[] cachedBody;
|
||||
|
||||
RepeatableReadRequestWrapper(HttpServletRequest request, byte[] cachedBody) {
|
||||
super(request);
|
||||
this.cachedBody = cachedBody != null ? cachedBody : new byte[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() {
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(cachedBody);
|
||||
return new ServletInputStream() {
|
||||
@Override
|
||||
public int read() {
|
||||
return bais.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return bais.available() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(jakarta.servlet.ReadListener readListener) {
|
||||
// no-op
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedReader getReader() {
|
||||
return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getContentLength() {
|
||||
return cachedBody.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getContentLengthLong() {
|
||||
return cachedBody.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -38,6 +38,11 @@ public class OnlineUser {
|
||||
*/
|
||||
private Integer dataScope;
|
||||
|
||||
/**
|
||||
* 租户ID
|
||||
*/
|
||||
private Long tenantId;
|
||||
|
||||
/**
|
||||
* 角色权限集合
|
||||
*/
|
||||
|
||||
@@ -56,6 +56,11 @@ public class SysUserDetails implements UserDetails {
|
||||
*/
|
||||
private Integer dataScope;
|
||||
|
||||
/**
|
||||
* 租户ID
|
||||
*/
|
||||
private Long tenantId;
|
||||
|
||||
/**
|
||||
* 用户角色权限集合
|
||||
*/
|
||||
@@ -73,6 +78,7 @@ public class SysUserDetails implements UserDetails {
|
||||
this.enabled = ObjectUtil.equal(user.getStatus(), 1);
|
||||
this.deptId = user.getDeptId();
|
||||
this.dataScope = user.getDataScope();
|
||||
this.tenantId = user.getTenantId();
|
||||
|
||||
// 初始化角色权限集合
|
||||
this.authorities = CollectionUtil.isNotEmpty(user.getRoles())
|
||||
|
||||
@@ -54,4 +54,9 @@ public class UserAuthCredentials {
|
||||
*/
|
||||
private Integer dataScope;
|
||||
|
||||
/**
|
||||
* 租户ID(从登录上下文中获取)
|
||||
*/
|
||||
private Long tenantId;
|
||||
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ import java.util.*;
|
||||
/**
|
||||
* SpringSecurity 权限校验
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2022/2/22
|
||||
* @author Ray.Hao
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@Component("ss")
|
||||
@RequiredArgsConstructor
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.youlai.boot.security.service;
|
||||
|
||||
import com.youlai.boot.common.tenant.TenantContextHolder;
|
||||
import com.youlai.boot.security.model.SysUserDetails;
|
||||
import com.youlai.boot.security.model.UserAuthCredentials;
|
||||
import com.youlai.boot.system.service.UserService;
|
||||
@@ -37,6 +38,8 @@ public class SysUserDetailsService implements UserDetailsService {
|
||||
if (userAuthCredentials == null) {
|
||||
throw new UsernameNotFoundException(username);
|
||||
}
|
||||
// 将当前上下文中的租户ID写入认证凭证,便于后续 Token 携带租户信息
|
||||
userAuthCredentials.setTenantId(TenantContextHolder.getTenantId());
|
||||
return new SysUserDetails(userAuthCredentials);
|
||||
} catch (Exception e) {
|
||||
// 记录异常日志
|
||||
|
||||
@@ -91,6 +91,7 @@ public class JwtTokenManager implements TokenManager {
|
||||
userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID
|
||||
userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID
|
||||
userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围
|
||||
userDetails.setTenantId(payloads.getLong(JwtClaimConstants.TENANT_ID)); // 租户ID
|
||||
|
||||
userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名
|
||||
// 角色集合
|
||||
@@ -275,6 +276,7 @@ public class JwtTokenManager implements TokenManager {
|
||||
payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID
|
||||
payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID
|
||||
payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围
|
||||
payload.put(JwtClaimConstants.TENANT_ID, userDetails.getTenantId()); // 租户ID
|
||||
|
||||
// claims 中添加角色信息
|
||||
Set<String> roles = authentication.getAuthorities().stream()
|
||||
|
||||
@@ -61,6 +61,7 @@ public class RedisTokenManager implements TokenManager {
|
||||
user.getUsername(),
|
||||
user.getDeptId(),
|
||||
user.getDataScope(),
|
||||
user.getTenantId(),
|
||||
user.getAuthorities().stream()
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.collect(Collectors.toSet())
|
||||
@@ -268,6 +269,7 @@ public class RedisTokenManager implements TokenManager {
|
||||
userDetails.setUsername(onlineUser.getUsername());
|
||||
userDetails.setDeptId(onlineUser.getDeptId());
|
||||
userDetails.setDataScope(onlineUser.getDataScope());
|
||||
userDetails.setTenantId(onlineUser.getTenantId());
|
||||
userDetails.setAuthorities(authorities);
|
||||
return userDetails;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user