Spring Security
开始使用Spring Security
加入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
自动探测Spring Security在classpath中
访问任何页面,将提示输入用户名和密码,默认用户名:user ,密码在启动日志中
不做任何操作我们得到了下列安全特性
- 所有HTTP请求路径都需要验证
- 不需要说明角色和权限
- 一个简单的登录页面
- 基础的HTTP请求验证
- 有一个user用户
配置用户仓库
Java-based配置的基本骨架:
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
需要@EnableWebSecurity
注解,并继承WebSecurityConfigurerAdapter
无论基于那种用户仓库,我们都可以通过覆盖configure()
方法来选择用户仓库。
in-memory
适用于如果只有少数用户,且这些用户不需要被修改。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("lrz").password("{bcrypt}$2a$10$6PFq.48e0kB44d2SCY8HoeIacJTAsm0SJQJWtRAPgZJJnY8WYdcqa").authorities("ROLE_USER")
.and()
.withUser("lrz2").password("{noop}abc123").authorities("ROLE_USER");
}
}
在Spring Security 5.0之前,默认的密码编码器是NoOpPasswordEncoder
,在5.0之后提供了更多的编码器,并要求我们明确的说明编码器。
使用{encodeid}password
格式
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
{noop}password
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
JDBC-based
将用户存储在数据库中
DataSource dataSource;
@Autowired
public SecurityConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication().dataSource(dataSource).passwordEncoder(NoOpPasswordEncoder.getInstance());
}
在验证用户时,SpringSecurity会自动查询下面的Sql:
--验证用户
select username,password,enabled from users where username=?
--验证用户权限
select username, authority from authorities where username=?
--查询用户的用户组
select g.id, g.group_name,ga.authority from groups g, group_members gm, group_authorities ga where gm.username=? and g.id = ga.group_id and g.id = gm.gourp_id
一个简单的表构造语句及数据初始化
create table if not exists users(
username varchar(24) not null,
password varchar(128) not null,
enabled int default 1
);
create table if not exists AUTHORITIES(
username varchar(24) not null,
authority varchar(24) not null
);
alter table AUTHORITIES
add foreign key (username) references users(username);
create table if not exists groups(
id identity PRIMARY KEY,
group_name varchar(24) not null
);
create table if not exists group_member(
id int not null,
username varchar(24) not null
);
alter table group_member
add foreign key (username) references users(username);
create table if not exists group_authorities(
authority varchar(24) not null,
id int not null
);
alter table group_authorities
add foreign key (id) references groups(id);
insert into users (username,password,enabled) values('user','password',1);
insert into AUTHORITIES(username,authority) values('user','ROLE_USER');
如果我们有自己的表结构,也可以自己配置查询语句:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication().dataSource(dataSource)
.usersByUsernameQuery("select name,pwd,enabled from person where name = ?")
.authoritiesByUsernameQuery("select pname,name from auth where pname = ?")
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
在自定义sql中,字段的顺序要与默认sql对应,字段名称不重要。
我们还需要为密码指定编码器,Spring Security提供的编码器有:
- BCryptPasswordEncoder
- Pbkdf2PasswordEncoder
- SCryptPasswordEncoder
- StandardPasswordEncoder(已不推荐使用)
- NoOpPasswordEncoder(已不推荐使用)
数据库中存放的密码应该是被编码后的字符串,且该字符串不应该被解码,Spring Security会将我们输入的密码使用指定的编码器编码后与数据库中存放的密码进行比较。
LDAP-base
略。。。
自定义用户服务
创建User
实体并实现org.springframework.security.core.userdetails.UserDetails
接口
创建UserRepository
并定义按名字查找用户的方法(假设使用了JPA)。
创建UserService
类并继承UserDetailsService
接口实现其LoadUserByUserName
方法,该方法实现按名超找用户。
将UserDetailsService
注入配置类,并配置:
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
保护Web请求
我们需要覆盖WebSecurityConfigurerAdapter
的另一个configure
方法,用于配置授权用户的Web请求。
@Override
protected void configure(HttpSecurity http) throws Exception {
}
这个方法为我们提供了一个HttpSecurity
对象,通过配置这个对象,我们可以实现:
- 应用只处理符合要求(安全条件)的请求
- 配置一个自定义登录页面
- 允许用户登出
- 配置保护跨站请求伪造(cross-site request forgery)
访问控制
我们可以配置请求规则,使得应用只响应具有相关权限的用户的请求。
http.authorizeRequests()
.antMatchers("/design","/orders").hasRole("USER")
.antMatchers("/","/**").permitAll();
上面的配置使得只有具有ROLE_USER
权限的用户可以访问/design和/orders,所有的用户都可以访问除上面URL的其它所有URL。每个请求都先匹配第一个规则,如果匹配不上,再匹配第二个,第三个…,所以安全规则的顺序十分的重要。
基本访问控制表达式
Expression | Description |
| Returns For example, By default if the supplied role does not start with 'ROLE_' it will be added. This can be customized by modifying the |
| Returns For example, By default if the supplied role does not start with 'ROLE_' it will be added. This can be customized by modifying the |
| Returns For example, |
| Returns For example, |
| Allows direct access to the principal object representing the current user |
| Allows direct access to the current |
| Always evaluates to |
| Always evaluates to |
| Returns |
| Returns |
| Returns |
| Returns |
| Returns |
| Returns |
基于SpEL(Spring Expression Language)的访问控制
我们可以只使用access()
方法,并向该方法传递SpEl表达式来达到更加细粒度的访问控制
Spring Security扩展的SpEL表达式:
前面我们的访问控制,使用SpEL的实现:
http.authorizeRequests()
.antMatchers("/design","/orders").access("haseRole('USER')")
.antMatchers("/","/**").access("permitAll");
配置自定义登录页面
http.formLogin().loginPage("/login");
loginPage
方法中传入的是请求登录页面的路径,我们需要处理这样的请求(创建Controller,或者是使用View控制器)。然后我们需要创建自己的登录页面
<form th:action="@{/login}" method="post">
...
</form>
要注意的是使用post
进行表单提交。SpringSecurity默认在/login
监听登录请求,且用户名和密码字段名为username
,password
,我们可以对其进行配置:
http.formLogin().loginPage("/login")
.loginProcessingUrl("/aauthenticate")
.usernamePaaarameter("user")
.passwordParaameter("pwd")
这样,Spring Security将在/authenticate监听登录,且用户名,密码字段名称为user
和pwd
。
默认情况下,登录成功后,页面会跳转到原先用户想要请求的页面。我们也可以配置登录成功后的默认跳转路径:
http.formLogin().loginPage("/login")
.loginProcessingUrl("/aauthenticate")
.usernamePaaarameter("user")
.passwordParaameter("pwd")
.defaultSeccessUrl("/design")
通过上面的配置,如果用户直接访问的登录页面,那么用户登录成功后会重定向到/design。还可以defaultSuccessUrl
指定第二个入参为true
,这样,无论用户在登录前请求的哪个页面,在登录成功后,都会被重定向到/design。
登出
直接以Post
方式请求/logou便可以登出。默认登出后重定向到登录页面,我们也可以配置登出后的默认跳转页面
http.logout().logoutSuccessUrl("/")
防护跨站请求伪造
Spring Security内置了CSRF防护,我们只需要在每一个Post请求提交的数据中增加_csrf
字段即可。如果使用的是Thymeleaf:
<input type="hidden" name="_csrf" th:value="${_csrf.token}"/>
如果是使用Thymeleaf模板或者是SpringMVC的JSP tag包, 我们甚至不需要显示的声明_csrf
,隐藏的_csrf
字段会自动添加到form表单中。如果使用Thymeleaf,我们只需要保证<form>
元素中有一个属性是Thymeleaf属性即可。如:
<form th:action="@{/login}" method="post">
...
</form>
我们也可以关闭对CSRF的防护:
http.csrf().disable();
获取登录用户
我们有一下几种方式来确定当前啊用户:
- 注入
Principal
对象到Controller方法中 - 注入
Authentication
对象到Controller方法中 - 在security上下文中使用
SecurityContextHolder
- 使用被
@AuthenticationPrincipal
注解的方法
Principal
对象
@PostMapping
public String processOrder(@Valid Order order, Errors errors, SessionStatus sessionStatus, Principal principal){
...
User user = userRepository.findByUserName(principal.getName());
Orser.setUser(user);
}
Authentication
对象
@PostMapping
public String processOrder(@Valid Order order, Errors errors, SessionStatus sessionStatus, Authentication authentication){
...
User user = (User)authentication.getPrincipal();//getPrincipal返回的是Object类型的对象,需要强制类型转换
Orser.setUser(user);
}
@AuthenticationPrincipal
@PostMapping
public String processOrder(@Valid Order order, Errors errors, SessionStatus sessionStatus, @AuthenticationPrincipal User user){
...
Orser.setUser(user);
}
SecurityContextHolder
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user = (User)authentication.getPrincipal();
使用这种方式虽然比较繁琐且与Spring Security耦合,但是我们可以在应用的任何地方使用,不仅仅局限于Controller中。