写在前面

本文的出现表示不再进行 Spring Security Oauth 实现的研究了,原因是原开源项目已经被废弃了不再更新了,而且 Oauth 实现的内容有些奇怪,新的项目 spring-authorization-server 目前才发布到 0.1.0,默认只提供了基于内存的实现,个人认为还不是很完善,不适合用到项目中。而且 Spring SecurityOauth 流程都实现了,要修改还得从新研究 spring-authorization-server 的实现逻辑,然后进行修改定制,太耗费精力了,不如使用 Shiro 自己实现 Oauth 的逻辑。

但是本文和 Oauth 无关,单纯是 Shiro + Springboot + JWT 的整合。

本文使用的依赖如下:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.7.1</version>
</dependency>
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.11.0</version>
</dependency>

还有一点要说明的是,Shiro 有针对 SpringBoot 的默认配置,详细配置见官网:

但是在本文中并没有使用 SpringBoot 集成方式,也没有按官网的教程导入默认配置,因为 Shiro 要运行起来配置相对较于简单,需要配置的并不多,而且从本人的了解来看,Shiro 适合基于 Session 进行登录状态存储的模式,使用 Jwt 将状态存储在了 Token 里,这种模式对配置的逻辑需要做部分更改,而且还有些坑,所以没有基于默认配置进行配置,需要注意的地方以下会有说明。

记录贴,刚接触 Shiro,内容如有错误,望大佬指正。

1. 实现源码

示例项目已经上传到 Github 直接可用:https://github.com/nineya/framework-study/tree/v0.1.0/shiro-study

项目结构:

Shiro + Springboot + JWT 整合

1.1 SpringBoot 程序入口

package com.nineya.shiro;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author 殇雪话诀别
 * 2021/2/15
 * 程序入口
 */
@SpringBootApplication
public class ShiroApplication {
    public static void main(String[] args) {
        SpringApplication.run(ShiroApplication.class);
    }
}

1.2 实体类

简单自定义用户、角色和权限实体类,一个用户可以包含多个角色,一个角色可以包含多个权限。

package com.nineya.shiro.entity;

/**
 * @author 殇雪话诀别
 * 2021/2/15
 * 权限
 */
public class Permissions {
    private long id;
    private String permissionsName;

    public Permissions() {
    }

    public Permissions(long id, String permissionsName) {
        this.id = id;
        this.permissionsName = permissionsName;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getPermissionsName() {
        return permissionsName;
    }

    public void setPermissionsName(String permissionsName) {
        this.permissionsName = permissionsName;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("Permissions{");
        sb.append("id=").append(id);
        sb.append(", permissionsName='").append(permissionsName).append('\'');
        sb.append('}');
        return sb.toString();
    }
}
package com.nineya.shiro.entity;

import java.util.Set;

/**
 * @author 殇雪话诀别
 * 2021/2/15
 * 角色,包含权限集合
 */
public class Role {
    private long id;
    private String roleName;
    /**
     * 角色拥有的权限集合
     */
    private Set<Permissions> permissions;

    public Role() {
    }

    public Role(long id, String roleName, Set<Permissions> permissions) {
        this.id = id;
        this.roleName = roleName;
        this.permissions = permissions;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    public Set<Permissions> getPermissions() {
        return permissions;
    }

    public void setPermissions(Set<Permissions> permissions) {
        this.permissions = permissions;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("Role{");
        sb.append("id=").append(id);
        sb.append(", roleName='").append(roleName).append('\'');
        sb.append(", permissions=").append(permissions);
        sb.append('}');
        return sb.toString();
    }
}
package com.nineya.shiro.entity;

import java.util.Set;

/**
 * @author 殇雪话诀别
 * 2021/2/15
 * 用户,包含角色集合
 */
public class User {
    private long uid;
    private String userName;
    private String password;
    /**
     * 用户对应的角色
     */
    private Set<Role> roles;

    public User() {
    }

    public User(long uid, String userName, String password, Set<Role> roles) {
        this.uid = uid;
        this.userName = userName;
        this.password = password;
        this.roles = roles;
    }

    public long getUid() {
        return uid;
    }

