本文集体博客地址: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 */@EnableWebFluxSecuritypublic 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,再进行比照,然而在配置的时候是没法获取某一用户的盐值)
所以下面的一版配置是没法通过验证的,必须在验证之前,给申请的明码混合该账号对应的盐进行二次加密后在比照,然而这里就有问题了:
- security 框架提供的几个加密\解密工具没有MD5的形式;
- 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()办法:
@Servicepublic 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链
@Servicepublic 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); }}
而后跑一下代码,申请登录接口,就登陆胜利了。