Spring Boot 2.x实战78 - Spring Security 2 - Spring Security的认证(Authentication)

1.3 Authentication

Spring Security为我们提供了一个专门的org.springframework.security.core.Authentication接口来代表认证;它最常用的实现类有UsernamePasswordAuthenticationToken

一旦请求被认证后,Authentication对象就会自动存储在由SecurityContextHolder管理的SecurityContext中。

认证的原理通过下面类的处理顺序来进行的:

  • FilterChainProxy:Servlet过滤器(FilterspringSecurityFilterChain实际类型是FilterChainProxy,它可能包含多个过滤器链(DefaultSecurityFilterChain),每个过滤器链包含多个过滤器。特定的过滤器会将请求中的认证信息(如用户名、密码)构造成Authentication对象交由AuthenticationManagerauthenticate方法处理。主要的过滤器有:

    • UsernamePasswordAuthenticationFilter:使用表单(用户名、密码)提交进行认证信息,构造的Authentication对象类型为UsernamePasswordAuthenticationToken;并调用AuthenticationManagerauthenticate来进行认证操作。
    • BasicAuthenticationFilter:使用HTTP请求的基础授权头提交认证信息,同样构造的Authentication对象的类型为UsernamePasswordAuthenticationToken;并调用AuthenticationManagerauthenticate来进行认证操作。
    • ExceptionTranslationFilter:处理过滤器链中的异常
      • AuthenticationException:认证异常,返回401状态码
      • AccessDeniedException:授权异常,返回403状态码
    • FilterSecurityInterceptor:它是AbstractSecurityInterceptor的子类,当认证成功后,再使用AccessDecisionManager对Web路径资源(web URI)进行授权操作。
  • AuthenticationManagerAuthenticationManager接口的实现为ProviderManager,我们使用AuthenticationManagerBuilder来定制构建AuthenticationManager

  • ProviderManagerProviderManager通过它authenticate方法将认证交给了一组顺序的AuthenticationProvider来完成认证。

  • AuthenticationProviderAuthenticationProvider接口包含两个方法:

    • supports:是否支持认证安全过滤器缓解构造的Authentication
    • authenticate:对Authentication进行认证,若认证通过返回Authentication,若不通过则抛出异常。
  • DaoAuthenticationProviderDaoAuthenticationProviderAuthenticationProvider接口的实现,他支持认证的Authentication类型为UsernamePasswordAuthenticationToken。它在认证中主要用到了下面三个部分:

    • UserDetailsService:从指定的位置(如数据库)获得用户信息;通过比较用户信息和AuthenticationUsernamePasswordAuthenticationToken)中的用户名和密码信息,若认证通过则构建新的AuthenticationUsernamePasswordAuthenticationToken),包含用户的权限信息。
    • PasswordEncoder:使用PasswordEncoder将请求传来的明文密码和存储的编码后的密码进行匹配比较。
1.3.1 配置AuthenticationManager

我们可以重载WebSecurityConfigurerAdapter类的方法,使用AuthenticationManagerBuilder来配置AuthenticationManager

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

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

}

我们可以通过配置UserDetailsServiceAuthenticationProvider定制认证。

1.3.2 UserDetailsService

本例定制一个UserDetailsService通过Spring Data JPA从数据库中获取用户。

基本外部配置:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/first_db?useSSL=false
    username: root
    password: zzzzzz
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update

