{"id":14972998,"url":"https://github.com/smith-cruise/spring-boot-shiro","last_synced_at":"2025-04-08T09:12:49.880Z","repository":{"id":26489595,"uuid":"107982897","full_name":"Smith-Cruise/Spring-Boot-Shiro","owner":"Smith-Cruise","description":"Shiro基于SpringBoot +JWT搭建简单的restful服务","archived":false,"fork":false,"pushed_at":"2022-03-31T18:38:15.000Z","size":46,"stargazers_count":1644,"open_issues_count":7,"forks_count":504,"subscribers_count":81,"default_branch":"master","last_synced_at":"2025-04-08T09:12:38.380Z","etag":null,"topics":["java","jwt","restful","shiro","springboot"],"latest_commit_sha":null,"homepage":"","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/Smith-Cruise.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":"2017-10-23T13:03:15.000Z","updated_at":"2025-04-07T14:00:39.000Z","dependencies_parsed_at":"2022-08-03T07:45:36.716Z","dependency_job_id":null,"html_url":"https://github.com/Smith-Cruise/Spring-Boot-Shiro","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Smith-Cruise%2FSpring-Boot-Shiro","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Smith-Cruise%2FSpring-Boot-Shiro/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Smith-Cruise%2FSpring-Boot-Shiro/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Smith-Cruise%2FSpring-Boot-Shiro/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Smith-Cruise","download_url":"https://codeload.github.com/Smith-Cruise/Spring-Boot-Shiro/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247809964,"owners_count":20999816,"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":["java","jwt","restful","shiro","springboot"],"created_at":"2024-09-24T13:47:54.860Z","updated_at":"2025-04-08T09:12:49.853Z","avatar_url":"https://github.com/Smith-Cruise.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Shiro + JWT + Spring Boot Restful 简易教程\n\nGitHub 项目地址：[https://github.com/Smith-Cruise/Spring-Boot-Shiro](https://github.com/Smith-Cruise/Spring-Boot-Shiro) 。\n\n## 序言\n\n我也是半路出家的人，如果大家有什么好的意见或批评，请务必 `issue` 下。\n\n如果想要直接体验，直接 `clone` 项目，运行 `mvn spring-boot:run` 命令即可进行访问。网址规则自行看教程后面。\n\n如果想了解 Spring Security 可以看\n\n[Spring Boot 2.0+Srping Security+Thymeleaf的简易教程](https://github.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo)\n\n[Spring Boot 2 + Spring Security 5 + JWT 的单页应用Restful解决方案](https://github.com/Smith-Cruise/Spring-Boot-Security-JWT-SPA) **（推荐）**\n\n## 特性\n\n* 完全使用了 Shiro 的注解配置，保持高度的灵活性。\n* 放弃 Cookie ，Session ，使用JWT进行鉴权，完全实现无状态鉴权。\n* JWT 密钥支持过期时间。\n* 对跨域提供支持。\n\n## 准备工作\n\n在开始本教程之前，请保证已经熟悉以下几点。\n\n- Spring Boot 基本语法，至少要懂得 `Controller` 、 `RestController` 、 `Autowired` 等这些基本注释。其实看看官方的 Getting-Start 教程就差不多了。\n- [JWT](https://jwt.io/) （Json Web Token）的基本概念，并且会简单操作JWT的 [JAVA SDK](https://github.com/auth0/java-jwt)。\n- Shiro 的基本操作，看下官方的 [10 Minute Tutorial](http://shiro.apache.org/10-minute-tutorial.html) 即可。\n- 模拟 HTTP 请求工具，我使用的是 PostMan。\n\n简要的说明下我们为什么要用 JWT ，因为我们要实现完全的前后端分离，所以不可能使用 `session`， `cookie` 的方式进行鉴权，所以 JWT 就被派上了用场，你可以通过一个加密密钥来进行前后端的鉴权。\n\n## 程序逻辑\n\n1. 我们 POST 用户名与密码到 `/login` 进行登入，如果成功返回一个加密 token，失败的话直接返回 401 错误。\n2. 之后用户访问每一个需要权限的网址请求必须在 `header` 中添加 `Authorization` 字段，例如 `Authorization: token` ，`token` 为密钥。\n3. 后台会进行 `token` 的校验，如果有误会直接返回 401。\n\n## Token加密说明\n\n- 携带了 `username` 信息在 token 中。\n- 设定了过期时间。\n- 使用用户登入密码对 `token` 进行加密。\n\n## Token校验流程\n\n1. 获得 `token` 中携带的 `username` 信息。\n2. 进入数据库搜索这个用户，得到他的密码。\n3. 使用用户的密码来检验 `token` 是否正确。\n\n## 准备Maven文件\n\n新建一个 Maven 工程，添加相关的 dependencies。\n\n```xml\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n\u003cproject xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\"\u003e\n    \u003cmodelVersion\u003e4.0.0\u003c/modelVersion\u003e\n\n    \u003cgroupId\u003eorg.inlighting\u003c/groupId\u003e\n    \u003cartifactId\u003eshiro-study\u003c/artifactId\u003e\n    \u003cversion\u003e1.0-SNAPSHOT\u003c/version\u003e\n\n    \u003cdependencies\u003e\n\n        \u003cdependency\u003e\n            \u003cgroupId\u003eorg.apache.shiro\u003c/groupId\u003e\n            \u003cartifactId\u003eshiro-spring\u003c/artifactId\u003e\n            \u003cversion\u003e1.3.2\u003c/version\u003e\n        \u003c/dependency\u003e\n        \u003cdependency\u003e\n            \u003cgroupId\u003ecom.auth0\u003c/groupId\u003e\n            \u003cartifactId\u003ejava-jwt\u003c/artifactId\u003e\n            \u003cversion\u003e3.2.0\u003c/version\u003e\n        \u003c/dependency\u003e\n        \u003cdependency\u003e\n            \u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n            \u003cartifactId\u003espring-boot-starter-web\u003c/artifactId\u003e\n            \u003cversion\u003e1.5.8.RELEASE\u003c/version\u003e\n        \u003c/dependency\u003e\n    \u003c/dependencies\u003e\n\n    \u003cbuild\u003e\n        \u003cplugins\u003e\n        \t\t\u003c!-- Srping Boot 打包工具 --\u003e\n            \u003cplugin\u003e\n                \u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n                \u003cartifactId\u003espring-boot-maven-plugin\u003c/artifactId\u003e\n                \u003cversion\u003e1.5.7.RELEASE\u003c/version\u003e\n                \u003cexecutions\u003e\n                    \u003cexecution\u003e\n                        \u003cgoals\u003e\n                            \u003cgoal\u003erepackage\u003c/goal\u003e\n                        \u003c/goals\u003e\n                    \u003c/execution\u003e\n                \u003c/executions\u003e\n            \u003c/plugin\u003e\n            \u003c!-- 指定JDK编译版本 --\u003e\n            \u003cplugin\u003e\n                \u003cgroupId\u003eorg.apache.maven.plugins\u003c/groupId\u003e\n                \u003cartifactId\u003emaven-compiler-plugin\u003c/artifactId\u003e\n                \u003cconfiguration\u003e\n                    \u003csource\u003e1.8\u003c/source\u003e\n                    \u003ctarget\u003e1.8\u003c/target\u003e\n                    \u003cencoding\u003eUTF-8\u003c/encoding\u003e\n                \u003c/configuration\u003e\n            \u003c/plugin\u003e\n        \u003c/plugins\u003e\n    \u003c/build\u003e\n\u003c/project\u003e\n```\n\n注意指定JDK版本和编码。\n\n## 构建简易的数据源\n\n为了缩减教程的代码，我使用 `HashMap` 本地模拟了一个数据库，结构如下：\n\n| username | password | role  | permission |\n| -------- | -------- | ----- | ---------- |\n| smith    | smith123 | user  | view       |\n| danny    | danny123 | admin | view,edit  |\n\n这是一个最简单的用户权限表，如果想更加进一步了解，自行百度 RBAC。\n\n之后再构建一个 `UserService` 来模拟数据库查询，并且把结果放到 `UserBean` 之中。\n\n**UserService.java**\n\n```java\n@Component\npublic class UserService {\n\n    public UserBean getUser(String username) {\n        // 没有此用户直接返回null\n        if (! DataSource.getData().containsKey(username))\n            return null;\n\n        UserBean user = new UserBean();\n        Map\u003cString, String\u003e detail = DataSource.getData().get(username);\n\n        user.setUsername(username);\n        user.setPassword(detail.get(\"password\"));\n        user.setRole(detail.get(\"role\"));\n        user.setPermission(detail.get(\"permission\"));\n        return user;\n    }\n}\n```\n\n**UserBean.java**\n\n```java\npublic class UserBean {\n    private String username;\n\n    private String password;\n\n    private String role;\n\n    private String permission;\n\n    public String getUsername() {\n        return username;\n    }\n\n    public void setUsername(String username) {\n        this.username = username;\n    }\n\n    public String getPassword() {\n        return password;\n    }\n\n    public void setPassword(String password) {\n        this.password = password;\n    }\n\n    public String getRole() {\n        return role;\n    }\n\n    public void setRole(String role) {\n        this.role = role;\n    }\n\n    public String getPermission() {\n        return permission;\n    }\n\n    public void setPermission(String permission) {\n        this.permission = permission;\n    }\n}\n```\n\n## 配置 JWT\n\n我们写一个简单的 JWT 加密，校验工具，并且使用用户自己的密码充当加密密钥，这样保证了 token 即使被他人截获也无法破解。并且我们在 `token` 中附带了 `username` 信息，并且设置密钥5分钟就会过期。\n\n```java\npublic class JWTUtil {\n\n    // 过期时间5分钟\n    private static final long EXPIRE_TIME = 5*60*1000;\n\n    /**\n     * 校验token是否正确\n     * @param token 密钥\n     * @param secret 用户的密码\n     * @return 是否正确\n     */\n    public static boolean verify(String token, String username, String secret) {\n        try {\n            Algorithm algorithm = Algorithm.HMAC256(secret);\n            JWTVerifier verifier = JWT.require(algorithm)\n                    .withClaim(\"username\", username)\n                    .build();\n            DecodedJWT jwt = verifier.verify(token);\n            return true;\n        } catch (Exception exception) {\n            return false;\n        }\n    }\n\n    /**\n     * 获得token中的信息无需secret解密也能获得\n     * @return token中包含的用户名\n     */\n    public static String getUsername(String token) {\n        try {\n            DecodedJWT jwt = JWT.decode(token);\n            return jwt.getClaim(\"username\").asString();\n        } catch (JWTDecodeException e) {\n            return null;\n        }\n    }\n\n    /**\n     * 生成签名,5min后过期\n     * @param username 用户名\n     * @param secret 用户的密码\n     * @return 加密的token\n     */\n    public static String sign(String username, String secret) {\n        try {\n            Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME);\n            Algorithm algorithm = Algorithm.HMAC256(secret);\n            // 附带username信息\n            return JWT.create()\n                    .withClaim(\"username\", username)\n                    .withExpiresAt(date)\n                    .sign(algorithm);\n        } catch (UnsupportedEncodingException e) {\n            return null;\n        }\n    }\n}\n```\n\n## 构建URL\n\n**ResponseBean.java**\n\n既然想要实现 restful，那我们要保证每次返回的格式都是相同的，因此我建立了一个 `ResponseBean` 来统一返回的格式。\n\n```java\npublic class ResponseBean {\n    \n    // http 状态码\n    private int code;\n\n    // 返回信息\n    private String msg;\n\n    // 返回的数据\n    private Object data;\n\n    public ResponseBean(int code, String msg, Object data) {\n        this.code = code;\n        this.msg = msg;\n        this.data = data;\n    }\n\n    public int getCode() {\n        return code;\n    }\n\n    public void setCode(int code) {\n        this.code = code;\n    }\n\n    public String getMsg() {\n        return msg;\n    }\n\n    public void setMsg(String msg) {\n        this.msg = msg;\n    }\n\n    public Object getData() {\n        return data;\n    }\n\n    public void setData(Object data) {\n        this.data = data;\n    }\n}\n```\n\n**自定义异常**\n\n为了实现我自己能够手动抛出异常，我自己写了一个 `UnauthorizedException.java`\n\n```java\npublic class UnauthorizedException extends RuntimeException {\n    public UnauthorizedException(String msg) {\n        super(msg);\n    }\n\n    public UnauthorizedException() {\n        super();\n    }\n}\n```\n\n**URL结构**\n\n| URL                 | 作用                      |\n| ------------------- | ----------------------- |\n| /login              | 登入                      |\n| /article            | 所有人都可以访问，但是用户与游客看到的内容不同 |\n| /require_auth       | 登入的用户才可以进行访问            |\n| /require_role       | admin的角色用户才可以登入         |\n| /require_permission | 拥有view和edit权限的用户才可以访问   |\n\n**Controller**\n\n```java\n@RestController\npublic class WebController {\n\n    private static final Logger LOGGER = LogManager.getLogger(WebController.class);\n\n    private UserService userService;\n\n    @Autowired\n    public void setService(UserService userService) {\n        this.userService = userService;\n    }\n\n    @PostMapping(\"/login\")\n    public ResponseBean login(@RequestParam(\"username\") String username,\n                              @RequestParam(\"password\") String password) {\n        UserBean userBean = userService.getUser(username);\n        if (userBean.getPassword().equals(password)) {\n            return new ResponseBean(200, \"Login success\", JWTUtil.sign(username, password));\n        } else {\n            throw new UnauthorizedException();\n        }\n    }\n\n    @GetMapping(\"/article\")\n    public ResponseBean article() {\n        Subject subject = SecurityUtils.getSubject();\n        if (subject.isAuthenticated()) {\n            return new ResponseBean(200, \"You are already logged in\", null);\n        } else {\n            return new ResponseBean(200, \"You are guest\", null);\n        }\n    }\n\n    @GetMapping(\"/require_auth\")\n    @RequiresAuthentication\n    public ResponseBean requireAuth() {\n        return new ResponseBean(200, \"You are authenticated\", null);\n    }\n\n    @GetMapping(\"/require_role\")\n    @RequiresRoles(\"admin\")\n    public ResponseBean requireRole() {\n        return new ResponseBean(200, \"You are visiting require_role\", null);\n    }\n\n    @GetMapping(\"/require_permission\")\n    @RequiresPermissions(logical = Logical.AND, value = {\"view\", \"edit\"})\n    public ResponseBean requirePermission() {\n        return new ResponseBean(200, \"You are visiting permission require edit,view\", null);\n    }\n\n    @RequestMapping(path = \"/401\")\n    @ResponseStatus(HttpStatus.UNAUTHORIZED)\n    public ResponseBean unauthorized() {\n        return new ResponseBean(401, \"Unauthorized\", null);\n    }\n}\n```\n\n**处理框架异常**\n\n之前说过 restful 要统一返回的格式，所以我们也要全局处理 `Spring Boot` 的抛出异常。利用 `@RestControllerAdvice` 能很好的实现。\n\n```java\n@RestControllerAdvice\npublic class ExceptionController {\n\n    // 捕捉shiro的异常\n    @ResponseStatus(HttpStatus.UNAUTHORIZED)\n    @ExceptionHandler(ShiroException.class)\n    public ResponseBean handle401(ShiroException e) {\n        return new ResponseBean(401, e.getMessage(), null);\n    }\n\n    // 捕捉UnauthorizedException\n    @ResponseStatus(HttpStatus.UNAUTHORIZED)\n    @ExceptionHandler(UnauthorizedException.class)\n    public ResponseBean handle401() {\n        return new ResponseBean(401, \"Unauthorized\", null);\n    }\n\n    // 捕捉其他所有异常\n    @ExceptionHandler(Exception.class)\n    @ResponseStatus(HttpStatus.BAD_REQUEST)\n    public ResponseBean globalException(HttpServletRequest request, Throwable ex) {\n        return new ResponseBean(getStatus(request).value(), ex.getMessage(), null);\n    }\n\n    private HttpStatus getStatus(HttpServletRequest request) {\n        Integer statusCode = (Integer) request.getAttribute(\"javax.servlet.error.status_code\");\n        if (statusCode == null) {\n            return HttpStatus.INTERNAL_SERVER_ERROR;\n        }\n        return HttpStatus.valueOf(statusCode);\n    }\n}\n\n```\n\n## 配置 Shiro\n\n大家可以先看下官方的 [Spring-Shiro](http://shiro.apache.org/spring.html) 整合教程，有个初步的了解。不过既然我们用了 `Spring-Boot`，那我们肯定要争取零配置文件。\n\n**实现JWTToken**\n\n`JWTToken` 差不多就是 `Shiro` 用户名密码的载体。因为我们是前后端分离，服务器无需保存用户状态，所以不需要 `RememberMe` 这类功能，我们简单的实现下 `AuthenticationToken` 接口即可。因为 `token` 自己已经包含了用户名等信息，所以这里我就弄了一个字段。如果你喜欢钻研，可以看看官方的 `UsernamePasswordToken` 是如何实现的。\n\n```java\npublic class JWTToken implements AuthenticationToken {\n\n    // 密钥\n    private String token;\n\n    public JWTToken(String token) {\n        this.token = token;\n    }\n\n    @Override\n    public Object getPrincipal() {\n        return token;\n    }\n\n    @Override\n    public Object getCredentials() {\n        return token;\n    }\n}\n```\n\n**实现Realm**\n\n`realm` 的用于处理用户是否合法的这一块，需要我们自己实现。\n\n```java\n@Service\npublic class MyRealm extends AuthorizingRealm {\n\n    private static final Logger LOGGER = LogManager.getLogger(MyRealm.class);\n\n    private UserService userService;\n\n    @Autowired\n    public void setUserService(UserService userService) {\n        this.userService = userService;\n    }\n\n    /**\n     * 大坑！，必须重写此方法，不然Shiro会报错\n     */\n    @Override\n    public boolean supports(AuthenticationToken token) {\n        return token instanceof JWTToken;\n    }\n\n    /**\n     * 只有当需要检测用户权限的时候才会调用此方法，例如checkRole,checkPermission之类的\n     */\n    @Override\n    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {\n        String username = JWTUtil.getUsername(principals.toString());\n        UserBean user = userService.getUser(username);\n        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();\n        simpleAuthorizationInfo.addRole(user.getRole());\n        Set\u003cString\u003e permission = new HashSet\u003c\u003e(Arrays.asList(user.getPermission().split(\",\")));\n        simpleAuthorizationInfo.addStringPermissions(permission);\n        return simpleAuthorizationInfo;\n    }\n\n    /**\n     * 默认使用此方法进行用户名正确与否验证，错误抛出异常即可。\n     */\n    @Override\n    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {\n        String token = (String) auth.getCredentials();\n        // 解密获得username，用于和数据库进行对比\n        String username = JWTUtil.getUsername(token);\n        if (username == null) {\n            throw new AuthenticationException(\"token invalid\");\n        }\n\n        UserBean userBean = userService.getUser(username);\n        if (userBean == null) {\n            throw new AuthenticationException(\"User didn't existed!\");\n        }\n\n        if (! JWTUtil.verify(token, username, userBean.getPassword())) {\n            throw new AuthenticationException(\"Username or password error\");\n        }\n\n        return new SimpleAuthenticationInfo(token, token, \"my_realm\");\n    }\n}\n```\n\n在 `doGetAuthenticationInfo()` 中用户可以自定义抛出很多异常，详情见文档。\n\n***重写 Filter***\n\n所有的请求都会先经过 `Filter`，所以我们继承官方的 `BasicHttpAuthenticationFilter` ，并且重写鉴权的方法。\n\n代码的执行流程 `preHandle` -\u003e `isAccessAllowed` -\u003e `isLoginAttempt` -\u003e `executeLogin` 。\n\n```java\npublic class JWTFilter extends BasicHttpAuthenticationFilter {\n\n    private Logger LOGGER = LoggerFactory.getLogger(this.getClass());\n\n    /**\n     * 判断用户是否想要登入。\n     * 检测header里面是否包含Authorization字段即可\n     */\n    @Override\n    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {\n        HttpServletRequest req = (HttpServletRequest) request;\n        String authorization = req.getHeader(\"Authorization\");\n        return authorization != null;\n    }\n\n    /**\n     *\n     */\n    @Override\n    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {\n        HttpServletRequest httpServletRequest = (HttpServletRequest) request;\n        String authorization = httpServletRequest.getHeader(\"Authorization\");\n\n        JWTToken token = new JWTToken(authorization);\n        // 提交给realm进行登入，如果错误他会抛出异常并被捕获\n        getSubject(request, response).login(token);\n        // 如果没有抛出异常则代表登入成功，返回true\n        return true;\n    }\n\n    /**\n     * 这里我们详细说明下为什么最终返回的都是true，即允许访问\n     * 例如我们提供一个地址 GET /article\n     * 登入用户和游客看到的内容是不同的\n     * 如果在这里返回了false，请求会被直接拦截，用户看不到任何东西\n     * 所以我们在这里返回true，Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入\n     * 如果有些资源只有登入用户才能访问，我们只需要在方法上面加上 @RequiresAuthentication 注解即可\n     * 但是这样做有一个缺点，就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法)，但实际上对应用影响不大\n     */\n    @Override\n    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {\n        if (isLoginAttempt(request, response)) {\n            try {\n                executeLogin(request, response);\n            } catch (Exception e) {\n                response401(request, response);\n            }\n        }\n        return true;\n    }\n\n    /**\n     * 对跨域提供支持\n     */\n    @Override\n    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {\n        HttpServletRequest httpServletRequest = (HttpServletRequest) request;\n        HttpServletResponse httpServletResponse = (HttpServletResponse) response;\n        httpServletResponse.setHeader(\"Access-control-Allow-Origin\", httpServletRequest.getHeader(\"Origin\"));\n        httpServletResponse.setHeader(\"Access-Control-Allow-Methods\", \"GET,POST,OPTIONS,PUT,DELETE\");\n        httpServletResponse.setHeader(\"Access-Control-Allow-Headers\", httpServletRequest.getHeader(\"Access-Control-Request-Headers\"));\n        // 跨域时会首先发送一个option请求，这里我们给option请求直接返回正常状态\n        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {\n            httpServletResponse.setStatus(HttpStatus.OK.value());\n            return false;\n        }\n        return super.preHandle(request, response);\n    }\n\n    /**\n     * 将非法请求跳转到 /401\n     */\n    private void response401(ServletRequest req, ServletResponse resp) {\n        try {\n            HttpServletResponse httpServletResponse = (HttpServletResponse) resp;\n            httpServletResponse.sendRedirect(\"/401\");\n        } catch (IOException e) {\n            LOGGER.error(e.getMessage());\n        }\n    }\n}\n```\n\n`getSubject(request, response).login(token);` 这一步就是提交给了 `realm` 进行处理。\n\n**配置Shiro**\n\n```java\n@Configuration\npublic class ShiroConfig {\n\n    @Bean(\"securityManager\")\n    public DefaultWebSecurityManager getManager(MyRealm realm) {\n        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();\n        // 使用自己的realm\n        manager.setRealm(realm);\n\n        /*\n         * 关闭shiro自带的session，详情见文档\n         * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29\n         */\n        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();\n        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();\n        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);\n        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);\n        manager.setSubjectDAO(subjectDAO);\n\n        return manager;\n    }\n\n    @Bean(\"shiroFilter\")\n    public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {\n        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();\n\n        // 添加自己的过滤器并且取名为jwt\n        Map\u003cString, Filter\u003e filterMap = new HashMap\u003c\u003e();\n        filterMap.put(\"jwt\", new JWTFilter());\n        factoryBean.setFilters(filterMap);\n\n        factoryBean.setSecurityManager(securityManager);\n        factoryBean.setUnauthorizedUrl(\"/401\");\n\n        /*\n         * 自定义url规则\n         * http://shiro.apache.org/web.html#urls-\n         */\n        Map\u003cString, String\u003e filterRuleMap = new HashMap\u003c\u003e();\n        // 所有请求通过我们自己的JWT Filter\n        filterRuleMap.put(\"/**\", \"jwt\");\n        // 访问401和404页面不通过我们的Filter\n        filterRuleMap.put(\"/401\", \"anon\");\n        factoryBean.setFilterChainDefinitionMap(filterRuleMap);\n        return factoryBean;\n    }\n\n    /**\n     * 下面的代码是添加注解支持\n     */\n    @Bean\n    @DependsOn(\"lifecycleBeanPostProcessor\")\n    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {\n        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();\n        // 强制使用cglib，防止重复代理和可能引起代理出错的问题\n        // https://zhuanlan.zhihu.com/p/29161098\n        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);\n        return defaultAdvisorAutoProxyCreator;\n    }\n\n    @Bean\n    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {\n        return new LifecycleBeanPostProcessor();\n    }\n\n    @Bean\n    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {\n        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();\n        advisor.setSecurityManager(securityManager);\n        return advisor;\n    }\n}\n```\n\n里面 URL 规则自己参考文档即可 http://shiro.apache.org/web.html 。\n\n## 总结\n\n我就说下代码还有哪些可以进步的地方吧\n\n- 没有实现 Shiro 的 `Cache` 功能。\n- Shiro 中鉴权失败时不能够直接返回 401 信息，而是通过跳转到 `/401` 地址实现。\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmith-cruise%2Fspring-boot-shiro","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsmith-cruise%2Fspring-boot-shiro","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmith-cruise%2Fspring-boot-shiro/lists"}