Spring Security 基本原理
Spring Security 过滤器链
Spring Security实现了一系列的过滤器链,就依照上面程序一个一个执行上来。
....class
一些自定义过滤器(在配置的时候你能够本人抉择插到哪个过滤器之前),因为这个需要因人而异,本文不探讨,大家能够本人钻研UsernamePasswordAithenticationFilter.class
Spring Security 自带的表单登入验证过滤器,也是本文次要应用的过滤器BasicAuthenticationFilter.class
ExceptionTranslation.class
异样解释器FilterSecurityInterceptor.class
拦截器最终决定申请是否通过Controller
咱们最初本人编写的控制器
相干类阐明
User.class
:留神这个类不是咱们本人写的,而是Spring Security官网提供的,他提供了一些根底的性能,咱们能够通过继承这个类来裁减办法。详见代码中的CustomUser.java
UserDetailsService.class
: Spring Security官网提供的一个接口,外面只有一个办法loadUserByUsername()
,Spring Security会调用这个办法来获取数据库中存在的数据,而后和用户POST过去的用户名明码进行比对,从而判断用户的用户名明码是否正确。所以咱们须要本人实现loadUserByUsername()
这个办法。详见代码中的CustomUserDetailsService.java
。
我的项目逻辑
为了体现权限区别,咱们通过HashMap结构了一个数据库,外面蕴含了4个用户
ID | 用户名 | 明码 | 权限 |
---|---|---|---|
1 | jack | jack123 | user |
2 | danny | danny123 | editor |
3 | alice | alice123 | reviewer |
4 | smith | smith123 | admin |
阐明下权限
user
:最根底的权限,只有是登入用户就有 user
权限
editor
:在 user
权限下面减少了 editor
的权限
reviewer
:与上同理,editor
和 reviewer
属于同一级的权限
admin
:蕴含所有权限
为了测验权限,咱们提供若干个页面
网址 | 阐明 | 可拜访权限 |
---|---|---|
/ | 首页 | 所有人均可拜访(anonymous) |
/login | 登入页面 | 所有人均可拜访(anonymous) |
/logout | 退出页面 | 所有人均可拜访(anonymous) |
/user/home | 用户核心 | user |
/user/editor | editor, admin | |
/user/reviewer | reviewer, admin | |
/user/admin | admin | |
/403 | 403谬误页面,丑化过,大家能够间接用 | 所有人均可拜访(anonymous) |
/404 | 404谬误页面,丑化过,大家能够间接用 | 所有人均可拜访(anonymous) |
/500 | 500谬误页面,丑化过,大家能够间接用 | 所有人均可拜访(anonymous) |
代码配置
Maven 配置
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.1.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>org.inlighting</groupId> <artifactId>security-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>security-demo</name> <description>Demo project for Spring Boot & Spring Security</description> <!--指定JDK版本,大家能够改成本人的--> <properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--对Thymeleaf增加Spring Security标签反对--> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <!--开发的热加载配置--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>
application.properties配置
为了使热加载(这样批改模板后无需重启 Tomcat )失效,咱们须要在Spring Boot的配置文件下面加上一段话
spring.thymeleaf.cache=false
如果须要具体理解热加载,请看官网文档:https://docs.spring.io/spring...
Spring Security 配置
首先咱们开启办法注解反对:只须要在类上增加 @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
注解,咱们设置 prePostEnabled = true
是为了反对 hasRole()
这类表达式。如果想进一步理解办法注解能够看 Introduction to Spring Method Security 这篇文章。
SecurityConfig.java
/** * 开启办法注解反对,咱们设置prePostEnabled = true是为了前面可能应用hasRole()这类表达式 * 进一步理解可看教程:https://www.baeldung.com/spring-security-method-security */@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)@Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter { /** * TokenBasedRememberMeServices的生成密钥, * 算法实现详见文档:https://docs.spring.io/spring-security/site/docs/5.1.3.RELEASE/reference/htmlsingle/#remember-me-hash-token */ private final String SECRET_KEY = "123456"; @Autowired private CustomUserDetailsService customUserDetailsService; /** * 必须有此办法,Spring Security官网规定必须要有一个明码加密形式。 * 留神:例如这里用了BCryptPasswordEncoder()的加密办法,那么在保留用户明码的时候也必须应用这种办法,确保前后一致。 * 详情参见我的项目中Database.java中保留用户的逻辑 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 配置Spring Security,上面阐明几点注意事项。 * 1. Spring Security 默认是开启了CSRF的,此时咱们提交的POST表单必须有暗藏的字段来传递CSRF, * 而且在logout中,咱们必须通过POST到 /logout 的办法来退出用户,详见咱们的login.html和logout.html. * 2. 开启了rememberMe()性能后,咱们必须提供rememberMeServices,例如上面的getRememberMeServices()办法, * 而且咱们只能在TokenBasedRememberMeServices中设置cookie名称、过期工夫等相干配置,如果在别的中央同时配置,会报错。 * 谬误示例:xxxx.and().rememberMe().rememberMeServices(getRememberMeServices()).rememberMeCookieName("cookie-name") */ @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login") // 自定义用户登入页面 .failureUrl("/login?error") // 自定义登入失败页面,前端能够通过url中是否有error来提供敌对的用户登入提醒 .and() .logout() .logoutUrl("/logout")// 自定义用户登出页面 .logoutSuccessUrl("/") .and() .rememberMe() // 开启记住明码性能 .rememberMeServices(getRememberMeServices()) // 必须提供 .key(SECRET_KEY) // 此SECRET须要和生成TokenBasedRememberMeServices的密钥雷同 .and() /* * 默认容许所有门路所有人都能够拜访,确保动态资源的失常拜访。 * 前面再通过办法注解的形式来管制权限。 */ .authorizeRequests().anyRequest().permitAll() .and() .exceptionHandling().accessDeniedPage("/403"); // 权限有余主动跳转403 } /** * 如果要设置cookie过期工夫或其余相干配置,请在下方自行配置 */ private TokenBasedRememberMeServices getRememberMeServices() { TokenBasedRememberMeServices services = new TokenBasedRememberMeServices(SECRET_KEY, customUserDetailsService); services.setCookieName("remember-cookie"); services.setTokenValiditySeconds(100); // 默认14天 return services; }}
UserService.java
本人模仿数据库操作的Service
,用于向本人通过HashMap
模仿的数据源获取数据。
@Servicepublic class UserService { private Database database = new Database(); public CustomUser getUserByUsername(String username) { CustomUser originUser = database.getDatabase().get(username); if (originUser == null) { return null; } /* * 此处有坑,之所以这么做是因为Spring Security取得到User后,会把User中的password字段置空,以确保安全。 * 因为Java类是援用传递,为避免Spring Security批改了咱们的源头数据,所以咱们复制一个对象提供给Spring Security。 * 如果通过实在数据库的形式获取,则没有这种问题须要放心。 */ return new CustomUser(originUser.getId(), originUser.getUsername(), originUser.getPassword(), originUser.getAuthorities()); }}
CustomUserDetailsService.java
/** * 实现官网提供的UserDetailsService接口即可 */@Servicepublic class CustomUserDetailsService implements UserDetailsService { private Logger LOGGER = LoggerFactory.getLogger(getClass()); @Autowired private UserService userService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { CustomUser user = userService.getUserByUsername(username); if (user == null) { throw new UsernameNotFoundException("该用户不存在"); } LOGGER.info("用户名:"+username+" 角色:"+user.getAuthorities().toString()); return user; }}
自定义权限注解
咱们在开发网站的过程中,比方 GET /user/editor
这个申请角色为 EDITOR
和 ADMIN
必定都能够,如果咱们在每一个须要判断权限的办法下面写一长串的权限表达式,肯定很简单。然而通过自定义权限注解,咱们能够通过 @IsEditor
这样的办法来判断,这样一来就简略了很多。进一步理解能够看:Introduction to Spring Method Security
IsUser.java
@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@PreAuthorize("hasAnyAuthority('ROLE_USER', 'ROLE_EDITOR', 'ROLE_REVIEWER', 'ROLE_ADMIN')")public @interface IsUser {}
IsEditor.java
@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@PreAuthorize("hasAnyRole('ROLE_USER', 'ROLE_EDITOR', 'ROLE_ADMIN')")public @interface IsEditor {}
IsReviewer.java
@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@PreAuthorize("hasAnyRole('ROLE_USER', 'ROLE_REVIEWER', 'ROLE_ADMIN')")public @interface IsReviewer {}
IsAdmin.java
@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@PreAuthorize("hasAnyRole('ROLE_ADMIN')")public @interface IsAdmin { }
Spring Security自带表达式
hasRole()
,是否领有某一个权限hasAnyRole()
,多个权限中有一个即可,如hasAnyRole("ADMIN","USER")
hasAuthority()
,Authority
和Role
很像,惟一的区别就是Authority
前缀多了ROLE_
,如hasAuthority("ROLE_ADMIN")
等价于hasRole("ADMIN")
,能够参考下面IsUser.java
的写法hasAnyAuthority()
,同上,多个权限中有一个即可permitAll()
,denyAll()
,isAnonymous()
,isRememberMe()
,通过字面意思能够了解isAuthenticated()
,isFullyAuthenticated()
,这两个区别就是isFullyAuthenticated()
对认证的平安要求更高。例如用户通过记住明码性能登入到零碎进行敏感操作,isFullyAuthenticated()
会返回false
,此时咱们能够让用户再输出一次明码以确保安全,而isAuthenticated()
只有是登入用户均返回true
。principal()
,authentication()
,例如咱们想获取登入用户的id,能够通过principal()
返回的Object
获取,实际上principal()
返回的Object
基本上能够等同咱们本人编写的CustomUser
。而authentication()
返回的Authentication
是Principal
的父类,相干操作可看Authentication
的源码。进一步理解能够看前面Controller编写中获取用户数据的四种办法hasPermission()
,参考字面意思即可
如果想进一步理解,能够参考 Intro to Spring Security Expressions。
增加Thymeleaf反对
咱们通过 thymeleaf-extras-springsecurity
来增加Thymeleaf对Spring Security的反对。
Maven配置
下面的Maven配置曾经加过了
<dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId></dependency>
应用例子
留神咱们要在html中增加 xmlns:sec
的反对
<!DOCTYPE html><html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security"><head> <meta charset="UTF-8"> <title>Admin</title></head><body><p>This is a home page.</p><p>Id: <th:block sec:authentication="principal.id"></th:block></p><p>Username: <th:block sec:authentication="principal.username"></th:block></p><p>Role: <th:block sec:authentication="principal.authorities"></th:block></p></body></html>
如果想进一步理解请看文档 thymeleaf-extras-springsecurity。
Controller编写
IndexController.java
本控制器没有任何的权限规定
@Controllerpublic class IndexController { @GetMapping("/") public String index() { return "index/index"; } @GetMapping("/login") public String login() { return "index/login"; } @GetMapping("/logout") public String logout() { return "index/logout"; }}
UserController.java
在这个控制器中,我综合展现了自定义注解的应用和4种获取用户信息的形式
@IsUser // 表明该控制器下所有申请都须要登入后能力拜访@Controller@RequestMapping("/user")public class UserController { @GetMapping("/home") public String home(Model model) { // 办法一:通过SecurityContextHolder获取 CustomUser user = (CustomUser)SecurityContextHolder.getContext().getAuthentication().getPrincipal(); model.addAttribute("user", user); return "user/home"; } @GetMapping("/editor") @IsEditor public String editor(Authentication authentication, Model model) { // 办法二:通过办法注入的模式获取Authentication CustomUser user = (CustomUser)authentication.getPrincipal(); model.addAttribute("user", user); return "user/editor"; } @GetMapping("/reviewer") @IsReviewer public String reviewer(Principal principal, Model model) { // 办法三:同样通过办法注入的办法,留神要转型,此办法很二,不举荐 CustomUser user = (CustomUser) ((Authentication)principal).getPrincipal(); model.addAttribute("user", user); return "user/reviewer"; } @GetMapping("/admin") @IsAdmin public String admin() { // 办法四:通过Thymeleaf的Security标签进行,详情见admin.html return "user/admin"; }}
留神
- 如果有安全控制的办法 A 被同一个类中别的办法调用,那么办法 A 的权限管制会被疏忽,公有办法同样会受到影响
- Spring 的
SecurityContext
是线程绑定的,如果咱们在以后的线程中新建了别的线程,那么他们的SecurityContext
是不共享的,进一步理解请看 Spring Security Context Propagation with @Async
Html的编写
在编写html的时候,基本上就是大同小异了,就是留神一点,如果开启了CSRF,在编写表单POST申请的时候增加上暗藏字段,如 <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
,不过大家其实不必加也没事,因为Thymeleaf主动会加上去的。
github地址及其起源:https://github.com/Smith-Crui...