{"id":19019676,"url":"https://github.com/smith-cruise/spring-boot-security-jwt-spa","last_synced_at":"2026-03-04T03:01:27.202Z","repository":{"id":90158472,"uuid":"178675663","full_name":"Smith-Cruise/Spring-Boot-Security-JWT-SPA","owner":"Smith-Cruise","description":"Spring Boot 2 + JWT + Spring Security 5的单页应用(SPA) Restful 解决方案","archived":false,"fork":false,"pushed_at":"2020-04-16T11:53:41.000Z","size":105,"stargazers_count":192,"open_issues_count":3,"forks_count":59,"subscribers_count":8,"default_branch":"master","last_synced_at":"2025-08-20T16:49:34.160Z","etag":null,"topics":["jwt","restful","single-page-applications","spa","springboot2","springsecurity"],"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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2019-03-31T10:54:31.000Z","updated_at":"2025-07-07T04:14:54.000Z","dependencies_parsed_at":null,"dependency_job_id":"455d73b2-d7ac-4e1c-869b-13906b531e39","html_url":"https://github.com/Smith-Cruise/Spring-Boot-Security-JWT-SPA","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Smith-Cruise/Spring-Boot-Security-JWT-SPA","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Smith-Cruise%2FSpring-Boot-Security-JWT-SPA","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Smith-Cruise%2FSpring-Boot-Security-JWT-SPA/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Smith-Cruise%2FSpring-Boot-Security-JWT-SPA/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Smith-Cruise%2FSpring-Boot-Security-JWT-SPA/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Smith-Cruise","download_url":"https://codeload.github.com/Smith-Cruise/Spring-Boot-Security-JWT-SPA/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Smith-Cruise%2FSpring-Boot-Security-JWT-SPA/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30070479,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-04T01:03:42.280Z","status":"online","status_checked_at":"2026-03-04T02:00:07.464Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["jwt","restful","single-page-applications","spa","springboot2","springsecurity"],"created_at":"2024-11-08T20:13:53.276Z","updated_at":"2026-03-04T03:01:27.196Z","avatar_url":"https://github.com/Smith-Cruise.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Spring Boot 2 + Spring Security 5 + JWT 的单页应用 Restful 解决方案\n\n此前我已经写过一篇类似的教程，但那时候使用了投机的方法，没有尊重 Spring Security 的官方设计，自己并不感到满意。这段时间比较空，故重新研究了一遍。\n\n项目 GitHub：[https://github.com/Smith-Cruise/Spring-Boot-Security-JWT-SPA](https://github.com/Smith-Cruise/Spring-Boot-Security-JWT-SPA)\n\n老版本：[https://github.com/Smith-Cruise/Spring-Boot-Security-JWT-SPA/blob/master/README_OLD.md](https://github.com/Smith-Cruise/Spring-Boot-Security-JWT-SPA/blob/master/README_OLD.md)\n\n## 特性\n\n* 使用 JWT 进行鉴权，支持 token 过期\n* 使用 Ehcache 进行缓存，减少每次鉴权对数据库的压力\n* 尽可能贴合 Spring Security 的设计\n* 实现注解权限控制\n\n## 准备\n\n开始本教程的时候希望对下面知识点进行粗略的了解。\n\n*  知道 JWT 的基本概念\n* 了解过 Spring Security\n\n我之前写过两篇关于安全框架的问题，大家可以大致看一看，打下基础。\n\n [Shiro+JWT+Spring Boot Restful简易教程](https://github.com/Smith-Cruise/Spring-Boot-Shiro \"Shiro+JWT+Spring Boot Restful简易教程\")\n\n [Spring Boot+Spring Security+Thymeleaf 简单教程](https://github.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo \"Spring Boot+Spring Security+Thymeleaf 简单教程\")\n\n本项目中 `JWT` 密钥是使用用户自己的登入密码，这样每一个 `token` 的密钥都不同，相对比较安全。\n\n### 大体思路：\n\n**登入：**\n\n1. POST 用户名密码到 \\login\n2. 请求到达 `JwtAuthenticationFilter` 中的 `attemptAuthentication()` 方法，获取 request 中的 POST 参数，包装成一个 `UsernamePasswordAuthenticationToken` 交付给 `AuthenticationManager` 的 `authenticate()` 方法进行鉴权。\n3. `AuthenticationManager` 会从 `CachingUserDetailsService` 中查找用户信息，并且判断账号密码是否正确。\n4. 如果账号密码正确跳转到 `JwtAuthenticationFilter` 中的 `successfulAuthentication()` 方法，我们进行签名，生成 token 返回给用户。\n5. 账号密码错误则跳转到 `JwtAuthenticationFilter` 中的 `unsuccessfulAuthentication()` 方法，我们返回错误信息让用户重新登入。\n\n**请求鉴权：**\n\n请求鉴权的主要思路是我们会从请求中的 Authorization 字段拿取 token，如果不存在此字段的用户，Spring Security 会默认会用 `AnonymousAuthenticationToken()` 包装它，即代表匿名用户。\n\n1. 任意请求发起\n2. 到达 `JwtAuthorizationFilter` 中的 `doFilterInternal()` 方法，进行鉴权。\n3. 如果鉴权成功我们把生成的 `Authentication` 用 `SecurityContextHolder.getContext().setAuthentication()` 放入 Security，即代表鉴权完成。此处如何鉴权由我们自己代码编写，后序会详细说明。\n\n\n## 准备 pom.xml\n\n```xml\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n\u003cproject xmlns=\"http://maven.apache.org/POM/4.0.0\" 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    \u003cparent\u003e\n        \u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n        \u003cartifactId\u003espring-boot-starter-parent\u003c/artifactId\u003e\n        \u003cversion\u003e2.1.7.RELEASE\u003c/version\u003e\n        \u003crelativePath/\u003e \u003c!-- lookup parent from repository --\u003e\n    \u003c/parent\u003e\n    \u003cgroupId\u003eorg.inlighting\u003c/groupId\u003e\n    \u003cartifactId\u003espring-boot-security-jwt\u003c/artifactId\u003e\n    \u003cversion\u003e0.0.1-SNAPSHOT\u003c/version\u003e\n    \u003cname\u003espring-boot-security-jwt\u003c/name\u003e\n    \u003cdescription\u003eDemo project for Spring Boot\u003c/description\u003e\n\n    \u003cproperties\u003e\n        \u003cjava.version\u003e1.8\u003c/java.version\u003e\n    \u003c/properties\u003e\n\n    \u003cdependencies\u003e\n        \u003cdependency\u003e\n            \u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n            \u003cartifactId\u003espring-boot-starter-security\u003c/artifactId\u003e\n        \u003c/dependency\u003e\n\n        \u003cdependency\u003e\n            \u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n            \u003cartifactId\u003espring-boot-starter-web\u003c/artifactId\u003e\n        \u003c/dependency\u003e\n        \u003c!-- JWT 支持 --\u003e\n        \u003cdependency\u003e\n            \u003cgroupId\u003ecom.auth0\u003c/groupId\u003e\n            \u003cartifactId\u003ejava-jwt\u003c/artifactId\u003e\n            \u003cversion\u003e3.8.2\u003c/version\u003e\n        \u003c/dependency\u003e\n\n        \u003c!-- cache 支持 --\u003e\n        \u003cdependency\u003e\n            \u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n            \u003cartifactId\u003espring-boot-starter-cache\u003c/artifactId\u003e\n        \u003c/dependency\u003e\n\n        \u003c!-- cache 支持 --\u003e\n        \u003cdependency\u003e\n            \u003cgroupId\u003eorg.ehcache\u003c/groupId\u003e\n            \u003cartifactId\u003eehcache\u003c/artifactId\u003e\n        \u003c/dependency\u003e\n\n        \u003c!-- cache 支持 --\u003e\n        \u003cdependency\u003e\n            \u003cgroupId\u003ejavax.cache\u003c/groupId\u003e\n            \u003cartifactId\u003ecache-api\u003c/artifactId\u003e\n        \u003c/dependency\u003e\n\n        \u003cdependency\u003e\n            \u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n            \u003cartifactId\u003espring-boot-starter-test\u003c/artifactId\u003e\n            \u003cscope\u003etest\u003c/scope\u003e\n        \u003c/dependency\u003e\n\n        \u003cdependency\u003e\n            \u003cgroupId\u003eorg.springframework.security\u003c/groupId\u003e\n            \u003cartifactId\u003espring-security-test\u003c/artifactId\u003e\n            \u003cscope\u003etest\u003c/scope\u003e\n        \u003c/dependency\u003e\n\n        \u003c!-- ehcache 读取 xml 配置文件使用 --\u003e\n        \u003cdependency\u003e\n            \u003cgroupId\u003ejavax.xml.bind\u003c/groupId\u003e\n            \u003cartifactId\u003ejaxb-api\u003c/artifactId\u003e\n            \u003cversion\u003e2.3.0\u003c/version\u003e\n        \u003c/dependency\u003e\n\n        \u003c!-- ehcache 读取 xml 配置文件使用 --\u003e\n        \u003cdependency\u003e\n            \u003cgroupId\u003ecom.sun.xml.bind\u003c/groupId\u003e\n            \u003cartifactId\u003ejaxb-impl\u003c/artifactId\u003e\n            \u003cversion\u003e2.3.0\u003c/version\u003e\n        \u003c/dependency\u003e\n\n        \u003c!-- ehcache 读取 xml 配置文件使用 --\u003e\n        \u003cdependency\u003e\n            \u003cgroupId\u003ecom.sun.xml.bind\u003c/groupId\u003e\n            \u003cartifactId\u003ejaxb-core\u003c/artifactId\u003e\n            \u003cversion\u003e2.3.0\u003c/version\u003e\n        \u003c/dependency\u003e\n\n        \u003c!-- ehcache 读取 xml 配置文件使用 --\u003e\n        \u003cdependency\u003e\n            \u003cgroupId\u003ejavax.activation\u003c/groupId\u003e\n            \u003cartifactId\u003eactivation\u003c/artifactId\u003e\n            \u003cversion\u003e1.1.1\u003c/version\u003e\n        \u003c/dependency\u003e\n    \u003c/dependencies\u003e\n\n    \u003cbuild\u003e\n        \u003cplugins\u003e\n            \u003cplugin\u003e\n                \u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n                \u003cartifactId\u003espring-boot-maven-plugin\u003c/artifactId\u003e\n            \u003c/plugin\u003e\n        \u003c/plugins\u003e\n    \u003c/build\u003e\n\n\u003c/project\u003e\n\n```\n\npom.xml 配置文件这块没有什么好说的，主要说明下面的几个依赖：\n\n```xml\n\u003c!-- ehcache 读取 xml 配置文件使用 --\u003e\n\u003cdependency\u003e\n  \u003cgroupId\u003ejavax.xml.bind\u003c/groupId\u003e\n  \u003cartifactId\u003ejaxb-api\u003c/artifactId\u003e\n  \u003cversion\u003e2.3.0\u003c/version\u003e\n\u003c/dependency\u003e\n\n\u003c!-- ehcache 读取 xml 配置文件使用 --\u003e\n\u003cdependency\u003e\n  \u003cgroupId\u003ecom.sun.xml.bind\u003c/groupId\u003e\n  \u003cartifactId\u003ejaxb-impl\u003c/artifactId\u003e\n  \u003cversion\u003e2.3.0\u003c/version\u003e\n\u003c/dependency\u003e\n\n\u003c!-- ehcache 读取 xml 配置文件使用 --\u003e\n\u003cdependency\u003e\n  \u003cgroupId\u003ecom.sun.xml.bind\u003c/groupId\u003e\n  \u003cartifactId\u003ejaxb-core\u003c/artifactId\u003e\n  \u003cversion\u003e2.3.0\u003c/version\u003e\n\u003c/dependency\u003e\n\n\u003c!-- ehcache 读取 xml 配置文件使用 --\u003e\n\u003cdependency\u003e\n  \u003cgroupId\u003ejavax.activation\u003c/groupId\u003e\n  \u003cartifactId\u003eactivation\u003c/artifactId\u003e\n  \u003cversion\u003e1.1.1\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n因为 ehcache 读取 xml 配置文件时使用了这几个依赖，而这几个依赖从 JDK 9 开始时是选配模块，所以高版本的用户需要添加这几个依赖才能正常使用。\n\n## 基础工作准备\n\n接下来准备下几个基础工作，就是新建个实体、模拟个数据库，写个 JWT 工具类这种基础操作。\n\n### UserEntity.java\n\n关于 role 为什么使用 GrantedAuthority 说明下：其实是为了简化代码，直接用了 Security 现成的 role 类，实际项目中我们肯定要自己进行处理，将其转换为 Security 的 role 类。\n\n```java\npublic class UserEntity {\n\n    public UserEntity(String username, String password, Collection\u003c? extends GrantedAuthority\u003e role) {\n        this.username = username;\n        this.password = password;\n        this.role = role;\n    }\n\n    private String username;\n\n    private String password;\n\n    private Collection\u003c? extends GrantedAuthority\u003e role;\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 Collection\u003c? extends GrantedAuthority\u003e getRole() {\n        return role;\n    }\n\n    public void setRole(Collection\u003c? extends GrantedAuthority\u003e role) {\n        this.role = role;\n    }\n}\n```\n\n### ResponseEntity.java\n\n前后端分离为了方便前端我们要统一 json 的返回格式，所以自定义一个 ResponseEntity.java。\n\n```java\npublic class ResponseEntity {\n\n    public ResponseEntity() {\n    }\n\n    public ResponseEntity(int status, String msg, Object data) {\n        this.status = status;\n        this.msg = msg;\n        this.data = data;\n    }\n\n    private int status;\n\n    private String msg;\n\n    private Object data;\n\n    public int getStatus() {\n        return status;\n    }\n\n    public void setStatus(int status) {\n        this.status = status;\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### Database.java\n\n这里我们使用一个 HashMap 模拟了一个数据库，密码我已经预先用 `Bcrypt` 加密过了，这也是 Spring Security 官方推荐的加密算法（MD5 加密已经在 Spring Security 5 中被移除了，不安全）。\n\n| 用户名 | 密码                      | 权限        |\n| ------ | ------------------------- | ----------- |\n| jack   | jack123 存 Bcrypt 加密后  | ROLE_USER   |\n| danny  | danny123 存 Bcrypt 加密后 | ROLE_EDITOR |\n| smith  | smith123 存 Bcrypt 加密后 | ROLE_ADMIN  |\n\n```java\n@Component\npublic class Database {\n    private Map\u003cString, UserEntity\u003e data = null;\n    \n    public Map\u003cString, UserEntity\u003e getDatabase() {\n        if (data == null) {\n            data = new HashMap\u003c\u003e();\n\n            UserEntity jack = new UserEntity(\n                    \"jack\",\n                    \"$2a$10$AQol1A.LkxoJ5dEzS5o5E.QG9jD.hncoeCGdVaMQZaiYZ98V/JyRq\",\n                    getGrants(\"ROLE_USER\"));\n            UserEntity danny = new UserEntity(\n                    \"danny\",\n                    \"$2a$10$8nMJR6r7lvh9H2INtM2vtuA156dHTcQUyU.2Q2OK/7LwMd/I.HM12\",\n                    getGrants(\"ROLE_EDITOR\"));\n            UserEntity smith = new UserEntity(\n                    \"smith\",\n                    \"$2a$10$E86mKigOx1NeIr7D6CJM3OQnWdaPXOjWe4OoRqDqFgNgowvJW9nAi\",\n                    getGrants(\"ROLE_ADMIN\"));\n            data.put(\"jack\", jack);\n            data.put(\"danny\", danny);\n            data.put(\"smith\", smith);\n        }\n        return data;\n    }\n    \n    private Collection\u003cGrantedAuthority\u003e getGrants(String role) {\n        return AuthorityUtils.commaSeparatedStringToAuthorityList(role);\n    }\n}\n```\n\n### UserService.java\n\n这里再模拟一个 service，主要就是模仿数据库的操作。\n\n```java\n@Service\npublic class UserService {\n\n    @Autowired\n    private Database database;\n\n    public UserEntity getUserByUsername(String username) {\n        return database.getDatabase().get(username);\n    }\n}\n```\n\n### JwtUtil.java\n\n自己编写的一个工具类，主要负责 JWT 的签名和鉴权。\n\n```java\npublic class JwtUtil {\n\n    // 过期时间5分钟\n    private final static long EXPIRE_TIME = 5 * 60 * 1000;\n\n    /**\n     * 生成签名,5min后过期\n     * @param username 用户名\n     * @param secret 用户的密码\n     * @return 加密的token\n     */\n    public static String sign(String username, String secret) {\n        Date expireDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);\n        try {\n            Algorithm algorithm = Algorithm.HMAC256(secret);\n            return JWT.create()\n                    .withClaim(\"username\", username)\n                    .withExpiresAt(expireDate)\n                    .sign(algorithm);\n        } catch (Exception e) {\n            return null;\n        }\n    }\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 e) {\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\n## Spring Security 改造\n\n登入这块，我们使用自定义的 `JwtAuthenticationFilter` 来进行登入。\n\n请求鉴权，我们使用自定义的 `JwtAuthorizationFilter` 来处理。\n\n\u003e 也许大家觉得两个单词长的有点像，😜。\n\n### UserDetailsServiceImpl.java\n\n我们首先实现官方的 `UserDetailsService` 接口，这里主要负责一个从数据库拿数据的操作。\n\n```java\n@Service\npublic class UserDetailsServiceImpl implements UserDetailsService {\n\n    @Autowired\n    private UserService userService;\n\n    @Override\n    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {\n        UserEntity userEntity = userService.getUserByUsername(username);\n        if (userEntity == null) {\n            throw new UsernameNotFoundException(\"This username didn't exist.\");\n        }\n        return new User(userEntity.getUsername(), userEntity.getPassword(), userEntity.getRole());\n    }\n}\n```\n\n后序我们还需要对其进行缓存改造，不然每次请求都要从数据库拿一次数据鉴权，对数据库压力太大了。\n\n### JwtAuthenticationFilter.java\n\n这个过滤器主要处理登入操作，我们继承了 `UsernamePasswordAuthenticationFilter`，这样能大大简化我们的工作量。\n\n```java\npublic class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {\n\n    /*\n    过滤器一定要设置 AuthenticationManager，所以此处我们这么编写，这里的 AuthenticationManager\n    我会从 Security 配置的时候传入\n    */\n    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {\n        /*\n        运行父类 UsernamePasswordAuthenticationFilter 的构造方法，能够设置此滤器指定\n        方法为 POST [\\login]\n        */\n        super();\n        setAuthenticationManager(authenticationManager);\n    }\n\n    @Override\n    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {\n        // 从请求的 POST 中拿取 username 和 password 两个字段进行登入\n        String username = request.getParameter(\"username\");\n        String password = request.getParameter(\"password\");\n        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);\n        // 设置一些客户 IP 啥信息，后面想用的话可以用，虽然没啥用\n        setDetails(request, token);\n        // 交给 AuthenticationManager 进行鉴权\n        return getAuthenticationManager().authenticate(token);\n    }\n\n    /*\n    鉴权成功进行的操作，我们这里设置返回加密后的 token\n    */\n    @Override\n    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {\n        handleResponse(request, response, authResult, null);\n    }\n\n    /*\n    鉴权失败进行的操作，我们这里就返回 用户名或密码错误 的信息\n    */\n    @Override\n    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {\n        handleResponse(request, response, null, failed);\n    }\n\n    private void handleResponse(HttpServletRequest request, HttpServletResponse response, Authentication authResult, AuthenticationException failed) throws IOException, ServletException {\n        ObjectMapper mapper = new ObjectMapper();\n        ResponseEntity responseEntity = new ResponseEntity();\n        response.setHeader(\"Content-Type\", \"application/json;charset=UTF-8\");\n        if (authResult != null) {\n            // 处理登入成功请求\n            User user = (User) authResult.getPrincipal();\n            String token = JwtUtil.sign(user.getUsername(), user.getPassword());\n            responseEntity.setStatus(HttpStatus.OK.value());\n            responseEntity.setMsg(\"登入成功\");\n            responseEntity.setData(\"Bearer \" + token);\n            response.setStatus(HttpStatus.OK.value());\n            response.getWriter().write(mapper.writeValueAsString(responseEntity));\n        } else {\n            // 处理登入失败请求\n            responseEntity.setStatus(HttpStatus.BAD_REQUEST.value());\n            responseEntity.setMsg(\"用户名或密码错误\");\n            responseEntity.setData(null);\n            response.setStatus(HttpStatus.BAD_REQUEST.value());\n            response.getWriter().write(mapper.writeValueAsString(responseEntity));\n        }\n    }\n}\n```\n\n\u003e `private void handleResponse()` 此处处理的方法不是很好，我的想法是跳转到控制器中进行处理，但是这样鉴权成功的 token 带不过去，所以先这么写了，有点复杂。\n\n### JwtAuthorizationFilter.java\n\n这个过滤器处理每个请求鉴权，我们选择继承 `BasicAuthenticationFilter` ，考虑到 Basic 认证和 JWT 比较像，就选择了它。\n\n```java\npublic class JwtAuthorizationFilter extends BasicAuthenticationFilter {\n\n    private UserDetailsService userDetailsService;\n\n    // 会从 Spring Security 配置文件那里传过来\n    public JwtAuthorizationFilter(AuthenticationManager authenticationManager, UserDetailsService userDetailsService) {\n        super(authenticationManager);\n        this.userDetailsService = userDetailsService;\n    }\n\n    @Override\n    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {\n        // 判断是否有 token，并且进行认证\n        Authentication token = getAuthentication(request);\n        if (token == null) {\n            chain.doFilter(request, response);\n            return;\n        }\n        // 认证成功\n        SecurityContextHolder.getContext().setAuthentication(token);\n        chain.doFilter(request, response);\n    }\n\n    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {\n        String header = request.getHeader(\"Authorization\");\n        if (header == null || ! header.startsWith(\"Bearer \")) {\n            return null;\n        }\n\n        String token = header.split(\" \")[1];\n        String username = JwtUtil.getUsername(token);\n        UserDetails userDetails = null;\n        try {\n            userDetails = userDetailsService.loadUserByUsername(username);\n        } catch (UsernameNotFoundException e) {\n            return null;\n        }\n        if (! JwtUtil.verify(token, username, userDetails.getPassword())) {\n            return null;\n        }\n        return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());\n    }\n}\n```\n\n### SecurityConfiguration.java\n\n此处我们进行 Security 的配置，并且实现缓存功能。缓存这块我们使用官方现成的 `CachingUserDetailsService` ，唯独的缺点就是它没有 public 方法，我们不能正常实例化，需要曲线救国，下面代码也有详细说明。\n\n```java\n// 开启 Security\n@EnableWebSecurity\n// 开启注解配置支持\n@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)\npublic class SecurityConfiguration extends WebSecurityConfigurerAdapter {\n\n    @Autowired\n    private UserDetailsServiceImpl userDetailsServiceImpl;\n\n    // Spring Boot 的 CacheManager，这里我们使用 JCache\n    @Autowired\n    private CacheManager cacheManager;\n\n    @Override\n    protected void configure(HttpSecurity http) throws Exception {\n        // 开启跨域\n        http.cors()\n                .and()\n                // security 默认 csrf 是开启的，我们使用了 token ，这个也没有什么必要了\n                .csrf().disable()\n                .authorizeRequests()\n                // 默认所有请求通过，但是我们要在需要权限的方法加上安全注解，这样比写死配置灵活很多\n                .anyRequest().permitAll()\n                .and()\n                // 添加自己编写的两个过滤器\n                .addFilter(new JwtAuthenticationFilter(authenticationManager()))\n                .addFilter(new JwtAuthorizationFilter(authenticationManager(), cachingUserDetailsService(userDetailsServiceImpl)))\n                // 前后端分离是 STATELESS，故 session 使用该策略\n                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);\n    }\n\n    // 此处配置 AuthenticationManager，并且实现缓存\n    @Override\n    protected void configure(AuthenticationManagerBuilder auth) throws Exception {\n        // 对自己编写的 UserDetailsServiceImpl 进一步包装，实现缓存\n        CachingUserDetailsService cachingUserDetailsService = cachingUserDetailsService(userDetailsServiceImpl);\n        // jwt-cache 我们在 ehcache.xml 配置文件中有声明\n        UserCache userCache = new SpringCacheBasedUserCache(cacheManager.getCache(\"jwt-cache\"));\n        cachingUserDetailsService.setUserCache(userCache);\n        /*\n        security 默认鉴权完成后会把密码抹除，但是这里我们使用用户的密码来作为 JWT 的生成密钥，\n        如果被抹除了，在对 JWT 进行签名的时候就拿不到用户密码了，故此处关闭了自动抹除密码。\n         */\n        auth.eraseCredentials(false);\n        auth.userDetailsService(cachingUserDetailsService);\n    }\n\n    @Bean\n    public PasswordEncoder passwordEncoder() {\n        return new BCryptPasswordEncoder();\n    }\n\n    /*\n    此处我们实现缓存的时候，我们使用了官方现成的 CachingUserDetailsService ，但是这个类的构造方法不是 public 的，\n    我们不能够正常实例化，所以在这里进行曲线救国。\n     */\n    private CachingUserDetailsService cachingUserDetailsService(UserDetailsServiceImpl delegate) {\n\n        Constructor\u003cCachingUserDetailsService\u003e ctor = null;\n        try {\n            ctor = CachingUserDetailsService.class.getDeclaredConstructor(UserDetailsService.class);\n        } catch (NoSuchMethodException e) {\n            e.printStackTrace();\n        }\n        Assert.notNull(ctor, \"CachingUserDetailsService constructor is null\");\n        ctor.setAccessible(true);\n        return BeanUtils.instantiateClass(ctor, delegate);\n    }\n}\n```\n\n### Ehcache 配置\n\nEhcache 3 开始，统一使用了 JCache，就是  JSR107 标准，网上很多教程都是基于 Ehcache 2 的，所以大家可能在参照网上的教程会遇到很多坑。\n\n\u003e JSR107：emm，其实 JSR107 是一种缓存标准，各个框架只要遵守这个标准，就是现实大一统。差不多就是我不需要更改系统代码，也能随意更换底层的缓存系统。\n\n在 resources 目录下创建 `ehcache.xml` 文件：\n\n```xml\n\u003cehcache:config\n        xmlns:ehcache=\"http://www.ehcache.org/v3\"\n        xmlns:jcache=\"http://www.ehcache.org/v3/jsr107\"\u003e\n\n    \u003cehcache:cache alias=\"jwt-cache\"\u003e\n        \u003c!-- 我们使用用户名作为缓存的 key，故使用 String --\u003e\n        \u003cehcache:key-type\u003ejava.lang.String\u003c/ehcache:key-type\u003e\n        \u003cehcache:value-type\u003eorg.springframework.security.core.userdetails.User\u003c/ehcache:value-type\u003e\n        \u003cehcache:expiry\u003e\n            \u003cehcache:ttl unit=\"days\"\u003e1\u003c/ehcache:ttl\u003e\n        \u003c/ehcache:expiry\u003e\n        \u003c!-- 缓存实体的数量 --\u003e\n        \u003cehcache:heap unit=\"entries\"\u003e2000\u003c/ehcache:heap\u003e\n    \u003c/ehcache:cache\u003e\n\n\u003c/ehcache:config\u003e\n```\n\n在 `application.properties` 中开启缓存支持：\n\n```properties\nspring.cache.type=jcache\nspring.cache.jcache.config=classpath:ehcache.xml\n```\n\n### 统一全局异常\n\n我们要把异常的返回形式也统一了，这样才能方便前端的调用。\n\n我们平常会使用 `@RestControllerAdvice` 来统一异常，但是它只能管理 Controller 层面抛出的异常。Security 中抛出的异常不会抵达 Controller，无法被 `@RestControllerAdvice` 捕获，故我们还要改造 `ErrorController` 。\n\n```java\n@RestController\npublic class CustomErrorController implements ErrorController {\n\n    @Override\n    public String getErrorPath() {\n        return \"/error\";\n    }\n\n    @RequestMapping(\"/error\")\n    public ResponseEntity handleError(HttpServletRequest request, HttpServletResponse response) {\n        return new ResponseEntity(response.getStatus(), (String) request.getAttribute(\"javax.servlet.error.message\"), null);\n    }\n}\n```\n\n## 测试\n\n写个控制器试试，大家也可以参考我控制器里面获取用户信息的方式，推荐使用 `@AuthenticationPrincipal` 这个注解！！！\n\n```java\n@RestController\npublic class MainController {\n\n    // 任何人都可以访问，在方法中判断用户是否合法\n    @GetMapping(\"everyone\")\n    public ResponseEntity everyone() {\n        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();\n        if (! (authentication instanceof AnonymousAuthenticationToken)) {\n            // 登入用户\n            return new ResponseEntity(HttpStatus.OK.value(), \"You are already login\", authentication.getPrincipal());\n        } else {\n            return new ResponseEntity(HttpStatus.OK.value(), \"You are anonymous\", null);\n        }\n    }\n\n    @GetMapping(\"user\")\n    @PreAuthorize(\"hasAuthority('ROLE_USER')\")\n    public ResponseEntity user(@AuthenticationPrincipal UsernamePasswordAuthenticationToken token) {\n        return new ResponseEntity(HttpStatus.OK.value(), \"You are user\", token);\n    }\n\n    @GetMapping(\"admin\")\n    @IsAdmin\n    public ResponseEntity admin(@AuthenticationPrincipal UsernamePasswordAuthenticationToken token) {\n        return new ResponseEntity(HttpStatus.OK.value(), \"You are admin\", token);\n    }\n}\n```\n\n我这里还使用了 `@IsAdmin` 注解，`@IsAdmin` 注解如下：\n\n```java\n@Target({ElementType.METHOD, ElementType.TYPE})\n@Retention(RetentionPolicy.RUNTIME)\n@PreAuthorize(\"hasAnyRole('ROLE_ADMIN')\")\npublic @interface IsAdmin {\n}\n```\n\n这样能省去每次编写一长串的 `@PreAuthorize()` ，而且更加直观。\n\n## FAQ\n\n### 如何解决JWT过期问题？\n\n我们可以在 `JwtAuthorizationFilter` 中加点料，如果用户快过期了，返回个特别的状态码，前端收到此状态码去访问 `GET /re_authentication` 携带老的 token 重新拿一个新的 token 即可。\n\n### 如何作废已颁发未过期的 token？\n\n我个人的想法是把每次生成的 token 放入缓存中，每次请求都从缓存里拿，如果没有则代表此缓存报废。\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmith-cruise%2Fspring-boot-security-jwt-spa","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsmith-cruise%2Fspring-boot-security-jwt-spa","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmith-cruise%2Fspring-boot-security-jwt-spa/lists"}