    public void setUid(long uid) {
        this.uid = uid;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Set<Role> getRoles() {
        return roles;
    }

    public void setRoles(Set<Role> roles) {
        this.roles = roles;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("User{");
        sb.append("uid=").append(uid);
        sb.append(", userName='").append(userName).append('\'');
        sb.append(", password='").append(password).append('\'');
        sb.append(", roles=").append(roles);
        sb.append('}');
        return sb.toString();
    }
}

1.3 服务层

创建 Login 服务层实现,能够取读取用户信息进行用户登录操作,在实际使用时此处修改为;连接数据库获取数据。

package com.nineya.shiro.service;

import com.nineya.shiro.entity.User;

/**
 * @author 殇雪话诀别
 * 2021/2/15
 * 用户登录服务接口
 */
public interface LoginService {

    /**
     * 通过用户名取得用户
     *
     * @param name 用户名
     * @return
     */
    User getUserByName(String name);
}
package com.nineya.shiro.service.impl;

import com.nineya.shiro.entity.Permissions;
import com.nineya.shiro.entity.Role;
import com.nineya.shiro.entity.User;
import com.nineya.shiro.service.LoginService;
import org.springframework.stereotype.Service;

import java.util.*;

/**
 * @author 殇雪话诀别
 * 2021/2/15
 * 用户登录服务,使用Map<用户名, 用户信息> 的格式存储用户信息进行查询
 * 在实际使用中将此处修改为连接数据库查询即可
 */
@Service
public class LoginServiceImpl implements LoginService {
    private final Map<String, User> users = new HashMap<>();

    public LoginServiceImpl() {
        // 定义三个权限
        Permissions permissions1 = new Permissions(1, "create");
        Permissions permissions2 = new Permissions(2, "delete");
        Permissions permissions3 = new Permissions(3, "select");
        // 定义两个角色
        Role role1 = new Role(1, "read", new HashSet<Permissions>(){{add(permissions3);}});
        Role role2 = new Role(1, "write", new HashSet<Permissions>(){{add(permissions1);add(permissions2);}});
        // 定义三个用户分别对应两个角色
        users.put("observe", new User(1, "observe", "123456", Collections.singleton(role1)));
        users.put("admin", new User(1, "admin", "123456", Collections.singleton(role2)));
        users.put("user", new User(1, "user", "123456", new HashSet<Role>(){{add(role1); add(role2);}}));
    }

    @Override
    public User getUserByName(String name) {
        return users.get(name);
    }
}

1.4 Token 工具类

token 工具类负责 Jwt 的创建和解析,在本文中将 username 加入到负载,实际使用中可以将角色等信息一起加入。但是不应该将大量数据和隐式数据加入到 jwt 中,因为 jwt 没有加密,大量的数据会导致 token 过长,加重网络负载。

package com.nineya.shiro.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Date;

/**
 * jwt处理类
 *
 * @author 殇雪话诀别
 * 2020/11/29
 */
@Component
public class UserTokenUtil {
    /**
     * jwt 加密算法
     */
    private Algorithm algorithm;
    private JWTVerifier verifier;

    public UserTokenUtil() {
        algorithm = Algorithm.HMAC256("secret");
        verifier = JWT.require(algorithm).build();
    }

    /**
     * 创建用户token,并将token创建时间存入
     *
     * @param username 用户名称
     * @return token字符串
     */
    public String createToken(String username) {
        Date expireTime = new Date(System.currentTimeMillis() + 60 * 60 * 1000);
        return JWT.create()
                .withClaim("username", username)
                .withExpiresAt(expireTime)
                .sign(algorithm);
    }

    /**
     * 校验token合法性
     *
     * @param token
     * @return
     */
    public DecodedJWT verifyToken(String token) {
        try {
            return verifier.verify(token);
        } catch (Exception e) {
            throw new TokenExpiredException("token 解析失败");
        }
    }

    /**
     * 取得用户名
     *
     * @param token
     * @return
     */
    public String getUserName(String token) {
        DecodedJWT jwt = verifyToken(token);
        return jwt.getClaim("username").asString();
    }
}

1.5 控制器(开始涉及Shiro)

添加 login 登录接口实现,添加几个简单接口作为后续的调用示例,此处开始设计上面所述的 jwt 逻辑不同的问题了。

package com.nineya.shiro.controller;

import com.nineya.shiro.entity.User;
import com.nineya.shiro.service.LoginService;
import com.nineya.shiro.util.UserTokenUtil;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.Date;

/**
 * @author 殇雪话诀别
 * 2021/2/15
 */
@RestController
public class LoginController {
    @Resource
    private LoginService loginService;
    @Resource
    private UserTokenUtil tokenUtil;

