!157 新增手机号登录授权方式

Merge pull request !157 from Hzq/dev
This commit is contained in:
lengleng 2021-09-16 02:16:52 +00:00 committed by Gitee
commit 7d63b14126
12 changed files with 285 additions and 13 deletions

View File

@ -273,8 +273,8 @@ INSERT INTO `sys_oauth_client_details` VALUES ('app', NULL, 'app', 'server', 'pa
INSERT INTO `sys_oauth_client_details` VALUES ('ASD', '', 'ASD', 'ASD', 'ASDddddxxxxxxxxxxxx', '', '', NULL, NULL, '', 'false', '2021-08-09 14:19:21', '2021-08-09 14:35:29', 'admin', 'admin');
INSERT INTO `sys_oauth_client_details` VALUES ('daemon', NULL, 'daemon', 'server', 'password,refresh_token', NULL, NULL, NULL, NULL, NULL, 'true', NULL, NULL, NULL, NULL);
INSERT INTO `sys_oauth_client_details` VALUES ('gen', NULL, 'gen', 'server', 'password,refresh_token', NULL, NULL, NULL, NULL, NULL, 'true', NULL, NULL, NULL, NULL);
INSERT INTO `sys_oauth_client_details` VALUES ('pig', NULL, 'pig', 'server', 'password,refresh_token,authorization_code,client_credentials', 'http://localhost:4040/sso1/login,http://localhost:4041/sso1/login', NULL, NULL, NULL, NULL, 'true', NULL, NULL, NULL, NULL);
INSERT INTO `sys_oauth_client_details` VALUES ('test', NULL, 'test', 'server', 'password,refresh_token', NULL, NULL, NULL, NULL, NULL, 'true', NULL, NULL, NULL, NULL);
INSERT INTO `sys_oauth_client_details` VALUES ('pig', NULL, 'pig', 'server', 'password,phone,refresh_token,authorization_code,client_credentials', 'http://localhost:4040/sso1/login,http://localhost:4041/sso1/login', NULL, NULL, NULL, NULL, 'true', NULL, NULL, NULL, NULL);
INSERT INTO `sys_oauth_client_details` VALUES ('test', NULL, 'test', 'server', 'password,phone,refresh_token', NULL, NULL, NULL, NULL, NULL, 'true', NULL, NULL, NULL, NULL);
INSERT INTO `sys_oauth_client_details` VALUES ('zxc', '', 'zxc', 'zxcxzc', 'cxz', 'cxz', 'zcxzxcxcxzccxz', NULL, NULL, 'zxc', 'true', '2021-08-09 14:37:45', '2021-08-09 14:37:55', 'admin', 'admin');
COMMIT;

View File

@ -17,6 +17,7 @@
package com.pig4cloud.pig.auth.config;
import com.pig4cloud.pig.auth.converter.CustomAccessTokenConverter;
import com.pig4cloud.pig.auth.grant.ResourceOwnerPhoneTokenGranter;
import com.pig4cloud.pig.common.core.constant.CacheConstants;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.security.component.PigWebResponseExceptionTranslator;
@ -36,11 +37,15 @@ import org.springframework.security.oauth2.config.annotation.web.configuration.A
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.CompositeTokenGranter;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
@ -80,6 +85,18 @@ public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdap
.pathMapping("/oauth/confirm_access", "/token/confirm_access")
.exceptionTranslator(new PigWebResponseExceptionTranslator())
.accessTokenConverter(new CustomAccessTokenConverter(pigClientDetailsService()));
setTokenGranter(endpoints);
}
private void setTokenGranter(AuthorizationServerEndpointsConfigurer endpoints) {
// 获取默认授权类型
TokenGranter tokenGranter = endpoints.getTokenGranter();
ArrayList<TokenGranter> tokenGranters = new ArrayList<>(Arrays.asList(tokenGranter));
ResourceOwnerPhoneTokenGranter resourceOwnerPhoneTokenGranter = new ResourceOwnerPhoneTokenGranter(authenticationManager,
endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory());
tokenGranters.add(resourceOwnerPhoneTokenGranter);
CompositeTokenGranter compositeTokenGranter = new CompositeTokenGranter(tokenGranters);
endpoints.tokenGranter(compositeTokenGranter);
}
@Bean

View File

