代码调整
This commit is contained in:
@@ -5,8 +5,9 @@ import cn.hutool.core.util.StrUtil;
|
||||
import com.njcn.auth.filter.CustomClientCredentialsTokenEndpointFilter;
|
||||
import com.njcn.auth.pojo.bo.BusinessUser;
|
||||
import com.njcn.auth.security.clientdetails.ClientDetailsServiceImpl;
|
||||
import com.njcn.auth.security.extension.captcha.CaptchaTokenGranter;
|
||||
import com.njcn.auth.security.extension.refresh.PreAuthenticatedUserDetailsService;
|
||||
import com.njcn.auth.security.granter.CaptchaTokenGranter;
|
||||
import com.njcn.auth.security.granter.PreAuthenticatedUserDetailsService;
|
||||
import com.njcn.auth.security.granter.SmsTokenGranter;
|
||||
import com.njcn.auth.service.UserDetailsServiceImpl;
|
||||
import com.njcn.common.pojo.constant.SecurityConstants;
|
||||
import com.njcn.common.pojo.enums.auth.ClientEnum;
|
||||
@@ -89,10 +90,11 @@ public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdap
|
||||
granterList.add(new CaptchaTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(),
|
||||
endpoints.getOAuth2RequestFactory(), authenticationManager, redisUtil
|
||||
));
|
||||
|
||||
// 添加短信授权模式授权者
|
||||
granterList.add(new SmsTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(),
|
||||
endpoints.getOAuth2RequestFactory(), authenticationManager, redisUtil
|
||||
));
|
||||
//todo... 后续可以扩展更多授权模式,比如:微信小程序、移动app
|
||||
|
||||
|
||||
CompositeTokenGranter compositeTokenGranter = new CompositeTokenGranter(granterList);
|
||||
endpoints.authenticationManager(authenticationManager)
|
||||
.accessTokenConverter(jwtAccessTokenConverter())
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.njcn.auth.config;
|
||||
|
||||
import com.njcn.auth.security.sm4.Sm4AuthenticationProvider;
|
||||
import com.njcn.auth.security.provider.Sm4AuthenticationProvider;
|
||||
import com.njcn.auth.security.provider.SmsAuthenticationProvider;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -29,6 +30,8 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
private final Sm4AuthenticationProvider sm4AuthenticationProvider;
|
||||
|
||||
private final SmsAuthenticationProvider smsAuthenticationProvider;
|
||||
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
@@ -63,21 +66,6 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 用户名密码认证授权提供者
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean
|
||||
public DaoAuthenticationProvider daoAuthenticationProvider() {
|
||||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
|
||||
provider.setUserDetailsService(sysUserDetailsService);
|
||||
provider.setPasswordEncoder(passwordEncoder());
|
||||
provider.setHideUserNotFoundExceptions(false); // 是否隐藏用户不存在异常,默认:true-隐藏;false-抛出异常;
|
||||
return provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重写父类自定义AuthenticationManager 将provider注入进去
|
||||
* 当然我们也可以考虑不重写 在父类的manager里面注入provider
|
||||
@@ -85,10 +73,24 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
@Bean
|
||||
@Override
|
||||
protected AuthenticationManager authenticationManager(){
|
||||
return new ProviderManager(sm4AuthenticationProvider);
|
||||
return new ProviderManager(sm4AuthenticationProvider,smsAuthenticationProvider);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 用户名密码认证授权提供者
|
||||
*/
|
||||
@Bean
|
||||
public DaoAuthenticationProvider daoAuthenticationProvider() {
|
||||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
|
||||
provider.setUserDetailsService(sysUserDetailsService);
|
||||
provider.setPasswordEncoder(passwordEncoder());
|
||||
// 是否隐藏用户不存在异常,默认:true-隐藏;false-抛出异常;
|
||||
provider.setHideUserNotFoundExceptions(false);
|
||||
return provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码编码器
|
||||
* <p>
|
||||
|
||||
@@ -75,15 +75,19 @@ public class AuthController extends BaseController {
|
||||
@ApiImplicitParam(name = SecurityConstants.USERNAME, value = "登录用户名"),
|
||||
@ApiImplicitParam(name = SecurityConstants.PASSWORD, value = "登录密码"),
|
||||
@ApiImplicitParam(name = SecurityConstants.IMAGE_CODE, value = "图形验证码"),
|
||||
@ApiImplicitParam(name = SecurityConstants.PHONE, value = "手机号"),
|
||||
@ApiImplicitParam(name = SecurityConstants.SMS_CODE, value = "短信验证码"),
|
||||
})
|
||||
@PostMapping("/token")
|
||||
public Object postAccessToken(@ApiIgnore Principal principal, @RequestParam @ApiIgnore Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
|
||||
String methodDescribe = getMethodDescribe("postAccessToken");
|
||||
String username = parameters.get(SecurityConstants.USERNAME);
|
||||
String grantType = parameters.get(SecurityConstants.GRANT_TYPE);
|
||||
//正式环境需删除,均是加密的用户名
|
||||
if (!grantType.equalsIgnoreCase(SecurityConstants.PASSWORD)) {
|
||||
if (grantType.equalsIgnoreCase(SecurityConstants.GRANT_CAPTCHA)) {
|
||||
username = DesUtils.aesDecrypt(username);
|
||||
}else if(grantType.equalsIgnoreCase(SecurityConstants.GRANT_SMS_CODE)){
|
||||
//短信方式登录,将手机号赋值为用户名
|
||||
username = parameters.get(SecurityConstants.PHONE);
|
||||
}
|
||||
if (grantType.equalsIgnoreCase(SecurityConstants.REFRESH_TOKEN_KEY)) {
|
||||
//如果是刷新token,需要去黑名单校验
|
||||
@@ -92,7 +96,9 @@ public class AuthController extends BaseController {
|
||||
RequestUtil.saveLoginName(username);
|
||||
OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody();
|
||||
//用户的登录名&密码校验成功后,判断当前该用户是否可以正常使用系统
|
||||
userFeignClient.judgeUserStatus(username);
|
||||
if(!grantType.equalsIgnoreCase(SecurityConstants.GRANT_SMS_CODE)){
|
||||
userFeignClient.judgeUserStatus(username);
|
||||
}
|
||||
//登录成功后,记录token信息,并处理踢人效果
|
||||
userTokenService.recordUserInfo(oAuth2AccessToken,RequestUtil.getRealIp());
|
||||
if (!grantType.equalsIgnoreCase(SecurityConstants.PASSWORD)) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.njcn.auth.security.extension.captcha;
|
||||
package com.njcn.auth.security.granter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.njcn.common.pojo.constant.SecurityConstants;
|
||||
@@ -37,7 +37,7 @@ public class CaptchaTokenGranter extends AbstractTokenGranter {
|
||||
OAuth2RequestFactory requestFactory, AuthenticationManager authenticationManager,
|
||||
RedisUtil redisUtil
|
||||
) {
|
||||
//SecurityConstants.GRANT_CAPTCHA:申明为授权码模式
|
||||
//SecurityConstants.GRANT_CAPTCHA:申明为验证码模式
|
||||
super(tokenServices, clientDetailsService, requestFactory, SecurityConstants.GRANT_CAPTCHA);
|
||||
this.authenticationManager = authenticationManager;
|
||||
this.redisUtil = redisUtil;
|
||||
@@ -49,6 +49,7 @@ public class CaptchaTokenGranter extends AbstractTokenGranter {
|
||||
String username = parameters.get(SecurityConstants.USERNAME);
|
||||
username = DesUtils.aesDecrypt(username);
|
||||
String verifyCode = parameters.get(SecurityConstants.VERIFY_CODE);
|
||||
//判断是否需要校验图形验证码,用户错误后,前端要求用户填写验证码
|
||||
if(StrUtil.isEmpty(verifyCode)||verifyCode.equals("1")){
|
||||
if (!judgeImageCode(parameters.get(SecurityConstants.IMAGE_CODE), RequestUtil.getRequest())) {
|
||||
throw new BusinessException(UserResponseEnum.LOGIN_WRONG_CODE);
|
||||
@@ -58,18 +59,20 @@ public class CaptchaTokenGranter extends AbstractTokenGranter {
|
||||
String ip = RequestUtil.getRequest().getHeader(SecurityConstants.REQUEST_HEADER_KEY_CLIENT_REAL_IP);
|
||||
//密码处理
|
||||
String privateKey = redisUtil.getStringByKey(username + ip);
|
||||
// //秘钥用完即删
|
||||
//秘钥用完即删
|
||||
redisUtil.delete(username + ip);
|
||||
//对SM2解密面进行验证
|
||||
password = Sm2.getPasswordSM2Verify(privateKey, password);
|
||||
if (StrUtil.isBlankIfStr(password)) {
|
||||
throw new BusinessException(UserResponseEnum.PASSWORD_TRANSPORT_ERROR);
|
||||
}
|
||||
//正式环境放行
|
||||
//1、不将密码放入details内,防止密码泄漏
|
||||
parameters.remove(SecurityConstants.PASSWORD);
|
||||
//2、组装用户密码模式的认证信息
|
||||
Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
|
||||
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
|
||||
try {
|
||||
//3、认证组装好的信息
|
||||
userAuth = authenticationManager.authenticate(userAuth);
|
||||
} catch (AccountStatusException | BadCredentialsException ase) {
|
||||
//covers expired, locked, disabled cases
|
||||
@@ -77,7 +80,7 @@ public class CaptchaTokenGranter extends AbstractTokenGranter {
|
||||
}
|
||||
// If the username/password are wrong the spec says we should send 400/invalid grant
|
||||
if (userAuth == null || !userAuth.isAuthenticated()) {
|
||||
throw new InvalidGrantException("Could not authenticate user: " + username);
|
||||
throw new InvalidGrantException("无法认证用户: " + username);
|
||||
}
|
||||
|
||||
OAuth2Request storedOauth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.njcn.auth.security.extension.refresh;
|
||||
package com.njcn.auth.security.granter;
|
||||
|
||||
import com.njcn.common.pojo.constant.SecurityConstants;
|
||||
import com.njcn.common.pojo.enums.auth.AuthenticationMethodEnum;
|
||||
import com.njcn.web.utils.RequestUtil;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
@@ -11,7 +9,6 @@ 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.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Map;
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.njcn.auth.security.granter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.njcn.auth.security.token.SmsCodeAuthenticationToken;
|
||||
import com.njcn.common.pojo.constant.SecurityConstants;
|
||||
import com.njcn.common.pojo.exception.BusinessException;
|
||||
import com.njcn.redis.pojo.enums.RedisKeyEnum;
|
||||
import com.njcn.redis.utils.RedisUtil;
|
||||
import com.njcn.user.enums.UserResponseEnum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.authentication.*;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
|
||||
import org.springframework.security.oauth2.provider.*;
|
||||
import org.springframework.security.oauth2.provider.token.AbstractTokenGranter;
|
||||
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author hongawen
|
||||
* @version 1.0.0
|
||||
* @date 2021年12月15日 14:23
|
||||
*/
|
||||
@Slf4j
|
||||
public class SmsTokenGranter extends AbstractTokenGranter {
|
||||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
|
||||
private final RedisUtil redisUtil;
|
||||
|
||||
public SmsTokenGranter(AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService,
|
||||
OAuth2RequestFactory requestFactory, AuthenticationManager authenticationManager,
|
||||
RedisUtil redisUtil
|
||||
) {
|
||||
//SecurityConstants.GRANT_CAPTCHA:申明为手机短信模式
|
||||
super(tokenServices, clientDetailsService, requestFactory, SecurityConstants.GRANT_SMS_CODE);
|
||||
this.authenticationManager = authenticationManager;
|
||||
this.redisUtil = redisUtil;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
|
||||
Map<String, String> parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters());
|
||||
String phone = parameters.get(SecurityConstants.PHONE);
|
||||
String smsCode = parameters.get(SecurityConstants.SMS_CODE);
|
||||
if (StrUtil.isBlank(phone)) {
|
||||
throw new BusinessException(UserResponseEnum.REGISTER_PHONE_WRONG);
|
||||
}
|
||||
if (judgeSmsCode(phone, smsCode)) {
|
||||
throw new BusinessException(UserResponseEnum.LOGIN_WRONG_CODE);
|
||||
}
|
||||
//2、组装用户手机号认证信息
|
||||
Authentication userAuth = new SmsCodeAuthenticationToken(phone, null);
|
||||
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
|
||||
try {
|
||||
//3、认证组装好的信息
|
||||
userAuth = authenticationManager.authenticate(userAuth);
|
||||
} catch (AccountStatusException | BadCredentialsException ase) {
|
||||
throw new InvalidGrantException(ase.getMessage());
|
||||
}
|
||||
if (userAuth == null || !userAuth.isAuthenticated()) {
|
||||
throw new InvalidGrantException("无法认证用户: " + phone);
|
||||
}
|
||||
|
||||
OAuth2Request storedOauth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
|
||||
return new OAuth2Authentication(storedOauth2Request, userAuth);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证用户的短信验证码是否正确
|
||||
*
|
||||
* @param phone 手机号
|
||||
* @param smsCode 用户输入的短信验证码
|
||||
* @return boolean
|
||||
* @author hongawen
|
||||
* @date 2023/6/14 15:25
|
||||
*/
|
||||
private boolean judgeSmsCode(String phone, String smsCode) {
|
||||
if (StrUtil.isBlankIfStr(smsCode)) {
|
||||
return false;
|
||||
}
|
||||
String key = RedisKeyEnum.SMS_LOGIN_KEY.getKey().concat(phone);
|
||||
String redisImageCode = redisUtil.getStringByKey(key);
|
||||
if (smsCode.equalsIgnoreCase(redisImageCode)) {
|
||||
redisUtil.delete(key);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
package com.njcn.auth.security.provider;
|
||||
|
||||
import com.njcn.auth.security.token.SmsCodeAuthenticationToken;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.context.MessageSourceAware;
|
||||
import org.springframework.context.support.MessageSourceAccessor;
|
||||
import org.springframework.security.authentication.*;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.SpringSecurityMessageSource;
|
||||
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
||||
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
|
||||
import org.springframework.security.core.userdetails.UserCache;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsChecker;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.core.userdetails.cache.NullUserCache;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @author hongawen
|
||||
* @version 1.0.0
|
||||
* @date 2023年06月15日 10:08
|
||||
*/
|
||||
public abstract class AbstractSmsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
|
||||
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
|
||||
|
||||
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
|
||||
private UserCache userCache = new NullUserCache();
|
||||
private boolean forcePrincipalAsString = false;
|
||||
protected boolean hideUserNotFoundExceptions = true;
|
||||
private UserDetailsChecker preAuthenticationChecks = new AbstractSmsAuthenticationProvider.DefaultPreAuthenticationChecks();
|
||||
private UserDetailsChecker postAuthenticationChecks = new AbstractSmsAuthenticationProvider.DefaultPostAuthenticationChecks();
|
||||
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
|
||||
|
||||
// ~ Methods
|
||||
// ========================================================================================================
|
||||
|
||||
/**
|
||||
* Allows subclasses to perform any additional checks of a returned (or cached)
|
||||
* <code>UserDetails</code> for a given authentication request. Generally a subclass
|
||||
* will at least compare the {@link Authentication#getCredentials()} with a
|
||||
* {@link UserDetails#getPassword()}. If custom logic is needed to compare additional
|
||||
* properties of <code>UserDetails</code> and/or
|
||||
* <code>SmsCodeAuthenticationToken</code>, these should also appear in this
|
||||
* method.
|
||||
*
|
||||
* @param userDetails as retrieved from the
|
||||
* {@link #retrieveUser(String, SmsCodeAuthenticationToken)} or
|
||||
* <code>UserCache</code>
|
||||
* @param authentication the current request that needs to be authenticated
|
||||
*
|
||||
* @throws AuthenticationException AuthenticationException if the credentials could
|
||||
* not be validated (generally a <code>BadCredentialsException</code>, an
|
||||
* <code>AuthenticationServiceException</code>)
|
||||
*/
|
||||
protected abstract void additionalAuthenticationChecks(UserDetails userDetails,
|
||||
SmsCodeAuthenticationToken authentication)
|
||||
throws AuthenticationException;
|
||||
|
||||
public final void afterPropertiesSet() throws Exception {
|
||||
Assert.notNull(this.userCache, "A user cache must be set");
|
||||
Assert.notNull(this.messages, "A message source must be set");
|
||||
doAfterPropertiesSet();
|
||||
}
|
||||
|
||||
public Authentication authenticate(Authentication authentication)
|
||||
throws AuthenticationException {
|
||||
Assert.isInstanceOf(SmsCodeAuthenticationToken.class, authentication,
|
||||
() -> messages.getMessage(
|
||||
"AbstractSmsAuthenticationProvider.onlySupports",
|
||||
"Only SmsCodeAuthenticationToken is supported"));
|
||||
|
||||
// Determine username
|
||||
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
|
||||
: authentication.getName();
|
||||
|
||||
boolean cacheWasUsed = true;
|
||||
UserDetails user = this.userCache.getUserFromCache(username);
|
||||
|
||||
if (user == null) {
|
||||
cacheWasUsed = false;
|
||||
|
||||
try {
|
||||
user = retrieveUser(username,
|
||||
(SmsCodeAuthenticationToken) authentication);
|
||||
}
|
||||
catch (UsernameNotFoundException notFound) {
|
||||
logger.debug("User '" + username + "' not found");
|
||||
|
||||
if (hideUserNotFoundExceptions) {
|
||||
throw new BadCredentialsException(messages.getMessage(
|
||||
"AbstractSmsAuthenticationProvider.badCredentials",
|
||||
"Bad credentials"));
|
||||
}
|
||||
else {
|
||||
throw notFound;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.notNull(user,
|
||||
"retrieveUser returned null - a violation of the interface contract");
|
||||
}
|
||||
|
||||
try {
|
||||
preAuthenticationChecks.check(user);
|
||||
additionalAuthenticationChecks(user,
|
||||
(SmsCodeAuthenticationToken) authentication);
|
||||
}
|
||||
catch (AuthenticationException exception) {
|
||||
if (cacheWasUsed) {
|
||||
// There was a problem, so try again after checking
|
||||
// we're using latest data (i.e. not from the cache)
|
||||
cacheWasUsed = false;
|
||||
user = retrieveUser(username,
|
||||
(SmsCodeAuthenticationToken) authentication);
|
||||
preAuthenticationChecks.check(user);
|
||||
additionalAuthenticationChecks(user,
|
||||
(SmsCodeAuthenticationToken) authentication);
|
||||
}
|
||||
else {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
postAuthenticationChecks.check(user);
|
||||
|
||||
if (!cacheWasUsed) {
|
||||
this.userCache.putUserInCache(user);
|
||||
}
|
||||
|
||||
Object principalToReturn = user;
|
||||
|
||||
if (forcePrincipalAsString) {
|
||||
principalToReturn = user.getUsername();
|
||||
}
|
||||
|
||||
return createSuccessAuthentication(principalToReturn, authentication, user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a successful {@link Authentication} object.
|
||||
* <p>
|
||||
* Protected so subclasses can override.
|
||||
* </p>
|
||||
* <p>
|
||||
* Subclasses will usually store the original credentials the user supplied (not
|
||||
* salted or encoded passwords) in the returned <code>Authentication</code> object.
|
||||
* </p>
|
||||
*
|
||||
* @param principal that should be the principal in the returned object (defined by
|
||||
* the {@link #isForcePrincipalAsString()} method)
|
||||
* @param authentication that was presented to the provider for validation
|
||||
* @param user that was loaded by the implementation
|
||||
*
|
||||
* @return the successful authentication token
|
||||
*/
|
||||
protected Authentication createSuccessAuthentication(Object principal,
|
||||
Authentication authentication, UserDetails user) {
|
||||
// Ensure we return the original credentials the user supplied,
|
||||
// so subsequent attempts are successful even with encoded passwords.
|
||||
// Also ensure we return the original getDetails(), so that future
|
||||
// authentication events after cache expiry contain the details
|
||||
SmsCodeAuthenticationToken result = new SmsCodeAuthenticationToken(
|
||||
principal, authentication.getCredentials(),
|
||||
authoritiesMapper.mapAuthorities(user.getAuthorities()));
|
||||
result.setDetails(authentication.getDetails());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected void doAfterPropertiesSet() throws Exception {
|
||||
}
|
||||
|
||||
public UserCache getUserCache() {
|
||||
return userCache;
|
||||
}
|
||||
|
||||
public boolean isForcePrincipalAsString() {
|
||||
return forcePrincipalAsString;
|
||||
}
|
||||
|
||||
public boolean isHideUserNotFoundExceptions() {
|
||||
return hideUserNotFoundExceptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows subclasses to actually retrieve the <code>UserDetails</code> from an
|
||||
* implementation-specific location, with the option of throwing an
|
||||
* <code>AuthenticationException</code> immediately if the presented credentials are
|
||||
* incorrect (this is especially useful if it is necessary to bind to a resource as
|
||||
* the user in order to obtain or generate a <code>UserDetails</code>).
|
||||
* <p>
|
||||
* Subclasses are not required to perform any caching, as the
|
||||
* <code>AbstractSmsAuthenticationProvider</code> will by default cache the
|
||||
* <code>UserDetails</code>. The caching of <code>UserDetails</code> does present
|
||||
* additional complexity as this means subsequent requests that rely on the cache will
|
||||
* need to still have their credentials validated, even if the correctness of
|
||||
* credentials was assured by subclasses adopting a binding-based strategy in this
|
||||
* method. Accordingly it is important that subclasses either disable caching (if they
|
||||
* want to ensure that this method is the only method that is capable of
|
||||
* authenticating a request, as no <code>UserDetails</code> will ever be cached) or
|
||||
* ensure subclasses implement
|
||||
* {@link #additionalAuthenticationChecks(UserDetails, SmsCodeAuthenticationToken)}
|
||||
* to compare the credentials of a cached <code>UserDetails</code> with subsequent
|
||||
* authentication requests.
|
||||
* </p>
|
||||
* <p>
|
||||
* Most of the time subclasses will not perform credentials inspection in this method,
|
||||
* instead performing it in
|
||||
* {@link #additionalAuthenticationChecks(UserDetails, SmsCodeAuthenticationToken)}
|
||||
* so that code related to credentials validation need not be duplicated across two
|
||||
* methods.
|
||||
* </p>
|
||||
*
|
||||
* @param username The username to retrieve
|
||||
* @param authentication The authentication request, which subclasses <em>may</em>
|
||||
* need to perform a binding-based retrieval of the <code>UserDetails</code>
|
||||
*
|
||||
* @return the user information (never <code>null</code> - instead an exception should
|
||||
* the thrown)
|
||||
*
|
||||
* @throws AuthenticationException if the credentials could not be validated
|
||||
* (generally a <code>BadCredentialsException</code>, an
|
||||
* <code>AuthenticationServiceException</code> or
|
||||
* <code>UsernameNotFoundException</code>)
|
||||
*/
|
||||
protected abstract UserDetails retrieveUser(String username,
|
||||
SmsCodeAuthenticationToken authentication)
|
||||
throws AuthenticationException;
|
||||
|
||||
public void setForcePrincipalAsString(boolean forcePrincipalAsString) {
|
||||
this.forcePrincipalAsString = forcePrincipalAsString;
|
||||
}
|
||||
|
||||
/**
|
||||
* By default the <code>AbstractSmsAuthenticationProvider</code> throws a
|
||||
* <code>BadCredentialsException</code> if a username is not found or the password is
|
||||
* incorrect. Setting this property to <code>false</code> will cause
|
||||
* <code>UsernameNotFoundException</code>s to be thrown instead for the former. Note
|
||||
* this is considered less secure than throwing <code>BadCredentialsException</code>
|
||||
* for both exceptions.
|
||||
*
|
||||
* @param hideUserNotFoundExceptions set to <code>false</code> if you wish
|
||||
* <code>UsernameNotFoundException</code>s to be thrown instead of the non-specific
|
||||
* <code>BadCredentialsException</code> (defaults to <code>true</code>)
|
||||
*/
|
||||
public void setHideUserNotFoundExceptions(boolean hideUserNotFoundExceptions) {
|
||||
this.hideUserNotFoundExceptions = hideUserNotFoundExceptions;
|
||||
}
|
||||
|
||||
public void setMessageSource(MessageSource messageSource) {
|
||||
this.messages = new MessageSourceAccessor(messageSource);
|
||||
}
|
||||
|
||||
public void setUserCache(UserCache userCache) {
|
||||
this.userCache = userCache;
|
||||
}
|
||||
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return (SmsCodeAuthenticationToken.class
|
||||
.isAssignableFrom(authentication));
|
||||
}
|
||||
|
||||
protected UserDetailsChecker getPreAuthenticationChecks() {
|
||||
return preAuthenticationChecks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the policy will be used to verify the status of the loaded
|
||||
* <tt>UserDetails</tt> <em>before</em> validation of the credentials takes place.
|
||||
*
|
||||
* @param preAuthenticationChecks strategy to be invoked prior to authentication.
|
||||
*/
|
||||
public void setPreAuthenticationChecks(UserDetailsChecker preAuthenticationChecks) {
|
||||
this.preAuthenticationChecks = preAuthenticationChecks;
|
||||
}
|
||||
|
||||
protected UserDetailsChecker getPostAuthenticationChecks() {
|
||||
return postAuthenticationChecks;
|
||||
}
|
||||
|
||||
public void setPostAuthenticationChecks(UserDetailsChecker postAuthenticationChecks) {
|
||||
this.postAuthenticationChecks = postAuthenticationChecks;
|
||||
}
|
||||
|
||||
public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
|
||||
this.authoritiesMapper = authoritiesMapper;
|
||||
}
|
||||
|
||||
private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
|
||||
public void check(UserDetails user) {
|
||||
if (!user.isAccountNonLocked()) {
|
||||
logger.debug("User account is locked");
|
||||
|
||||
throw new LockedException(messages.getMessage(
|
||||
"AbstractSmsAuthenticationProvider.locked",
|
||||
"User account is locked"));
|
||||
}
|
||||
|
||||
if (!user.isEnabled()) {
|
||||
logger.debug("User account is disabled");
|
||||
|
||||
throw new DisabledException(messages.getMessage(
|
||||
"AbstractSmsAuthenticationProvider.disabled",
|
||||
"User is disabled"));
|
||||
}
|
||||
|
||||
if (!user.isAccountNonExpired()) {
|
||||
logger.debug("User account is expired");
|
||||
|
||||
throw new AccountExpiredException(messages.getMessage(
|
||||
"AbstractSmsAuthenticationProvider.expired",
|
||||
"User account has expired"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
|
||||
public void check(UserDetails user) {
|
||||
if (!user.isCredentialsNonExpired()) {
|
||||
logger.debug("User account credentials have expired");
|
||||
|
||||
throw new CredentialsExpiredException(messages.getMessage(
|
||||
"AbstractSmsAuthenticationProvider.credentialsExpired",
|
||||
"User credentials have expired"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.njcn.auth.security.sm4;
|
||||
package com.njcn.auth.security.provider;
|
||||
|
||||
import com.njcn.auth.pojo.bo.BusinessUser;
|
||||
import com.njcn.common.utils.sm.Sm4Utils;
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.njcn.auth.security.provider;
|
||||
|
||||
import com.njcn.auth.security.token.SmsCodeAuthenticationToken;
|
||||
import com.njcn.auth.service.UserDetailsServiceImpl;
|
||||
import com.njcn.common.pojo.exception.BusinessException;
|
||||
import com.njcn.user.enums.UserResponseEnum;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 手机短信码验证完后,返回用户的
|
||||
* @author hongawen
|
||||
* @version 1.0.0
|
||||
* @date 2021年06月08日 15:43
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class SmsAuthenticationProvider extends AbstractSmsAuthenticationProvider {
|
||||
|
||||
private final UserDetailsServiceImpl userDetailsService;
|
||||
|
||||
|
||||
/**
|
||||
* 校验密码有效性.
|
||||
* 因为手机号验证码登录,验证码没问题后,密码无需校验,直接返回该用户的token信息便可以
|
||||
*
|
||||
* @param userDetails 用户详细信息
|
||||
* @param authentication 用户登录的密码
|
||||
* @throws AuthenticationException .
|
||||
*/
|
||||
@Override
|
||||
protected void additionalAuthenticationChecks(
|
||||
UserDetails userDetails, SmsCodeAuthenticationToken authentication)
|
||||
throws AuthenticationException {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户
|
||||
*
|
||||
* @param phone 手机号
|
||||
* @param authentication 认证token
|
||||
* @throws AuthenticationException .
|
||||
*/
|
||||
@Override
|
||||
protected UserDetails retrieveUser(
|
||||
String phone, SmsCodeAuthenticationToken authentication)
|
||||
throws AuthenticationException {
|
||||
//根据手机号获取用户信息
|
||||
UserDetails loadedUser = userDetailsService.loadUserByPhone(phone);
|
||||
if (loadedUser == null) {
|
||||
throw new BusinessException(UserResponseEnum.LOGIN_PHONE_NOT_REGISTER);
|
||||
}
|
||||
return loadedUser;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 授权持久化.
|
||||
*/
|
||||
@Override
|
||||
protected Authentication createSuccessAuthentication(Object principal,
|
||||
Authentication authentication, UserDetails user) {
|
||||
return super.createSuccessAuthentication(principal, authentication, user);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.njcn.auth.security.token;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* UsernamePasswordAuthenticationToken 一样,
|
||||
* 继承 AbstractAuthenticationToken 抽象类,
|
||||
* 需要实现 getPrincipal 和 getCredentials 两个方法。
|
||||
* 在用户名/密码认证中,principal 表示用户名,
|
||||
* credentials 表示密码,在此,我们可以让它们指代手机号和验证码。
|
||||
*
|
||||
* @author hongawen
|
||||
* @version 1.0.0
|
||||
* @date 2023年06月14日 16:25
|
||||
*/
|
||||
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
|
||||
|
||||
private final Object principal;
|
||||
|
||||
private Object credentials;
|
||||
|
||||
public SmsCodeAuthenticationToken(Object principal, Object credentials) {
|
||||
super(null);
|
||||
this.principal = principal;
|
||||
this.credentials = credentials;
|
||||
setAuthenticated(false);
|
||||
}
|
||||
public SmsCodeAuthenticationToken(Object principal, Object credentials,
|
||||
Collection<? extends GrantedAuthority> authorities) {
|
||||
super(authorities);
|
||||
this.principal = principal;
|
||||
this.credentials = credentials;
|
||||
super.setAuthenticated(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return this.credentials;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.principal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
|
||||
Assert.isTrue(!isAuthenticated,
|
||||
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
|
||||
super.setAuthenticated(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void eraseCredentials() {
|
||||
super.eraseCredentials();
|
||||
this.credentials = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.njcn.auth.service;
|
||||
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
|
||||
/**
|
||||
* @author hongawen
|
||||
* @version 1.0.0
|
||||
* @date 2023年06月15日 10:26
|
||||
*/
|
||||
public interface CustomUserDetailsService extends UserDetailsService {
|
||||
|
||||
/**
|
||||
* @param username 用户名
|
||||
* @return 用户信息
|
||||
* @throws UsernameNotFoundException
|
||||
*/
|
||||
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
|
||||
|
||||
/**
|
||||
* @param phone 手机号
|
||||
* @return 用户信息
|
||||
* @throws UsernameNotFoundException
|
||||
*/
|
||||
UserDetails loadUserByPhone(String phone) throws UsernameNotFoundException;
|
||||
|
||||
}
|
||||
@@ -25,7 +25,7 @@ import org.springframework.stereotype.Service;
|
||||
@Slf4j
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class UserDetailsServiceImpl implements UserDetailsService {
|
||||
public class UserDetailsServiceImpl implements CustomUserDetailsService {
|
||||
|
||||
private final UserFeignClient userFeignClient;
|
||||
|
||||
@@ -44,4 +44,18 @@ public class UserDetailsServiceImpl implements UserDetailsService {
|
||||
return businessUser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByPhone(String phone) throws UsernameNotFoundException {
|
||||
String clientId = RequestUtil.getOAuth2ClientId();
|
||||
BusinessUser businessUser = new BusinessUser(phone, null, null);
|
||||
businessUser.setClientId(clientId);
|
||||
HttpResult<UserDTO> result = userFeignClient.getUserByPhone(phone);
|
||||
LogUtil.njcnDebug(log, "用户验证码认证时,用户名:{}获取用户信息:{}", phone, result.toString());
|
||||
//成功获取用户信息
|
||||
UserDTO userDTO = result.getData();
|
||||
BeanUtil.copyProperties(userDTO,businessUser,true);
|
||||
businessUser.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(String.join(",", userDTO.getRoleName())));
|
||||
return businessUser;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,16 +26,14 @@ public class AuthTest extends BaseJunitTest {
|
||||
@SneakyThrows
|
||||
@Test
|
||||
public void test(){
|
||||
String userUrl = "http://127.0.0.1:10214/oauth/token";
|
||||
String userUrl = "http://127.0.0.1:10215/pqs-auth/oauth/token";
|
||||
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(userUrl)
|
||||
.queryParam("grant_type", "password")
|
||||
.queryParam("client_id", "njcn_app")
|
||||
.queryParam("client_id", "njcn")
|
||||
.queryParam("client_secret", "njcnpqs")
|
||||
.queryParam("username", "root")
|
||||
.queryParam("password", "@#001njcnpqs");
|
||||
URI uri = builder.build().encode().toUri();
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.set("","");
|
||||
|
||||
ResponseEntity<OAuth2AccessToken> userEntity = RestTemplateUtil.post(uri, OAuth2AccessToken.class);
|
||||
}
|
||||
|
||||
@@ -94,6 +94,8 @@ public interface SecurityConstants {
|
||||
String REFRESH_TOKEN = "refresh_token";
|
||||
String USERNAME = "username";
|
||||
String PASSWORD = "password";
|
||||
String PHONE = "phone";
|
||||
String SMS_CODE = "smsCode";
|
||||
String IMAGE_CODE = "imageCode";
|
||||
String VERIFY_CODE = "verifyCode";
|
||||
|
||||
|
||||
@@ -16,6 +16,11 @@ public enum RedisKeyEnum {
|
||||
ROLE_FUNCTION_KEY("ROLES_FUNCTIONS",-1L),
|
||||
PUBLIC_FUNCTIONS_KEY("PUBLIC_FUNCTIONS",-1L),
|
||||
|
||||
/***
|
||||
* app短信验证码,保存10分钟缓存
|
||||
*/
|
||||
SMS_LOGIN_KEY("SMS_LOGIN",10L),
|
||||
|
||||
/**
|
||||
* 终端信息查询缓存的公共key前缀
|
||||
*/
|
||||
|
||||
@@ -31,6 +31,15 @@ public interface UserFeignClient {
|
||||
@GetMapping("/getUserByName/{loginName}")
|
||||
HttpResult<UserDTO> getUserByName(@PathVariable("loginName") String loginName);
|
||||
|
||||
/**
|
||||
* 根据手机号查询用户信息
|
||||
*
|
||||
* @param phone 登录名
|
||||
* @return 用户基本信息
|
||||
*/
|
||||
@GetMapping("/getUserByPhone/{phone}")
|
||||
HttpResult<UserDTO> getUserByPhone(@PathVariable("phone")String phone);
|
||||
|
||||
/**
|
||||
* 认证后根据用户名判断用户状态
|
||||
* @param loginName 登录名
|
||||
@@ -55,4 +64,6 @@ public interface UserFeignClient {
|
||||
*/
|
||||
@PostMapping("/userByIdList")
|
||||
HttpResult<List<User>> getUserByIdList(@RequestBody List<String> ids);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -43,6 +43,12 @@ public class UserFeignClientFallbackFactory implements FallbackFactory<UserFeign
|
||||
throw new BusinessException(finalExceptionEnum);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpResult<UserDTO> getUserByPhone(String phone) {
|
||||
log.error("{}异常,降级处理,异常为:{}","根据手机号查询用户信息",cause.toString());
|
||||
throw new BusinessException(finalExceptionEnum);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpResult<Boolean> judgeUserStatus(String loginName) {
|
||||
log.error("{}异常,降级处理,异常为:{}","认证后根据用户名判断用户状态",cause.toString());
|
||||
|
||||
@@ -24,6 +24,7 @@ public enum UserResponseEnum {
|
||||
LOGIN_USERNAME_INVALID("A0101", "用户名非法"),
|
||||
LOGIN_USER_INDEX_INVALID("A0101", "用户索引非法"),
|
||||
LOGIN_PHONE_NOT_FOUND("A0101", "手机号不存在"),
|
||||
LOGIN_PHONE_NOT_REGISTER("A0101", "手机号未注册"),
|
||||
KEY_WRONG("A0101","登录密码/验证码为空"),
|
||||
LOGIN_WRONG_PWD("A0101", "用户名密码错误"),
|
||||
LOGIN_WRONG_PHONE_CODE("A0101", "短信验证码错误"),
|
||||
|
||||
@@ -58,12 +58,12 @@ public class AuthClient {
|
||||
private String authorities;
|
||||
|
||||
/**
|
||||
* 认证令牌时效
|
||||
* 认证令牌时效 单位:秒
|
||||
*/
|
||||
private Integer accessTokenValidity;
|
||||
|
||||
/**
|
||||
* 刷新令牌时效
|
||||
* 刷新令牌时效 单位:秒
|
||||
*/
|
||||
private Integer refreshTokenValidity;
|
||||
|
||||
|
||||
@@ -80,6 +80,24 @@ public class UserController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@OperateInfo
|
||||
@ApiIgnore
|
||||
@GetMapping("/getUserByPhone/{phone}")
|
||||
@ApiOperation("根据手机号查询用户信息")
|
||||
@ApiImplicitParam(name = "phone", value = "手机号", required = true)
|
||||
public HttpResult<UserDTO> getUserByPhone(@PathVariable String phone) {
|
||||
RequestUtil.saveLoginName(phone);
|
||||
String methodDescribe = getMethodDescribe("getUserByPhone");
|
||||
LogUtil.njcnDebug(log, "{},手机号为:{}", methodDescribe, phone);
|
||||
UserDTO user = userService.loadUserByPhone(phone);
|
||||
if (Objects.isNull(user)) {
|
||||
throw new BusinessException(UserResponseEnum.LOGIN_PHONE_NOT_REGISTER);
|
||||
} else {
|
||||
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, user, methodDescribe);
|
||||
}
|
||||
}
|
||||
|
||||
@OperateInfo
|
||||
@GetMapping("/judgeUserStatus/{loginName}")
|
||||
@ApiOperation("认证后根据用户名判断用户状态")
|
||||
|
||||
@@ -27,6 +27,14 @@ public interface IUserService extends IService<User> {
|
||||
*/
|
||||
UserDTO getUserByName(String loginName);
|
||||
|
||||
/**
|
||||
* 根据手机号查询用户信息
|
||||
*
|
||||
* @param phone 登录名
|
||||
* @return 用户基本信息
|
||||
*/
|
||||
UserDTO loadUserByPhone(String phone);
|
||||
|
||||
/**
|
||||
* 认证结束后,判断用户状态是否能正常访问系统
|
||||
* @param loginName 登录名
|
||||
@@ -160,4 +168,6 @@ public interface IUserService extends IService<User> {
|
||||
String exportUser(UserParam.UserQueryParam queryParam,String methodDescribe);
|
||||
|
||||
boolean activateUser(String id);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -98,6 +98,17 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IU
|
||||
return new UserDTO(user.getId(), user.getLoginName(), user.getName(), user.getPassword(), roleNames, userSet.getSecretKey(), userSet.getStandBy(), user.getDeptId(), user.getType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDTO loadUserByPhone(String phone) {
|
||||
User user = getUserByPhone(phone,false,null);
|
||||
if (Objects.isNull(user)) {
|
||||
return null;
|
||||
}
|
||||
List<String> roleNames = roleService.getRoleNameByUserId(user.getId());
|
||||
UserSet userSet = userSetService.lambdaQuery().eq(UserSet::getUserId, user.getId()).one();
|
||||
return new UserDTO(user.getId(), user.getLoginName(), user.getName(), user.getPassword(), roleNames, userSet.getSecretKey(), userSet.getStandBy(), user.getDeptId(), user.getType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void judgeUserStatus(String loginName) {
|
||||
User user = getUserByLoginName(loginName);
|
||||
|
||||
Reference in New Issue
Block a user