Spring Security身份认证Authentication

文章目录

  • Authentication(身份认证框架)的架构
    • SecurityContextHolder
    • AuthenticationManager
    • AbstractAuthenticationProcessingFilter
  • 关于密码存储
    • 密码存储的历史
    • Spring Scurity中的密码存储机制
  • 用户/密码认证
    • 根据HttpRequest提取用户密码的方式区分
      • FormLogin 页面表单登陆
        • 默认页面登陆认证
        • 自定义登陆页面
      • Basic Authentication
    • 根据用户密码不同的存储方式实现区分
      • In-Memory Authentication
      • JDBC Authentication
      • 自定义UserDetailsService

Authentication(身份认证框架)的架构

SecurityContextHolder

SpringSecurity会将用户的信息存储在SecurityContextHolder中,以下为SecurityContextHolder的模型
Spring Security身份认证Authentication_第1张图片
SecurityContextHolder中包含了SecurityContext对象,SecurityContext中包含了Authentication对象
Authentication对象的功能有两个:

  • 作为后续验证的入参
  • 获取当前验证通过的用户信息

Authentication包含三个属性:

  • principal:用户身份,如果是用户/密码认证,这个属性就是UserDetails实例
  • credentials:通常就是密码,在大多数情况下,在用户验证通过后就会被清除,以防密码泄露。
  • authorities:用户权限

AuthenticationManager

Spring Security身份认证Authentication_第2张图片

  • AuthenticationManager是用来实现身份认证的API接口,入参是Authentication,最常用的子类是ProviderManager
  • AuthenticationProvider是某种具体的认证实现,例如DaoAuthenticationProvider用来实现用户/密码认证,JwtAuthenticationProvider实现JWT Token认证
  • 支持多种类型AuthenticationProvider会注入到ProviderManager中,ProviderManager会根据Authentication的类型调用相应类型的AuthenticationProvider

AbstractAuthenticationProcessingFilter

Spring Security身份认证Authentication_第3张图片
AbstractAuthenticationProcessingFilter使用来进行用户认证的过滤器,会编排进SecurityFilterChain中,其处理流程为:

  1. 用户提交信息后,AbstractAuthenticationProcessingFilter 或从HttpServletRequest 提取信息并创建Authentication,Authentication的类型是由过滤器的类型决定的(AbstractAuthenticationProcessingFilter的子类),例如 UsernamePasswordAuthenticationFilter 创建 UsernamePasswordAuthenticationToken
  2. Authentication 被传入 the AuthenticationManager进行认证
  3. 如果认证失败
    (1)SecurityContextHolder 清除掉
    (2)RememberMeServices.loginFail 被调用,如果remember me 没有配置,则此方法为空方法
    (3)AuthenticationFailureHandler 被调用
  4. 认证成功
    SessionAuthenticationStrategy 被通知用户登陆.
    Authentication 存入 SecurityContextHolder. 之后 SecurityContextPersistenceFilter 会将SecurityContext存入HttpSession.
    RememberMeServices.loginSuccess 被调用,如果remember me没有配置,则改方法为空方法
    ApplicationEventPublisher 发布 InteractiveAuthenticationSuccessEvent.

用户密码验证 DaoAuthenticationProvider
DaoAuthenticationProvider实现了接口AuthenticationProvider,并且会使用UserDetailsService和PasswordEncoder来验证用户和密码
Spring Security身份认证Authentication_第4张图片

  1. 身份认证的过滤器会提取请求信息中的用户密码信息,传递UsernamePasswordAuthenticationToken(包含用户名和密码)对象至AuthenticationManager,其中ProviderManager是AuthenticationManager接口的实现类
  2. ProviderManager 中注册了DaoAuthenticationProvider(AuthenticationProvider接口的一个实现类,用于用户密码认证)
  3. DaoAuthenticationProvider 调用UserDetailsService获取存储的用户信息,得到UserDetails对象,UserDetails提供了对用户信息的基本操作
  4. DaoAuthenticationProvider 调用PasswordEncoder对传入的密码进行加密,并与上一步得到的UserDetails中的密码进行比较
  5. 验证通过后,用户信息会被存储于SecurityContextHolder中

