模仿oauth2设计实现对老项目升级client

场景

有一个2018年的老项目,没有使用spring security和oauth2,现在有一个需求-“实现与crm系统数据的同步”。项目并没有针对第三方系统的安全鉴权,一切从零开始。
根据项目的登录接口查看有关 token 的生成和校验,摸清楚项目登录的 token 是根据随机数+用户hash值得到的,token相关信息保存在redis,由项目的拦截器实现对 token 的校验,并将用户基础信息保存到上下文中。

oauth2的client

oauth2有四种鉴权模式,密码模式,隐藏式,客户端模式,授权码模式,而客户端模式就符合系统之间的对接。
oauth2有两个关键的基础配置,一个是用户配置,另一个是客户端配置,而客户端模式主要使用到客户端配置,所以老项目可以创建自己的客户端配置,实现客户端模式。

老项目改造

目标

1,模仿oauth2给老项目加客户端模式,但是不能影响原来的登录和鉴权。
2,客户端模式支持数据持久化和新增
3,客户端模式的接口与普通用户的接口进行隔离

表设计

CREATE TABLE `client` (
  `id` bigint(20) NOT NULL,
  `app_id` varchar(255) NOT NULL COMMENT '账号',
  `app_secret` varchar(255) NOT NULL COMMENT '秘钥',
  `create_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `create_user_id` varchar(255) DEFAULT NULL,
  `update_user_id` varchar(255) DEFAULT NULL,
  `is_delete` tinyint(2) DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户端配置';

表相关的mybatis-plus配置

实体类

@Getter
@Setter
@Accessors(chain = true)
@TableName("client")
public class Client implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId("id")
    private Long id;

    /**
     * 账号
     */
    @TableField("app_id")
    private String appId;

    /**
     * 秘钥
     */
    @TableField("app_secret")
    private String appSecret;

    @TableField("create_time")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;

    @TableField("update_time")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date updateTime;

    @TableField("create_user_id")
    private String createUserId;

    @TableField("update_user_id")
    private String updateUserId;

    @TableField("is_delete")
    private Integer isDelete;
}

mapper接口

public interface ClientMapper extends BaseMapper<Client> {

}

xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zhang.product.plus.system.ClientMapper">

</mapper>

api

@PermissionMapping是原来项目的白名单注解,这里开放获取token的接口

@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/systemClient")
public class ClientController {

    private final IClientService clientService;

    /**
     * 客户端获取token
     *
     * @param in 客户端信息
     * @return token
     * @author zfj
     * @date 2024/7/31
     */
    @PermissionMapping(name = "客户端获取token", loginIntercept = false, isIntercept = false)
    @PostMapping("/getToken")
    public Output<String> getToken(@Valid @RequestBody ClientGetTokenReqDTO in) {

        return Output.success(clientService.getToken(in));
    }

    /**
     * 新增客户端
     *
     * @param in 客户端信息
     * @return 客户信息
     * @author zfj
     * @date 2024/7/31
     */
    @PostMapping("/addClient")
    public Output<PClient> addClient(@Valid @RequestBody ClientGetTokenReqDTO in) {

        return Output.success(clientService.addClient(in));
    }
}

接口

public interface IClientService {

    /**
     * 客户端获取token
     *
     * @param in 客户端信息
     * @return token
     * @author zfj
     * @date 2024/7/31
     */
    String getToken(ClientGetTokenReqDTO in);

    /**
     * 新增客户端
     *
     * @param in 客户端信息
     * @return 客户信息
     * @author zfj
     * @date 2024/7/31
     */
    PClient addClient(ClientGetTokenReqDTO in);
}

实现类

  • 这里addClient用于开发环境给客户端新增配置,并不开放出(由于数量少,无需做页面配置),所以并不做具体appId的校验。
  • 密码使用密文保存
  • authManager是项目原来的token管理
@Slf4j
@RequiredArgsConstructor
@Service
public class ClientServiceImpl extends ServiceImpl<ClientMapper, PClient> implements IClientService {

    private final AuthManager authManager;
    private final PasswordEncoder passwordEncoder;

    @Override
    public String getToken(ClientGetTokenReqDTO in) {

        PClient one = new LambdaQueryChainWrapper<>(this.getBaseMapper())
                .eq(PClient::getAppId,in.getAppId())
                .eq(PClient::getIsDelete, YesOrNoEnum.NO.getCode())
                .one();
        if(Objects.isNull(one)){
            KingHoodExceptionUtil.throwException("appId不正确");
        }

        boolean matches = passwordEncoder.matches(in.getAppSecret(), one.getAppSecret());
        if (!matches){
            KingHoodExceptionUtil.throwException("appSecret不正确");
        }

        CurrentUserExtInfo info = new CurrentUserExtInfo();
        info.setClient(true);
        info.setClientInfo(one);
        return authManager.getToken(in.getAppId(), JSON.toJSONString(info));
    }

    @Override
    public PClient addClient(ClientGetTokenReqDTO in) {

        PClient client = new PClient();
        client.setAppId(in.getAppId());
        client.setAppSecret(passwordEncoder.encode(in.getAppSecret()));
        client.setId(IdGenUtil.getId());
        client.setCreateTime(new Date());
        client.setCreateUserId(SystemUserUtil.getCurrentUser().getId());
        this.save(client);
        return client;
    }
}

authManager的getToken方法

  • createAccessToken方法自定义一个token生成规则即可,比如uuid+id,然后做MD5摘要
	/**
	 * 获取token并刷新缓存
	 * */
	public String getToken(String id, String info) {

		String token = redisClusterHelper.get(APP + id);
		if (StringUtils.isEmpty(token)) {
			token = createAccessToken(id);
		}
		int expire = 3600 * 24;
		redisClusterHelper.set(APP + id, token, expire);
		redisClusterHelper.set(APP + token, info, expire);
		return token;
	}

CurrentUserExtInfo 是上下文保存的数据,原来是采用json字符串作为value存储
在原来的上下文配置中新增了客户端的属性

	/**
     * 是否客户端
     * */
	private boolean client;
	/**
     * 客户端信息
     * */
	private PClient clientInfo;

拦截器兼容

拦截器主要做两件事,一个是对token进行校验,另一个是封装上下文,所以兼容处理做到以下几点
1,token校验兼容
2,上下文兼容
3,新增开放接口的识别

token校验兼容

  • 原来的token校验走 authManager的 isAuth方法

原来的代码是

	public boolean isAuth(String token) {
		boolean auth = true;
		String json = redisClusterHelper.get(user_+token);
		if(StringUtils.isEmpty(json)) return false;
		return auth;
	}
	

修改后的代码,用户登录缓存的key采用常量user_作为前缀,客户端采用常量APP作为前缀
代码大体上没有变动,新增了json = redisClusterHelper.get(APP+token);

	public boolean isAuth(String token) {
		String json = redisClusterHelper.get(user_+token);
		if(StringUtils.isEmpty(json)){
			json = redisClusterHelper.get(APP+token);
			if(Strings.isNullOrEmpty(json)){
				return false;
			}
		}
		return true;
	}

上下文保存兼容

  • 上下文的保存关键在于从缓存中获取到token的相关数据

原来的代码

    public CurrentUserExtInfo getUserInfo(String token) {
    	log.info("获取基础用户信息{}",token);
    	if(StringUtils.isBlank(token)){
			HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
					.getRequest();
			token = request.getHeader("Authorization");
			if(!AssertValue.isEmpty(token)){
				token = token.replace("Bearer ","");
			}
		}

    	if(StringUtils.isEmpty(token)) throw new MessageException(OutputCode.TX.getCode(), OutputCode.TX.getMessage());
    	String json = redisClusterHelper.get(user_+token);
    	if(StringUtils.isEmpty(json)) throw new MessageException(OutputCode.TX.getCode(), OutputCode.TX.getMessage());
		return JSON.parseObject(json,CurrentUserExtInfo.class);
    }

修改后的代码,大体上没有变,新增了 json = redisClusterHelper.get(APP+token);

    public CurrentUserExtInfo getUserInfo(String token) {
    	log.info("获取基础token信息{}",token);
    	if(StringUtils.isBlank(token)){
			HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
					.getRequest();
			token = request.getHeader("Authorization");
			if(!AssertValue.isEmpty(token)){
				token = token.replace("Bearer ","");
			}
		}

    	if(StringUtils.isEmpty(token)) throw new MessageException(OutputCode.TX.getCode(), OutputCode.TX.getMessage());
    	String json = redisClusterHelper.get(user_+token);
    	if(StringUtils.isEmpty(json)){
			json = redisClusterHelper.get(APP+token);
			if(Strings.isNullOrEmpty(json)){
				throw new MessageException(OutputCode.TX.getCode(), OutputCode.TX.getMessage());
			}
		}
		return JSON.parseObject(json,CurrentUserExtInfo.class);
    }

新增api接口的区分

自定义注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface OpenApi {

    String value() default "";

}

对接的接口上添加注解

    @OpenApi
    @PostMapping("/sync")
    public Output<Boolean> sync(@Valid @RequestBody UserReqDTO in) {

        return Output.success(userService.sync(in));
    }

拦截上针对该注解做处理
新增代码如下

OpenApi openApi = method.getAnnotation(OpenApi.class);


 else if(Objects.nonNull(openApi)){
				// 判断是否客户端token
				hasPermission = authManager.checkOpenApi(token);
			}

完整代码如下

	public boolean hasPermission(HttpServletRequest request, String token,Object handler) {
        boolean hasPermission = false;//默认无权限
		if (handler instanceof HandlerMethod) {
			HandlerMethod hm = (HandlerMethod) handler;
			Method method = hm.getMethod();

			PermissionMapping mm = method.getAnnotation(PermissionMapping.class);
			OpenApi openApi = method.getAnnotation(OpenApi.class);
			if (null != mm) {
				boolean isIntercept = mm.isIntercept();
				if (isIntercept) {//拦截
					String permissionKey = mm.key();
					String basePath = "";
					String nodePath = "";

					Object bean = hm.getBean();
					RequestMapping brm = bean.getClass().getAnnotation(RequestMapping.class);

					if (null != brm) {
						String[] paths = brm.value() == null ? brm.path() : brm.value();
						basePath = (null != paths && paths.length > 0) ? paths[0] : "";
					}
					RequestMapping nrm = method.getAnnotation(RequestMapping.class);
					if (null != nrm) {
						String[] paths = nrm.value() == null ? nrm.path() : nrm.value();
						nodePath = (null != paths && paths.length > 0) ? paths[0] : "";
					}
					String path = basePath + nodePath;
					if(StringUtils.isNotEmpty(path)){
						hasPermission = authManager.hasPermission(token, permissionKey);
					}
                }else{//不拦截
					hasPermission=true;//有权限
				}
            } else if(Objects.nonNull(openApi)){
				// 判断是否客户端token
				hasPermission = authManager.checkOpenApi(token);
			} else{
				String permissionKey = request.getServletPath();
				if(StringUtils.isNotEmpty(permissionKey)){
					hasPermission = authManager.hasPermission(token, permissionKey.substring(1));
				}
			}
			
			if(hasPermission && StringUtils.isNoneEmpty(token)) {
				authManager.putInfo(token);
			}
			
        }
        return hasPermission;
    }
}

  • 19
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值