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

avatar
作者
猴君
阅读量:0

文章目录

场景

有一个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;     } }  

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!