多模块重构 3:security 实现多用户的认证支持

This commit is contained in:
YunaiV 2022-01-29 00:44:03 +08:00
parent 928b7dbe23
commit e9efff7076
23 changed files with 279 additions and 184 deletions

View File

@ -2,6 +2,10 @@
"local": { "local": {
"baseUrl": "http://127.0.0.1:48080/api", "baseUrl": "http://127.0.0.1:48080/api",
"userServerUrl": "http://127.0.0.1:28080/api", "userServerUrl": "http://127.0.0.1:28080/api",
"token": "test1" "token": "test1",
"userApi": "http://127.0.0.1:48080/app-api",
"userToken": "test1",
"userTenentId": "1"
} }
} }

View File

@ -14,7 +14,6 @@
<module>yudao-user-server</module> <module>yudao-user-server</module>
<module>yudao-core-service</module> <module>yudao-core-service</module>
<module>yudao-module-member</module> <module>yudao-module-member</module>
<module>yudao-server</module>
</modules> </modules>
<name>${artifactId}</name> <name>${artifactId}</name>

View File

@ -13,7 +13,11 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<name>yudao-admin-server</name> <name>yudao-admin-server</name>
<description>管理后台 Server提供其 API 接口</description> <description>
后端 Server 的主项目,通过引入需要 yudao-module-xxx 的依赖,
从而实现提供 RESTful API 给 yudao-ui-admin、yudao-ui-user 等前端项目。
本质上来说,它就是个空壳(容器)!
</description>
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<dependencies> <dependencies>

View File

@ -33,7 +33,7 @@ public class SecurityConfiguration {
registry.antMatchers(buildAdminApi("/system/sms/callback/**")).anonymous(); registry.antMatchers(buildAdminApi("/system/sms/callback/**")).anonymous();
// 设置 App API 无需认证 // 设置 App API 无需认证
registry.antMatchers(buildAppApi("/**")); registry.antMatchers(buildAppApi("/**")).permitAll();
}; };
} }

View File

