简单的spring security切库双系统登录

简单例子spring security双系统登录实现

1. 需求分析

现在有两个系统,分别为A系统以及B系统,两个系统对应的数据库为两个,用户菜单以及角色信息结构基本相同,现在想使用spring security实现两个服务的登录问题,通过一个固定标识,进行分库查询,登录不同的操作员信息,整体使用前后端分离方式,登录token信息存储在redis中。

2. 简单的实现步骤

  1. 分别编写密码校验比对,登录,登出,登录成功,权限控制等组件类
  2. 编写spring security基础配置框架SecurityConfig,实现WebSecurityConfigurerAdapter,注入之前编写对的各种组件到security过滤链中
  3. 编写动态切换数据源相关类
  4. 进行简单测试

3. 执行过程

  1. 前台登录的时候header中传递一个systemFLag标识,以及对应的用户密码(密码使用AES以及Base64进行加密)传递到后台,进行登录
  2. 后台接收到用户传入登录信息,获取header中系统信息,切换到对应系统的数据库,再判定传入用户是否存在,不存在返回用户名或密码错误,存在则继续往下执行
  3. 判定用户存在后,进行密码解密,解密后再进行md5加盐加密,与数据库中的密码进行比对,判定是否密码正确,正确往下执行,错误则返回用户名或密码错误
  4. 用户密码验证成功,则进行用户的岗位,机构信息加载(此处也需要切换到对应的库进行查询),并且加载后对的数据需要按照一定格式存储到redis中,生成对应的token信息。
  5. 最后将生成的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时候,请求的结果如下
在这里插入图片描述

  • 8
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot中使用Redisson换Redis数据是相对简单的。首先,您需要在您的项目中添加Redisson的依赖。在pom.xml文件中添加以下依赖项: ```xml <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.16.1</version> </dependency> ``` 接下来,您需要在application.properties或application.yml文件中配置Redis连接信息: ```properties spring.redis.host=127.0.0.1 spring.redis.port=6379 spring.redis.password= ``` 然后,在您的代码中,您可以使用`RedissonClient`接口来连接Redis,并使用`getBucket`方法来获取Redisson对象,从而实现换Redis数据。例如: ```java import org.redisson.api.RedissonClient; import org.redisson.api.RBucket; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class RedisService { @Autowired private RedissonClient redissonClient; public void switchDatabase(int databaseIndex) { redissonClient.getBucket("databaseIndex").set(databaseIndex); } public int getCurrentDatabase() { RBucket<Integer> bucket = redissonClient.getBucket("databaseIndex"); return bucket.get(); } // 其他操作方法... } ``` 在上面的示例代码中,`switchDatabase`方法用于换Redis数据,将数据索引存储在名为`databaseIndex`的Redis键中。`getCurrentDatabase`方法用于获取当前正在使用的数据索引。 这样,您就可以使用Redisson在Spring Boot中换Redis数据了。注意,Redis数据索引从0开始,您可以根据自己的需求进行调整。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值