    /**
     * 使用 jwt 进行登录时此处逻辑将有些不同
     * 如果没有使用Token,用户将在此方法中通过 subject.login(usernamePasswordToken) 进行登录。
     * 使用 jwt 时,将不再使用 session 存储登录状态,subject.login(usernamePasswordToken) 逻辑将在 Filter 解析 token 时进行,并且
     * 每次请求都需要进行 token 解析和登录操作。
     * 也就是说认证、授权两个步骤,原本只要登录时进行认证,每次请求进行授权,使用 jwt 后每次请求都需要记性jwt解析、认证和授权三个步骤。
     * @param userName 用户名
     * @param password 密码
     * @return
     */
    @GetMapping("/login")
    public String login(@RequestParam("userName") String userName, @RequestParam("password") String password) {
        if (StringUtils.isEmpty(userName) || StringUtils.isEmpty(password)) {
            return "请输入用户名和密码!";
        }
        User user = loginService.getUserByName(userName);
        if (!user.getPassword().equals(password)) {
            return "密码不正确!";
        }
        return tokenUtil.createToken(userName);
    }

    // 这是没有使用 jwt 时,基于 session 的实现方式
//    @GetMapping("/login")
//    public String login(User user) {
//        if (StringUtils.isEmpty(user.getUserName()) || StringUtils.isEmpty(user.getPassword())) {
//            return "请输入用户名和密码!";
//        }
//        //用户认证信息
//        Subject subject = SecurityUtils.getSubject();
//        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
//                user.getUserName(),
//                user.getPassword()
//        );
//        try {
//            //进行验证,这里可以捕获异常,然后返回对应信息
//            subject.login(usernamePasswordToken);
////            subject.checkRole("admin");
////            subject.checkPermissions("query", "add");
//        } catch (UnknownAccountException e) {
//            return "用户名不存在!";
//        } catch (AuthenticationException  e) {
//            return "账号或密码错误!";
//        } catch (AuthorizationException e) {
//            return "没有权限";
//        }
//        return "login success";
//    }

    /**
     * 允许角色为 read 且为 write 用户访问
     * @return
     */
    @RequiresRoles({"read", "write"})
    @GetMapping("/admin")
    public String admin() {
        return "admin";
    }

    /**
     * 允许拥有 select 权限的用户访问
     * @return
     */
    @RequiresPermissions("select")
    @GetMapping("/select")
    public String select() {
        return "select";
    }

    /**
     * 允许拥有 create 权限的用户访问
     * @return
     */
    @RequiresPermissions("create")
    @GetMapping("/create")
    public String create() {
        return "create";
    }
}
package com.nineya.shiro.controller;

import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @author 殇雪话诀别
 * 2021/2/15
 * 对未通过权限认证的部分异常进行异常处理
 */
@ControllerAdvice
public class ExceptionController {

    @ExceptionHandler
    @ResponseBody
    public String ErrorHandler(AuthorizationException e) {
        return "没有通过权限验证!\n" + e.getMessage();
    }
}

1.6 自定义 JWT 过滤器

检查请求头是否有 Authorization 字段,如果包含则进行 token 解析和登录操作。

package com.nineya.shiro.filter;

import com.nineya.shiro.controller.ExceptionController;
import org.apache.shiro.authc.BearerToken;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

/**
 * @author 殇雪话诀别
 * 2021/2/15
 */
public class TokenFilter extends BasicHttpAuthenticationFilter {

    /**
     * 判断用户是否想要登入。
     * 检测header里面是否包含Authorization字段即可
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String authorization = req.getHeader(AUTHORIZATION_HEADER);
        return authorization != null;
    }

    /**
     * 该方法返回值表示是否跳过认证,这里如果返回 true,则不会再走一遍认证流程
     * 如果返回 false,则会执行 isAccessAllowed 方法,再执行isLoginAttempt方法,如果为false继续执行executeLogin
     * 方法,如果没有执行executeLogin或者执行结果也是false,则将执行sendChallenge方法,表示认证失败。
     * 返回 true 时,则必须在postHandle配置退出登录,这个方法将在执行完业务逻辑后执行,否则将导致下次没有携带token时直接使用上次的登录
     * 结果,从而非法访问接口。
     * 默认返回 false 时,isLoginAttempt这些方法将重复调用,所以不建议。如果要返回 false,建议复写 sendChallenge 方法,因为其响应内容为空。
     * 
     * 也可以将 isAccessAllowed 作为纯判断是否需要认证,或者不复写该方法,本文不提供实现,如果有问题欢迎留言
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //判断请求的请求头是否带上 "Token"
        if (isLoginAttempt(request, response)) {
            //如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
            try {
                return executeLogin(request, response);
            } catch (Exception e) {
                //token 错误
                e.printStackTrace();
                return false;
            }
        }
        //如果请求头不存在 Token,则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true
        return true;
    }

    /**
     * 用户登录
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader(AUTHORIZATION_HEADER);
        BearerToken jwtToken = new BearerToken(token, request.getRemoteAddr());
        getSubject(request, response).login(jwtToken);
        return true;
    }

    /**
     * 该方法将在过滤器执行完成后执行
     * 当isAccessAllowed默认为true时必须实现该方法
     * 在执行完请求后执行退出登录逻辑,否则下次请求时没有携带将可以直接访问接口,无须重新登录
     * @param request
     * @param response
     * @throws Exception
     */
    @Override
    protected void postHandle(ServletRequest request, ServletResponse response) throws Exception {
        SecurityUtils.getSubject().logout();
    }