@ -16,8 +16,10 @@
package com.pig4cloud.pig.auth.config;
import com.pig4cloud.pig.auth.grant.PhoneAuthenticationProvider;
import com.pig4cloud.pig.common.security.handler.FormAuthenticationFailureHandler;
import com.pig4cloud.pig.common.security.handler.SsoLogoutSuccessHandler;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -27,6 +29,7 @@ import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
@ -39,18 +42,32 @@ import org.springframework.security.web.authentication.logout.LogoutSuccessHandl
@Primary
@Order(90)
@Configuration
@AllArgsConstructor
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
@Override
@SneakyThrows
protected void configure(HttpSecurity http) {
http.formLogin().loginPage("/token/login").loginProcessingUrl("/token/form")
http.authenticationProvider(phoneAuthenticationProvider())
.formLogin().loginPage("/token/login").loginProcessingUrl("/token/form")
.failureHandler(authenticationFailureHandler()).and().logout()
.logoutSuccessHandler(logoutSuccessHandler()).deleteCookies("JSESSIONID").invalidateHttpSession(true)
.and().authorizeRequests().antMatchers("/token/**", "/actuator/**", "/mobile/**").permitAll()
.anyRequest().authenticated().and().csrf().disable();
}
/**
* 不要直接使用@Bean注入 会导致默认的提供者无法注入DaoAuthenticationProvider
*/
private PhoneAuthenticationProvider phoneAuthenticationProvider() {
PhoneAuthenticationProvider phoneAuthenticationProvider = new PhoneAuthenticationProvider();
phoneAuthenticationProvider.setPasswordEncoder(passwordEncoder());
phoneAuthenticationProvider.setUserDetailsService(userDetailsService);
return phoneAuthenticationProvider;
}
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/css/**");
@ -70,6 +87,7 @@ public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
/**
* 支持SSO 退出
*
* @return LogoutSuccessHandler
*/
@Bean
@ -80,6 +98,7 @@ public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
/**
* https://spring.io/blog/2017/11/01/spring-security-5-0-0-rc1-released#password-storage-updated
* Encoded password does not look like BCrypt
*
* @return PasswordEncoder
*/
@Bean

View File

@ -0,0 +1,62 @@
package com.pig4cloud.pig.auth.grant;
import com.pig4cloud.pig.common.security.service.PigUserDetailsServiceImpl;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author hzq
* @since 2021-09-14
*/
@Slf4j
public class PhoneAuthenticationProvider implements AuthenticationProvider {
@Setter
private UserDetailsService userDetailsService;
@Setter
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
log.debug("Failed to authenticate since no credentials provided");
throw new BeanCreationException("Bad credentials");
}
// 手机号
String phone = authentication.getName();
// 验证码/密码
// 验证码模式 自己去实现验证码检验
// 这里的code指的是密码
String code = authentication.getCredentials().toString();
UserDetails userDetails = ((PigUserDetailsServiceImpl) userDetailsService).loadUserByPhone(phone);
String password = userDetails.getPassword();
boolean matches = passwordEncoder.matches(code, password);
if (!matches) {
throw new BeanCreationException("Bad credentials");
}
PhoneAuthenticationToken token = new PhoneAuthenticationToken(userDetails);
token.setDetails(authentication.getDetails());
return token;
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.isAssignableFrom(PhoneAuthenticationToken.class);
}
}

View File

@ -0,0 +1,40 @@
package com.pig4cloud.pig.auth.grant;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
/**
* @author hzq
* @since 2021-09-14
*/
public class PhoneAuthenticationToken extends AbstractAuthenticationToken {
private Object principal;
// 验证码/密码
private String code;
public PhoneAuthenticationToken(String phone, String code) {
super(AuthorityUtils.NO_AUTHORITIES);
this.principal = phone;
this.code = code;
}
public PhoneAuthenticationToken(UserDetails sysUser) {
super(sysUser.getAuthorities());
this.principal = sysUser;
super.setAuthenticated(true); // 设置认证成功 必须
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public Object getCredentials() {
return this.code;
}
}

View File

@ -0,0 +1,79 @@
package com.pig4cloud.pig.auth.grant;
import cn.hutool.core.util.StrUtil;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
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 hzq
* @since 2021-09-14
*/
public class ResourceOwnerPhoneTokenGranter extends AbstractTokenGranter {
private static final String GRANT_TYPE = "phone";
private final AuthenticationManager authenticationManager;
public ResourceOwnerPhoneTokenGranter(AuthenticationManager authenticationManager,
AuthorizationServerTokenServices tokenServices,
ClientDetailsService clientDetailsService,
OAuth2RequestFactory requestFactory) {
this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
}
protected ResourceOwnerPhoneTokenGranter(AuthenticationManager authenticationManager,
AuthorizationServerTokenServices tokenServices,
ClientDetailsService clientDetailsService,
OAuth2RequestFactory requestFactory, String grantType) {
super(tokenServices, clientDetailsService, requestFactory, grantType);
this.authenticationManager = authenticationManager;
}
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters());
// 手机号
String phone = parameters.get("phone");
// 验证码/密码
String code = parameters.get("code");
if (StrUtil.isBlank(phone) || StrUtil.isBlank(code)) {
throw new InvalidGrantException("Bad credentials [ params must be has phone with code ]");
}
// Protect from downstream leaks of code
parameters.remove("code");
Authentication userAuth = new PhoneAuthenticationToken(phone, code);
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
try {
userAuth = authenticationManager.authenticate(userAuth);
} catch (AccountStatusException ase) {
//covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
throw new InvalidGrantException(ase.getMessage());
} catch (BadCredentialsException e) {
// If the phone/code are wrong the spec says we should send 400/invalid grant
throw new InvalidGrantException(e.getMessage());
}
if (userAuth == null || !userAuth.isAuthenticated()) {
throw new InvalidGrantException("Could not authenticate user: " + phone);
}
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
}