@ -26,6 +26,7 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.security.core.LoginUser; import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.security.core.authentication.MultiUsernamePasswordAuthenticationToken;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthUser; import me.zhyd.oauth.model.AuthUser;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
@ -154,7 +155,8 @@ public class SysAuthServiceImpl implements SysAuthService {
try { try {
// 调用 Spring Security AuthenticationManager#authenticate(...) 方法使用账号密码进行认证 // 调用 Spring Security AuthenticationManager#authenticate(...) 方法使用账号密码进行认证
// 在其内部会调用到 loadUserByUsername 方法获取 User 信息 // 在其内部会调用到 loadUserByUsername 方法获取 User 信息
authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); authentication = authenticationManager.authenticate(new MultiUsernamePasswordAuthenticationToken(
username, password, getUserType()));
// org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(username); // org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(username);
} catch (BadCredentialsException badCredentialsException) { } catch (BadCredentialsException badCredentialsException) {
this.createLoginLog(username, logTypeEnum, SysLoginResultEnum.BAD_CREDENTIALS); this.createLoginLog(username, logTypeEnum, SysLoginResultEnum.BAD_CREDENTIALS);

View File

@ -1,5 +1,7 @@
package cn.iocoder.yudao.framework.common.enums; package cn.iocoder.yudao.framework.common.enums;
import cn.hutool.core.lang.Matcher;
import cn.hutool.core.util.ArrayUtil;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
@ -22,4 +24,8 @@ public enum UserTypeEnum {
*/ */
private final String name; private final String name;
public static UserTypeEnum valueOf(Integer value) {
return ArrayUtil.firstMatch(userType -> userType.getValue().equals(value), UserTypeEnum.values());
}
} }

View File

@ -149,6 +149,7 @@ public class OperateLogAspect {
private static void fillUserFields(OperateLogCreateReqDTO operateLogDTO) { private static void fillUserFields(OperateLogCreateReqDTO operateLogDTO) {
operateLogDTO.setUserId(WebFrameworkUtils.getLoginUserId()); operateLogDTO.setUserId(WebFrameworkUtils.getLoginUserId());
operateLogDTO.setUserType(WebFrameworkUtils.getLoginUserType());
} }
private static void fillModuleFields(OperateLogCreateReqDTO operateLogDTO, private static void fillModuleFields(OperateLogCreateReqDTO operateLogDTO,

View File

@ -21,6 +21,9 @@ public class OperateLogCreateReqDTO {
@ApiModelProperty(value = "用户编号", required = true, example = "1024") @ApiModelProperty(value = "用户编号", required = true, example = "1024")
@NotNull(message = "用户编号不能为空") @NotNull(message = "用户编号不能为空")
private Long userId; private Long userId;
@ApiModelProperty(value = "用户类型", required = true, example = "1")
@NotNull(message = "用户类型不能为空")
private Integer userType;
@ApiModelProperty(value = "操作模块", required = true, example = "订单") @ApiModelProperty(value = "操作模块", required = true, example = "订单")
@NotEmpty(message = "操作模块不能为空") @NotEmpty(message = "操作模块不能为空")

View File

@ -1,14 +1,13 @@
package cn.iocoder.yudao.framework.security.config; package cn.iocoder.yudao.framework.security.config;
import cn.iocoder.yudao.framework.security.core.aop.PreAuthenticatedAspect; import cn.iocoder.yudao.framework.security.core.aop.PreAuthenticatedAspect;
import cn.iocoder.yudao.framework.security.core.authentication.MultiUserDetailsAuthenticationProvider;
import cn.iocoder.yudao.framework.security.core.context.TransmittableThreadLocalSecurityContextHolderStrategy; import cn.iocoder.yudao.framework.security.core.context.TransmittableThreadLocalSecurityContextHolderStrategy;
import cn.iocoder.yudao.framework.security.core.filter.JWTAuthenticationTokenFilter; import cn.iocoder.yudao.framework.security.core.filter.JWTAuthenticationTokenFilter;
import cn.iocoder.yudao.framework.security.core.handler.AccessDeniedHandlerImpl; import cn.iocoder.yudao.framework.security.core.handler.AccessDeniedHandlerImpl;
import cn.iocoder.yudao.framework.security.core.handler.AuthenticationEntryPointImpl; import cn.iocoder.yudao.framework.security.core.handler.AuthenticationEntryPointImpl;
import cn.iocoder.yudao.framework.security.core.handler.LogoutSuccessHandlerImpl; import cn.iocoder.yudao.framework.security.core.handler.LogoutSuccessHandlerImpl;
import cn.iocoder.yudao.framework.security.core.service.SecurityAuthFrameworkService; import cn.iocoder.yudao.framework.security.core.service.SecurityAuthFrameworkService;
import cn.iocoder.yudao.framework.security.core.service.SecurityAuthService;
import cn.iocoder.yudao.framework.security.core.service.SecurityAuthServiceImpl;
import cn.iocoder.yudao.framework.web.config.WebProperties; import cn.iocoder.yudao.framework.web.config.WebProperties;
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
import org.springframework.beans.factory.config.MethodInvokingFactoryBean; import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
@ -68,8 +67,8 @@ public class YudaoSecurityAutoConfiguration {
* 退出处理类 Bean * 退出处理类 Bean
*/ */
@Bean @Bean
public LogoutSuccessHandler logoutSuccessHandler(SecurityAuthService securityAuthService) { public LogoutSuccessHandler logoutSuccessHandler(MultiUserDetailsAuthenticationProvider authenticationProvider) {
return new LogoutSuccessHandlerImpl(securityProperties, securityAuthService); return new LogoutSuccessHandlerImpl(securityProperties, authenticationProvider);
} }
/** /**
@ -87,18 +86,19 @@ public class YudaoSecurityAutoConfiguration {
* Token 认证过滤器 Bean * Token 认证过滤器 Bean
*/ */
@Bean @Bean
public JWTAuthenticationTokenFilter authenticationTokenFilter(SecurityAuthService securityAuthService, public JWTAuthenticationTokenFilter authenticationTokenFilter(MultiUserDetailsAuthenticationProvider authenticationProvider,
GlobalExceptionHandler globalExceptionHandler) { GlobalExceptionHandler globalExceptionHandler) {
return new JWTAuthenticationTokenFilter(securityProperties, securityAuthService, globalExceptionHandler); return new JWTAuthenticationTokenFilter(securityProperties, authenticationProvider, globalExceptionHandler);
} }
/** /**
* 安全认证的 Service Bean * 身份验证的 Provider Bean通过它实现账号 + 密码的认证
*/ */
@Bean @Bean
public SecurityAuthService securityAuthService(List<SecurityAuthFrameworkService> securityFrameworkServices, public MultiUserDetailsAuthenticationProvider authenticationProvider(
WebProperties webProperties) { List<SecurityAuthFrameworkService> securityFrameworkServices,
return new SecurityAuthServiceImpl(securityFrameworkServices, webProperties); WebProperties webProperties, PasswordEncoder passwordEncoder) {
return new MultiUserDetailsAuthenticationProvider(securityFrameworkServices, webProperties, passwordEncoder);
} }
/** /**

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.framework.security.config; package cn.iocoder.yudao.framework.security.config;
import cn.iocoder.yudao.framework.security.core.authentication.MultiUserDetailsAuthenticationProvider;
import cn.iocoder.yudao.framework.security.core.filter.JWTAuthenticationTokenFilter; import cn.iocoder.yudao.framework.security.core.filter.JWTAuthenticationTokenFilter;
import cn.iocoder.yudao.framework.security.core.service.SecurityAuthFrameworkService; import cn.iocoder.yudao.framework.security.core.service.SecurityAuthFrameworkService;
import cn.iocoder.yudao.framework.web.config.WebProperties; import cn.iocoder.yudao.framework.web.config.WebProperties;
@ -35,16 +36,8 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap
@Resource @Resource
private WebProperties webProperties; private WebProperties webProperties;
/**
* 自定义用户认证逻辑
*/
@Resource @Resource
private SecurityAuthFrameworkService userDetailsService; private MultiUserDetailsAuthenticationProvider authenticationProvider;
/**
* Spring Security 加密器
*/
@Resource
private PasswordEncoder passwordEncoder;
/** /**
* 认证失败处理类 Bean * 认证失败处理类 Bean
*/ */
@ -91,8 +84,7 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap
*/ */
@Override @Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception { protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth auth.authenticationProvider(authenticationProvider);
.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
} }
/** /**

View File

@ -0,0 +1,149 @@
package cn.iocoder.yudao.framework.security.core.authentication;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.security.core.service.SecurityAuthFrameworkService;
import cn.iocoder.yudao.framework.web.config.WebProperties;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 支持多用户类型的 AuthenticationProvider 实现类
*
* 为什么不用 {@link org.springframework.security.authentication.ProviderManager}
* 原因是需要每个用户类型实现对应的 {@link AuthenticationProvider} + authentication略显麻烦实际也是可以实现的
*
* 另外额外支持 verifyTokenAndRefresh 校验令牌logout 登出mockLogin 模拟登陆等操作
* 实际上它就是 {@link SecurityAuthFrameworkService} 定义的三个接口
* 因为需要支持多种类型所以需要根据请求的 URL判断出对应的用户类型从而使用对应的 SecurityAuthFrameworkService 是吸纳
*
* @see cn.iocoder.yudao.framework.common.enums.UserTypeEnum
* @author 芋道源码
*/
public class MultiUserDetailsAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private final Map<UserTypeEnum, SecurityAuthFrameworkService> services = new HashMap<>();
private final WebProperties properties;
private final PasswordEncoder passwordEncoder;
public MultiUserDetailsAuthenticationProvider(List<SecurityAuthFrameworkService> serviceList,
WebProperties properties, PasswordEncoder passwordEncoder) {
serviceList.forEach(service -> services.put(service.getUserType(), service));
this.properties = properties;
this.passwordEncoder = passwordEncoder;
}
// ========== AuthenticationProvider 相关 ==========
@Override
protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
// 执行用户的加载
return selectService(authentication).loadUserByUsername(username);
}
private SecurityAuthFrameworkService selectService(UsernamePasswordAuthenticationToken authentication) {
// 第一步获得用户类型
UserTypeEnum userType = getUserType(authentication);
// 第二步获得 SecurityAuthFrameworkService
SecurityAuthFrameworkService service = services.get(userType);
Assert.notNull(service, "用户类型({}) 找不到 SecurityAuthFrameworkService 实现类", userType);
return service;
}
private UserTypeEnum getUserType(UsernamePasswordAuthenticationToken authentication) {
Assert.isInstanceOf(MultiUsernamePasswordAuthenticationToken.class, authentication);
MultiUsernamePasswordAuthenticationToken multiAuthentication = (MultiUsernamePasswordAuthenticationToken) authentication;
UserTypeEnum userType = multiAuthentication.getUserType();
Assert.notNull(userType, "用户类型不能为空");
return userType;
}
@Override // copy DaoAuthenticationProvider additionalAuthenticationChecks 方法
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
// 校验 credentials
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
// 校验 password
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
// ========== SecurityAuthFrameworkService 相关 ==========
/**
* 校验 token 的有效性并获取用户信息
* 通过后刷新 token 的过期时间
*
* @param request 请求
* @param token token
* @return 用户信息
*/
public LoginUser verifyTokenAndRefresh(HttpServletRequest request, String token) {
return selectService(request).verifyTokenAndRefresh(token);
}
/**
* 模拟指定用户编号的 LoginUser
*
* @param request 请求
* @param userId 用户编号
* @return 登录用户
*/
public LoginUser mockLogin(HttpServletRequest request, Long userId) {
return selectService(request).mockLogin(userId);
}
/**
* 基于 token 退出登录
*
* @param request 请求
* @param token token
*/
public void logout(HttpServletRequest request, String token) {
selectService(request).logout(token);
}
private SecurityAuthFrameworkService selectService(HttpServletRequest request) {
// 第一步获得用户类型
UserTypeEnum userType = getUserType(request);
// 第二步获得 SecurityAuthFrameworkService
SecurityAuthFrameworkService service = services.get(userType);
Assert.notNull(service, "URI({}) 用户类型({}) 找不到 SecurityAuthFrameworkService 实现类",
request.getRequestURI(), userType);
return service;
}
private UserTypeEnum getUserType(HttpServletRequest request) {
if (request.getRequestURI().startsWith(properties.getAdminApi().getPrefix())) {
return UserTypeEnum.ADMIN;
}
if (request.getRequestURI().startsWith(properties.getAppApi().getPrefix())) {
return UserTypeEnum.MEMBER;
}
throw new IllegalArgumentException(StrUtil.format("URI({}) 找不到匹配的用户类型", request.getRequestURI()));
}
}

View File

@ -0,0 +1,43 @@
package cn.iocoder.yudao.framework.security.core.authentication;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import lombok.Getter;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
/**
* 支持多用户的 UsernamePasswordAuthenticationToken 实现类
*
* @author 芋道源码
*/
@Getter
public class MultiUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken {
/**
* 用户类型
*/
private UserTypeEnum userType;
public MultiUsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(principal, credentials);
}
public MultiUsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
}
public MultiUsernamePasswordAuthenticationToken(Object principal, Object credentials, UserTypeEnum userType) {
super(principal, credentials);
this.userType = userType;
}
public MultiUsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities, UserTypeEnum userType) {
super(principal, credentials, authorities);
this.userType = userType;
}
}

