Spring Security的基本介绍

Spring Security的基本介绍

在讲解Spring Security 之前,我们先来了解一下认证授权的这两个基本概念。所谓认证(Authentication), 通俗地来说,就是搞清楚你是谁(who are you?); 而授权(Authorization), 就是你能干什么(what are you allowed to do?) 。 所有的安全框架,基本都是为了解决这两个问题的,Spring Security也不例外。

1. Spring Security的简介

Spring Security是Spring家族推出一款用于认证和授权的安全框架。该框架采用责任链的设计模式,由多个过滤器组成过滤器链来完成认证和授权的功能。框架的整体结构如图1所示。

图1- Spring Security 的过滤器链结构

图1中SecurityFilterChain中的N个过滤器就是Spring Security的核心部分。对于每个客户端的请求,在到达Controller之前,除了应用本身定义的过滤器之外,还需要通过Spring Security过滤器链的处理。认证和授权的实现,都隐藏在这条过滤器链中。下面是过滤器链中的部分过滤器以及它们处理请求的顺序。

Filter Duty
...
LogoutFilter 用于处理退出登录请求
UsernamePasswordAuthenticationFilter 使用用户名和密码来进行登录认证
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
...
RememberMeAuthenticationFilter 记住我认证处理过滤器
AnonymousAuthenticationFilter 匿名认证处理过滤器
SessionManagementFilter Session管理过滤器,用户管理session信息
ExceptionTranslationFilter 异常处理过滤器,用于处理认证过程中抛出的异常
FilterSecurityInterceptor 最后一个filter, 用于权限的判断

上述过滤器中,最重要的是UsernamePasswordAuthenticationFilter过滤器。该过滤器会根据Post请求的用户名和密码产生一个token信息,然后调用AuthenticationManager.authenticate(token)来完成认证。

2. Spring Security的基本概念

Spring Security的核心部分都在一条过滤器链中,而在这条链中,想要完整认证和授权,还需要借助Spring Security提供的一些组件,下面让我们来认识一下它们。

  • Authentication: 在Spring Security中,Authentication用于表示一个被认证的用户。Authentication里面包含了用户信息(Principal)、密码(Credentials)以及所拥有的权限(Authorities)。(Principal通常是一个UserDetails)

  • SecurityContext: Spring Security中的上下文对象,用于保存当前被认证的用户信息Authentication

  • SecurityContextHolder: 从名字上也可以看出这个是用于保存SecurityContext的。默认的情况下,使用ThreadLocal来保存SecurityContext对象,因此在认证过后,可以在任意方法中通过SecurityContext.getContext()方法来获取当前SecurityContext

AuthenticationSecurityContext以及SecurityContextHolder这三者之间的关系如图2所示。

图2- Authentication,SecurityContext和SecurityContextHolder的关系

在认证的时候,通常需要产生一个Authentication,然后按照Authentication -> SecurityContext -> SecurityContextHolder的顺序将认证的信息存放到安全上下文中。相反地,在认证后,想要获取当前用户信息,可以按照SecurityContextHolder -> SecurityContext -> Authentication的顺序获得,即Authentication authentication = SecurityContextHolder.getContext.getAuthentication()

  • AuthenticationManager: Spring Security默认的一个认证接口,定义了Spring Security认证的方法。默认的情况下,会在该方法中验证用户提交的信息,验证成功后将返回的Authentication对象保存到上下文中。在UsernamePasswordAuthenticationFilter会调用AuthenticationManagerauthenticate方法进行认证。

  • ProviderManager: AuthenticationManager最常用的实现。用于管理一个由AuthenticationProvider组成的链表。根据需要调用AuthenticationProvider提供的认证方式,如果全部都认证失败,则会抛出认证异常信息。

  • AuthenticationProvider: 每个AuthenticationProvider代表了一种特定的认证方式。比如常用的DAOAuthenticationProvider会根据开发的需要加载usernamepassword来进行认证。

  • DAOAuthenticationProvider: 调用UserDetailsService.loadUserByUsername()方法得到用户信息,以供后续认证需要。

  • UserDetailsService: 要完成用户的认证,必不可少的一部分就是验证用户的usernamepassword是否正确。Spring Security提供In-Memory和配置文件保存用户名和密码,然而在真实的系统中,用户的信息通常保存在数据库中。加载用户信息的逻辑,通常需要实现Spring Security的UserDetailsService接口,并在对应的方法中返回一个UserDetails以供后续的认证和授权。

  • UserDetails: UserDetailsService.loadUserByUsername()放回的类型,内部包含用户名、密码、权限等用户信息。AuthenticationPrincipal通常也是一个UserDetails

  • PassowrdEncoder: 用户密码不可能明文保存,Spring Security提供了一些PasswordEncoder用于encode password。官方推荐使用的是BCryptPasswordEncoder

3. 认证和授权的实现

  • 引入spring security依赖

    org.springframework.boot
    spring-boot-starter-security

  • 配置WebSecurityConfigurerAdapter

Spring Security为我们提供了一个默认的登录页面和登录用户,默认的用户名为user,密码打印在控制台中。在实际的应用场景中,默认的登录页面和用户信息肯定是不能满足我们的需求的。因此需要进行一定的配置。WebSecurityConfigurerAdapter是Spring Security中最重要的一个配置类,在这个配置类中我们可以配置登录页面、需要认证的URL等信息、异常处理等。想要开启Spring Security的配置,还需要在配置类中加入@EnableWebSecurity注解。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/hello", "/login").permitAll()  // 1: /hello 和 /login 不要认证就可以访问
                .anyRequest().authenticated();  // 2: 其他所有的URL都需要认证之后才可以访问

        http.formLogin()
                .loginPage("/login")  // 3: 自定义登录页面
                .loginProcessingUrl("/login") // 4: 自定义登录请求
                .failureHandler(new LoginFailureHandler()); // 5: 自定义登录失败处理器
        http.exceptionHandling()
                .accessDeniedHandler(new MyAccessDeniedHandler()); // 6: 自定义权限不足处理器
    }   
}

