首页 > 其他分享 >12-Spring-Cloud-Security

12-Spring-Cloud-Security

时间:2023-08-06 23:34:04浏览次数:58  
标签:username 12 String Spring 认证 Security public

一、Spring Security认证和授权

(一)安全性和Spring Security框架

​    1、从一个安全性应用场景说起

​    如下图所示,工单的生成需要使用用户的订单记录等数据,而这些数据保存在外部平台中;那么就需要用户将自己在订单管理平台上用户名和密码告诉工单系统,工单系统登录到订单管理平台并读取订单记录。

​        

​    这种方式很明显有很大的安全问题,例如:

​      认证问题:工单系统为了开展后续的服务,会保存用户在订单管理平台上的密码,这样很不安全。 如果用户密码不小心被泄露了,那么就是导致订单管理平台上的用户数据发生泄露。

​      授权问题:工单系统拥有了获取用户存储在订单管理平台上所有资料的权限,用户没法限制工单系统获得授权的范围和有效期。

​      密码变更问题:如果用户修改了订单管理平台的密码,那么工单系统就没法正常访问订单管理平台了,这会导致业务中断,但我们又无法限制用户不能修改密码。

​    2、Spring Security

​    Spring Security是一个非常老牌的Spring框架,提供了丰富的功能,如:用户信息管理、权限控制、用户认证、敏感信息加解密、全局安全方法、跨域支持、跨站点请求伪造保护、单点登录等。

​    (1)Spring Security与单体应用

​      在单体应用中使用Spring Security,主要就是做认证和登录,认证就是验证用户名密码是否正确,即Authentication,授权是认证通过的用户拥有哪些权限,即Authorization,

​        

​      在单体系统中,我们一般使用http请求的方式暴露端点,那么端点中怎么来控制,就可以进一步细化,例如是否有某个端点的访问权限等

​      那么在单体系统中认证和授权的整合示意图如下所示,根据HTTP请求的身份凭证做认证和授权,然后访问端点。

​      在单体系统中使用Spring Security非常简单,只需要引入即可,基本上不需要做什么开发。

​        

​    (2)Spring Security与微服务架构

​      对于微服务体系来说,单体服务不再叫单体服务,而是叫资源服务器,因为这些服务提供的都是资源,有了资源才可以做认证和授权操作。

​      由于在微服务体系中,请求是跨服务的,因此需要在服务之间传递一种媒介,保证安全的凭证可以向下传递,这个媒介一般叫做Token(令牌),这个Token是通过授权中心授权的。通俗的讲,就是根据用户凭证去授权中心认证,认证通过后会颁发一个令牌,然后访问下游服务就可以带着这个令牌,下游系统在接收到请求后会使用令牌到授权中心验证令牌是否正确。

​        

​      在微服务体系中,要使用Spring Security,不光需要引入Spring Security,还需要引入特定的协议,这个协议就是OAuth2。

​      OAuth2是一个授权协议,而不是认证协议,只不过我们在使用的时候往往通过客户端封装时把认证协议给封装了起来。

​      授权协议(OAuth2协议)在服务访问场景中的应用就是通过传递Token来实现的,而Token就是通过OAuth2服务器获取的。

​        

​    同时 Spring Security 也提供了响应式系统的处理,例如用户账户体系的建立、用户认证和授权、方法级别的安全访问、OAuth2协议等,都提供了响应式的处理。

​    3、Spring Security配置体系

​    对于单体系统,使用Spring Security是非常简单的,因为Spring Security是一个配置驱动的框架,通过配置就可以实现授权和认证操作。

​    Spring Security自带了一个WebSecurityConfigurerAdapter类,这个类是开发人员操作Spring Security最主要的入口。

​    该类实现了WebSecurityConfigurer接口,重写了configure方法,在该方法中通过http.authorizeRequests添加请求限制,即哪些请求需要认证,requests.anyRequest().authenticated()表示所有的请求都要认证,认证方式有两种,一种是通过http.formLogin() 执行表单登录认证,一种是通过http.httpBasic()执行HTTP基础认证

​    也就是说在系统自带的抽象类中WebSecurityConfigurerAdapter已经做了一层控制,只需要引入Spring Security就可以做系统拦截。

// 实现WebSecurityConfigurer接口
public abstract class WebSecurityConfigurerAdapter implements WebSecurityConfigurer<WebSecurity> { 
    ...
    // 覆写configure方法
    protected void configure(HttpSecurity http) throws Exception {
        ...
        // 对所有访问HTTP端点 的请求进行限制
        http.authorizeRequests((requests) ->
                // 对于所有请求都需要执行认证
                requests.anyRequest().authenticated());
        // 执行表单登录认证和HTTP基础认证
        http.formLogin();
        http.httpBasic();
    }
}

​    4、Spring Security单体应用

​    上面提到在单体应用中使用非常简单,只需要引入即可,基本上不需要做什么开发。

​    首先引入依赖

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

​    启动服务会发现有以下输出,这是因为Spring Security会默认创建一个用户名为user的用户,密码就是每次启动时日志上输出的一串字符。

Using generated security password: a9ff6cb3-29df-483e-be8e-ffa8c843df32

This generated password is for development use only. Your security configuration must be updated before running your application in production.

​    当访问原有接口时,会报401错误,401是指认证异常,403是指授权异常

GET http://localhost:9004/chats/

HTTP/1.1 401 
WWW-Authenticate: Basic realm="Realm"
X-Content-Type-Options: nosniff

​    但是当使用浏览器访问接口时,会提示需要输入用户名密码,当用户名输入成功后,即可正常访问。

(二)用户认证及其实现方法

​    1、登录

​    Spring Security 有基础认证和表单认证。

​    HTTP 基础认证的原理比较简单,只是通过HTTP协议的消息头携带用户名和密码进行登录验,用户名密码使用Base64编码生成一个字符串进行传递。

​    但是Base64只是一种编码,不是一种加密手段,安全性一般,因此 HTTP 基础认证也只是一种基础认证而已。

​    使用 HTTP 基础认证的方式,就是在WebSecurityConfigurerAdapter的configure方法中添加http.httpBasic();

public abstract class WebSecurityConfigurerAdapter implements WebSecurityConfigurer<WebSecurity> { 
    ...
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic();
    }
}

​    HTTP基础认证比较简单,没有定制的登录页面,所以单独使用的场景比较有限。在使用Spring Security时,我们一般会把HTTP基础认证和表单登录认证结合起来一起使用。

​    表单登录认证就是在WebSecurityConfigurerAdapter的configure方法中添加http.formLogin()

public abstract class WebSecurityConfigurerAdapter implements WebSecurityConfigurer<WebSecurity> { 
    ...
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin();
    }
}

​    前面提到,如果使用了表单认证,在浏览器访问时会出现登录界面,如果想要做一些定制化的内容,例如自定义登录界面、登录表单提交时处理地址、定制化登录界面和操作入口等,都是可以做定制化设置的。例如使用loginPage设置登录界面,使用loginProcessingUrl设置认证方法地址,使用defaultSuccessUrl设置登录成功后跳转的地址。

​    现在都是前后端分离项目,因此一般不会使用formLogin来做这些事情。

public abstract class WebSecurityConfigurerAdapter implements WebSecurityConfigurer<WebSecurity> { 
    ...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin().loginPage("/login.html")//自定义登录页面
                .loginProcessingUrl("/action")//登录表单提交时的处理地址 
                // 定制化登录界面和操作入口
                .defaultSuccessUrl("/index");//登录认证成功后的跳转页面 
    }
}

​    2、认证

​    用户认证配置有很多种,包括配置文件、内存、JDBC、LDAP、自定义等

​    (1)基于配置文件

​      主要就是使用如下配置来对用户名密码进行配置,这种方式需要将用户名密码写在配置文件,修改密码就需要修改配置文件,一般不会使用

spring:
  security:
    user:
      name: spring
      password: spring_password

​    (2)基于内存

​      使用builder.inMemoryAuthentication来设置用户名密码和权限,设置权限有两种方式,一种是使用authorities,传值是ROLE+角色,一种是使用roles,传值角色名称,authorities更符合Spring Security 的内部结构,而roles更符合业务逻辑,虽然Spring Security 内部没有roles结构,但是也提供了roles支持。

​      基于内存的方式和基于配置的方式一样,一般只会在演示的时候使用,不会在正式环境使用。

@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
    builder.inMemoryAuthentication()
            .withUser("spring_user").password("password1").authorities("ROLE_USER")
            .and()
            .withUser("spring_admin").password("password2").authorities("ROLE_USER", "ROLE_ADMIN");
}

@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
    builder.inMemoryAuthentication()
            .withUser("spring_user").password("password1").roles("USER")
            .and()
            .withUser("spring_admin").password("password2").roles("USER", "ADMIN");
}

​    (3)基于 JDBC(数据库)

​      数据库的方式,首先得有用户表和权限表。

create table users(username varchar_ignorecase(50) not null primary key,password varchar_ignorecase(500)not null,enabled boolean not null);
create table authorities(username varchar_ignorecase(50)not null,authority varchar_ignorecase(50)not null,constraint fk_authorities_users foreign key(username)references users(username));
create unique index ix_auth_username on authorities(username,authority);

​      在配置时,使用auth.jdbcAuthentication().dataSource(dataSource)进行设置,然后使用usersByUsernameQuery查询用户信息,使用authoritiesByUsernameQuery来获取用户权限,使用passwordEncoder来设置密码加密方式。

@Autowired
DataSource dataSource;

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

​    3、数据加解密 - PasswordEncoder

​    (1)加密与密码匹配

​      PasswordEncoder是Spring Security 所提供的的一种加密方式。

​      PasswordEncoder接口提供了加密方法encode和匹配方法matches,而没有解密方法,从密码管理的角度来说,这是非常合理的,只对密码加密和验证,不做解密,防止密码泄露。

public interface PasswordEncoder {
    // 对原始密码进行编码
    String encode(CharSequence rawPassword);
    // 对提交的原始密码与库中存储的加密密码进行比对
    boolean matches(CharSequence rawPassword, String encodedPassword);

    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

​      针对PasswordEncoder接口可以有很多实现,例如底层的加密算法,都可以依赖很多三方的加密算法。例如下面的BCryptPasswordEncoder,将密码加盐后进行加密。

public class BCryptPasswordEncoder implements PasswordEncoder {
    public String encode(CharSequence rawPassword) {
        String salt;
        if (random != null) {
            salt = BCrypt.gensalt(version.getVersion(), strength, random);
        } else {
            salt = BCrypt.gensalt(version.getVersion(), strength);
        }
        return BCrypt.hashpw(rawPassword.toString(), salt);
    }
}

​      针对这种方式,我们也可以自定义加密处理, 如使用Sha512的方式进行加密,实现逻辑是使用MessageDigest集成SHA-512算法进行加密,在匹配密码时对原始密码重新加密后再与加密密码比较。

// 自定义一个Sha512PasswordEncoder
public class Sha512PasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence rawPassword) {
        return hashWithSHA512(rawPassword.toString());
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        String hashedPassword = encode(rawPassword);
        return encodedPassword.equals(hashedPassword);
    }

    private String hashWithSHA512(String input) {
        StringBuilder result = new StringBuilder();
        try {
            // 使用MessageDigest集成SHA-512算法
            MessageDigest md = MessageDigest.getInstance("SHA-512");
            byte[] digested = md.digest(input.getBytes());
            for (int i = 0; i < digested.length; i++) {
                result.append(Integer.toHexString(0xFF & digested[i]));
            }
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("Bad algorithm");
        }
        return result.toString();
    }
}

​    (2)加密通用模块

​      除了上述的加密和密码匹配外,很多时候还需要解密操作,Spring Security 也提供了加密通用模块。

​      其提供了加解密器(Encryptor)和键生成器(Key Generator),首先生成一个键,然后调用Encryptors.standard对密码和键值做处理生成BytesEncryptor,然后可以使用BytesEncryptor进行加解密。

// 生成加密键
String salt = KeyGenerators.string().generateKey();
String password = "secret";
String valueToEncrypt = "HELLO";
// 使用加密器进行加解密
BytesEncryptor e = Encryptors.standard(password, salt);
byte[] encrypted = e.encrypt(valueToEncrypt.getBytes());
byte[] decrypted = e.decrypt(encrypted);

(三)访问授权及其实现方法

​    1、用户与权限

​    一个用户会对应多个权限,在Spring Security 中,可以使用authorities来对一个用户设置多个权限,同时其提供了两个接口来对用户和权限进行表示,其中UserDetails接口表示一个用户,GrantedAuthority表示一个权限,两者是一对多的关系。

UserDetails user = User.withUsername("jianxiang") .password("123456") .authorities("create", "delete") .build();

​    Spring Security 提供了用户和权限的判断方法hasAuthority和hasAnyAuthority,其中 hasAuthority(String)用来判断是否允许具有特定权限的用户进行访问 ,hasAnyAuthority(String)用来判断是否允许具有任一权限的用户进行访问。

