本文部分内容参考6-Spring Security - Charlie’s Blog (chillcharlie357.github.io)
- 划分为两类:
- 针对客户web请求权限控制
- 针对方法级的权限控制
- 针对业务层代码
- 调用前控制,调用后控制
- 例:对数据库delete操作做权限控制
- 在spring中使用:添加依赖
spring-boot-starter-security
后会自动加载安全相关的bean
添加依赖
1 | <dependency> |
加了security starter后自动获得登录界面
- 用户名:user
- 密码查看日志:Using generated security password: cb81f1d1-c11e-4b0a-b728- 92d591ffa9c5
Cookie访问会话的维持
访问一开始的design页面会获得一个http状态码302,重定向到login页面
登录完成以后,服务端返回一个set cookie的属性,里面有JSESSIONID,以后的每次请求都会带上这个id,作为请求头发送到服务端,服务端就知道每次的请求来自哪个
开发要做什么
除了框架提供的,开发人员还需要做什么,很重要
- 实现接口
UserDetailsService
接口:给Spring框架提供用户详细信息。用户信息注册存储,需要用到用户信息的时候从数据访问层获取- 这里用到之前讲到数据访问层实现技术。
- 和spring security解耦,只需要提供用户信息但不关心怎么实现。
- 被Spring Security调用
- 实现密码加密/解密对象
- PasswordEncoder
- Bean对象
- (optional)实现登录页面
- 有默认页面
/login
, Spring已经自动实现了对应的Controller
- 权限设定
SecurityFilterChain
,基于注入的httpSecurity
对象- 继承父类
WebSecurityConfigurerAdapter
,实现configure
方法
框架实现了什么
- 实现用户登录控制器get post
- 请求重定向到用户登录页面
- eg.用户未登录时,访问URL,服务端重定向到登录页面
- 通过Filter对用户设定的权限进行权限控制
两种配置
-
纯Java配置类
@Configuration public class SecurityConfig { }
SecurityFilterChain :直接从上下文注入,然后不用实现,可以在里面对web请求做权限控制authorizeRequests/替换login页面
-
继承自WebSecurityConfigurerAdapter
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {
第一种
security config
1 | package tacos.security; |
第二种
security config
1 | package tacos.security; |
纯Java配置类
- 提供密码转换器:PasswordEncoder
- 提供接口实现:UserDetailsService,用于从用户名获取用户信息
密码转码器
- NoOpPasswordEncoder:不编码密码,而保持明文,因为它不会对密码进行哈希化,所以永远不要在真实场 景中使用它
- StandardPasswordEncoder:使用SHA-256对密码进行哈希化。这个实现现在已经不推荐了,不应该在新的 实现中使用它
- Pbkdf2PasswordEncoder:使用基于密码的密钥派生函数2(PBKDF2)
- BCryptPasswordEncoder:使用bcrypt强哈希函数对密码进行编码
- SCryptPasswordEncoder:使用scrypt强哈希函数对密码进行编码
用户信息存储
来自多个渠道,spring security不关心。
-
内存用户存储 : security config中的configure
-
// IN MEMORY AUTHENTICATION EXAMPLE // @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("buzz") .password("infinity") .authorities("ROLE_USER") .and() .withUser("woody") .password("bullseye") .authorities("ROLE_USER"); } // // JDBC Authentication example // /* @Autowired DataSource dataSource; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .jdbcAuthentication() .dataSource(dataSource); } */ /* @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .jdbcAuthentication() .dataSource(dataSource) .usersByUsernameQuery( "select username, password, enabled from Users " + "where username=?") .authoritiesByUsernameQuery( "select username, authority from UserAuthorities " + "where username=?"); } */ /* @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .jdbcAuthentication() .dataSource(dataSource) .usersByUsernameQuery( "select username, password, enabled from Users " + "where username=?") .authoritiesByUsernameQuery( "select username, authority from UserAuthorities " + "where username=?") .passwordEncoder(new BCryptPasswordEncoder()); } */ // // LDAP Authentication example // /* @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .ldapAuthentication() .userSearchFilter("(uid={0})") .groupSearchFilter("member={0}"); } */ /* @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .ldapAuthentication() .userSearchBase("ou=people") .userSearchFilter("(uid={0})") .groupSearchBase("ou=groups") .groupSearchFilter("member={0}"); } */ /* @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .ldapAuthentication() .userSearchBase("ou=people") .userSearchFilter("(uid={0})") .groupSearchBase("ou=groups") .groupSearchFilter("member={0}") .passwordCompare(); } */ /* @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .ldapAuthentication() .userSearchBase("ou=people") .userSearchFilter("(uid={0})") .groupSearchBase("ou=groups") .groupSearchFilter("member={0}") .passwordCompare() .passwordEncoder(new BCryptPasswordEncoder()) .passwordAttribute("passcode"); } */ /* @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .ldapAuthentication() .userSearchBase("ou=people") .userSearchFilter("(uid={0})") .groupSearchBase("ou=groups") .groupSearchFilter("member={0}") .passwordCompare() .passwordEncoder(new BCryptPasswordEncoder()) .passwordAttribute("passcode") .and() .contextSource() .url("ldap://tacocloud.com:389/dc=tacocloud,dc=com"); } */ /* @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .ldapAuthentication() .userSearchBase("ou=people") .userSearchFilter("(uid={0})") .groupSearchBase("ou=groups") .groupSearchFilter("member={0}") .passwordCompare() .passwordEncoder(new BCryptPasswordEncoder()) .passwordAttribute("passcode") .and() .contextSource() .root("dc=tacocloud,dc=com"); } */ /* @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .ldapAuthentication() .userSearchBase("ou=people") .userSearchFilter("(uid={0})") .groupSearchBase("ou=groups") .groupSearchFilter("member={0}") .passwordCompare() .passwordEncoder(new BCryptPasswordEncoder()) .passwordAttribute("passcode") .and() .contextSource() .root("dc=tacocloud,dc=com") .ldif("classpath:users.ldif"); } */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
* JDBC用户存储
* LDAP用户存储
## 使用Spring Data存储库来保存用户
* 定义领域对象:User,实现了UserDetails接口
* UserDetailsService接口方法:UserDetails
* -loadUserByUsername(String username)
* 定义持久化接口:UserRepository,增加自定义方法:findByUsername
user
```java
package tacos;
import java.util.Arrays;
import java.util.Collection;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.
SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
@Entity
@Data
@NoArgsConstructor(access=AccessLevel.PRIVATE, force=true)
@RequiredArgsConstructor
public class User implements UserDetails {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private final String username;
private final String password;
private final String fullname;
private final String street;
private final String city;
private final String state;
private final String zip;
private final String phoneNumber;
//获取权限集合,自定了一个user的role:用户的角色叫user,是user角色的用户拥有ROLE_USER权限
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
-
login.xml
1 | <!-- tag::all[] --> |
web config
- ViewController:视图控制器:
- addViewController(“/”).setViewName(“home”):请求是/,转向home页面
1 | package tacos.web; |
注册用户
- RegistrationController
保护Web请求
@Bean
public SecurityFilterChain filterChain(HttpSecurity http)
启用HTTP Basic认证👍
HTTP协议内容,与Spring框架无关。
由于用户 ID 与密码是是以明文的形式在网络中进行传输的(尽管采用了 base64 编码,但是 base64 算法是可逆的),所以基本验证方案并不安全。
- 启用HTTP basic认证:
httpBasic()
- 默认关闭
- 在请求时带上用户名密码,一般在测试的时候使用
Authorization
属性https://username:password@www.example.com/
HTTP authentication - HTTP | MDN
权限分类
- Authority,权限
- Role,角色,===>>>权限,加前缀:ROLE_
自定义登录页面
使用HttpSecurity
对象配置。
- 当需要认证时转向的登录页:
.loginPage("/login")
- 视图控制器,定义login请求对应的视图:
registry.addViewController("/login")
; - 登录的post请求由Spring Security自动处理,名称默认:
username
、password
,可配置
1 | formLogin() |
实现方法级别的安全
1 |
|
获取当前登录的用户
- 注入
Principal
对象- 来自
java.security
,是JDK中JASS的低层框架 String username = principal.getName()
获取用户名
- 来自
@AuthenticationPrincipal
注解- 来自
Spring Security
@AuthenticationPrincipal User user
作为函数参数获得user对象
- 来自
-
DesignTacoController 参数:
1
Principal principal String username = principal.getName();
-
OrderController
1
User user
-
安全上下文获取
1
2Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user = (User) authentication.getPrincipal();
第二种方式的代码:OrderController
1 | package tacos.web; |
CSRF攻击
- 跨站请求伪造
- 攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。
==出现报错 403 + forbidden的原因:缺少_csrf==
-
例子:本段借鉴charlie老师的博客内容
- 受害者登录a.com,并保留了登录凭证(Cookie)。
- 攻击者引诱受害者访问了b.com。
- b.com 向 a.com 发送了一个请求:a.com/act=xx。浏览器会默认携带a.com的Cookie。
- a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求。
- a.com以受害者的名义执行了act=xx。
- 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作。
-
解决:C每次提交表单A,_csrf 字段有唯一ID,无法伪造
- get得到
_csrf
, post请求携带_csrf
,防止第三方伪造 - ==不在cookie中==
- get得到
更改日志的等级
application.properties
1 | logging.level.root=debug |