Spring Security

开始使用Spring Security

加入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

自动探测Spring Security在classpath中

访问任何页面,将提示输入用户名和密码,默认用户名:user ,密码在启动日志中

spring security按钮权限 spring security in action_ci

不做任何操作我们得到了下列安全特性

  • 所有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对象,通过配置这个对象,我们可以实现:

  1. 应用只处理符合要求(安全条件)的请求
  2. 配置一个自定义登录页面
  3. 允许用户登出
  4. 配置保护跨站请求伪造(cross-site request forgery)

访问控制

我们可以配置请求规则,使得应用只响应具有相关权限的用户的请求。

http.authorizeRequests()
	.antMatchers("/design","/orders").hasRole("USER")
	.antMatchers("/","/**").permitAll();

上面的配置使得只有具有ROLE_USER权限的用户可以访问/design和/orders,所有的用户都可以访问除上面URL的其它所有URL。每个请求都先匹配第一个规则,如果匹配不上,再匹配第二个,第三个…,所以安全规则的顺序十分的重要。

基本访问控制表达式

Expression

Description

hasRole(String role)

Returns true if the current principal has the specified role.

For example, hasRole('admin')

By default if the supplied role does not start with 'ROLE_' it will be added. This can be customized by modifying the defaultRolePrefix on DefaultWebSecurityExpressionHandler.

hasAnyRole(String… roles)

Returns true if the current principal has any of the supplied roles (given as a comma-separated list of strings).

For example, hasAnyRole('admin', 'user')

By default if the supplied role does not start with 'ROLE_' it will be added. This can be customized by modifying the defaultRolePrefix on DefaultWebSecurityExpressionHandler.

hasAuthority(String authority)

Returns true if the current principal has the specified authority.

For example, hasAuthority('read')

hasAnyAuthority(String… authorities)

Returns true if the current principal has any of the supplied authorities (given as a comma-separated list of strings)

For example, hasAnyAuthority('read', 'write')

principal

Allows direct access to the principal object representing the current user

authentication

Allows direct access to the current Authentication object obtained from the SecurityContext

permitAll

Always evaluates to true

denyAll

Always evaluates to false

isAnonymous()

Returns true if the current principal is an anonymous user

isRememberMe()

Returns true if the current principal is a remember-me user

isAuthenticated()

Returns true if the user is not anonymous

isFullyAuthenticated()

Returns true if the user is not an anonymous or a remember-me user

hasPermission(Object target, Object permission)

Returns true if the user has access to the provided target for the given permission. For example, hasPermission(domainObject, 'read')

hasPermission(Object targetId, String targetType, Object permission)

Returns true if the user has access to the provided target for the given permission. For example, hasPermission(1, 'com.example.domain.Message', 'read')

基于SpEL(Spring Expression Language)的访问控制

我们可以只使用access()方法,并向该方法传递SpEl表达式来达到更加细粒度的访问控制

Spring Security扩展的SpEL表达式:

spring security按钮权限 spring security in action_ide_02

前面我们的访问控制,使用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监听登录,且用户名,密码字段名称为userpwd

默认情况下,登录成功后,页面会跳转到原先用户想要请求的页面。我们也可以配置登录成功后的默认跳转路径:

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();

获取登录用户

我们有一下几种方式来确定当前啊用户:

  1. 注入Principal对象到Controller方法中
  2. 注入Authentication对象到Controller方法中
  3. 在security上下文中使用SecurityContextHolder
  4. 使用被@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中。