简单例子spring security双系统登录实现
1. 需求分析
现在有两个系统,分别为A系统以及B系统,两个系统对应的数据库为两个,用户菜单以及角色信息结构基本相同,现在想使用spring security实现两个服务的登录问题,通过一个固定标识,进行分库查询,登录不同的操作员信息,整体使用前后端分离方式,登录token信息存储在redis中。
2. 简单的实现步骤
- 分别编写密码校验比对,登录,登出,登录成功,权限控制等组件类
- 编写spring security基础配置框架SecurityConfig,实现WebSecurityConfigurerAdapter,注入之前编写对的各种组件到security过滤链中
- 编写动态切换数据源相关类
- 进行简单测试
3. 执行过程
- 前台登录的时候header中传递一个systemFLag标识,以及对应的用户密码(密码使用AES以及Base64进行加密)传递到后台,进行登录
- 后台接收到用户传入登录信息,获取header中系统信息,切换到对应系统的数据库,再判定传入用户是否存在,不存在返回用户名或密码错误,存在则继续往下执行
- 判定用户存在后,进行密码解密,解密后再进行md5加盐加密,与数据库中的密码进行比对,判定是否密码正确,正确往下执行,错误则返回用户名或密码错误
- 用户密码验证成功,则进行用户的岗位,机构信息加载(此处也需要切换到对应的库进行查询),并且加载后对的数据需要按照一定格式存储到redis中,生成对应的token信息。
- 最后将生成的token信息以及机构岗位信息返回到前台,前台进行动态选择机构,岗位然后获取对应的菜单信息
4. security部分具体代码如下
4.1 用户校验UserDetailsService
此处使用用户名(用户唯一标识)在用户表中进行查询,注意多个系统间需要切换数据库,最后返回security组件信息,我们项目为微服务前后分离,登录为一个单独模块,所以没有设置权限信息,权限相关信息在另一个uaa登录api组件中实现,其他服务模块引入使用security注解即可实现资源权限验证
package cn.git.auth.security;
import cn.git.auth.entity.TbCsmSupervision;
import cn.git.auth.entity.TpScuUser;
import cn.git.auth.entity.TpScuUserRisk;
import cn.git.auth.mapper.TbCsmSupervisionMapper;
import cn.git.auth.service.HomeService;
import cn.git.auth.util.UaaConstant;
import cn.git.common.util.UaaContext;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @program: bank-credit-sy
* @description: 通过数据库查询登录用户
* @author: lixuchun
* @create: 2021-02-06 12:41
*/
@Slf4j
@Service
public class AuthUserDetailsServiceImpl implements UserDetailsService {
@Autowired
private HomeService homeService;
@Autowired
private HttpServletRequest request;
@Autowired
private TbCsmSupervisionMapper tbCsmSupervisionMapper;
/**
* 用户登录时调用返回登录用户信息及角色信息
*
* @param username 登录用户名
* @return UserDetails
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 登录用户进行查询
log.info("柜员[{}]开始进行登录,查询是否存在!", username);
// 获取系统标识 0 信贷 1 风控系统
String systemFlag = UaaConstant.SYSTEM_LOAN;
if (StrUtil.isNotBlank(request.getHeader(UaaConstant.SYSTEM_FLAG))) {
systemFlag = request.getHeader(UaaConstant.SYSTEM_FLAG);
}
// 设置值到ThreadLocal中,在密码比对中使用
UaaContext.setUserCd(username);
UaaContext.setSystemFlag(systemFlag);
// 权限信息后期其他模块引入通用权限验证api实现,此处不设置
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
// 查询用户
if (UaaConstant.SYSTEM_LOAN.equals(systemFlag)) {
TpScuUser tpScuUser = homeService.selectUserInfo(username, systemFlag);
if (ObjectUtil.isEmpty(tpScuUser)) {
String errorMessage = StrUtil.format("用户{}不存在", username);
throw new UsernameNotFoundException(errorMessage);
}
return new User(JSON.toJSONString(tpScuUser), tpScuUser.getPassword(), grantedAuthorities);
} else if (UaaConstant.SYSTEM_SUPERVISION.equals(systemFlag)) {
// 监管账户信息校验
QueryWrapper<TbCsmSupervision> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(TbCsmSupervision::getUserCd, username);
TbCsmSupervision tbCsmSupervision = tbCsmSupervisionMapper.selectOne(queryWrapper);
if (ObjectUtil.isNull(tbCsmSupervision)) {
throw new RuntimeException("监管账户校验失败");
}
// 正常登录
TpScuUser tpScuUser = homeService.selectUserInfo(username, systemFlag);
if (ObjectUtil.isEmpty(tpScuUser)) {
String errorMessage = StrUtil.format("用户{}不存在", username);
throw new UsernameNotFoundException(errorMessage);
}
return new User(JSON.toJSONString(tpScuUser), tpScuUser.getPassword(), grantedAuthorities);
} else {
TpScuUserRisk tpScuUserRisk = homeService.selectRiskUserInfo(username, systemFlag);
if (ObjectUtil.isEmpty(tpScuUserRisk)) {
String errorMessage = StrUtil.format("用户{}不存在", username);
throw new UsernameNotFoundException(errorMessage);
}
return new User(JSON.toJSONString(tpScuUserRisk), tpScuUserRisk.getPassword(), grantedAuthorities);
}
}
}
4.2 用户登录密码加密解密比对
前端传入密码进行aes解密(base64解密(前端传入password))解密后,进行md5加密,然后再与本地数据库中加盐加密密码进行比较
package cn.git.auth.security;
import cn.git.common.util.PasswordOptionUtil;
import cn.git.common.util.UaaContext;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* @program: bank-credit-sy
* @description: 登录密码使用md5加密
* @author: liudong
* @create: 2021-02-06 14:21
*/
@Slf4j
@Component
public class AuthPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
return SecureUtil.md5(charSequence.toString());
}
@Autowired
private PasswordOptionUtil passwordOptionUtil;
@Override
public boolean matches(CharSequence charSequence, String s) {
// 前端传入密码进行aes解密(base64解密(前端传入password))
log.info("柜员前端输入密码为[{}]", charSequence.toString());
String decodePassword = passwordOptionUtil.decryptPassword(charSequence.toString());
boolean equalFlag = StrUtil.equals(passwordOptionUtil.encryptForDB(decodePassword, UaaContext.getUserCd()),
s,
true);
return equalFlag;
}
}
PasswordOptionUtil 工具类代码如下
package cn.git.common.util;
import cn.git.common.constant.CommonConstant;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
/**
* 密码aes加密解密通用方法
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2021-07-05
*/
@Component
@Slf4j
public class PasswordOptionUtil {
/**
* 加密解密算法hutoo工具类
*/
private static AES aes = null;
/**
* 单例静态方法
*/
static {
getAesInstance();
}
/**
* aes构造器
* @return aes对象
*/
public static synchronized AES getAesInstance() {
if (ObjectUtil.isNull(aes)) {
aes = new AES(Mode.ECB, Padding.PKCS5Padding, CommonConstant.LOCK_KEY.getBytes(StandardCharsets.UTF_8));
}
return aes;
}
/**
* 再次进行密码加密
* @param password 前端传输解密密码
* @param userCd salt
* @return 加密后密码
*/
public String encryptForDB(String password, String userCd) {
// 获取客户名称作为密码salt
password = SecureUtil.md5(SecureUtil.md5(StrUtil.format(CommonConstant.TWO_STR_APPEND_TEMPLATE,
password,
SecureUtil.md5(userCd))));
return password;
}
/**
* aes(base64(password))解密
* @param encryptPassword 解密的字符串
* @return 解密后信息
*/
public String decryptPassword(String encryptPassword) {
// 默认为空
byte[] decrypt = null;
try {
decrypt = PasswordOptionUtil.getAesInstance().decrypt(encryptPassword);
} catch (Exception exception) {
exception.printStackTrace();
log.error("解析登录密码异常,密码为 [{}]", encryptPassword);
}
if (ObjectUtil.isNull(decrypt)) {
return CommonConstant.EMPTY_STRING;
}
return new String(decrypt);
}
}
4.3 用户密码比对成功后岗位token生成
此处为用户名称密码验证通过后,进行到用户岗位信息以及token信息生成,并且生成token信息存入到redis中,后续为前端访问后台的登录标识
package cn.git.auth.security.handler;
import cn.git.auth.custom.Organization;
import cn.git.auth.custom.OrganizationPositionRelation;
import cn.git.auth.service.HomeService;
import cn.git.auth.util.UaaConstant;
import cn.git.common.constant.CacheManageConstant;
import cn.git.common.constant.CurrentUserInfoConstant;
import cn.git.common.constant.HeaderConstant;
import cn.git.common.result.Result;
import cn.git.common.result.ResultStatusEnum;
import cn.git.common.util.FastJsonUtil;
import cn.git.common.util.UaaContext;
import cn.git.redis.RedisUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @program: bank-credit-sy
* @description: 登录成功处理类
* @author: lixuchun
* @create: 2021-02-05 16:26
*/
@Component
public class AuthSuccessHandler implements AuthenticationSuccessHandler {
/**
* 登录柜员信息头标识
*/
private static final String USER_CD_FLAG = "userCd";
/**
* 用户名称
*/
private static final String USER_NAME_FLAG = "userName";
/**
* 机构号
*/
private static final String ORG_CD_FLAG = "orgCd";
/**
* 机构名称
*/
private static final String ORG_NAME_FLAG = "orgName";
/**
* 登录Token信息
*/
private static final String HEADER_TOKEN = "X_Token";
/**
* token 前缀
*/
private static final String CURRENT_LOGIN_PREFIX = "LOGIN_TOKEN";
@Value("${spring.profiles.active}")
private String profile;
/**
* 生产环境
*/
private static final String ENV_PROD = "prod";
@Autowired
private RedisUtil redisUtil;
@Autowired
private HomeService homeService;
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
Authentication authentication) throws IOException {
// 返回信息给前端
Result result;
// 登录成功时返回json格式数据
PrintWriter out = null;
try {
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
httpServletResponse.setStatus(HttpServletResponse.SC_OK);
// 获取用户信息
String user = authentication.getName();
Map<Object, Object> redisData = FastJsonUtil.toBean(user, Map.class);
// 判断当前登录柜员是否已经登录,如果登录则踢出登录状态,当前操作线程重新登录
if (profile.contains(ENV_PROD)) {
this.doubleLoginOption(redisData.get(CurrentUserInfoConstant.USER_CD).toString());
}
// 查看当前登录柜员状态是否正常
if (ObjectUtil.isNotNull(redisData.get(CurrentUserInfoConstant.USER_STATUS))) {
String userStatus = redisData.get(CurrentUserInfoConstant.USER_STATUS).toString();
if (!CurrentUserInfoConstant.USER_NORMAL_STATUS_CODE.equals(userStatus)) {
result = Result.error("当前用户状态非法,登录失败!");
httpServletResponse.setContentType(HeaderConstant.APPLICATION_JSON_UTF8_VALUE);
out = httpServletResponse.getWriter();
out.write(JSONObject.toJSONString(result));
return;
}
} else {
result = Result.error("获取用户状态信息失败,请联系管理员!");
httpServletResponse.setContentType(HeaderConstant.APPLICATION_JSON_UTF8_VALUE);
out = httpServletResponse.getWriter();
out.write(JSONObject.toJSONString(result));
return;
}
// 获取信贷机构信息,风险则获取机构以及对应机构向下岗位信息
List<Organization> organizationDTOList = null;
List<OrganizationPositionRelation> relationList = null;
if (UaaConstant.SYSTEM_RISK.equals(UaaContext.getSystemFlag())) {
redisData.put(UaaConstant.IF_RISK_FLAG, UaaConstant.SYSTEM_RISK);
relationList = homeService.selectOrganizationPositionRelation(
redisData.get(CurrentUserInfoConstant.USER_CD).toString(),
UaaContext.getSystemFlag());
// 判断如果机构信息为空 则抛出异常
if (ObjectUtil.isEmpty(relationList)) {
result = Result.error("获取登录用户机构信息失败,请确认!");
httpServletResponse.setContentType(HeaderConstant.APPLICATION_JSON_UTF8_VALUE);
out = httpServletResponse.getWriter();
out.write(JSONObject.toJSONString(result));
return;
}
} else {
redisData.put(UaaConstant.IF_RISK_FLAG, UaaConstant.SYSTEM_LOAN);
organizationDTOList = homeService.selectOrganizationInfo(
redisData.get(CurrentUserInfoConstant.USER_CD).toString());
// 判断如果机构信息为空 则抛出异常
if (ObjectUtil.isEmpty(organizationDTOList)) {
result = Result.error("获取登录用户机构信息失败,请确认!");
httpServletResponse.setContentType(HeaderConstant.APPLICATION_JSON_UTF8_VALUE);
out = httpServletResponse.getWriter();
out.write(JSONObject.toJSONString(result));
return;
}
}
// 生成token,并且存储token信息
String token = StrUtil.concat(true, CacheManageConstant.LOGIN_TOKEN_PREFIX, IdUtil.simpleUUID());
redisData.put(HeaderConstant.HEADER_TOKEN, token);
// 保存信息到redis
boolean setResult = redisUtil.hmset(token, redisData, CacheManageConstant.LOGIN_TIMEOUT);
if (setResult) {
// redis保存成功
JSONObject returnData = new JSONObject();
returnData.put(HeaderConstant.HEADER_TOKEN, token);
if (UaaConstant.SYSTEM_RISK.equals(UaaContext.getSystemFlag())) {
returnData.put(CurrentUserInfoConstant.ORGANIZATION, relationList);
} else {
returnData.put(CurrentUserInfoConstant.ORGANIZATION, organizationDTOList);
}
result = Result.ok(returnData);
} else {
// redis保存失败
result = Result.error(ResultStatusEnum.TOKEN_CREATE_ERROR);
}
httpServletResponse.setContentType(HeaderConstant.APPLICATION_JSON_UTF8_VALUE);
out = httpServletResponse.getWriter();
out.write(JSONObject.toJSONString(result));
} finally {
if (ObjectUtil.isNotNull(out)) {
out.flush();
out.close();
}
UaaContext.removeSystemFlag();
UaaContext.removeUserCd();
}
}
/**
* 如果当前账号已经处于登录状态,原登录状态强制下线,当前登录生效
* @param userCd 柜员编号
*/
public void doubleLoginOption(String userCd) {
// 获取登录信息字段集合
byte[][] fieldArray = {USER_CD_FLAG.getBytes(StandardCharsets.UTF_8),
USER_NAME_FLAG.getBytes(StandardCharsets.UTF_8),
ORG_CD_FLAG.getBytes(StandardCharsets.UTF_8),
ORG_NAME_FLAG.getBytes(StandardCharsets.UTF_8),
HEADER_TOKEN.getBytes(StandardCharsets.UTF_8),
UaaConstant.IF_RISK_FLAG.getBytes(StandardCharsets.UTF_8)};
List<Object> onlineUserList = redisUtil.getPrefixKeys(CURRENT_LOGIN_PREFIX, fieldArray);
// 删除登录柜员
if (ObjectUtil.isNotEmpty(onlineUserList)) {
// 设置在线人数信息
AtomicInteger onlineNum = new AtomicInteger(onlineUserList.size());
// 重复柜员登录,剔除上一登录柜员
onlineUserList.forEach(user -> {
List<Object> userFieldList = (List<Object>) user;
// 系统标识
String systemFlag = UaaContext.getSystemFlag();
if (userCd.equals(StrUtil.toString(userFieldList.get(UaaConstant.NUM_0)))
&& (StrUtil.isNotBlank(systemFlag)
&& systemFlag.equals(StrUtil.toString(userFieldList.get(UaaConstant.NUM_5))))) {
if (ObjectUtil.isNotEmpty(userFieldList.get(UaaConstant.NUM_4))) {
// 前一相同柜员登出
redisUtil.del((String) userFieldList.get(UaaConstant.NUM_4));
onlineNum.set(onlineNum.get() - 1);
}
}
});
// 记录当前在线人数信息 todo: 无需登录修改在线人数,只需定时任务修改即可
homeService.updateOnlineUserNumber(onlineNum.addAndGet(UaaConstant.NUM_1), null);
}
}
}
4.4 用户登录失败处理
如果用户登录失败,则会调用该类进行处理
package cn.git.auth.security.handler;
import cn.git.auth.util.UaaConstant;
import cn.git.common.constant.HeaderConstant;
import cn.git.common.result.Result;
import cn.git.common.result.ResultStatusEnum;
import cn.git.common.util.UaaContext;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @program: bank-credit-sy
* @description: 登录失败处理类
* @author: lixuchun
* @create: 2021-02-05 16:26
*/
@Component
public class AuthFailureHandler implements AuthenticationFailureHandler {
@Autowired
private HttpServletRequest request;
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
// 登录失败时返回json格式的提示
PrintWriter out = null;
try {
httpServletResponse.setContentType(HeaderConstant.APPLICATION_JSON_UTF8_VALUE);
httpServletResponse.setStatus(HttpServletResponse.SC_OK);
// 监管账户登录校验失败提示
ResultStatusEnum resultStatusEnum = ResultStatusEnum.USERNAME_PASSWORD_ERROR;
String systemFlag = request.getHeader(UaaConstant.SYSTEM_FLAG);
if (UaaConstant.SYSTEM_SUPERVISION.equals(systemFlag)) {
resultStatusEnum = ResultStatusEnum.USER_SUPERVISION_FAIL;
UaaContext.removeSystemFlag();
UaaContext.removeUserCd();
}
Result result = Result.error(resultStatusEnum.getMsg());
out = httpServletResponse.getWriter();
out.write(JSONObject.toJSONString(result));
}finally {
if(ObjectUtil.isNotNull(out)){
out.flush();
out.close();
}
}
}
}
4.5 权限资源处理
当前系统为微服务,登录模块与其他业务子模块分离,此权限处理起不到作用,也写上凑个字数吧
package cn.git.auth.security.handler;
import cn.git.common.constant.HeaderConstant;
import cn.git.common.result.Result;
import cn.git.common.result.ResultStatusEnum;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSONObject;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @program: bank-credit-sy
* @description: 无权访问处理类
* @author: liudong
* @create: 2021-02-06 12:26
*/
@Component
public class AuthAccessDeniedHandler implements AuthenticationEntryPoint, AccessDeniedHandler {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
// 未登录无权访问
this.setResponse(httpServletResponse);
}
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
// 登录后无权访问
this.setResponse(httpServletResponse);
}
/**
* 设置无权访问响应
* @param httpServletResponse 响应
* @throws IOException
*/
private void setResponse(HttpServletResponse httpServletResponse) throws IOException {
PrintWriter out = null;
try {
httpServletResponse.setContentType(HeaderConstant.APPLICATION_JSON_UTF8_VALUE);
httpServletResponse.setStatus(HttpServletResponse.SC_OK);
Result result = Result.error(ResultStatusEnum.USER_NOT_AUTH);
out = httpServletResponse.getWriter();
out.write(JSONObject.toJSONString(result));
}finally {
if(ObjectUtil.isNotNull(out)){
out.flush();
out.close();
}
}
}
}
4.6 登出处理
退出系统调用此方法,去除对应的token信息,前端再访问则无效
package cn.git.auth.security.handler;
import cn.git.auth.service.HomeService;
import cn.git.common.constant.HeaderConstant;
import cn.git.common.result.Result;
import cn.git.redis.RedisUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @program: bank-credit-sy
* @description: 退出成功处理类
* @author: liudong
* @create: 2021-02-06 12:41
*/
@Component
public class AuthLogoutSuccessHandler implements LogoutSuccessHandler {
/**
* 登录柜员信息头标识
*/
private static final String USER_CD_FLAG = "userCd";
/**
* 用户名称
*/
private static final String USER_NAME_FLAG = "userName";
/**
* 机构号
*/
private static final String ORG_CD_FLAG = "orgCd";
/**
* 机构名称
*/
private static final String ORG_NAME_FLAG = "orgName";
/**
* 登录Token信息
*/
private static final String HEADER_TOKEN = "X_Token";
/**
* token 前缀
*/
private static final String CURRENT_LOGIN_PREFIX = "LOGIN_TOKEN";
@Autowired
private RedisUtil redisUtil;
@Autowired
private HomeService homeService;
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
// 登录成功时返回json格式的提示
PrintWriter out = null;
try {
final String token = httpServletRequest.getHeader(HeaderConstant.HEADER_TOKEN);
//删除token
redisUtil.del(token);
// 获取当前在线人数 todo: 只使用定时任务修改无关紧要在线人数,无需登录登出修改
byte[][] fieldArray = {USER_CD_FLAG.getBytes(StandardCharsets.UTF_8),
USER_NAME_FLAG.getBytes(StandardCharsets.UTF_8),
ORG_CD_FLAG.getBytes(StandardCharsets.UTF_8),
ORG_NAME_FLAG.getBytes(StandardCharsets.UTF_8),
HEADER_TOKEN.getBytes(StandardCharsets.UTF_8)};
List<Object> onlineUserList = redisUtil.getPrefixKeys(CURRENT_LOGIN_PREFIX, fieldArray);
if (ObjectUtil.isNotNull(onlineUserList)) {
// 记录在线人数信息到人数表
homeService.updateOnlineUserNumber(onlineUserList.size(), null);
}
httpServletResponse.setContentType(HeaderConstant.APPLICATION_JSON_UTF8_VALUE);
httpServletResponse.setStatus(HttpServletResponse.SC_OK);
Result result = Result.ok();
out = httpServletResponse.getWriter();
out.write(JSONObject.toJSONString(result));
}finally {
if(ObjectUtil.isNotNull(out)){
out.flush();
out.close();
}
}
}
}
4.7 基础组件类注入到Security过滤器链中
此类为security的配置整合类,将各个基础组件类注入到链儿中,实现登录的整体逻辑,配置具体登录的登录路径信息,登录的用户名称字段密码字段分别是什么,那些路径无需登录即可访问,登出路径等等等。。。。
package cn.git.auth.security;
import cn.git.auth.filter.CustomAuthenticationFilter;
import cn.git.auth.security.handler.AuthAccessDeniedHandler;
import cn.git.auth.security.handler.AuthFailureHandler;
import cn.git.auth.security.handler.AuthLogoutSuccessHandler;
import cn.git.auth.security.handler.AuthSuccessHandler;
import cn.git.common.constant.CurrentUserInfoConstant;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
/**
* @program: bank-credit-sy
* @description: security配置类
* @author: liudong
* @create: 2021-02-05 15:51
*/
@Configuration
@EnableWebSecurity
public class GitSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private AuthAccessDeniedHandler accessDeniedHandler;
@Autowired
private AuthSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthFailureHandler authenticationFailureHandler;
@Autowired
private AuthLogoutSuccessHandler logoutSuccessHandler;
@Autowired
private AuthUserDetailsServiceImpl userDetailsService;
@Autowired
private AuthPasswordEncoder authPasswordEncoder;
/**
* 登录过滤链配置
* 统一处理失败异常
* failureUrl 登录异常处理 暂时注释
* @param http 请求信息
* @throws Exception 异常
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 未登录处理
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
.and().formLogin()
// 配置登录表单action post请求地址
.loginProcessingUrl(CurrentUserInfoConstant.LOGIN_URL)
// .failureUrl(CurrentUserInfoConstant.LOGIN_ERROR_URL)
.failureHandler(authenticationFailureHandler)
.successHandler(authenticationSuccessHandler)
// 指定表单用户名密码的key(默认为username password)
.usernameParameter(CurrentUserInfoConstant.USERNAME).passwordParameter(CurrentUserInfoConstant.PASSWORD)
// 登录操作无条件允许访问
.permitAll()
// 设置访问权限
.and().authorizeRequests()
.antMatchers(CurrentUserInfoConstant.HOME_URL,
CurrentUserInfoConstant.MONITOR_URL).permitAll()
// 其余的登录后就能访问
.anyRequest().authenticated()
// 登录后无权访问处理
.and().exceptionHandling().accessDeniedHandler(accessDeniedHandler)
// 退出成功处理
.and().logout().logoutUrl(CurrentUserInfoConstant.LOGOUT_URL).logoutSuccessHandler(logoutSuccessHandler).permitAll();
// 开启模拟请求(例如API POST测试工具的测试)
http.csrf().disable();
//加一个filter 获取body里登录信息
http.addFilterAt(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
// 前后端分离不创建不使用session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
/**
* 使用自定义类
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(authPasswordEncoder);
}
/**
* spring security处理json格式登录
* @return CustomAuthenticationFilter 过滤器
* @throws Exception 异常
*/
@Bean
CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
// 登录成功处理
filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
// 登录失败处理
filter.setAuthenticationFailureHandler(authenticationFailureHandler);
//将存有的身份信息传进去
filter.setAuthenticationManager(authenticationManagerBean());
return filter;
}
}
4.8 api组件信息核心类
此组件信息由其他子模块引入,主要包含了security 配置类,配置权限失败对应的处理逻辑,以及具体验签逻辑。
4.8.1 配置类
由于项目为微服务项目,登录与业务模块分开,所以需要重新编写security组件的权限验证逻辑,新增权限配置类到业务服务中,才能实现security的权限校验
package cn.git.security.config;
import cn.git.common.constant.CurrentUserInfoConstant;
import cn.git.security.filter.SecurityFilter;
import cn.git.security.point.SecurityAuthenticationEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* @program: bank-credit-sy
* @description: security配置类
* @author: dixl
* @create: 2021-05-11
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
SecurityAuthenticationEntryPoint securityAuthenticationEntryPoint;
@Autowired
SecurityFilter securityFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 禁止csrf
.csrf().disable()
// 非Session管理方式
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 开启认证
.authorizeRequests()
.antMatchers(CurrentUserInfoConstant.ALL_URL).permitAll()
.anyRequest().authenticated();
// 异常处理
http.exceptionHandling().authenticationEntryPoint(securityAuthenticationEntryPoint);
http.addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class);
}
}
4.8.2 权限验证过滤器
所有的请求都需要走此过滤器,获取登录用户的权限信息
package cn.git.security.filter;
import cn.git.common.constant.CurrentUserInfoConstant;
import cn.git.common.constant.HeaderConstant;
import cn.git.redis.RedisUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
/**
* @program: bank-credit-sy
* @description: 权限过滤器
* @author: dixl
* @create: 2021-05-11
*/
@Slf4j
@Component
public class SecurityFilter extends OncePerRequestFilter {
@Autowired
private RedisUtil redisUtil;
/**
* 认证授权
*/
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
try {
final String token = httpServletRequest.getHeader(HeaderConstant.HEADER_TOKEN);
if (StrUtil.isNotBlank(token)) {
log.info("访问的链接是:{}", httpServletRequest.getRequestURL());
Map<Object, Object> userMap = redisUtil.hmget(token);
if (MapUtil.isNotEmpty(userMap)) {
Object name = userMap.get(CurrentUserInfoConstant.NAME);
Object role = userMap.get(CurrentUserInfoConstant.ROLE);
//将主体放入内存
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(name, null, AuthorityUtils.createAuthorityList(Convert.toStrArray(role)));
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
//放入内存中去
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} catch (Exception e) {
log.error("认证授权时出错:{}", Arrays.toString(e.getStackTrace()));
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
4.8.3 权限校验失败处理
此处处理权限验证失败情况
package cn.git.security.point;
import cn.git.common.constant.HeaderConstant;
import cn.git.common.result.Result;
import cn.git.common.result.ResultStatusEnum;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSONObject;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Serializable;
/**
* @program: bank-credit-sy
* @description: 无权访问处理类
* @author: dixl
* @create: 2021-05-11
*/
@Component
public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -2433353509733499340L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
// 登录后无权访问
this.setResponse(response);
}
/**
* 设置无权访问响应
*
* @param response 响应
* @throws IOException
*/
private void setResponse(HttpServletResponse response) throws IOException {
PrintWriter out = null;
try {
response.setContentType(HeaderConstant.APPLICATION_JSON_UTF8_VALUE);
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
Result result = Result.error(ResultStatusEnum.USER_NOT_AUTH);
out = response.getWriter();
out.write(JSONObject.toJSONString(result));
}finally {
if(ObjectUtil.isNotNull(out)){
out.flush();
out.close();
}
}
}
}
4.8.4 权限校验失败异常捕获
统一捕获
package cn.git.security.exception;
import cn.git.common.result.Result;
import cn.git.common.result.ResultStatusEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* @program: bank-credit-sy
* @description: 权限异常捕获
* @author: dixl
* @create: 2021-05-11
*/
@Slf4j
@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SecurityExceptionHandler {
/**
* 捕获Security的AccessDeniedException
*/
@ExceptionHandler({AccessDeniedException.class})
@ResponseStatus(HttpStatus.FORBIDDEN)
public Result<Object> accessDeniedException(AccessDeniedException exception) {
log.error("权限异常捕获:{}",exception);
return Result.error(ResultStatusEnum.USER_NOT_AUTH);
}
}
4.8.5 验签使用
注解:@PreAuthorize(“hasRole(‘ROLE_USER’)”) 使用最广泛的注解,决定该方法视口可以被该调用,添加到接口上则该接口只能被权限为ROLE_USER的用户才能调用
FOR EXMAPLE:
@PreAuthorize("hasRole('ROLE_USER')")
public void doSomething();
有多重注解表达式,含义如下
表达式 | 说明 |
---|---|
hasRole([role]) | 用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀) |
hasAnyRole([role1,role2]) | 用户拥有任意一个制定的角色时返回true |
hasAuthority([authority]) | 等同于hasRole,但不会带有ROLE_前缀 |
asAnyAuthority([auth1,auth2]) | 等同于hasAnyRole |
permitAll | 永远返回true |
denyAll | 永远返回false |
authentication | 当前登录用户的authentication对象 |
fullAuthenticated | 当前用户既不是anonymous也不是rememberMe用户时返回true |
hasIpAddress(‘192.168.1.0/24’)) | 请求发送的IP匹配时返回true |
5. 动态数据源处理
整个过程需要使用动态数据源,使用AbstractRoutingDataSource实现即可,主要是配置多个数据源,多实例话数据源信息,注入到spring容器之中,然后再使用的时候通过固定规则进行切换,切换过程使用ThreadLocal没有多线程切换的影响,具体实现如下
5.1 数据源切换操作类
数据源的切换操作类,ThreadLocal保证线程切换,对其他线程数据源无影响
package cn.git.auth.config;
/**
* 数据源操作类
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2021-04-16
*/
public class DbContextHolder {
/**
* 当前线程
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 设置数据源
* @param dbName 数据源枚举类型
*/
public static void setDbType(String dbName) {
CONTEXT_HOLDER.set(dbName);
}
/**
* 获取当前线程设置数据源
*/
public static String getDbType() {
return CONTEXT_HOLDER.get();
}
/**
* 清除上下文数据
*/
public static void clearDbType() {
CONTEXT_HOLDER.remove();
}
}
5.2 动态数据源决策类
多数据源的核心类,主要继承AbstractRoutingDataSource类,实现多数据源的封装构建
package cn.git.auth.config;
import cn.hutool.core.util.ObjectUtil;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* 动态数据源决策类
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2021-04-16
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 私有对象
*/
private static DynamicDataSource dynamicDataSource = null;
/**
* 构造方法
*/
private DynamicDataSource() {
}
/**
* 获取数据源对象
* @return dynamicDataSource 动态数据源对象
*/
public static synchronized DynamicDataSource getInstance() {
if (ObjectUtil.isNull(dynamicDataSource)) {
dynamicDataSource = new DynamicDataSource();
}
return dynamicDataSource;
}
/**
* 动态数据源决策
*/
@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder.getDbType();
}
}
5.3 多数据源初始化
获取配置信息中的多数据源信息,并且初始化到DynamicDataSource中,我们使用的是nacos配置
package cn.git.auth.config;
import cn.git.auth.util.BusinessModuleType;
import cn.git.auth.util.UaaConstant;
import com.alibaba.druid.pool.DruidDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 动态数据源配置信息
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2021-06-23
*/
@Slf4j
@Configuration
public class DynamicDataSourceConfig {
@Autowired
private DataSource dataSource;
@Autowired
private BusinessModuleType businessModuleType;
/**
* 动态数据源配置
* @return DataSource 动态数据源
*/
@Primary
@Bean
public DataSource multipleDataSource() {
// 从获取动态数据源配置信息
List<BusinessModuleType.Module> moduleList = businessModuleType.getModules();
DynamicDataSource dynamicDataSource = DynamicDataSource.getInstance();
Map<Object, Object> targetDataSources = new HashMap<>(UaaConstant.DEFAULT_MAP_SIZE);
moduleList.forEach(module -> {
DruidDataSource moduleDataSource = new DruidDataSource();
// 设置密码用户错误重连次数1次
moduleDataSource.setConnectionErrorRetryAttempts(UaaConstant.ERROR_RETRY_ATTEMPTS);
moduleDataSource.setBreakAfterAcquireFailure(true);
moduleDataSource.setDriverClassName(module.getClassName());
moduleDataSource.setUrl(module.getUrl());
moduleDataSource.setUsername(module.getUsername());
moduleDataSource.setPassword(module.getPassword());
moduleDataSource.setInitialSize(module.getInitialSize());
moduleDataSource.setMinIdle(module.getMinSize());
moduleDataSource.setMaxActive(module.getMaxSize());
moduleDataSource.setMaxWait(module.getWaitTime());
moduleDataSource.setTimeBetweenEvictionRunsMillis(module.getTimeBetweenEvictionRunsMillis());
moduleDataSource.setMinEvictableIdleTimeMillis(module.getMinEvictableIdleTimeMillis());
moduleDataSource.setValidationQuery(module.getValidation());
if (UaaConstant.DEFAULT_FLAG.equals(module.getName())) {
targetDataSources.put(module.getDb(), dataSource);
} else {
targetDataSources.put(module.getDb(), moduleDataSource);
}
});
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(dataSource);
return dynamicDataSource;
}
}
5.4 其他工具类以及nacos配置信息
package cn.git.auth.util;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.List;
/**
* 定义模块类,信息从nacos中获取
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2021-04-25
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "business")
public class BusinessModuleType {
/**
* 映射模块列表
*/
private List<Module> modules;
/**
* 通用模块配置类
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2021-4-25
*/
@Data
public static class Module {
/**
* 模块名称
*/
private String name;
/**
* 模块描述
*/
private String desc;
/**
* 模块数据源
*/
private String db;
/**
* 驱动名称
*/
private String className;
/**
* 数据库链接url
*/
private String url;
/**
* 数据库链接url
*/
private String username;
/**
* 数据库链接url
*/
private String password;
/**
* 初始化连接池大小
*/
private Integer initialSize;
/**
* 最小连接池数量
*/
private Integer minSize;
/**
* 最大连接池数量
*/
private Integer maxSize;
/**
* 获取连接时最大等待时间,单位毫秒。
* 配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,
* 如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
*/
private Integer waitTime;
/**
* Destroy线程会检测连接的间隔时间
* 如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接
*/
private Integer timeBetweenEvictionRunsMillis;
/**
* 连接保持空闲而不被驱逐的最长时间
*/
private Integer minEvictableIdleTimeMillis;
/**
* 校验sql
*/
private String validation;
}
}
nacos的配置信息,其中对应的信息都是统一配置到配置类中的,各个子模块都是使用${xxx}这种形式获取
server:
port: ${git.uaa-server.port}
spring:
datasource:
druid:
driver-class-name: oracle.jdbc.OracleDriver
url: ${git.oracle.uaa-server.url}
username: ${git.oracle.uaa-server.username}
password: ${git.oracle.uaa-server.password}
# 初始化连接池的连接数量 大小,最小,最大
initial-size: 5
min-idle: 5
max-active: 20
# 配置获取连接等待超时的时间 毫秒
max-wait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 30000
validation-query: SELECT 1 FROM DUAL
business:
modules:
- name: MANAGE
desc: 公共管理模块
db: dataSource
className: oracle.jdbc.OracleDriver
url: ${git.oracle.manage-server.url}
username: ${git.oracle.manage-server.username}
password: ${git.oracle.manage-server.password}
initialSize: 5
minSize: 5
maxSize: 20
waitTime: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 30000
validation: SELECT 1 FROM DUAL
- name: RISK
desc: 风险模块
db: RISK_DB
className: oracle.jdbc.OracleDriver
url: ${git.oracle.risk-server.url}
username: ${git.oracle.risk-server.username}
password: ${git.oracle.risk-server.password}
initialSize: 5
minSize: 5
maxSize: 20
waitTime: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 30000
validation: SELECT 1 FROM DUAL
5.5 数据库切换方式
有两种切换方式,一种是切面,切固定方法,一种是配置类,在代码中使用,随用随切,代码如下
5.5.1 切面方法
注解类如下
package cn.git.auth.annotation;
import java.lang.annotation.*;
/**
* 数据源配置注解
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2021-04-16
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface DS {
}
切面类如下,使用ThreadLocal获取的request header一个参数systemFlag参数进行切库
package cn.git.auth.aspect;
import cn.git.auth.config.DbContextHolder;
import cn.git.auth.util.BusinessModuleType;
import cn.git.auth.util.UaaConstant;
import cn.git.common.util.UaaContext;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* 动态数据源切面类
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2021-04-16
*/
@Component
@Slf4j
@Aspect
public class DataSourceAspect {
@Autowired
private HttpServletRequest request;
/**
* 业务模块通用类
*/
@Autowired
private BusinessModuleType businessModuleType;
/**
* 数据库连接源准备变更
*/
@Before("@annotation(cn.git.auth.annotation.DS)")
public void beforeSwitchDataSource(JoinPoint joinPoint) {
// 判断调用方法对应数据源信息 线程没有提供系统标识 则从参数获取
String systemFlag = UaaContext.getSystemFlag();
if (StrUtil.isBlank(systemFlag)) {
systemFlag = request.getHeader(UaaConstant.SYSTEM_FLAG);
}
if (UaaConstant.SYSTEM_RISK.equals(systemFlag)) {
DbContextHolder.setDbType(UaaConstant.SYS_RISK);
}
}
/**
* 变更后恢复默认数据源
*/
@After("@annotation(cn.git.auth.annotation.DS)")
public void afterSwitchDataSource() {
DbContextHolder.clearDbType();
}
}
5.5.2 切换数据库
使用手动切换,随用随切,切换工具类如下
package cn.git.auth.util;
import cn.git.auth.config.DbContextHolder;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 切换数据库通用类
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2022-05-04
*/
@Component
public class DatabaseUtil {
/**
* 业务模块通用类
*/
@Autowired
private BusinessModuleType businessModuleType;
/**
* 切换数据库
* @param dataSourceTypeEnum 切换的数据库枚举
*/
public void chooseDB(DataSourceTypeEnum dataSourceTypeEnum) {
if (ObjectUtil.isNull(dataSourceTypeEnum)) {
throw new RuntimeException("规则引擎切换库操作,参数为空!");
}
List<BusinessModuleType.Module> moduleList = businessModuleType.getModules();
BusinessModuleType.Module businessModule = moduleList.stream().filter(module ->
dataSourceTypeEnum.getDbName().toUpperCase().startsWith(module.getName())
).findAny().orElse(null);
if (ObjectUtil.isNull(businessModule)) {
throw new RuntimeException(StrUtil.format("输入enum[{}]未配置对应数据库信息!",
dataSourceTypeEnum.getDbName()));
}
if (ObjectUtil.isNotNull(businessModule)) {
DbContextHolder.setDbType(businessModule.getDb());
}
}
/**
* 清理选择数据源,切换为默认数据源,本规则模块默认即manage模块
*/
public void clearToDefault() {
DbContextHolder.clearDbType();
}
}
切换使用代码如下
// 返回参数
HomeDTO homeDTO = new HomeDTO();
databaseUtil.chooseDB(DataSourceTypeEnum.RISK);
homeDTO.setPositionList(homeMapper.positionByPositionCd(positionCd));
// 获取登录信息
Object userCdObject = redisUtil.hmget(token, CurrentUserInfoConstant.USER_CD);
Object userNameObject = redisUtil.hmget(token, CurrentUserInfoConstant.USER_NAME);
String userName = StrUtil.toString(userNameObject);
String userCd = StrUtil.toString(userCdObject);
// 查询风险最高级别机构,获取跑批完成时间作为系统时间
databaseUtil.chooseDB(DataSourceTypeEnum.RISK);
QueryWrapper<TpScuOrganization> organizationQueryWrapper = new QueryWrapper<>();
organizationQueryWrapper.lambda().eq(TpScuOrganization::getOrgLevelCd, CurrentUserInfoConstant.BAT_DEFAULT_ORG_LEVEL);
TpScuOrganization organization = tpScuOrganizationMapper.selectOne(organizationQueryWrapper);
6. 最后进行简单测试
启动uaa服务,然后使用postman进行测试,测试环境使用的用户名密码如下
{
"username": "310907",
"password": "k9smtz4HKgOD47ef5SNbIQ=="
}
postman请求headersystemFlag=1时候,请求的结果如下
postman请求headersystemFlag=2时候,请求的结果如下