{"id":19019683,"url":"https://github.com/smith-cruise/spring-boot-security-thymeleaf-demo","last_synced_at":"2025-04-23T05:21:17.957Z","repository":{"id":90158474,"uuid":"166395373","full_name":"Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo","owner":"Smith-Cruise","description":"Spring Boot 2.0+Srping Security+Thymeleaf的简易教程","archived":false,"fork":false,"pushed_at":"2020-02-29T05:54:05.000Z","size":611,"stargazers_count":60,"open_issues_count":0,"forks_count":19,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-04-17T20:39:25.359Z","etag":null,"topics":["springboot2","springsecurity","thymeleaf"],"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-01-18T11:34:24.000Z","updated_at":"2025-03-15T01:54:19.000Z","dependencies_parsed_at":null,"dependency_job_id":"5e248c3c-4cfb-4c69-80c2-48fbd7dc9d44","html_url":"https://github.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Smith-Cruise%2FSpring-Boot-Security-Thymeleaf-Demo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Smith-Cruise%2FSpring-Boot-Security-Thymeleaf-Demo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Smith-Cruise%2FSpring-Boot-Security-Thymeleaf-Demo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Smith-Cruise%2FSpring-Boot-Security-Thymeleaf-Demo/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-Thymeleaf-Demo/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250374340,"owners_count":21419950,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["springboot2","springsecurity","thymeleaf"],"created_at":"2024-11-08T20:13:53.557Z","updated_at":"2025-04-23T05:21:17.943Z","avatar_url":"https://github.com/Smith-Cruise.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Spring Boot + Spring Security + Thymeleaf 简单教程\n因为有一个项目需采用MVC构架，所以学习了Spring Security并记录下来，希望大家一起学习提供意见\n\nGitHub地址：[https://github.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo]( https://github.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo)。\n\n**如果有疑问，请在 GitHub 中发布 issue，我有空会为大家解答的**\n\n本项目基于Spring Boot 2 + Spring Security 5 + Thymeleaf 2 + JDK11（你也可以用8，应该区别不大）\n\n实现了以下功能：\n\n* 基于注解的权限控制\n* 在Thymeleaf中使用Spring Security的标签\n* 自定义权限注解\n* 记住密码功能\n\n如果需要前后端分离的安全框架搭建教程可以参考：[Spring Boot 2 + Spring Security 5 + JWT 的单页应用Restful解决方案]( https://github.com/Smith-Cruise/Spring-Boot-Security-JWT-SPA)\n\n## 项目演示\n\n如果想要直接体验，直接 `clone` 项目，运行 `mvn spring-boot:run` 命令即可进行访问，网址规则自行看教程后面\n\n***首页***\n\n![首页](https://github.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo/blob/master/file/index.png?raw=true)\n\n***登入***\n\n![登入](https://github.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo/blob/master/file/login.png?raw=true)\n\n***登出***\n\n![登出](https://github.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo/blob/master/file/logout.png?raw=true)\n\n***Home页面***\n\n![Home](https://github.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo/blob/master/file/home.png?raw=true)\n\n***Admin页面***\n\n![Admin](https://github.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo/blob/master/file/admin.png?raw=true)\n\n***403无权限页面***\n\n![403](https://github.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo/blob/master/file/403.png?raw=true)\n\n## Spring Security 基本原理\n\n***Spring Security 过滤器链***\n\nSpring Security实现了一系列的过滤器链，就按照下面顺序一个一个执行下去。\n\n1. `....class` 一些自定义过滤器（在配置的时候你可以自己选择插到哪个过滤器之前），因为这个需求因人而异，本文不探讨，大家可以自己研究\n2. `UsernamePasswordAithenticationFilter.class` Spring Security 自带的表单登入验证过滤器，也是本文主要使用的过滤器\n3. `BasicAuthenticationFilter.class`\n4. `ExceptionTranslation.class` 异常解释器\n5. `FilterSecurityInterceptor.class` 拦截器最终决定请求能否通过\n6. `Controller` 我们最后自己编写的控制器\n\n***相关类说明***\n\n* `User.class` ：注意这个类不是我们自己写的，而是Spring Security官方提供的，他提供了一些基础的功能，我们可以通过继承这个类来扩充方法。详见代码中的 `CustomUser.java`\n* `UserDetailsService.class`： Spring Security官方提供的一个接口，里面只有一个方法`loadUserByUsername()` ，Spring Security会调用这个方法来获取数据库中存在的数据，然后和用户POST过来的用户名密码进行比对，从而判断用户的用户名密码是否正确。所以我们需要自己实现`loadUserByUsername()` 这个方法。详见代码中的 `CustomUserDetailsService.java`。\n\n## 项目逻辑\n\n为了体现权限区别，我们通过HashMap构造了一个数据库，里面包含了4个用户\n\n| ID   | 用户名 | 密码     | 权限     |\n| ---- | ------ | -------- | -------- |\n| 1    | jack   | jack123  | user     |\n| 2    | danny  | danny123 | editor   |\n| 3    | alice  | alice123 | reviewer |\n| 4    | smith  | smith123 | admin    |\n\n说明下权限\n\n`user`：最基础的权限，只要是登入用户就有 `user` 权限\n\n`editor`：在 `user` 权限上面增加了 `editor` 的权限\n\n`reviewer`：与上同理，`editor` 和  `reviewer` 属于同一级的权限\n\n`admin`：包含所有权限\n\n为了检验权限，我们提供若干个页面\n\n| 网址           | 说明                                | 可访问权限                  |\n| -------------- | ----------------------------------- | --------------------------- |\n| /              | 首页                                | 所有人均可访问（anonymous） |\n| /login         | 登入页面                            | 所有人均可访问（anonymous） |\n| /logout        | 退出页面                            | 所有人均可访问（anonymous） |\n| /user/home     | 用户中心                            | user                        |\n| /user/editor   |                                     | editor, admin               |\n| /user/reviewer |                                     | reviewer, admin             |\n| /user/admin    |                                     | admin                       |\n| /403           | 403错误页面，美化过，大家可以直接用 | 所有人均可访问（anonymous） |\n| /404           | 404错误页面，美化过，大家可以直接用 | 所有人均可访问（anonymous） |\n| /500           | 500错误页面，美化过，大家可以直接用 | 所有人均可访问（anonymous） |\n\n## 代码配置\n\n***Maven 配置***\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.1.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\u003esecurity-demo\u003c/artifactId\u003e\n    \u003cversion\u003e0.0.1-SNAPSHOT\u003c/version\u003e\n    \u003cname\u003esecurity-demo\u003c/name\u003e\n    \u003cdescription\u003eDemo project for Spring Boot \u0026amp; Spring Security\u003c/description\u003e\n\n    \u003c!--指定JDK版本，大家可以改成自己的--\u003e\n    \u003cproperties\u003e\n        \u003cjava.version\u003e11\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        \u003cdependency\u003e\n            \u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n            \u003cartifactId\u003espring-boot-starter-thymeleaf\u003c/artifactId\u003e\n        \u003c/dependency\u003e\n        \u003cdependency\u003e\n            \u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n            \u003cartifactId\u003espring-boot-starter-web\u003c/artifactId\u003e\n        \u003c/dependency\u003e\n        \u003c!--对Thymeleaf添加Spring Security标签支持--\u003e\n        \u003cdependency\u003e\n            \u003cgroupId\u003eorg.thymeleaf.extras\u003c/groupId\u003e\n            \u003cartifactId\u003ethymeleaf-extras-springsecurity5\u003c/artifactId\u003e\n        \u003c/dependency\u003e\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        \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        \u003c!--开发的热加载配置--\u003e\n        \u003cdependency\u003e\n            \u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n            \u003cartifactId\u003espring-boot-devtools\u003c/artifactId\u003e\n            \u003coptional\u003etrue\u003c/optional\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\u003c/project\u003e\n```\n\n***application.properties配置***\n\n为了使热加载（这样修改模板后无需重启 Tomcat ）生效，我们需要在Spring Boot的配置文件上面加上一段话\n\n```properties\nspring.thymeleaf.cache=false\n```\n\n如果需要详细了解热加载，请看官方文档：[https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-hotswapping](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-hotswapping)\n\n## Spring Security 配置\n\n首先我们开启方法注解支持：只需要在类上添加 `@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)` 注解，我们设置 `prePostEnabled = true` 是为了支持 `hasRole()` 这类表达式。如果想进一步了解方法注解可以看 [Introduction to Spring Method Security](https://www.baeldung.com/spring-security-method-security) 这篇文章。\n\n***SecurityConfig.java***\n\n```java\n/**\n * 开启方法注解支持，我们设置prePostEnabled = true是为了后面能够使用hasRole()这类表达式\n * 进一步了解可看教程：https://www.baeldung.com/spring-security-method-security\n */\n@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)\n@Configuration\npublic class SecurityConfig extends WebSecurityConfigurerAdapter {\n\n    /**\n     * TokenBasedRememberMeServices的生成密钥，\n     * 算法实现详见文档：https://docs.spring.io/spring-security/site/docs/5.1.3.RELEASE/reference/htmlsingle/#remember-me-hash-token\n     */\n    private final String SECRET_KEY = \"123456\";\n\n    @Autowired\n    private CustomUserDetailsService customUserDetailsService;\n\n    /**\n     * 必须有此方法，Spring Security官方规定必须要有一个密码加密方式。\n     * 注意：例如这里用了BCryptPasswordEncoder()的加密方法，那么在保存用户密码的时候也必须使用这种方法，确保前后一致。\n     * 详情参见项目中Database.java中保存用户的逻辑\n     */\n    @Bean\n    public PasswordEncoder passwordEncoder() {\n        return new BCryptPasswordEncoder();\n    }\n\n    /**\n     * 配置Spring Security，下面说明几点注意事项。\n     * 1. Spring Security 默认是开启了CSRF的，此时我们提交的POST表单必须有隐藏的字段来传递CSRF，\n     * 而且在logout中，我们必须通过POST到 /logout 的方法来退出用户，详见我们的login.html和logout.html.\n     * 2. 开启了rememberMe()功能后，我们必须提供rememberMeServices，例如下面的getRememberMeServices()方法，\n     * 而且我们只能在TokenBasedRememberMeServices中设置cookie名称、过期时间等相关配置,如果在别的地方同时配置，会报错。\n     * 错误示例：xxxx.and().rememberMe().rememberMeServices(getRememberMeServices()).rememberMeCookieName(\"cookie-name\")\n     */\n    @Override\n    protected void configure(HttpSecurity http) throws Exception {\n        http.formLogin()\n                .loginPage(\"/login\") // 自定义用户登入页面\n                .failureUrl(\"/login?error\") // 自定义登入失败页面，前端可以通过url中是否有error来提供友好的用户登入提示\n                .and()\n                .logout()\n                .logoutUrl(\"/logout\")// 自定义用户登出页面\n                .logoutSuccessUrl(\"/\")\n                .and()\n                .rememberMe() // 开启记住密码功能\n                .rememberMeServices(getRememberMeServices()) // 必须提供\n                .key(SECRET_KEY) // 此SECRET需要和生成TokenBasedRememberMeServices的密钥相同\n                .and()\n                /*\n                 * 默认允许所有路径所有人都可以访问，确保静态资源的正常访问。\n                 * 后面再通过方法注解的方式来控制权限。\n                 */\n                .authorizeRequests().anyRequest().permitAll()\n                .and()\n                .exceptionHandling().accessDeniedPage(\"/403\"); // 权限不足自动跳转403\n    }\n\n    /**\n     * 如果要设置cookie过期时间或其他相关配置，请在下方自行配置\n     */\n    private TokenBasedRememberMeServices getRememberMeServices() {\n        TokenBasedRememberMeServices services = new TokenBasedRememberMeServices(SECRET_KEY, customUserDetailsService);\n        services.setCookieName(\"remember-cookie\");\n        services.setTokenValiditySeconds(100); // 默认14天\n        return services;\n    }\n}\n```\n\n***UserService.java***\n\n自己模拟数据库操作的`Service`，用于向自己通过`HashMap`模拟的数据源获取数据。\n\n```java\n@Service\npublic class UserService {\n\n    private Database database = new Database();\n\n    public CustomUser getUserByUsername(String username) {\n        CustomUser originUser = database.getDatabase().get(username);\n        if (originUser == null) {\n            return null;\n        }\n\n        /*\n         * 此处有坑，之所以这么做是因为Spring Security获得到User后，会把User中的password字段置空，以确保安全。\n         * 因为Java类是引用传递，为防止Spring Security修改了我们的源头数据，所以我们复制一个对象提供给Spring Security。\n         * 如果通过真实数据库的方式获取，则没有这种问题需要担心。\n          */\n        return new CustomUser(originUser.getId(), originUser.getUsername(), originUser.getPassword(), originUser.getAuthorities());\n    }\n}\n```\n\n***CustomUserDetailsService.java***\n\n```java\n/**\n * 实现官方提供的UserDetailsService接口即可\n */\n@Service\npublic class CustomUserDetailsService implements UserDetailsService {\n\n    private Logger LOGGER = LoggerFactory.getLogger(getClass());\n\n    @Autowired\n    private UserService userService;\n\n    @Override\n    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {\n        CustomUser user = userService.getUserByUsername(username);\n        if (user == null) {\n            throw new  UsernameNotFoundException(\"该用户不存在\");\n        }\n        LOGGER.info(\"用户名：\"+username+\" 角色：\"+user.getAuthorities().toString());\n        return user;\n    }\n}\n```\n\n\n\n## 自定义权限注解\n\n我们在开发网站的过程中，比如 `GET /user/editor ` 这个请求角色为 `EDITOR` 和 `ADMIN` 肯定都可以，如果我们在每一个需要判断权限的方法上面写一长串的权限表达式，一定很复杂。但是通过自定义权限注解，我们可以通过 `@IsEditor` 这样的方法来判断，这样一来就简单了很多。进一步了解可以看：[Introduction to Spring Method Security](https://www.baeldung.com/spring-security-method-security)\n\n***IsUser.java***\n\n```java\n@Target({ElementType.METHOD, ElementType.TYPE})\n@Retention(RetentionPolicy.RUNTIME)\n@PreAuthorize(\"hasAnyAuthority('ROLE_USER', 'ROLE_EDITOR', 'ROLE_REVIEWER', 'ROLE_ADMIN')\")\npublic @interface IsUser {\n}\n```\n\n***IsEditor.java***\n\n```java\n@Target({ElementType.METHOD, ElementType.TYPE})\n@Retention(RetentionPolicy.RUNTIME)\n@PreAuthorize(\"hasAnyRole('ROLE_USER', 'ROLE_EDITOR', 'ROLE_ADMIN')\")\npublic @interface IsEditor {\n}\n```\n\n***IsReviewer.java***\n\n```java\n@Target({ElementType.METHOD, ElementType.TYPE})\n@Retention(RetentionPolicy.RUNTIME)\n@PreAuthorize(\"hasAnyRole('ROLE_USER', 'ROLE_REVIEWER', 'ROLE_ADMIN')\")\npublic @interface IsReviewer {\n}\n```\n\n***IsAdmin.java***\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***Spring Security自带表达式***\n\n- `hasRole()`，是否拥有某一个权限\n\n-  `hasAnyRole()`，多个权限中有一个即可，如 `hasAnyRole(\"ADMIN\",\"USER\")`\n- `hasAuthority()`，`Authority` 和 `Role` 很像，唯一的区别就是 `Authority` 前缀多了 `ROLE_` ，如 `hasAuthority(\"ROLE_ADMIN\")` 等价于 `hasRole(\"ADMIN\")` ，可以参考上面   `IsUser.java` 的写法\n-  `hasAnyAuthority()`，同上，多个权限中有一个即可\n- `permitAll()`, `denyAll()`,`isAnonymous()`, `isRememberMe()`，通过字面意思可以理解\n- `isAuthenticated()`, `isFullyAuthenticated()`，这两个区别就是`isFullyAuthenticated()`对认证的安全要求更高。例如用户通过**记住密码功能**登入到系统进行敏感操作，`isFullyAuthenticated()`会返回`false`，此时我们可以让用户再输入一次密码以确保安全，而 `isAuthenticated()` 只要是登入用户均返回`true`。\n- `principal()`, `authentication()`，例如我们想获取登入用户的id，可以通过`principal()` 返回的 `Object` 获取，实际上 `principal()` 返回的 `Object` 基本上可以等同我们自己编写的 `CustomUser` 。而 `authentication()` 返回的 `Authentication` 是 `Principal` 的父类，相关操作可看 `Authentication` 的源码。进一步了解可以看后面**Controller编写中获取用户数据的四种方法**\n- `hasPermission()`，参考字面意思即可\n\n如果想进一步了解，可以参考 [Intro to Spring Security Expressions](https://www.baeldung.com/spring-security-expressions)。\n\n## 添加Thymeleaf支持\n\n我们通过 `thymeleaf-extras-springsecurity` 来添加Thymeleaf对Spring Security的支持。\n\n***Maven配置***\n\n上面的Maven配置已经加过了\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003eorg.thymeleaf.extras\u003c/groupId\u003e\n    \u003cartifactId\u003ethymeleaf-extras-springsecurity5\u003c/artifactId\u003e\n\u003c/dependency\u003e\n```\n\n***使用例子***\n\n注意我们要在html中添加 `xmlns:sec` 的支持\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml xmlns:th=\"http://www.thymeleaf.org\" xmlns:sec=\"http://www.thymeleaf.org/extras/spring-security\"\u003e\n\u003chead\u003e\n    \u003cmeta charset=\"UTF-8\"\u003e\n    \u003ctitle\u003eAdmin\u003c/title\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cp\u003eThis is a home page.\u003c/p\u003e\n\u003cp\u003eId: \u003cth:block sec:authentication=\"principal.id\"\u003e\u003c/th:block\u003e\u003c/p\u003e\n\u003cp\u003eUsername: \u003cth:block sec:authentication=\"principal.username\"\u003e\u003c/th:block\u003e\u003c/p\u003e\n\u003cp\u003eRole: \u003cth:block sec:authentication=\"principal.authorities\"\u003e\u003c/th:block\u003e\u003c/p\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n如果想进一步了解请看文档 [thymeleaf-extras-springsecurity](https://github.com/thymeleaf/thymeleaf-extras-springsecurity)。\n\n## Controller编写\n\n***IndexController.java***\n\n本控制器没有任何的权限规定\n\n```java\n@Controller\npublic class IndexController {\n\n    @GetMapping(\"/\")\n    public String index() {\n        return \"index/index\";\n    }\n\n    @GetMapping(\"/login\")\n    public String login() {\n        return \"index/login\";\n    }\n\n    @GetMapping(\"/logout\")\n    public String logout() {\n        return \"index/logout\";\n    }\n}\n```\n\n***UserController.java***\n\n在这个控制器中，我综合展示了自定义注解的使用和4种获取用户信息的方式\n\n```java\n@IsUser // 表明该控制器下所有请求都需要登入后才能访问\n@Controller\n@RequestMapping(\"/user\")\npublic class UserController {\n\n    @GetMapping(\"/home\")\n    public String home(Model model) {\n        // 方法一：通过SecurityContextHolder获取\n        CustomUser user = (CustomUser)SecurityContextHolder.getContext().getAuthentication().getPrincipal();\n        model.addAttribute(\"user\", user);\n        return \"user/home\";\n    }\n\n    @GetMapping(\"/editor\")\n    @IsEditor\n    public String editor(Authentication authentication, Model model) {\n        // 方法二：通过方法注入的形式获取Authentication\n        CustomUser user = (CustomUser)authentication.getPrincipal();\n        model.addAttribute(\"user\", user);\n        return \"user/editor\";\n    }\n\n    @GetMapping(\"/reviewer\")\n    @IsReviewer\n    public String reviewer(Principal principal, Model model) {\n        // 方法三：同样通过方法注入的方法，注意要转型，此方法很二，不推荐\n        CustomUser user = (CustomUser) ((Authentication)principal).getPrincipal();\n        model.addAttribute(\"user\", user);\n        return \"user/reviewer\";\n    }\n\n    @GetMapping(\"/admin\")\n    @IsAdmin\n    public String admin() {\n        // 方法四：通过Thymeleaf的Security标签进行，详情见admin.html\n        return \"user/admin\";\n    }\n}\n```\n\n***注意***\n\n* 如果有安全控制的方法 A 被同一个类中别的方法调用，那么方法 A 的权限控制会被忽略，私有方法同样会受到影响\n* Spring 的 `SecurityContext` 是线程绑定的，如果我们在当前的线程中新建了别的线程，那么他们的 `SecurityContext` 是不共享的，进一步了解请看 [Spring Security Context Propagation with @Async](https://www.baeldung.com/spring-security-async-principal-propagation)\n\n## Html的编写\n\n在编写html的时候，基本上就是大同小异了，就是注意一点，**如果开启了CSRF，在编写表单POST请求的时候添加上隐藏字段，如 **`\u003cinput type=\"hidden\" th:name=\"${_csrf.parameterName}\" th:value=\"${_csrf.token}\"/\u003e` **，不过大家其实不用加也没事，因为Thymeleaf自动会加上去的😀。**\n\n## 总结\n\n教程粗糙，欢迎指正！\n\n如需深入了解，如果想系统的学习可以看看 [Security with Spring](https://www.baeldung.com/security-spring)。\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmith-cruise%2Fspring-boot-security-thymeleaf-demo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsmith-cruise%2Fspring-boot-security-thymeleaf-demo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmith-cruise%2Fspring-boot-security-thymeleaf-demo/lists"}