In this paper, a personal blog address: www.leafage.top/posts/detai…
In recent days, gateway needs to be integrated into security for authentication and authentication. Before, Gateway and Auth were two services, and Auth was one written by Shiro, one filter and one configuration. The content is very simple, such as generating token and verifying token. No other security checks, and then let the project refactor.
First, we need to integrate Gateway and Shiro. However, because Gateway is Webflux and Shro-Spring is WebMVC, we failed. If you have done and succeeded, please tell me how to integrate, thank you very much.
Spring Cloud Gateway is based on WebFlux, so many online tutorials will not be used. There will be some changes to the webFlux configuration, as shown in the following code examples:
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 {
// Used to get remote data
private final HypervisorApi hypervisorApi;
public ServerSecurityConfiguration(HypervisorApi hypervisorApi) {
this.hypervisorApi = hypervisorApi;
}
/** * Password configuration, use BCryptPasswordEncoder **@returnBCryptPasswordEncoder Encryption mode */
@Bean
protected PasswordEncoder passwordEncoder(a) {
return new BCryptPasswordEncoder();
}
/** * User data load **@returnJdbcReactiveUserDetailsService interface * /
@Bean
public ReactiveUserDetailsService userDetailsService(a) {
// Custom implementation of ReactiveUserDetails
return new JdbcReactiveUserDetailsService(hypervisorApi);
}
/** * Security configuration */
@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();
}
/** * Processor */ executed after successful login
private ServerAuthenticationSuccessHandler authenticationSuccessHandler(a) {
return new ServerSuccessHandler();
}
/** * Processor */ executed after login failure
private ServerAuthenticationFailureHandler authenticationFailureHandler(a) {
return newServerFailureHandler(); }}Copy the code
The above sample code is a section of my open source project, the general configuration as written above, can be used, but since our previous project is Shiro, then there is a custom encryption and decryption logic.
First explain the situation, before that a set of encryption (front MD5, don’t add salt, then stored in the database is the data after salt and the corresponding salt (one per account), wants to compare the password before login for dynamic salt, then add salt to the MD5, compare again, but at the time of configuration is couldn’t get a user’s salt value)
Therefore, the configuration of the above version cannot pass verification. Before verification, the password of the request must be mixed with the salt corresponding to the account for second encryption before comparison, but there is a problem here:
- Several encryption/decryption tools provided by the Security framework do not have MD5 mode;
- Security configures encryption \ decryption mode, cannot fill in the dynamic account encryption salt;
The first problem is easy to handle, the solution is: custom encryption \ decrypt method, and then injected into the configuration class, as shown in the following example:
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;
/** * Custom encryption and decryption */
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; }}Copy the code
The solution to the second problem is found in the findByUsername() method of the UserDetailsService interface. When the UserDetailsService implementation is returned, Use the UserBuilder inner class that implements User by default to solve this problem, because the UserBuilder class has one property, the passwordEncoder property, which is of type Fucntion<String, String>, The default implementation is password -> password, that is, the password does not do any processing, first look at the source code:
Look again at the findByUsername() method before solving the problem:
@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()); }}Copy the code
Finding a solution to the problem, we changed the code as follows:
Add a code handling method
private Function<String, String> passwordEncoder(String salt) {
return rawPassword -> SecureUtil.md5(rawPassword + salt);
}
Copy the code
Then add the Builder chain
@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())) // Set the dynamic salt here.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) {
returnrawPassword -> SecureUtil.md5(rawPassword + salt); }}Copy the code
Then run the code, request the login interface, and login is successful.