View File

@ -56,6 +56,7 @@ public interface SecurityConstants {
* grant_type
*/
String REFRESH_TOKEN = "refresh_token";
String PHONE = "phone";
/**
* {bcrypt} 加密的特征码

View File

@ -62,6 +62,7 @@ public class PigUserDetailsServiceImpl implements UserDetailsService {
/**
* 用户密码登录
*
* @param username 用户名
* @return
*/
@ -81,8 +82,21 @@ public class PigUserDetailsServiceImpl implements UserDetailsService {
return userDetails;
}
/**
* 手机号码登录
*
* @param phone 手机号码
* @return 用户信息
*/
public UserDetails loadUserByPhone(String phone) {
R<UserInfo> result = remoteUserService.infoByPhone(phone, SecurityConstants.FROM_IN);
UserDetails userDetails = getUserDetails(result);
return userDetails;
}
/**
* 构建userdetails
*
* @param result 用户信息
* @return UserDetails
*/

View File

@ -68,9 +68,9 @@ public class ValidateCodeGatewayFilter extends AbstractGatewayFilterFactory<Obje
return chain.filter(exchange);
}
// 刷新token直接向下执行
// 刷新token手机号登录也可以这里进行校验 直接向下执行
String grantType = request.getQueryParams().getFirst("grant_type");
if (StrUtil.equals(SecurityConstants.REFRESH_TOKEN, grantType)) {
if (StrUtil.equals(SecurityConstants.REFRESH_TOKEN, grantType) || StrUtil.equals(SecurityConstants.PHONE, grantType)) {
return chain.filter(exchange);
}
@ -80,8 +80,7 @@ public class ValidateCodeGatewayFilter extends AbstractGatewayFilterFactory<Obje
if (!isIgnoreClient) {
checkCode(request);
}
}
catch (Exception e) {
} catch (Exception e) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.PRECONDITION_REQUIRED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
@ -93,8 +92,7 @@ public class ValidateCodeGatewayFilter extends AbstractGatewayFilterFactory<Obje
DataBuffer dataBuffer = response.bufferFactory().wrap(bytes);
monoSink.success(dataBuffer);
}
catch (JsonProcessingException jsonProcessingException) {
} catch (JsonProcessingException jsonProcessingException) {
log.error("对象输出异常", jsonProcessingException);
monoSink.error(jsonProcessingException);
}

View File

@ -40,6 +40,7 @@ public interface RemoteUserService {
/**
* 通过用户名查询用户角色信息
*
* @param username 用户名
* @param from 调用标志
* @return R
@ -47,8 +48,19 @@ public interface RemoteUserService {
@GetMapping("/user/info/{username}")
R<UserInfo> info(@PathVariable("username") String username, @RequestHeader(SecurityConstants.FROM) String from);
/**
* 通过手机号码查询用户角色信息
*
* @param phone 手机号码
* @param from 调用标志
* @return R
*/
@GetMapping("/user/infoByPhone/{phone}")
R<UserInfo> infoByPhone(@PathVariable("phone") String phone, @RequestHeader(SecurityConstants.FROM) String from);
/**
* 通过社交账号查询用户角色信息
*
* @param inStr appid@code
* @return
*/
@ -57,6 +69,7 @@ public interface RemoteUserService {
/**
* 根据部门id查询对应的用户 id 集合
*
* @param deptIds 部门id 集合
* @param from 调用标志
* @return 用户 id 集合

View File

@ -39,6 +39,7 @@ public class RemoteUserServiceFallbackImpl implements RemoteUserService {
/**
* 通过用户名查询用户角色信息
*
* @param username 用户名
* @param from 内外标志
* @return R
@ -49,8 +50,22 @@ public class RemoteUserServiceFallbackImpl implements RemoteUserService {
return null;
}
/**
* 通过手机号码查询用户角色信息
*
* @param phone 手机号码
* @param from 调用标志
* @return R
*/
@Override
public R<UserInfo> infoByPhone(String phone, String from) {
log.error("feign 查询用户信息失败手机号码:{}", phone, cause);
return null;
}
/**
* 通过社交账号查询用户角色信息
*
* @param inStr appid@code
* @return
*/

View File

@ -94,6 +94,20 @@ public class UserController {
return R.ok(userService.getUserInfo(user));
}
/**
* 获取指定用户全部信息
* @return 用户信息
*/
@Inner
@GetMapping("/infoByPhone/{phone}")
public R infoByPhone(@PathVariable String phone) {
SysUser user = userService.getOne(Wrappers.<SysUser>query().lambda().eq(SysUser::getPhone, phone));
if (user == null) {
return R.failed(String.format("用户信息为空 %s", phone));
}
return R.ok(userService.getUserInfo(user));
}
/**
* 根据部门id查询对应的用户 id 集合
* @param deptIds 部门id 集合