Spring Security

本文详细介绍了如何启用和配置Spring Security,包括基于内存、JDBC和LDAP的用户存储,以及自定义用户认证。通过添加Spring Boot security starter依赖,应用会自动创建一个登录页面并提供基础的安全特性。Spring Security提供了多种用户存储方式,如内存、JDBC和LDAP,并允许自定义用户详情服务。文章还展示了如何保护Web请求、创建自定义登录页面、防止跨站请求伪造,并提供了识别当前认证用户的方法。
摘要由CSDN通过智能技术生成

启用Spring Security

保护Spring应用的第一步就是将Spring Boot security starter依赖添加到构建文件中。

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

只要添加了该依赖,当应用启动的时候,自动配置功能会探测到Spring Security出现在了类路径中,因此它就会初始化一些安全配置。这时,你可以尝试启动应用访问主页,应用会展示一个登录页面并提示你进行认证。用户名为user,密码会被写出应用的日志文件中。日志条目大致如下所示:

Using generated security password: 893043b6-a647-4f21-8787-70d25c6151a1

假设输入了正确的用户名和密码,你就有权限访问应用了。

通过将security 起步依赖添加到项目的构建文件中,我们得到了以下安全特性:

  • 所有的HTTP请求路径都需要认证
  • 不需要特定的角色和权限
  • 系统只有一个用户,用户名为user

我们需要:

  • 提供多个用户,并提供一个注册页面
    -对不同的请求路径,执行不同的安全规则

配置Spring Security

基础安全配置

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{

}

配置用户储存

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

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

不管使用那种用户存储,都可以通过覆盖WebSecurityConfigureAdapter基础配置类中定义的configure()方法进行配置。首先,我们可以将如下的方法添加到SecurityConfig类中:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        ...
    }
}

现在,我们需要使用指定的AuthenticationManagerBuilder替换上面的省略号。以此来定义在认证过程中如何查找用户。

基于用户内存的用户存储

用户信息可以存储在内存之中。假设我们只有数量有限的几个用户,而且这些用户几乎不会发生改变,将这些用户定义成安全配置的一部分是非常简单的。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{


     /*基于内存的用户存储*/

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("buzz")
                .password(passwordEncoder().encode("123"))
                .authorities("ROLE_USER");
    }

}

AuthenticationManagerBuilder使用构造者(builder)风格的接口来构建认证环节。我们调用inMemoryAuthentication方法来指定用户信息。通过withUser()方法来配置用户而密码就是password,用户权限就是authorities方法。

需要注意的是,我们需要定义一个密码编码器,能够用正确方式解析密码。

基于JDBC的用户存储

用户信息通常会在关系型数据库中进行维护,基于JDBC的用户存储方案会更加合理一些。

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication()
                .dataSource(dataSource)
                .usersByUsernameQuery(
                        "select username,password,enabled from Users "
                        + "where username = ?"
                )
                .passwordEncoder(passwordEncoder());
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
    DataSource dataSource;

    @Autowired(required = false)
    public SecurityConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

在这里的configure实现中,调用了AuthenticationManagerBuilder的jdbcAuthentication方法,我们还必须设置一个DataSource,这样它才能知道如何访问数据库。这里的DataSource是通过自动装配的技巧获得的。

Spring Security内部源码,展现了当查找用户时所执行的SQL查询语句:

public static final String DEF_USERS_BY_USERNAME_QUERY =
  "select username,password,enabled " +
  "from users " +
  "where username = ?";

public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY =
  "select username,authority " +
  "from authorities " +
  "where username = ?";

public static final String DEF_GROUP_BY_USERNAME_QUERY =
  "select g.id,g.goup_name,ga.authority " +
  "from groups g,goup_members gm,group_authorities ga " +
  "where username = ?" +
  "and g.id = ga.group_id" +
  "and g.id = gm.group_id";

在第一个查询中,我们获取了用户的用户名,密码以及是否启用的信息,用来进行用户认证。接下来的查询查找了用户所授予的权限。

如果你能够在数据库中定义和填充满徐这些查询的表,那么基本上就不需要在做什么。你可能会希望在查询上获得更多的控制权,你可以自定义用户查询。

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication()
                .dataSource(dataSource)
                .usersByUsernameQuery(
                        "select username,password,enabled from Users "
                        + "where username = ?"
                )
                .passwordEncoder(passwordEncoder());
    }

