关于spring:Spring-Security-密码验证动态加盐的验证处理

51次阅读

共计 6030 个字符,预计需要花费 16 分钟才能阅读完成。

本文集体博客地址:https://www.leafage.top/posts/detail/21697I2R

最近几天在革新我的项目,须要将 gateway 整合 security 在一起进行认证和鉴权,之前 gateway 和 auth 是两个服务,auth 是 shiro 写的一个,一个 filter 和一个配置,内容很简略,生成 token,验证 token,没有其余的安全检查,而后让对我的项目进行重构。

先是要整合 gateway 和 shiro,然而因为 gateway 是 webflux,而 shiro-spring 是 webmvc,所以没搞胜利,如果有做过并胜利的,请通知我如何进行整合,非常感谢。

那整合 security 呢,因为 spring cloud gateway 基于 webflux,所以网上很多教程是用不了的,webflux 的配置会有一些变动,具体看如下代码示例:

import io.leafage.gateway.api.HypervisorApi;
import io.leafage.gateway.handler.ServerFailureHandler;
import io.leafage.gateway.handler.ServerSuccessHandler;
import io.leafage.gateway.service.JdbcReactiveUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint;
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.logout.HttpStatusReturningServerLogoutSuccessHandler;
import org.springframework.security.web.server.csrf.CookieServerCsrfTokenRepository;

/**
 * spring security config .
 *
 * @author liwenqiang 2019/7/12 17:51
 */
@EnableWebFluxSecurity
public class ServerSecurityConfiguration {

    // 用于获取近程数据
    private final HypervisorApi hypervisorApi;

    public ServerSecurityConfiguration(HypervisorApi hypervisorApi) {this.hypervisorApi = hypervisorApi;}

    /**
     * 明码配置,应用 BCryptPasswordEncoder
     *
     * @return BCryptPasswordEncoder 加密形式
     */
    @Bean
    protected PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();
    }

    /**
     * 用户数据加载
     *
     * @return JdbcReactiveUserDetailsService 接口
     */
    @Bean
    public ReactiveUserDetailsService userDetailsService() {
        // 自定义的 ReactiveUserDetails 实现
        return new JdbcReactiveUserDetailsService(hypervisorApi);
    }

    /**
     * 平安配置
     */
    @Bean
    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {http.formLogin(f -> f.authenticationSuccessHandler(authenticationSuccessHandler())
                .authenticationFailureHandler(authenticationFailureHandler()))
                .logout(l -> l.logoutSuccessHandler(new HttpStatusReturningServerLogoutSuccessHandler()))
                .csrf(c -> c.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()))
                .authorizeExchange(a -> a.pathMatchers(HttpMethod.OPTIONS).permitAll()
                        .anyExchange().authenticated())
                .exceptionHandling(e -> e.authenticationEntryPoint(new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED)));
        return http.build();}

    /**
     * 登陆胜利后执行的处理器
     */
    private ServerAuthenticationSuccessHandler authenticationSuccessHandler() {return new ServerSuccessHandler();
    }

    /**
     * 登陆失败后执行的处理器
     */
    private ServerAuthenticationFailureHandler authenticationFailureHandler() {return new ServerFailureHandler();
    }

}

下面的示例代码,是我开源我的项目中的一段,个别的配置就如下面写的,就能够应用了,然而因为咱们之前的我的项目中的是 shiro,而后有一个自定义的加密解密的逻辑。

首先阐明一下状况,之前那一套加密(前端 MD5,不加盐,而后数据库存储的是加盐后的数据和对应的盐(每个账号一个),要登录比拟之前对明码要获取动静的盐,而后加盐进行 MD5,再进行比照,然而在配置的时候是没法获取某一用户的盐值)

所以下面的一版配置是没法通过验证的,必须在验证之前,给申请的明码混合该账号对应的盐进行二次加密后在比照,然而这里就有问题了:

  1. security 框架提供的几个加密 \ 解密工具没有 MD5 的形式;
  2. security 配置加密 \ 解密形式的时候,无奈填入动静的账号的加密盐;

对于第一个问题还好解决,解决形式是:自定义加密 \ 解密形式,而后注入到配置类中,示例如下:

import cn.hutool.crypto.SecureUtil;
import com.ichinae.imis.gateway.utils.SaltUtil;
import org.springframework.security.crypto.codec.Utf8;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.security.MessageDigest;

/**
 * 自定义加密解密
 */
public class MD5PasswordEncoder implements PasswordEncoder {

    @Override
    public String encode(CharSequence charSequence) {String salt = SaltUtil.generateSalt();
        return SecureUtil.md5(SecureUtil.md5(charSequence.toString()) + salt);
    }

    @Override
    public boolean matches(CharSequence charSequence, String encodedPassword) {byte[] expectedBytes = bytesUtf8(charSequence.toString());
        byte[] actualBytes = bytesUtf8(charSequence.toString());
        return MessageDigest.isEqual(expectedBytes, actualBytes);
    }

    private static byte[] bytesUtf8(String s) {// need to check if Utf8.encode() runs in constant time (probably not).
        // This may leak length of string.
        return (s != null) ? Utf8.encode(s) : null;
    }

}

第二个问题的解决办法,找了很多材料,也没有找到,起初查看 security 的源码发现,能够在 UserDetailsService 接口的 findByUsername() 办法中,在返回 UserDetails 实现的时候,应用默认实现 User 的 UserBuilder 外部类来解决这个问题,因为 UserBuilder 类中有一个属性,passwordEncoder 属性,它是 Fucntion<String, String> 类型的,默认实现是 password -> password,即对明码不做任何解决,先看下它的源码:

再看下解决问题之前的 findByUsername() 办法:

@Service
public class UserDetailsServiceImpl implements ReactiveUserDetailsService {

    @Resource
    private RemoteService remoteService;

    @Override
    public Mono<UserDetails> findByUsername(String username) {return remoteService.getUser(username).map(userBO -> User.builder()
                .username(username)
                .password(userBO.getPassword())
                .authorities(grantedAuthorities(userBO.getAuthorities()))
                .build());
    }

    private Set<GrantedAuthority> grantedAuthorities(Set<String> authorities) {return authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
    }

}

那找到了问题的解决办法,就来改代码了,如下所示:

新增一个代码解决办法

private Function<String, String> passwordEncoder(String salt) {return rawPassword -> SecureUtil.md5(rawPassword + salt);
}

而后增加 builder 链

@Service
public class UserDetailsServiceImpl implements ReactiveUserDetailsService {

    @Resource
    private RemoteService remoteService;

    @Override
    public Mono<UserDetails> findByUsername(String username) {return remoteService.getUser(username).map(userBO -> User.builder()
                .passwordEncoder(passwordEncoder(userBO.getSalt())) // 在这里设置动静的盐
                .username(username)
                .password(userBO.getPassword())
                .authorities(grantedAuthorities(userBO.getAuthorities()))
                .build());
    }

    private Set<GrantedAuthority> grantedAuthorities(Set<String> authorities) {return authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
    }

    private Function<String, String> passwordEncoder(String salt) {return rawPassword -> SecureUtil.md5(rawPassword + salt);
    }
}

而后跑一下代码,申请登录接口,就登陆胜利了。

正文完
 0