View File

@ -5,10 +5,10 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.security.config.SecurityProperties; import cn.iocoder.yudao.framework.security.config.SecurityProperties;
import cn.iocoder.yudao.framework.security.core.LoginUser; import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.security.core.service.SecurityAuthService; import cn.iocoder.yudao.framework.security.core.authentication.MultiUserDetailsAuthenticationProvider;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
import lombok.AllArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain; import javax.servlet.FilterChain;
@ -23,12 +23,12 @@ import java.io.IOException;
* *
* @author 芋道源码 * @author 芋道源码
*/ */
@AllArgsConstructor @RequiredArgsConstructor
public class JWTAuthenticationTokenFilter extends OncePerRequestFilter { public class JWTAuthenticationTokenFilter extends OncePerRequestFilter {
private final SecurityProperties securityProperties; private final SecurityProperties securityProperties;
private final SecurityAuthService authService; private final MultiUserDetailsAuthenticationProvider authenticationProvider;
private final GlobalExceptionHandler globalExceptionHandler; private final GlobalExceptionHandler globalExceptionHandler;
@ -40,7 +40,7 @@ public class JWTAuthenticationTokenFilter extends OncePerRequestFilter {
if (StrUtil.isNotEmpty(token)) { if (StrUtil.isNotEmpty(token)) {
try { try {
// 验证 token 有效性 // 验证 token 有效性
LoginUser loginUser = authService.verifyTokenAndRefresh(request, token); LoginUser loginUser = authenticationProvider.verifyTokenAndRefresh(request, token);
// 模拟 Login 功能方便日常开发调试 // 模拟 Login 功能方便日常开发调试
if (loginUser == null) { if (loginUser == null) {
loginUser = this.mockLoginUser(request, token); loginUser = this.mockLoginUser(request, token);
@ -78,7 +78,7 @@ public class JWTAuthenticationTokenFilter extends OncePerRequestFilter {
return null; return null;
} }
Long userId = Long.valueOf(token.substring(securityProperties.getMockSecret().length())); Long userId = Long.valueOf(token.substring(securityProperties.getMockSecret().length()));
return authService.mockLogin(request, userId); return authenticationProvider.mockLogin(request, userId);
} }
} }

View File

@ -4,7 +4,7 @@ import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.security.config.SecurityProperties; import cn.iocoder.yudao.framework.security.config.SecurityProperties;
import cn.iocoder.yudao.framework.security.core.service.SecurityAuthService; import cn.iocoder.yudao.framework.security.core.authentication.MultiUserDetailsAuthenticationProvider;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@ -24,14 +24,14 @@ public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
private final SecurityProperties securityProperties; private final SecurityProperties securityProperties;
private final SecurityAuthService authService; private final MultiUserDetailsAuthenticationProvider authenticationProvider;
@Override @Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
// 执行退出 // 执行退出
String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader()); String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader());
if (StrUtil.isNotBlank(token)) { if (StrUtil.isNotBlank(token)) {
authService.logout(request, token); authenticationProvider.logout(request, token);
} }
// 返回成功 // 返回成功
ServletUtils.writeJSON(response, CommonResult.success(null)); ServletUtils.writeJSON(response, CommonResult.success(null));

View File

@ -1,43 +0,0 @@
package cn.iocoder.yudao.framework.security.core.service;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import javax.servlet.http.HttpServletRequest;
/**
* 安全认证的 Service 接口 security 组件提供统一的 Auth 相关的方法
* 主要是会基于 {@link HttpServletRequest} 参数匹配对应的 {@link SecurityAuthFrameworkService} 实现然后调用其方法
* 因此在方法的定义上 {@link SecurityAuthFrameworkService} 差不多
*
* @author 芋道源码
*/
public interface SecurityAuthService {
/**
* 校验 token 的有效性并获取用户信息
* 通过后刷新 token 的过期时间
*
* @param request 请求
* @param token token
* @return 用户信息
*/
LoginUser verifyTokenAndRefresh(HttpServletRequest request, String token);
/**
* 模拟指定用户编号的 LoginUser
*
* @param request 请求
* @param userId 用户编号
* @return 登录用户
*/
LoginUser mockLogin(HttpServletRequest request, Long userId);
/**
* 基于 token 退出登录
*
* @param request 请求
* @param token token
*/
void logout(HttpServletRequest request, String token);
}

View File

@ -1,64 +0,0 @@
package cn.iocoder.yudao.framework.security.core.service;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.web.config.WebProperties;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 安全认证的 Service 实现类基于请求地址计算到对应的 {@link UserTypeEnum} 枚举从而拿到对应的 {@link SecurityAuthFrameworkService} 实现
*
* @author 芋道源码
*/
public class SecurityAuthServiceImpl implements SecurityAuthService {
private final Map<UserTypeEnum, SecurityAuthFrameworkService> services = new HashMap<>();
private final WebProperties properties;
public SecurityAuthServiceImpl(List<SecurityAuthFrameworkService> serviceList, WebProperties properties) {
serviceList.forEach(service -> services.put(service.getUserType(), service));
this.properties = properties;
}
@Override
public LoginUser verifyTokenAndRefresh(HttpServletRequest request, String token) {
return selectService(request).verifyTokenAndRefresh(token);
}
@Override
public LoginUser mockLogin(HttpServletRequest request, Long userId) {
return selectService(request).mockLogin(userId);
}
@Override
public void logout(HttpServletRequest request, String token) {
selectService(request).logout(token);
}
private SecurityAuthFrameworkService selectService(HttpServletRequest request) {
// 第一步获得用户类型
UserTypeEnum userType = getUserType(request);
// 第二步获得 SecurityAuthFrameworkService
SecurityAuthFrameworkService service = services.get(userType);
Assert.notNull(service, "URI({}) 用户类型({}) 找不到 SecurityAuthFrameworkService 实现类",
request.getRequestURI(), userType);
return service;
}
private UserTypeEnum getUserType(HttpServletRequest request) {
if (request.getRequestURI().startsWith(properties.getAdminApi().getPrefix())) {
return UserTypeEnum.ADMIN;
}
if (request.getRequestURI().startsWith(properties.getAppApi().getPrefix())) {
return UserTypeEnum.MEMBER;
}
throw new IllegalArgumentException(StrUtil.format("URI({}) 找不到匹配的用户类型", request.getRequestURI()));
}
}

View File

@ -55,6 +55,11 @@ public class WebFrameworkUtils {
return (Integer) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE); return (Integer) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE);
} }
public static Integer getLoginUserType() {
HttpServletRequest request = getRequest();
return getLoginUserType(request);
}
public static Long getLoginUserId() { public static Long getLoginUserId() {
HttpServletRequest request = getRequest(); HttpServletRequest request = getRequest();
return getLoginUserId(request); return getLoginUserId(request);

View File

@ -30,6 +30,10 @@
<artifactId>yudao-core-service</artifactId> <artifactId>yudao-core-service</artifactId>
</dependency> </dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId>
</dependency>
<dependency> <dependency>
<groupId>cn.iocoder.boot</groupId> <groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-sms</artifactId> <artifactId>yudao-spring-boot-starter-biz-sms</artifactId>

View File

@ -1,6 +1,7 @@
### 请求 /login 接口 => 成功 ### 请求 /login 接口 => 成功
POST {{userServerUrl}}/login POST {{userApi}}/login
Content-Type: application/json Content-Type: application/json
tenant-id: {{userTenentId}}
{ {
"mobile": "15601691300", "mobile": "15601691300",

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.member.controller.app.auth;
import cn.iocoder.yudao.coreservice.modules.system.service.social.SysSocialCoreService; import cn.iocoder.yudao.coreservice.modules.system.service.social.SysSocialCoreService;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated; import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
import cn.iocoder.yudao.module.member.controller.app.auth.vo.*; import cn.iocoder.yudao.module.member.controller.app.auth.vo.*;
import cn.iocoder.yudao.module.member.service.auth.AuthService; import cn.iocoder.yudao.module.member.service.auth.AuthService;
@ -40,6 +41,7 @@ public class AppAuthController {
@PostMapping("/login") @PostMapping("/login")
@ApiOperation("使用手机 + 密码登录") @ApiOperation("使用手机 + 密码登录")
@OperateLog(enable = false) // 避免 Post 请求被记录操作日志
public CommonResult<AppAuthLoginRespVO> login(@RequestBody @Valid AppAuthLoginReqVO reqVO) { public CommonResult<AppAuthLoginRespVO> login(@RequestBody @Valid AppAuthLoginReqVO reqVO) {
String token = authService.login(reqVO, getClientIP(), getUserAgent()); String token = authService.login(reqVO, getClientIP(), getUserAgent());
// 返回结果 // 返回结果
@ -48,6 +50,7 @@ public class AppAuthController {
@PostMapping("/sms-login") @PostMapping("/sms-login")
@ApiOperation("使用手机 + 验证码登录") @ApiOperation("使用手机 + 验证码登录")
@OperateLog(enable = false) // 避免 Post 请求被记录操作日志
public CommonResult<AppAuthLoginRespVO> smsLogin(@RequestBody @Valid AppAuthSmsLoginReqVO reqVO) { public CommonResult<AppAuthLoginRespVO> smsLogin(@RequestBody @Valid AppAuthSmsLoginReqVO reqVO) {
String token = authService.smsLogin(reqVO, getClientIP(), getUserAgent()); String token = authService.smsLogin(reqVO, getClientIP(), getUserAgent());
// 返回结果 // 返回结果
@ -56,12 +59,13 @@ public class AppAuthController {
@PostMapping("/send-sms-code") @PostMapping("/send-sms-code")
@ApiOperation(value = "发送手机验证码") @ApiOperation(value = "发送手机验证码")
@OperateLog(enable = false) // 避免 Post 请求被记录操作日志
public CommonResult<Boolean> sendSmsCode(@RequestBody @Valid AppAuthSendSmsReqVO reqVO) { public CommonResult<Boolean> sendSmsCode(@RequestBody @Valid AppAuthSendSmsReqVO reqVO) {
smsCodeService.sendSmsCode(reqVO.getMobile(), reqVO.getScene(), getClientIP()); smsCodeService.sendSmsCode(reqVO.getMobile(), reqVO.getScene(), getClientIP());
return success(true); return success(true);
} }
@GetMapping("/send-sms-code-login") @GetMapping("/send-sms-code-login") // TODO 芋艿post 比较合理
@ApiOperation(value = "向已登录用户发送验证码",notes = "修改手机时验证原手机号使用") @ApiOperation(value = "向已登录用户发送验证码",notes = "修改手机时验证原手机号使用")
public CommonResult<Boolean> sendSmsCodeLogin() { public CommonResult<Boolean> sendSmsCodeLogin() {
smsCodeService.sendSmsCodeLogin(getLoginUserId()); smsCodeService.sendSmsCodeLogin(getLoginUserId());
@ -71,6 +75,7 @@ public class AppAuthController {
@PostMapping("/reset-password") @PostMapping("/reset-password")
@ApiOperation(value = "重置密码", notes = "用户忘记密码时使用") @ApiOperation(value = "重置密码", notes = "用户忘记密码时使用")
@PreAuthenticated @PreAuthenticated
@OperateLog(enable = false) // 避免 Post 请求被记录操作日志
public CommonResult<Boolean> resetPassword(@RequestBody @Valid AppAuthResetPasswordReqVO reqVO) { public CommonResult<Boolean> resetPassword(@RequestBody @Valid AppAuthResetPasswordReqVO reqVO) {
authService.resetPassword(reqVO); authService.resetPassword(reqVO);
return success(true); return success(true);
@ -106,6 +111,7 @@ public class AppAuthController {
@PostMapping("/social-login2") @PostMapping("/social-login2")
@ApiOperation("社交登录,使用 手机号 + 手机验证码") @ApiOperation("社交登录,使用 手机号 + 手机验证码")
@OperateLog(enable = false) // 避免 Post 请求被记录操作日志
public CommonResult<AppAuthLoginRespVO> socialLogin2(@RequestBody @Valid AppAuthSocialLogin2ReqVO reqVO) { public CommonResult<AppAuthLoginRespVO> socialLogin2(@RequestBody @Valid AppAuthSocialLogin2ReqVO reqVO) {
String token = authService.socialLogin2(reqVO, getClientIP(), getUserAgent()); String token = authService.socialLogin2(reqVO, getClientIP(), getUserAgent());
return success(AppAuthLoginRespVO.builder().token(token).build()); return success(AppAuthLoginRespVO.builder().token(token).build());
@ -113,6 +119,7 @@ public class AppAuthController {
@PostMapping("/social-bind") @PostMapping("/social-bind")
@ApiOperation("社交绑定,使用 code 授权码") @ApiOperation("社交绑定,使用 code 授权码")
@PreAuthenticated
public CommonResult<Boolean> socialBind(@RequestBody @Valid AppAuthSocialBindReqVO reqVO) { public CommonResult<Boolean> socialBind(@RequestBody @Valid AppAuthSocialBindReqVO reqVO) {
authService.socialBind(getLoginUserId(), reqVO); authService.socialBind(getLoginUserId(), reqVO);
return CommonResult.success(true); return CommonResult.success(true);
@ -120,6 +127,7 @@ public class AppAuthController {
@DeleteMapping("/social-unbind") @DeleteMapping("/social-unbind")
@ApiOperation("取消社交绑定") @ApiOperation("取消社交绑定")
@PreAuthenticated
public CommonResult<Boolean> socialUnbind(@RequestBody AppAuthSocialUnbindReqVO reqVO) { public CommonResult<Boolean> socialUnbind(@RequestBody AppAuthSocialUnbindReqVO reqVO) {
socialService.unbindSocialUser(getLoginUserId(), reqVO.getType(), reqVO.getUnionId(), UserTypeEnum.MEMBER); socialService.unbindSocialUser(getLoginUserId(), reqVO.getType(), reqVO.getUnionId(), UserTypeEnum.MEMBER);
return CommonResult.success(true); return CommonResult.success(true);

View File

@ -14,6 +14,7 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.security.core.LoginUser; import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.security.core.authentication.MultiUsernamePasswordAuthenticationToken;
import cn.iocoder.yudao.module.member.controller.app.auth.vo.*; import cn.iocoder.yudao.module.member.controller.app.auth.vo.*;
import cn.iocoder.yudao.module.member.convert.auth.AuthConvert; import cn.iocoder.yudao.module.member.convert.auth.AuthConvert;
import cn.iocoder.yudao.module.member.dal.dataobject.user.UserDO; import cn.iocoder.yudao.module.member.dal.dataobject.user.UserDO;
@ -176,7 +177,8 @@ public class AuthServiceImpl implements AuthService {
try { try {
// 调用 Spring Security AuthenticationManager#authenticate(...) 方法使用账号密码进行认证 // 调用 Spring Security AuthenticationManager#authenticate(...) 方法使用账号密码进行认证
// 在其内部会调用到 loadUserByUsername 方法获取 User 信息 // 在其内部会调用到 loadUserByUsername 方法获取 User 信息
authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); authentication = authenticationManager.authenticate(new MultiUsernamePasswordAuthenticationToken(
username, password, getUserType()));
} catch (BadCredentialsException badCredentialsException) { } catch (BadCredentialsException badCredentialsException) {
this.createLoginLog(username, logTypeEnum, SysLoginResultEnum.BAD_CREDENTIALS); this.createLoginLog(username, logTypeEnum, SysLoginResultEnum.BAD_CREDENTIALS);
throw exception(AUTH_LOGIN_BAD_CREDENTIALS); throw exception(AUTH_LOGIN_BAD_CREDENTIALS);

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>yudao</artifactId>
<groupId>cn.iocoder.boot</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yudao-server</artifactId>
<packaging>jar</packaging>
<name>${artifactId}</name>
<description>
后端 Server 的主项目,通过引入需要 yudao-module-xxx 的依赖,
从而实现提供 RESTful API 给 yudao-ui-admin、yudao-ui-user 等前端项目。
本质上来说,它就是个空壳(容器)!
</description>
</project>

View File

@ -4,29 +4,30 @@
* 钉钉、飞书等通知 * 钉钉、飞书等通知
* Vue3 支持 * Vue3 支持
## [v1.4.0] 计划
* 工作流
* 修改表单为外置表单
* 修改请假流程
* 暂时以用户的岗位作为activiti 的用户组
* 请假需要请假人部门下具有项目经理岗位, 部门经理, 和人事 岗位的用户
* 新增 芋道源码部门下 用户 normal(岗位 普通用户) projectmgr(岗位 项目经理) depmgr岗位 部门经理) hradmin (岗位 人事)
* 请假流程如下
1. 请假人 normal (密码 123456) 登录在我的请假表单,点击新增,填写请假表单
2. 如果请假天数<=3, 项目经理 进行审批. 项目经理 projectmgr(密码123456) 登录 待办请假,中进行审批,可以查看历史跟踪,和流程图
3. 如果请假天数>3 需部门经理 进行审批部门经理depmgr(密码123456) 登录 待办请假,中进行审批,可以查看历史跟踪,和流程图
4. 人事登陆用户名hradmin 密码:123456) 登录 待办请假, 中进行审批,可以查看历史跟踪,和流程图
5. 流程结束
* 我的请假中,可以查询本人的请假申请, 和进度
### 📝 TODO ### 📝 TODO
* 支付 * 支付
* 用户前台的社交登陆 * 用户前台的社交登陆
* 用户前台的修改手机、修改密码、忘记密码 * 用户前台的修改手机、修改密码、忘记密码
## [v1.3.0] 进行中 ## [v1.4.0] 计划,预计 2022.02.28 发布
### ⚠️ Warning
### 📈 Statistic
### ⭐ New Features
*【优化】操作日志新增用户类型,实现 APP 端的 API 的操作日志的记录
### 🐞 Bug Fixes
*【修复】用户无权限访问 指定 API 时,未返回 FORBIDDEN 结果码
### 🔨 Dependency Upgrades
## [v1.3.0] 2022.01.24
### ⚠️ Warning ### ⚠️ Warning