这里只写了认证的查询语句。重要的是认证和授予权限必须具备,否则无法实现。所以群组认证非必须。

将默认的SQL查询替换成自定义的设计时,很重要的一点是要遵循查询的基本协议。所有查询都将用户名作为唯一的参数。认证查询会选取用户名,密码,以及启用权限。权限查询会选取零行或多行包含该用户名及其权限信息的数据。群组权限会选取零行或多行数据,每行数据都会包含群组ID,群组名称以及权限。

为了解决明文密码储存的问题,我们需要借助以下passwordEncoder方法指定一个密码转码器:

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

passwordEncoder方法可以接受SpringSecurity中PasswordEncoder接口的任意实现:

  • BCryptPasswordEncoder:使用bcrypt强哈希加密
  • NoOpPasswordEncoder:不进行任何转码
  • Pbkdf2PassWordEncoder:使用PBKDF2加密
  • SCryptPasswordEncoder:使用scrypt哈希加密
  • StandardPasswordEncoder:使用SHA-256加密

上述代码中使用了NoOpPasswordEncoder,已被弃用,仅在开发中使用。

如果内置的加密方法无法满足要求,可以提供自定义的实现,PasswordEncoder接口:

public interface PasswordEncoder {
    String encoder(CharSequence rawPassword);
    boolean matches(CharSequence rawPassword,String encodedPassword);
}

无论使用哪一种密码转码器,数据库中密码是永远不会解码的。

用户在登陆时所采取的策略相反,输入的密码按照相同的算法进行转码,然后与数据库中已经转码过的密码进行比较,这个对比是在passwordEncoder的matches方法中进行的。

以LDAP作为后端的用户存储

自定义用户认证

使用Spring Data repository来存储用户。在此之前我们首先要创建领域对象。

定义用户领域对象和持久化

为了捕获用户信息,我们需要创建User类:

@Data
@Entity
@NoArgsConstructor(access = AccessLevel.PRIVATE,force = true)
@RequiredArgsConstructor
public class User implements UserDetails {

    private static final long serialVersionID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    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 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;
    }
}

除了定义了一些属性之外,User类还实现了Spring Security的UsersDetails接口。
通过实现UsersDetails接口,我们能够提供更多信息给框架,比如用户被授予了哪些权限,以及用户的账号是否可用。

getAuthorities方法应该返回用户被授予权限的一个集合。各种is…Expired方法要返回一个Boolean值,表明用户的账号是否可用或过期。

User实体定义完成之后,我们就可以定义repository接口了:

public interface UserRepository extends CrudRepository<User,Long> {
    User findByUsername(String username);
}

我们额外定义了一个findByUsername方法以便于再用户详情服务中用到,根据用户名查找User。

创建用户详情服务

public interface UserDetailsService extends org.springframework.security.core.userdetails.UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

记住,该接口需要继承UserDetailsService接口

正如我们所看到的,这个接口的实现会得到一个用户的用户名。

因为我们的User类实现了UserDetails接口,并且UserRepository提供了findByUsername方法,所以它们非常适合用在UserDetailsService实现中。

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private UserRepository userRepository;
    
    @Autowired
    public UserDetailsServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);
        if(user != null) {
            return user;
        } else {
            throw new UsernameNotFoundException(
                    "User " + username + "not found"
            );
        }
    }
}

我们注意到UserDetailsServiceImpl上添加了@Service。这是Spring的另外一个构造性注解,它表明这个类要包含到Spring的组件扫描当中。Spring将会自动发现它并将它初始化为一个bean。

我们需要将这个自定义的用户详情服务与Spring Security配置在一起。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{



    DataSource dataSource;
    UserDetailsService userDetailsService;

    @Autowired(required = false)
    public SecurityConfig(DataSource dataSource,UserDetailsService userDetailsService) {
        this.dataSource = dataSource;
        this.userDetailsService = userDetailsService;
    }



    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    /*基于自定义的用户存储*/
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder());
    }
}

