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

文章目录

场景

有一个2018年的老项目,没有使用spring security和oauth2,现在有一个需求-"实现与crm系统数据的同步"。项目并没有针对第三方系统的安全鉴权,一切从零开始。

根据项目的登录接口查看有关 token 的生成和校验,摸清楚项目登录的 token 是根据随机数+用户hash值得到的,token相关信息保存在redis,由项目的拦截器实现对 token 的校验,并将用户基础信息保存到上下文中。

oauth2的client

oauth2有四种鉴权模式,密码模式,隐藏式,客户端模式,授权码模式,而客户端模式就符合系统之间的对接。

oauth2有两个关键的基础配置,一个是用户配置,另一个是客户端配置,而客户端模式主要使用到客户端配置,所以老项目可以创建自己的客户端配置,实现客户端模式。

老项目改造

目标

1,模仿oauth2给老项目加客户端模式,但是不能影响原来的登录和鉴权。

2,客户端模式支持数据持久化和新增

3,客户端模式的接口与普通用户的接口进行隔离

表设计

sql 复制代码
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配置

实体类

java 复制代码
@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接口

java 复制代码
public interface ClientMapper extends BaseMapper<Client> {

}

xml

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的接口

java 复制代码
@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));
    }
}

接口

java 复制代码
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管理
java 复制代码
@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摘要
java 复制代码
	/**
	 * 获取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存储

在原来的上下文配置中新增了客户端的属性

java 复制代码
	/**
     * 是否客户端
     * */
	private boolean client;
	/**
     * 客户端信息
     * */
	private PClient clientInfo;

拦截器兼容

拦截器主要做两件事,一个是对token进行校验,另一个是封装上下文,所以兼容处理做到以下几点

1,token校验兼容

2,上下文兼容

3,新增开放接口的识别

token校验兼容

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

原来的代码是

java 复制代码
	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);

java 复制代码
	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的相关数据

原来的代码

java 复制代码
    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);

java 复制代码
    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接口的区分

自定义注解

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface OpenApi {

    String value() default "";

}

对接的接口上添加注解

java 复制代码
    @OpenApi
    @PostMapping("/sync")
    public Output<Boolean> sync(@Valid @RequestBody UserReqDTO in) {

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

拦截上针对该注解做处理

新增代码如下

java 复制代码
OpenApi openApi = method.getAnnotation(OpenApi.class);


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

完整代码如下

java 复制代码
	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;
    }
}
相关推荐
有梦想的攻城狮9 小时前
spring中的@MapperScan注解详解
java·后端·spring·mapperscan
柚个朵朵10 小时前
Spring的Validation,这是一套基于注解的权限校验框架
java·后端·spring
程序员小杰@10 小时前
【MCP教程系列】SpringBoot 搭建基于 Spring AI 的 SSE 模式 MCP 服务
人工智能·spring boot·spring
程序员buddha11 小时前
Spring & Spring Boot 常用注解整理
java·spring boot·spring
C_V_Better12 小时前
Java Spring Boot 控制器中处理用户数据详解
java·开发语言·spring boot·后端·spring
LUCIAZZZ13 小时前
JVM之虚拟机运行
java·jvm·spring·操作系统·springboot
神秘的t14 小时前
Spring Web MVC————入门(2)
java·spring·mvc
冷心笑看丽美人14 小时前
Spring MVC数据绑定和响应 你了解多少?
java·spring·mvc
蒂法就是我17 小时前
详细说说Spring的IOC机制
java·后端·spring
唐僧洗头爱飘柔952718 小时前
【SSM-SpringMVC(二)】Spring接入Web环境!本篇开始研究SpringMVC的使用!SpringMVC数据响应和获取请求数据
java·spring·文件上传·页面跳转·数据响应·获取请求数据·静态资源访问