    /**
     * 当 isAccessAllowed 可能返回false时需要复写该接口,否则默认将返回空白界面
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean sendChallenge(ServletRequest request, ServletResponse response) {
        super.sendChallenge(request, response);
        HttpServletResponse httpResponse = WebUtils.toHttp(response);
        httpResponse.setContentType("application/json; charset=utf-8");
        try {
            response.getWriter().write(
                    JSONObject.toJSONString(ResponseResult.failure(StatusCode.UNAUTHORIZED, "登录状态已失效!").toMap()));
        } catch (IOException e) {
            throw new UnauthenticatedException("登录状态已失效!");
        }
        return false;
    }

    /**
     * 登录异常处理类,该方法将捕获所有在本过滤器中遇到的异常,默认将会进行登录重试,重试失败后执行 afterCompletion 方法进行善后处理,
     * 后将异常封装为 ServletException。
     * 建议复写本方法 jwt 登录失败,重试也是无果的。
     * @param request
     * @param response
     * @param existing 异常信息,如果为 null表示没有异常,不需要进行处理
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void cleanup(ServletRequest request, ServletResponse response, Exception existing) throws ServletException, IOException {
        // 不管什么异常,全部抛出登录失败就是了
        if (existing != null) {
            sendChallenge(request, response);
        }
    }
}

1.7 自定义 Realm

Shiro 默认提供了 JdbcRealmIniRealmDefaultLdapRealm 等实现,但是并不是很符合我所需要的场景,也不能实现 jwt 的解析,所以采用继承 AuthorizingRealm 自定义 Realm 类,复写 doGetAuthorizationInfo(授权)和 doGetAuthenticationInfo(认证)两个接口。

此处其实有坑的,而且逻辑并不是很适合 jwt 的实现,但是没有好的方案进行修改,后续将详细说明。

  • 此处只是示例,在授权和认证两次的 loginService.getUserByName(name) 操作均可以想办法避免。

  • 可以在认证步骤解析 JWT 为中间对象,然后将中间对象作为 Principal,能够避免重复解析 JWT 带来性能浪费。

package com.nineya.shiro.config;

import com.nineya.shiro.entity.Permissions;
import com.nineya.shiro.entity.Role;
import com.nineya.shiro.entity.User;
import com.nineya.shiro.service.LoginService;
import com.nineya.shiro.util.UserTokenUtil;
import org.apache.catalina.realm.AuthenticatedUserRealm;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.util.stream.Collectors;

/**
 * 自定义 realm
 *
 * @author 殇雪话诀别
 * 2021/2/15
 */
public class StudyRealm extends AuthorizingRealm {
    @Resource
    private LoginService loginService;
    @Resource
    private UserTokenUtil tokenUtil;

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof BearerToken;
    }