​    如下代码样例所示,可以使用http.authorizeRequests().anyRequest()的hasAuthority和hasAnyAuthority来控制,也可以使用Sple表达式来做判断,将整个方法传入让其判断。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.httpBasic();
    http.authorizeRequests().anyRequest().hasAuthority("CREATE");
    http.authorizeRequests().anyRequest().hasAnyAuthority("CREATE", "DELETE");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.httpBasic();
    http.authorizeRequests().anyRequest().access("hasAuthority('CREATE')");
    String expression = "hasAuthority('CREATE') and !hasAuthority('Retrieve')";
    http.authorizeRequests().anyRequest().access(expression);
}

​    2、角色与权限

​    在实际应用时,一般会将角色与权限绑定,例如现在有两个角色,user和admin,就可以通过对用户设置了不同的角色,从而批量的将权限赋值给用户。这就是所谓的Role Based Access Controller,即RBAC。

​    Spring Security的代码本身是没有角色的概念的,但是通过roles或authorities就可以模拟角色。

UserDetails user = User.withUsername("jianxiang").password("123456").roles("ADMIN").build();
UserDetails user = User.withUsername("jianxiang").password("123456").authorities("ROLE_ADMIN").build();

​    3、配置方法与作用

​    上面提到可以使用anyRequest来进行控制,实际上Spring Security提供了很多相关的方法来进行控制,大体如下所示:

​      anonymous():允许匿名访问

​      authenticated():允许认证用户访问

​      denyAll():无条件禁止一切访问

​      hasAnyAuthority(String):允许具有任一权限的用户进行访问

​      hasAnyRole(String):允许具有任一角色的用户进行访问

​      hasAuthority(String):允许具有特定权限的用户进行访问

​      hasIpAddress(String):允许来自特定IP地址的用户进行访问

​      hasRole(String):允许具有特定角色的用户进行访问

​      permitAll():无条件允许一切访问

http.authorizeRequests().anyRequest().hasRole("ADMIN");

​    4、授权配置:

​    在实际做授权配置应用时,可以使用MVC匹配器、Ant匹配器、正则表达式匹配器,使用最多的是MVC匹配器,Ant是一个比较老牌的java控制器,正则表达式控制粒度非常细,一般也不使用。

// 控制只有user权限可以访问hello_user接口,只有admin权限才能访问hello_admin接口
http.authorizeRequests().mvcMatchers("/hello_user").hasRole("USER").mvcMatchers("/hello_admin").hasRole("ADMIN");
// 显式设置除了上述两个请求外,别的请求都允许认证用户访问(默认也是)
http.authorizeRequests().mvcMatchers("/hello_user").hasRole("USER").mvcMatchers("/hello_admin").hasRole("ADMIN").anyRequest().authenticated();
// 同一HTTP端点方法级别控:设置只允许访问Get方式访问hello接口
http.authorizeRequests().mvcMatchers(HttpMethod.POST, "/hello").authenticated().mvcMatchers(HttpMethod.GET, "/hello").permitAll().anyRequest().denyAll();

​    Ant匹配器

// 子路径自动匹配
http.authorizeRequests().mvcMatchers(HttpMethod.GET, "/user/*").authenticated();
http.authorizeRequests().antMatchers("/hello").authenticated();

​    正则表达式匹配

http.authorizeRequests().mvcMatchers("/email/{email:.*(.+@.+\\.com)}").permitAll().anyRequest().denyAll();

(四)客服系统案例演进

​    引入依赖

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

​    添加配置:创建WebSecurityConfigurer类并继承WebSecurityConfigurerAdapter类,重写认证方法configure(AuthenticationManagerBuilder builder)和授权方法configure(HttpSecurity http)。

​    在认证方法中,暂时使用内存当时来设置cs_user和cs_admin两个用户,同时为用户设置USER角色和ADMIN角色。在之前的说明中,可以使用PasswordEncoder设置密码的加密方式。这里使用明文的方式,可以使用实现类NoOpPasswordEncoder,对应的就是在password里面设置{noop}+明文密码,因为Spring Security在读取密码时,会先读取前缀,当看到前缀是{noop}时,就会走NoOpPasswordEncoder,即不对密码加密。

​    在授权方法中,使用了基础认证和表单认证,在表单认证中设置了对于Get请求允许所有用户访问,对于Post请求只允许USER角色访问,Delete请求只允许ADMIN角色访问。

​    这里单独说明一下,对于http基础认证,使用http.httpBasic()即可,但是Spring Security中默认开启了跨域限制,在模拟请求时,使用http文件请求会存在跨域问题,导致返回401,因此需要使用http.csrf().disable().httpBasic()会排除跨域限制。

