Skip to content

Commit

Permalink
Feature: server add Ldap user authentication (#1392)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pan-YuJie authored Aug 15, 2023
1 parent 0a08f09 commit de703cf
Show file tree
Hide file tree
Showing 12 changed files with 584 additions and 7 deletions.
4 changes: 4 additions & 0 deletions threadpool/server/auth/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,9 @@
<artifactId>hippo4j-threadpool-server-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-ldap</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import cn.hippo4j.auth.constant.Constants;
import cn.hippo4j.auth.filter.JWTAuthenticationFilter;
import cn.hippo4j.auth.filter.JWTAuthorizationFilter;
import cn.hippo4j.auth.filter.LdapAuthenticationFilter;
import cn.hippo4j.auth.security.JwtTokenManager;
import cn.hippo4j.auth.service.impl.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -28,6 +29,7 @@
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
Expand All @@ -54,9 +56,12 @@ public class GlobalSecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${hippo4j.core.auth.enabled:true}")
private Boolean enableAuthentication;

@Resource
@Resource(name = "userDetailsServiceImpl")
private UserDetailsService userDetailsService;

@Resource(name = "ldapUserDetailsServiceImpl")
private UserDetailsService ldapUserDetailsService;

@Resource
private JwtTokenManager tokenManager;

Expand Down Expand Up @@ -93,7 +98,9 @@ protected void configure(HttpSecurity http) throws Exception {
.antMatchers("/static/**", "/index.html", "/favicon.ico", "/avatar.jpg").permitAll()
.antMatchers("/doc.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs").anonymous()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
// .addFilter(new JWTAuthenticationFilter(authenticationManager())).authenticationProvider(authenticationProvider())
.addFilter(JWTAuthenticationFilter()).authenticationProvider(ldapAuthenticationProvider())
.addFilter(LdapAuthenticationFilter()).authenticationProvider(ldapAuthenticationProvider())
.addFilter(new JWTAuthorizationFilter(tokenManager, authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
disableAuthenticationIfNeeded(http);
Expand All @@ -106,6 +113,20 @@ public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(ignores);
}

private LdapAuthenticationFilter LdapAuthenticationFilter() throws Exception {
LdapAuthenticationFilter filter = new LdapAuthenticationFilter(authenticationManager());
filter.setLdapUserDetailsService(ldapUserDetailsService);
filter.setAuthenticationManager(authenticationManagerBean());
return filter;
}

private JWTAuthenticationFilter JWTAuthenticationFilter() throws Exception {
JWTAuthenticationFilter filter = new JWTAuthenticationFilter(authenticationManager());
filter.setLdapUserDetailsService(userDetailsService);
filter.setAuthenticationManager(authenticationManagerBean());
return filter;
}

/**
* Injection DaoAuthenticationProvider
* Modify hideUserNotFoundExceptions initial value to false
Expand All @@ -120,6 +141,20 @@ public DaoAuthenticationProvider authenticationProvider() {
return provider;
}

@Bean
public DaoAuthenticationProvider ldapAuthenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(ldapUserDetailsService);
authProvider.setPasswordEncoder(bCryptPasswordEncoder());
return authProvider;
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider())
.authenticationProvider(ldapAuthenticationProvider());
}

private void disableAuthenticationIfNeeded(HttpSecurity http) throws Exception {
if (Boolean.FALSE.equals(enableAuthentication)) {
http.authorizeRequests().antMatchers("/hippo4j/v1/cs/**").permitAll();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package cn.hippo4j.auth.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.LdapContextSource;

import java.util.HashMap;
import java.util.Map;

/**
* Ldap config.
*/
@Configuration
public class LdapConfiguration {

private LdapTemplate ldapTemplate;

@Value("${spring.ldap.urls:}")
private String url;

@Value("${spring.ldap.base:}")
private String base;

@Value("${spring.ldap.embedded.credential.username:}")
private String username;

@Value("${spring.ldap.embedded.credential.password:}")
private String password;

@Bean
public LdapContextSource contextSource() {
LdapContextSource contextSource = new LdapContextSource();
Map<String, Object> config = new HashMap<>(10);
contextSource.setUrl(url);
contextSource.setBase(base);
contextSource.setUserDn(username);
contextSource.setPassword(password);
// fix garbled characters
config.put("java.naming.ldap.attributes.binary", "objectGUID");

contextSource.setPooled(true);
contextSource.setBaseEnvironmentProperties(config);
return contextSource;
}

@Bean
public LdapTemplate ldapTemplate() {
if (null == ldapTemplate) {
ldapTemplate = new LdapTemplate(contextSource());
}
return ldapTemplate;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,25 @@
import cn.hippo4j.common.toolkit.JSONUtil;
import cn.hippo4j.server.common.base.Results;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.codec.DecodingException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
Expand All @@ -60,11 +62,18 @@ public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilte

private final ThreadLocal<Integer> rememberMe = new ThreadLocal();

private UserDetailsService userDetailsService;

public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
super.setFilterProcessesUrl(BASE_PATH + "/auth/login");
}

public void setLdapUserDetailsService(UserDetailsService userDetailsServiceImpl) {
this.userDetailsService = userDetailsServiceImpl;
}

@SneakyThrows
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
Expand All @@ -78,8 +87,9 @@ public Authentication attemptAuthentication(HttpServletRequest request,

request.setAttribute("loginUser", loginUser);
rememberMe.set(loginUser.getRememberMe());
authenticate = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword(), new ArrayList()));
UserDetails userDetails = userDetailsService.loadUserByUsername(loginUser.getUsername());
authenticate = new PreAuthenticatedAuthenticationToken(userDetails, null, userDetails.getAuthorities());

} catch (GeneralSecurityException e) {
log.warn("Password decode exception: {}", e.getMessage());
throw new DecodingException(e.getMessage());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package cn.hippo4j.auth.filter;

import cn.hippo4j.auth.model.biz.user.JwtUser;
import cn.hippo4j.auth.model.biz.user.LoginUser;
import cn.hippo4j.auth.toolkit.AESUtil;
import cn.hippo4j.auth.toolkit.JwtTokenUtil;
import cn.hippo4j.auth.toolkit.ReturnT;
import cn.hippo4j.common.toolkit.JSONUtil;
import cn.hippo4j.server.common.base.Results;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import static cn.hippo4j.auth.constant.Constants.SPLIT_COMMA;
import static cn.hippo4j.common.constant.Constants.BASE_PATH;
import static cn.hippo4j.common.constant.Constants.MAP_INITIAL_CAPACITY;

/**
* Ldap Filter
*/
@Slf4j
public class LdapAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

private final ThreadLocal<Integer> rememberMe = new ThreadLocal<>();

private UserDetailsService ldapUserDetailsService;

public void setLdapUserDetailsService(UserDetailsService ldapUserDetailsServiceImpl) {
this.ldapUserDetailsService = ldapUserDetailsServiceImpl;
}

public LdapAuthenticationFilter(AuthenticationManager authenticationManager) {
super.setFilterProcessesUrl(BASE_PATH + "/auth/ldap/login");
}

/**
* Whether it's just the post way
*/
private boolean postOnly = true;


/**
* filter obtains the username and password of LDAP and assembles it on the token.
* Then give the token for authorization
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request
, HttpServletResponse response) throws AuthenticationException {
if (postOnly && !"POST".equals(request.getMethod())) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
// Get logged in information from the input stream.
Authentication authenticate = null;
try {
LoginUser loginUser = new ObjectMapper().readValue(request.getInputStream(), LoginUser.class);
String key = new StringBuffer(loginUser.getTag()).reverse().toString();
String password = AESUtil.decrypt(loginUser.getPassword(), key);
loginUser.setPassword(password);
request.setAttribute("loginUser", loginUser);
rememberMe.set(loginUser.getRememberMe());
// ldap validated
UserDetails userDetails = ldapUserDetailsService.loadUserByUsername(loginUser.getUsername());
authenticate = new PreAuthenticatedAuthenticationToken(userDetails, null, userDetails.getAuthorities());
} catch (UsernameNotFoundException e) {
log.debug("User {} not found", e.getMessage());
throw e;
} catch (BadCredentialsException e) {
log.debug("Bad credentials exception: {}", e.getMessage());
throw e;
} catch (Exception e) {
log.debug("Attempt authentication error", e);
}
return authenticate;
}
}

@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException {
try {
JwtUser jwtUser = (JwtUser) authResult.getPrincipal();
boolean isRemember = rememberMe.get() == 1;
String role = "";
Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
for (GrantedAuthority authority : authorities) {
role = authority.getAuthority();
}
String token = JwtTokenUtil.createToken(jwtUser.getId(), jwtUser.getUsername(), role, isRemember);
response.setHeader("token", JwtTokenUtil.TOKEN_PREFIX + token);
response.setCharacterEncoding("UTF-8");
Map<String, Object> maps = new HashMap<>(MAP_INITIAL_CAPACITY);
maps.put("data", JwtTokenUtil.TOKEN_PREFIX + token);
maps.put("roles", role.split(SPLIT_COMMA));
response.getWriter().write(JSONUtil.toJSONString(Results.success(maps)));
} finally {
rememberMe.remove();
}
}

@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {
response.setCharacterEncoding("UTF-8");
response.getWriter().write(JSONUtil.toJSONString(new ReturnT<>(ReturnT.JWT_FAIL_CODE, getMessage(failed))));
}

/**
* Return different echo information to the front end according to different exception types
*/
private String getMessage(AuthenticationException failed) {
String message = "Server Error";
if (failed instanceof UsernameNotFoundException) {
message = "用户不存在";
} else if (failed instanceof BadCredentialsException) {
message = "密码错误";
}
return message;
}
}
Loading

0 comments on commit de703cf

Please sign in to comment.