    /**
     * 授权,在认证之后执行
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String token = (String) principals.getPrimaryPrincipal();
        String name = tokenUtil.getUserName(token);
        User user = loginService.getUserByName(name);
        // 添加角色和权限
        SimpleAuthorizationInfo simpleAuthenticationInfo = new SimpleAuthorizationInfo();
        for (Role role : user.getRoles()) {
            // 添加角色
            simpleAuthenticationInfo.addRole(role.getRoleName());
            // 添加权限
            simpleAuthenticationInfo.addStringPermissions(role.getPermissions().stream()
                    .map(Permissions::getPermissionsName).collect(Collectors.toSet()));
        }
        return simpleAuthenticationInfo;
    }

    /**
     * 认证
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        if (StringUtils.isEmpty(token.getPrincipal())) {
            return null;
        }
        String name = tokenUtil.getUserName((String) token.getPrincipal());
        User user = loginService.getUserByName(name);
        if (user == null) {
            return null;
        }
        // 第一个参数是主体,将会在授权时封装成PrincipalCollection.getPrimaryPrincipal()进行使用,所以必须将jwt内容传回
        // 第二个参数是认证信息,即密码,为后面验证可以通过,需要和token中的内容一样
        // 第三个参数是领域名称
        return new SimpleAuthenticationInfo(token.getPrincipal(), token.getCredentials(), user.getUserName());
    }
}

1.8 Shiro 配置

package com.nineya.shiro.config;

import com.nineya.shiro.filter.TokenFilter;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.Map;

/**
 * @author 殇雪话诀别
 * 2021/2/15
 * 配置类
 */
@Configuration
public class ShiroConfiguration {

    /**
     * 配置代理,没有配置将会导致注解不生效
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }

    /**
     * 配置代理,没有配置将会导致注解不生效
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 将自己的验证方式加入容器
     * @return
     */
    @Bean
    public Realm studyRealm() {
        StudyRealm studyRealm = new StudyRealm();
        return studyRealm;
    }

    /**
     * 不应该将过滤器的实现注册为bean,否则会导致Filter过滤器顺序混乱,导致抛出异常
     * 如果一定要注册为 Bean,可以使用 Order 指定优先级,还未尝试过
     * @return
     */
    public TokenFilter tokenFilter() {
        return new TokenFilter();
    }

    /**
     * Filter工厂,设置对应的过滤条件和跳转条件
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean() {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager());
        Map<String, String> map = new HashMap<>();
        //登出
        map.put("/logout", "logout");
        // 使用我们自己创建的jwt过滤器名称
        map.put("/**", "jwt");
        //登录
        shiroFilterFactoryBean.setLoginUrl("/login");
        //首页
        shiroFilterFactoryBean.setSuccessUrl("/select");
        //错误页面,认证不通过跳转
        shiroFilterFactoryBean.setUnauthorizedUrl("/error");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        shiroFilterFactoryBean.setFilters(new HashMap<String, Filter>(){{put("jwt", tokenFilter());}});
        return shiroFilterFactoryBean;
    }

    /**
     * 权限管理,配置主要是Realm的管理认证,同时可以配置缓存管理等
     * @return
     */
    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager webSecurityManager = new DefaultWebSecurityManager();
        //realm管理
        webSecurityManager.setRealm(studyRealm());
        return webSecurityManager;
    }
}

2. 为什么说 Shiro 对 JWT支持不是很好?

本文一开始就说了 ShiroJwt 模式的支持不是很好,全部都是本人个人的理解,如果内容有误请留言提问。

2.1 认证/授权模式藏BUG

  1. 对性能的影响

Shiro 整体都是围绕着认证/授权模式展开的,该模式适用于 有状态 (即服务端存储登录信息)的系统,在登录时进行 认证,一旦登录完成后就不再需要进行认证步骤,直接根据权限配置进行认证。而 JWT无状态的实现方式,所有的登录信息都存储在 Token 里,在过滤器中每次都需要根据 token 信息执行 getSubject(request, response).login(jwtToken) 方法,然后都会走一次 realmdoGetAuthenticationInfo 方法进行认证。

对于 JWT 来说,这个授权和认证步骤实际上是可以合并在一起作为授权步骤,登录账号密码校验才是认证步骤,但是不能在登录时调用 getSubject(request, response).login(jwtToken) 进行登录,必须在过滤器中根据 token 进行调用登录,这导致了性能的浪费,可以在认证步骤解析 JWT 为中间对象,然后将中间对象作为 Principal,能够避免重复解析 JWT 带来性能浪费,但是 Shiro 重复走认证流程的性能浪费不可避免。

  1. cache 藏 BUG

由于 JWTToken 具有时效性,而 Token 又是作为认证步骤的凭证,如果开启了缓存,缓存将根据 Token 进行存储,导致后续对于该 Token 不会再进行认证流程,将会导致失效的 Token 还可以访问系统。

恶心的是 public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) 方法无法复写的,所以只能关闭缓存功能才能解决这个问题,或者手动实现这些内容。如果关闭缓存的话,授权步骤又是同样的缓存逻辑,授权步骤的缓存也将被一并关闭,对性能也会造成影响。

综上,ShiroJWT 的支持并不是很好,但是也不是没办法解决,手动复写这些类就好,只是将会比较繁琐。