关于密码存储

在介绍SpringSecurity认证机制之前,先说一些关于密码存储的内容。在SpringSecurity中PasswordEncoder接口用来提供单向加密机制,使密码存储更加安全。

密码存储的历史

在过去的多年时间里,密码存储机制进化了多次。最开始,密码就是明文文本存储,例如存储在数据库中,获取密码则需要使用数据库的用户与密码,因此人们则认为这种方式是安全的,但一些恶意用户却可以轻易的破解此方式,例如使用SQL注入等手段。

此后,密码存储前会通过单向Hash加密,例如SHA-256,当一个用户申请认证时,系统会对用户输入的密码进行hash加密,然后与系统存储的加密密码进行比较,如此系统只需要存储加密后的密文,这样即使密码泄露,也不会泄露真实的密码。不幸的是,黑客们发明了彩虹表(Rainbow Tables),通过查询彩虹表,很多密码都可以轻易的破解。

为了降低彩虹表的能力,安全专家建议使用加盐密码(salted passwords),在密码进行单向hash加密时,加入一些随机字符,这些随机字符被称为盐(salt)。

但近些年来,随着计算机硬件的快速发展,hash运算速度已提升为每秒十亿级,这意味着单向hash加密机制已经不再安全。现在更加推荐使用自适应的单向加密机制,自适应的单向加密机制可以配置一个工作因子(work factor),该因子会跟随硬件设备性能自行调整,使得加密过程所需要的时间在一个固定的值,这个值推荐在1秒钟左右,如此破解密码变得更加困难,常见的自适应加密机制包括bcrypt, PBKDF2, scrypt, 和 argon2

Spring Scurity中的密码存储机制

在Spring Security 5.0以前,默认的PasswordEncoder是NoOpPasswordEncoder,即明文密码,现在Spring Security的默认PasswordEncoder是DelegatingPasswordEncoder,DelegatingPasswordEncoder可以代理多种加密机制,其实例化过程如下:

public class DelegatingPasswordEncoderTest {
    public static void main(String[] args) {
        String idForEncode = "bcrypt";
        Map encoders = new HashMap<>();
        encoders.put(idForEncode, new BCryptPasswordEncoder());
        encoders.put("noop", NoOpPasswordEncoder.getInstance());
        encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
        encoders.put("scrypt", new SCryptPasswordEncoder());
        encoders.put("sha256", new StandardPasswordEncoder());

        PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders);

        //对密码进行加密
        String encodedPassword = passwordEncoder.encode("password");
        System.out.println(encodedPassword); //{bcrypt}$2a$10$8nfdxLtGwvDu4MRI.CpCmOg56d7zNyq2xVJqaO4b.OPDFjlsu47Am

        //验证密码
        boolean res = passwordEncoder.matches("password", "{bcrypt}$2a$10$550FMm3qENbQtIWOa6qfsuqBftBF3BfVibViHv2ueQ3wGs45jEPWu");
        System.out.println("res = " + res); // res = true
    }
}

密码的存储格式为,其中{id}为对应的加密算法:

{id}encodedPassword

以下为对字符串"password"加密后的各种算法的结果:

{bcrypt}$2a$10$4lVeyJaX4NeUGLcAOG1KsuhguK30dwxMt9vO4woC.5elEC2xufWSu
{noop}password
{pbkdf2}8c3682ac6c8a7285060cc984fe3cf6da2af1a98410ad244b8b2b90fb90f60665c437a5cff05682e3
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
{sha256}9b050796bcdab7d9f9009669fac770584071e4057f4edae723e28096fe21910e7e95420647a2749f

用户/密码认证

根据HttpRequest提取用户密码的方式区分

