mirror of https://github.com/longtai-cn/hippo4j
Feature: server add Ldap user authentication (#1392)
parent
0a08f09e85
commit
de703cfca1
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package cn.hippo4j.auth.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.Data;
|
||||
import org.springframework.ldap.odm.annotations.Attribute;
|
||||
import org.springframework.ldap.odm.annotations.DnAttribute;
|
||||
import org.springframework.ldap.odm.annotations.Entry;
|
||||
import org.springframework.ldap.odm.annotations.Id;
|
||||
|
||||
import javax.naming.Name;
|
||||
|
||||
@Data
|
||||
@Entry(objectClasses = {"inetOrgPerson", "top"})
|
||||
public class LdapUserInfo {
|
||||
|
||||
@JsonIgnore
|
||||
@Id
|
||||
private Name dn;
|
||||
|
||||
@Attribute(name = "cn")
|
||||
@DnAttribute(value = "cn")
|
||||
private String userName;
|
||||
|
||||
@Attribute(name = "sn")
|
||||
private String lastName;
|
||||
|
||||
@Attribute(name = "description")
|
||||
private String description;
|
||||
|
||||
@Attribute(name = "telephoneNumber")
|
||||
private String telephoneNumber;
|
||||
|
||||
@Attribute(name = "userPassword")
|
||||
private String password;
|
||||
|
||||
@Attribute(name = "ou")
|
||||
private String organizational;
|
||||
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.service;
|
||||
|
||||
/**
|
||||
* Ldap service.
|
||||
*/
|
||||
public interface LdapService {
|
||||
/**
|
||||
* Login ldap
|
||||
*/
|
||||
void login(String username, String password);
|
||||
}
|
@ -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.service.impl;
|
||||
|
||||
import cn.hippo4j.auth.service.LdapService;
|
||||
import cn.hippo4j.server.common.base.exception.ServiceException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.ldap.AuthenticationException;
|
||||
import org.springframework.ldap.UncategorizedLdapException;
|
||||
import org.springframework.ldap.core.LdapTemplate;
|
||||
import org.springframework.ldap.query.LdapQueryBuilder;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import static org.springframework.ldap.query.LdapQueryBuilder.query;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class LdapServiceImpl implements LdapService {
|
||||
|
||||
private final LdapTemplate ldapTemplate;
|
||||
|
||||
@Value("${spring.ldap.object-class:}")
|
||||
private String objectClassName;
|
||||
|
||||
@Value("${spring.ldap.account-attribute:}")
|
||||
private String accountAttribute;
|
||||
|
||||
public LdapServiceImpl(LdapTemplate ldapTemplate) {
|
||||
this.ldapTemplate = ldapTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void login(String username, String password) {
|
||||
try {
|
||||
ldapTemplate.authenticate(LdapQueryBuilder.query()
|
||||
.where(accountAttribute).is(username)
|
||||
.and(query().where("objectClass").is(objectClassName))
|
||||
, password);
|
||||
log.debug("{} ldap Login successful", username);
|
||||
} catch (EmptyResultDataAccessException e) {
|
||||
throw new UsernameNotFoundException("ldap Can't find the user information ");
|
||||
} catch (AuthenticationException e) {
|
||||
log.debug("The user name or account error");
|
||||
throw new BadCredentialsException("The username or password error");
|
||||
} catch (UncategorizedLdapException e) {
|
||||
log.debug("Please check whether the user name password input");
|
||||
throw new BadCredentialsException("Please check whether the username password input");
|
||||
} catch (Exception e) {
|
||||
throw new ServiceException("Abnormal server");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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.service.impl;
|
||||
|
||||
import cn.hippo4j.auth.mapper.UserMapper;
|
||||
import cn.hippo4j.auth.model.UserInfo;
|
||||
import cn.hippo4j.auth.model.biz.user.JwtUser;
|
||||
import cn.hippo4j.auth.model.biz.user.LoginUser;
|
||||
import cn.hippo4j.auth.service.LdapService;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
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.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* User details service impl.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class LdapUserDetailsServiceImpl implements UserDetailsService {
|
||||
|
||||
@Value("${hippo4j.core.auth.enabled:true}")
|
||||
private Boolean enableAuthentication;
|
||||
|
||||
@Resource
|
||||
private UserMapper userMapper;
|
||||
|
||||
@Resource
|
||||
private LdapService ldapService;
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
|
||||
JwtUser anonymous = dealWithAnonymous();
|
||||
if (!Objects.isNull(anonymous)) {
|
||||
return anonymous;
|
||||
}
|
||||
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(requestAttributes)).getRequest();
|
||||
LoginUser loginUser = (LoginUser) request.getAttribute("loginUser");
|
||||
// ldap authentication
|
||||
ldapService.login(userName, loginUser.getPassword());
|
||||
// By querying the data inventory this user does not exist
|
||||
UserInfo userInfo = userMapper.selectOne(Wrappers.lambdaQuery(UserInfo.class)
|
||||
.eq(UserInfo::getUserName, userName)
|
||||
);
|
||||
// the database does not, create a ROLE_USER permission to the default user, password is empty
|
||||
if (Objects.isNull(userInfo)) {
|
||||
userInfo = new UserInfo();
|
||||
userInfo.setPassword("");
|
||||
userInfo.setUserName(loginUser.getUsername());
|
||||
userInfo.setRole("ROLE_USER");
|
||||
userMapper.insert(userInfo);
|
||||
}
|
||||
// structure jwtUser
|
||||
JwtUser jwtUser = new JwtUser();
|
||||
jwtUser.setId(userInfo.getId());
|
||||
jwtUser.setUsername(userName);
|
||||
jwtUser.setPassword("");
|
||||
Set<SimpleGrantedAuthority> authorities = Collections.singleton(new SimpleGrantedAuthority(userInfo.getRole() + ""));
|
||||
jwtUser.setAuthorities(authorities);
|
||||
return jwtUser;
|
||||
}
|
||||
|
||||
private JwtUser dealWithAnonymous() {
|
||||
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||
if (requestAttributes == null) {
|
||||
return null;
|
||||
}
|
||||
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
|
||||
LoginUser loginUser = (LoginUser) request.getAttribute("loginUser");
|
||||
if (Objects.isNull(loginUser)) {
|
||||
return null;
|
||||
}
|
||||
if (Boolean.FALSE.equals(enableAuthentication)) {
|
||||
JwtUser jwtUser = new JwtUser();
|
||||
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
|
||||
jwtUser.setId(1L);
|
||||
jwtUser.setUsername("anonymous");
|
||||
jwtUser.setPassword(bCryptPasswordEncoder.encode(loginUser.getPassword()));
|
||||
Set<SimpleGrantedAuthority> authorities = Collections.singleton(new SimpleGrantedAuthority("ROLE_ADMIN"));
|
||||
jwtUser.setAuthorities(authorities);
|
||||
return jwtUser;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package cn.hippo4j.auth.toolkit;
|
||||
|
||||
import cn.hippo4j.common.toolkit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
|
||||
public class BCryptPasswordEncoderTest {
|
||||
|
||||
@Test
|
||||
public void bCryptPasswordEncoderTest() {
|
||||
|
||||
String password = "12345abc";
|
||||
|
||||
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
|
||||
String encode = bCryptPasswordEncoder.encode(password);
|
||||
boolean matches = bCryptPasswordEncoder.matches(password, encode);
|
||||
Assert.isTrue(matches);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
#*************** Ldap Sample Configurations ***************#
|
||||
### This configuration file does not take effect
|
||||
### Change the LDAP server information to yourself
|
||||
### Configure the following configuration file into application.properties
|
||||
|
||||
# Ldap Config
|
||||
spring.ldap.urls=ldap://127.0.0.1:389
|
||||
spring.ldap.base=dc=xxx,dc=com
|
||||
spring.ldap.embedded.credential.username=cn=xxxx,dc=xxx,dc=com
|
||||
spring.ldap.embedded.credential.password=password
|
||||
# Ldap Entry object-class
|
||||
spring.ldap.object-class=person
|
||||
# Ldap account-attribute CommonName ( cn / uid / username / ... )
|
||||
spring.ldap.account-attribute=cn
|
Loading…
Reference in new issue