Spring in action. 4 保护Spring

174 阅读7分钟

启用Spring Security

  • 添加依赖
    • spring-boot-starter-security
  • 只要添加了该依赖,自动配置功能就会探测到Spring Security出现在了类路径中,因此它就会初始化一些基本的安全配置
    • 所有的HTTp请求路径都需要认证;
    • 不需要特定的角色和权限;
    • 没有登陆页面;
    • 认证过程通过HTTP basic认证对话框实现;
    • 系统只有一个用户,用户名为user。
  • 目标配置
    • 通过登陆页面来提示用户进行认证,而不是使用HTTP basic对话框;
    • 提供多个用户,并提供一个注册页面;
    • 对不同的请求路径,执行不同的安全规则,举例来说,主页和注册页面根本不需要进行认证。

配置Spring Security

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
}

Spring Security为配置用户存储提供了多个可选方案:

  • 基于内存的用户存储;
  • 基于JDBC的用户存储;
  • 以LDAP作为后端的用户存储;
  • 自定义用户详情服务。

不管使用哪种,都可以通过覆盖 WebSecurityConfigurerAdapter基础配置类中定义的configure()方法来进行配置。

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    super.configure(auth);
}

基于内存的用户存储

@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的用户存储

@Autowired
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=?"
            );
}
  • 上述的认证查询,它预期使用了明文密码存储,这样是有风险的。下面我们需要借助passwordEncoder()方法指定一个密码转码器(encoder);
    • .passwordEncoder(new StandardPasswordEncoder("53cr3t");
  • passwordEncoder()方法可以接受Spring Security中PasswordEncoder接口的任意实现。Spring Security的加密模块包括了多个这样的实现:
    • BCryptPasswordEncoder:使用bcrypt强哈希加密。
    • NoOpPasswordEncoder:不进行任何转码。
    • Pbkdf2PasswordEncoder:使用PBKDF2进行加密。
    • SCryptPasswordEncoder:使用scrypt哈希加密。
    • StandardPasswordEncoder:使用SHA-256哈希加密。
  • 不管使用哪一个密码转码器,都需要理解一点,即数据库中的密码是永远不会解码的。
    • 用户在登录时所采取的策略与之相反,输入的密码会按照相同的算法进行转码,然后与数据库中已经转码过的密码进行对比。(这个对比是在PasswordEncoder的matches()方法中进行的。

以LDAP作为后端的用户存储

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth
            .ldapAuthentication()
            .userSearchBase("ou=people")
            .userSearchFilter("(uid={0})")
            .groupSearchBase("ou=groups")
            .groupSearchFilter("member={0}");
}
  • userSearchFilter()和groupSearchFilter():为基础LDAP查询提供过滤条件,分别用于搜索用户和组。

  • user/groupSearchBase():为查找用户/组提供了基础查询

    • 参数ou=people/groups:声明了用户/组应该在名为people/groups的组织单元下开始搜索。
  • 密码策略:

    • .passwordCompare():
    • .passwordEncoder(new BCryptPasswordEncoder())
    • .passwordAttribute("passcode")

...

自定义用户认证

@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;

  @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;
  }

}
  • UserDetails:通过实现该接口,我们能够提供能多信息给框架,比如用户都被赋予了哪些权限以及用户的账号是否可用。
  • is...Expired():各种方法都会返回一个boolean值,表明用户的账号是否可用或过期。
  • getAuthorities():该方法返回用户被授予权限的一个集合。
    • 对于User来说,getAuthorities()方法只是简单的返回了一个集合,这个集合表明所有的用户都被授予了ROLE_USER权限
public interface UserRepository extends CrudRepository<User,Long> {

    User findByUsername(String username);
    
}

创建用户详情服务

public interface UserDetailsService{
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
  • UserDetailsService:该接口是Spring Security提供的一个相当简单直接的接口
    • 该接口的实现会得到一个用户的用户名,并且要么返回查找到的UserDetails对象,要么在根据用户名无法得到任何结果的情况下返回UsernameNotFoundException。
@Service
public class UserRepositoryUserDetailsService implements UserDetailsService {
    
    private UserRepository userRepo;

    @Autowired
    public UserRepositoryUserDetailsService(UserRepository userRepo) {
        this.userRepo = userRepo;
    }
    
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        User user = userRepo.findByUsername(s);
        if (user != null) {
            return user;
        }
        throw new UsernameNotFoundException("User '" + s + "'' not found");
    }
}
  • UserRepository通过构造器被注入
  • loadByUsername()方法有一个简单的规则:它绝不能返回null
  • @Service:这是Spring的另一个构造型(stereotype)注解,它表明这个类要包含到Spring的组件扫描中

回到configure()方法

@Autowired
private UserDetailsService userDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth
            .userDetailsService(userDetailsService)
            .passwordEncoder(encoder());

}

@Bean
public PasswordEncoder encoder() {
    return new StandardPasswordEncoder("53cr3t");
}
  • 这里,我们只是简单地调用userDetailService()方法,并将自动装配到SecurityConfig中的UserDetailsService实例传递了进去。