用户注册控制器

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

    public RegistrationController(UserRepository repository, PasswordEncoder passwordEncoder) {
        this.userRepository = repository;
        this.passwordEncoder = passwordEncoder;
    }

    @PostMapping
    public String processRegistration(RegistrationForm registrationForm) {
        userRepository.save(registrationForm.toUser(passwordEncoder));
        return "test";
    }
}
@Data
public class RegistrationForm {
    private final String username;
    private final String password;

    public User toUser(PasswordEncoder passwordEncoder) {
        return new User(username,passwordEncoder.encode(password));
    }
}

保护Web请求

因为默认情况下,所有请求都需要认证,但是我们应该让所有用户都能够访问注册以及登录页面。为了配置这些安全性规则,需要用到其他的configure方法。

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

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

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

配置HttpSecurity常见的需求就是拦截请求以确保用户具备适当的权限。


我们需要确保只有认证过的用户才能发起对"/taco"和"/test"的请求,而其他请求对所有均可用:

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/taco","/test")
                .hasRole("ADMIN")
                .antMatchers("/","/**").permitAll();

    }

对authorizeRequests的调用会返回一个对象(ExpressionInterceptUrlRegistry),基于它我们可以指定URL路径和这些路径的安全需求。

需要注意的是在定义User实体类时,我们给予的权限为”ROLE_ADMIN”,因为在configure中”ADMIN“会自动加上”ROLE_“前缀。

这些规则的顺序是很重要的,声明在前面的安全规则比后面声明的规则有更高的优先级。如果我们交换这两个安全规则的顺序,那么所有的请求都会有permitAll的规则,对"taco","test"声明的规则就不会生效了。

声明请求路径的安全需求的所有可用方法

方法能够做什么
access(String)如果给定的SpEL表达式计算结果为true,就允许访问
anonymous()允许匿名用户访问
authenticated()允许认证过的用户访问
denyAll()无条件拒绝所有访问
fullyAuthenticated()如果用户是完整认证的(不是通过Remember-me功能认证的),就允许访问
hasAnyAuthority(String…)如果用户具备给定权限中的某一个,就允许访问
hasAnyRole(String…)如果用户具备给定角色中的某一个,就允许访问
hasAuthority(String)如果用户具备给定权限,就允许访问
hasIpAddress(String)如果请求来自给定IP地址,就允许访问
hasRole(String)如果用户具备给定角色,就允许访问
not()对其他访问方法的结果求反
permitAll()无条件允许访问
remenberMe)()如果用户是通过remember-me功能认证的,就允许访问

我们还可以使用access方法,通过为其提供SpEL表达式来声明更丰富的安全规则。Spring Security拓展了SpEL,包含了多个安全相关的值和函数

安全表达式计算结果
authentication用户的认证对象
denyAll结果始终为false
hasAnyRole(list of roles)如果用户被授予了列表中任意的指定角色,结果为true
hasRole(role)如果用户被授予了指定的角色,结果为true
hasIpAddress(IP Address)如果请求来自指定IP,结果为true
isAnonymous()如果当前用户为匿名用户,结果为true
isAuthenticated()如果当前用户进行了认证,结果为true
isFullyAuthenticated()如果当前用户进行了完整认证(不是通过Remember-me功能进行的认证),结果为true
isRememberMe()如果当前用户是通过Remember-me自动认证的,结果为true
permitAll结果始终为true
principal用户的principal对象

下面为上面配置HttpSecurity的SpEL版:

@Overrride
protected void configure(HttpSecurity http) throws Exception {
    http
      .authorizeRequests()
        .antMatchers("/taco","/test")
           .access("hasRole('ADMIN')")
        .antMatchers("/","/**").access("permitAll");
}

我们可以使用SpEL实现各种各样的安全性限制。

创建自定义的登录页

为了替换内置的登录页,我们需要告诉Spring Security自定义登录页的路径是什么。这可以通过调用传入到configure()中的HttpSecurity对象的formLogin方法啊来实现:

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/taco","/test")
                .hasRole("ADMIN")
                .antMatchers("/","/**").permitAll()

                .and()
                .formLogin()
                .loginPage("/login")
                .permitAll()
                .defaultSuccessUrl("/taco")

                .and()
                .logout()
                .permitAll();
    }

在调用formlogin之前,我们通过and方法将这一部分的配置与前面的配置连接在一起。and方法表示我们已经完成了授权相关的配置,并且要添加一些其他的HTTP配置,我们可以多次调用and方法。

