每个程序员总是要成长的,从最开始的写接口,到慢慢的接触系统架构,再到成为技术大牛,总有一个过程。这篇文章主要是为了给初学者提供一些系统架构方面的帮助。
本人水平有限,如有误导,欢迎斧正,一起学习,共同进步!
登陆成功以后,后台传给前端,以后前端就带着token,通过校验token(是否存在,是否过期,是否被篡改)来判断当前用户是否登陆过。有的功能不登陆也能看
登陆鉴权方面,从最开始的单体架构将 token 存入数据库,在到后来的存入 redis ,以及现在颇为主流的 JWT ,一直在变化。
最开始的单体架构时将 token 存入数据库。流程是:用户先输入用户名、密码进行登陆,登陆失败就不说了,重新登陆呗;登陆成功的话,就生成一个 token ,存入数据库,并返回给前端,以后前端再次调用后端接口时,都带着这个 token ,后端做一个全局拦截,拦截全部请求,只要没 token 或者 token 不正确的,统统抛出异常。数据库中除了存 token 信息外,还存着过期时间,用系统时间和过期时间比较,过期了也抛出异常。若 token 是正常的,则将 token 对应的用户信息存入 applicationContext 中,以便于后期适用。缺点是: 但随着系统的量级的提高,单体架构逐渐发展成为分布式架构。而分布式系统中,再将用户信息依次存进各个系统的 applicationContext 中,不仅冗余,而且效率低下。
由于 token 存数据库存在上述的缺点,因此就发展成了将 token 存入 redis 中。流程是:用户输入用户名密码进行登陆,若能根据用户名密码搜索到 User 对象,则将这个 User 对象(或者UserId) 存入 redis 中,redis 也是一个 kv 结构,v 就是你需要存的对象,k 可以是你的系统名+模块名的前缀+用户的id。然后设置好 redis 的过期时间。将这个 redis 的 k 传给前端,用 cookie 也好,放到请求头也罢,可以跟前端沟通,不过主流的还是放到请求头中。后面的就相同了,还是后端做一个全局拦截,拦截全部请求,只要没 token 或者 token 不正确的(根据 token 不能从 redis 中查到数据),就抛出异常。缺点是: 不过这个也有不便之处,比如说它需要验证,查完数据库后,存入 redis ,每次都需要去 redis 中验证。
JWT 和 token 存redis的一个重要的区别是,除了登陆时候的用户名密码,查到对应的用户信息以后,就直接将用户信息存入 JWT 中了,以后直接从 JWT 中拿就行,而不用去一遍遍的验证;而 token 存 redis 的话,每次都需要验证 redis 中有没有这个 User 的信息。JWT分为三部分,第一部分是名称和加密方式;第二部分是自定义的,可以存用户信息,还可以设置过期时间,接收人等,更加的方便快捷,但是千万不要存敏感信息(密码),因为他是不安全的,可能会被破解。常见的存用户ID,毕竟这个ID只对你这个系统有用,别人看到了ID也没有实际的意义。但是你要是存密码就不一样了,别人看到了密码就不安全了。第三部分是两部分的加密后的字符串,用来验证 token 有没有被修改过的,感兴趣的童鞋可以搜一下 JWT 。缺点是: 当然, JWT 也是有它的缺点的,就是不能灵活的修改权限信息。登陆完就直接将用户信息存进 JWT 了,不能够方便的及时修改这个用户的权限。
发
CREATE TABLE `back_token` (
`id` int(12) NOT NULL AUTO_INCREMENT COMMENT 'id',
`access_token` varchar(50) COLLATE utf8_unicode_ci NOT NULL COMMENT 'token值',
`refresh_token` varchar(50) COLLATE utf8_unicode_ci NOT NULL COMMENT 'refresh backtoken',
`user_id` varchar(50) COLLATE utf8_unicode_ci NOT NULL COMMENT '维护人id',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`modify_time` datetime DEFAULT NULL COMMENT '刷新时间',
`state` varchar(50) COLLATE utf8_unicode_ci DEFAULT 'ENABLE' COMMENT '状态 开启/关闭',
PRIMARY KEY (`id`),
KEY `access_token` (`access_token`),
KEY `refresh_token` (`refresh_token`),
KEY `state` (`state`)
) ENGINE=InnoDB AUTO_INCREMENT=1659 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
生成token的规则有很多,比如说用户ID+当前系统时间、用户ID+token过期时间、用户ID通过某种算法加密。。等等。此次例子是使用的 (用户ID+当前系统时间的7天后的毫秒值)的整体DES加密后的字符串做为token。之后的全部请求,前端访问接口的时候,带上token(常见的是放在请求头中)。
相关代码如下:
String token = DesUtil.encrypt(userDTO.getSystemuserid() + "_" + DateUtil.getCurrentTimeBefor7Day(), "password")
DES工具类:
package com.jinmao.map.util;
import lombok.extern.slf4j.Slf4j;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import java.security.SecureRandom;
import java.util.UUID;
/**
* @author yuanyn
* @Description DES对称加密
* @data 2019/07/10
*/
@Slf4j
public class DesUtil {
/**
* 数据加密,算法(DES)
* @param data 要进行加密的数据
* @param sKey DES算法密钥
* @return 加密后的数据
*/
public static String encrypt(String data, String sKey) {
String encryptedData = null;
try {
// DES算法要求有一个可信任的随机数源
SecureRandom sr = new SecureRandom();
DESKeySpec deskey = new DESKeySpec(sKey.getBytes());
// 创建一个密匙工厂,然后用它把DESKeySpec转换成一个SecretKey对象
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
SecretKey key = keyFactory.generateSecret(deskey);
// 加密对象
Cipher cipher = Cipher.getInstance("DES");
cipher.init(Cipher.ENCRYPT_MODE, key, sr);
// 加密,并把字节数组编码成字符串
encryptedData = new sun.misc.BASE64Encoder().encode(cipher.doFinal(data.getBytes()));
} catch (Exception e) {
log.error("加密错误,错误信息:", e);
throw new RuntimeException("加密错误,错误信息:", e);
}
return encryptedData;
}
/**
* 数据解密,算法(DES)
* @param cryptData 加密数据
* @param sKey DES算法密钥
* @return 解密后的数据
*/
public static String decrypt(String cryptData, String sKey) {
String decryptedData = null;
try {
// DES算法要求有一个可信任的随机数源
SecureRandom sr = new SecureRandom();
DESKeySpec deskey = new DESKeySpec(sKey.getBytes());
// 创建一个密匙工厂,然后用它把DESKeySpec转换成一个SecretKey对象
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
SecretKey key = keyFactory.generateSecret(deskey);
// 解密对象
Cipher cipher = Cipher.getInstance("DES");
cipher.init(Cipher.DECRYPT_MODE, key, sr);
// 把字符串解码为字节数组,并解密
decryptedData = new String(cipher.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(cryptData)));
} catch (Exception e) {
log.error("解密错误,错误信息:", e);
throw new RuntimeException("解密错误,错误信息:", e);
}
return decryptedData;
}
public static void main(String[] args){
System.out.println("加密:" + DesUtil.encrypt("bb8dd7c25ca442ee94f4b5e5386ecfd0_1587970624132", "password"));
System.out.println("解密:" + DesUtil.decrypt("33CvKyPRqc+h/9jNpAQZH+JVhvLhZZlSgSf760cTfBAFQ0uSP+UOMz/5eS1cISdG","password"));
}
}
日期时间工具类:
//获取7天后的毫秒值
public static Long getCurrentTimeBefor7Day(){
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.DAY_OF_YEAR, calendar.get(Calendar.DAY_OF_YEAR) + 7);
return calendar.getTimeInMillis();
}
步骤可参考 2.1、token存数据库 在token生成的时候,是一致的,无非是换中加密方式或者将过期时间换成生成时间,而在校验出严重时间差而已。不同点在于。上面是将 token 生成以后,放入数据库,通过前端传递过来的 token 去查数据库,来判断 token 是否过期,是否被篡改。而这里,是将生成的 token 存入 redis 中,以后直接去 redis 中拿,而不去数据库中查询 token 了。
将生成的 token 存入 redis 的时候,也可以有多种方式存储,比如说最常用的 String ,一个 key 多个 value 的 Hash 等。 String 类型的存储就是单纯的 k-v 类型的结构,常见的可以是 (redis中的key)系统名+模块名+token 组成的字符串, (redis中的value) 用户ID 字符串。若是希望存 Hash 类型的,可以是(redis中的key)系统名+模块名+token组成的字符串, (redis中的value) 用户ID,用户Name,用户性别。。等等。 当根据 token 去 redis 中查不到时,要么是 reids 存的过期时间,导致 token 失效了;要么就是 token 被篡改了。
常见的方法如下:
redis 中支持五种数据类型:String(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合)
keys * 查询全部的key
set key value 给这个key赋值这个value
get key 拿到这个key对应的value
del key 删除key。不管这个key是string还是hash,都可以直接删除
ttl key 查看这个key的过期时间,单位是秒,-1是永不过期,-2是已经失效
expire key time(秒) 设置一个过期时间,到达过期时间以后,删除key以及value
persist key 移出过期时间,这个key会永久保存
hset key field value 建立一个数据,key value
hget key field 取出值
hmset key field1 value1 field2 value2 ... 存储一个或多个哈希是键值对的集合
hmget key field ... field 获取多个指定的键的值
hgetall key 得到key的全部字段和值
hkeys key 得到key中的全部字段
hlen key 得到key中的hash字段(元素的个数)
hdel key field1 ... fieldN 删除key中的一个或多个属性
hincrby key field 增量值 给key中的属性增加指定的值(属性必须是数值型)
select 1 切换到第几个数据库(默认是第0个库),select 1 就是切换到第二个库(redis默认有16个库,可以修改配置文件来改)
rename a bb 将key是a的值,将key更新为bb
move a 2 将key是a的数据,移动到索引为2的数据库中
type a 返回key是a的数据的类型,(string/hash/list/set/zset)
flushdb 删除当前数据库的全部的key
flushall 删除所有数据库的所有的key
dbsize 返回当前数据库的key的个数
setnx key value 若此key不存在,则赋值成功;若此key存在,则赋值失败(不做任何操作)
getrange key 0 1 将key截取,截取从所有0到索引1的范围
incr key incr命令将key中存储的数字(value)增1,若key不存在,则会先被初始化为0,再执行ince操作
decr key decr命令将key中存储的数字减一
decrby key 增量值 decrby命令将key中存储的数据减去指定的增量值
总的来说,jwt分为三部分,第一部分head,token类型和采取的加密算法。类型就是jwt,加密算法就是你的加密算法,默认是hs256。第二部分payload是自定义的一些键值对,比较常见的有用户id,过期时间,iss签发着,aud接收者等。第三部分signature是前两者的base64编码后的连接+盐 用你定义的算法加密后的字符串(第三部分需要使用base64编码后的head,base64编码后的payload,以及你自定义的一个字符串盐,一起用head中的加密算法加密后的密文,叫做signature,他的作用是验证jwt没有被修改过)。base64是一种编码格式,不是加密,他是可逆的,可逆的话,就证明前俩是可能会被破解拿到数据的,是不安全的,所以不建议放敏感信息,你可以放你的系统中的该用户的id,这个id仅对你有用,别人拿到了也没用,但是不要放用户名,密码,用户名密码别人拿到了也能用。正常情况下,用户输入用户名密码,错了就不说了,提示错误,正确了,我就生成一个jwt,返回给前段,然后前段每次访问的时候,带上jwt,若是退出登录,直接让前段删了jwt,或者你更新一下盐,换个新盐,解密也就不成功了。然后后端校验的时候,检查签名(第三部分)是否正确,检车token是否过期(第二部分可以有过期时间),检查token的接收方是否是自己(第二部分可以定义接收方),可选可不选,校验全部通过以后,根据第二部分的用户id进行其他逻辑操作。
JWT 的前面操作和别的一样,都是输入账号密码,校验。失败了就是失败了,成功了以后,生成 JWT token。简单的介绍一下 JWT 。 JWT 由三部分组成,每个部分之间是通过 . 来分隔的。第一部分是两小部分组成的,分别是加密算法和类型(类型是固定的JWT,算法可以选择,算法默认是HS256)。第二部分可以自定义key-value结构的值,然后通过 base64 编码,是编码,不是加密。第三部分是将第一,二部分的密文拼接起来,再用第一部分所选择的加密算法进行加密。
服务器端进行解密的时候,步骤是这样的:1、获取token。2、对token进行分割。3、将第二段进行 base64 编码的解码。拿到自定义的一些信息。4、将分割后的第一、第二部分字符串,进行加密,然后与解密时候拿到的第三部分对比,若一致,则表明 toen 没有被篡改。因为 哈希256 和 MD5 加密一样,都是不可逆的,所以是第二次加密,然后跟结果对比 ,来判断是够呗篡改。
相关代码如下:
=========================maven依赖:==============================
io.jsonwebtoken
jjwt
0.9.1
=========================代码==============================
package com.test;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.Test;
import java.util.Date;
/**
* @author: ZhengTianLiang
* @date: 2020/11/2 16:14
* @desc: 測試jwt的类
*/
public class JwtTest {
/**
* @author: ZhengTianLiang
* @date: 2020/11/2 16:14
* @desc: 測試生成jwt的方法
*/
@Test
public void generateJwtTest(){
JwtBuilder jwtBuilder = Jwts.builder().setId("123") // 可以setid,也可以不setid,约定俗成的是set
.setSubject("良哥帅") // 可以setSubject,也可以不setSubject,约定俗成的是set
.setIssuedAt(new Date()) // 设置签发日期
.signWith(SignatureAlgorithm.HS256, "zhengtianliang") // 设置加密算法和盐
.claim("key","value") // 可以自定义key
.claim("key2","nihao"); // 自定义key
String token = jwtBuilder.compact(); // 生成token
System.out.println(token);
}
/**
* @author: ZhengTianLiang
* @date: 2020/11/2 16:24
* @desc: 解析jwt的方法
*/
@Test
public void parserJwtTest(){
// 将上个方法中生成的token 拿到
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjMiLCJzdWIiOiLoia_lk6XluIUiLCJpYXQiOjE2MDQzMDc4NjIsImtleSI6InZhbHVlIiwia2V5MiI6Im5paGFvIn0.9zb0rZWHhe4KB-kxdOarCd1arIhiJQYNLOX7SRsBX4w";
Claims claims = Jwts.parser().setSigningKey("zhengtianliang").parseClaimsJws(token).getBody();
// 输出一下信息
System.out.println(claims.getId()); // 123
System.out.println(claims.getSubject()); // 良哥帅
System.out.println(claims.getIssuedAt()); // Mon Nov 02 16:19:58 CST 2020
System.out.println(claims.get("key"));
System.out.println(claims.get("key2"));
}
}
这个功能是基于 spring 的 AOP 功能来实现的。AOP 就不必多说了,基于切面编程嘛。大概原理是,将一些通用的功能,提取出来,写到一处。比如说日志功能,你要是每个模块都写一遍打印的话,以后修改的时候,也要一次次的修改,不如直接提取到 AOP 中,只写一遍,这样减少了代码的冗余性,提高了系统的稳定性和可读性。
实现方式可以是通过 @Component、@Aspect、@Around 三个注解实现 AOP 功能。当然, aop 有前置操作、后置操作、环绕操作。我个人更喜欢环绕操作(@Around),感兴趣的可以去搜索一下“AOP前置操作实现”。
pom依赖:
org.springframework.boot
spring-boot-starter-aop
拦截器:
package com.jinmao.map.common;
import com.jinmao.map.util.DesUtil;
import com.jinmao.map.util.StringUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
/**
* @Classname 校验token的aop
* @Date 2020/04/27 15:23
* @Created by yuanyn
*/
@Component
@Aspect
public class CheckTokenAop {
// 这个主要是获取
@Resource
private ApplicationContext applicationContext;
/**
* 拦截请求头信息里面的token
*
* @param pro
*/
// 前面没感叹号是要拦截的controller,前面有感叹号是要放行的controller当中的某个方法
@Around("execution (* com.jinmao.map.controller..*.*(..)) &&"
+ "!execution(* com.jinmao.map.controller.HomepageController.userLoginByCode(..)) ")
public Object checkToken(ProceedingJoinPoint pro) throws Throwable {
//获取请求参数信息
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String accessToken = request.getHeader("token");//获取token
if (StringUtil.isEmpty(accessToken)) {
throw new RuntimeException("当前用户无权限,请重新登录");
}
//DES解密
String token = DesUtil.decrypt(accessToken,"password");
//token由两部分组成 用户id + 系统时间的毫秒值 再通过DES加密
String[] s = token.split("_");
if (2 != s.length){
throw new RuntimeException("用户信息错误,请重新登录");
}
//如果当前时间大于token时间 则token失效
try {
if (System.currentTimeMillis() > Long.valueOf(s[1])) {
throw new RuntimeException("长时间未登录,请登录管理地图");
}
}catch (NumberFormatException e){
//token后半部分不是long类型 返回错误
throw new RuntimeException("用户信息错误,请重新登录");
}
TokenBean tokenBean = new TokenBean(s[0]);
//注入上下文
this.setJwtTokenBen(tokenBean);
// 这一步是固定的,全部的token拦截器都是这样的
Object proceed = pro.proceed();
return proceed;
}
/**
* 注入ben
*
* @param tokenBean
*/
private void setJwtTokenBen(TokenBean tokenBean) {
TokenBean bean = applicationContext.getBean(TokenBean.class);
bean.setAccountId(tokenBean.getAccountId());
}
}
核心代码解析:
@Around(“execution (* com.jinmao.map.controller….(…)) &&”
+ "!execution(* com.jinmao.map.controller.HomepageController.userLoginByCode(…)) ")
execution( 控制器controller 包的全限定路径,这是要被拦截的包名,后面的第一个 * 代表任意controller,第二个 * 代表任意方法,里面的 . . 代表任意参数)
!execution(包名.controller名.方法名) 则是代表要放行的某些方法,不被拦截器所拦截。
比如说,你的某些功能,不需要用户登录也可以看到,就可以将这些接口放行;而有些功能,必须是用户登录成功以后才可以看的, 就将这些拦截住,来达到控制权限的目的。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperationLog {
//操作大类
String category();
//操作子类
String subcategory();
//操作描述
String desc();
//操作类型
OperationLogTypeEnum type() ;
}
public enum OperationLogTypeEnum {
add("1","新增"),
update("2","修改"),
delete("3","删除");
private String code;
private String value;
OperationLogTypeEnum(String code,String value ) {
this.code=code;
this.value=value;
}
public String getCode() {
return code;
}
public String getValue() {
return value;
}
public static String getValueByCode(String code) {
if (code == null) {
return null;
}
for (OperationLogTypeEnum e : OperationLogTypeEnum.values()) {
if (e.getCode().equals(code)) {
return e.getValue();
}
}
return null;
}
public static boolean isExistByCode(String code) {
if (code == null) {
return false;
}
for (OperationLogTypeEnum e : OperationLogTypeEnum.values()) {
if (e.getCode().equals(code)) {
return true;
}
}
return false;
}
}
@RestController
@RequestMapping(value = "/corpus/cust")
@Api(value = "客户化知识controller", tags = "客户化知识")
public class CustKnowledgeController {
/**
* @author: ZhengTianLiang
* @date: 2021/3/19 14:36
* @desc: 上线客户化知识(单轮问答)
*/
@OperationLog(category = "客户化知识", subcategory = "单轮问答知识", desc = "上线客户化知识(单轮问答)", type = OperationLogTypeEnum.update)
@ApiOperation(value = "上线客户化知识(单轮问答)")
@PostMapping(value = "/onlineKnowledge")
public ReturnMsg onlineKnowledge(@Valid @RequestBody CustKnowledgeOnOffLineDTO custKnowledgeOnOffLineDTO) {
custKnowledgeService.onlineKnowledge(custKnowledgeOnOffLineDTO);
return ReturnMsg.okR("SUCCESS", FormatUtil.listConvertString(custKnowledgeOnOffLineDTO.getIds()));
}
}
package com.hollycrm.hollycloud.ics.knowledge.admin.aspect;
import com.alibaba.fastjson.JSON;
import com.hollycrm.hollycloud.ics.knowledge.admin.annotation.OperationLog;
import com.hollycrm.hollycloud.ics.knowledge.admin.entity.operation.OperationLogKm;
import com.hollycrm.hollycloud.ics.knowledge.admin.enums.operationlog.OperationLogTypeEnum;
import com.hollycrm.hollycloud.ics.knowledge.admin.handler.LoginUtils;
import com.hollycrm.hollycloud.ics.knowledge.admin.service.operationlog.OperationLogService;
import com.hollycrm.hollycloud.ics.knowledge.admin.util.IDRule;
import com.hollycrm.hollycloud.ics.knowledge.admin.util.ReturnMsg;
import com.hollycrm.os.model.Department;
import com.hollycrm.os.model.Employee;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Slf4j
@Component
@Aspect
public class KmOperationLogAspect {
@Autowired
private OperationLogService operationLogService;
@Around("@annotation(com.hollycrm.hollycloud.ics.knowledge.admin.annotation.OperationLog)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
long start = System.currentTimeMillis();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
OperationLog opLog = method.getAnnotation(OperationLog.class);
OperationLogKm operationLog = new OperationLogKm();
operationLog.setCreatedTime(new Date());
Employee employee = LoginUtils.getEmployeeInfo();
Department department = LoginUtils.getDeptInfo();
if (employee != null) {
operationLog.setOpUserId(employee.getEmployeeId());
operationLog.setOpUserAccount(employee.getNickName());
operationLog.setOpUserName(employee.getEmployeeName());
operationLog.setOpDeptCode(employee.getDeptId());
operationLog.setCreatedBy(employee.getEmployeeId());
}
if(department!=null){
operationLog.setOpDeptName(department.getDeptName());
}
String opCatetory = opLog.category();
String opSubcategory = opLog.subcategory();
String opDesc = opLog.desc();
OperationLogTypeEnum opType = opLog.type();
operationLog.setOpCategory(opCatetory);
operationLog.setOpSubCategory(opSubcategory);
operationLog.setOpDesc(opDesc);
operationLog.setOpType(opType.getCode());
String className = joinPoint.getTarget().getClass().getName();
String methodName = method.getName();
methodName = className + "." + methodName;
operationLog.setOpMethod(methodName);
List
其中的 3.1.3、注解的用法 当中的
@OperationLog(category = “客户化知识”, subcategory = “单轮问答知识”, desc = “上线客户化知识(单轮问答)”, type = OperationLogTypeEnum.update)
其中的 3.1.2、注解的原理 当中的
@Around("@annotation(com.hollycrm.hollycloud.ics.knowledge.admin.annotation.OperationLog)")
其中的 3.1.2、注解的原理 当中的
OperationLog opLog = method.getAnnotation(OperationLog.class);
…
operationLogService.save(operationLog);
统一异常处理类似于统一返回结果,不过统一返回结果是在结果的最外层封装一层,然后将需要返回的内容存到这个统一结果中,是为了方便后台与前端工程师联调的。而统一异常处理则是方便我们后端更快速的定位问题的。比如说,你没有统一异常处理的时候,遇到问题,则抛出 throw new RuntimeException(“XXX异常”),都叫做RuntimeException。但是如果统一异常处理就不一样了。你可以定义几个特殊的异常。比如说登陆异常 throw new LoginException(“XXX异常”),权限异常 throw new AuthorityException(“XXX异常”),业务异常 throw new BusinessException(“XXX异常”)。当你定位问题时,直接一看到“BusinessException”就知道是业务异常,一看到“LoginException”就知道是登陆异常。而不是看到的全是“RuntimeException”,不方便我们快速的定位问题。
全局异常处理主要是通过 @RestControllerAdvice、@ExceptionHandler 这两个注解来实现的。其中@RestControllerAdvice(value=“com.springcloud”)是包的名称,value可以写,可以不写@ExceptionHandler(value = CustomException.class) 其中value可以不写,不写的话,默认就是参数的异常类型
继承 RuntimeException 即可
package com.hollycrm.hollycloud.ics.knowledge.admin.exception;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author: ZhengTianLiang
* @date: 2021/1/14 11:26
* @desc: 权限异常,当前用户无权限时抛出此异常
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AuthorityException extends RuntimeException{
/**
* 状态码
*/
private String code;
/**
* 错误信息,给开发人员看的,前端不展示
*/
private String msg;
/**
* 错误信息,给用户看的,前端展示
*/
private String msgVo;
/**
* @author: ZhengTianLiang
* @date: 2021/1/14 11:18
* @desc: 创建一个两个参数的异常,报错信息给前端展示,方便抛出异常用的
*/
public AuthorityException(String code, String msgVo){
this.code = code;
this.msgVo = msgVo;
}
}
继承 RuntimeException 即可
package com.hollycrm.hollycloud.ics.knowledge.admin.exception;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author: ZhengTianLiang
* @date: 2021/1/14 11:25
* @desc: 登陆异常,用户未登陆或登陆超时时抛出此异常
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginException extends RuntimeException{
/**
* 状态码
*/
private String code;
/**
* 错误信息,给开发人员看的,前端不展示
*/
private String msg;
/**
* 错误信息,给用户看的,前端展示
*/
private String msgVo;
/**
* @author: ZhengTianLiang
* @date: 2021/1/14 11:18
* @desc: 创建一个两个参数的异常,报错信息给前端展示,方便抛出异常用的
*/
public LoginException(String code, String msgVo){
this.code = code;
this.msgVo = msgVo;
}
}
捕获全部的异常,包括自定义异常和其他异常。
package com.hollycrm.hollycloud.ics.knowledge.admin.exception;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
/**
* @author: ZhengTianLiang
* @date: 2021/1/14 10:49
* @desc: 全局异常类,是返回给前端的
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
public class GlobalException {
/**
* 状态码
*/
private String code;
/**
* 错误信息,给开发人员看的,前端不展示
*/
private String msg;
/**
* 错误信息,给用户看的,前端展示
*/
private String msgVo;
/**
* @author: ZhengTianLiang
* @date: 2021/1/14 11:18
* @desc: 创建一个两个参数的异常,报错信息给前端展示,方便抛出异常用的
*/
public GlobalException(String code, String msgVo){
this.code = code;
this.msgVo = msgVo;
}
}
package com.hollycrm.hollycloud.ics.knowledge.admin.exception.handler;
import com.hollycrm.hollycloud.ics.knowledge.admin.exception.AuthorityException;
import com.hollycrm.hollycloud.ics.knowledge.admin.exception.BusinessException;
import com.hollycrm.hollycloud.ics.knowledge.admin.exception.GlobalException;
import com.hollycrm.hollycloud.ics.knowledge.admin.exception.LoginException;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* @author: ZhengTianLiang
* @date: 2021/1/14 11:28
* @desc: 全局异常拦截器
*/
@Log4j2
@RestControllerAdvice(value = "com.hollycrm.hollycloud.ics.knowledge.admin.controller")
@PropertySource(value = "classpath:/exception/exception.properties", encoding = "UTF-8")
public class GlobalExceptionHandler {
@Autowired
Environment environment;
/**
* 内部系统出错状态码
*/
private final String businessMsgCode = "KLB_B_CATALOG_006";
/**
* @author: ZhengTianLiang
* @date: 2021/1/14 11:30
* @desc: 捕获登陆异常
*/
@ExceptionHandler(value = LoginException.class)
public GlobalException handlerException(LoginException e) {
log.error(e);
return new GlobalException(e.getCode(), e.getMsg(), e.getMsgVo());
}
/**
* @author: ZhengTianLiang
* @date: 2021/1/14 14:05
* @desc: 捕获权限异常
*/
@ExceptionHandler(value = AuthorityException.class)
public GlobalException handlerException(AuthorityException e) {
log.error(e);
return new GlobalException(e.getCode(), e.getMsg(), e.getMsgVo());
}
/**
* @author: ZhengTianLiang
* @date: 2021/1/14 14:07
* @desc: 捕获业务异常
*/
@ExceptionHandler(value = BusinessException.class)
public GlobalException handlerException(BusinessException e) {
log.error(e);
return new GlobalException(e.getCode(), e.getMsg(), e.getMsgVo() == null ? environment.getProperty(businessMsgCode) : e.getMsgVo());
}
/**
* @author: ZhengTianLiang
* @date: 2021/1/14 14:07
* @desc: 捕获空指针异常
*/
@ExceptionHandler(value = NullPointerException.class)
public GlobalException handlerException(NullPointerException e) {
log.error(e);
return new GlobalException(businessMsgCode, e.toString(), environment.getProperty(businessMsgCode));
}
/**
* @author: ZhengTianLiang
* @date: 2021/1/14 14:14
* @desc: 捕获非上述异常以外的异常
*/
@ExceptionHandler(value = Exception.class)
public GlobalException handlerException(Exception e) {
log.info(e);
return new GlobalException(businessMsgCode, e.toString(), environment.getProperty(businessMsgCode));
}
}
其中2.3、全局异常类(返回给前端看的) 中的 private String msg; private String msgVo;
当中的 msgVo 是给用户看的,一般是“"系统繁忙,请稍后重试” 这些不会暴露出具体业务信息的话;而 msg 是给后台开发人员看的,目的是为了快速的定位问题。
其中的 2.4、全局异常捕获类 当中的
@RestControllerAdvice(value = “com.hollycrm.hollycloud.ics.knowledge.admin.controller”)
@ExceptionHandler(value = LoginException.class)
@ExceptionHandler(value = AuthorityException.class)
@ExceptionHandler(value = Exception.class)
其中 @RestControllerAdvice 里面的值是控制器(controller)所在的包的全限定路径;@ExceptionHandler 里面的值是要拦截的异常分类。
通过这个特性,我们可以 @ExceptionHandler(value = AuthorityException.class) 拦截授权异常,并做出一些处理;@ExceptionHandler(value = LoginException.class) 拦截登陆异常,并做出一些处理, @ExceptionHandler(value = Exception.class) 来拦截其余的全部异常,并做出一些处理。只不过我此次未做处理,直接抛出了全局异常而已。
总的来说,还是利用了 Spring的 AOP 功能来帮我们达到简化开发,减少冗余代码的目的,同时也达成了统一返回结果,提高前后端联调的效率的期望。