FormLogin 页面表单登陆

默认页面登陆认证

在Springboot中引入Spring Security依赖

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-securityartifactId>
dependency>

启动服务,Spring Scurity就会自动创建一个页面登陆身份认证,默认的用户为user,密码会在启动日志中打印出来
在这里插入图片描述
输入任何地址,就会跳转至默认的登陆页面,输入用户密码后可以实现登陆。
Spring Security身份认证Authentication_第5张图片

自定义登陆页面

自定义页页面配置:HttpSecurity .formLogin().loginPage("/login") 指定了自定义的登陆页面,关于授访问控制相关的配置会在以后的章节进行介绍

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

   @Override
   protected void configure(HttpSecurity http) throws Exception {
       http
               .authorizeRequests().antMatchers("/login").permitAll()
               .anyRequest().authenticated()
               .and()
               .formLogin().loginPage("/login");
   }
}

创建登陆页面的Controller

@Controller
public class LoginController {
    @RequestMapping("/login")
    public String login() {
        return "login";
    }
}

创建登陆页面,这里我使用的Themeleaf


<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Please Log Intitle>
head>
<body>
<div style="width:500px;margin:0 auto">
    <h1>Please Log Inh1>
    <div th:if="${param.error}">
        Invalid username and password.
    div>
    <div th:if="${param.logout}">
        You have been logged out.
    div>
    <form th:action="@{/login}" method="post">
            <div>
                <input type="text" name="username" placeholder="Username">
            div>
            <div>
                <input type="password" name="password" placeholder="Password">
            div>
            <input type="submit" value="Login">
    form>
div>
body>
html>

Basic Authentication

Basic Authentication是针对服务接口的身份认证,当用户的请求中没有携带用户/密码信息,或者密码不匹配时,Spring Security会在Http的response中增加 WWW-Authenticate头信息,客户端根据这个头信息判断是否认证通过。
Basic Authentication开启配置

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests().antMatchers("/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login") //指定自定义登陆页面
                .and()
                .httpBasic(); //开启Basic Authentication
    }
}

开发一个restful接口进行验证

@Controller
public class LoginController {

    @RequestMapping("/basic")
    @ResponseBody
    public String basic() {
        return "hello";
    }
}

使用Postman发送get请求至接口,当不填写用户密码或者错误密码时,可以看到返回信息头中包含WWW-Authenticate
Spring Security身份认证Authentication_第6张图片
当填写用户密码信息后则可以正常访问。
Spring Security身份认证Authentication_第7张图片

根据用户密码不同的存储方式实现区分

In-Memory Authentication

Spring Security身份认证Authentication_第8张图片

InMemoryUserDetailsManager实现接口UserDetailsManager, UserDetailsManager接口继承UserDetailsService接口,支持查询存储在内存中的用户密码信息,也提供了堆UserDetails的管理

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests().antMatchers("/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login") //指定自定义登陆页面
                .and()
                .httpBasic(); //开启Basic Authentication
    }

    /**
     * 用户密码存储在内存中
     * @return
     */
    @Bean
    public UserDetailsService inMemoryUserDetailsManager() {
        UserDetails user = User.builder()
                .username("user")
                .password("{bcrypt}$2a$10$XpaYxCM8xmpzfQYh/xw3ougC5MsADnED25UtGOxR3a3zbklhWpgtq")
                .roles("USER")
                .build();

        UserDetails admin = User.builder()
                .username("admin")
                .password("{bcrypt}$2a$10$XpaYxCM8xmpzfQYh/xw3ougC5MsADnED25UtGOxR3a3zbklhWpgtq")
                .roles("USER", "ADMIN")
                .build();
        return new InMemoryUserDetailsManager(user, admin);
    }
}

以上代码中使用的是Spring Security默认的PasswordEncoder
此种方式在维护用户信息时需要修改源代码并编译,有些繁琐,所以往往被使用在项目初期的验证阶段,并不推荐使用在正式的产品中。