@Component
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter  {

    /**
     * 认证
     * @param builder
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        // 通过内存认证的方式初始化了两个用户及角色
        builder.inMemoryAuthentication().withUser("cs_user").password("{noop}password1").roles("USER").
                and()
                .withUser("cs_admin").password("{noop}password2").roles("ADMIN");
    }


    /**
     * 授权
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // http 基础认证
        http.csrf().disable().httpBasic();
        // 表单认证
        http.authorizeHttpRequests()
                .mvcMatchers(HttpMethod.GET, "/outsourcingSystems/*").permitAll()
                .mvcMatchers(HttpMethod.POST, "/outsourcingSystems/*").hasRole("USER")
                .mvcMatchers(HttpMethod.DELETE, "/outsourcingSystems/*").hasRole("ADMIN")
                .anyRequest().authenticated();
    }

}

​        

二、Spring Security核心原理解析

(一)Spring Security中的用户和认证

​    在Spring Security中:

​      UserDetails用来描述Spring Security中的用户

​      GrantedAuthority用来定义用户所能执行的操作权限

​      UserDetailsService定义了对UserDetails的查询操作

​      UserDetailsManager扩展了UserDetailsService,添加新增和修改用户功能。

​    1、用户

​    (1)UserDetails接口

​      UserDetails除了有获取权限、密码、用户名方法之外,还提供了判断用户是否已经失效、是否已经被锁定、凭证是否已失效、用户是否可用方法。

​      这四个方法看起来很简单,但是抽象的很好,因为用户是有过期时间的,那么就需要有是否已失效;

​      锁定是指现在不可用,但是过一会又可以使用,有一个加锁解锁的过程;

​      用户凭证是否已失效不太常用;

​      用户是否可用和是否锁住是两个概念,可用指的是这个用户是否是一种合理的状态,锁住是指当前用户本身是否被冻结或者其他;可用不代表已经解锁。

public interface UserDetails extends Serializable {
    //获取该用户的权限信息 
    Collection<? extends GrantedAuthority> getAuthorities();

    // 获取密码 
    String getPassword();

    // 获取用户名 
    String getUsername();

    // 判断该账户是否已失效 
    boolean isAccountNonExpired();

    // 判断该账户是否已被锁定 
    boolean isAccountNonLocked();

    // 判断该账户的凭证信息是否已失效 
    boolean isCredentialsNonExpired();

    // 判断该用户是否可用 
    boolean isEnabled();
}

​      另外Spring Security提供了MutableUserDetails扩展接口,该接口只提供了一个方法setPassword,即在密码变更时重新设置密码。

// 可变的UserDetails
interface MutableUserDetails extends UserDetails {
    //设置密码 
    void setPassword(String password);
}

​      构建UserDetails有两种方式,一种是使用User对象来构建,一种是使用User.UserBuilder来构建。

UserDetails user = User.withUsername("jianxiang").password("123456").authorities("read","write").accountExpired(false).disabled(true).build();
// 通过Builder构建UserDetails
User.UserBuilder builder = User.withUsername("jianxiang");
UserDetails user = builder.password("12345").authorities("read", "write").accountExpired(false).disabled(true).build();

​    (2)权限:GrantedAuthority

​      权限本身比较简单,只提供了获取权限信息方法getAuthority。

// 获取权限信息
public interface GrantedAuthority extends Serializable {
    //获取权限信息 
    String getAuthority();
}

​    (3)UserDetailsService:“读”和“写”职责分离

​      UserDetailsService使用了读写职责分离,读在UserDetailsService中,使用loadUserByUsername方法根据用户名获取用户信息,而对于用户的创建、更新、删除、修改密码、判断用户是否存在等操作,则是在扩展类UserDetailsManager中。

public interface UserDetailsService {
    //根据用户名获取用户信息 
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

public interface UserDetailsManager extends UserDetailsService {
    //创建用户 
    void createUser(UserDetails user);

    //更新用户 
    void updateUser(UserDetails user);

    //删除用户 
    void deleteUser(String username);

    //修改密码 
    void changePassword(String oldPassword, String newPassword);

    //判断指定用户名的用户是否存在 
    boolean userExists(String username);
}

​      UserDetailsManager有多个实现类,例如基于内存的、基于 JDBC 的,下面是基于 JDBC 的实现类JdbcUserDetailsManager。

​      例如下面代码是基于 JDBC 的,首先校验用户对象是否正确,然后通过getJdbcTemplate()获取了一个 jdbcTemplate,然后向数据库插入一条用户数据;如果有权限数据,也会对权限信息做插入操作。

public class JdbcUserDetailsManager extends JdbcDaoImpl implements UserDetailsManager

        public void createUser(final UserDetails user) {
            validateUserDetails(user);
            getJdbcTemplate().update(createUserSql, ps -> {
                ps.setString(1, user.getUsername());
                ps.setString(2, user.getPassword());
                ps.setBoolean(3, user.isEnabled());
                int paramCount = ps.getParameterMetaData().getParameterCount();
                if (paramCount > 3) {
                    ps.setBoolean(4, !user.isAccountNonLocked());
                    ps.setBoolean(5, !user.isAccountNonExpired());
                    ps.setBoolean(6, !user.isCredentialsNonExpired());
                }
            });
            if (getEnableAuthorities()) {
                insertUserAuthorities(user);
            }
        }
}

​    (4)认证配置

​    在配置认证时会使用链式操作,如下所示:

// 通过链式语法来完成用户信息的设置
.withUser("spring_user").password("password1").authorities("ROLE_USER")
.and()
.withUser("spring_admin").password("password2").authorities("ROLE_ADMIN");

​      链式操作主要是通过UserDetailsManagerConfigurer来实现的,首先withUser创建了一个UserDetailsBuilder对象,UserDetailsBuilder就是一个构造器,可以同构构造器模式来初始化用户信息配置。

public class UserDetailsManagerConfigurer {
    private final List<UserDetailsBuilder> userBuilders = new ArrayList<>();
    private final List<UserDetails> users = new ArrayList<>();

    public final C withUser(UserDetails userDetails) {
        this.users.add(userDetails);
        return (C) this;
    }

    public final UserDetailsBuilder withUser(String username) {
        UserDetailsBuilder userBuilder = new UserDetailsBuilder((C) this);
        userBuilder.username(username);
        this.userBuilders.add(userBuilder);
        return userBuilder;
    }

    public final class UserDetailsBuilder {
        private UserBuilder user;

        public UserDetailsBuilder authorities(GrantedAuthority... authorities) {
            this.user.authorities(authorities);
            return this;
        }
        ......
    }
}

​    (4)Spring Security中用户相关类结构图:

​      Spring Security用户相关的类图如下所示:

​        最核心的是UserDeatails,是对于用户的抽象,然后可以自己定义实现类User,Spring Security也提供了子接口MutableUserDeatils,也可以对该接口添加自定义的实现类MutableUser。

​        然后是UserDetailsService接口,提供用户的查询操作,UserDetailsManager是UserDetailsService的子接口,提供了对于用户的增删改操作。

​        UserDetailsManager有很多实现类,例如基于内存的、基于 JDBC的等,下面的类图是基于内存的实现类InMemoryUserDetailsManager,该实现要依赖UserDetails来做相关的增删改查处理。

​        所有的配置要通过AuthenticationManagerBuilder来进行配置,配置时要设置对应的UserDetailsManagerConfiger,例如下面的类图说明使用了内存进的方式,那么实际上依赖的是其子类InMemoryUserDetailsManagerConfiger,而UserDetailsManagerConfiger又要依赖对应的UserDetailsManager,下图也同样表示使用了UserDetailsManager的内存实现InMemoryUserDetailsManager。

​        

​    2、认证

​    (1)Spring Security中的认证对象

​      认证对象是Authentication,提供了关于认证相关的一些方法,具体如下所示。

public interface Authentication extends Principal, Serializable {
    //安全主体所具有的权限
    Collection<? extends GrantedAuthority> getAuthorities();

    //证明主体有效性的凭证
    Object getCredentials();

    //认证请求的明细信息
    Object getDetails();

    //主体的标识信息
    Object getPrincipal();

    //是否认证通过
    boolean isAuthenticated();

    //设置认证结果
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

​    (2)AuthenticationProvider 和 AuthenticationManager

​      对于认证对象来说,主要是如何做认证的,Spring Security提供了两个接口来处理认证,分别是AuthenticationProvider和AuthenticationManager。

​      其中AuthenticationProvider是对单一一个内容做认证,但是认证是个链式操作,可能存在一组认证,AuthenticationManager就负责做一组认证。

​      如下代码所示,AuthenticationManager的实现类是AuthenticationProvider,内部包含了AuthenticationProvider集合,在调用AuthenticationManager的认证方法authenticate时,会迭代调用所有AuthenticationProvider的authenticate方法进行认证。

public interface AuthenticationProvider {
    //执行认证,返回认证结果
    // 包含UserDetailsService,负责从内存、 数据库或LDAP中获取用户信息
    Authentication authenticate(Authentication authentication) throws AuthenticationException;

    //判断是否支持当前的认证对象
    boolean supports(Class<?> authentication);
}

public interface AuthenticationManager {
    //执行认证,返回认证结果
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

public class ProviderManager implements AuthenticationManager {
    //AuthenticationProvider列表
    // 迭代所有AuthenticationProvider实例并尝试用每个实例验证用户
    private List<AuthenticationProvider> providers = Collections.emptyList();
}

​      AuthenticationManager一般会在过滤器中调用,如下代码所示,组装了用户请求,然后调用AuthenticationManager的authenticate方法进行认证。

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        ......
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        ......
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        setDetails(request, authRequest);
        // 迭代所有AuthenticationManager执行认证操作
        return this.getAuthenticationManager().authenticate(authRequest);
    }
    ......
}

​    (3)Spring Security中认证相关类结构图

​      主要的类是AuthenticationProvider 和 AuthenticationManager,AuthenticationManager负责一组认证,而AuthenticationProvider负责单一场景认证,对于AuthenticationManager有实现类ProviderManager,因此ProviderManager中需要依赖AuthenticationProvider。

​      而AuthenticationProvider做认证需要使用UserDeatilService获取用户信息,因此在AuthenticationProvider的实现类AbstactUserDetailsAuthenticationProvider中需要依赖UserDetailsService;然后需要根据获取的用户信息匹配密码,因此需要基于用户名密码认证的接口Authentication,针对该接口,有封装用户名密码的实现类UsernamePasswordAuthenticationToken。

​        

(二)Spring Security过滤器机制

​    1、Spring Security过滤器

​    Spring Security的过滤器机制是直接实现了Servlet中的Filter。

​    Servlet中的Filter如下所示,提供了初始化过滤器、销毁过滤器和执行过滤三个方法。

public interface Filter {
    public void init(FilterConfig filterConfig) throws ServletException;

    // request:表示HTTP请求,使用该对象来获 取有关请求的详细信息
    // response:表示HTTP响应,使用该对象来构 建响应结果,然后再将其发送回 客户端或沿着过滤器链向后传递
    // chain:表示过滤器链,使用该对象将请 求转发到链中的下一个过滤器
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;

    public void destroy();
}

​    2、Spring Security过滤器链

​    Spring Security中的过滤器都直接或间接的实现了Servlet中的Filter接口。

​    Spring Security中的过滤器链如下图所示,首先经过用户名密码认证过滤器、Http Basic基础认证过滤器,然后在执行特定的过滤器,在这里可以做一些自定义的业务处理,例如打印日志等,然后经过Security拦截器来完成请求的权限控制,最后以Restful的方式进行返回。

​        

​    3、Spring Security内置过滤器

​    Spring Security内置了很多过滤器,详情如下所示:

​        

​    这些过滤器的序号是指在过滤器链中的序号,过滤器链会按照序号的顺序执行。

​    其中着重要关注的是UsernamePasswordAuthenticationFilter、 CasAuthenticationFilter、BasicAuthenticationFilter这一组内置的认证过滤器,以及权限控制过滤器 FilterSecurityInterceptor。

​    以UsernamePasswordAuthenticationFilter过滤器为例,其主要是使用用户名密码构建了认证对象,然后执行认证。

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 获取用户名和密码
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        ...
        username = username.trim();
        // 构建认证对象
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        setDetails(request, authRequest);
        // 执行认证
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

​    4、集成Filter的认证相关类结构图

​      这个比较简单,基本上将认证中的用户和认证类结构组合了起来,然后在UsernamePasswordAuthenticationFilter用调用了AuthenticationManager来执行过滤,而UsernamePasswordAuthenticationFilter需要依赖UsernamePasswordAuthenticationToken类和上下文SecurityContextHolder。

​        

​    5、实现自定义过滤器

​    自定义过滤器实现Filter接口,然后实现doFilter,在该方法中做想过的过滤操作即可。

​    如下自定义的过滤器为例,从请求头中获取请求数据UniqueRequestId,用以记录一次请求,做了日志输出后,调用filterChain.doFilter将请求继续在过滤器链上进行传递,这样才能完成一个完整的过滤器链。

​    也可以做的时候在某个过滤器中抛出异常,不再往后执行,也可以不再执行filterChain.doFilter,认为其就是最后一个过滤器,这也是常用的一种做法,但是一般情况下我们还是希望继续往后执行的。

public class LoggingFilter implements Filter {
    private final Logger logger = Logger.getLogger(AuthenticationLoggingFilter.class.getName());

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        // 从ServletRequest获取请求数据并记录
        String uniqueRequestId = httpRequest.getHeader("UniqueRequestId");
        logger.info("成功对请求进行了认证: " + uniqueRequestId);
        // 将请求继续在过滤器链上进行传递
        filterChain.doFilter(request, response);
    }
}

​    再升级另外一种做法,即在过滤器中获取一个标志位,如果存在标志位,则继续向后请求,如果不存在标志位,则直接返回一个没有标志位的异常状态码。

public class RequestValidationFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        String requestId = httpRequest.getHeader("SecurityFlag");
        if (requestId == null || requestId.isBlank()) {
            httpResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }
        filterChain.doFilter(request, response);
    }
}

​    6、过滤器的顺序

​    Spring Security内置的过滤器都带的有顺序,对于自定义的过滤器,也是需要根据业务需求来设置执行顺序的。

​    例如请求验证过滤器RequestValidationFilter,主要是用来确保那些没有携带有效请求头信息的请求不会执行不必要的用户认证。基于这种场景,把RequestValidationFilter放在BasicAuthenticationFilter之后就不是很合适了,因为用户已经完成了认证操作,这种过滤器一般会放在整个过滤器的第一个。

​    而对于日志过滤器LoggingFilter来说,位置位于请求校验RequestValidationFilter和认证BasicAuthenticationFilter之后最为合适。

​    对于过滤器的位置,Spring Security提供了一组方法:

​      addFilterBefore:将过滤器放在某个过滤器前面

​      addFilterAfter:将过滤器放在某个过滤器后面

​      addFilterAt:将过滤器放在某个位置

​      addFilter:将过滤器放在过滤器链的最后

​    例如下面的代码样例,使用addFilterBefore将RequestValidationFilter在过滤器放在BasicAuthenticationFilter过滤器之前,使用addFilterAfter将过滤器LoggingFilter放在过滤器BasicAuthenticationFilter之后。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.addFilterBefore(
                new RequestValidationFilter(), 
                BasicAuthenticationFilter.class)
            .addFilterAfter(
                new LoggingFilter(), 
                BasicAuthenticationFilter.class)
            .authorizeRequests()
            .anyRequest()
            .permitAll();
}

(三)Spring Security授权流程

​    Spring Security授权整体流程如下图所示,当http请求到达后:

​      首先通过过滤器链进行过滤,最后一个过滤器是FilterSecurityInterceptor;

​      过滤链执行完毕后,调用SecurityMetadataSource获取访问策略,即访问这个资源锁需要的权限,这个资源是在configure中配置的,配置完成后会存在SecurityMetadataSource中,物理上对于一个资源的权限以ConfigAttribute表示;

​      拿到了访问策略和用户的基本信息后,就可以调用访问决策管理器AccessDecisionManager执行授权决策,AccessDecisionManager进行投票决策,决策通过后进行返回,允许访问资源。

​    因此Spring Security的授权流程主要就是分为以上的三步,首先拦截请求,然后获取访问策略,最后执行授权决策。

​        

​    1、拦截请求:FilterSecurityInterceptor

​    拦截请求比较简单,就是在配置类中配置的http.authorizeRequests()

​    这句话一旦配置,就会启动FilterSecurityInterceptor过滤拦截器,在这个拦截器中,有invoke方法,在该方法中调用父类的beforeInvocation方法在调用资源之前做权限控制,调用完毕后执行父类的afterInvocation方法做后续处理。

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter

        public void invoke(FilterInvocation fi) throws IOException, ServletException {
            ......
            InterceptorStatusToken token = super.beforeInvocation(fi);
            ......
            super.afterInvocation(token, null);
        }
}

​    在 FilterSecurityInterceptor 的父类 AbstractSecurityInterceptor 中,首先获取代表权限的ConfigAttribute对象,然后调用authenticateIfRequired方法获取认证信息,最后调用accessDecisionManager.decide来执行授权。

​    对于authenticateIfRequired方法,其不是用来做认证的,认证在过滤器链中已经处理,这里主要是获取认证结果。

public abstract class AbstractSecurityInterceptor {
    protected InterceptorStatusToken beforeInvocation(Object object) {
        ......
        // 获取代表权限的ConfigAttribute对象
        Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
        ......
        // 获取认证信息
        Authentication authenticated = authenticateIfRequired();
        try {
            // 执行授权
            this.accessDecisionManager.decide(authenticated, object, attributes);
        } catch (AccessDeniedException accessDeniedException) {
            ......
        }
        ......
    }
}

​    2、获取访问策略 - MetadataSource

​    以下面的访问策略为例,表示任何请求都需要认证,有CREATE权限才可以访问。

​    authorizeRequests方法的返回结果是ExpressionInterceptUrlRegistry;anyRequest方法的返回结果是AuthorizedUrl;hasAuthority方法返回的是ExpressionInterceptUrlRegistry。

http
   .authorizeRequests() // 返回ExpressionInterceptUrlRegistry
   .anyRequest() // 返回AuthorizedUrl
   .hasAuthority("CREATE"); // 返回ExpressionInterceptUrlRegistry

​    ExpressionInterceptUrlRegistry是一个注册表,内部提供了acess方法来进行判断,在该方法中调用了interceptUrl方法来实现请求匹配器跟配置中的attribute做映射。

​    在interceptUrl方法中,对配置的一组匹配器requestMatchers进行遍历,然后将requestMatcher与configAttributes之间的映射关系形成一个Map添加到注册表中。

// 执行权限判断
public ExpressionInterceptUrlRegistry access(String attribute) {
    interceptUrl(requestMatchers, SecurityConfig.createList(attribute));
    return ExpressionUrlAuthorizationConfigurer.this.REGISTRY;
}

private void interceptUrl(Iterable<? extends RequestMatcher> requestMatchers, Collection<ConfigAttribute> configAttributes) {
    for (RequestMatcher requestMatcher : requestMatchers) {
        // 拦截URL,添加RequestMatcher与ConfigAttribute之间的映射关系
        REGISTRY.addMapping(new AbstractConfigAttributeRequestMatcherRegistry.UrlMapping(requestMatcher, configAttributes));
    }
}

​    3、执行授权决策 - AccessDecisionManager

​    上面的注册表会一直向后传递,最后传递到决策器AccessDecisionManager中。

​    在AccessDecisionManager中有好几种决策类型,下面代码使用了默认的决策类型,返回AffirmativeBased,即投票决策。

​    在AffirmativeBased中提供了decide方法,基于认证信息authentication和configAttributes来做授权和判断。授权判断又把一部分功能转移给AccessDecisionVoter,即投票器。然后调用AccessDecisionVoter的vote方法进行投票,如果结果是ACCESS_GRANTED,则表示通过。同时只要有一个状态是ACCESS_DENIED,则表示不通过。即只要有一个权限不匹配,则校验不通过。

    private AccessDecisionManager createDefaultAccessDecisionManager(H http) {
        AffirmativeBased result = new AffirmativeBased(getDecisionVoters(http));
        return postProcess(result);
    }

public class AffirmativeBased {
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
        int deny = 0;
        for (AccessDecisionVoter voter : getDecisionVoters()) {
            // 委托给一组AccessDecisionVoter对象执行投票, 只要其中有任意一个的结果是拒绝,就会抛出异常
            int result = voter.vote(authentication, object, configAttributes);
            switch (result) {
                case AccessDecisionVoter.ACCESS_GRANTED:
                    return;
                case AccessDecisionVoter.ACCESS_DENIED:
                    deny++;
                    break;
                default:
                    break;
            }
        }
        if (deny > 0) {
            throw new AccessDeniedException(...);
        }
    }
}

​    4、执行授权决策 - AccessDecisionVoter

​    如上面提到的,真正执行决策是是AccessDecisionVoter接口,其提供了一个vote方法来执行决策。

​    基于Web请求,其真正进行投票的是WebExpressionVoter实现类,在该实现类的vote方法中,会从配置中获取SpEL表达式,然后通过Spring所提供的SpEL表达式语言进行评估。

public interface AccessDecisionVoter<S> {
    int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);
}

public class WebExpressionVoter implements AccessDecisionVoter<FilterInvocation> {
    public int vote(Authentication authentication, FilterInvocation fi, Collection<ConfigAttribute> attributes) { 
        ......
        WebExpressionConfigAttribute weca = findConfigAttribute(attributes); 
        ......
        EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, fi);
        ctx = weca.postProcess(ctx, fi);
        return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED : ACCESS_DENIED;
    }
}

public final class ExpressionUtils {
    public static boolean evaluateAsBoolean(Expression expr, EvaluationContext ctx) {
        // 通过Spring所提供的SpEL表达式语言进行评估
        return expr.getValue(ctx, Boolean.class);
    }
}

​    5、Spring Security授权相关核心类图

​      授权的入口是在整个拦截器走到最后的FilterSecurityInterception中调用父类AbstractSecurityInterceptor的beforeInvocation方法来的。

​      在AbstractSecurityInterceptor中使用AccessDecisionManager来进行授权操作,而授权操作需要依赖代表权限的对象ConfigAttribute。

​      AccessDecisionManager的实现类是AbstractAccessDecisionManager,该类有好几个子类,例如AffirmativeBased等,在AffirmativeBased中要进行投票,具体的投票是AccessDecisionVoter接口来实现的,而真正投票的是其实现类WebExpressionVoter。

​        

三、Spring Security扩展

(一)实现定制化用户认证

​    要实现定制化用户认证方案,首先要扩展UserDetails,可以自定义用户实体,然后扩展UserDetailsService,用来自定义用户查询,例如查询数据库等操作,但是不会使用UserDetailsManager,因为用户的增删改一般会有其他的流程去操作,有可能都用不到UserDetailsManager;然后还要扩展AuthenticationProvider来自定义认证方式与算法;最后、需要整合定制化配置,将相关的配做做整合。

​    1、定制化步骤分析

​    (1)扩展UserDetails

​        自定义用户实体实现UserDetails接口,并实现其方法即可。

public class SpringUser implements UserDetails {
    private static final long serialVersionUID = 1L;
    private Long id;
    private final String username;
    private final String password;
    private final String phoneNumber; //省略getter/setter

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

​    (2)扩展UserDetailsService

​        自定义UserDetailsService接口实现类,重新其中的根据用户名获取用户的方法,在该方法中,我们可以按照自己的设定获取用户信息。

// 通过Spring Data JPA实现数据访问
public interface SpringUserRepository extends CrudRepository<SpringUser, Long> {
    SpringUser findByUsername(String username);
}

@Service
public class SpringUserDetailsService implements UserDetailsService {
    @Autowired
    private SpringUserRepository repository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SpringUser user = repository.findByUsername(username);
        if (user != null) {
            return user;
        }
        throw new UsernameNotFoundException("SpringUser '" + username + "' not found");
    }
}

​    (3)扩展AuthenticationProvider

​        在用户请求时,首先通过AuthenticationProvider调用UserDeatilsService获取用户信息,如果用户不存在,则直接抛出异常,如果用户存在,则调用PasswordEncoder判断密码是否匹配。

​        那么我们就需要自定义AuthenticationProvider的实现类,并重写认证方法authenticate,在该方法中,首先调用UserDetailsService获取用户信息,然后使用自定义的认证方式对密码做认证。

​        

@Component
public class SpringAuthenticationProvider implements AuthenticationProvider {
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) {
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();
        UserDetails user = userDetailsService.loadUserByUsername(username);
        if (passwordEncoder.matches(password, user.getPassword())) {
            // 认证成功,返回认证信息
            return new UsernamePasswordAuthenticationToken(username, password, u.getAuthorities());
        } else {
            throw new BadCredentialsException("The username or password is wrong!");
        }
    }

    @Override
    public boolean supports(Class<?> authenticationType) {
        return authenticationType.equals(UsernamePasswordAuthenticationToken.class);
    }
}

​    (4)整合定制化配置

​        自定义了UserDeatils、UserDetailsService、AuthenticationProvider,就需要通过配置整合让整个自定义配置生效。

​        首先自定义一个配置类继承WebSecurityConfigurerAdapter,并重写configure方法,在方法中设置上自定义的对象。

@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService springUserDetailsService;
    @Autowired
    private AuthenticationProvider springAuthenticationProvider;

    // 覆写configure方法,指定UserDetailsService 和AuthenticationProvider
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(springUserDetailsService).authenticationProvider(springAuthenticationProvider);
    }
}

​    2、定制化用户认证详细实现

​    (1)引入依赖

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

​    (2)创建表

​        创建用户表、权限表,同时插入用户数据和权限数据

CREATE TABLE IF NOT EXISTS `cs_user` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `username` VARCHAR(45) NOT NULL,
  `password` TEXT NOT NULL,
  `password_encoder_type` VARCHAR(45) NOT NULL,
  PRIMARY KEY (`id`));

CREATE TABLE IF NOT EXISTS `cs_authority` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(45) NOT NULL,
  `user` INT NOT NULL,
  PRIMARY KEY (`id`));


-- 用户名:lcl;密码:12345
INSERT IGNORE INTO `cs_user` (`id`, `username`, `password`, `password_encoder_type`) VALUES ('1', 'lcl', '$2a$10$xn3LI/AjqicFYZFruSwve.681477XaVNaUQbr1gioaWPn4t1KsnmG', 'BCRYPT');

INSERT IGNORE INTO `cs_authority` (`id`, `name`, `user`) VALUES ('1', 'READ', '1');
INSERT IGNORE INTO `cs_authority` (`id`, `name`, `user`) VALUES ('2', 'WRITE', '1');

​    (3)创建实体类与数据库操作类

​        这里使用Spring JPA来操作数据库

@Entity
@Table(name = "cs_user")
@Data
public class CustomerUser implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    private String username;
    private String password;

    @Enumerated(EnumType.STRING)
    private PasswordEncoderType passwordEncoderType;

    @OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
    private List<CustomerAuthority> authorities;
}

@Entity
@Table(name = "cs_authority")
@Data
public class CustomerAuthority {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    private String name;

    @JoinColumn(name = "user")
    @ManyToOne
    private CustomerUser user;
}

public interface CustomerUserRepository extends JpaRepository<CustomerUser, Long> {
    Optional<CustomerUser> findCustomerUserByUsername(String userName);
}

​    (4)自定义UserDetails

​        这里主要说一下getAuthorities方法,该方法用于获取用户的权限集合,具体实现是从CustomerUser对象中获取用户的权限,将每个权限转换为SimpleGrantedAuthority对象,然后将它们收集成一个集合并返回

@AllArgsConstructor
public class CustomerUserDetails implements UserDetails {

    private CustomerUser customerUser;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return customerUser.getAuthorities().stream()
                .map(a -> new SimpleGrantedAuthority(a.getName()))
                        .collect(Collectors.toList());
    }

    @Override
    public String getPassword() {
        return customerUser.getPassword();
    }

    @Override
    public String getUsername() {
        return customerUser.getUsername();
    }

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

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

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

    @Override
    public boolean isEnabled() {
        return false;
    }

    public final CustomerUser getCustomerUser(){
        return customerUser;
    }
}

​    (5)定义和配置加密类,并设置加密类型枚举

public enum PasswordEncoderType {
    BCRYPT, SCRYPT;
}

@Configuration
public class PasswordEncoderConfig {

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SCryptPasswordEncoder sCryptPasswordEncoder(){
        return new SCryptPasswordEncoder();
    }
}

​    (6)自定义UserDetailsService

​        实现loadUserByUsername方法,调用CustomerUserRepository根据用户名获取用户信息并将其封装为自定义的UserDetails,获取不到就抛出异常。

@Service
public class CustomerUserDetailsService implements UserDetailsService {

    @Autowired
    private CustomerUserRepository customerUserRepository;

    @Override
    public CustomerUserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Supplier<UsernameNotFoundException> supplier = () -> new UsernameNotFoundException("username" + username + "bot found");
        CustomerUser customerUser = customerUserRepository.findCustomerUserByUsername(username)
                .orElseThrow(supplier);
        return new CustomerUserDetails(customerUser);
    }
}

​    (7)自定义认证接口AuthenticationProvider

​      在实现类中实现authenticate方法,首先从Authentication中获取用户名密码,使用自定义的UserDetailsService根据用户名获取自定义的用户对象CustomerUserDetails,然后根据数据库配置的不同加密算法校验密码是否正确。

​      在校验密码时,使用密码编码器对用户输入的密码进行验证,如果匹配成功,则返回一个经过认证的UsernamePasswordAuthenticationToken对象;否则,抛出BadCredentialsException异常表示认证失败。

​      在supports方法中设置了AuthenticationProvider支持UsernamePasswordAuthenticationToken的请求类型。

@Service
public class AuthenticationProviderService implements AuthenticationProvider {

    @Autowired
    private CustomerUserDetailsService customerUserDetailsService;
    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    @Autowired
    private SCryptPasswordEncoder sCryptPasswordEncoder;

    /**
     * 认证
     * @param authentication
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String userName = authentication.getName();
        String password = authentication.getCredentials().toString();
        CustomerUserDetails customerUserDetails = customerUserDetailsService.loadUserByUsername(userName);
        // 根据配置加密算法验证密码
        switch (customerUserDetails.getCustomerUser().getPasswordEncoderType()) {
            case BCRYPT:
                return checkPassword(customerUserDetails, password, bCryptPasswordEncoder);
            case SCRYPT:
                return checkPassword(customerUserDetails, password, sCryptPasswordEncoder);

        }
        throw new BadCredentialsException("Bad Credentials");
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }


    private Authentication checkPassword(CustomerUserDetails customerUserDetails, String rowPassword, PasswordEncoder passwordEncoder){
        if (passwordEncoder.matches(rowPassword, customerUserDetails.getPassword())) {
            return new UsernamePasswordAuthenticationToken(customerUserDetails.getUsername(), customerUserDetails.getPassword(), customerUserDetails.getAuthorities());
        }else {
            throw new BadCredentialsException("Bad Credentials");
        }
    }
}

​    (8)Security配置类

​      在配置类中配置认证信息:所有的请求都需要认证,同时配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  
    @Autowired
    private AuthenticationProviderService authenticationProviderService;
  
    /**
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic();
        http.authorizeHttpRequests().anyRequest().authenticated();
    }


    /**
     * 认证
     * @param builder
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        builder.authenticationProvider(authenticationProviderService);
    }


}

​    3、项目引入自定义认证组件

​    (1)引入自定义组件

<dependency>
    <groupId>com.lcl.galaxy</groupId>
    <artifactId>microservice-security-auth-service</artifactId>
    <version>${parent.version}</version>
</dependency>

​    (2)配置

​        假设该应用项目使用的是mybatis-plus

server:
  port: 9004

spring:
  application:
    name: chat-service
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.249.130:8848
        namespace: dae2f8c4-a44a-4143-afc5-1f8aaa84c72c
        group: LCL_GALAXY_GROUP
        cluster-name: beijing
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: Asia/Shanghai
  datasource:
    dynamic:
      primary: master
      druid:
        initial-size: 3
        min-idle: 3
        max-active: 40
        max-wait: 60000
      datasource:
        master:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/chat_security?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
          username: root
          password: 123456


mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      # 逻辑删除字段名
      logic-delete-field: is_deleted
      # 逻辑删除字面值:未删除为0
      logic-not-delete-value: 0
      # 逻辑删除字面值:删除为1
      logic-delete-value: 1

​    (3)启动类配置

​      这里需要注意几个容易踩坑的点:

​        由于应用项目和自定义的认证组件包路径不一致,需要在@SpringBootApplication注解中使用scanBasePackages配置包路径,保证注解都可以被扫描到

​        因为同时使用了Mybatis和Spring JPA,因此要使用@MapperScan和@EnableJpaRepositories设置不同的扫描路径

​        因为使用Stpring JPA,因此要用@EntityScan设置实体的扫描路径

@SpringBootApplication(scanBasePackages = {"com.lcl.galaxy.microservice.frontend.chat","com.lcl.galaxy.microservice.service"})
@MapperScan("com.lcl.galaxy.microservice.frontend.chat.mapper")
@EnableJpaRepositories("com.lcl.galaxy.microservice.service.repository")
@EntityScan("com.lcl.galaxy.microservice.service.domain")
public class FronttendImChatApplication {
    public static void main(String[] args) {
        SpringApplication.run(FronttendImChatApplication.class, args);
    }
}

​    (4)验证

​        在验证访问时,就需要加上认证信息。

GET http://localhost:9004/chats/
Accept: application/json
Authorization: Basic lcl 12345

(二)使用认证缓存

​    每次认证请求都会需要执行数据库操作,但是用户名密码时很少发生变化的,因此可以使用缓存来替代每次都查询数据库。

​    1、Spring Security中的缓存

​    (1)认证缓存定义

​      在Spring Security中定义了用户缓存UserCache,提供了三个从缓存获取用户信息、把用户信息放入缓存中、从缓存中移除用户信息三个方法。

​      在Spring Security中UserCache有三个实现类,分别是EhCacheBasedUserCache、NullUserCache、SpringCacheBaseUserCache,NullUserCache表示不使用用户缓存,用的很少,一般使用EhCacheBasedUserCache和SpringCacheBaseUserCache。

public interface UserCache {
    //从缓存获取用户信息 
    UserDetails getUserFromCache(String username);

    //把用户信息放入缓存中 
    void putUserInCache(UserDetails user);

    //从缓存中移除用户信息 
    void removeUserFromCache(String username);
}

​    (2)认证缓存实现

​      以SpringCacheBasedUserCache为例,使用了Spring内置的Cache,对于UserCache的实现也都比较好理解。

public class SpringCacheBasedUserCache implements UserCache {
    // 使用了Spring内置Cache
    private final Cache cache;

    public UserDetails getUserFromCache(String username) {
        Cache.ValueWrapper element = username != null ? cache.get(username) : null;
        if (element == null) {
            return null;
        } else {
            return (UserDetails) element.get();
        }
    }

    public void putUserInCache(UserDetails user) {
        cache.put(user.getUsername(), user);
    }

    public void removeUserFromCache(UserDetails user) {
        this.removeUserFromCache(user.getUsername());
    }

    public void removeUserFromCache(String username) {
        cache.evict(username);
    }
}

​    (3)认证缓存应用

​      再认证时要是使用缓存,首先获取用户名,然后需要从UserCache中获取UserDetails对象,如果为空,则通过DaoAuthenticationProvider从数据库中进行获取,如果获取到,就将UserDetails放入缓存中;最后调用createSuccessAuthentication来做认证并返回一个认证结果。

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    // 获取用户名
    String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
    boolean cacheWasUsed = true;
    // 从UserCache中获取UserDetails
    UserDetails user = this.userCache.getUserFromCache(username);
    if (user == null) {
        cacheWasUsed = false;
        try {
            // 如果UserCache中没有目标对象,则通过DaoAuthenticationProvider从数据库中进行获取
            user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
        } catch (UsernameNotFoundException notFound) {
        }
    }
    ......
    postAuthenticationChecks.check(user);
    if (!cacheWasUsed) {
        // 将获取的UserDetails对象放入UserCache中
        this.userCache.putUserInCache(user);
    }

    Object principalToReturn = user;
    return createSuccessAuthentication(principalToReturn, authentication, user);
}

​    (4)实现自定义认证缓存 - 配置UserCache

​      上面提到的都是底层源码的实现,而真正实现自定义认证缓存,就需要在Spring中注入UserCache和管理缓存的CacheManager,为了更灵活的配置,可以设置生产UserCache和UserManager的工厂Bean。

@Configuration
@EnableCaching
public class SpringAuthCacheConfig {
    @Bean
    public EhCacheFactoryBean ehCacheFactoryBean() {
        EhCacheFactoryBean ehCacheFactory = new EhCacheFactoryBean();
        ehCacheFactory.setCacheManager(cacheManagerFactoryBean().getObject());
        return ehCacheFactory;
    }

    @Bean
    public CacheManager cacheManager() {
        return new EhCacheCacheManager(cacheManagerFactoryBean().getObject());
    }

    @Bean
    public EhCacheManagerFactoryBean cacheManagerFactoryBean() {
        EhCacheManagerFactoryBean cacheManager = new EhCacheManagerFactoryBean();
        return cacheManager;
    }

    @Bean
    public UserCache userCache() {
        EhCacheBasedUserCache userCache = new EhCacheBasedUserCache();
        userCache.setCache(ehCacheFactoryBean().getObject());
        return userCache;
    }
}

​    (5)实现自定义认证缓存 - 集成UserCache

​      设置了UserCache了,那么在Spring Security中需要设置拦截器AuthenticationProvider中使用UserCache进行认证。

@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserCache userCache;
    @Autowired
    private PasswordEncoder passwordEncoder;

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

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setPasswordEncoder(passwordEncoder);
        authenticationProvider.setUserCache(userCache);
        authenticationProvider.setUserDetailsService(userDetailsService());
        return authenticationProvider;
    }
}

​    2、自定义认证缓存实现(以EhCacheBasedUserCache为例)

​      下面是一个完整的自定义Spring Security Cache的实现。

​    (1)配置数据库

​      为了演示方便,暂时先使用内存数据库h2来作为数据库。

​      首先是相关的表和数据

-- 位于 org.springframework.security.core.userdetails.jdbc包中
CREATE TABLE IF NOT EXISTS users(
    username varchar_ignorecase(50) NOT NULL PRIMARY KEY,
    password VARCHAR_IGNORECASE(500) NOT NULL,
    enabled BOOLEAN NOT NULL);
CREATE TABLE IF NOT EXISTS authorities(
    username varchar_ignorecase(50) NOT NULL,
    authority varchar_ignorecase(50) NOT NULL,
    CONSTRAINT fk_authorities_users FOREIGN KEY(username) REFERENCES users(username));

CREATE UNIQUE INDEX IF NOT EXISTS ix_auth_username ON authorities (username,authority);
--user: customer; password: Test@123
INSERT INTO users(username, password, enabled) VALUES ('customer', '$2a$10$P.2FmSPSL6nt7BQmAyWhv.Z2g5Gy0jMDcHpA6UlfEmgMKgz2yL4Pu', true);

--user: manager; password: Test@123
INSERT INTO users(username, password, enabled) VALUES ('manager', '$2a$10$P.2FmSPSL6nt7BQmAyWhv.Z2g5Gy0jMDcHpA6UlfEmgMKgz2yL4Pu', true);
INSERT INTO authorities(username, authority) VALUES ('customer', 'USER');
INSERT INTO authorities(username, authority) VALUES ('manager', 'USER');
INSERT INTO authorities(username, authority) VALUES ('manager', 'ADMIN');

​    (2)配置依赖

<dependency>
   <groupId>com.h2database</groupId>
   <artifactId>h2</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
   <groupId>net.sf.ehcache</groupId>
   <artifactId>ehcache</artifactId>
</dependency>

​    (3)数据库操作配置

​      首先使用EmbeddedDatabaseBuilder来创建了一个使用h2数据库的数据源,然后自定义JDBCTemplat和事务管理器PlatformTransactionManager,并将自定义的数据源注入。

@Configuration
@EnableTransactionManagement
public class SpringDatabaseConfig {

    @Bean
    public NamedParameterJdbcTemplate myJdbcTemplate(){
        return new NamedParameterJdbcTemplate(dataSource());
    }

    @Bean
    public DataSource dataSource(){
        EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
        EmbeddedDatabase db = builder.setType(EmbeddedDatabaseType.H2)
                .addScript("schema.sql")
                .addScript("user.sql")
                .addScript("authorities.sql")
                .build();
        return db;
    }

    @Bean
    public PlatformTransactionManager transactionManager(){
        return new DataSourceTransactionManager(dataSource());
    }
}

​    (4)设置EhCache相关参数

<ehcache>
    <diskStore path="java.io.tmpdir"/>
    <defaultCache
            maxEntriesLocalHeap="10000"
            eternal="false"
            timeToIdleSeconds="60"
            timeToLiveSeconds="300"
            maxEntriesLocalDisk="10000000"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU">
        <persistence strategy="localTempSwap"/>
    </defaultCache>
</ehcache>

​    (5)自定义UserCache和CacheManager

​      自定义UserCache和CacheManager,其中UserCache使用EhCache作为缓存,CacheManager使用EhCacheCacheManager作为缓存管理器。

@Configuration
@EnableCaching
public class EhCacheAuthCacheConfig {

    @Bean
    public UserCache userCache(){
        EhCacheBasedUserCache userCache = new EhCacheBasedUserCache();
        userCache.setCache(ehCacheFactoryBean().getObject());
        return userCache;
    }

    @Bean
    public EhCacheFactoryBean ehCacheFactoryBean(){
        EhCacheFactoryBean ehCacheFactoryBean = new EhCacheFactoryBean();
        ehCacheFactoryBean.setCacheManager(ehCacheManagerFactoryBean().getObject());
        return ehCacheFactoryBean;
    }

    @Bean
    public CacheManager cacheManager(){
        return new EhCacheCacheManager(ehCacheManagerFactoryBean().getObject());
    }

    @Bean
    public EhCacheManagerFactoryBean ehCacheManagerFactoryBean(){
        return new EhCacheManagerFactoryBean();
    }
}

​    (6)在Spring Security中配置UserCache

​      再认证中配置自定义的认证器AuthenticationProvider,在自定义的AuthenticationProvider中,首先创建了一个使用数据库操作的认证器DaoAuthenticationProvider,然后在DaoAuthenticationProvider中设置使用用户缓存,即使用自定义的UserCache。

​      http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) 这段代码是用来配置 Spring Security 中的会话管理策略,特别是关于会话创建的策略。sessionManagement() 方法表示开始配置会话管理相关的设置,sessionCreationPolicy(SessionCreationPolicy.STATELESS) 则设置了会话创建的策略为 sessionCreationPolicy.STATELESS,表示不会在服务器端创建任何会话,也不会使用会话来存储用户的状态信息。在每次请求中,用户的认证信息将被重新验证,而不会依赖于会话来维护认证状态。

​      使用 SessionCreationPolicy.STATELESS 策略时,适用于实现无状态的认证,通常用于基于令牌(Token)的认证机制,比如 OAuth、JWT 等。这种策略适合于分布式系统,不需要维护会话状态,从而避免了会话跟踪带来的复杂性和性能开销。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private DataSource dataSource;
    @Autowired
    private UserCache userCache;
    @Autowired
    private PasswordEncoder passwordEncoder;


    /**
     * 认证
     * @param builder
     * @throws Exception
     */
    @Override
    @Autowired
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        builder.eraseCredentials(false)
                .authenticationProvider(authenticationProvider())
                .jdbcAuthentication()
                .dataSource(dataSource);
    }

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

    @Bean
    public AuthenticationProvider authenticationProvider(){
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setPasswordEncoder(passwordEncoder);
        authenticationProvider.setUserCache(userCache);
        authenticationProvider.setUserDetailsService(userDetailsService());
        return authenticationProvider;
    }

    /**
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .httpBasic();
    }
}

​    (7)验证

​      在访问时,只有第一次访问了数据库并将数据放入缓存,输出了Cache put: customer,第二次访问直接命中了缓存,日志输出Cache hit: true; username: customer

2023-08-05 17:56:20.696 DEBUG 98616 --- [nio-8080-exec-1] o.s.s.c.u.cache.EhCacheBasedUserCache    : Cache put: customer
2023-08-05 17:56:25.669 DEBUG 98616 --- [nio-8080-exec-2] o.s.s.c.u.cache.EhCacheBasedUserCache    : Cache hit: true; username: customer

​    3、自定义认证缓存实现(以SpringCacheBaseUserCache为例)

​    直接将上述的EhCacheAuthCacheConfig替换为下面的代码即可。

​    @EnableCaching:这个注解标记在 SpringAuthCacheConfig 类上,表示启用 Spring 的缓存功能。cacheManager(): 这个方法创建了一个 SimpleCacheManager,并设置了一个缓存 userCache,使用了 ConcurrentMapCache 来实现。这里的 ConcurrentMapCache 是 Spring 框架提供的一个简单的缓存实现,它将数据存储在内存中的并发哈希映射中。

​    userCache(): 这个方法创建了一个 SpringCacheBasedUserCache,并将之前创建的缓存 userCache 传递给它。这样就将缓存的实现和管理与 UserCache 关联起来了。

​    通过这样的配置,你可以使用 SpringCacheBaseUserCache 来实现自定义的认证缓存,同时使用了 Spring 框架提供的缓存管理和缓存实现,省去了一些复杂的配置步骤。这种方式适合于简单的场景或者对缓存要求不高的情况下。如果需要更复杂的缓存配置或者其他特定的缓存实现,可以继续使用之前的配置方式。

@Configuration
@EnableCaching
public class SpringAuthCacheConfig {

    @Bean
    public CacheManager cacheManager(){
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("userCache")));
        return cacheManager;
    }

    @Bean
    public UserCache userCache(){
        Cache cache = cacheManager().getCache("userCache");
        return new SpringCacheBasedUserCache(cache);
    }

}

(三)多因子认证流程及原理

​    单纯使用用户名密码的方式不是特别安全,因此会采用多步认证的方式,也就是所谓的多因子认证(Multi-Factor Authentication,MFA)

​    多因子认证架构是有一个登录服务和一个多因子认证服务,登录服务是通过集成多因子认证服务,完成具体的认证操作,多因子认证服务同时提供基于用户名+密码以及用户名+安全码的认证形式。

​    1、多因子认证实现流程

​    首先第一步是用用户名+密码获取一个安全码,然后第二步是使用用户名+安全码的方式进行验证。

​        

​    安全码是一个动态生成的过程,在访问时,如果安全码不存在则新增安全码,如果存在则刷新安全码。

​        

​    多因子认证需要自定义过滤器,在过滤器中,从请求头中获取用户名、密码、安全码,如果安全码为空,则使用用户名密码认证,如果安全码不为空,则使用用户名和安全码认证。

@Component
public class MfaFilter extends OncePerRequestFilter {
    @Autowired
    private AuthenticationManager authenticationManager;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String username = request.getHeader("username");
        String password = request.getHeader("password");
        String code = request.getHeader("code");
        if (code == null) {
            Authentication userCredentialAuthentication = new UserCredentialAuthentication(username, password);
            authenticationManager.authenticate(userCredentialAuthentication);
        } else {
            Authentication securityCodeAuthentication = new SecurityCodeAuthentication(username, code);
            authenticationManager.authenticate(securityCodeAuthentication);
            // 通过消息头返回安全码
            response.setHeader("SecurityCode", code);
        }
    }
}

​    同时需要自定义认证器AuthenticationProvider,获取认证对象中的用户名和安全码,调用认证服务完成认证,如果认证成功,返回一个认证成功的对象,否则则抛出安全码认证失败的异常。

@Component
public class SecurityCodeAuthenticationProvider implements AuthenticationProvider {
    @Autowired
    private MfaAcl mfaAcl;

    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String code = String.valueOf(authentication.getCredentials());
        // 调用认证服务完成认证
        boolean result = mfaAcl.validateSecurityCode(username, code);
        if (result) {
            return new SecurityCodeAuthentication(username, code);
        } else {
            throw new BadCredentialsException("安全码认证失败");
        }
    }

    public boolean supports(Class<?> aClass) {
        return SecurityCodeAuthentication.class.isAssignableFrom(aClass);
    }
}

​    多因子认证相关类结构图:

​      多因子认证首先要有一个自定义的多因子认证过滤器MfaFilter、在该过滤器中依赖认证管理器AuthenticationManager,AuthenticationManager需要两个认证器,分别是UserCredentialAuthenticationProvider和SecurityCodeAuthenticationProvider,分别验证用户名密码和用户名安全码,这两个AuthenticationProvider都需要远程调用MfaAcl做认证,同时这两个AuthenticationProvider又依赖对应的Authentication。

​        

(四)多因子认证实现

​    多因子认证实现主要包含两部分,一个是服务端,主要提供使用验证用户名密码和验证用户名安全码,同时负责保存用户名密码、用户名安全码,以及生成和更新安全码的操作。一个是客户端项目,该项目对请求做拦截,先调用mfa服务验证用户名密码,再调用mfa服务验证用户名安全码。

​    1、mfa服务

​    这是一个简单的使用JPA操作数据库的web项目

​    (1)pom依赖

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

​    (2)数据库相关内容

​      主要提供 JPA 需要的相关类,比较简单,直接上代码

@Entity
@Data
public class SecurityCode {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String username;
    private String code;

}
@Entity
@Data
public class UserCredential {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String username;
    private String password;
}
public interface SecurityCodeRepository extends JpaRepository<SecurityCode, Integer> {

    SecurityCode findSecurityCodeByUsername(String username);
}
public interface UserCredentialRepository extends JpaRepository<UserCredential, Integer> {
    UserCredential findUserCredentialByUsername(String username);
}

​    (3)生成安全码工具类

public final class SecurityCodeUtils {

    private SecurityCodeUtils() {}

    public static String generateSecurityCode() {
        String code;

        try {
            SecureRandom random = SecureRandom.getInstanceStrong();
            code = String.valueOf(random.nextInt(900000) + 100000);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("安全码生成失败");
        }

        return code;
    }
}

​    (4)mfa 验证

​      主要提供两个方法, 一个是验证用户名密码是否正确,一个是验证用户名安全码是否正确。

​      以验证用户名密码为例,首先根据用户名从数据库中获取数据,使用Spring Security中的加密对象passwordEncoder验证密码是否正确,如果正确,新增或刷新数据库中该用户的安全码。

@Service
public class MfaService {

    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private UserCredentialRepository userCredentialRepository;
    @Autowired
    private SecurityCodeRepository securityCodeRepository;

    public void addUserCredential(UserCredential userCredential){
        userCredential.setPassword(passwordEncoder.encode(userCredential.getPassword()));
        userCredentialRepository.save(userCredential);
    }

    public void validateUserCredential(UserCredential userCredentialToValidate) {
        UserCredential userCredential = userCredentialRepository.findUserCredentialByUsername(userCredentialToValidate.getUsername());
        if(!Objects.isNull(userCredential)) {
            if(passwordEncoder.matches(userCredentialToValidate.getPassword(), userCredential.getPassword())){
                generateOrRefreshSecurityCode(userCredential);
            } else {
                throw new BadCredentialsException("用户名密码错误");
            }
        }else {
            throw new BadCredentialsException("用户名密码错误");
        }
    }

    public boolean validateSecurityCode(SecurityCode securityCodeToValidate){
        SecurityCode securityCode = securityCodeRepository.findSecurityCodeByUsername(securityCodeToValidate.getUsername());
        if(!Objects.isNull(securityCode) && securityCode.getCode().equals(securityCodeToValidate.getCode())){
            return true;
        }
        return false;
    }


    private void generateOrRefreshSecurityCode(UserCredential userCredential){
        String generateSecurityCode = SecurityCodeUtils.generateSecurityCode();
        SecurityCode securityCode = securityCodeRepository.findSecurityCodeByUsername(userCredential.getUsername());
        if(Objects.isNull(securityCode)){
            securityCode = new SecurityCode();
            securityCode.setUsername(userCredential.getUsername());
            securityCode.setCode(generateSecurityCode);
        }else {
            securityCode.setCode(generateSecurityCode);
        }
        securityCodeRepository.save(securityCode);
    }
}

​    (5)对外提供认证服务

​      提供服务让客户端做认证访问。

@RestController
public class MfaController {

    @Autowired
    private MfaService mfaService;

    //添加UserCredential
    @PostMapping("/userCredential/add")
    public void addUserCredential(@RequestBody UserCredential userCredential) {
        mfaService.addUserCredential(userCredential);
    }

    //通过用户名/密码对用户进行首次认证
    @PostMapping("/userCredential/validate")
    public void validateUserCredential(@RequestBody UserCredential userCredential) {
        mfaService.validateUserCredential(userCredential);
    }

    //通过用户名/安全码进行二次认证
    @PostMapping("/securityCode/validate")
    public void validateSecurityCode(@RequestBody SecurityCode securityCode, HttpServletResponse response) {
        if (mfaService.validateSecurityCode(securityCode)) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        }
    }
}

​    (6)Spring Security配置

​      这个项目时不需要Spring Security配置的,但是使用了Spring Security中的PasswordEncoder来做密码和安全码加密认证,因此需要配置PasswordEncoder实现类。

​      另外在pom文件中引入了spring-boot-starter-security,如果不做任何配置,那么任何请求都会被拦截,而对于mfa服务来说,所有的服务都不被拦截才是合理的,因此需要做以下配置。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.authorizeRequests()
                .anyRequest().permitAll();
    }
}

​    2、客户端

​      先说一下总体思路:

​        自定义认证对象:以用户名安全码认证为例,自定义SecurityCodeAuthentication

​        自定义认证器:在认证器中的supports方法中设定其支持的认证对象为自定义的认证对象,以用户名安全码认证为例,自定义SecurityCodeAuthenticationProvider,并在supports方法中配置可支持的认证对象。

​        自定义过滤器:根据不同的情况封装不同的认证对象,并将其放入认证管理器做认证处理,这样认证管理器就可以从认证器中获取可以处理该认证对象的认证器来做认证处理。

​        认证配置:在Spring Security配置中,首先需要将自定义的认证器放入认证管理器中,同时也需要将过滤器放入整个过滤器链中。

​    (1)MfaAcl

​      MfaAcl指多因子认证(Multi-Factor Authentication)中的访问控制列表(Access Control List)。

​      在该类中,封装了使用用户名密码和用户名安全码验证的方法,在方法中,使用RestTemplate远程访问上面的Mfa服务做验证。

@Data
public class MfaDto {

    private String username;
    private String password;
    private String code;
}
@Component
public class MfaAcl {
    @Autowired
    private RestTemplate restTemplate;

    @Value("${mfa.service.url}")
    private String mfaServiceUrl;

    public void validateUserCredential(String username, String password) {
        String url = mfaServiceUrl + "/userCredential/validate";

        MfaDto body = new MfaDto();
        body.setUsername(username);
        body.setPassword(password);

        HttpEntity<MfaDto> request = new HttpEntity<MfaDto>(body);

        restTemplate.postForEntity(url, request, Void.class);
    }

    public boolean validateSecurityCode(String username, String code) {
        String url = mfaServiceUrl + "/securityCode/validate";

        MfaDto body = new MfaDto();
        body.setUsername(username);
        body.setCode(code);

        HttpEntity<MfaDto> request = new HttpEntity<MfaDto>(body);

        ResponseEntity<Void> response = restTemplate.postForEntity(url, request, Void.class);

        return response.getStatusCode().equals(HttpStatus.OK);
    }
}

​    (2)扩展认证对象

​      新增根据用户名密码和用户名安全码的认证对象,认证对象集成使用用户名密码的认证对象UsernamePasswordAuthenticationToken。

public class UserCredentialAuthentication extends UsernamePasswordAuthenticationToken {

    public UserCredentialAuthentication(Object principal, Object credentials) {
        super(principal, credentials);
    }
}
public class SecurityCodeAuthentication extends UsernamePasswordAuthenticationToken {

    public SecurityCodeAuthentication(Object principal, Object credentials) {
        super(principal, credentials);
    }
}

​    (3)新增认证器

​      新增根据用户名密码和用户名安全码的认证器。

​      用户名密码认证器:从传入的认证对象中获取用户名密码,调用MfaAcl做远程调用认证,认证成功后返回一个用户名密码认证对象UsernamePasswordAuthenticationToken。

​      用户名安全码认证器:从传入的认证对象中获取用户名和安全码,调用MfaAcl做远程调用认证,如果认证成功,则返回自定义的用户名安全码认证对象SecurityCodeAuthentication。

@Component
public class UserCredentialAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private MfaAcl mfaAcl;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = String.valueOf(authentication.getCredentials());

        //调用认证服务完成认证
        mfaAcl.validateUserCredential(username, password);
        return new UsernamePasswordAuthenticationToken(username, password);
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return UserCredentialAuthentication.class.isAssignableFrom(aClass);
    }
}
@Component
public class SecurityCodeAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private MfaAcl mfaAcl;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String code = String.valueOf(authentication.getCredentials());

        //调用认证服务完成认证
        boolean result = mfaAcl.validateSecurityCode(username, code);

        if (result) {
            return new SecurityCodeAuthentication(username, code);
        } else {
            throw new BadCredentialsException("安全码认证失败");
        }
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return SecurityCodeAuthentication.class.isAssignableFrom(aClass);
    }
}

​    (4)添加自定义过滤器

​      添加一个Mfa过滤器,集成OncePerRequestFilter,即每一次请求都需要进行过滤。

​      从请求中获取用户名、密码、安全码,如果安全码为空,则封装自定义的用户名密码认证对象userCredentialAuthentication,调用认证管理器AuthenticationManager进行认证,如果安全码不为空,则封装自定义的用户名安全码认证对象SecurityCodeAuthentication,并调用认证管理器认证。

@Component
public class MfaFilter extends OncePerRequestFilter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String username = request.getHeader("username");
        String password = request.getHeader("password");
        String code = request.getHeader("code");

        if (code == null) {
            Authentication userCredentialAuthentication = new UserCredentialAuthentication(username, password);
            authenticationManager.authenticate(userCredentialAuthentication);
        } else {
            Authentication securityCodeAuthentication = new SecurityCodeAuthentication(username, code);
            authenticationManager.authenticate(securityCodeAuthentication);

            response.setHeader("SecurityCode", code);
        }
    }

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        return !request.getServletPath().equals("/login");
    }
}

​    (5)Spring Security配置

​      首先在认证管理器中添加两个自定义的认证器。

​      然后在认证的配置中,在BasicAuthenticationFilter过滤器前添加自定义的mfaFilter,让整个认证流程先走自定义的mfaFilter,自定义的mfaFilter认证通过后再走BasicAuthenticationFilter及后续流程。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MfaFilter mfaFilter;

    @Autowired
    private SecurityCodeAuthenticationProvider securityCodeAuthenticationProvider;

    @Autowired
    private UserCredentialAuthenticationProvider userCredentialAuthenticationProvider;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(securityCodeAuthenticationProvider)
                .authenticationProvider(userCredentialAuthenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();

        http.addFilterAt(
                mfaFilter,
                BasicAuthenticationFilter.class);

        http.authorizeRequests()
                .anyRequest().authenticated();
    }

    @Override
    @Bean
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }
}

​    (6)验证

​      首先使用用户名密码做认证,认证通过后从数据库中查看生成的安全码,在用用户名安全码做认证。

GET http://localhost:8081/login
Accept: */*
Cache-Control: no-cache
username: lcl
password: 12345
GET http://localhost:8081/login
Accept: */*
Cache-Control: no-cache
username:lcl
code:526284 #根据具体情况做调整

四、Spring Cloud Security和OAuth2协议

(一)OAuth2协议基础

​    1、安全性问题与OAuth2

​    在微服务环境中,如果单纯使用用户名密码传递的方式在各个系统中做用户认证,会存在用户密码泄露、授权的范围和有效期问题、密码修改一致性问题。

​        

​    针对以上问题,可以引入OAuth2协议解决,OAuth2对于传统的授权机制来说,密码从平台管理编程由用户管理,增加了授权范围和有效期,并且可以做到一次授权多次使用。

​        

​    2、OAuth2角色与Token

​    在OAuth2协议中,角色分为资源拥有者(也就是用户,也可以是接口或者服务)、客户端(第三方应用程序)、授权服务器(用来发布Token)、资源管理器(用户资源)

​        

​    OAuth2协议的Token:

​        access_token:代表OAuth2令牌,用于当访问受保护资源时的验证

​        token_type:代表令牌类型,包括bearer、mac等,最常见的是bearer类型

​        refresh_token:作用在于当access_token 过期之后,重新下发一个新的access_token

​        expires_in:用于指定access_token的有效时间

​        scope:指定请求可访问的权限范围,如代表Web资源的“webclient”

{
    "access_token":"0efa61be-32ab-4351-9dga-8ab668ababae",
    "token_type":"bearer",
    "refresh_token":"738c42f6-79a6-457d-8d5a-f9eab0c7cc5e",
    "expires_in":43199,
    "scope":"webclient"
}

​    3、OAuth2授权流程与授权模式

​    OAuth2协议的授权流程:

​      第一步:客户端请求用户授权

​      第二步:用户操作同意授权

​      第三步:客户端请求授权服务器发放令牌

​      第四步:授权服务器给客户端发放令牌

​      第五步:客户端向资源服务器申请资源

​      第六步:资源服务器向客户端开放资源

​        

​    OAuth2协议的授权模式包括密码模式(Password Credentials)、授权码模式(Authorization Code)、客户端模式(Client Credentials)、简化模式(Implicit),客户端模式和简化模式使用的较少,一般使用密码模式和授权码模式,授权码模式类似于上面的安全码。

​    密码授权模式流程:用户访问客户端,客户端将用户名密码传入授权服务器,授权服务器发放一个令牌给客户端。

​    授权码授权模式流程:

​      用户访问客户端,客户端将用户导向授权服务器

​      用户手动操作统一授权

​      授权服务器导回客户端并携带授权码

​      客户端向授权服务器申请令牌

​      授权服务器给客户端发放令牌

​        

​    4、OAuth2协议与微服务架构

​    在服务访问场景中,多个微服务相互访问,那么发起调用的服务就需要先从授权中心获取Token,在服务调用过程中要携带Token,然后在被调用的服务中,要调用授权中心验证Token是否正确,如果正确,则正常返回资源,否则,接口调用失败。

(二)构建OAuth2授权体系

​    1、OAuth2客户端

​    (1)定义

​      首先是客户端的定义ClientDetails,抱恨了客户端唯一性ID、客户端完全码、客户端的访问范围、客户端可以使用的授权模式。

public interface ClientDetails extends Serializable {
    // 客户端唯一性Id
    String getClientId();
    // 客户端安全码
    String getClientSecret();
    // 客户端的访问范围
    Set<String> getScope();
    // 客户端可以使用的授权模式
    Set<String> getAuthorizedGrantTypes();
}

​    (2)配置

​      在Spring Security中有针对授权服务器的配置AuthorizationServerConfigurerAdapter,那么我们就可以自定义一个SpringAuthorizationServerConfigurer来继承它,从而实现自定义的授权服务器配置。

​      这里面有一组配置方法,其中一个就是来配置客户端程序的,如下代码所示,入参是客户端的配置类ClientDetailsServiceConfigurer。

​      在该方法中可以对客户端进行配置,例如配置客户端名称(withClient)、加密方式(secret)、授权模式(authorizedGrantTypes)、作用范围(scope)。

@Configuration
public class SpringAuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory().withClient("spring").secret("{noop}spring_secret").authorizedGrantTypes("refresh_token", "password", "client_credentials").scopes("webclient", "mobileclient");
    }
}

​    2、服务端

​    (1)配置授权服务器

​      使用@EnableAuthorizationServer注解启用授权服务器,这个注解可以加在配置类上,也可以加在启动类上,一般会将其配置在配置类上,如果配置类不用,删除配置类就可以了,不需要在调整启动类。

​      在服务端的配置仍然是继承AuthorizationServerConfigurerAdapter类,只不过实现其另外一个接口即可,具体代码如下所示,重写了入参为服务端端点配置AuthorizationServerEndpointsConfigurer的configure方法,在该方法中,配置了认证管理器和UserDetailsService。

​      这里关于为什么要设置认证管理器和UserDetailService,是因为这里使用了用户名密码的授权模式,而密码认证包含了认证环节,所以这里需要把认证服务器相关的组件初始化进来。

@Configuration
@EnableAuthorizationServer // 启用授权服务器
public class SpringAuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager) // 密码模式包含认证环节
                .userDetailsService(userDetailsService);
    }
}

​      关于认证服务器中用户名、密码、角色等等,其使用的是入参为HTTP Security的configure方法,具体和前面的处理时一样的,这里不再赘述。

​    (2)生成Token

​      当有了授权服务器后,就需要申请Token。

​      当OAuth2服务器启动后,会启动oauth端点,在端点下有一个人token接口,具体接口时:http:ip:port/oauth/token

​      对于这个端点接口,不是简简单单的输入用户名密码请求就行了,而是需要输入用户名、密码、客户端相关信息、密码模式相关信息。

​      客户端相关信息就是刚才在客户端初始化的相关参数,密码相关的信息是上面提到的用户认证的一套东西。

​      例如使用PostMain请求时,需要在两个地方设置内容。首先如下图所示,在Authorization中设置用户名密码,这个用户名密码就是客户端启动时设置的withClient和secret。然后还需要设置在body中设置grat_type(授权模式)、scope(授权范围)、username(自建用户体系的用户名)、password(自建用户体系的密码),这些

​        

​        

​    3、OAuth2资源服务器

​    如授权服务器相关的有资源服务器,资源服务器对于微服务架构来说,就是一个个微服务,下面要对资源服务器进行配置。

​    配置资源服务器需要继承ResourceServerConfigurerAdapter,并创建RESOURCE_ID、注入TokenStore,TokenStore是Token的存储方式。

​    在自定义的配置类中,需要设置其resourceId、tokenStore以及是否是无状态的。

​    其还要设置HttpSecurity,一般情况下会设置所有的请求都需要认证。

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    public static final String RESOURCE_ID = "resource1";
    @Resource
    private TokenStore tokenStore;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(RESOURCE_ID).tokenStore(tokenStore).stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated();
    }
}

(三)集成JWT

​    1、JWT介绍

​    JWT的全称是JSON Web Token,所以它本质上就是一种基于JSON表示的Token。JWT的设计目标就是为OAuth2协议中所使用的Token提供一种标准结构,所以它经常与OAuth2协议集成在一起使用。

​    JWT的Token样例如下所示,中间有两个点将整个token分成了三个部分,分别来表示header、payload、signature。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3NwcmluZy5leGFtcGlLmNvbSIsInN1YiI6Im1haWx0bzpzcHJpbmdA ZXhhbXBsZS5jb20iLCJuYmYiOjE2MTU4MTg2NDYsImV4cCI6MTYxNTgyMjI0NiwiaWF0IjoxNjE1DE4NjQ2LCJqdGkiOiJpZDEyMzQ1NiIsInR5cC I6Imh0dHBzOi8vc3ByaW5nLmV4YW1wbGUuY29tL3JlZ2lzdGVyIn0.Nweh3OPKl-p0PrSNDUQZ9LkJVWxjAP76uQscYJFQr9w

​    JWT Token具有业务含义,是对业务数据的Base64编码,如果反编码之后,就可以展示其真正的内容,上述的Token对应的内容如下所示。

​    我们可以根据自己的业务来生成对应的JWT Token,但是header、payload、signature这种三段式的表达方式是不会改变的。

{
  alg: "HS256", 
  typ: "JWT" 
}.
{
  iss: "https://spring.example.com", 
  sub: "mailto:[email protected]", 
  nbf: 1615818646, 
  exp: 1615822246, 
  iat: 1615818646, 
  jti: "id123456", 
  typ: "https://spring.example.com/register" 
}.
[signature]

​    JWT的优势:

​      语言无关:JSON格式,可以与各个异构系统进行集成

​      数据标准:header、payload、signature这种三段式的表达方式是不会改变的,所有人都可以遵循这种标准来传递数据

​      扩展性:JWT的Token具有业务含义,因此可以根据需求增加一些额外信息用于处理复杂的业务逻辑

​      数据加密:JWT Token使用了Base64编码处理,因此除了可以直接被用于认证之外,也可以处理加密需求

​    基于以上优势,在使用JWT时就不需要频繁的和授权服务器进行交互,因为交互的基本信息都在JWT Token中包含,这样就可以轻量级的处理,这在很多场景下会非常有优势。

​    2、集成OAuth2与JWT

​    在上面提到OAuth2中有TokenStore来存储Token,如果集成OAuth2和JWT,需要自定义Token Store,不过Spring Security已经提供了实现JwtTokenStore,不过要传入一个JwtAccessTokenConverter,即将业务数据转换为 JWT Token的转换器。

​    在自定义的JwtAccessTokenConverter中可以使用Spring Security内置的 JwtAccessTokenConverter,同时对其设置签名键,这样编码和解码双方才可以对称的编码和解码。

​    最后还需要自定义DefaultTokenServices,然后设置自定义的TokenStore。

@Configuration
public class JWTTokenStoreConfig {
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        // JWT转换器,转换过程需要签名键
        converter.setSigningKey("123456");
        return converter;
    }

    @Bean
    public DefaultTokenServices tokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        // 存储Token
        defaultTokenServices.setTokenStore(tokenStore());
        defaultTokenServices.setSupportRefreshToken(true);
        return defaultTokenServices;
    }
}

​    除了配置JWTTokenStoreConfig外,还需要配置端点,如果不修改端点,返回的是比较短的Token,如果想要返回 JWT Token,则需要配置端点。

​    在自定义的端点中,需要配置TokenStore和jwtAccessTokenConverter,同时也可以配置增强器链TokenEnhancerChain对JWT做增强。由于是基于密码模式的认证,因此在增强完成后,需要设置认证管理器和userDetailsService。

​    增强器链中包含了一串增强器,用于用来扩展JWT。

@Configuration
@EnableAuthorizationServer
public class SpringAuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // Token增强链
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
        endpoints.tokenStore(tokenStore)
                .accessTokenConverter(jwtAccessTokenConverter)
                .tokenEnhancer(tokenEnhancerChain)
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService);
    }
}

​    完成上述集成后,就会获取到一个如下所示的 Token,其中access_token就是生成的一串token,refresh_token是与access_token对应的一个Token,同时还有授权类型、过期时间、作用范围等。

​    同时可以看到还有system,这个是通过增强器Enhancer增强的,添加了自定义的内容,对JWT做了扩展,

{
    "access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzeXN0ZW0iOiJTcHJpbmcgU3lzdGVtIiwidXNlcl9uYW1lIjoic3ByaW 5nX3VzZXIiLCJzY29wZSI6WyJ3ZWJjbGllbnQiXSwiZXhwIjoxNjE3NTYwODU0LCJhdXRob3JpdGllcyI6WyJST0xFX1VTRV IiXSwianRpIjoiY2UyYTgzZmYtMjMzMC00YmQ1LTk4MzUtOWIyYzE0N2Y2MTcyIiwiY2xpZW50X2lkIjoic3ByaW5nIn0.Cd _x3r-Fi9hudA2W80amLEga0utPiOJCgBxxLI4Lsb8",
"token_type":"bearer",    "refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzeXN0ZW0iOiJTcHJpbmcgU3lzdGVtIiwidXNlcl9uYW1lIjoic3ByaW 5nX3VzZXIiLCJzY29wZSI6WyJ3ZWJjbGllbnQiXSwiYXRpIjoiY2UyYTgzZmYtMjMzMC00YmQ1LTk4MzUtOWIyYzE0N2Y2MT cyIiwiZXhwIjoxNjIwMTA5NjU0LCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiMDA0NjIxY2MtMmRmZi00ZD JiLWE0YWUtNTU5MzM5YzkyYmFhIiwiY2xpZW50X2lkIjoic3ByaW5nIn0.xDhGwhNTq7Iun9yLENaCvh8mrVHkabu3J8sP0N XENq0",
    "expires_in":43199,
    "scope":"webclient",
    "system":"Spring System",
    "jti":"ce2a83ff-2330-4bd5-9835-9b2c147f6172"
}

(四)客服系统案例演进

​    1、JWT通用配置

​    对于认证服务器和资源服务器都要配置Token Store,还要设置JwtAccessTokenConverter对业务数据和 JWT Token做转换,以及设置DefaultTokenServices,因此将这些配置放在一个独立的 jar 包中,可以让认证服务器和资源服务器都去引用,不需要每个服务都写相同的配置。

​    鉴于上述内容,新增microservice-security-auth-common服务。

​    先引入oauth2依赖

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-oauth2</artifactId>
   <version>2.2.5.RELEASE</version>
</dependency>

​    新增 JWT Token Store的配置,主要配置Token存储TokenStore、JWT Token 转换器 JwtAccessTokenConverter、tokenServices,其中tokenServices是用于管理令牌的生成、存储和管理。

@Configuration
public class JWTTokenStoreConfig {

    private final String SIGNING_KEY = "lcl_cs_123456";

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
        return jwtAccessTokenConverter;
    }

    @Bean
    @Primary
    public DefaultTokenServices tokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore());
        defaultTokenServices.setSupportRefreshToken(true);
        return defaultTokenServices;
    }
}

​    2、认证服务器

​    (1)配置依赖

​      引入spring-boot-starter-security,以及上面定义的配置Token Store的依赖。

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

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-oauth2</artifactId>
   <version>2.2.5.RELEASE</version>
</dependency>

<dependency>
   <groupId>com.lcl.galaxy</groupId>
   <artifactId>microservice-security-auth-common</artifactId>
   <version>${parent.version}</version>
</dependency>

​    (2)配置自定义增强器

public class JWTTokenEnhancer implements TokenEnhancer {

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
        Map<String, Object> info = new HashMap<>();
        info.put("system", "my_cs_system");
        ((DefaultOAuth2AccessToken)oAuth2AccessToken).setAdditionalInformation(info);
        return oAuth2AccessToken;
    }
}
@Configuration
public class TokenEnhancerConfig {

    @Bean
    public TokenEnhancer jwtTokenEnhancer() {
        return new JWTTokenEnhancer();
    }
}

​    (3)配置认证服务器

​      配置认证服务器,集成AuthorizationServerConfigurerAdapter类并重写其方法。

​      在端点配置方法中,配置 Token Store、JWT Token转换器、增强器链、认证管理器和userDetailsService。

​      在客户端配置方法中,配置客户端账号、密码、授权方式、作用范围

​      在认证服务端配置方法中,配置认证服务器的安全约束的,它定义了谁可以访问认证服务器的不同端点,一般情况下,认证服务器是需要放开让所有的服务都能访问的,对应的配置解释如下:

​        tokenKeyAccess:这个配置允许所有人访问认证服务器的公开端点 /oauth/token_key

​        checkTokenAccess:这个配置允许所有人访问认证服务器的公开端点 /oauth/check_token

​        allowFormAuthenticationForClients:这个配置允许客户端使用表单身份验证来进行认证,即客户端可以使用用户名密码方式获取令牌

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private TokenStore tokenStore;
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;
    @Autowired
    private TokenEnhancer jwtTokenEnhancer;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, jwtAccessTokenConverter));

        endpoints.tokenStore(tokenStore)
                .accessTokenConverter(jwtAccessTokenConverter)
                .tokenEnhancer(tokenEnhancerChain)
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("cs_client")
                .secret("{noop}cs_secret")
                .authorizedGrantTypes("refresh_token", "password", "client_credentials")
                .scopes("webclient", "mobileclient");
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess("permitAll()")
                .checkTokenAccess("permitAll()")
                .allowFormAuthenticationForClients();
    }
}

​    (4)配置 Spring Security

​      为了配置简单,使用内存方式配置用户名、密码、角色。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception{
        return super.authenticationManagerBean();
    }

    @Override
    @Bean
    public UserDetailsService userDetailsServiceBean() throws Exception {
        return super.userDetailsServiceBean();
    }


    /**
     * 认证
     * @param builder
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        builder.inMemoryAuthentication()
                .withUser("user1").password("{noop}password1").roles("USER")
                .and()
                .withUser("user2").password("{noop}password2").roles("ADMIN");
    }
}

​    3、资源服务器配置

​    (1)引入相关依赖

<dependency>
    <groupId>com.lcl.galaxy</groupId>
    <artifactId>microservice-security-auth-common</artifactId>
    <version>${parent.version}</version>
</dependency>

​    (2)配置资源服务器

​      自定义资源服务器配置ResourceServerConfig并继承ResourceServerConfigurerAdapter,重写其中的方法。

​      在资源服务器配置中,配置其资源ID、Token Store、无状态标识。

​      在HttpSecurity配置中,配置所有的访问都需要认证,同时配置/chats/**只能有ADMIN权限的用户访问。

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    public static final String RESOURCE_ID = "chat-resource";
    @Resource
    private TokenStore tokenStore;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId(RESOURCE_ID)
                .tokenStore(tokenStore)
                .stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/chats/**")
                .hasRole("ADMIN")
                .anyRequest()
                .authenticated();
    }
}

​    4、验证

​    (1)获取 JWT Token

​      配置认证:在Authorization中配置用户名密码,这个用户名密码是认证服务器中客户端配置中的withClient和secret

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.inMemory()
            .withClient("cs_client")
            .secret("{noop}cs_secret")
            .authorizedGrantTypes("refresh_token", "password", "client_credentials")
            .scopes("webclient", "mobileclient");
}

​        

​      配置body:首先需要配置授权类型grant_type、作用域scope、用户名、密码。

​      其中授权类型grant_type、作用域scope就是上面认证服务器中客户端配置中的authorizedGrantTypes和scopes。

​      用户名密码是Spring Security中配置的用户名密码。

@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
    builder.inMemoryAuthentication()
            .withUser("user1").password("{noop}password1").roles("USER")
            .and()
            .withUser("user2").password("{noop}password2").roles("ADMIN");
}

​        

​    (3)获取 JWT Token

​        根据上面的配置,请求后就可以获取对应的 JWT Token

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzeXN0ZW0iOiJteV9jc19zeXN0ZW0iLCJ1c2VyX25hbWUiOiJ1c2VyMiIsInNjb3BlIjpbIndlYmNsaWVudCJdLCJleHAiOjE2OTEzNzM0NDgsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iXSwianRpIjoiOTE1ZDkzMWEtOTc3ZS00NDVjLTkxNjUtNWMzZjQyODdmODcwIiwiY2xpZW50X2lkIjoiY3NfY2xpZW50In0.l_NQOWcYQdHlZd0EHBxKZ_ksO9Kj565cQjMn9Z8UO9k",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzeXN0ZW0iOiJteV9jc19zeXN0ZW0iLCJ1c2VyX25hbWUiOiJ1c2VyMiIsInNjb3BlIjpbIndlYmNsaWVudCJdLCJhdGkiOiI5MTVkOTMxYS05NzdlLTQ0NWMtOTE2NS01YzNmNDI4N2Y4NzAiLCJleHAiOjE2OTM5MjIyNDgsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iXSwianRpIjoiNTVjOTAxYTItZmJiYS00M2U0LTllYmQtY2E4MzYzNjM5Nzg1IiwiY2xpZW50X2lkIjoiY3NfY2xpZW50In0.04Jlsp8jBvAIGiaNAPLzQ61if5hhOwUkZecfErI7ots",
    "expires_in": 43199,
    "scope": "webclient",
    "system": "my_cs_system",
    "jti": "915d931a-977e-445c-9165-5c3f4287f870"
}

​    (4)携带 JWT Token请求

​      在请求时,携带 JWT Token即可:在请求的body上设置token参数

​        

​    (5)也可以在http文件中请求

​      只需要设置Authorization参数,首先设置token类型,这里使用的是Bearer,然后传入Token即可。

GET http://localhost:9004/chats/
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzeXN0ZW0iOiJteV9jc19zeXN0ZW0iLCJ1c2VyX25hbWUiOiJ1c2VyMiIsInNjb3BlIjpbIndlYmNsaWVudCJdLCJleHAiOjE2OTEzNzM0NDgsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iXSwianRpIjoiOTE1ZDkzMWEtOTc3ZS00NDVjLTkxNjUtNWMzZjQyODdmODcwIiwiY2xpZW50X2lkIjoiY3NfY2xpZW50In0.l_NQOWcYQdHlZd0EHBxKZ_ksO9Kj565cQjMn9Z8UO9k

标签:username,12,String,Spring,认证,Security,public
From: https://www.cnblogs.com/liconglong/p/17610338.html

相关文章

  • Docker部署SpringBoot项目准备
     创建一个SpringBoot项目测试程序,在Windows中执行java  -jar  xxx.jar查看效果java-jarlesson01-0.0.1-SNAPSHOT.jar主要代码1、pom.xml文件<?xmlversion="1.0"encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0&q......
  • Springboot - mybatis
    目录入门入门1.准备工作(创建springboot工程,数据库表user,实体类user)2.引入Mybatis的相关依赖,配置Mybatis3.编写SQL语句(注解/xml)创建工程时直接引入mybatis依赖:dao层编写mapper:packagecom.chuangzhou.dao;importcom.chuangzhou.pojo.User;importorg.apache.ibati......
  • 一种SpringBoot下Scheduler定时任务优雅退出方案
    背景近期业务中有一个定时任务发现每次服务部署时,偶发性的会触发问题,这里记录一下问题的跟进解决。分析现象该定时任务每2分钟执行一次,完成数据的更新处理。同时服务部署了多个服务器节点,为保证每次只有一个服务器节点上的任务在跑,引入了基于Redis缓存的分布式锁。示例源码@Schedu......
  • 异常: java.security.InvalidKeyException: Illegal key size
    问题描述importorg.apache.commons.codec.digest.DigestUtils;importorg.bouncycastle.jce.provider.BouncyCastleProvider;importjavax.crypto.Cipher;importjavax.crypto.spec.SecretKeySpec;importjava.security.Security;importjava.util.Base64;publicclass......
  • 12. 数据通信
    一、什么是数据通信  数据通信是通信技术和计算机技术相结合而产生的一种新的通信方式。要在两地间传输信息必须有传输信道,根据传输媒体的不同,有有线数据通信与无线数据通信之分。但它们都是通过传输信道将数据终端与计算机联结起来,而使不同地点的数据终端实现软、硬件和信息资......
  • 12306节假日余票监控
    前言  航空、铁路、高速三种交通方式中要数铁路的性价比最高了,周边城市来回比高速通畅又比航空实惠,但是逢年过节探亲或旅游要是忘了提前买票,想着当天买当天走是不太现实的,这时候估计只剩下三更半夜出发的票或者站票,令人难受。  于是我想着写个监控程序用于节假日来临提前通......
  • spring面试必会
    1.什么是Spring框架?Spring框架有哪些主要模块?Spring是针对bean的生命周期进行管理的轻量级容器,一个控制反转和面向切面的容器框架Spring有七大功能模块:1、CoreCore模块是Spring的核心类库,Core实现了IOC功能。2、AOPApringAOP模块是Spring的AOP库,提供了AOP(拦截器)机制,并提......
  • 8、Spring之基于注解的自动装配
    8.1、场景模拟8.1.1、UserDao接口及实现类packageorg.rain.spring.dao;/***@authorliaojy*@date2023/8/5-18:32*/publicinterfaceUserDao{voidsaveUser();}packageorg.rain.spring.dao.impl;importorg.rain.spring.dao.UserDao;importorg......
  • 龙邱512正交编码器
    基本概念龙邱的512线正交编码器,工作电压在3.3v-5v。我们只需要关注该款编码器的LSB及DIR引脚。LSP:该引脚在编码器转动时,会输出步进脉冲,在不同的转速下,步进脉冲的数量是不同的。所以我们可以设置一个定时器,把定时器的时钟输入通道改为外部引脚输入,这样我们就可以把单片机时钟......
  • 【springBoot】搭建多模块项目指南
    二、多环境配置内容①在reources目录下建立多个配置文件②在pom.xml配置profile节点③mvn命令行打包命令,基于参数进行打包替换占位符mvn-Ucleaninstall-Dmaven.test.skip=true-Pprod④将target目录下的jar包copy出来,执行命令启动,使用的就是不同环境的配置进行启动的......