├── .gitignore ├── src ├── main │ ├── resources │ │ ├── application.properties │ │ └── templates │ │ │ ├── index │ │ │ ├── index.html │ │ │ ├── logout.html │ │ │ └── login.html │ │ │ ├── user │ │ │ ├── home.html │ │ │ ├── editor.html │ │ │ ├── reviewer.html │ │ │ └── admin.html │ │ │ ├── 403.html │ │ │ ├── 500.html │ │ │ └── 404.html │ └── java │ │ └── org │ │ └── inlighting │ │ └── security │ │ ├── SecurityDemoApplication.java │ │ ├── security │ │ ├── IsAdmin.java │ │ ├── IsEditor.java │ │ ├── IsReviewer.java │ │ ├── IsUser.java │ │ ├── CustomUserDetailsService.java │ │ └── SecurityConfig.java │ │ ├── controller │ │ ├── ErrorController.java │ │ ├── IndexController.java │ │ └── UserController.java │ │ └── service │ │ ├── CustomUser.java │ │ ├── UserService.java │ │ └── Database.java └── test │ └── java │ └── org │ └── inlighting │ └── security │ └── SecurityDemoApplicationTests.java ├── file ├── 403.png ├── admin.png ├── home.png ├── index.png ├── login.png └── logout.png ├── pom.xml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | target/ 3 | *.iml 4 | */.DS_Store -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.thymeleaf.cache=false -------------------------------------------------------------------------------- /file/403.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo/HEAD/file/403.png -------------------------------------------------------------------------------- /file/admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo/HEAD/file/admin.png -------------------------------------------------------------------------------- /file/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo/HEAD/file/home.png -------------------------------------------------------------------------------- /file/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo/HEAD/file/index.png -------------------------------------------------------------------------------- /file/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo/HEAD/file/login.png -------------------------------------------------------------------------------- /file/logout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo/HEAD/file/logout.png -------------------------------------------------------------------------------- /src/test/java/org/inlighting/security/SecurityDemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package org.inlighting.security; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class SecurityDemoApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/main/resources/templates/index/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Index 6 | 7 | 8 | Login
9 | Logout
10 | Home
11 | Editor
12 | Reviewer
13 | Admin
14 | 15 | -------------------------------------------------------------------------------- /src/main/java/org/inlighting/security/SecurityDemoApplication.java: -------------------------------------------------------------------------------- 1 | package org.inlighting.security; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.annotation.Bean; 6 | 7 | @SpringBootApplication 8 | public class SecurityDemoApplication { 9 | 10 | public static void main(String[] args) { 11 | SpringApplication.run(SecurityDemoApplication.class, args); 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/main/java/org/inlighting/security/security/IsAdmin.java: -------------------------------------------------------------------------------- 1 | package org.inlighting.security.security; 2 | 3 | import org.springframework.security.access.prepost.PreAuthorize; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | @Target({ElementType.METHOD, ElementType.TYPE}) 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @PreAuthorize("hasAnyRole('ROLE_ADMIN')") 13 | public @interface IsAdmin { 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/inlighting/security/security/IsEditor.java: -------------------------------------------------------------------------------- 1 | package org.inlighting.security.security; 2 | 3 | import org.springframework.security.access.prepost.PreAuthorize; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | @Target({ElementType.METHOD, ElementType.TYPE}) 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @PreAuthorize("hasAnyRole('ROLE_USER', 'ROLE_EDITOR', 'ROLE_ADMIN')") 13 | public @interface IsEditor { 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/inlighting/security/security/IsReviewer.java: -------------------------------------------------------------------------------- 1 | package org.inlighting.security.security; 2 | 3 | import org.springframework.security.access.prepost.PreAuthorize; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | @Target({ElementType.METHOD, ElementType.TYPE}) 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @PreAuthorize("hasAnyRole('ROLE_USER', 'ROLE_REVIEWER', 'ROLE_ADMIN')") 13 | public @interface IsReviewer { 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/inlighting/security/security/IsUser.java: -------------------------------------------------------------------------------- 1 | package org.inlighting.security.security; 2 | 3 | import org.springframework.security.access.prepost.PreAuthorize; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | @Target({ElementType.METHOD, ElementType.TYPE}) 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @PreAuthorize("hasAnyAuthority('ROLE_USER', 'ROLE_EDITOR', 'ROLE_REVIEWER', 'ROLE_ADMIN')") 13 | public @interface IsUser { 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/inlighting/security/controller/ErrorController.java: -------------------------------------------------------------------------------- 1 | package org.inlighting.security.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | @Controller 7 | public class ErrorController { 8 | 9 | @GetMapping("/404") 10 | public String handle404() { 11 | return "404"; 12 | } 13 | 14 | @GetMapping("/403") 15 | public String handle403() { 16 | return "403"; 17 | } 18 | 19 | @GetMapping("/500") 20 | public String handle500() { 21 | return "500"; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/inlighting/security/controller/IndexController.java: -------------------------------------------------------------------------------- 1 | package org.inlighting.security.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | @Controller 7 | public class IndexController { 8 | 9 | @GetMapping("/") 10 | public String index() { 11 | return "index/index"; 12 | } 13 | 14 | @GetMapping("/login") 15 | public String login() { 16 | return "index/login"; 17 | } 18 | 19 | @GetMapping("/logout") 20 | public String logout() { 21 | return "index/logout"; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/resources/templates/user/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Home 6 | 7 | 8 |

This is a home page.

9 |

Id:

10 |

Username:

11 |

Role:

12 | Logout
13 | Home
14 | Editor
15 | Reviewer
16 | Admin
17 | 18 | -------------------------------------------------------------------------------- /src/main/resources/templates/user/editor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Editor 6 | 7 | 8 |

This is a editor page.

9 |

Id:

10 |

Username:

11 |

Role:

12 | Logout
13 | Home
14 | Editor
15 | Reviewer
16 | Admin
17 | 18 | -------------------------------------------------------------------------------- /src/main/resources/templates/user/reviewer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Reviewer 6 | 7 | 8 |

This is a reviewer page.

9 |

Id:

10 |

Username:

11 |

Role:

12 | Logout
13 | Home
14 | Editor
15 | Reviewer
16 | Admin
17 | 18 | -------------------------------------------------------------------------------- /src/main/java/org/inlighting/security/service/CustomUser.java: -------------------------------------------------------------------------------- 1 | package org.inlighting.security.service; 2 | 3 | import org.springframework.security.core.GrantedAuthority; 4 | import org.springframework.security.core.userdetails.User; 5 | 6 | import java.util.Collection; 7 | 8 | public class CustomUser extends User { 9 | 10 | private int id; 11 | 12 | public CustomUser(int id, String username, String password, Collection authorities) { 13 | super(username, password, authorities); 14 | this.id = id; 15 | } 16 | 17 | public int getId() { 18 | return id; 19 | } 20 | 21 | public void setId(int id) { 22 | this.id = id; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/resources/templates/user/admin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Admin 6 | 7 | 8 |

This is a admin page.

9 |

Id:

10 |

Username:

11 |

Role:

12 | Logout
13 | Home
14 | Editor
15 | Reviewer
16 | Admin
17 | 18 | -------------------------------------------------------------------------------- /src/main/resources/templates/index/logout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 15 |
16 |
17 | 18 | -------------------------------------------------------------------------------- /src/main/java/org/inlighting/security/service/UserService.java: -------------------------------------------------------------------------------- 1 | package org.inlighting.security.service; 2 | 3 | import org.springframework.stereotype.Service; 4 | 5 | @Service 6 | public class UserService { 7 | 8 | private Database database = new Database(); 9 | 10 | public CustomUser getUserByUsername(String username) { 11 | CustomUser originUser = database.getDatabase().get(username); 12 | if (originUser == null) { 13 | return null; 14 | } 15 | 16 | /* 17 | * 此处有坑,之所以这么做是因为Spring Security获得到User后,会把User中的password字段置空,以确保安全。 18 | * 因为Java类是引用传递,为防止Spring Security修改了我们的源头数据,所以我们复制一个对象提供给Spring Security。 19 | * 如果通过真实数据库的方式获取,则没有这种问题需要担心。 20 | */ 21 | return new CustomUser(originUser.getId(), originUser.getUsername(), originUser.getPassword(), originUser.getAuthorities()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/inlighting/security/security/CustomUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package org.inlighting.security.security; 2 | 3 | import org.inlighting.security.service.CustomUser; 4 | import org.inlighting.security.service.UserService; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.security.core.userdetails.UserDetails; 9 | import org.springframework.security.core.userdetails.UserDetailsService; 10 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 11 | import org.springframework.stereotype.Service; 12 | 13 | /** 14 | * 实现官方提供的UserDetailsService接口即可 15 | */ 16 | @Service 17 | public class CustomUserDetailsService implements UserDetailsService { 18 | 19 | private Logger LOGGER = LoggerFactory.getLogger(getClass()); 20 | 21 | @Autowired 22 | private UserService userService; 23 | 24 | @Override 25 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 26 | CustomUser user = userService.getUserByUsername(username); 27 | if (user == null) { 28 | throw new UsernameNotFoundException("该用户不存在"); 29 | } 30 | LOGGER.info("用户名:"+username+" 角色:"+user.getAuthorities().toString()); 31 | return user; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/resources/templates/index/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 登入 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

登入

14 | 17 |
18 |
19 | 20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 | 28 | 29 |
30 | 31 |
32 |
33 | 34 | -------------------------------------------------------------------------------- /src/main/java/org/inlighting/security/service/Database.java: -------------------------------------------------------------------------------- 1 | package org.inlighting.security.service; 2 | 3 | import org.springframework.security.core.GrantedAuthority; 4 | import org.springframework.security.core.authority.AuthorityUtils; 5 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 6 | import org.springframework.security.crypto.password.PasswordEncoder; 7 | 8 | import java.util.Collection; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | public class Database { 13 | 14 | private Map data; 15 | 16 | private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); 17 | 18 | public Database() { 19 | data = new HashMap<>(); 20 | 21 | CustomUser jack = new CustomUser(1, "jack", getPassword("jack123"), getGrants("ROLE_USER")); 22 | CustomUser danny = new CustomUser(2, "danny", getPassword("danny123"), getGrants("ROLE_EDITOR")); 23 | CustomUser alice = new CustomUser(3, "alice", getPassword("alice123"), getGrants("ROLE_REVIEWER")); 24 | CustomUser smith = new CustomUser(4, "smith", getPassword("smith123"), getGrants("ROLE_ADMIN")); 25 | data.put("jack", jack); 26 | data.put("danny", danny); 27 | data.put("alice", alice); 28 | data.put("smith", smith); 29 | } 30 | 31 | public Map getDatabase() { 32 | return data; 33 | } 34 | 35 | private String getPassword(String raw) { 36 | return passwordEncoder.encode(raw); 37 | } 38 | 39 | private Collection getGrants(String role) { 40 | return AuthorityUtils.commaSeparatedStringToAuthorityList(role); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/org/inlighting/security/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package org.inlighting.security.controller; 2 | 3 | import org.inlighting.security.security.IsAdmin; 4 | import org.inlighting.security.security.IsEditor; 5 | import org.inlighting.security.security.IsReviewer; 6 | import org.inlighting.security.security.IsUser; 7 | import org.inlighting.security.service.CustomUser; 8 | import org.springframework.security.access.annotation.Secured; 9 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 10 | import org.springframework.security.core.Authentication; 11 | import org.springframework.security.core.context.SecurityContext; 12 | import org.springframework.security.core.context.SecurityContextHolder; 13 | import org.springframework.stereotype.Controller; 14 | import org.springframework.ui.Model; 15 | import org.springframework.web.bind.annotation.GetMapping; 16 | import org.springframework.web.bind.annotation.RequestMapping; 17 | 18 | import java.security.Principal; 19 | 20 | @IsUser // 表明该控制器下所有请求都需要登入后才能访问 21 | @Controller 22 | @RequestMapping("/user") 23 | public class UserController { 24 | 25 | @GetMapping("/home") 26 | public String home(Model model) { 27 | // 方法一:通过SecurityContextHolder获取 28 | CustomUser user = (CustomUser)SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 29 | model.addAttribute("user", user); 30 | return "user/home"; 31 | } 32 | 33 | @GetMapping("/editor") 34 | @IsEditor 35 | public String editor(Authentication authentication, Model model) { 36 | // 方法二:通过方法注入的形式获取Authentication 37 | CustomUser user = (CustomUser)authentication.getPrincipal(); 38 | model.addAttribute("user", user); 39 | return "user/editor"; 40 | } 41 | 42 | @GetMapping("/reviewer") 43 | @IsReviewer 44 | public String reviewer(Principal principal, Model model) { 45 | // 方法三:同样通过方法注入的方法,注意要转型,此方法很二,不推荐 46 | CustomUser user = (CustomUser) ((Authentication)principal).getPrincipal(); 47 | model.addAttribute("user", user); 48 | return "user/reviewer"; 49 | } 50 | 51 | @GetMapping("/admin") 52 | @IsAdmin 53 | public String admin() { 54 | // 方法四:通过Thymeleaf的Security标签进行,详情见admin.html 55 | return "user/admin"; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/resources/templates/403.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 403 权限不足 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 | 24 |
25 |
26 |
27 |

403

28 |

禁止访问

29 |

权限不足

30 |
31 |

Search

32 |
33 | 41 |
42 |
43 |
44 |
45 |
46 | 47 |
48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/main/resources/templates/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 500 网站出错 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 | 24 |
25 |
26 |
27 |

500

28 |

网站出错

29 |

网站内部发生错误

30 |
31 |

Search

32 |
33 | 41 |
42 |
43 |
44 |
45 |
46 | 47 |
48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/main/resources/templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 404 页面不存在 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 | 24 |
25 |
26 |
27 |

404

28 |

页面不存在

29 |

你访问的页面已经不存在

30 |
31 |

Search

32 |
33 | 41 |
42 |
43 |
44 |
45 |
46 | 47 |
48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.1.1.RELEASE 9 | 10 | 11 | org.inlighting 12 | security-demo 13 | 0.0.1-SNAPSHOT 14 | security-demo 15 | Demo project for Spring Boot & Spring Security 16 | 17 | 18 | 19 | 11 20 | 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-security 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-thymeleaf 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-web 34 | 35 | 36 | 37 | org.thymeleaf.extras 38 | thymeleaf-extras-springsecurity5 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-test 43 | test 44 | 45 | 46 | org.springframework.security 47 | spring-security-test 48 | test 49 | 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-devtools 54 | true 55 | 56 | 57 | 58 | 59 | 60 | 61 | org.springframework.boot 62 | spring-boot-maven-plugin 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/main/java/org/inlighting/security/security/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package org.inlighting.security.security; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 7 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 8 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 9 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 10 | import org.springframework.security.crypto.password.PasswordEncoder; 11 | import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices; 12 | 13 | /** 14 | * 开启方法注解支持,我们设置prePostEnabled = true是为了后面能够使用hasRole()这类表达式 15 | * 进一步了解可看教程:https://www.baeldung.com/spring-security-method-security 16 | */ 17 | @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) 18 | @Configuration 19 | public class SecurityConfig extends WebSecurityConfigurerAdapter { 20 | 21 | /** 22 | * TokenBasedRememberMeServices的生成密钥, 23 | * 算法实现详见文档:https://docs.spring.io/spring-security/site/docs/5.1.3.RELEASE/reference/htmlsingle/#remember-me-hash-token 24 | */ 25 | private final String SECRET_KEY = "123456"; 26 | 27 | @Autowired 28 | private CustomUserDetailsService customUserDetailsService; 29 | 30 | /** 31 | * 必须有此方法,Spring Security官方规定必须要有一个密码加密方式。 32 | * 注意:例如这里用了BCryptPasswordEncoder()的加密方法,那么在保存用户密码的时候也必须使用这种方法,确保前后一致。 33 | * 详情参见项目中Database.java中保存用户的逻辑 34 | */ 35 | @Bean 36 | public PasswordEncoder passwordEncoder() { 37 | return new BCryptPasswordEncoder(); 38 | } 39 | 40 | /** 41 | * 配置Spring Security,下面说明几点注意事项。 42 | * 1. Spring Security 默认是开启了CSRF的,此时我们提交的POST表单必须有隐藏的字段来传递CSRF, 43 | * 而且在logout中,我们必须通过POST到 /logout 的方法来退出用户,详见我们的login.html和logout.html. 44 | * 2. 开启了rememberMe()功能后,我们必须提供rememberMeServices,例如下面的getRememberMeServices()方法, 45 | * 而且我们只能在TokenBasedRememberMeServices中设置cookie名称、过期时间等相关配置,如果在别的地方同时配置,会报错。 46 | * 错误示例:xxxx.and().rememberMe().rememberMeServices(getRememberMeServices()).rememberMeCookieName("cookie-name") 47 | */ 48 | @Override 49 | protected void configure(HttpSecurity http) throws Exception { 50 | http.formLogin() 51 | .loginPage("/login") // 自定义用户登入页面 52 | .failureUrl("/login?error") // 自定义登入失败页面,前端可以通过url中是否有error来提供友好的用户登入提示 53 | .and() 54 | .logout() 55 | .logoutUrl("/logout")// 自定义用户登出页面 56 | .logoutSuccessUrl("/") 57 | .and() 58 | .rememberMe() // 开启记住密码功能 59 | .rememberMeServices(getRememberMeServices()) // 必须提供 60 | .key(SECRET_KEY) // 此SECRET需要和生成TokenBasedRememberMeServices的密钥相同 61 | .and() 62 | /* 63 | * 默认允许所有路径所有人都可以访问,确保静态资源的正常访问。 64 | * 后面再通过方法注解的方式来控制权限。 65 | */ 66 | .authorizeRequests().anyRequest().permitAll() 67 | .and() 68 | .exceptionHandling().accessDeniedPage("/403"); // 权限不足自动跳转403 69 | } 70 | 71 | /** 72 | * 如果要设置cookie过期时间或其他相关配置,请在下方自行配置 73 | */ 74 | private TokenBasedRememberMeServices getRememberMeServices() { 75 | TokenBasedRememberMeServices services = new TokenBasedRememberMeServices(SECRET_KEY, customUserDetailsService); 76 | services.setCookieName("remember-cookie"); 77 | services.setTokenValiditySeconds(100); // 默认14天 78 | return services; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Boot + Spring Security + Thymeleaf 简单教程 2 | 因为有一个项目需采用MVC构架,所以学习了Spring Security并记录下来,希望大家一起学习提供意见 3 | 4 | GitHub地址:[https://github.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo]( https://github.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo)。 5 | 6 | **如果有疑问,请在 GitHub 中发布 issue,我有空会为大家解答的** 7 | 8 | 本项目基于Spring Boot 2 + Spring Security 5 + Thymeleaf 2 + JDK11(你也可以用8,应该区别不大) 9 | 10 | 实现了以下功能: 11 | 12 | * 基于注解的权限控制 13 | * 在Thymeleaf中使用Spring Security的标签 14 | * 自定义权限注解 15 | * 记住密码功能 16 | 17 | 如果需要前后端分离的安全框架搭建教程可以参考:[Spring Boot 2 + Spring Security 5 + JWT 的单页应用Restful解决方案]( https://github.com/Smith-Cruise/Spring-Boot-Security-JWT-SPA) 18 | 19 | ## 项目演示 20 | 21 | 如果想要直接体验,直接 `clone` 项目,运行 `mvn spring-boot:run` 命令即可进行访问,网址规则自行看教程后面 22 | 23 | ***首页*** 24 | 25 | ![首页](https://github.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo/blob/master/file/index.png?raw=true) 26 | 27 | ***登入*** 28 | 29 | ![登入](https://github.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo/blob/master/file/login.png?raw=true) 30 | 31 | ***登出*** 32 | 33 | ![登出](https://github.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo/blob/master/file/logout.png?raw=true) 34 | 35 | ***Home页面*** 36 | 37 | ![Home](https://github.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo/blob/master/file/home.png?raw=true) 38 | 39 | ***Admin页面*** 40 | 41 | ![Admin](https://github.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo/blob/master/file/admin.png?raw=true) 42 | 43 | ***403无权限页面*** 44 | 45 | ![403](https://github.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo/blob/master/file/403.png?raw=true) 46 | 47 | ## Spring Security 基本原理 48 | 49 | ***Spring Security 过滤器链*** 50 | 51 | Spring Security实现了一系列的过滤器链,就按照下面顺序一个一个执行下去。 52 | 53 | 1. `....class` 一些自定义过滤器(在配置的时候你可以自己选择插到哪个过滤器之前),因为这个需求因人而异,本文不探讨,大家可以自己研究 54 | 2. `UsernamePasswordAithenticationFilter.class` Spring Security 自带的表单登入验证过滤器,也是本文主要使用的过滤器 55 | 3. `BasicAuthenticationFilter.class` 56 | 4. `ExceptionTranslation.class` 异常解释器 57 | 5. `FilterSecurityInterceptor.class` 拦截器最终决定请求能否通过 58 | 6. `Controller` 我们最后自己编写的控制器 59 | 60 | ***相关类说明*** 61 | 62 | * `User.class` :注意这个类不是我们自己写的,而是Spring Security官方提供的,他提供了一些基础的功能,我们可以通过继承这个类来扩充方法。详见代码中的 `CustomUser.java` 63 | * `UserDetailsService.class`: Spring Security官方提供的一个接口,里面只有一个方法`loadUserByUsername()` ,Spring Security会调用这个方法来获取数据库中存在的数据,然后和用户POST过来的用户名密码进行比对,从而判断用户的用户名密码是否正确。所以我们需要自己实现`loadUserByUsername()` 这个方法。详见代码中的 `CustomUserDetailsService.java`。 64 | 65 | ## 项目逻辑 66 | 67 | 为了体现权限区别,我们通过HashMap构造了一个数据库,里面包含了4个用户 68 | 69 | | ID | 用户名 | 密码 | 权限 | 70 | | ---- | ------ | -------- | -------- | 71 | | 1 | jack | jack123 | user | 72 | | 2 | danny | danny123 | editor | 73 | | 3 | alice | alice123 | reviewer | 74 | | 4 | smith | smith123 | admin | 75 | 76 | 说明下权限 77 | 78 | `user`:最基础的权限,只要是登入用户就有 `user` 权限 79 | 80 | `editor`:在 `user` 权限上面增加了 `editor` 的权限 81 | 82 | `reviewer`:与上同理,`editor` 和 `reviewer` 属于同一级的权限 83 | 84 | `admin`:包含所有权限 85 | 86 | 为了检验权限,我们提供若干个页面 87 | 88 | | 网址 | 说明 | 可访问权限 | 89 | | -------------- | ----------------------------------- | --------------------------- | 90 | | / | 首页 | 所有人均可访问(anonymous) | 91 | | /login | 登入页面 | 所有人均可访问(anonymous) | 92 | | /logout | 退出页面 | 所有人均可访问(anonymous) | 93 | | /user/home | 用户中心 | user | 94 | | /user/editor | | editor, admin | 95 | | /user/reviewer | | reviewer, admin | 96 | | /user/admin | | admin | 97 | | /403 | 403错误页面,美化过,大家可以直接用 | 所有人均可访问(anonymous) | 98 | | /404 | 404错误页面,美化过,大家可以直接用 | 所有人均可访问(anonymous) | 99 | | /500 | 500错误页面,美化过,大家可以直接用 | 所有人均可访问(anonymous) | 100 | 101 | ## 代码配置 102 | 103 | ***Maven 配置*** 104 | 105 | ```xml 106 | 107 | 109 | 4.0.0 110 | 111 | org.springframework.boot 112 | spring-boot-starter-parent 113 | 2.1.1.RELEASE 114 | 115 | 116 | org.inlighting 117 | security-demo 118 | 0.0.1-SNAPSHOT 119 | security-demo 120 | Demo project for Spring Boot & Spring Security 121 | 122 | 123 | 124 | 11 125 | 126 | 127 | 128 | 129 | org.springframework.boot 130 | spring-boot-starter-security 131 | 132 | 133 | org.springframework.boot 134 | spring-boot-starter-thymeleaf 135 | 136 | 137 | org.springframework.boot 138 | spring-boot-starter-web 139 | 140 | 141 | 142 | org.thymeleaf.extras 143 | thymeleaf-extras-springsecurity5 144 | 145 | 146 | org.springframework.boot 147 | spring-boot-starter-test 148 | test 149 | 150 | 151 | org.springframework.security 152 | spring-security-test 153 | test 154 | 155 | 156 | 157 | org.springframework.boot 158 | spring-boot-devtools 159 | true 160 | 161 | 162 | 163 | 164 | 165 | 166 | org.springframework.boot 167 | spring-boot-maven-plugin 168 | 169 | 170 | 171 | 172 | ``` 173 | 174 | ***application.properties配置*** 175 | 176 | 为了使热加载(这样修改模板后无需重启 Tomcat )生效,我们需要在Spring Boot的配置文件上面加上一段话 177 | 178 | ```properties 179 | spring.thymeleaf.cache=false 180 | ``` 181 | 182 | 如果需要详细了解热加载,请看官方文档:[https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-hotswapping](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-hotswapping) 183 | 184 | ## Spring Security 配置 185 | 186 | 首先我们开启方法注解支持:只需要在类上添加 `@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)` 注解,我们设置 `prePostEnabled = true` 是为了支持 `hasRole()` 这类表达式。如果想进一步了解方法注解可以看 [Introduction to Spring Method Security](https://www.baeldung.com/spring-security-method-security) 这篇文章。 187 | 188 | ***SecurityConfig.java*** 189 | 190 | ```java 191 | /** 192 | * 开启方法注解支持,我们设置prePostEnabled = true是为了后面能够使用hasRole()这类表达式 193 | * 进一步了解可看教程:https://www.baeldung.com/spring-security-method-security 194 | */ 195 | @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) 196 | @Configuration 197 | public class SecurityConfig extends WebSecurityConfigurerAdapter { 198 | 199 | /** 200 | * TokenBasedRememberMeServices的生成密钥, 201 | * 算法实现详见文档:https://docs.spring.io/spring-security/site/docs/5.1.3.RELEASE/reference/htmlsingle/#remember-me-hash-token 202 | */ 203 | private final String SECRET_KEY = "123456"; 204 | 205 | @Autowired 206 | private CustomUserDetailsService customUserDetailsService; 207 | 208 | /** 209 | * 必须有此方法,Spring Security官方规定必须要有一个密码加密方式。 210 | * 注意:例如这里用了BCryptPasswordEncoder()的加密方法,那么在保存用户密码的时候也必须使用这种方法,确保前后一致。 211 | * 详情参见项目中Database.java中保存用户的逻辑 212 | */ 213 | @Bean 214 | public PasswordEncoder passwordEncoder() { 215 | return new BCryptPasswordEncoder(); 216 | } 217 | 218 | /** 219 | * 配置Spring Security,下面说明几点注意事项。 220 | * 1. Spring Security 默认是开启了CSRF的,此时我们提交的POST表单必须有隐藏的字段来传递CSRF, 221 | * 而且在logout中,我们必须通过POST到 /logout 的方法来退出用户,详见我们的login.html和logout.html. 222 | * 2. 开启了rememberMe()功能后,我们必须提供rememberMeServices,例如下面的getRememberMeServices()方法, 223 | * 而且我们只能在TokenBasedRememberMeServices中设置cookie名称、过期时间等相关配置,如果在别的地方同时配置,会报错。 224 | * 错误示例:xxxx.and().rememberMe().rememberMeServices(getRememberMeServices()).rememberMeCookieName("cookie-name") 225 | */ 226 | @Override 227 | protected void configure(HttpSecurity http) throws Exception { 228 | http.formLogin() 229 | .loginPage("/login") // 自定义用户登入页面 230 | .failureUrl("/login?error") // 自定义登入失败页面,前端可以通过url中是否有error来提供友好的用户登入提示 231 | .and() 232 | .logout() 233 | .logoutUrl("/logout")// 自定义用户登出页面 234 | .logoutSuccessUrl("/") 235 | .and() 236 | .rememberMe() // 开启记住密码功能 237 | .rememberMeServices(getRememberMeServices()) // 必须提供 238 | .key(SECRET_KEY) // 此SECRET需要和生成TokenBasedRememberMeServices的密钥相同 239 | .and() 240 | /* 241 | * 默认允许所有路径所有人都可以访问,确保静态资源的正常访问。 242 | * 后面再通过方法注解的方式来控制权限。 243 | */ 244 | .authorizeRequests().anyRequest().permitAll() 245 | .and() 246 | .exceptionHandling().accessDeniedPage("/403"); // 权限不足自动跳转403 247 | } 248 | 249 | /** 250 | * 如果要设置cookie过期时间或其他相关配置,请在下方自行配置 251 | */ 252 | private TokenBasedRememberMeServices getRememberMeServices() { 253 | TokenBasedRememberMeServices services = new TokenBasedRememberMeServices(SECRET_KEY, customUserDetailsService); 254 | services.setCookieName("remember-cookie"); 255 | services.setTokenValiditySeconds(100); // 默认14天 256 | return services; 257 | } 258 | } 259 | ``` 260 | 261 | ***UserService.java*** 262 | 263 | 自己模拟数据库操作的`Service`,用于向自己通过`HashMap`模拟的数据源获取数据。 264 | 265 | ```java 266 | @Service 267 | public class UserService { 268 | 269 | private Database database = new Database(); 270 | 271 | public CustomUser getUserByUsername(String username) { 272 | CustomUser originUser = database.getDatabase().get(username); 273 | if (originUser == null) { 274 | return null; 275 | } 276 | 277 | /* 278 | * 此处有坑,之所以这么做是因为Spring Security获得到User后,会把User中的password字段置空,以确保安全。 279 | * 因为Java类是引用传递,为防止Spring Security修改了我们的源头数据,所以我们复制一个对象提供给Spring Security。 280 | * 如果通过真实数据库的方式获取,则没有这种问题需要担心。 281 | */ 282 | return new CustomUser(originUser.getId(), originUser.getUsername(), originUser.getPassword(), originUser.getAuthorities()); 283 | } 284 | } 285 | ``` 286 | 287 | ***CustomUserDetailsService.java*** 288 | 289 | ```java 290 | /** 291 | * 实现官方提供的UserDetailsService接口即可 292 | */ 293 | @Service 294 | public class CustomUserDetailsService implements UserDetailsService { 295 | 296 | private Logger LOGGER = LoggerFactory.getLogger(getClass()); 297 | 298 | @Autowired 299 | private UserService userService; 300 | 301 | @Override 302 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 303 | CustomUser user = userService.getUserByUsername(username); 304 | if (user == null) { 305 | throw new UsernameNotFoundException("该用户不存在"); 306 | } 307 | LOGGER.info("用户名:"+username+" 角色:"+user.getAuthorities().toString()); 308 | return user; 309 | } 310 | } 311 | ``` 312 | 313 | 314 | 315 | ## 自定义权限注解 316 | 317 | 我们在开发网站的过程中,比如 `GET /user/editor ` 这个请求角色为 `EDITOR` 和 `ADMIN` 肯定都可以,如果我们在每一个需要判断权限的方法上面写一长串的权限表达式,一定很复杂。但是通过自定义权限注解,我们可以通过 `@IsEditor` 这样的方法来判断,这样一来就简单了很多。进一步了解可以看:[Introduction to Spring Method Security](https://www.baeldung.com/spring-security-method-security) 318 | 319 | ***IsUser.java*** 320 | 321 | ```java 322 | @Target({ElementType.METHOD, ElementType.TYPE}) 323 | @Retention(RetentionPolicy.RUNTIME) 324 | @PreAuthorize("hasAnyAuthority('ROLE_USER', 'ROLE_EDITOR', 'ROLE_REVIEWER', 'ROLE_ADMIN')") 325 | public @interface IsUser { 326 | } 327 | ``` 328 | 329 | ***IsEditor.java*** 330 | 331 | ```java 332 | @Target({ElementType.METHOD, ElementType.TYPE}) 333 | @Retention(RetentionPolicy.RUNTIME) 334 | @PreAuthorize("hasAnyRole('ROLE_USER', 'ROLE_EDITOR', 'ROLE_ADMIN')") 335 | public @interface IsEditor { 336 | } 337 | ``` 338 | 339 | ***IsReviewer.java*** 340 | 341 | ```java 342 | @Target({ElementType.METHOD, ElementType.TYPE}) 343 | @Retention(RetentionPolicy.RUNTIME) 344 | @PreAuthorize("hasAnyRole('ROLE_USER', 'ROLE_REVIEWER', 'ROLE_ADMIN')") 345 | public @interface IsReviewer { 346 | } 347 | ``` 348 | 349 | ***IsAdmin.java*** 350 | 351 | ```java 352 | @Target({ElementType.METHOD, ElementType.TYPE}) 353 | @Retention(RetentionPolicy.RUNTIME) 354 | @PreAuthorize("hasAnyRole('ROLE_ADMIN')") 355 | public @interface IsAdmin { 356 | } 357 | ``` 358 | 359 | ***Spring Security自带表达式*** 360 | 361 | - `hasRole()`,是否拥有某一个权限 362 | 363 | - `hasAnyRole()`,多个权限中有一个即可,如 `hasAnyRole("ADMIN","USER")` 364 | - `hasAuthority()`,`Authority` 和 `Role` 很像,唯一的区别就是 `Authority` 前缀多了 `ROLE_` ,如 `hasAuthority("ROLE_ADMIN")` 等价于 `hasRole("ADMIN")` ,可以参考上面 `IsUser.java` 的写法 365 | - `hasAnyAuthority()`,同上,多个权限中有一个即可 366 | - `permitAll()`, `denyAll()`,`isAnonymous()`, `isRememberMe()`,通过字面意思可以理解 367 | - `isAuthenticated()`, `isFullyAuthenticated()`,这两个区别就是`isFullyAuthenticated()`对认证的安全要求更高。例如用户通过**记住密码功能**登入到系统进行敏感操作,`isFullyAuthenticated()`会返回`false`,此时我们可以让用户再输入一次密码以确保安全,而 `isAuthenticated()` 只要是登入用户均返回`true`。 368 | - `principal()`, `authentication()`,例如我们想获取登入用户的id,可以通过`principal()` 返回的 `Object` 获取,实际上 `principal()` 返回的 `Object` 基本上可以等同我们自己编写的 `CustomUser` 。而 `authentication()` 返回的 `Authentication` 是 `Principal` 的父类,相关操作可看 `Authentication` 的源码。进一步了解可以看后面**Controller编写中获取用户数据的四种方法** 369 | - `hasPermission()`,参考字面意思即可 370 | 371 | 如果想进一步了解,可以参考 [Intro to Spring Security Expressions](https://www.baeldung.com/spring-security-expressions)。 372 | 373 | ## 添加Thymeleaf支持 374 | 375 | 我们通过 `thymeleaf-extras-springsecurity` 来添加Thymeleaf对Spring Security的支持。 376 | 377 | ***Maven配置*** 378 | 379 | 上面的Maven配置已经加过了 380 | 381 | ```xml 382 | 383 | org.thymeleaf.extras 384 | thymeleaf-extras-springsecurity5 385 | 386 | ``` 387 | 388 | ***使用例子*** 389 | 390 | 注意我们要在html中添加 `xmlns:sec` 的支持 391 | 392 | ```html 393 | 394 | 395 | 396 | 397 | Admin 398 | 399 | 400 |

This is a home page.

401 |

Id:

402 |

Username:

403 |

Role:

404 | 405 | 406 | ``` 407 | 408 | 如果想进一步了解请看文档 [thymeleaf-extras-springsecurity](https://github.com/thymeleaf/thymeleaf-extras-springsecurity)。 409 | 410 | ## Controller编写 411 | 412 | ***IndexController.java*** 413 | 414 | 本控制器没有任何的权限规定 415 | 416 | ```java 417 | @Controller 418 | public class IndexController { 419 | 420 | @GetMapping("/") 421 | public String index() { 422 | return "index/index"; 423 | } 424 | 425 | @GetMapping("/login") 426 | public String login() { 427 | return "index/login"; 428 | } 429 | 430 | @GetMapping("/logout") 431 | public String logout() { 432 | return "index/logout"; 433 | } 434 | } 435 | ``` 436 | 437 | ***UserController.java*** 438 | 439 | 在这个控制器中,我综合展示了自定义注解的使用和4种获取用户信息的方式 440 | 441 | ```java 442 | @IsUser // 表明该控制器下所有请求都需要登入后才能访问 443 | @Controller 444 | @RequestMapping("/user") 445 | public class UserController { 446 | 447 | @GetMapping("/home") 448 | public String home(Model model) { 449 | // 方法一:通过SecurityContextHolder获取 450 | CustomUser user = (CustomUser)SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 451 | model.addAttribute("user", user); 452 | return "user/home"; 453 | } 454 | 455 | @GetMapping("/editor") 456 | @IsEditor 457 | public String editor(Authentication authentication, Model model) { 458 | // 方法二:通过方法注入的形式获取Authentication 459 | CustomUser user = (CustomUser)authentication.getPrincipal(); 460 | model.addAttribute("user", user); 461 | return "user/editor"; 462 | } 463 | 464 | @GetMapping("/reviewer") 465 | @IsReviewer 466 | public String reviewer(Principal principal, Model model) { 467 | // 方法三:同样通过方法注入的方法,注意要转型,此方法很二,不推荐 468 | CustomUser user = (CustomUser) ((Authentication)principal).getPrincipal(); 469 | model.addAttribute("user", user); 470 | return "user/reviewer"; 471 | } 472 | 473 | @GetMapping("/admin") 474 | @IsAdmin 475 | public String admin() { 476 | // 方法四:通过Thymeleaf的Security标签进行,详情见admin.html 477 | return "user/admin"; 478 | } 479 | } 480 | ``` 481 | 482 | ***注意*** 483 | 484 | * 如果有安全控制的方法 A 被同一个类中别的方法调用,那么方法 A 的权限控制会被忽略,私有方法同样会受到影响 485 | * Spring 的 `SecurityContext` 是线程绑定的,如果我们在当前的线程中新建了别的线程,那么他们的 `SecurityContext` 是不共享的,进一步了解请看 [Spring Security Context Propagation with @Async](https://www.baeldung.com/spring-security-async-principal-propagation) 486 | 487 | ## Html的编写 488 | 489 | 在编写html的时候,基本上就是大同小异了,就是注意一点,**如果开启了CSRF,在编写表单POST请求的时候添加上隐藏字段,如 **`` **,不过大家其实不用加也没事,因为Thymeleaf自动会加上去的😀。** 490 | 491 | ## 总结 492 | 493 | 教程粗糙,欢迎指正! 494 | 495 | 如需深入了解,如果想系统的学习可以看看 [Security with Spring](https://www.baeldung.com/security-spring)。 496 | --------------------------------------------------------------------------------