JDBC Authentication

Spring Security身份认证Authentication_第9张图片
JdbcDaoImpl 实现接口UserDetailsService,可以通过JDBC查询用户密码信息
JdbcUserDetailsManager 继承了JdbcDaoImpl ,同时实现了UserDetailsManager,提供对UserDetails的管理

(1)根据官方文档创建用户表,这里使用PostgreSQL数据库

create table users(
    username varchar(50) not null primary key,
    password varchar(200) not null,
    enabled boolean not null
);

create table authorities (
    username varchar(50) not null,
    authority varchar(200) not null,
    constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username,authority);

如果使用了用户组,还需要创建组信息表

create table groups (
    id bigint generated by default as identity(start with 1) primary key,
    group_name varchar(50) not null
);

create table group_authorities (
    group_id bigint not null,
    authority varchar(50) not null,
    constraint fk_group_authorities_group foreign key(group_id) references groups(id)
);

create table group_members (
    id bigint generated by default as identity(start with 1) primary key,
    username varchar(50) not null,
    group_id bigint not null,
    constraint fk_group_members_group foreign key(group_id) references groups(id)
);

(2)创建数据源
这里使用的PostgreSQL数据库
首先添加maven依赖

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-jdbcartifactId>
dependency>

<dependency>
    <groupId>org.postgresqlgroupId>
    <artifactId>postgresqlartifactId>
dependency>

在application.yml中配置数据库

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/postgres
    username: postgres
    password: daiwl
    driver-class-name: org.postgresql.Driver

(3) 配置JdbcUserDetailsManager

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private DataSource dataSource; //注入数据源

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests().antMatchers("/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login") //指定自定义登陆页面
                .and()
                .httpBasic(); //开启Basic Authentication
    }

    /**
     * 用户密码存储在内存中
     * @return
     */
//    @Bean
//    public UserDetailsService inMemoryUserDetailsManager() {
//        UserDetails user = User.builder()
//                .username("user")
//                .password("{bcrypt}$2a$10$XpaYxCM8xmpzfQYh/xw3ougC5MsADnED25UtGOxR3a3zbklhWpgtq")
//                .roles("USER")
//                .build();
//
//        UserDetails admin = User.builder()
//                .username("admin")
//                .password("{bcrypt}$2a$10$XpaYxCM8xmpzfQYh/xw3ougC5MsADnED25UtGOxR3a3zbklhWpgtq")
//                .roles("USER", "ADMIN")
//                .build();
//        return new InMemoryUserDetailsManager(user, admin);
//    }

  /**
   * JDBC Authentication
   * @param dataSource
   * @return
   */
    @Bean
    public UserDetailsManager users(DataSource dataSource) {
        JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);

        //创建user和admin用户
        UserDetails user = User.builder()
                .username("user")
                .password("{bcrypt}$2a$10$XpaYxCM8xmpzfQYh/xw3ougC5MsADnED25UtGOxR3a3zbklhWpgtq")
                .roles("USER")
                .build();
        UserDetails jdbc= User.builder()
                .username("jdbc")
                .password("{bcrypt}$2a$10$XpaYxCM8xmpzfQYh/xw3ougC5MsADnED25UtGOxR3a3zbklhWpgtq")
                .roles("USER", "JDBC")
                .build();
        users.deleteUser("user");
        users.createUser(user);
        users.deleteUser("jdbc");
        users.createUser(jdbc);
        return users;
    }
}

以上代码在配置JdbcUserDetailsManager之后,立即创建了user和jdbc用户

自定义UserDetailsService

用户也可以通过实现UserDetailsService接口自定义身份认证,Spring Security提供了以上两种实现,In-memory和jdbc,如果不能满足需求,则可以自行定义:

@Bean
CustomUserDetailsService customUserDetailsService() {
    return new CustomUserDetailsService();
}

你可能感兴趣的:(SpringSecurity)