我们用户的实体:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class SysUser implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String realName;

    @Column(unique = true)
    private String username;

    private String password;
  
    public SysUser(String realName, String username, String password) {
        this.realName = realName;
        this.username = username;
        this.password = password;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() { //1
        return null;
    }

    @Override
    public String getPassword() { //2
        return this.password;
    }

    @Override
    public String getUsername() { //3
        return this.username;
    }

    @Override
    public boolean isAccountNonExpired() { //4
        return true;
    }

    @Override
    public boolean isAccountNonLocked() { //5
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() { //6
        return true;
    }

    @Override
    public boolean isEnabled() { //7
        return true;
    }
}

实现UserDetails接口的用户,通过接口的方法构建Authentication对象的用户信息。

  1. getAuthorities方法获得用户的权限信息,我们会在后面详细讲解;
  2. getPassword获得用户的密码,使用存储的密码;
  3. getUsername获得用户名,使用存储的用户名;
  4. isAccountNonExpired是否账户未过期,设置为true,即未过期;
  5. isAccountNonLocked是否账户未被锁定,设置为true,即未被锁定;
  6. isCredentialsNonExpired是否密码未过期,设置为true,即未过期;
  7. isEnabled用户是否弃用,设置为true,即弃用。

用户的Repository:

public interface SysUserRepository extends JpaRepository<SysUser, Long> {
   Optional<SysUser> findByUsername(String username);
}

我们自定义UserDetailsService

public class CusotmUserDetailsService implements UserDetailsService {

    SysUserRepository sysUserRepository;

    public CusotmUserDetailsService(SysUserRepository sysUserRepository) {
        this.sysUserRepository = sysUserRepository; //1
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<SysUser> sysUserOptional = sysUserRepository.findByUsername(username); //2
        return  sysUserOptional
                   .orElseThrow(() -> new UsernameNotFoundException("Username not found")); //3 
    }
}
  1. 注入SysUserRepository用来查询数据库用户信息;
  2. 通过用户名从数据库里查询用户对象;
  3. 如果存在则返回用户对象,不存在则抛出异常;本方法的返回值要求为UserDetails类型,我们的用户也是UserDetails可直接返回。

我们可以通过配置AuthenticationManager来注册UserDetailsService,它会替换`。

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    SysUserRepository sysUserRepository; //1

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(new CusotmUserDetailsService(sysUserRepository)); //2
    }

    @Bean
    PasswordEncoder passwordEncoder(){ //3
        return new BCryptPasswordEncoder();
    }
    
}
  1. 注入SysUserRepositoryCusotmUserDetailsService的构造使用;
  2. 使用AuthenticationManagerBuilderuserDetailsService方法注册自定义的UserDetailsService
  3. 使用BCrypt作为我们的密码编码加密算法给DaoAuthenticationProvider使用;

我们还可以更简单的,直接通过声明UserDetailsService的Bean让DaoAuthenticationProvider使用自定义UserDetailsService

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    UserDetailsService userDetailsService(SysUserRepository sysUserRepository){
        return new CusotmUserDetailsService(sysUserRepository);
    }

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

}

建立一个控制器用来测试访问:

@RestController
public class IndexController {

    @GetMapping("/")
    public String hello(){
        return "Hello Spring Security";
    }
}

应用启动时,向系统添加一个用户:

@Bean
CommandLineRunner createUser(SysUserRepository sysUserRepository, PasswordEncoder passwordEncoder){
   return args -> {
      SysUser user = new SysUser("wangyunfei", "wyf", passwordEncoder.encode("111111"));
      sysUserRepository.save(user);
   };
}

Spring Security默认使用表单登陆,且为我们自动提供了一个表单,我们访问http://localhost:8080/会自动转向http://localhost:8080/login;当我们用错误的账号密码登陆时会提示“用户名或密码错误”,用正确的账号密码访问显示测试的控制器返回内容。
在这里插入图片描述

1.3.3 AuthenticationProvider

上面自定义UserDetailsService实际上还是AuthenticationProvider的一部分,我们也可以通过的Bean来使用自定义的AuthenticationManager来设置AuthenticationProvider

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    SysUserRepository sysUserRepository;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(new CusotmUserDetailsService(sysUserRepository));
        authProvider.setPasswordEncoder(new BCryptPasswordEncoder());
        auth.authenticationProvider(authProvider); //
    }
}

若我们自定义AuthenticationProvider,则完全使用自己的验证逻辑。

public class CustomAuthenticationProvider implements AuthenticationProvider {

    SysUserRepository sysUserRepository;

    PasswordEncoder passwordEncoder;

    public CustomAuthenticationProvider(SysUserRepository sysUserRepository, PasswordEncoder passwordEncoder) {
        this.sysUserRepository = sysUserRepository;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String usernameFromRequest = authentication.getName(); //1
        String passwordFromRequest = authentication.getCredentials().toString(); //2
        Optional<SysUser> sysUserOptional = sysUserRepository.findByUsername(usernameFromRequest);
        SysUser sysUser = sysUserOptional
                .orElseThrow(() -> new UsernameNotFoundException("Username not found")); //3
        if(passwordEncoder.matches(passwordFromRequest, sysUser.getPassword()) && //4
            sysUser.isAccountNonExpired() &&
            sysUser.isAccountNonLocked() &&
            sysUser.isCredentialsNonExpired() &&
            sysUser.isEnabled())
            return new UsernamePasswordAuthenticationToken(sysUser.getUsername(), sysUser.getPassword(), sysUser.getAuthorities()); //5
        else
            throw new BadCredentialsException("Bad Credentials");
    }

    @Override
    public boolean supports(Class<?> authentication) { //6
        return (UsernamePasswordAuthenticationToken.class
                .isAssignableFrom(authentication));
    }
}
  1. 获取请求传递的用户名;
  2. 获取请求传递的密码;
  3. 检查用户是否存在于数据库中;
  4. 比较传递密码和存储的编码密码是否一致,包含用户的有效性的比对;
  5. 构建新的AuthenticationUsernamePasswordAuthenticationToken对象;
  6. 声明当前自定义AuthenticationProvider能处理Authentication类型为UsernamePasswordAuthenticationToken的认证。

使用这个AuthenticationProvider同样可通过AuthenticationManager来配置:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    SysUserRepository sysUserRepository;

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

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(new CustomAuthenticationProvider(sysUserRepository, passwordEncoder()));
    }
}

更简单的注册AuthenticationProvider的Bean的效果也是一致的:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

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

    @Bean
    AuthenticationProvider authenticationProvider(SysUserRepository sysUserRepository){
        return new CustomAuthenticationProvider(sysUserRepository, passwordEncoder());
    }
}

运行的结果和自定义UserDetailsService一致,普通情况下我们使用自定义UserDetailsService就可以了;只有涉及到复杂的认证逻辑时才需要自定义AuthenticationProvider

1.3.4 HTTP基础认证

HttpSecurity用来针对不同的HTTP请求进行Web安全配置。我们前面的认证都是通过表单登陆进行认证,我们可以通过HttpSecurity配置在请求时将账号信息放置于头部进行登录。在现代应用中,服务端和客户端(web、app等)分离,一般都是通过类似的方式来请求服务端的接口的。

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated() //1 
                .and()
                .httpBasic().authenticationEntryPoint(authenticationEntryPoint()); //2
    }

    @Bean
    AuthenticationEntryPoint authenticationEntryPoint(){ //3
        BasicAuthenticationEntryPoint authenticationEntryPoint = new BasicAuthenticationEntryPoint();
        authenticationEntryPoint.setRealmName("wisely");
        return authenticationEntryPoint;
    }

    @Bean
    UserDetailsService userDetailsService(SysUserRepository sysUserRepository){
        return new CusotmUserDetailsService(sysUserRepository);
    }

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


}
  1. 设置所有的请求都要认证后才能访问;
  2. 设置登录方式为HTTP Basic,设置认证入口点为BasicAuthenticationEntryPoint
  3. BasicAuthenticationEntryPoint将认证信息放置于头部,当认证不通过返回状态值为401

Postman支持Basic Auth:
在这里插入图片描述
HTTP Basic实际上是在请求头的Authorization中添加值为Basic 账号:密码的Base64编码wyf:111111的Base64编码为d3lmOjExMTExMQ==,那我们请求头为Authorization: Basic d3lmOjExMTExMQ==即可完成认证。
在这里插入图片描述

1.3.5 获取用户信息

我们可以通过SecurityContextHolder中获取SecurityContext从而获得Authentication

SecurityContext context = SecurityContextHolder.getContext(); //获得SecurityContext
Authentication auth = context.getAuthentication(); //获得Authentication
Object principal = auth.getPrincipal(); //获取用户信息 
Object details = auth.getDetails(); //认证请求的更多信息

Spring Security给我们注册了ArgumentResolvers,我们可以直接通过@CurrentSecurityContext注解获得SecurityContext@CurrentSecurityContext注解还支持表达式获得Authenticationprincipaldetails

我们更简单的通过使用@AuthenticationPrincipal注解获得用户信息。

    @GetMapping("/user")
    public Map<String, Object> getUserInfo(@AuthenticationPrincipal SysUser sysUser, //1
                           @CurrentSecurityContext SecurityContext securityContext, //2
  @CurrentSecurityContext(expression = "authentication")  Authentication authentication, //3
      @CurrentSecurityContext(expression = "authentication.principal") Object principal,//4
         @CurrentSecurityContext(expression = "authentication.details") Object details){ //5
        Map<String, Object> map = new HashMap<>();
        map.put("sysUser", sysUser);
        map.put("authentication", authentication);
        map.put("principal", principal);
      	map.put("details", details);
        return map;
    }
  1. @AuthenticationPrincipal注册系统用户可获得用户对象;
  2. @CurrentSecurityContext可直接获得SecurityContext对象;
  3. 使用表达式expression = "authentication",可获得Authentication对象
  4. 使用表达式expression = "authentication.principal",可获得用户信息;
  5. 使用表达式expression = "authentication.details",可获得认证请求的额外信息;

我们用Postman访问http://localhost:8080/user,并使用Basic Auth认证。
在这里插入图片描述
那我们上一章“审计功能”获得用户的代码修改为:

@Bean
AuditorAware<String> auditorProvider(){
   return () -> Optional.of(SecurityContextHolder.getContext().getAuthentication().getName());
}
1.3.6 密码编码

在生产中我们肯定会对密码进行编码,Spring Security给我们提供了PassowrdEncoder接口用来编码密码和匹配密码;在生产中建议使用工业标准的Bcrypt的实现BCryptPasswordEncoder

public interface PasswordEncoder {
   String encode(CharSequence rawPassword); //将明文密码进行编码
   boolean matches(CharSequence rawPassword, String encodedPassword); //匹配明文密码和编码密码
   default boolean upgradeEncoding(String encodedPassword) { //更新密码编码机制
      return false;
   }
}

前面我们在应用中已经注册了BCryptPasswordEncoder的Bean:

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

我们用代码检验一下:

@Bean
CommandLineRunner passwordOperation(PasswordEncoder passwordEncoder){
   return args -> {
      String passwordPlain = "123456";
      String passwordEncoded = passwordEncoder.encode(passwordPlain);
      boolean isMatched = passwordEncoder.matches(passwordPlain, passwordEncoded);
      System.out.println("明文密码为:" + passwordPlain);
      System.out.println("编码密码为:" + passwordEncoded);
      System.out.println("密码是否匹配:" + isMatched);
   };
}

在这里插入图片描述

新书推荐:

我的新书《从企业级开发到云原生微服务:Spring Boot 实战》已出版,内容涵盖了丰富Spring Boot开发的相关知识
购买地址:https://item.jd.com/12760084.html
在这里插入图片描述
主要包含目录有:

第一章 初识Spring Boot(快速领略Spring Boot的美丽)
第二章 开发必备工具(对常用开发工具进行介绍:包含IntelliJ IDEA、Gradle、Lombok、Docker等)
第三章 函数式编程
第四章 Spring 5.x基础(以Spring 5.2.x为基础)
第五章 深入Spring Boot(以Spring Boot 2.2.x为基础)
第六章 Spring Web MVC
第七章 数据访问(包含Spring Data JPA、Spring Data Elasticsearch和数据缓存)
第八章 安全控制(包含Spring Security和OAuth2)
第九章 响应式编程(包含Project Reactor、Spring WebFlux、Reactive NoSQL、R2DBC、Reactive Spring Security)
第十章 事件驱动(包含JMS、RabbitMQ、Kafka、Websocket、RSocket)
第11章 系统集成和批处理(包含Spring Integration和Spring Batch)
第12章 Spring Cloud与微服务
第13章 Kubernetes与微服务(包含Kubernetes、Helm、Jenkins、Istio)
多谢大家支持。

猜你喜欢

转载自blog.csdn.net/wiselyman/article/details/106823060