{"id":20432174,"url":"https://github.com/davidfantasy/shrio-with-jwt-spring-boot-starter","last_synced_at":"2025-07-05T09:33:22.150Z","repository":{"id":44527426,"uuid":"247276452","full_name":"davidfantasy/shrio-with-jwt-spring-boot-starter","owner":"davidfantasy","description":"spring-boot环境下基于JWT Token的无状态shiro权限验证框架","archived":false,"fork":false,"pushed_at":"2022-02-09T22:23:19.000Z","size":100,"stargazers_count":19,"open_issues_count":2,"forks_count":7,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-12T20:52:26.737Z","etag":null,"topics":["accesstoken","authorization","jwt","shiro","spring-boot","stateless","token"],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/davidfantasy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-03-14T12:55:02.000Z","updated_at":"2024-08-09T12:38:59.000Z","dependencies_parsed_at":"2022-08-24T18:00:58.577Z","dependency_job_id":null,"html_url":"https://github.com/davidfantasy/shrio-with-jwt-spring-boot-starter","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidfantasy%2Fshrio-with-jwt-spring-boot-starter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidfantasy%2Fshrio-with-jwt-spring-boot-starter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidfantasy%2Fshrio-with-jwt-spring-boot-starter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidfantasy%2Fshrio-with-jwt-spring-boot-starter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/davidfantasy","download_url":"https://codeload.github.com/davidfantasy/shrio-with-jwt-spring-boot-starter/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248631728,"owners_count":21136560,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["accesstoken","authorization","jwt","shiro","spring-boot","stateless","token"],"created_at":"2024-11-15T08:14:05.239Z","updated_at":"2025-04-12T20:52:34.013Z","avatar_url":"https://github.com/davidfantasy.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"如需了解本框架的设计细节，请阅读：[基于 Shiro 和 JWT 的无状态安全验证方案](https://blog.csdn.net/weixin_46628206/article/details/105033980)这篇文章\n\n## 简介\n\n用户权限管理是每个信息系统最基本的需求，对基于 Java 的项目来说，最常用的权限管理框架就是大名鼎鼎的 Apache Shiro。Apache Shiro 功能非常强大，使用广泛，几乎成为了权限管理的代名词。但对于普通项目来说，Shiro 的设计理念因为追求灵活性，一些概念如 Realm，Subject 的抽象级别都比较高，显得比较复杂。如果没有对框架细节进行深入了解的话，很难理解其中的准确含义。要将其应用于实际项目，还需要针对项目的实际情况做大量的配置和改造，时间成本较高。\n\n而且 Shiro 兴起的时代主流应用还是传统的基于 Session 的 Web 网站，并没有过多的考虑目前流行的微服务等应用形式的权限管理需求。导致其并没有提供一套无状态微服务的开箱即用的整合方案。需要在项目层面对 Shiro 进行二次封装和改进，开发难度较大。\n\nshrio-with-jwt-spring-boot-starter 正是针对上述情况而开发的。它基于 spring-boot 环境，使用 Shiro 作为基础验证框架，整合了 JWT（[JSON Web token](https://jwt.io/)）规范，通过简单的一些配置，提供在微服务环境下开箱即用的无状态权限管理框架。\n\n## 特点\n\n- 完全兼容 Shiro\n- 无状态设计，无需 Session\n- 基于 JWT 规范的 Token 设计\n- 在 spring-boot 环境下自动配置，开箱即用\n- 基于注解的权限配置，并且兼容 Shiro 的层级权限设置\n- 通过接口灵活定义获取用户权限（permission）的方式，兼容多种权限模型\n- Token 过期前自动刷新（需配合客户端的实现）\n\n## 使用方法\n\n1. 引入 shrio-with-jwt-spring-boot-starter。\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003ecom.github.davidfantasy\u003c/groupId\u003e\n    \u003cartifactId\u003eshrio-with-jwt-spring-boot-starter\u003c/artifactId\u003e\n    \u003cversion\u003e${version}\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n2. 根据实际业务的需要，实现**com.github.davidfantasy.jwtshiro.JWTUserAuthService**接口，并注册为Spring的bean（如果框架没有找到任何一个JWTUserAuthService的实现类，则不会进行任何处理）。JWTUserAuthService 接口是框架的一个扩展点，便于应用端根据自身的业务规则对权限模型，错误处理等进行自定义实现。**getUserInfo**方法用于客户端访问时根据客户端传回 token 中包含的用户 account 信息，获取用户的实际权限。获取的方式由应用程序端来控制，可以从配置文件中加载，也可以根据 account 查询数据库，获取用户实际权限。**getAuthenticatedUser**方法已提供默认实现，用于获取当前请求接口的客户信息，以下是一个例子\n\n```java\n@Service\npublic class JWTUserAuthServiceImpl implements JWTUserAuthService {\n\n    @Autowired\n    private UserService userService;\n\n    private Cache\u003cString, UserInfo\u003e userCache = CacheBuilder.newBuilder().maximumSize(1000)\n            .expireAfterWrite(30, TimeUnit.MINUTES).build();\n\n    @Override\n    public UserInfo getUserInfo(String account) {\n        try {\n            UserInfo user = userCache.getIfPresent(account);\n            if (user == null) {\n                user = this.queryUserInfo(account);\n                if (user != null) {\n                    userCache.put(account, user);\n                }\n            }\n            return user;\n        } catch (Exception e) {\n            log.error(\"读取用户缓存信息发生错误:\" + e.getMessage());\n        }\n        return null;\n    }\n\n    /**\n     * 自定义访问资源认证失败时的处理方式，例如返回json格式的错误信息\n     * {\\\"code\\\":401,\\\"message\\\":\\\"用户认证失败！\\\")\n     */\n    @Override\n    public void onAuthenticationFailed(HttpServletRequest req, HttpServletResponse res) {\n        res.setStatus(HttpStatus.UNAUTHORIZED.value());\n    }\n\n    /**\n     * 自定义访问资源权限不足时的处理方式，例如返回json格式的错误信息\n     * {\\\"code\\\":403,\\\"message\\\":\\\"permission denied！\\\")\n     */\n    @Override\n    public void onAuthorizationFailed(HttpServletRequest req, HttpServletResponse res) {\n        res.setStatus(HttpStatus.FORBIDDEN.value());\n    }\n\n    private ShiroUserInfo queryUserInfo(String account) {\n    // 这里编写获取ShiroUserInfo的逻辑，例如从数据库进行查询\n    }\n\n    /**\n     * 调用接口的getAuthenticatedUser获取当前请求的用户信息\n     */\n    public ShiroUserInfo getCurrentUser(){\n        return (ShiroUserInfo)this.getAuthenticatedUser(false);\n    }\n\n    /**\n     * 刷新指定account的缓存信息\n     */\n    public void refreshUserCache(String account) {\n        this.userCache.invalidate(account);\n    }\n\n}\n```\n\n**注意**：getUserInfo 这个方法在每次接口调用的时候都会触发，用于检查用户权限，请实现时根据需要对接口的返回结果进行缓存（例如使用 Guava 的 Cache）。\n\n返回值 com.github.davidfantasy.jwtshiro.UserInfo 类封装了一个系统用户必要的权限信息，可以根据实际需要进行扩展：\n\n```java\npublic class UserInfo {\n\n  /**\n    * 用户的唯一标识\n    */\n   private String account;\n\n   /**\n    * accessToken的密钥，用于对accessToken进行加密和解密\n    * 建议为每个用户配置不同的密钥（比如使用用户的password）\n    */\n   private String secret;\n\n   /**\n    * 用户权限集合，含义类似于Shiro中的perms\n    */\n   private Set\u003cString\u003e permissions;\n\n}\n```\n\n3. 对需要进行权限控制的 Controller 添加对应的注解，实现灵活的权限控制。**为了简化配置，框架默认所有被拦截的资源必须是要经过认证的用户才可以被访问。**即如果配置的拦截范围是/api/_,则会添加一条默认的验证规则: /api/_=authc。但任何通过注解添加的验证规则都拥有比默认规则更高的优先级。如果需要精确控制某个接口的用户权限，就需要利用到 RequiresPerms 和 AlowAnonymous 注解。添加了 AlowAnonymous 注解的 url 允许匿名访问，而 RequiresPerms 则用于指定某个 url 所需的用户权限，访问用户必须拥有该权限才允许访问该接口（用法和Shiro原生的@RequiresPermissions\n基本一致，不过是基于url进行拦截，不需要配置动态代理）。\n\n**注意**：RequiresPerms 比 AlowAnonymous 拥有更高的优先级，如果一个 url 同时被设定了两种规则，则 AlowAnonymous 不会起作用。如果method和class同时添加了RequiresPerms注解，则method的注解拥有更高优先级。\n\n下面是一个访问控制规则设置的例子：\n\n```java\n@RestController\n@RequestMapping(\"/api/user\")\n@RequiresPerms(\"user:basic\")\npublic class UserController {\n\n    @AlowAnonymous\n    @PostMapping(\"/login\")\n    public String login() {\n       return \"ok\";\n    }\n\n    @GetMapping(\"/detail\")\n    public String getUserDetail() {\n       return \"ok\";\n    }\n\n    @PostMapping(\"/modify\")\n    @RequiresPerms(\"user:modify\")\n    public String modifyUser() {\n        return \"ok\";\n    }\n\n    @PostMapping(\"/delete\")\n    @RequiresPerms({\"system\",\"user:delete\"})\n    public String deleteUser() {\n        return \"ok\";\n    }\n\n    @PostMapping(\"/modify-logs\")\n    @RequiresPerms(value={\"system\",\"user:logs\"}, logical = Logical.OR)\n    public String deleteUser() {\n        return \"ok\";\n    }\n\n}\n```\n\n在上面的例子中,接口与用户权限的对应关系如下：\n| 接口 | 所需权限 |\n| :----------------- | :------------------------------ |\n| /api/user/login | 无需权限，可匿名访问 |\n| /api/user/detail | 访问用户需具备权限\"user:basic\" |\n| /api/user/modify | 访问用户需具备权限\"user:modify\" |\n| /api/user/delete | 访问用户需同时具备权限\"system\",\"user:delete\" |\n| /api/user/modify-logs | 访问用户需具备权限\"system\"或者\"user:logs\" |\n\n类似于 Shiro 官方的如下配置\n\n```xml\n\u003cproperty name=\"filterChainDefinitions\"\u003e\n    \u003cvalue\u003e\n        /api/user/login     = anon\n        /api/user/detail    = perms[\"user\"]\n        /api/user/modify    = perms[\"user:modify\"]\n        /api/user/delete    = perms[\"user:delete\"]\n    \u003c/value\u003e\n\u003c/property\u003e\n```\n\n**注意**：和在 Shiro 中一样，权限是按层级划分的（使用:分割），即在上例中，如果用户拥有的权限中有“user”，则可以同时访问/api/user/detail,/api/user/modify,/api/user/delete 三个接口\n\n## 客户端调用\n\n客户端在访问非匿名接口前，都需要调用服务端的登录接口获取 accessToken，accessToken 有时效限制，在生命周期内由客户端负责对 accessToken 进行存储和管理。服务端的登录接口生成 accessToken 的示例代码如下：\n\n```java\n\n@RestController\n@RequestMapping(\"/security\")\npublic class MockController {\n\n    @Autowired\n    private MockUserService userService;\n\n    @Autowired\n    private JWTHelper jwtHelper;\n\n    @AlowAnonymous\n    @PostMapping(\"/login\")\n    public Result login(String account,String password) {\n        UserInfo user = userService.getUserInfo(account);\n        if(user==null||!user.getPassword().equals(password)){\n            throw new IllegalArgumentException(\"用户名或密码错误\");\n        }\n        String accessToken = jwtHelper.sign(user.getAccount(), user.getPassword());\n        //后续token的刷新由客服端负责维护\n        Result result = new Result();\n        result.setToken(accessToken);\n        return result;\n    }\n\n}\n```\n\n客户端登录后获取的 accessToken,每次调用接口时，都将 accessToken 加入到请求的 header 中供服务端进行权限验证。header 中的名称默认为\"jwt-token\"，也可以通过配置修改为其它名称，请求示例如下：\n\n```http\naccept: application/json, text/plain, */*\naccept-encoding: gzip, deflate, br\naccept-language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6\njwt-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1ODQzNjG5OTtsImFjY291bnQiOiIxODcxNjYxODEzOCJ9.7eJYVmSys6YBu51Al5hdXdPMdrKsQFCqMwHu8ATaOPY\n```\n\n## 客户端 accessToken 自动刷新\n\naccessToken 的有效期由两个配置构成，maxAliveMinute 和 maxIdleMinute。maxAliveMinute 定义了 accessToken 的理论过期时间，而 maxIdleMinute 定义了\naccessToken 的最大生存周期。框架会自动注册一个 Spring 的 HandlerInterceptor 用来处理 Token 的自动刷新问题，如果传入的 Token 已经超过 maxAliveMinute 设定的时间，但还没有达到 maxIdleMinute 的限制，则会自动刷新该用户的 accessToken 并添加在 response header（header 中的名称取决于配置值），客户端如果在响应头中发现有新的 token 返回,说明当前 token 即将失效，需要及时更新自身存储的 token。\n\n这个机制实际是提供一个窗口期，让客户端安全的刷新 accessToken。试想如果 token 失效了就必须立即重新登录，那势必会严重影响到用户的实际体验。\n\n**注意**：要启用accessToken自动刷新机制，需配置enableAutoRefreshToken参数为true\n\n## 配置项说明\n\n| 参数名                     | 默认值    | 说明                                                                                                |\n| :------------------------- | :-------- | :-------------------------------------------------------------------------------------------------- |\n| jwt-shiro.urlPattern       | /\\*       | 需要进行权限拦截的 URL pattern, 多个使用 url 隔开，例如：/api/_,/rest/_                             |\n| jwt-shiro.maxAliveMinute   | 30        | accessToken 的理论过期时间，单位分钟，token 如果超过该时间则接口响应的 header 中附带新的 token 信息 |\n| jwt-shiro.maxIdleMinute    | 60        | accessToken 的最大生存周期，单位分钟，在此时间内的 token 无需重新登录即可刷新                       |\n| jwt-shiro.headerKeyOfToken | jwt-token | accessToken 在 http header 中的 name                                                                |\n| jwt-shiro.accountAlias     | account   | token 中保存的用户名的 key name                                                                     |\n| jwt-shiro.enableAutoRefreshToken     | false   | 是否启用token自动刷新机制                                                                    |\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavidfantasy%2Fshrio-with-jwt-spring-boot-starter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdavidfantasy%2Fshrio-with-jwt-spring-boot-starter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavidfantasy%2Fshrio-with-jwt-spring-boot-starter/lists"}