注册用户

@Controller
@RequestMapping("/register")
public class RegistrationController {
    
    private UserRepository userRepo;
    private PasswordEncoder passwordEncoder;

    public RegistrationController(UserRepository userRepo, PasswordEncoder passwordEncoder) {
        this.userRepo = userRepo;
        this.passwordEncoder = passwordEncoder;
    }
    
    @GetMapping
    public String registerForm(){
        return "registration";
    }
    
    @PostMapping
    public String processRegistration(RegistrationForm form){
        userRepo.save(form.toUser(passwordEncoder));
        return "redirect:/login";
    }
}

保护Web请求

为了配置一些安全性规则,下面介绍一个WebSecurityConfigurerAdapter的其他configure()方法。

@Override
protected void configure(HttpSecurity http) throws Exception{
...
}

configure()方法接受一个HttpSecurity对象,能够用来配置在web级别该如何处理安全性。我们可以使用HttpSecurity配置的功能包括

  • 在为某个请求提供服务之前,需要预先满足特定的条件;
  • 配置自定义的登陆页;
  • 支持用户退出应用;
  • 预防跨站请求伪造。

保护请求

我们需要确保只有认证过的用户才能发起对“/design”和“/orders”对请求,而其他请求对其他用户均可用。如下配置即可

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .authorizeRequests()
            .antMatchers("/design", "/orders")
            .hasRole("ROLE_USER")
            .antMatchers("/", "/**")
            .permitAll();
}
  • 定义了两条规则
    • 具备ROLE_USER权限的用户才能访问“/design”和“/orders”
    • 其他的请求允许所有用户访问
  • 声明在前面的安全规则优先级更高
  • 一些其他的方法
方法能够做什么
access(String)如果给定的SqlEl表达式计算结果为true,就允许反问
anonymous()允许匿名用户访问
authenticated()允许认证过的用户访问
denyAll()无条件拒绝所有访问
fullyAuthenticated()如果用户是完整认证的,就允许访问
hasAnyAuthority(String...)如果用户具备给定权限中的某一个,就允许访问
hasAnyRole(String...)如果用户具备给定权限中的某一个,就允许访问
hasAuthority(String)如果用户具备给定权限,就允许访问
hasIpAddress(String)如果请求来自给定IP地址,就允许访问
hasRole(String)如果用户具备给定角色,就允许访问
not()对其他访问方法的结果求反
permitAll()无条件允许访问
rememberMe()如果用户是通过Remember-me功能认证的,就允许访问

创建自定义的登陆页

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .authorizeRequests()
            .antMatchers("/design", "/orders")
            .access("hasRole('ROLE_USER')")
            .antMatchers("/", "/**")
            .access("permitAll")

            .and()
            .formLogin()
            .loginPage("/login")

            ;
}
  • 在configure里配置formLogin来指明路径,之后可以为“/login”路径addViewController
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/authenticate")
.usernameParameter("user")
.passwordParameter("pwd")
.defaultSuccessUrl("/design",true)
  • 这里,声明Spring Security要监听对“/authenticate”对请求来处理登录信息的提交
  • 同时,用户名和密码的字段名应该是user和pwd
  • defaultSuccessUrl("/design",true)
    • 默认情况下,登录成功后会被定向至/design
    • 第二个参数true意味:强制用户在登录成功之后统一访问design页面,即使用户在登录之前正在访问其他页面,在登录之后也会被定向到design。

退出

.and()
.logout()
.logoutSuccessUrl("/")
  • 该配置会搭建一个安全过滤器,该过滤器会拦截对“/logout”的请求。
  • 当请求/logout时,它们的session会被清理掉,这样它们就能退出应用。
  • logoutSuccessUrl可以指定推出后的不同页面。

防止跨站请求伪造

了解用户是谁

@Data
@Entity
@Table(name="Taco_Order")
public class Order implements Serializeble{
...
    @ManyToOne
    private User user;
...

}
  • ManyToOne:表示用户和订单一对多的关系

我们有多种方式来确定用户是谁

  • 注入Principal对象到控制器方法中;
    • principal.getName()
  • 注入Authentication对象到控制器方法中;
    • (User)authentication.getPrincipal()
  • 使用SecurityContextHolder来获取上下文;
  • 使用@AuthenticationPrincipal注解来标注方法。
    • @AuthenticationPrincipal User user

小结

  • Spring Security的自动配置是实现基本安全性功能的好办法,但是大多数应用都需要显式的安全配置,这样才能够满足特定的安全需求。
  • 用户详情可以通过用户存储进行管理,它的后端可以是关系型数据库、LDAP或完全自定义实现。
  • Spring Security会自动防范CSRF攻击。
  • 已认证用户的信息可以通过SecurityContext对象(该对象可由SecurityContextHolder.getContext()返回)来获取,也可以借助@AuthenticationPrincipal注解将其注入到控制器中。