汇客huikeCRM项目实战-初出茅庐
來自編程小趴菜的分享~~
希望對你有所幫助~~
你的小小的贊就是對我最大的鼓勵~~
有不明白的地方可以私信解答~
文章目錄
任務一:運行后端代碼和前端代碼
任務二:技術調研,接口權限,數據權限控制,自定義注解打印日志等
前言
該文章記錄了匯客CRM項目實戰第一天可能出現的問題,以及一些技術方案
一、運行后端代碼和前端代碼
????????1、看代碼階段
注意:該階段主要是理清楚項目的層次結構,各個模塊的功能以及作用是什么,并且要明白數據庫表與表之間的關聯關系
?
? ? ? ? ?2、運行后端代碼
? ? ? ? ? ? ? ? a、打開項目首先修改一下maven的配置,然后設置一下jdk版本為1.8
? ? ? ? ? ? ? ? b、在huike-admin模塊下的application.yml文件中修改一下當前使用的redis的IP和地址,記得一定要先啟動redis,再啟動后端項目,不然會報redis連接異常的問題
? ? ? ? ? ? ? ? c、在huike-admin模塊下的application-druid.yml文件下修改一下數據庫的連接信息,改為自己的數據庫,然后啟動后端項目
? ? ? ? ? ? ? ? 這樣后端代碼就能跑起來了~~
? ? ? ? 3、運行前端代碼
? ? ? ? ? ? ? ? a、前置工作,記得先安裝一下node.js,附上百度網盤鏈接
鏈接:https://pan.baidu.com/s/1xV4tWIEa5RArc5DdUGpWIw?pwd=kvwm?
提取碼:kvwm?
? ? ? ? ? ? ? ? b、在前端代碼的文件夾下打開命令行窗口,運行以下命令
? ? ? ? ? ? ? ? 這樣前端代碼就能跑起來了~~
二、技術調研,自定義接口權限,自定義數據權限控制,自定義注解打印日志
? ? ? ? 1、自定義接口權限控制
通過自定義注解來完成接口權限控制,有使用到jwt相關技術,也可以使用ThreadLocal來完成
????????接口權限:顧名思義,配置不通角色調用接口的權限。有些敏感接口,是只能有固定的一些角色才能調用,普通角色是不能調用的。這種情況需要有一個明確的系統來控制對應的訪問權限。
????????接口權限系統,可以控制某些接口只能由固定的角色調用,可以動態控制不同的角色對不同接口的訪問權限。
????????通過接口配置實現,對接口的訪問權限控制和數據權限控制。
????????接口是REST接口,接口權限認證機制使用Json web token (JWT)。
????????接口權限調用流程:
(1)通過接口用戶的用戶名密碼,調用鑒權token接口獲取接口用戶的token,該token,2個小時內有效
(2)把獲取的token作為參數,調用接口的時候,會根據token去鑒權
(3)鑒權通過,接口會根據接口定義的編碼,檢驗是否有訪問權限,有則可以繼續訪問,無則提示訪問受限
(4)有訪問權限,則獲取接口的數據權限規則,根據授權的數據權限規則返回需要的數據
實現一個新的接口,無需關注token的鑒權機制,接口權限判斷方式
使用AOP實現接口攔截:@PreAuth
鑒權配置注解名稱為 @PreAuth ,在需要進行鑒權配置的方法加上 @PreAuth 注解,并在注解內寫 入相關的鑒權方法。
?????????具體實現方法:
@Aspect public class AuthAspect implements ApplicationContextAware {/*** 表達式處理*/private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();/*** 切 方法 和 類上的 @PreAuth 注解** @param point 切點* @return Object* @throws Throwable 沒有權限的異常*/@Around("@annotation(com.dindo.core.secure.annotation.PreAuth) || " +"@within(com.dindo.core.secure.annotation.PreAuth)")public Object preAuth(ProceedingJoinPoint point) throws Throwable {if (handleAuth(point)) {return point.proceed();}throw new SecureException(ResultCode.UN_AUTHORIZED);}/*** 處理權限** @param point 切點*/private boolean handleAuth(ProceedingJoinPoint point) {MethodSignature ms = (MethodSignature) point.getSignature();Method method = ms.getMethod();// 讀取權限注解,優先方法上,沒有則讀取類PreAuth preAuth = ClassUtil.getAnnotation(method, PreAuth.class);// 判斷表達式String condition = preAuth.value();if (StringUtil.isNotBlank(condition)) {Expression expression = EXPRESSION_PARSER.parseExpression(condition);// 方法參數值Object[] args = point.getArgs();StandardEvaluationContext context = getEvaluationContext(method, args);return expression.getValue(context, Boolean.class);}return false;}/*** 獲取方法上的參數** @param method 方法* @param args 變量* @return {SimpleEvaluationContext}*/private StandardEvaluationContext getEvaluationContext(Method method, Object[] args) {// 初始化Sp el表達式上下文,并設置 AuthFunStandardEvaluationContext context = new StandardEvaluationContext(new AuthFun());// 設置表達式支持spring beancontext.setBeanResolver(new BeanFactoryResolver(applicationContext));for (int i = 0; i < args.length; i++) {// 讀取方法參數MethodParameter methodParam = ClassUtil.getMethodParameter(method, i);// 設置方法 參數名和值 為sp el變量context.setVariable(methodParam.getParameterName(), args[i]);}return context;}private ApplicationContext applicationContext;@Overridepublic void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {this.applicationContext = applicationContext;}} public class AuthFun {/*** 權限校驗處理器*/private static IPermissionHandler permissionHandler;private static IPermissionHandler getPermissionHandler() {if (permissionHandler == null) {permissionHandler = SpringUtil.getBean(IPermissionHandler.class);}return permissionHandler;}/*** 判斷角色是否具有接口權限** @return {boolean}*/public boolean permissionAll() {return getPermissionHandler().permissionAll();}/*** 判斷角色是否具有接口權限** @param permission 權限編號* @return {boolean}*/public boolean hasPermission(String permission) {return getPermissionHandler().hasPermission(permission);}/*** 放行所有請求** @return {boolean}*/public boolean permitAll() {return true;}/*** 只有超管角色才可訪問** @return {boolean}*/public boolean denyAll() {return hasRole(RoleConstant.ADMIN);}/*** 是否已授權** @return {boolean}*/public boolean hasAuth() {return Func.isNotEmpty(AuthUtil.getUser());}/*** 是否有時間授權** @param start 開始時間* @param end 結束時間* @return {boolean}*/public boolean hasTimeAuth(Integer start, Integer end) {Integer hour = DateUtil.hour();return hour >= start && hour <= end;}/*** 判斷是否有該角色權限** @param role 單角色* @return {boolean}*/public boolean hasRole(String role) {return hasAnyRole(role);}/*** 判斷是否具有所有角色權限** @param role 角色集合* @return {boolean}*/public boolean hasAllRole(String... role) {for (String r : role) {if (!hasRole(r)) {return false;}}return true;}/*** 判斷是否有該角色權限** @param role 角色集合* @return {boolean}*/public boolean hasAnyRole(String... role) {BladeUser user = AuthUtil.getUser();if (user == null) {return false;}String userRole = user.getRoleName();if (StringUtil.isBlank(userRole)) {return false;}String[] roles = Func.toStrArray(userRole);for (String r : role) {if (CollectionUtil.contains(roles, r)) {return true;}}return false;}}????????使用方法:可以注釋到類上、方法上 ?
/** * 新增 */ @PostMapping("/save") @PreAuth("hasPermission(#test)") public R save(@RequestBody Blog blof){return R.status(service.save(blog)); }????????2、自定義數據權限控制
其基本原理同接口 權限控制,主要是判斷訪問該數據的用戶的權限是否足夠,依次來拒絕訪問,或者自定義sql查詢語句,返回部分數據
?????????所謂數據權限,就是有或者沒有對某些數據的訪問權限,具體表現形式就是當某用戶有操作權限的時候,但不代表其對所有的數據都有查看或者管理權限。
????????數據權限有兩種表現形式:一種是行權限、另外一種是列權限。
????????所謂行權限,就是限制用戶對某些行的訪問權限,比如:只能對本人、本部門、本組織的數據進行訪問;也可以是根據數據的范圍進行限制,比如:合同額大小來限制用戶對數據的訪問。
????????所謂列權限,就是限制用戶對某些列的訪問權限,比如:某些內容的摘要可以被查閱,但是詳細內容就只有VIP用戶才能查看。通過數據權限,可以從物理層級限制用戶對數據的行或列進行獲取。
????????再比如:同樣一個部門經理的角色,看到的數據是不一樣的,所以,牽扯到數據二字,就應該不和操作二字等同起來。所以我們是通過職位來解決數據權限的,職位也可以叫崗位,是和數據查看范圍有關系的,也就是組織結構里的關系。 所以在設計數據結構的時候,每個有數據范圍的數據實體,都需要具備數據擁有人的字段,比如A部門的小b同學,他創建的數據,只能由A部門的部門經理看到,而同樣在角色里具有部門經理的B部門的部門經理是不能看到的。所以延伸出來了一個設計思路,根據數據擁有人,圈定查看范圍。數據范圍的維度有:全部、本集團、本公司、本部門、自己,五個維度,可以滿足大部分業務場景。 還有一個維度是自定義維度,可以自定義機構進行設置。這樣的設計就達到了數據權限的操作靈活性。
????????核心注解:
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface DataAuth {/*** 資源編號*/String code() default "";/*** 數據權限對應字段*/String column() default DataScopeConstant.DEFAULT_COLUMN;/*** 數據權限規則*/DataScopeEnum type() default DataScopeEnum.ALL;/*** 可見字段*/String field() default "*";/*** 數據權限規則值域*/String value() default ""; }? ? ? ? 枚舉類:?
@Getter @AllArgsConStructor public enum DataScopeEnum{/*** 全部數據*/ALL(1,"全部");/*** 本人可見*/OWN(2,"本人可見");/*** 所在機構可見*/OWN_DEPT(3,"所在機構可見");/*** 所在機構及其子級可見*/OWN_DEPT_CHILD(4,"所在機構及其子級可見");/*** 自定義*/CUSTOM(5,"自定義");private final int type;private final String description; }????????目前的數據權限類型一共有五種,前面四種都是不需要自定義寫sql的,只有選擇了CUSTOM類型,才需要定義注解的value屬性。
????????注解默認過濾的字段名為create_dept,如果有修改,則需要定義對應的字段名。 ?
? ? ? ? ?數據權限攔截器配置:
/*** mybatis 數據權限攔截器* @author L.cm, Chill*/ @Slf4j @RequiredArgsConstructor @SuppressWarnings({"rawtypes"}) public class DataScopeInterceptor implements QueryInterceptor {private final ConcurrentMap<String, DataAuth> dataAuthMap = new ConcurrentHashMap<>(8);private final DataScopeHandler dataScopeHandler;private final DataScopeProperties dataScopeProperties;@Overridepublic void intercept(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {//未啟用則放行if (!dataScopeProperties.getEnabled()) {return;}//未取到用戶則放行BladeUser bladeUser = AuthUtil.getUser();if (bladeUser == null) {return;}if (SqlCommandType.SELECT != ms.getSqlCommandType() || StatementType.CALLABLE == ms.getStatementType()) {return;}String originalSql = boundSql.getSql();//查找注解中包含DataAuth類型的參數DataAuth dataAuth = findDataAuthAnnotation(ms);//注解為空并且數據權限方法名未匹配到,則放行String mapperId = ms.getId();String className = mapperId.substring(0, mapperId.lastIndexOf(StringPool.DOT));String mapperName = ClassUtil.getShortName(className);String methodName = mapperId.substring(mapperId.lastIndexOf(StringPool.DOT) + 1);boolean mapperSkip = dataScopeProperties.getMapperKey().stream().noneMatch(methodName::contains)|| dataScopeProperties.getMapperExclude().stream().anyMatch(mapperName::contains);if (dataAuth == null && mapperSkip) {return;}//創建數據權限模型DataScopeModel dataScope = new DataScopeModel();//若注解不為空,則配置注解項if (dataAuth != null) {dataScope.setResourceCode(dataAuth.code());dataScope.setScopeColumn(dataAuth.column());dataScope.setScopeType(dataAuth.type().getType());dataScope.setScopeField(dataAuth.field());dataScope.setScopeValue(dataAuth.value());}//獲取數據權限規則對應的篩選SqlString sqlCondition = dataScopeHandler.sqlCondition(mapperId, dataScope, bladeUser, originalSql);if (!StringUtil.isBlank(sqlCondition)) {PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);mpBoundSql.sql(sqlCondition);}}/*** 獲取數據權限注解信息** @param mappedStatement mappedStatement* @return DataAuth*/private DataAuth findDataAuthAnnotation(MappedStatement mappedStatement) {String id = mappedStatement.getId();return dataAuthMap.computeIfAbsent(id, (key) -> {String className = key.substring(0, key.lastIndexOf(StringPool.DOT));String mapperBean = StringUtil.firstCharToLower(ClassUtil.getShortName(className));Object mapper = SpringUtil.getBean(mapperBean);String methodName = key.substring(key.lastIndexOf(StringPool.DOT) + 1);Class<?>[] interfaces = ClassUtil.getAllInterfaces(mapper);for (Class<?> mapperInterface : interfaces) {for (Method method : mapperInterface.getDeclaredMethods()) {if (methodName.equals(method.getName()) && method.isAnnotationPresent(DataAuth.class)) {return method.getAnnotation(DataAuth.class);}}}return null;});}}? ? ? ? ?數據權限處規則:
/*** 默認數據權限規則* 獲取過濾sql* @param mapperId 數據查詢類* @param dataScope 數據權限類* @param bladeUser 當前用戶信息* @param originalSql 原始Sql* @author Chill*/ @RequiredArgsConstructor public class BladeDataScopeHandler implements DataScopeHandler {private final ScopeModelHandler scopeModelHandler;@Overridepublic String sqlCondition(String mapperId, DataScopeModel dataScope, BladeUser bladeUser, String originalSql) {//數據權限資源編號String code = dataScope.getResourceCode();//根據mapperId從數據庫中獲取對應模型DataScopeModel dataScopeDb = scopeModelHandler.getDataScopeByMapper(mapperId, bladeUser.getRoleId());//mapperId配置未取到則從數據庫中根據資源編號獲取if (dataScopeDb == null && StringUtil.isNotBlank(code)) {dataScopeDb = scopeModelHandler.getDataScopeByCode(code);}//未從數據庫找到對應配置則采用默認dataScope = (dataScopeDb != null) ? dataScopeDb : dataScope;//判斷數據權限類型并組裝對應SqlInteger scopeRule = Objects.requireNonNull(dataScope).getScopeType();DataScopeEnum scopeTypeEnum = DataScopeEnum.of(scopeRule);List<Long> ids = new ArrayList<>();String whereSql = "where scope.{} in ({})";//需要注意的是,下面的這個判斷,如果角色是ADMINISTRATOR的話也是不執行直接返回null的,我就是在這里掉坑if (DataScopeEnum.ALL == scopeTypeEnum || StringUtil.containsAny(bladeUser.getRoleName(), RoleConstant.ADMINISTRATOR)) {return null;} else if (DataScopeEnum.CUSTOM == scopeTypeEnum) {whereSql = PlaceholderUtil.getDefaultResolver().resolveByMap(dataScope.getScopeValue(), BeanUtil.toMap(bladeUser));} else if (DataScopeEnum.OWN == scopeTypeEnum) {ids.add(bladeUser.getUserId());} else if (DataScopeEnum.OWN_DEPT == scopeTypeEnum) {ids.addAll(Func.toLongList(bladeUser.getDeptId()));} else if (DataScopeEnum.OWN_DEPT_CHILD == scopeTypeEnum) {List<Long> deptIds = Func.toLongList(bladeUser.getDeptId());ids.addAll(deptIds);deptIds.forEach(deptId -> {List<Long> deptIdList = scopeModelHandler.getDeptAncestors(deptId);ids.addAll(deptIdList);});}return StringUtil.format(" select {} from ({}) scope " + whereSql, Func.toStr(dataScope.getScopeField(), "*"), originalSql, dataScope.getScopeColumn(), StringUtil.join(ids));}}? ? ? ? 3、自定義注解打印日志
完成思路:主要是使用自定義注解,以及aop的思想來完成方法調用的日志打印
引入依賴:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId> </dependency>創建注解:
/*** 自定義注解練習* 聲明在方法上的注解*/ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface LogAnnotation {/*** 日志內容* @return*/String value() default "";/*** 操作方類型* 0-未知來源,1-pc端,2-小程序端,3-其他*/int type() default 0; }實現注解環繞:
@Slf4j @Component @Aspect public class OpenLogUtil {/*** 配置切入點:注釋中引號的部分為自己創建的注解的路徑,可以通過該注解請求到切入點中去。*/@Pointcut("@annotation(com.example.BootDemo.Annotation.LogAnnotation)")public void logPointcut() {// 該方法無方法體,主要為了讓同類中其他方法使用此切入點}/*** 配置環繞通知,使用自定義方法上注冊的切入點。*/@Around("logPointcut()")public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {//前置long startTime= System.currentTimeMillis();/*** 環繞通知=前置+目標方法執行+后置,proceed方法就是用于啟動目標方法執行的* Proceedingjoinpoint 繼承了 JoinPoint。是在JoinPoint的基礎上暴露出 proceed 這個方法。proceed很重要,這個是aop代理鏈執行的方法。* 暴露出這個方法,就能支持 aop:around 這種切面(而其他的幾種切面只需要用到JoinPoint,,這也是環繞通知和前置、后置通知方法的一個最大區別。這跟切面類型有關)* */Object result=joinPoint.proceed();//后置long time=System.currentTimeMillis()-startTime;recordLog(joinPoint,time);return result;}/*** 記錄日志*/public void recordLog(ProceedingJoinPoint proceedingJoinPoint,long time){//getSignature());是獲取到這樣的信息 :修飾符+ 包名+組件名(類名) +方法名MethodSignature methodSignature= (MethodSignature) proceedingJoinPoint.getSignature();Method method=methodSignature.getMethod();//getAnnotation:方法如果存在這樣的注釋,則返回指定類型的元素的注釋,否則為nullLogAnnotation logAnnotation=method.getAnnotation(LogAnnotation.class);log.info("==============================開始記錄日志===============================");log.info("value:{}",logAnnotation.value());log.info("type:{}",logAnnotation.type());//proceedingJoinPoint.getTarget():獲取切入點所在目標對象String className=proceedingJoinPoint.getTarget().getClass().getName();String methodName=methodSignature.getName();log.info("請求的方法是:{}",className+"."+methodName+"()");//這里返回的是切入點方法的參數列表Object[] args=proceedingJoinPoint.getArgs();String params= JSON.toJSONString(args.length==0?"":args[0]);log.info("請求的參數是:{}",params);log.info("執行時間總共為:{}",time);log.info("=================================end===================================");} }創建controller:
@Slf4j @RestController public class MyController {@LogAnnotation(value = "記錄日志",type=1)@GetMapping("/SourceB")public String SourceB(){log.info("正在執行數據源B");return "Source B, All Right!";} }打印結果:
?簡簡單單的自定義注解打印日志就完成了,是不是很簡單啊~~后續可以通過IO流的方式來把日志打印到本地指定文件夾,如下:
package com.huike.common.advice;import com.huike.common.utils.LogPrint; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List;@Slf4j @Aspect @Component public class LogAdvice {/*** 配置切入點*/@Pointcut("@annotation(com.huike.common.annotation.Log)")public void log() {}/*** 環繞通知** @param pjp* @return* @throws Throwable*/@Around("log()")public Object logAround(ProceedingJoinPoint pjp) throws Throwable {Long startTime = System.currentTimeMillis();// 執行原始方法Object proceed = pjp.proceed();//記錄日志Long endTime = System.currentTimeMillis();Long time = endTime - startTime;logOut(pjp, time,proceed);return proceed;}/*** 記錄日志的方法** @param pjp* @param time*/private void logOut(ProceedingJoinPoint pjp, Long time,Object proceed) {Signature signature = pjp.getSignature();String typeName = signature.getDeclaringTypeName();//獲取包名String methondName = signature.getName();//獲取執行的方法名Date date = new Date();String format = new SimpleDateFormat("yyyy-HH-dd HH:mm:ss").format(date);Object[] args = pjp.getArgs();//獲取參數RequestAttributes requestAttribute = RequestContextHolder.getRequestAttributes();HttpServletRequest request = ((ServletRequestAttributes) requestAttribute).getRequest();String requestURL = request.getRequestURL().toString();String contextPath = request.getContextPath();if (proceed == null){proceed = "";}else {proceed = proceed.toString();}log.info("=======================" + format + "=========================");log.info("請求路徑是:{}", requestURL);log.info("請求包結構是:{}", typeName);log.info("請求訪問的方法是:{}", methondName);log.info("攜帶的參數是:{}", Arrays.toString(args));log.info("返回的結果是:{}", proceed);log.info("執行耗時:{} 毫秒", time);log.info("=============================================================");List<String> list = new ArrayList<>();list.add("=======================" + format + "=========================");list.add("請求路徑是:" + requestURL);list.add("請求包結構是:" + typeName);list.add("請求訪問的方法是:" + methondName);list.add("攜帶的參數是:" + Arrays.toString(args));list.add("返回的結果是:" + proceed);list.add("執行耗時:" + time + "毫秒");list.add("=============================================================");LogPrint.print(list,"D:\\huike.log");} }打印日志輸出的方法的工具類:
package com.huike.common.utils;import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; import java.util.List;public class LogPrint {public static void print(List<String> list,String path){try {BufferedWriter bw = new BufferedWriter(new FileWriter(path,true));for (String s : list) {bw.write(s);//輸出日志bw.newLine();//換行}bw.newLine();//換行bw.close();} catch (IOException e) {e.printStackTrace();}} }?
總結
以上就是CRM第一天的重點內容啦,是不是很簡單,有什么不動的可以私信答復。
總結
以上是生活随笔為你收集整理的汇客huikeCRM项目实战-初出茅庐的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: EAI dashgo底盘(不带陀螺仪版本
- 下一篇: CTPAT认证辅导,安全程序紧对于那些符