2021终极版shiro+jwt整合策略,包含shiro1.5+新特性,极简配置,全网独家。

前言:shiro1.4的配置之繁琐业内闻名,其实它自1.5之后就有了不小的改进,能够大大精简我们前期的整合配置工作。但离奇的是1.5版至今也好几年了,网上依然铺天盖地都是1.4版的繁琐教程,所以干脆就由我来填上这一小块空白,回馈一下开源社区。

另:你也可以直接使用本人开发的框架KRest来实现两者的集成使用,只需完成一些最必要的配置即可在您项目内添加一套完整的RESTful服务的核心支持功能能,简易配置无感使用。
目前包含的功能是jwt认证、通信加密、接口权限控制。
项目源码在gitee上 https://gitee.com/ckw1988/krest ,也发布到了maven中央库,使用非常方便。
如果您出于对知识的热忱和追求依然打算自己亲手完成一套shiro+jwt的配置,那么请继续往下看下去。

本文在介绍配置时会深入讲解一些相关shiro和jwt的机制原理,所以此贴同时也是一篇shiro机制原理的介绍教程。

下面正式开始。

示例源码

源码地址:https://gitee.com/ckw1988/shiro-jwt-integration
原文地址:https://blog.csdn.net/ckw1988/article/details/123691453

并包含一个调试用的postman脚本,强烈建议下载下来跑通了再来看教程,心里比较踏实。

配置文件

首先在pom里配上shiro1.8(本文撰写时的最新版)

<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring-boot-web-starter</artifactId><version>1.8.0</version>
</dependency>

然后是配置文件,如今在的1.8也换上了springboot自动装配机制,config中只需配置两个bean。代码如下:

@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<String, String> 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旧版本的时代,最靠谱的方案就是继承HttpAuthenticationFilter自行实现一个过滤器来处理jwt功能,如今这个BearerHttpAuthenticationFilter即是该功能的官方实现。我们只需学会配置即可。

  • factoryBean.setGlobalFilters(Arrays.asList(“noSessionCreation”))。
    这里配置了个特别强大的过滤器NoSessionCreationFilter。shiro默认是保存状态的服务,所以必须配上这个filter,才能将整个shiro系统转换为一个真正的no-session服务。所以说,假如有啥教程缺乏了这一步骤,尽管功能一样能跑,但他们所说的no-session并不是真正的no-session。

如今的config部分只需要配置这么多,旧方案里那一大堆东西都不再需要了。此后你自定义的realm只需在类定义时加上@Component标签,即可由shiro自动装配使用(赞美SpringBoot)。

身份验证。

因为我们整个服务已经变成no-session状态,所以事实上对shiro来说整个系统中已经不存在"已登录用户"这个概念了,这就意味着每一次独立的请求事实上都需要一个身份验证过程,这种身份验证行为在shiro里都被称为"登录(Login)"。

整合后的流程为:首次登陆时用户提交用户名和密码,验证通过后服务器生成一个初始的Jwt Token返回给客户端。此后客户端在任何请求时都把Jwt Token带上,服务端如果验证通过后即视为当次身份验证通过(或者说以token的方式登陆成功)。这个流程也即jwt token的官方标准使用方法。

既然有两种登陆方式,则需要两个realm,我们需要一个UsernamePasswordRealm来处理用户名和密码登录;一个TokenValidateAndAuthorizingRealm,处理token验证方式的"登录"。

