# shiro1.8+jwt整合 **Repository Path**: abcwg420/shiro-jwt-integration ## Basic Information - **Project Name**: shiro1.8+jwt整合 - **Description**: shiro1.8整合jwt的演示 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 4 - **Created**: 2022-05-07 - **Last Updated**: 2022-05-07 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 目前最好的shiro+jwt整合策略,2021终极版,包含shiro1.8新特性,更强大更简洁。 2021年发布shiro1.8带来了质的飞跃,对于本文的需求来说,最利好的包括两点:一是增加了对SpringBoot自动装配机制的支持;二是增加了BearerHttpAuthenticationFilter这个默认过滤器,从而让Jwt的整合获得了原生级的支持。以上两项新增特性大大精简了我们的配置工作,且让当前网络上所有的教程都落后于时代。(包括官网和英文网络,但凡我搜到的都是旧版本的配置。) > 你也可以直接使用本人开发的框架KRest来实现两者的集成使用,该框架在整合了常用的shiro+jwt+通信加密模块的基础上,提供了一套极为简便易用的一体化配置,同样运用了本文中即将介绍的最新机制。 > 项目发布在gitee上 https://gitee.com/ckw1988/krest ,源码同时也发布到了maven中央库,直接配置依赖即可使用。教程完善使用方便,欢迎大家选用。 如果您出于对知识的热忱和追求依然打算自己亲手完成一套shiro+jwt的配置,那么请继续往下看下去。本文在介绍配置时会深入讲解一些相关shiro和jwt的机制原理,所以此贴同时也是一篇极好的机制原理介绍教程。 话不多说,开搞。 ## 示例源码 本教程的源码地址为 https://gitee.com/ckw1988/shiro-jwt-integration 并包含一个调试用的postman脚本,强烈建议下载下来跑通了再来看教程,心里比较踏实。 ## 配置文件 首先在pom里配上shiro1.8 ```xml org.apache.shiro shiro-spring-boot-web-starter 1.8.0 ``` 然后是配置文件,如今在的1.8也换上了springboot自动装配机制,config中只需配置两个bean。代码如下: ```java @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); /* * filter配置规则参考官网 * http://shiro.apache.org/web.html#urls- * 默认过滤器对照表 * https://shiro.apache.org/web.html#default-filters */ Map filterRuleMap = new HashMap<>(); filterRuleMap.put("/static/*", "anon"); filterRuleMap.put("/error", "anon"); filterRuleMap.put("/register", "anon"); filterRuleMap.put("/login", "anon"); //↑配置不参与验证的映射路径。 // 关键:配置jwt验证过滤器。 //↓ 此处即为shiro1.8新增的默认过滤器:authcBearer-BearerHttpAuthenticationFilter。jwt验证的很多操作都由该filter自动完成,以致我们只需理解其机制而无需亲手实现。 filterRuleMap.put("/**", "authcBearer"); //↑ 如果有其他过滤法则配在/**上,则在第二个参数的字符串里使用逗号间隔。 factoryBean.setGlobalFilters(Arrays.asList("noSessionCreation")); //↑ 关键:全局配置NoSessionCreationFilter,把整个项目切换成无状态服务。 factoryBean.setSecurityManager(securityManager); factoryBean.setFilterChainDefinitionMap(filterRuleMap); return factoryBean; } @Bean protected Authorizer authorizer() { ModularRealmAuthorizer authorizer = new ModularRealmAuthorizer(); return authorizer; } ``` 关键代码的功能和含义看我注释就行。这里重点解释两句: * filterRuleMap.put("/**", "authcBearer"): 这条语句配置了BearerHttpAuthenticationFilter过滤器,是JWT验证机制的核心。功能是自动解析请求头信息中的Authorization字段,并将其所携带的jwt token内容包装成一个BearerToken对象,并调用login方法进入realm进行身份验证。 > 在shiro旧版本的时代,有些比较靠谱的教程会实现一个类似功能的过滤器,如今也完全不需要了。该filter完美契合jwt机制,使用简单方便,希望大家多多推广。 * factoryBean.setGlobalFilters(Arrays.asList("noSessionCreation"))。 这里配置了个特别强大的过滤器NoSessionCreationFilter,一旦配上,整个shiro系统就不再存储任何用户信息,彻底成为一个真正的no-session服务。也就是说,假如有啥教程不加该过滤器的就声称在教你用Shiro实现no-session(包括RESTful)服务,那就真的只是说说而已。 如今的config部分只需要配置这么多,旧方案里那一大堆东西都不再需要了。此后你自定义的realm只需在类定义时加上@Component标签,即可由shiro自动装配使用(赞美SpringbBoot)。 ## 身份验证。 因为我们整个服务已经变成no-session状态,所以事实上对shiro来说整个系统中已经不存在"已登录用户"这个概念了,这就意味着每一次独立的请求事实上都需要一个身份验证过程,这种身份验证行为在shiro里都被称为"登录(Login)"。 大致的流程为:首次登陆时用户提交用户名和密码,验证通过后服务器生成一个初始的Jwt Token返回给客户端。此后客户端在任何请求时都把Jwt Token带上,服务端如果验证通过后即视为当次身份验证通过(或者说以token的方式登陆成功)。这个流程也即jwt token的官方标准使用方法。 既然有两种登陆方式,则需要两个realm,我们需要一个UsernamePasswordRealm来处理用户名和密码登录;一个TokenValidateAndAuthorizingRealm,处理token验证方式的"登录"。 ### UsernamePasswordRealm 1. 首先这种登录方式需要显示调用,毕竟用户名和密码怎么传没啥特别严格的规范。参考语法如下,定义在controller中 ```java /** * 登陆 */ @PostMapping("/login") public Map login(@RequestBody User userInput) throws Exception { String username = userInput.getUsername(); String password = userInput.getPassword(); Assert.notNull(username, "username不能为空"); Assert.notNull(password, "password不能为空"); UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password); Subject subject = SecurityUtils.getSubject(); subject.login(usernamePasswordToken);//显示调用登录方法 //生成返回token Map res=new HashMap<>(); JwtUser jwtUser = (JwtUser) SecurityUtils.getSubject().getPrincipal(); res.put("token",JwtUtil.createJwtTokenByUser(jwtUser)); res.put("result","login success or other result message"); return res; } ``` 2. subject.login(usernamePasswordToken)的操作,事实上是就进入了由realm处理身份验证的环节。我们先看代码 ```java //Username Password Realm,用户名密码登陆专用Realm @Slf4j @Component public class UsernamePasswordRealm extends AuthenticatingRealm { @Autowired private UserService userService; /*构造器里配置Matcher*/ public UsernamePasswordRealm() { super(); HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("md5"); hashedCredentialsMatcher.setHashIterations(2);//密码保存策略一致,2次md5加密 this.setCredentialsMatcher(hashedCredentialsMatcher); } /** * 通过该方法来判断是否由本realm来处理login请求 * * 调用{@code doGetAuthenticationInfo(AuthenticationToken)}之前会shiro会调用{@code supper.supports(AuthenticationToken)} * 来判断该realm是否支持对应类型的AuthenticationToken,如果相匹配则会走此realm * * @return */ @Override public Class getAuthenticationTokenClass() { log.info("getAuthenticationTokenClass"); return UsernamePasswordToken.class; } @Override public boolean supports(AuthenticationToken token) { //继承但啥都不做就为了打印一下info boolean res = super.supports(token);//会调用↑getAuthenticationTokenClass来判断 log.debug("[UsernamePasswordRealm is supports]" + res); return res; } /** * 用户名和密码验证,login接口专用。 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) { UsernamePasswordToken usernamePasswordToken=(UsernamePasswordToken) token; User userFromDB=userService.queryUserByName(usernamePasswordToken.getUsername()); String passwordFromDB = userFromDB.getPassword(); String salt = userFromDB.getSalt(); //在使用jwt访问时,shiro中能拿到的用户信息只能是token中携带的jwtUser,所以此处保持统一。 JwtUser jwtUser=new JwtUser(userFromDB.getUsername(),userFromDB.getRoles()); SimpleAuthenticationInfo res = new SimpleAuthenticationInfo(jwtUser, passwordFromDB, ByteSource.Util.bytes(salt), getName()); return res; } } ``` 3. 首先是覆盖getAuthenticationTokenClass方法,此时设定返回值为UsernamePasswordToken.class。shiro的机制是根据login方法中传入的token类型来分配realm,步骤1中是UsernamePasswordToken,所以分配给本realm来处理。 ```java @Override public Class getAuthenticationTokenClass() { log.info("getAuthenticationTokenClass"); return UsernamePasswordToken.class; } ``` 4. doGetAuthenticationInfo中的返回值按照new SimpleAuthenticationInfo(jwtUser, passwordFromDB, ByteSource.Util.bytes(salt),getName());来配,第一个参数依然是登陆成功后的用户信息,第三个是密码的盐。 ```java /** * 用户名和密码验证,login接口专用。 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) { UsernamePasswordToken usernamePasswordToken=(UsernamePasswordToken) token; User userFromDB=userService.queryUserByName(usernamePasswordToken.getUsername()); String passwordFromDB = userFromDB.getPassword(); String salt = userFromDB.getSalt(); //在使用jwt访问时,shiro中能拿到的用户信息只能是token中携带的jwtUser,所以此处保持统一。 JwtUser jwtUser=new JwtUser(userFromDB.getUsername(),userFromDB.getRoles()); SimpleAuthenticationInfo res = new SimpleAuthenticationInfo(jwtUser, passwordFromDB, ByteSource.Util.bytes(salt), getName()); return res; } } ``` 5. 密码验证策略是md5哈希2次加盐,因为这个验证规则shiro里有现成的实现,就不用自己写了,直接用HashedCredentialsMatcher即可。这部分其实更推荐自定义matcher,用自己熟悉的加密策略和加密工具自由地实现,学习成本更低,灵活度更高。这里还是演示用他自带的用法,自定义的示例参考后面jwt的realm。 ```java /*构造器里配置Matcher*/ public UsernamePasswordRealm() { super(); HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("md5"); hashedCredentialsMatcher.setHashIterations(2);//密码保存策略一致,2次md5加密 this.setCredentialsMatcher(hashedCredentialsMatcher); } ``` 8. 再次提醒一下不要遗漏@Component注解。 #### 进阶扩展 事实上这个UsernamePasswordRealm是个可选环节。获得初始jwt token的方式多种多样,可以是用户名密码登陆,可以是手机+验证码登陆,可以是第三方平台登录,甚至可以是通过其他服务登录已经获得了jwt token后再拿到本服务上来使用。 所以事实上最简单做法是,只要你认为某个登陆请求已经完成了登陆步骤,只需要在返回值中带上一个新token ```java …… res.put("token",JwtUtil.createJwtTokenByUser(jwtUser)); ``` 即可视为登陆成功。之后的其他请求自然会进入你在TokenValidateAndAuthorizingRealm中定义好的验证流程来处理。 ### TokenValidateAndAuthorizingRealm ```java @Slf4j @Component public class TokenValidateAndAuthorizingRealm extends AuthorizingRealm { //权限管理部分的代码先行略过 //...... public TokenValidateAndAuthorizingRealm() { //CredentialsMatcher,自定义匹配策略(即验证jwt token的策略) super(new CredentialsMatcher() { @Override public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) { log.info("doCredentialsMatch token合法性验证"); BearerToken bearerToken = (BearerToken) authenticationToken; String bearerTokenString = bearerToken.getToken(); log.debug(bearerTokenString); boolean verified = JwtUtil.verifyTokenOfUser(bearerTokenString); return verified; } }); } @Override public String getName() { return "TokenValidateAndAuthorizingRealm"; } @Override public Class getAuthenticationTokenClass() { //设置由本realm处理的token类型。BearerToken是在filter里自动装配的。 return BearerToken.class; } @Override public boolean supports(AuthenticationToken token) { boolean res=super.supports(token); log.debug("[TokenValidateRealm is supports]" + res); return res; } @Override//装配用户信息,供Matcher调用 public AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException, TokenExpiredException { log.debug("doGetAuthenticationInfo 将token装载成用户信息"); BearerToken bearerToken = (BearerToken) authenticationToken; String bearerTokenString = bearerToken.getToken(); JwtUser jwtUser = JwtUtil.recreateUserFromToken(bearerTokenString);//只带着用户名和roles SimpleAuthenticationInfo res = new SimpleAuthenticationInfo(jwtUser, bearerTokenString, this.getName()); /*Constructor that takes in an account's identifying principal(s) and its corresponding credentials that verify the principals.*/ // 这个返回值是造Subject用的,返回值供createSubject使用 return res; } ``` 该realm的功能除了身份验证还包含权限控制。为免干扰理解先行省略,后面会单独介绍,这里先说身份验证。 1. 首先,让客户端在请求中带上jwt token。按照jwt的通用规范,具体的做法是客户端将token字符串加上"Bearer "前缀后放在头信息的Authorization字段里。该信息会在authcBearer过滤器中自动解析,并将其所携带的jwt token内容包装成一个BearerToken对象。这一部分可参考实例源码中的postman脚本。 2. 然后实现realm的代码,依然覆盖getAuthenticationTokenClass方法,本类中令该方法返回BearerToken.class。该token由authcBearer filter自动封装而成。由此shiro就就会将authcBearer filter中发起的,用于验证jwt的login操作交给该realm处理。 ```java @Override public Class getAuthenticationTokenClass() { //设置由本realm处理的token类型。BearerToken是在filter里自动装配的。 return BearerToken.class; } ``` > 注意区分两种token的概念,jwt token是一串字符串,用于在客户端和服务端常规通信时的身份保持。而shiro中的token是一个java bean,它是对用户身份信息的一种封装,用于服务器内部、在shiro框架中包装和传递待验证的用户信息:在用户名密码登陆时它是封装了用户名密码的UsernamePasswordToken,在jwt验证时它是封装了jwt token字符串的BearerToken。 3. 接下来是实现doGetAuthenticationInfo方法,该方法依然不是真正的身份验证过程,而是装配登陆成功后的用户信息(返回值的第一个参数)和供验证的身份信息(返回值的第二个参数),第三个参数大约是用于区分本次登陆是由哪个realm通过的,不太重要,带上即可。 ```java @Override//装配用户信息,供Matcher调用 public AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException, TokenExpiredException { log.debug("doGetAuthenticationInfo 将token装载成用户信息"); BearerToken bearerToken = (BearerToken) authenticationToken; String bearerTokenString = bearerToken.getToken(); JwtUser jwtUser = JwtUtil.recreateUserFromToken(bearerTokenString);//只带着用户名和roles SimpleAuthenticationInfo res = new SimpleAuthenticationInfo(jwtUser, bearerTokenString, this.getName()); /*Constructor that takes in an account's identifying principal(s) and its corresponding credentials that verify the principals.*/ // 这个返回值是造Subject用的,返回值供createSubject使用 return res; } ``` 4. 配置一个CredentialsMatcher。该对象才是真正处理验证登陆的步骤,我将其用匿名类创建在realm的构造器里,语法很好懂,看源码即可。 ```java public TokenValidateAndAuthorizingRealm() { //CredentialsMatcher,自定义匹配策略(即验证jwt token的策略) super(new CredentialsMatcher() { @Override public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) { log.info("doCredentialsMatch token合法性验证"); BearerToken bearerToken = (BearerToken) authenticationToken; String bearerTokenString = bearerToken.getToken(); log.debug(bearerTokenString); boolean verified = JwtUtil.verifyTokenOfUser(bearerTokenString); return verified; } }); } ``` 5. 同时,该步骤中还用到了工具类JwtUtil,代码如下: ```java @Slf4j public class JwtUtil { //指定一个token过期时间(毫秒) private static final long EXPIRE_TIME = 20 * 60 * 1000; //20分钟 private static final String JWT_TOKEN_SECRET_KEY = "yourTokenKey"; //↑ 记得换成你自己的秘钥 public static String createJwtTokenByUser(JwtUser user) { String secret = JWT_TOKEN_SECRET_KEY; Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME); Algorithm algorithm = Algorithm.HMAC256(secret); //使用密钥进行哈希 // 附带username信息的token return JWT.create() .withClaim("username", user.getUsername()) .withClaim("roles", user.getRoles()) // .withClaim("permissions",permissionService.getPermissionsByUser(user)) .withExpiresAt(date) //过期时间 .sign(algorithm); //签名算法 //r-p的映射在服务端运行时做,不放进token中 } /** * 校验token是否正确 */ public static boolean verifyTokenOfUser(String token) throws TokenExpiredException {//user要从sercurityManager拿,确保用户用的是自己的token log.info("verifyTokenOfUser"); String secret = JWT_TOKEN_SECRET_KEY;// //根据密钥生成JWT效验器 Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier verifier = JWT.require(algorithm) .withClaim("username", getUsername(token))//从不加密的消息体中取出username .build(); //生成的token会有roles的Claim,这里不加不知道行不行。 // 一个是直接从客户端传来的token,一个是根据盐和用户名等信息生成secret后再生成的token DecodedJWT jwt = verifier.verify(token); //能走到这里 return true; } /** * 在token中获取到username信息 */ public static String getUsername(String token) { try { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("username").asString(); } catch (JWTDecodeException e) { return null; } } public static JwtUser recreateUserFromToken(String token) { JwtUser user = new JwtUser(); DecodedJWT jwt = JWT.decode(token); user.setUsername(jwt.getClaim("username").asString()); user.setRoles(jwt.getClaim("roles").asList(String.class)); //r-p映射在运行时去取 return user; } /** * 判断是否过期 */ public static boolean isExpire(String token) { DecodedJWT jwt = JWT.decode(token); return jwt.getExpiresAt().getTime() < System.currentTimeMillis(); } } ``` 因为封装比较简单,看看源码和注解即可。 该类中所用到的JWT验证框架是 ```xml com.auth0 java-jwt 3.18.2 ``` 配到pom里去。 8. 至此,jwt验证部分的功能配置完毕。DemoController中的whoami方法是这部分的使用范例。 ```java @GetMapping("/whoami") public Map whoami(){ JwtUser jwtUser = (JwtUser) SecurityUtils.getSubject().getPrincipal(); Map res=new HashMap<>(); res.put("result","you are "+jwtUser); res.put("token",JwtUtil.createJwtTokenByUser(jwtUser)); return res; } ``` JwtUser是携带在JwtToken中的用户信息,因为no-session服务不再储存用户信息,所以用户信息就得放在jwtToken中携带,这也是jwt的规范之一。同时这个jwtUser也即是在先前第3步骤的返回值第一个参数中配置进去的用户信息,你可以根据需要自行设定这个对象,步骤3中传进去啥,getSubject中取出来的就是啥。 注意返回值中还需要加上新生成的Jwt token,因为token有过期时间,所以一次成功的带jwt的请求成功返回时,还应当把新的token带给客户端,供它下次请求时使用。进阶些的做法是仅在token即将过期时才生成新token返回给客户端,从而节约一些服务器资源。 9. 客户端在拿到返回信息后,将token中的内容取代步骤1中的旧token,下次请求时用同样的规则带上即可。如果用了即将过期时才刷新token的机制且还没到token刷新时间,则继续使用旧token即可。如此新token连续不断地替换掉旧token,用户的登录状态就能视为一直保持。 10. 当然如果两次请求的间隔时间超过了token中预设的过期时间(即上面JWTUtil源码中的EXPIRE_TIME),则token验证会不通过,提示tokne过期,此时客户端应重新把页面跳转到用户名和密码的登录页要求用户重新登录。 ## 权限管理 首先你的用户-权限的数据模型要符合RBAC规范,这个概念这里不再赘述。 因为服务端不存用户信息了,所以此时role、permission和这两级数据和user怎么关联就是一个问题,我这里决定的方案是,roles信息跟着user一起存在jwt token里,然后permissions和role的对应因为相对固定,所以在服务端维护一份对应表即可。 代码也是在TokenValidateAndAuthorizingRealm中,这里把权限相关部分贴一遍 ```java @Slf4j @Component public class TokenValidateAndAuthorizingRealm extends AuthorizingRealm { UserService userService; Map> rolePermissionsMap; @Autowired public void setUserService(UserService userService){ this.userService=userService; rolePermissionsMap= userService.getRolePermissionMap(); //自动注入时查询一次存成变量,避免每次权限管理都去调用userService } ……//身份验证部分省略 @Override//权限管理 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { log.debug("doGetAuthorizationInfo 权限验证"); JwtUser user = (JwtUser) SecurityUtils.getSubject().getPrincipal(); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); simpleAuthorizationInfo.addRoles(user.getRoles());//roles跟着user走,放到token里。 Set stringPermissions = new HashSet(); for (String role : user.getRoles()) { stringPermissions.addAll(rolePermissionsMap.get(role)); } simpleAuthorizationInfo.addStringPermissions(stringPermissions); return simpleAuthorizationInfo; } } ``` rolePermissionsMap顾名思义,就是所有角色-权限的对照表。这里配置一份后供以后每次用户有需要时调用,查出权限集合。 doGetAuthorizationInfo方法,本质是返回当前用户所拥有的角色和权限的集合,角色本身就存在token里,用user.getRoles()即可获取;权限通过对照表(rolePermissionsMap),由roles查询添加而来,代码应该都不难懂。 在controller中配一个这样的方法来试用该功能 ```java @GetMapping("/permissionDemo") @RequiresPermissions("pd") public Map permissionDemo(){ Map res=new HashMap<>(); res.put("result","you have got the permission [pd]"); JwtUser jwtUser = (JwtUser) SecurityUtils.getSubject().getPrincipal(); res.put("token",JwtUtil.createJwtTokenByUser(jwtUser)); return res; } ``` @RequiresPermissions("pd")表示拥有"pd"权限的用户才有访问当前方法的权限。 用postman脚本测试,zhang3(拥有admin角色以及pd权限)可以正常访问,li4(没有pd权限)则会返回异常。 ## 异常返回 自行阅读GlobalExceptionController即可,与本帖主题关系不大的代码就不在这里专门说了。 ```java @Slf4j @RestControllerAdvice public class GlobalExceptionController { // 身份验证错误 @ExceptionHandler(AuthenticationException.class) public ResponseEntity authenticationExceptionHandler(AuthenticationException e) { log.error("AuthenticationException"); log.error(e.getLocalizedMessage()); Map body=new HashMap(); body.put("status", HttpStatus.FORBIDDEN.value()); body.put("message",e.getLocalizedMessage()); body.put("exception",e.getClass().getName()); body.put("error", HttpStatus.FORBIDDEN.getReasonPhrase()); return new ResponseEntity(body, HttpStatus.FORBIDDEN);//仅是示例,按需求定义 } //权限验证错误 @ExceptionHandler(UnauthorizedException.class) public ResponseEntity unauthorizedExceptionHandler(UnauthorizedException e) { log.error("unauthorizedExceptionHandler"); log.error(e.getLocalizedMessage()); Map body=new HashMap(); body.put("status", HttpStatus.UNAUTHORIZED.value()); body.put("message",e.getLocalizedMessage()); body.put("exception",e.getClass().getName()); body.put("error", HttpStatus.UNAUTHORIZED.getReasonPhrase()); return new ResponseEntity(body, HttpStatus.UNAUTHORIZED);//仅是示例,按需求定义 } //对应路径不存在 @ExceptionHandler(NoHandlerFoundException.class) public ResponseEntity noHandlerFoundExceptionHandler(NoHandlerFoundException e) { log.error("noHandlerFoundExceptionHandler"); log.error(e.getLocalizedMessage()); Map body=new HashMap(); body.put("message",e.getLocalizedMessage()); body.put("exception",e.getClass().getName()); body.put("error", HttpStatus.NOT_FOUND.getReasonPhrase()); return new ResponseEntity(body, HttpStatus.NOT_FOUND);//仅是示例,按需求定义 } @ExceptionHandler(Exception.class) public ResponseEntity exceptionHandler(Exception e) { log.error("exceptionHandler"); log.error(e.getLocalizedMessage()); log.error(e.getStackTrace().toString()); Map body=new HashMap(); body.put("message",e.getLocalizedMessage()); body.put("exception",e.getClass().getName()); body.put("error", HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()); return new ResponseEntity(body, HttpStatus.INTERNAL_SERVER_ERROR);//仅是示例,按需求定义 } } ```