默认情况下,Spring Security会在”/login“路径监听登录请求并且预期的用户名和密码输入域的名称为username和password。但这都是可配置的,举例来说,如下配置了自定义的路径以及输入域的名称:

.and()
                .formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/authenticate")
                .usernameParameter("user")
                .passwordParameter("pwd")

在这里,我们声明Spring Security要监听”/authenticate“的请求来处理登录信息的提交。同时用户名和密码的字段应该是user和pwd。

默认情况下,登陆成功后,用户将会被导航到Spring Security决定让用户登陆之前的页面。如果用户直接访问登录页,那么登录成功之后用户将会被导航到根路径,但是我们可以指定默认的成功页也来更改这种行为:

and()
                .formLogin()
                .permitAll()
                .defaultSuccessUrl("/taco")

如果我们想要强制要求用户登陆之后统一访问某页面,即使用户登录之前正在访问其他页面。我们可以使用defaultSuccessUrl的第二个参数true来实现:

and()
                .formLogin()
                .permitAll()
                .defaultSuccessUrl("/taco", true)

退出

.and()
                .logout()
                .permitAll()
                .logoutSuccessUrl("/")

启用退出功能,这样会搭建一个安全过滤器,该过滤器会拦截对”、logout“的请求。当用户退出的时候,他们的session将会被清理。

防止跨站请求伪造

跨站请求伪造(Cross-Site Request Forgery,CSRF)是一种常见的安全攻击,它会让用户在一个恶意的Web页面填写信息,然后自动(通常是秘密的)将表单以攻击受害者的身份提交到另外一个应用上。

为了防止这种类型的攻击,应用可以在展现表单的时候生成一个CSRF token,并放到隐藏域中,然后将其临时存储起来,以便后续在服务器上使用。在提交表单的时候,token将和其他的表单数据一起发送至服务器端。请求会被服务器拦截,并与最初生成的token对比,如果token匹配,那么请求将会允许处理;否则,表单肯定是由恶意网站渲染的,因为它不知道服务器生成的token。

比较幸运的是,Spring Security提供了内置的CSRF保护。更幸运的是,默认它就是启用的,我们不需要显式配置它,我们唯一需要做的就是确保应用中的每个表单都要有一个名为 “_csrf” 字段它会持有服务器所生成的token.

我们可以禁用

.and()
                .csrf()
                .disable();

了解用户是谁

我们有多种方式确定用户是谁,常用的方式如下:

  • 注入Principal对象到控制器方法中,
  • 注入Authentication对象到控制器方法中
  • 使用SecurityContextHolder来获取安全上下文
  • 使用@AuthenticationPrincipal注解来标注方法

我们可以接受一个Principal参数就能够从UserRepository中查找用户:

UserRepository userRepository;

    public GetPrincipalController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @GetMapping
    public String Order(Principal principal) {
        return userRepository.findByUsername(principal.getName()).toString();
    }

但这个在安全无关的代码中参杂安全性的代码,是不推荐的。我们可以让它不再接收Principal参数,而是接受Authentication对象作为参数:

UserRepository userRepository;

    public GetPrincipalController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @GetMapping
    public String Order(Authentication authentication) {
        User user = (User) authentication.getPrincipal();
        return user.toString();
    }

有了Authentication对象之后,我们就可以通过调用getPrincipal来获取principal对象,需要注意的是getPrincipal返回的是java.util.Object,所以我们需要将他转换成User。

最整洁的方案就是直接接受一个User对象,不过我们需要为其添加@AuthenticationPrincipal注解,这样它才会变成认证的principal:

@GetMapping
    public String Order(@AuthenticationPrincipal User user) {
        return user.toString();
    }

@AuthenticationPrincipal非常好的一点就是它不需要类型转换,同时能够将安全相关的怠慢仅限于注解本身。

还有一种方法能够识别当前的认证用户是谁,但是这种方式有点麻烦,包含大量安全性相关的代码:
我们可以从安全上下文中获取一个Authentication对象:

@GetMapping
    public String Order() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        User user = (User) authentication.getPrincipal();
        return user.toString();
    }

但是它可以使用在应用程序任何地方,不仅仅只是控制器的处理方法中,还比较适合在较低级别的代码中使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值