WebSecurityConfigurerAdapter有多个重载的configure方法,在public void configure(HttpSecurity http)中可以配置哪些URL是不需要认证即可访问,哪些必须要认证才能访问。如上面的代码中,请求/hello/login的时候不需要登录认证即可访问,其余请求都需要认证之后才能访问。http.formLogin().loginPage()loginProcessingUrl()则配置了自定义的登录页面和登录处理的URL。该方法中还可以给登录失败和权限不足添加自定义的一个处理器,这个前后端分离的情况下应用广泛。自定义的处理器如下所示。

public class LoginFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        ObjectMapper objectMapper = new ObjectMapper();
        String value = objectMapper.writeValueAsString(CommonResult.fail("login failed"));
        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.write(value);
        writer.flush();
        writer.close();
    }
}
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        ObjectMapper objectMapper = new ObjectMapper();
        String value = objectMapper.writeValueAsString(CommonResult.fail("access denied"));
        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.write(value);
        writer.flush();
        writer.close();
    }
}
  • 实现UserDetailsService

前面提到过,Spring Security会调用AuthenticationManager.authenticate()方法来进行用户认证。那么认证所需要的真实用户信息来自哪里呢?答案就是UserDetailsService接口。这个接口只有一个方法UserDetails loadUserByUsername(String username),该方法用于加载用户的信息,然后和前端提交的用户名密码做对比。在实际应用中,通常需要实现该接口,然后在loadUserByUsername(String username)加载数据库中的用户信息。例如:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional userPoOptional = userRepository.findByUsername(username);
        if (!userPoOptional.isPresent()) {
            throw new UsernameNotFoundException("username not found");
        }
        UserPo userPo = userPoOptional.get();
        Set roles = userPo.getRoles();
        List roleNames = roles.stream().map(rolePo -> "ROLE_" + rolePo.getRoleName()).collect(Collectors.toList());
        return UserInfo.builder()
                .username(userPo.getUsername())
                .password(userPo.getPassword())
                .authorities(AuthorityUtils.commaSeparatedStringToAuthorityList(String.join(",", roleNames)))
                .build();
    }
}

这里使用了Spring JPA来加载UserPo对象。查询较为简单,这里不单独列出来,完整代码可以参考底下的链接。

前面提到过,UserDetails是Spring Security提供的用户信息接口,因此我们需要实现该接口或者使用Spring Security提供的User类,这里选择前者。

@Data
@Builder
public class UserInfo implements UserDetails {
    private String username;
    private String password;
    private Collection authorities;
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    @Override
    public boolean isEnabled() {
        return true;
    }
}
  • 使用UserDetailsService

在使用UserDetailsService之前,还有一件事,那就是用户密码匹配的问题。Spring Security提供了多种PasswordEncoder,其中官方推荐使用的是BCryptPasswordEncoderPasswordEncoder提供了String encode(CharSequence rawPassword)boolean matches(CharSequence rawPassword, String encodedPassword)两个简单的方法,用于计算明文的摘要,以及根据明文和摘要信息判断密码是否正确。通常在注册用户的时候,要使用其encode方法,而在认证的时候使用match方法。

有了UserDetailsServicePasswordEncoder之后,我们就可以配置认证的过程了。在Spring Security的默认认证流程中,会调用AuthenticationManager.authenticate()方法来进行认证,该方法会使用到UserDetailsService.loadUserByUsername(String username)来加载数据库中的用户信息。因此,我们将上面定义好的UserDetailsService加入到配置中。配置的方式是在WebSecurityConfigurerAdapter的实现类中重写protected void configure(AuthenticationManagerBuilder auth)方法。代码如下面所示。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
    
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

配置好自定义的UserDetailsService之后,我们就完成了用户的认证了。

授权的实现

Spring提供了@PreAuthorize@PostAuthorize@Secured 三个注解,用于控制某个API的权限。其中用的较多的是@PreAuthorize ,在该注解中,可以使用Spring EL表达式来控制对应的权限,如hasRolehasAnyRolehasAuthorityhasAnyAuthority等。而在使用@PreAuthorize(value = "hasRole('ROLE_ADMIN')")之前,还需要在WebSecurityConfigurerAdapter配置类中添加@EnableGlobalMethodSecurity(prePostEnabled = true)注解。

用户的角色往往在UserDetailsService.loadUserByUsername()方法中,从数据库中检索出该用户的角色。在使用过程中,需要为每个角色添加ROLE_的前缀,然后将其封装成GrantedAuthority

Optional userPoOptional = userRepository.findByUsername(username);
// ...
Set roles = userPo.getRoles();
List roleNames = roles.stream().map(rolePo -> "ROLE_" + rolePo.getRoleName()).collect(Collectors.toList());
// ...

AuthorityUtils.commaSeparatedStringToAuthorityList(String.join(",", roleNames));

将用户的角色附带给UserDetails之后,只需要在Controller中的需要权限控制的API上@PreAuthorize即可。例如:

@PreAuthorize(value = "hasRole('ROLE_ADMIN')")
@GetMapping("/{username}")
public UserPo findByUsername(@PathVariable String username) {
    return userService.findByUsername(username);
}

4. 代码链接

https://github.com/sylinder/spring-security-demo.git

你可能感兴趣的:(Spring Security的基本介绍)