您的位置:首页 > 娱乐 > 明星 > 模仿oauth2设计实现对老项目升级client

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

2025/2/27 23:56:31 来源:https://blog.csdn.net/weixin_45149147/article/details/140849412  浏览:    关键词:模仿oauth2设计实现对老项目升级client

文章目录

  • 场景
  • oauth2的client
  • 老项目改造
    • 目标
    • 表设计
      • 表相关的mybatis-plus配置
    • api
    • 拦截器兼容
      • token校验兼容
      • 上下文保存兼容
      • 新增api接口的区分

场景

有一个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;@Overridepublic 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));}@Overridepublic 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)){// 判断是否客户端tokenhasPermission = 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)){// 判断是否客户端tokenhasPermission = 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;}
}

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com