[
  {
    "path": ".gitignore",
    "content": "HELP.md\n/target/\n!.mvn/wrapper/maven-wrapper.jar\n\n### STS ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\n\n### IntelliJ IDEA ###\n.idea\n*.iws\n*.iml\n*.ipr\n\n### NetBeans ###\n/nbproject/private/\n/nbbuild/\n/dist/\n/nbdist/\n/.nb-gradle/\n/build/\n\n### VS Code ###\n.vscode/\n.DS_Store\n"
  },
  {
    "path": ".mvn/wrapper/MavenWrapperDownloader.java",
    "content": "/*\nLicensed to the Apache Software Foundation (ASF) under one\nor more contributor license agreements.  See the NOTICE file\ndistributed with this work for additional information\nregarding copyright ownership.  The ASF licenses this file\nto you under the Apache License, Version 2.0 (the\n\"License\"); you may not use this file except in compliance\nwith the License.  You may obtain a copy of the License at\n\n  https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing,\nsoftware distributed under the License is distributed on an\n\"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\nKIND, either express or implied.  See the License for the\nspecific language governing permissions and limitations\nunder the License.\n*/\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.net.URL;\nimport java.nio.channels.Channels;\nimport java.nio.channels.ReadableByteChannel;\nimport java.util.Properties;\n\npublic class MavenWrapperDownloader {\n\n    /**\n     * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.\n     */\n    private static final String DEFAULT_DOWNLOAD_URL =\n            \"https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar\";\n\n    /**\n     * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to\n     * use instead of the default one.\n     */\n    private static final String MAVEN_WRAPPER_PROPERTIES_PATH =\n            \".mvn/wrapper/maven-wrapper.properties\";\n\n    /**\n     * Path where the maven-wrapper.jar will be saved to.\n     */\n    private static final String MAVEN_WRAPPER_JAR_PATH =\n            \".mvn/wrapper/maven-wrapper.jar\";\n\n    /**\n     * Name of the property which should be used to override the default download url for the wrapper.\n     */\n    private static final String PROPERTY_NAME_WRAPPER_URL = \"wrapperUrl\";\n\n    public static void main(String args[]) {\n        System.out.println(\"- Downloader started\");\n        File baseDirectory = new File(args[0]);\n        System.out.println(\"- Using base directory: \" + baseDirectory.getAbsolutePath());\n\n        // If the maven-wrapper.properties exists, read it and check if it contains a custom\n        // wrapperUrl parameter.\n        File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);\n        String url = DEFAULT_DOWNLOAD_URL;\n        if (mavenWrapperPropertyFile.exists()) {\n            FileInputStream mavenWrapperPropertyFileInputStream = null;\n            try {\n                mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);\n                Properties mavenWrapperProperties = new Properties();\n                mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);\n                url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);\n            } catch (IOException e) {\n                System.out.println(\"- ERROR loading '\" + MAVEN_WRAPPER_PROPERTIES_PATH + \"'\");\n            } finally {\n                try {\n                    if (mavenWrapperPropertyFileInputStream != null) {\n                        mavenWrapperPropertyFileInputStream.close();\n                    }\n                } catch (IOException e) {\n                    // Ignore ...\n                }\n            }\n        }\n        System.out.println(\"- Downloading from: : \" + url);\n\n        File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);\n        if (!outputFile.getParentFile().exists()) {\n            if (!outputFile.getParentFile().mkdirs()) {\n                System.out.println(\n                        \"- ERROR creating output direcrory '\" + outputFile.getParentFile().getAbsolutePath() + \"'\");\n            }\n        }\n        System.out.println(\"- Downloading to: \" + outputFile.getAbsolutePath());\n        try {\n            downloadFileFromURL(url, outputFile);\n            System.out.println(\"Done\");\n            System.exit(0);\n        } catch (Throwable e) {\n            System.out.println(\"- Error downloading\");\n            e.printStackTrace();\n            System.exit(1);\n        }\n    }\n\n    private static void downloadFileFromURL(String urlString, File destination) throws Exception {\n        URL website = new URL(urlString);\n        ReadableByteChannel rbc;\n        rbc = Channels.newChannel(website.openStream());\n        FileOutputStream fos = new FileOutputStream(destination);\n        fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);\n        fos.close();\n        rbc.close();\n    }\n\n}\n"
  },
  {
    "path": ".mvn/wrapper/maven-wrapper.properties",
    "content": "distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.0/apache-maven-3.6.0-bin.zip\n"
  },
  {
    "path": "README.md",
    "content": "# 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<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project 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\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>2.1.7.RELEASE</version>\n        <relativePath/> <!-- lookup parent from repository -->\n    </parent>\n    <groupId>org.inlighting</groupId>\n    <artifactId>spring-boot-security-jwt</artifactId>\n    <version>0.0.1-SNAPSHOT</version>\n    <name>spring-boot-security-jwt</name>\n    <description>Demo project for Spring Boot</description>\n\n    <properties>\n        <java.version>1.8</java.version>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-security</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n        <!-- JWT 支持 -->\n        <dependency>\n            <groupId>com.auth0</groupId>\n            <artifactId>java-jwt</artifactId>\n            <version>3.8.2</version>\n        </dependency>\n\n        <!-- cache 支持 -->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-cache</artifactId>\n        </dependency>\n\n        <!-- cache 支持 -->\n        <dependency>\n            <groupId>org.ehcache</groupId>\n            <artifactId>ehcache</artifactId>\n        </dependency>\n\n        <!-- cache 支持 -->\n        <dependency>\n            <groupId>javax.cache</groupId>\n            <artifactId>cache-api</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.security</groupId>\n            <artifactId>spring-security-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <!-- ehcache 读取 xml 配置文件使用 -->\n        <dependency>\n            <groupId>javax.xml.bind</groupId>\n            <artifactId>jaxb-api</artifactId>\n            <version>2.3.0</version>\n        </dependency>\n\n        <!-- ehcache 读取 xml 配置文件使用 -->\n        <dependency>\n            <groupId>com.sun.xml.bind</groupId>\n            <artifactId>jaxb-impl</artifactId>\n            <version>2.3.0</version>\n        </dependency>\n\n        <!-- ehcache 读取 xml 配置文件使用 -->\n        <dependency>\n            <groupId>com.sun.xml.bind</groupId>\n            <artifactId>jaxb-core</artifactId>\n            <version>2.3.0</version>\n        </dependency>\n\n        <!-- ehcache 读取 xml 配置文件使用 -->\n        <dependency>\n            <groupId>javax.activation</groupId>\n            <artifactId>activation</artifactId>\n            <version>1.1.1</version>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>\n\n```\n\npom.xml 配置文件这块没有什么好说的，主要说明下面的几个依赖：\n\n```xml\n<!-- ehcache 读取 xml 配置文件使用 -->\n<dependency>\n  <groupId>javax.xml.bind</groupId>\n  <artifactId>jaxb-api</artifactId>\n  <version>2.3.0</version>\n</dependency>\n\n<!-- ehcache 读取 xml 配置文件使用 -->\n<dependency>\n  <groupId>com.sun.xml.bind</groupId>\n  <artifactId>jaxb-impl</artifactId>\n  <version>2.3.0</version>\n</dependency>\n\n<!-- ehcache 读取 xml 配置文件使用 -->\n<dependency>\n  <groupId>com.sun.xml.bind</groupId>\n  <artifactId>jaxb-core</artifactId>\n  <version>2.3.0</version>\n</dependency>\n\n<!-- ehcache 读取 xml 配置文件使用 -->\n<dependency>\n  <groupId>javax.activation</groupId>\n  <artifactId>activation</artifactId>\n  <version>1.1.1</version>\n</dependency>\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<? extends GrantedAuthority> 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<? extends GrantedAuthority> 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<? extends GrantedAuthority> getRole() {\n        return role;\n    }\n\n    public void setRole(Collection<? extends GrantedAuthority> 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<String, UserEntity> data = null;\n    \n    public Map<String, UserEntity> getDatabase() {\n        if (data == null) {\n            data = new HashMap<>();\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<GrantedAuthority> 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> 也许大家觉得两个单词长的有点像，😜。\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> `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<CachingUserDetailsService> 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> JSR107：emm，其实 JSR107 是一种缓存标准，各个框架只要遵守这个标准，就是现实大一统。差不多就是我不需要更改系统代码，也能随意更换底层的缓存系统。\n\n在 resources 目录下创建 `ehcache.xml` 文件：\n\n```xml\n<ehcache:config\n        xmlns:ehcache=\"http://www.ehcache.org/v3\"\n        xmlns:jcache=\"http://www.ehcache.org/v3/jsr107\">\n\n    <ehcache:cache alias=\"jwt-cache\">\n        <!-- 我们使用用户名作为缓存的 key，故使用 String -->\n        <ehcache:key-type>java.lang.String</ehcache:key-type>\n        <ehcache:value-type>org.springframework.security.core.userdetails.User</ehcache:value-type>\n        <ehcache:expiry>\n            <ehcache:ttl unit=\"days\">1</ehcache:ttl>\n        </ehcache:expiry>\n        <!-- 缓存实体的数量 -->\n        <ehcache:heap unit=\"entries\">2000</ehcache:heap>\n    </ehcache:cache>\n\n</ehcache:config>\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"
  },
  {
    "path": "README_OLD.md",
    "content": "# Spring Boot 2 + Spring Security 5 + JWT 的单页应用Restful解决方案-过时\n\n## 准备\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 我之前写过两篇关于安全框架的问题，大家可以大致看一看，打下基础。\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在开始前你至少需要了解 `Spring Security` 的基本配置和 `JWT` 机制。\n\n一些关于 `Maven` 的配置和 `Controller` 的编写这里就不说了，自己看下源码即可。\n\n本项目中 `JWT` 密钥是使用用户自己的登入密码，这样每一个 `token` 的密钥都不同，相对比较安全。\n\n## 改造思路\n\n平常我们使用 `Spring Security` 会用到 `UsernamePasswordAuthenticationFilter` 和 `UsernamePasswordAuthenticationToken` 这两个类，但这两个类初衷是为了解决表单登入，对 `JWT` 这类 `Token` 鉴权的方式并不是很友好。所以我们要开发属于自己的 `Filter` 和 `AuthenticationToken` 来替换掉  `Spring Security` 自带的类。\n\n同时默认的 `Spring Security` 鉴定用户是使用了 `ProviderManager` 这个类进行判断，同时 `ProviderManager` 会调用 `AuthenticationUserDetailsService` 这个接口中的 `UserDetails loadUserDetails(T token) throws UsernameNotFoundException` 来从数据库中获取用户信息（这个方法需要用户自己继承实现）。因为考虑到自带的实现方式并不能很好的支持JWT，例如 `UsernamePasswordAuthenticationToken`  中有 `username` 和 `password` 字段进行赋值，但是 `JWT` 是附带在请求的 `header` 中，只有一个 token ，何来 `username` 和 `password` 这种说法。\n\n所以我对其进行了大换血，例如获取用户的方法并没有在 `AuthenticationUserDetailsService`  中实现，但这样就可能不能完美的遵守 `Spring Security` 的官方设计，如果有更好的方法请指正。\n\n## 改造\n\n### 改造 `Authentication`\n\n`Authentication` 是 `Security` 官方提供的一个接口，是保存在 `SecurityContextHolder` 供调用鉴权使用的核心。\n\n这里主要说下三个方法\n\n`getCredentials()` 原本是用于获取密码，现我们打算用其存放前端传递过来的 `token`\n\n`getPrincipal()` 原本用于存放用户信息，现在我们继续保留。比如存储一些用户的 `username`，`id` 等关键信息供 `Controller` 中使用\n\n`getDetails()` 原本返回一些客户端 `IP` 等杂项，但是考虑到这里基本都是  `restful` 这类无状态请求，这个就显的无关紧要 ，所以就被阉割了:happy:\n\n**默认提供的Authentication接口**\n\n```java\npublic interface Authentication extends Principal, Serializable {\n\n\tCollection<? extends GrantedAuthority> getAuthorities();\n\n\tObject getCredentials();\n\n\tObject getDetails();\n\n\tObject getPrincipal();\n\n\tboolean isAuthenticated();\n\n\tvoid setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;\n}\n```\n\n**JWTAuthenticationToken**\n\n我们编写属于自己的 `Authentication` ，注意**两个构造方法的不同**。 `AbstractAuthenticationToken` 是官方实现 `Authentication` 的一个类。\n\n```java\npublic class JWTAuthenticationToken extends AbstractAuthenticationToken {\n\n    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;\n\n    private final Object principal;\n    private final Object credentials;\n\n    /**\n     * 鉴定token前使用的方法，因为还没有鉴定token是否合法，所以要setAuthenticated(false)\n     * @param token JWT密钥\n     */\n    public JWTAuthenticationToken(String token) {\n        super(null);\n        this.principal = null;\n        this.credentials = token;\n        setAuthenticated(false);\n    }\n\n    /**\n     * 鉴定成功后调用的方法，返回的JWTAuthenticationToken供Controller里面调用。\n     * 因为已经鉴定成功，所以要setAuthenticated(true)\n     * @param token JWT密钥\n     * @param userInfo 一些用户的信息，比如username, id等\n     * @param authorities 所拥有的权限\n     */\n    public JWTAuthenticationToken(String token, Object userInfo, Collection<? extends GrantedAuthority> authorities) {\n        super(authorities);\n        this.principal = userInfo;\n        this.credentials = token;\n        setAuthenticated(true);\n    }\n\n    @Override\n    public Object getCredentials() {\n        return credentials;\n    }\n\n    @Override\n    public Object getPrincipal() {\n        return principal;\n    }\n}\n```\n\n### 改造 AuthenticationManager\n\n用于判断用户 `token` 是否合法\n\n**JWTAuthenticationManager**\n\n```java\n@Component\npublic class JWTAuthenticationManager implements AuthenticationManager {\n\n    @Autowired\n    private UserService userService;\n\n    /**\n     * 进行token鉴定\n     * @param authentication 待鉴定的JWTAuthenticationToken\n     * @return 鉴定完成的JWTAuthenticationToken，供Controller使用\n     * @throws AuthenticationException 如果鉴定失败，抛出\n     */\n    @Override\n    public Authentication authenticate(Authentication authentication) throws AuthenticationException {\n        String token = authentication.getCredentials().toString();\n        String username = JWTUtil.getUsername(token);\n\n        UserEntity userEntity = userService.getUser(username);\n        if (userEntity == null) {\n            throw new UsernameNotFoundException(\"该用户不存在\");\n        }\n\n        /*\n         * 官方推荐在本方法中必须要处理三种异常，\n         * DisabledException、LockedException、BadCredentialsException\n         * 这里为了方便就只处理了BadCredentialsException，大家可以根据自己业务的需要进行定制\n         * 详情看AuthenticationManager的JavaDoc\n         */\n        boolean isAuthenticatedSuccess = JWTUtil.verify(token, username, userEntity.getPassword());\n        if (! isAuthenticatedSuccess) {\n            throw new BadCredentialsException(\"用户名或密码错误\");\n        }\n\n        JWTAuthenticationToken authenticatedAuth = new JWTAuthenticationToken(\n                token, userEntity, AuthorityUtils.commaSeparatedStringToAuthorityList(userEntity.getRole())\n        );\n        return authenticatedAuth;\n    }\n}\n```\n\n### 开发属于自己的 Filter\n\n接下来我们要使用属于自己的过滤器，考虑到 `token` 是附加在 `header` 中，这和 `BasicAuthentication` 认证很像，所以我们继承 `BasicAuthenticationFilter`  进行重写核心方法改造。\n\n**JWTAuthenticationFilter**\n\n```java\npublic class JWTAuthenticationFilter extends BasicAuthenticationFilter {\n\n    /**\n     * 使用我们自己开发的JWTAuthenticationManager\n     * @param authenticationManager 我们自己开发的JWTAuthenticationManager\n     */\n    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {\n        super(authenticationManager);\n    }\n\n    @Override\n    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {\n        String header = request.getHeader(\"Authorization\");\n        if (header == null || !header.toLowerCase().startsWith(\"bearer \")) {\n            chain.doFilter(request, response);\n            return;\n        }\n\n        try {\n            String token = header.split(\" \")[1];\n            JWTAuthenticationToken JWToken = new JWTAuthenticationToken(token);\n            // 鉴定权限，如果鉴定失败，AuthenticationManager会抛出异常被我们捕获\n            Authentication authResult = getAuthenticationManager().authenticate(JWToken);\n            // 将鉴定成功后的Authentication写入SecurityContextHolder中供后序使用\n            SecurityContextHolder.getContext().setAuthentication(authResult);\n        } catch (AuthenticationException failed) {\n            SecurityContextHolder.clearContext();\n            // 返回鉴权失败\n            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, failed.getMessage());\n            return;\n        }\n        chain.doFilter(request, response);\n    }\n}\n```\n\n### 配置\n\n**SecurityConfig**\n\n```java\n// 开启方法注解功能\n@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)\n@Configuration\npublic class SecurityConfig extends WebSecurityConfigurerAdapter {\n\n    @Autowired\n    private JWTAuthenticationManager jwtAuthenticationManager;\n\n    @Override\n    protected void configure(HttpSecurity http) throws Exception {\n        // restful具有先天的防范csrf攻击，所以关闭这功能\n        http.csrf().disable()\n                // 默认允许所有的请求通过，后序我们通过方法注解的方式来粒度化控制权限\n                .authorizeRequests().anyRequest().permitAll()\n                .and()\n                // 添加属于我们自己的过滤器，注意因为我们没有开启formLogin()，所以UsernamePasswordAuthenticationFilter根本不会被调用\n                .addFilterAt(new JWTAuthenticationFilter(jwtAuthenticationManager), UsernamePasswordAuthenticationFilter.class)\n                // 前后端分离本身就是无状态的，所以我们不需要cookie和session这类东西。所有的信息都保存在一个token之中。\n                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);\n    }\n\n}\n```\n\n关于**方法注解鉴权** 这块有很多奇淫巧技，可以看看 [Spring Boot+Spring Security+Thymeleaf 简单教程](https://github.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo#spring-security-%E9%85%8D%E7%BD%AE \"Spring Boot+Spring Security+Thymeleaf 简单教程\") 这篇文章\n\n## 统一全局异常\n\n一个 `restful` 最后的异常抛出肯定是要格式统一的，这样才方便前端的调用。\n\n我们平常会使用 `RestControllerAdvice` 来统一异常，但是他只能管理我们自己抛出的异常，而管不住框架本身的异常，比如404啥的，所以我们还要改造 `ErrorController`\n\n**ExceptionController**\n\n```java\n@RestControllerAdvice\npublic class ExceptionController {\n\n    // 捕捉控制器里面自己抛出的所有异常\n    @ExceptionHandler(Exception.class)\n    public ResponseEntity<ResponseBean> globalException(Exception ex) {\n        return new ResponseEntity<>(\n                new ResponseBean(\n                        HttpStatus.INTERNAL_SERVER_ERROR.value(), ex.getMessage(), null), HttpStatus.INTERNAL_SERVER_ERROR\n        );\n    }\n}\n```\n\n**CustomErrorController**\n\n如果直接去实现 `ErrorController` 这个接口，有很多现成方法都没有，不好用，所以我们选择 `AbstractErrorController`\n\n```java\n@RestController\npublic class CustomErrorController extends AbstractErrorController {\n\n    // 异常路径网址\n    private final String PATH = \"/error\";\n\n    public CustomErrorController(ErrorAttributes errorAttributes) {\n        super(errorAttributes);\n    }\n\n    @RequestMapping(\"/error\")\n    public ResponseEntity<ResponseBean> error(HttpServletRequest request) {\n        // 获取request中的异常信息，里面有好多，比如时间、路径啥的，大家可以自行遍历map查看\n        Map<String, Object> attributes = getErrorAttributes(request, true);\n        // 这里只选择返回message字段\n        return new ResponseEntity<>(\n                new ResponseBean(\n                       getStatus(request).value() , (String) attributes.get(\"message\"), null), getStatus(request)\n        );\n    }\n\n    @Override\n    public String getErrorPath() {\n        return PATH;\n    }\n}\n```\n\n## 测试\n\n写个控制器试试，大家也可以参考我控制器里面获取用户信息的方式，推荐使用 `@AuthenticationPrincipal` 这个方法！！！\n\n```java\n@RestController\npublic class MainController {\n\n    @Autowired\n    private UserService userService;\n\n    // 登入，获取token\n    @PostMapping(\"login\")\n    public ResponseEntity<ResponseBean> login(@RequestParam String username, @RequestParam String password) {\n        UserEntity userEntity = userService.getUser(username);\n        if (userEntity==null || !userEntity.getPassword().equals(password)) {\n            return new ResponseEntity<>(new ResponseBean(HttpStatus.BAD_REQUEST.value(), \"login fail\", null), HttpStatus.BAD_REQUEST);\n        }\n\n        // JWT签名\n        String token = JWTUtil.sign(username, password);\n        return new ResponseEntity<>(new ResponseBean(HttpStatus.OK.value(), \"login success\", token), HttpStatus.OK);\n    }\n\n    // 任何人都可以访问，在方法中判断用户是否合法\n    @GetMapping(\"everyone\")\n    public ResponseEntity<ResponseBean> everyone() {\n        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();\n        if (authentication.isAuthenticated()) {\n            // 登入用户\n            return new ResponseEntity<>(new ResponseBean(HttpStatus.OK.value(), \"You are already login\", authentication.getPrincipal()), HttpStatus.OK);\n        } else {\n            return new ResponseEntity<>(new ResponseBean(HttpStatus.OK.value(), \"You are anonymous\", null), HttpStatus.OK);\n        }\n    }\n    \n    @GetMapping(\"user\")\n    @PreAuthorize(\"hasAuthority('ROLE_USER')\")\n    public ResponseEntity<ResponseBean> user(@AuthenticationPrincipal UserEntity userEntity) {\n        return new ResponseEntity<>(new ResponseBean(HttpStatus.OK.value(), \"You are user\", userEntity), HttpStatus.OK);\n    }\n\n    @GetMapping(\"admin\")\n    @PreAuthorize(\"hasAuthority('ROLE_ADMIN')\")\n    public ResponseEntity<ResponseBean> admin(@AuthenticationPrincipal UserEntity userEntity) {\n        return new ResponseEntity<>(new ResponseBean(HttpStatus.OK.value(), \"You are admin\", userEntity), HttpStatus.OK);\n    }\n\n}\n```\n\n\n\n## 其他\n\n这里简单解答下一些常见问题。\n### 鉴定Token是否合法是每次请求数据库过于耗费资源\n\n我们不可能每一次鉴定都去数据库拿一次数据来判断 `token` 是否合法，这样非常浪费资源还影响效率。\n\n我们可以在 `JWTAuthenticationManager` 使用缓存。\n\n当用户第一次访问，我们查询数据库判断 `token` 是否合法，如果合法将其放入缓存（缓存过期时间和token过期时间一致），此后每个请求先去缓存中寻找，如果存在则跳过请求数据库环节，直接当做该 `token` 合法。\n\n### 如何解决JWT过期问题\n\n在 `JWTAuthenticationManager` 中编写方法，当 `token` 即将过期时抛出一个特定的异常，例如 `ReAuthenticateException`，然后我们在 `JWTAuthenticationFilter` 中单独捕获这个异常，返回一个特定的 `http` 状态码，然后前端去单独另外访问 `GET /re_authentication` 获取一个新的token来替代掉原本的，同时从缓存中删除老的 `token`。"
  },
  {
    "path": "mvnw",
    "content": "#!/bin/sh\n# ----------------------------------------------------------------------------\n# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#    https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n# ----------------------------------------------------------------------------\n\n# ----------------------------------------------------------------------------\n# Maven2 Start Up Batch script\n#\n# Required ENV vars:\n# ------------------\n#   JAVA_HOME - location of a JDK home dir\n#\n# Optional ENV vars\n# -----------------\n#   M2_HOME - location of maven2's installed home dir\n#   MAVEN_OPTS - parameters passed to the Java VM when running Maven\n#     e.g. to debug Maven itself, use\n#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n# ----------------------------------------------------------------------------\n\nif [ -z \"$MAVEN_SKIP_RC\" ] ; then\n\n  if [ -f /etc/mavenrc ] ; then\n    . /etc/mavenrc\n  fi\n\n  if [ -f \"$HOME/.mavenrc\" ] ; then\n    . \"$HOME/.mavenrc\"\n  fi\n\nfi\n\n# OS specific support.  $var _must_ be set to either true or false.\ncygwin=false;\ndarwin=false;\nmingw=false\ncase \"`uname`\" in\n  CYGWIN*) cygwin=true ;;\n  MINGW*) mingw=true;;\n  Darwin*) darwin=true\n    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home\n    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html\n    if [ -z \"$JAVA_HOME\" ]; then\n      if [ -x \"/usr/libexec/java_home\" ]; then\n        export JAVA_HOME=\"`/usr/libexec/java_home`\"\n      else\n        export JAVA_HOME=\"/Library/Java/Home\"\n      fi\n    fi\n    ;;\nesac\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  if [ -r /etc/gentoo-release ] ; then\n    JAVA_HOME=`java-config --jre-home`\n  fi\nfi\n\nif [ -z \"$M2_HOME\" ] ; then\n  ## resolve links - $0 may be a link to maven's home\n  PRG=\"$0\"\n\n  # need this for relative symlinks\n  while [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n      PRG=\"$link\"\n    else\n      PRG=\"`dirname \"$PRG\"`/$link\"\n    fi\n  done\n\n  saveddir=`pwd`\n\n  M2_HOME=`dirname \"$PRG\"`/..\n\n  # make it fully qualified\n  M2_HOME=`cd \"$M2_HOME\" && pwd`\n\n  cd \"$saveddir\"\n  # echo Using m2 at $M2_HOME\nfi\n\n# For Cygwin, ensure paths are in UNIX format before anything is touched\nif $cygwin ; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=`cygpath --unix \"$M2_HOME\"`\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=`cygpath --unix \"$JAVA_HOME\"`\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=`cygpath --path --unix \"$CLASSPATH\"`\nfi\n\n# For Mingw, ensure paths are in UNIX format before anything is touched\nif $mingw ; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=\"`(cd \"$M2_HOME\"; pwd)`\"\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=\"`(cd \"$JAVA_HOME\"; pwd)`\"\n  # TODO classpath?\nfi\n\nif [ -z \"$JAVA_HOME\" ]; then\n  javaExecutable=\"`which javac`\"\n  if [ -n \"$javaExecutable\" ] && ! [ \"`expr \\\"$javaExecutable\\\" : '\\([^ ]*\\)'`\" = \"no\" ]; then\n    # readlink(1) is not available as standard on Solaris 10.\n    readLink=`which readlink`\n    if [ ! `expr \"$readLink\" : '\\([^ ]*\\)'` = \"no\" ]; then\n      if $darwin ; then\n        javaHome=\"`dirname \\\"$javaExecutable\\\"`\"\n        javaExecutable=\"`cd \\\"$javaHome\\\" && pwd -P`/javac\"\n      else\n        javaExecutable=\"`readlink -f \\\"$javaExecutable\\\"`\"\n      fi\n      javaHome=\"`dirname \\\"$javaExecutable\\\"`\"\n      javaHome=`expr \"$javaHome\" : '\\(.*\\)/bin'`\n      JAVA_HOME=\"$javaHome\"\n      export JAVA_HOME\n    fi\n  fi\nfi\n\nif [ -z \"$JAVACMD\" ] ; then\n  if [ -n \"$JAVA_HOME\"  ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n      # IBM's JDK on AIX uses strange locations for the executables\n      JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n      JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n  else\n    JAVACMD=\"`which java`\"\n  fi\nfi\n\nif [ ! -x \"$JAVACMD\" ] ; then\n  echo \"Error: JAVA_HOME is not defined correctly.\" >&2\n  echo \"  We cannot execute $JAVACMD\" >&2\n  exit 1\nfi\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  echo \"Warning: JAVA_HOME environment variable is not set.\"\nfi\n\nCLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher\n\n# traverses directory structure from process work directory to filesystem root\n# first directory with .mvn subdirectory is considered project base directory\nfind_maven_basedir() {\n\n  if [ -z \"$1\" ]\n  then\n    echo \"Path not specified to find_maven_basedir\"\n    return 1\n  fi\n\n  basedir=\"$1\"\n  wdir=\"$1\"\n  while [ \"$wdir\" != '/' ] ; do\n    if [ -d \"$wdir\"/.mvn ] ; then\n      basedir=$wdir\n      break\n    fi\n    # workaround for JBEAP-8937 (on Solaris 10/Sparc)\n    if [ -d \"${wdir}\" ]; then\n      wdir=`cd \"$wdir/..\"; pwd`\n    fi\n    # end of workaround\n  done\n  echo \"${basedir}\"\n}\n\n# concatenates all lines of a file\nconcat_lines() {\n  if [ -f \"$1\" ]; then\n    echo \"$(tr -s '\\n' ' ' < \"$1\")\"\n  fi\n}\n\nBASE_DIR=`find_maven_basedir \"$(pwd)\"`\nif [ -z \"$BASE_DIR\" ]; then\n  exit 1;\nfi\n\n##########################################################################################\n# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n# This allows using the maven wrapper in projects that prohibit checking in binary data.\n##########################################################################################\nif [ -r \"$BASE_DIR/.mvn/wrapper/maven-wrapper.jar\" ]; then\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Found .mvn/wrapper/maven-wrapper.jar\"\n    fi\nelse\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ...\"\n    fi\n    jarUrl=\"https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar\"\n    while IFS=\"=\" read key value; do\n      case \"$key\" in (wrapperUrl) jarUrl=\"$value\"; break ;;\n      esac\n    done < \"$BASE_DIR/.mvn/wrapper/maven-wrapper.properties\"\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Downloading from: $jarUrl\"\n    fi\n    wrapperJarPath=\"$BASE_DIR/.mvn/wrapper/maven-wrapper.jar\"\n\n    if command -v wget > /dev/null; then\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Found wget ... using wget\"\n        fi\n        wget \"$jarUrl\" -O \"$wrapperJarPath\"\n    elif command -v curl > /dev/null; then\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Found curl ... using curl\"\n        fi\n        curl -o \"$wrapperJarPath\" \"$jarUrl\"\n    else\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Falling back to using Java to download\"\n        fi\n        javaClass=\"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java\"\n        if [ -e \"$javaClass\" ]; then\n            if [ ! -e \"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class\" ]; then\n                if [ \"$MVNW_VERBOSE\" = true ]; then\n                  echo \" - Compiling MavenWrapperDownloader.java ...\"\n                fi\n                # Compiling the Java class\n                (\"$JAVA_HOME/bin/javac\" \"$javaClass\")\n            fi\n            if [ -e \"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class\" ]; then\n                # Running the downloader\n                if [ \"$MVNW_VERBOSE\" = true ]; then\n                  echo \" - Running MavenWrapperDownloader.java ...\"\n                fi\n                (\"$JAVA_HOME/bin/java\" -cp .mvn/wrapper MavenWrapperDownloader \"$MAVEN_PROJECTBASEDIR\")\n            fi\n        fi\n    fi\nfi\n##########################################################################################\n# End of extension\n##########################################################################################\n\nexport MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-\"$BASE_DIR\"}\nif [ \"$MVNW_VERBOSE\" = true ]; then\n  echo $MAVEN_PROJECTBASEDIR\nfi\nMAVEN_OPTS=\"$(concat_lines \"$MAVEN_PROJECTBASEDIR/.mvn/jvm.config\") $MAVEN_OPTS\"\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=`cygpath --path --windows \"$M2_HOME\"`\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=`cygpath --path --windows \"$JAVA_HOME\"`\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=`cygpath --path --windows \"$CLASSPATH\"`\n  [ -n \"$MAVEN_PROJECTBASEDIR\" ] &&\n    MAVEN_PROJECTBASEDIR=`cygpath --path --windows \"$MAVEN_PROJECTBASEDIR\"`\nfi\n\nWRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nexec \"$JAVACMD\" \\\n  $MAVEN_OPTS \\\n  -classpath \"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar\" \\\n  \"-Dmaven.home=${M2_HOME}\" \"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}\" \\\n  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG \"$@\"\n"
  },
  {
    "path": "mvnw.cmd",
    "content": "@REM ----------------------------------------------------------------------------\n@REM Licensed to the Apache Software Foundation (ASF) under one\n@REM or more contributor license agreements.  See the NOTICE file\n@REM distributed with this work for additional information\n@REM regarding copyright ownership.  The ASF licenses this file\n@REM to you under the Apache License, Version 2.0 (the\n@REM \"License\"); you may not use this file except in compliance\n@REM with the License.  You may obtain a copy of the License at\n@REM\n@REM    https://www.apache.org/licenses/LICENSE-2.0\n@REM\n@REM Unless required by applicable law or agreed to in writing,\n@REM software distributed under the License is distributed on an\n@REM \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n@REM KIND, either express or implied.  See the License for the\n@REM specific language governing permissions and limitations\n@REM under the License.\n@REM ----------------------------------------------------------------------------\n\n@REM ----------------------------------------------------------------------------\n@REM Maven2 Start Up Batch script\n@REM\n@REM Required ENV vars:\n@REM JAVA_HOME - location of a JDK home dir\n@REM\n@REM Optional ENV vars\n@REM M2_HOME - location of maven2's installed home dir\n@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands\n@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending\n@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven\n@REM     e.g. to debug Maven itself, use\n@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n@REM ----------------------------------------------------------------------------\n\n@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'\n@echo off\n@REM set title of command window\ntitle %0\n@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'\n@if \"%MAVEN_BATCH_ECHO%\" == \"on\"  echo %MAVEN_BATCH_ECHO%\n\n@REM set %HOME% to equivalent of $HOME\nif \"%HOME%\" == \"\" (set \"HOME=%HOMEDRIVE%%HOMEPATH%\")\n\n@REM Execute a user defined script before this one\nif not \"%MAVEN_SKIP_RC%\" == \"\" goto skipRcPre\n@REM check for pre script, once with legacy .bat ending and once with .cmd ending\nif exist \"%HOME%\\mavenrc_pre.bat\" call \"%HOME%\\mavenrc_pre.bat\"\nif exist \"%HOME%\\mavenrc_pre.cmd\" call \"%HOME%\\mavenrc_pre.cmd\"\n:skipRcPre\n\n@setlocal\n\nset ERROR_CODE=0\n\n@REM To isolate internal variables from possible post scripts, we use another setlocal\n@setlocal\n\n@REM ==== START VALIDATION ====\nif not \"%JAVA_HOME%\" == \"\" goto OkJHome\n\necho.\necho Error: JAVA_HOME not found in your environment. >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n:OkJHome\nif exist \"%JAVA_HOME%\\bin\\java.exe\" goto init\n\necho.\necho Error: JAVA_HOME is set to an invalid directory. >&2\necho JAVA_HOME = \"%JAVA_HOME%\" >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n@REM ==== END VALIDATION ====\n\n:init\n\n@REM Find the project base dir, i.e. the directory that contains the folder \".mvn\".\n@REM Fallback to current working directory if not found.\n\nset MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%\nIF NOT \"%MAVEN_PROJECTBASEDIR%\"==\"\" goto endDetectBaseDir\n\nset EXEC_DIR=%CD%\nset WDIR=%EXEC_DIR%\n:findBaseDir\nIF EXIST \"%WDIR%\"\\.mvn goto baseDirFound\ncd ..\nIF \"%WDIR%\"==\"%CD%\" goto baseDirNotFound\nset WDIR=%CD%\ngoto findBaseDir\n\n:baseDirFound\nset MAVEN_PROJECTBASEDIR=%WDIR%\ncd \"%EXEC_DIR%\"\ngoto endDetectBaseDir\n\n:baseDirNotFound\nset MAVEN_PROJECTBASEDIR=%EXEC_DIR%\ncd \"%EXEC_DIR%\"\n\n:endDetectBaseDir\n\nIF NOT EXIST \"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\" goto endReadAdditionalConfig\n\n@setlocal EnableExtensions EnableDelayedExpansion\nfor /F \"usebackq delims=\" %%a in (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a\n@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%\n\n:endReadAdditionalConfig\n\nSET MAVEN_JAVA_EXE=\"%JAVA_HOME%\\bin\\java.exe\"\nset WRAPPER_JAR=\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.jar\"\nset WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nset DOWNLOAD_URL=\"https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar\"\nFOR /F \"tokens=1,2 delims==\" %%A IN (%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.properties) DO (\n\tIF \"%%A\"==\"wrapperUrl\" SET DOWNLOAD_URL=%%B \n)\n\n@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n@REM This allows using the maven wrapper in projects that prohibit checking in binary data.\nif exist %WRAPPER_JAR% (\n    echo Found %WRAPPER_JAR%\n) else (\n    echo Couldn't find %WRAPPER_JAR%, downloading it ...\n\techo Downloading from: %DOWNLOAD_URL%\n    powershell -Command \"(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')\"\n    echo Finished downloading %WRAPPER_JAR%\n)\n@REM End of extension\n\n%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% \"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%\" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*\nif ERRORLEVEL 1 goto error\ngoto end\n\n:error\nset ERROR_CODE=1\n\n:end\n@endlocal & set ERROR_CODE=%ERROR_CODE%\n\nif not \"%MAVEN_SKIP_RC%\" == \"\" goto skipRcPost\n@REM check for post script, once with legacy .bat ending and once with .cmd ending\nif exist \"%HOME%\\mavenrc_post.bat\" call \"%HOME%\\mavenrc_post.bat\"\nif exist \"%HOME%\\mavenrc_post.cmd\" call \"%HOME%\\mavenrc_post.cmd\"\n:skipRcPost\n\n@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'\nif \"%MAVEN_BATCH_PAUSE%\" == \"on\" pause\n\nif \"%MAVEN_TERMINATE_CMD%\" == \"on\" exit %ERROR_CODE%\n\nexit /B %ERROR_CODE%\n"
  },
  {
    "path": "pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project 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\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>2.1.7.RELEASE</version>\n        <relativePath/> <!-- lookup parent from repository -->\n    </parent>\n    <groupId>org.inlighting</groupId>\n    <artifactId>spring-boot-security-jwt</artifactId>\n    <version>0.0.1-SNAPSHOT</version>\n    <name>spring-boot-security-jwt</name>\n    <description>Demo project for Spring Boot</description>\n\n    <properties>\n        <java.version>1.8</java.version>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-security</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n        <!-- JWT 支持 -->\n        <dependency>\n            <groupId>com.auth0</groupId>\n            <artifactId>java-jwt</artifactId>\n            <version>3.8.2</version>\n        </dependency>\n\n        <!-- cache 支持 -->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-cache</artifactId>\n        </dependency>\n\n        <!-- cache 支持 -->\n        <dependency>\n            <groupId>org.ehcache</groupId>\n            <artifactId>ehcache</artifactId>\n        </dependency>\n\n        <!-- cache 支持 -->\n        <dependency>\n            <groupId>javax.cache</groupId>\n            <artifactId>cache-api</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.security</groupId>\n            <artifactId>spring-security-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <!-- ehcache 读取 xml 配置文件使用 -->\n        <dependency>\n            <groupId>javax.xml.bind</groupId>\n            <artifactId>jaxb-api</artifactId>\n            <version>2.3.0</version>\n        </dependency>\n\n        <!-- ehcache 读取 xml 配置文件使用 -->\n        <dependency>\n            <groupId>com.sun.xml.bind</groupId>\n            <artifactId>jaxb-impl</artifactId>\n            <version>2.3.0</version>\n        </dependency>\n\n        <!-- ehcache 读取 xml 配置文件使用 -->\n        <dependency>\n            <groupId>com.sun.xml.bind</groupId>\n            <artifactId>jaxb-core</artifactId>\n            <version>2.3.0</version>\n        </dependency>\n\n        <!-- ehcache 读取 xml 配置文件使用 -->\n        <dependency>\n            <groupId>javax.activation</groupId>\n            <artifactId>activation</artifactId>\n            <version>1.1.1</version>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>\n"
  },
  {
    "path": "src/main/java/org/inlighting/security/Main.java",
    "content": "package org.inlighting.security;\n\nimport org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;\n\npublic class Main {\n\n\n    public static void main(String[] args) {\n\n        try {\n            BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();\n            System.out.println(encoder.encode(\"smith123\"));\n        } catch (Exception e) {\n            System.out.println(e.getMessage());\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/inlighting/security/WebApplication.java",
    "content": "package org.inlighting.security;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.cache.annotation.EnableCaching;\n\n@SpringBootApplication\n@EnableCaching\npublic class WebApplication {\n\n    public static void main(String[] args) {\n        SpringApplication.run(WebApplication.class, args);\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/inlighting/security/controller/CustomErrorController.java",
    "content": "package org.inlighting.security.controller;\n\nimport org.inlighting.security.entity.ResponseEntity;\nimport org.springframework.boot.web.servlet.error.ErrorController;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\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"
  },
  {
    "path": "src/main/java/org/inlighting/security/controller/MainController.java",
    "content": "package org.inlighting.security.controller;\n\nimport org.inlighting.security.entity.ResponseEntity;\nimport org.inlighting.security.security.IsAdmin;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.security.access.prepost.PreAuthorize;\nimport org.springframework.security.authentication.AnonymousAuthenticationToken;\nimport org.springframework.security.authentication.UsernamePasswordAuthenticationToken;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.annotation.AuthenticationPrincipal;\nimport org.springframework.security.core.context.SecurityContextHolder;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\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"
  },
  {
    "path": "src/main/java/org/inlighting/security/entity/ResponseEntity.java",
    "content": "package org.inlighting.security.entity;\n\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}"
  },
  {
    "path": "src/main/java/org/inlighting/security/entity/UserEntity.java",
    "content": "package org.inlighting.security.entity;\n\nimport org.springframework.security.core.GrantedAuthority;\n\nimport java.util.Collection;\n\npublic class UserEntity {\n\n    public UserEntity(String username, String password, Collection<? extends GrantedAuthority> 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<? extends GrantedAuthority> 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<? extends GrantedAuthority> getRole() {\n        return role;\n    }\n\n    public void setRole(Collection<? extends GrantedAuthority> role) {\n        this.role = role;\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/inlighting/security/security/IsAdmin.java",
    "content": "package org.inlighting.security.security;\n\nimport org.springframework.security.access.prepost.PreAuthorize;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Target({ElementType.METHOD, ElementType.TYPE})\n@Retention(RetentionPolicy.RUNTIME)\n@PreAuthorize(\"hasAnyRole('ROLE_ADMIN')\")\npublic @interface IsAdmin {\n}\n"
  },
  {
    "path": "src/main/java/org/inlighting/security/security/IsUser.java",
    "content": "package org.inlighting.security.security;\n\nimport org.springframework.security.access.prepost.PreAuthorize;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Target({ElementType.METHOD, ElementType.TYPE})\n@Retention(RetentionPolicy.RUNTIME)\n@PreAuthorize(\"hasAnyRole('ROLE_USER')\")\npublic @interface IsUser {\n}\n"
  },
  {
    "path": "src/main/java/org/inlighting/security/security/JwtAuthenticationFilter.java",
    "content": "package org.inlighting.security.security;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.inlighting.security.entity.ResponseEntity;\nimport org.inlighting.security.util.JwtUtil;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.security.authentication.AuthenticationManager;\nimport org.springframework.security.authentication.UsernamePasswordAuthenticationToken;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.AuthenticationException;\nimport org.springframework.security.core.userdetails.User;\nimport org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;\n\nimport javax.servlet.FilterChain;\nimport javax.servlet.ServletException;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\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"
  },
  {
    "path": "src/main/java/org/inlighting/security/security/JwtAuthorizationFilter.java",
    "content": "package org.inlighting.security.security;\n\nimport org.inlighting.security.util.JwtUtil;\nimport org.springframework.security.authentication.AuthenticationManager;\nimport org.springframework.security.authentication.UsernamePasswordAuthenticationToken;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.SecurityContextHolder;\nimport org.springframework.security.core.userdetails.UserDetails;\nimport org.springframework.security.core.userdetails.UserDetailsService;\nimport org.springframework.security.core.userdetails.UsernameNotFoundException;\nimport org.springframework.security.web.authentication.www.BasicAuthenticationFilter;\n\nimport javax.servlet.FilterChain;\nimport javax.servlet.ServletException;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\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"
  },
  {
    "path": "src/main/java/org/inlighting/security/security/SecurityConfiguration.java",
    "content": "package org.inlighting.security.security;\n\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.cache.CacheManager;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;\nimport org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;\nimport org.springframework.security.config.annotation.web.builders.HttpSecurity;\nimport org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;\nimport org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;\nimport org.springframework.security.config.authentication.CachingUserDetailsService;\nimport org.springframework.security.config.http.SessionCreationPolicy;\nimport org.springframework.security.core.userdetails.UserCache;\nimport org.springframework.security.core.userdetails.UserDetailsService;\nimport org.springframework.security.core.userdetails.cache.SpringCacheBasedUserCache;\nimport org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;\nimport org.springframework.security.crypto.password.PasswordEncoder;\nimport org.springframework.util.Assert;\n\nimport java.lang.reflect.Constructor;\n\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<CachingUserDetailsService> 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"
  },
  {
    "path": "src/main/java/org/inlighting/security/security/UserDetailsServiceImpl.java",
    "content": "package org.inlighting.security.security;\n\nimport org.inlighting.security.entity.UserEntity;\nimport org.inlighting.security.service.UserService;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.security.core.userdetails.User;\nimport org.springframework.security.core.userdetails.UserDetails;\nimport org.springframework.security.core.userdetails.UserDetailsService;\nimport org.springframework.security.core.userdetails.UsernameNotFoundException;\nimport org.springframework.stereotype.Service;\n\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"
  },
  {
    "path": "src/main/java/org/inlighting/security/service/Database.java",
    "content": "package org.inlighting.security.service;\n\nimport org.inlighting.security.entity.UserEntity;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.core.authority.AuthorityUtils;\nimport org.springframework.stereotype.Component;\n\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.Map;\n\n@Component\npublic class Database {\n    private Map<String, UserEntity> data = null;\n\n    public Map<String, UserEntity> getDatabase() {\n        if (data == null) {\n            data = new HashMap<>();\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<GrantedAuthority> getGrants(String role) {\n        return AuthorityUtils.commaSeparatedStringToAuthorityList(role);\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/inlighting/security/service/UserService.java",
    "content": "package org.inlighting.security.service;\n\nimport org.inlighting.security.entity.UserEntity;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\n\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"
  },
  {
    "path": "src/main/java/org/inlighting/security/util/JwtUtil.java",
    "content": "package org.inlighting.security.util;\n\nimport com.auth0.jwt.JWT;\nimport com.auth0.jwt.JWTVerifier;\nimport com.auth0.jwt.algorithms.Algorithm;\nimport com.auth0.jwt.exceptions.JWTDecodeException;\nimport com.auth0.jwt.interfaces.DecodedJWT;\n\nimport java.util.Date;\n\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"
  },
  {
    "path": "src/main/resources/application.properties",
    "content": "spring.cache.type=jcache\nspring.cache.jcache.config=classpath:ehcache.xml"
  },
  {
    "path": "src/main/resources/ehcache.xml",
    "content": "<ehcache:config\n        xmlns:ehcache=\"http://www.ehcache.org/v3\"\n        xmlns:jcache=\"http://www.ehcache.org/v3/jsr107\">\n\n    <ehcache:cache alias=\"jwt-cache\">\n        <!-- 我们使用用户名作为缓存的 key，故使用 String -->\n        <ehcache:key-type>java.lang.String</ehcache:key-type>\n        <ehcache:value-type>org.springframework.security.core.userdetails.User</ehcache:value-type>\n        <ehcache:expiry>\n            <ehcache:ttl unit=\"days\">1</ehcache:ttl>\n        </ehcache:expiry>\n        <!-- 缓存实体的数量 -->\n        <ehcache:heap unit=\"entries\">2000</ehcache:heap>\n    </ehcache:cache>\n\n</ehcache:config>\n"
  },
  {
    "path": "src/test/java/org/inlighting/security/SpringBootSecurityJwtApplicationTests.java",
    "content": "package org.inlighting.security;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.test.context.junit4.SpringRunner;\n\n@RunWith(SpringRunner.class)\n@SpringBootTest\npublic class SpringBootSecurityJwtApplicationTests {\n\n    @Test\n    public void contextLoads() {\n    }\n\n}\n"
  }
]