UsernamePasswordRealm

  1. 首先,这种登录方式无论token的封装和login的调用都需要由用户自行完成,所以也特别适合演示shiro的完整处理流程。参考语法如下,定义在controller中

     /*** 登陆*/@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);//显示调用登录方法//生成返回tokenMap<String,String> 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处理身份验证的环节。我们先看代码

     //Username Password Realm,用户名密码登陆专用Realm@Slf4j@Componentpublic class UsernamePasswordRealm extends AuthenticatingRealm {@Autowiredprivate 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*/@Overridepublic Class getAuthenticationTokenClass() {log.info("getAuthenticationTokenClass");return UsernamePasswordToken.class;}@Overridepublic boolean supports(AuthenticationToken token) {//继承但啥都不做就为了打印一下infoboolean res = super.supports(token);//会调用↑getAuthenticationTokenClass来判断log.debug("[UsernamePasswordRealm is supports]" + res);return res;}/*** 用户名和密码验证,login接口专用。*/@Overrideprotected 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来处理。

       @Overridepublic Class getAuthenticationTokenClass() {log.info("getAuthenticationTokenClass");return UsernamePasswordToken.class;}
    
  4. doGetAuthenticationInfo,顾名思义,是配置验证成功后的用户信息,同时也是为后续的验证提供素材的步骤。将其返回值按照new SimpleAuthenticationInfo(jwtUser, passwordFromDB, ByteSource.Util.bytes(salt),getName());来配,第一个参数是登陆成功后的用户信息,第二个是来自数据库经过处理的目标密码,第三个是密码的盐。

    /*** 用户名和密码验证,login接口专用。*/@Overrideprotected 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,用自己熟悉的加密策略和加密工具自由地实现,学习成本更低,灵活度更高,也更便于和注册方法中的加密策略保持统一(注册的步骤shiro不会接管)。这里出于教学目的选择展示他自带的用法,自定义matcher的示例参考后面jwt的realm。

        /*构造器里配置Matcher*/public UsernamePasswordRealm() {super();HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();hashedCredentialsMatcher.setHashAlgorithmName("md5");hashedCredentialsMatcher.setHashIterations(2);//密码保存策略一致,2次md5加密this.setCredentialsMatcher(hashedCredentialsMatcher);}
    
  6. 再次提醒一下不要遗漏@Component注解。

进阶扩展

事实上这个UsernamePasswordRealm是个可选环节。获得初始jwt token的方式多种多样,可以是用户名密码登陆,可以是手机+验证码登陆,可以是第三方平台登录,甚至可以是通过其他服务登录已经获得了jwt token后再拿到本服务上来使用。

所以事实上最简单做法是,只要你认为某个登陆请求已经完成了登陆步骤,只需要在返回值中带上一个新token

   ……res.put("token",JwtUtil.createJwtTokenByUser(jwtUser));

即可视为登陆成功。之后的其他请求自然会进入你在TokenValidateAndAuthorizingRealm中定义好的验证流程来处理。

TokenValidateAndAuthorizingRealm

 @Slf4j@Componentpublic class TokenValidateAndAuthorizingRealm extends AuthorizingRealm {//权限管理部分的代码先行略过//......public TokenValidateAndAuthorizingRealm() {//CredentialsMatcher,自定义匹配策略(即验证jwt token的策略)super(new CredentialsMatcher() {@Overridepublic 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;}});}@Overridepublic String getName() {return "TokenValidateAndAuthorizingRealm";}@Overridepublic Class getAuthenticationTokenClass() {//设置由本realm处理的token类型。BearerToken是在filter里自动装配的。return BearerToken.class;}@Overridepublic 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);//只带着用户名和rolesSimpleAuthenticationInfo 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脚本。

    完成后的效果类似下表

    KEY VALUE
    Authorization Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbiJdLCJleHAiOjE2NDY3OTcyMjEsInVzZXJuYW1lIjoiemhhbmczIn0.HroVIdxf5qmpjWJlOs0QGW7OtaTcjirD9aMViK4oDdI

    注意:Bearer和令牌字符串之间有且仅有一个半角空格。显示时可能会自动换行但不要被迷惑,复制粘贴一下就知道了。

  2. 不同于用户名密码的登录方式,包装token和login调用的操作,已经由filter接管,所以我们不需要自己来写这部分代码,直接进入realm的环节。

  3. 然后实现realm的代码,依然覆盖getAuthenticationTokenClass方法,本类中令该方法返回BearerToken.class(即由authcBearer filter自动封装而成的token类型)。由此shiro就就会将authcBearer filter中发起的“登录”请求交给该realm处理。

     @Overridepublic Class getAuthenticationTokenClass() {//设置由本realm处理的token类型。BearerToken是在filter里自动装配的。return BearerToken.class;}
    

    注意区分两种token的概念,jwt token是一串字符串,用于在客户端和服务端常规通信时的身份保持。而shiro中的token是一个java bean,它是对用户身份信息的一种封装,用于服务器内部、在shiro框架中包装和传递待验证的用户信息:在用户名密码登陆时它是封装了用户名密码的UsernamePasswordToken,在jwt验证时它是封装了jwt token字符串的BearerToken。

  4. 接下来是实现doGetAuthenticationInfo方法,该方法用于装配登陆成功后的用户信息(返回值的第一个参数)和供验证的身份信息(返回值的第二个参数),第三个参数大约是用于区分本次登陆是由哪个realm通过的,不太重要,带上即可。

       @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);//只带着用户名和rolesSimpleAuthenticationInfo 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;
    }
    
  5. 自定义且实现一个CredentialsMatcher,用以处理验证jwt token的登陆方式,核心中的核心。我将其用匿名类创建在realm的构造器里。两个入参来自上一步骤,语法很好懂,看源码即可。

    public TokenValidateAndAuthorizingRealm() {//CredentialsMatcher,自定义匹配策略(即验证jwt token的策略)super(new CredentialsMatcher() {@Overridepublic 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;}});
    }
    
  6. 同时,该步骤中还用到了自己封装的工具类JwtUtil,代码如下:

      @Slf4jpublic 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信息的tokenreturn 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拿,确保用户用的是自己的tokenlog.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后再生成的tokenDecodedJWT 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验证规则有任何自定义的要求都在这里实现。
    该类中所用到的JWT验证框架是

      <dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.18.2</version></dependency>
    

    配到pom里去。

  7. 至此,jwt验证部分的功能配置完毕。DemoController中的whoami方法是这部分的使用范例。

      @GetMapping("/whoami")public Map whoami(){JwtUser jwtUser = (JwtUser) SecurityUtils.getSubject().getPrincipal();Map<String,String> 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返回给客户端,从而节约一些服务器资源。该功能在我自行封装的KRest框架中也已经实现,欢迎选用。

  8. 客户端在拿到返回信息后,将token中的内容取代步骤1中的旧token,下次请求时用同样的规则带上即可。如果用了即将过期时才刷新token的机制且还没到token刷新时间,则继续使用旧token即可。如此新token连续不断地替换掉旧token,用户的登录状态就能视为一直保持。

  9. 当然如果两次请求的间隔时间超过了token中预设的过期时间(即上面JWTUtil源码中的EXPIRE_TIME),则token验证会不通过,提示tokne过期,此时客户端应重新把页面跳转到用户名和密码的登录页要求用户重新登录。

权限管理

首先你的用户-权限的数据模型要符合RBAC规范,这个概念这里不再赘述。
因为服务端不存用户信息了,所以此时role、permission和这两级数据和user怎么关联就是一个问题,我这里决定的方案是,roles信息跟着user一起存在jwt token里,然后permissions和role的对应因为相对固定,所以在服务端维护一份对应表即可。
代码也是在TokenValidateAndAuthorizingRealm中,这里把权限相关部分贴一遍

    @Slf4j@Componentpublic class TokenValidateAndAuthorizingRealm extends AuthorizingRealm {UserService userService;Map<String, Collection<String>> rolePermissionsMap;@Autowiredpublic 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<String> stringPermissions = new HashSet<String>();for (String role : user.getRoles()) {stringPermissions.addAll(rolePermissionsMap.get(role));}simpleAuthorizationInfo.addStringPermissions(stringPermissions);return simpleAuthorizationInfo;}}

rolePermissionsMap顾名思义,就是所有角色-权限的对照表。这里配置一份后供以后每次用户有需要时调用,查出权限集合。

doGetAuthorizationInfo方法,本质是返回当前用户所拥有的角色和权限的集合,角色本身就存在token里,用user.getRoles()即可获取;权限通过对照表(rolePermissionsMap),由roles查询添加而来,代码应该都不难懂。

在controller中配一个这样的方法来试用该功能

    @GetMapping("/permissionDemo")@RequiresPermissions("pd")public Map permissionDemo(){Map<String,String> 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即可,与本帖主题关系不大的代码就不在这里专门说了。


@Slf4j
@RestControllerAdvice
public class GlobalExceptionController {// 身份验证错误@ExceptionHandler(AuthenticationException.class)public ResponseEntity authenticationExceptionHandler(AuthenticationException e) {log.error("AuthenticationException");log.error(e.getLocalizedMessage());Map<String,Object> body=new HashMap<String,Object>();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<String,Object> body=new HashMap<String,Object>();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<String,Object> body=new HashMap<String,Object>();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<String,Object> body=new HashMap<String,Object>();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);//仅是示例,按需求定义}
}

补充1:解决jwt验证出错时的异常丢失和跨域问题

这两个问题各自通过一个自定义filter来解决,所以放这里一起介绍。

1. jwt验证出错时的异常丢失

这个问题源自多realm时的异常机制:当某个realm出现异常时并不会直接一路抛上来,而是去验下一个realm,确认每个realm都异常或无法通过时,才统一返回个请求被拒,所以具体realm中抛出的异常的细节会丢失,在外面只收得到一个。
目前的解决办法是手动继承一下BearerHttpAuthenticationFilter,在请求被拒时补上一个异常抛出。有更好的方案也欢迎留言讨论。
(后续补充:按前面说的,用户名密码验证可以不走shiro的realm,那么整个shiro就不用配成多realm模式,单独一个realm用来验证jwt也许是个更好的选择,等我有空确认一下。)

@Slf4j
public class JwtFilter extends BearerHttpAuthenticationFilter {@Overrideprotected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {boolean res=super.onAccessDenied(request, response);//jwt的登录在这里面log.info("onAccessDenied "+res);if (!res){throw new RuntimeException("token失效或异常,请重新登录");//jwt验证器的错误抛不上来,应该是shiro机制的不完善()}
//        jwt验证失败导致的登陆失败里,拿不到jwt验证失败的具体异常,因为要试过多个realmjwt的token错了还会去试其他realm,
//        导致他把具体异常截断了,这里只拿得到一个"试过所有realm但是都没登陆成功"的异常。return res;}
}

filter的配置与后面跨域的filter一起说,一样是通过自定义filter解决。

2.跨域问题

一样是增加一个自定义filter处理

@Slf4j
public class CorsFilter extends PathMatchingFilter {@Overrideprotected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {HttpServletRequest httpRequest = (HttpServletRequest) request;HttpServletResponse httpResponse = (HttpServletResponse) response;configHeaders(httpRequest, httpResponse);//options和其他方法共用if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {log.debug("收到一个OPTIONS请求--"+httpRequest.getRequestURI());httpResponse.setStatus(HttpStatus.NO_CONTENT.value());return false;}return super.preHandle(request, response);}private void configHeaders(HttpServletRequest request, HttpServletResponse response){//↓ 该部分均可按照自己需要自行订制,这里只是做个参考response.setHeader("Access-Control-Allow-Origin", "http://yourclientdomain:1111");//TODO 配置你自己允许的前端源response.setHeader("Access-Control-Allow-Methods", request.getMethod());response.setHeader("Access-Control-Allow-Credentials", "true");response.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization");//防止乱码,适用于传输JSON数据response.setHeader("Content-Type","application/json;charset=UTF-8");}
}

3.配置以上两个自定义filter

在ShiroConfig中增加相应配置,具体说明看后面的注释即可。

    @Beanpublic ShiroFilterFactoryBean shiroFilterFactoryBean(SessionsSecurityManager securityManager) {ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();//配置自定义filterMap<String, Filter> filterMap = new HashMap<>();filterMap.put("cors",new CorsFilter());filterMap.put("jwt", new JwtFilter());factoryBean.setFilters(filterMap);……// jwt验证过滤器。
//        filterRuleMap.put("/**", "authcBearer");filterRuleMap.put("/**", "jwt");//用自定义的jwt代替authcBearer,前者是后者的子类factoryBean.setGlobalFilters(Arrays.asList("cors","noSessionCreation"));//将corsFilter配置成全局,注意不能放在上面jwt的位置。只有放在这里才能不受"anon"等其他过滤器的影响,是真正的全局。factoryBean.setSecurityManager(securityManager);factoryBean.setFilterChainDefinitionMap(filterRuleMap);return factoryBean;}

续签策略

本示例中采用了最简单的jwt续签机制,即在普通的业务请求中检查过期时间后直接发放新token,该策略安全性较低,如果在公司企业的项目中使用,建议参考另一篇拙作《“长短令牌三验证”的JWT续签策略》进一步完善续签机制。

【SpringBoot】2021终极版shiro+jwt整合策略,包含shiro1.5+新特性,极简配置,全网独家。相关推荐

  1. python 3.9特性,Python 3.9 正式版要来了,会有哪些新特性?

    " 编译:CSDN-明明如月,作者:James Briggs Python 发布了版本号为 3.9.0b3 的 beta 版,后续即将发布 Python 3.9 的正式版.该版本包含了一些令 ...

  2. python3.9出了吗_Python 3.9 正式版要来了,会有哪些新特性?

    Python 正在一直马不停蹄地更新,历时数月,我们迎来了又一个 Beta 版 -- 3.9.0b3,Python 3.9 正式版已经不远了,一起来看它带来了哪些值得开发者关注的重要新特性! 以下为译 ...

  3. python3 循环写入一对多键值对_Python 3.9 正式版要来了,会有哪些新特性?

    Python 正在一直马不停蹄地更新,历时数月,我们迎来了又一个 Beta 版 -- 3.9.0b3,Python 3.9 正式版已经不远了,一起来看它带来了哪些值得开发者关注的重要新特性! 作者 | ...

  4. Python 3.9 正式版要来了,会有哪些新特性?

    Python 正在一直马不停蹄地更新,历时数月,我们迎来了又一个 Beta 版 -- 3.9.0b3,Python 3.9 正式版已经不远了,一起来看它带来了哪些值得开发者关注的重要新特性! 作者 | ...

  5. 走进JavaWeb技术世界16:极简配置的SpringBoot

    本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial 喜欢的话麻烦点下 ...

  6. SpringBoot 3.0最低版本要求的JDK 17,这几个新特性不能不知道!

    △Hollis, 一个对Coding有着独特追求的人△ 这是Hollis的第 387 篇原创分享 作者 l Hollis 来源 l Hollis(ID:hollischuang) 最近,有很多人在传说 ...

  7. Asp.net Core中SignalR Core预览版的一些新特性前瞻,附源码(消息订阅与发送二进制数据)

    前言 一晃一个月又过去了,上个月有个比较大的项目要验收上线.所以忙的脚不沾地.现在终于可以忙里偷闲,写一篇关于SignalR Core的文章了. 先介绍一下SignalR吧,如下: ASP.NET S ...

  8. 单realm模式下前后端分离实现springboot+shiro+jwt+vue整合

    shiro+jwt实现前后端分离 一.RBAC概念 基于角色的权限访问控制(Role-Based Access Control)作为传统访问控制(自主访问,强制访问)的有前景的代替受到广泛的关注.在R ...

  9. 【Gorho】springboot整合Shiro+jwt 前后端分离 超级详细的shiro+jwt鉴权过程

    shiro+jwt+springboot 说在前面 简介 项目环境(pom.xml) 项目结构(各种包和类) 鉴权流程 具体代码 配置Shiro 配置JWTUtils 定义JwtFilter 定义Jw ...

最新文章

  1. js如何动态的加载js文件
  2. 运动控制器对比:Windows MR、Rift、Vive、PSVR(译文修正版)
  3. 【Flask项目】sqlalchemy原生sql查询,返回字典形式数据
  4. Kubernetes pod滚动升级rolling update的一些例子,截图和命令
  5. 海洋工程-专业名词-学科关键词(终极版)
  6. VMware虚拟机下安装Ubuntu16.04镜像完整教程
  7. 计算机室内设计cad实践报告,cad实习报告3000字
  8. iPhone 12 Pro火爆程度超预期 苹果紧急向关键组件厂商加单
  9. 17.如何正确使用TCP
  10. 拓端tecdat|R语言聚类有效性:确定最优聚类数分析IRIS鸢尾花数据和可视化
  11. 电商支付-使用Restful api接口集成Paypal支付方式(一)
  12. 2022年中科院信工所考研杂记
  13. 图像 像素与分辨率的关系
  14. mysql 正则表达式 包含中文_MYSQL 中文检索匹配与正则表达式
  15. 计算机组成存储器实验心得,《计算机组成原理》存储器读写实验报告
  16. 和Windows10的垃圾“照片”说再见,找回“Windows照片查看器”
  17. Deep Learning Chapter01:机器学习中线性代数
  18. 基于预训练模型 ERNIE 实现语义匹配
  19. 【核心基础知识】javascript的数据类型
  20. 农村土地确权之调查公示 —— 三轮公示注意问题说明

热门文章

  1. 用html做个随机点名系统代码,html座位表随机点名的实例代码
  2. centos7-14-升级系统内核到最新版
  3. 冷启动和热启动的小知识
  4. C语言system函数
  5. 在虚拟机上安装PFsense详细图解
  6. 开机运行到window启动画面马上蓝屏重启
  7. Android 10 获取图片失败解决办法
  8. HTML5+CSS大作业——明星薛之谦(7页面))带轮播特效
  9. SmartGit的使用教程(详细)
  10. 步进电机的匀加速程序