共计 10679 个字符,预计需要花费 27 分钟才能阅读完成。
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)
@Configuration
public 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
模仿的数据源获取数据。
@Service
public 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 接口即可
*/
@Service
public 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
本控制器没有任何的权限规定
@Controller
public 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…