Commit e9efff7076f66305b5bc80ee416a32ee7487b6dc
1 parent
928b7dbe
多模块重构 3:security 实现多用户的认证支持
Showing
23 changed files
with
279 additions
and
184 deletions
http-client.env.json
| ... | ... | @@ -2,6 +2,10 @@ |
| 2 | 2 | "local": { |
| 3 | 3 | "baseUrl": "http://127.0.0.1:48080/api", |
| 4 | 4 | "userServerUrl": "http://127.0.0.1:28080/api", |
| 5 | - "token": "test1" | |
| 5 | + "token": "test1", | |
| 6 | + | |
| 7 | + "userApi": "http://127.0.0.1:48080/app-api", | |
| 8 | + "userToken": "test1", | |
| 9 | + "userTenentId": "1" | |
| 6 | 10 | } |
| 7 | 11 | } | ... | ... |
pom.xml
yudao-admin-server/pom.xml
| ... | ... | @@ -13,7 +13,11 @@ |
| 13 | 13 | <packaging>jar</packaging> |
| 14 | 14 | |
| 15 | 15 | <name>yudao-admin-server</name> |
| 16 | - <description>管理后台 Server,提供其 API 接口</description> | |
| 16 | + <description> | |
| 17 | + 后端 Server 的主项目,通过引入需要 yudao-module-xxx 的依赖, | |
| 18 | + 从而实现提供 RESTful API 给 yudao-ui-admin、yudao-ui-user 等前端项目。 | |
| 19 | + 本质上来说,它就是个空壳(容器)! | |
| 20 | + </description> | |
| 17 | 21 | <url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
| 18 | 22 | |
| 19 | 23 | <dependencies> | ... | ... |
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/framework/security/SecurityConfiguration.java
| ... | ... | @@ -33,7 +33,7 @@ public class SecurityConfiguration { |
| 33 | 33 | registry.antMatchers(buildAdminApi("/system/sms/callback/**")).anonymous(); |
| 34 | 34 | |
| 35 | 35 | // 设置 App API 无需认证 |
| 36 | - registry.antMatchers(buildAppApi("/**")); | |
| 36 | + registry.antMatchers(buildAppApi("/**")).permitAll(); | |
| 37 | 37 | }; |
| 38 | 38 | } |
| 39 | 39 | ... | ... |
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/auth/impl/SysAuthServiceImpl.java
| ... | ... | @@ -26,6 +26,7 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; |
| 26 | 26 | import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; |
| 27 | 27 | import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; |
| 28 | 28 | import cn.iocoder.yudao.framework.security.core.LoginUser; |
| 29 | +import cn.iocoder.yudao.framework.security.core.authentication.MultiUsernamePasswordAuthenticationToken; | |
| 29 | 30 | import lombok.extern.slf4j.Slf4j; |
| 30 | 31 | import me.zhyd.oauth.model.AuthUser; |
| 31 | 32 | import org.springframework.context.annotation.Lazy; |
| ... | ... | @@ -154,7 +155,8 @@ public class SysAuthServiceImpl implements SysAuthService { |
| 154 | 155 | try { |
| 155 | 156 | // 调用 Spring Security 的 AuthenticationManager#authenticate(...) 方法,使用账号密码进行认证 |
| 156 | 157 | // 在其内部,会调用到 loadUserByUsername 方法,获取 User 信息 |
| 157 | - authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); | |
| 158 | + authentication = authenticationManager.authenticate(new MultiUsernamePasswordAuthenticationToken( | |
| 159 | + username, password, getUserType())); | |
| 158 | 160 | // org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(username); |
| 159 | 161 | } catch (BadCredentialsException badCredentialsException) { |
| 160 | 162 | this.createLoginLog(username, logTypeEnum, SysLoginResultEnum.BAD_CREDENTIALS); | ... | ... |
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/UserTypeEnum.java
| 1 | 1 | package cn.iocoder.yudao.framework.common.enums; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.lang.Matcher; | |
| 4 | +import cn.hutool.core.util.ArrayUtil; | |
| 3 | 5 | import lombok.AllArgsConstructor; |
| 4 | 6 | import lombok.Getter; |
| 5 | 7 | |
| ... | ... | @@ -22,4 +24,8 @@ public enum UserTypeEnum { |
| 22 | 24 | */ |
| 23 | 25 | private final String name; |
| 24 | 26 | |
| 27 | + public static UserTypeEnum valueOf(Integer value) { | |
| 28 | + return ArrayUtil.firstMatch(userType -> userType.getValue().equals(value), UserTypeEnum.values()); | |
| 29 | + } | |
| 30 | + | |
| 25 | 31 | } | ... | ... |
yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/aop/OperateLogAspect.java
| ... | ... | @@ -149,6 +149,7 @@ public class OperateLogAspect { |
| 149 | 149 | |
| 150 | 150 | private static void fillUserFields(OperateLogCreateReqDTO operateLogDTO) { |
| 151 | 151 | operateLogDTO.setUserId(WebFrameworkUtils.getLoginUserId()); |
| 152 | + operateLogDTO.setUserType(WebFrameworkUtils.getLoginUserType()); | |
| 152 | 153 | } |
| 153 | 154 | |
| 154 | 155 | private static void fillModuleFields(OperateLogCreateReqDTO operateLogDTO, | ... | ... |
yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/dto/OperateLogCreateReqDTO.java
| ... | ... | @@ -21,6 +21,9 @@ public class OperateLogCreateReqDTO { |
| 21 | 21 | @ApiModelProperty(value = "用户编号", required = true, example = "1024") |
| 22 | 22 | @NotNull(message = "用户编号不能为空") |
| 23 | 23 | private Long userId; |
| 24 | + @ApiModelProperty(value = "用户类型", required = true, example = "1") | |
| 25 | + @NotNull(message = "用户类型不能为空") | |
| 26 | + private Integer userType; | |
| 24 | 27 | |
| 25 | 28 | @ApiModelProperty(value = "操作模块", required = true, example = "订单") |
| 26 | 29 | @NotEmpty(message = "操作模块不能为空") | ... | ... |
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java
| 1 | 1 | package cn.iocoder.yudao.framework.security.config; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.security.core.aop.PreAuthenticatedAspect; |
| 4 | +import cn.iocoder.yudao.framework.security.core.authentication.MultiUserDetailsAuthenticationProvider; | |
| 4 | 5 | import cn.iocoder.yudao.framework.security.core.context.TransmittableThreadLocalSecurityContextHolderStrategy; |
| 5 | 6 | import cn.iocoder.yudao.framework.security.core.filter.JWTAuthenticationTokenFilter; |
| 6 | 7 | import cn.iocoder.yudao.framework.security.core.handler.AccessDeniedHandlerImpl; |
| 7 | 8 | import cn.iocoder.yudao.framework.security.core.handler.AuthenticationEntryPointImpl; |
| 8 | 9 | import cn.iocoder.yudao.framework.security.core.handler.LogoutSuccessHandlerImpl; |
| 9 | 10 | import cn.iocoder.yudao.framework.security.core.service.SecurityAuthFrameworkService; |
| 10 | -import cn.iocoder.yudao.framework.security.core.service.SecurityAuthService; | |
| 11 | -import cn.iocoder.yudao.framework.security.core.service.SecurityAuthServiceImpl; | |
| 12 | 11 | import cn.iocoder.yudao.framework.web.config.WebProperties; |
| 13 | 12 | import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; |
| 14 | 13 | import org.springframework.beans.factory.config.MethodInvokingFactoryBean; |
| ... | ... | @@ -68,8 +67,8 @@ public class YudaoSecurityAutoConfiguration { |
| 68 | 67 | * 退出处理类 Bean |
| 69 | 68 | */ |
| 70 | 69 | @Bean |
| 71 | - public LogoutSuccessHandler logoutSuccessHandler(SecurityAuthService securityAuthService) { | |
| 72 | - return new LogoutSuccessHandlerImpl(securityProperties, securityAuthService); | |
| 70 | + public LogoutSuccessHandler logoutSuccessHandler(MultiUserDetailsAuthenticationProvider authenticationProvider) { | |
| 71 | + return new LogoutSuccessHandlerImpl(securityProperties, authenticationProvider); | |
| 73 | 72 | } |
| 74 | 73 | |
| 75 | 74 | /** |
| ... | ... | @@ -87,18 +86,19 @@ public class YudaoSecurityAutoConfiguration { |
| 87 | 86 | * Token 认证过滤器 Bean |
| 88 | 87 | */ |
| 89 | 88 | @Bean |
| 90 | - public JWTAuthenticationTokenFilter authenticationTokenFilter(SecurityAuthService securityAuthService, | |
| 89 | + public JWTAuthenticationTokenFilter authenticationTokenFilter(MultiUserDetailsAuthenticationProvider authenticationProvider, | |
| 91 | 90 | GlobalExceptionHandler globalExceptionHandler) { |
| 92 | - return new JWTAuthenticationTokenFilter(securityProperties, securityAuthService, globalExceptionHandler); | |
| 91 | + return new JWTAuthenticationTokenFilter(securityProperties, authenticationProvider, globalExceptionHandler); | |
| 93 | 92 | } |
| 94 | 93 | |
| 95 | 94 | /** |
| 96 | - * 安全认证的 Service Bean | |
| 95 | + * 身份验证的 Provider Bean,通过它实现账号 + 密码的认证 | |
| 97 | 96 | */ |
| 98 | 97 | @Bean |
| 99 | - public SecurityAuthService securityAuthService(List<SecurityAuthFrameworkService> securityFrameworkServices, | |
| 100 | - WebProperties webProperties) { | |
| 101 | - return new SecurityAuthServiceImpl(securityFrameworkServices, webProperties); | |
| 98 | + public MultiUserDetailsAuthenticationProvider authenticationProvider( | |
| 99 | + List<SecurityAuthFrameworkService> securityFrameworkServices, | |
| 100 | + WebProperties webProperties, PasswordEncoder passwordEncoder) { | |
| 101 | + return new MultiUserDetailsAuthenticationProvider(securityFrameworkServices, webProperties, passwordEncoder); | |
| 102 | 102 | } |
| 103 | 103 | |
| 104 | 104 | /** | ... | ... |
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java
| 1 | 1 | package cn.iocoder.yudao.framework.security.config; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.framework.security.core.authentication.MultiUserDetailsAuthenticationProvider; | |
| 3 | 4 | import cn.iocoder.yudao.framework.security.core.filter.JWTAuthenticationTokenFilter; |
| 4 | 5 | import cn.iocoder.yudao.framework.security.core.service.SecurityAuthFrameworkService; |
| 5 | 6 | import cn.iocoder.yudao.framework.web.config.WebProperties; |
| ... | ... | @@ -35,16 +36,8 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap |
| 35 | 36 | @Resource |
| 36 | 37 | private WebProperties webProperties; |
| 37 | 38 | |
| 38 | - /** | |
| 39 | - * 自定义用户【认证】逻辑 | |
| 40 | - */ | |
| 41 | - @Resource | |
| 42 | - private SecurityAuthFrameworkService userDetailsService; | |
| 43 | - /** | |
| 44 | - * Spring Security 加密器 | |
| 45 | - */ | |
| 46 | 39 | @Resource |
| 47 | - private PasswordEncoder passwordEncoder; | |
| 40 | + private MultiUserDetailsAuthenticationProvider authenticationProvider; | |
| 48 | 41 | /** |
| 49 | 42 | * 认证失败处理类 Bean |
| 50 | 43 | */ |
| ... | ... | @@ -91,8 +84,7 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap |
| 91 | 84 | */ |
| 92 | 85 | @Override |
| 93 | 86 | protected void configure(AuthenticationManagerBuilder auth) throws Exception { |
| 94 | - auth | |
| 95 | - .userDetailsService(userDetailsService).passwordEncoder(passwordEncoder); | |
| 87 | + auth.authenticationProvider(authenticationProvider); | |
| 96 | 88 | } |
| 97 | 89 | |
| 98 | 90 | /** | ... | ... |
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/authentication/MultiUserDetailsAuthenticationProvider.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.security.core.authentication; | |
| 2 | + | |
| 3 | +import cn.hutool.core.lang.Assert; | |
| 4 | +import cn.hutool.core.util.StrUtil; | |
| 5 | +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; | |
| 6 | +import cn.iocoder.yudao.framework.security.core.LoginUser; | |
| 7 | +import cn.iocoder.yudao.framework.security.core.service.SecurityAuthFrameworkService; | |
| 8 | +import cn.iocoder.yudao.framework.web.config.WebProperties; | |
| 9 | +import org.springframework.security.authentication.AuthenticationProvider; | |
| 10 | +import org.springframework.security.authentication.BadCredentialsException; | |
| 11 | +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | |
| 12 | +import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider; | |
| 13 | +import org.springframework.security.core.AuthenticationException; | |
| 14 | +import org.springframework.security.core.userdetails.UserDetails; | |
| 15 | +import org.springframework.security.crypto.password.PasswordEncoder; | |
| 16 | + | |
| 17 | +import javax.servlet.http.HttpServletRequest; | |
| 18 | +import java.util.HashMap; | |
| 19 | +import java.util.List; | |
| 20 | +import java.util.Map; | |
| 21 | + | |
| 22 | +/** | |
| 23 | + * 支持多用户类型的 AuthenticationProvider 实现类 | |
| 24 | + * | |
| 25 | + * 为什么不用 {@link org.springframework.security.authentication.ProviderManager} 呢? | |
| 26 | + * 原因是,需要每个用户类型实现对应的 {@link AuthenticationProvider} + authentication,略显麻烦。实际,也是可以实现的。 | |
| 27 | + * | |
| 28 | + * 另外,额外支持 verifyTokenAndRefresh 校验令牌、logout 登出、mockLogin 模拟登陆等操作。 | |
| 29 | + * 实际上,它就是 {@link SecurityAuthFrameworkService} 定义的三个接口。 | |
| 30 | + * 因为需要支持多种类型,所以需要根据请求的 URL,判断出对应的用户类型,从而使用对应的 SecurityAuthFrameworkService 是吸纳 | |
| 31 | + * | |
| 32 | + * @see cn.iocoder.yudao.framework.common.enums.UserTypeEnum | |
| 33 | + * @author 芋道源码 | |
| 34 | + */ | |
| 35 | +public class MultiUserDetailsAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { | |
| 36 | + | |
| 37 | + private final Map<UserTypeEnum, SecurityAuthFrameworkService> services = new HashMap<>(); | |
| 38 | + | |
| 39 | + private final WebProperties properties; | |
| 40 | + | |
| 41 | + private final PasswordEncoder passwordEncoder; | |
| 42 | + | |
| 43 | + public MultiUserDetailsAuthenticationProvider(List<SecurityAuthFrameworkService> serviceList, | |
| 44 | + WebProperties properties, PasswordEncoder passwordEncoder) { | |
| 45 | + serviceList.forEach(service -> services.put(service.getUserType(), service)); | |
| 46 | + this.properties = properties; | |
| 47 | + this.passwordEncoder = passwordEncoder; | |
| 48 | + } | |
| 49 | + | |
| 50 | + // ========== AuthenticationProvider 相关 ========== | |
| 51 | + | |
| 52 | + @Override | |
| 53 | + protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) | |
| 54 | + throws AuthenticationException { | |
| 55 | + // 执行用户的加载 | |
| 56 | + return selectService(authentication).loadUserByUsername(username); | |
| 57 | + } | |
| 58 | + | |
| 59 | + private SecurityAuthFrameworkService selectService(UsernamePasswordAuthenticationToken authentication) { | |
| 60 | + // 第一步,获得用户类型 | |
| 61 | + UserTypeEnum userType = getUserType(authentication); | |
| 62 | + // 第二步,获得 SecurityAuthFrameworkService | |
| 63 | + SecurityAuthFrameworkService service = services.get(userType); | |
| 64 | + Assert.notNull(service, "用户类型({}) 找不到 SecurityAuthFrameworkService 实现类", userType); | |
| 65 | + return service; | |
| 66 | + } | |
| 67 | + | |
| 68 | + private UserTypeEnum getUserType(UsernamePasswordAuthenticationToken authentication) { | |
| 69 | + Assert.isInstanceOf(MultiUsernamePasswordAuthenticationToken.class, authentication); | |
| 70 | + MultiUsernamePasswordAuthenticationToken multiAuthentication = (MultiUsernamePasswordAuthenticationToken) authentication; | |
| 71 | + UserTypeEnum userType = multiAuthentication.getUserType(); | |
| 72 | + Assert.notNull(userType, "用户类型不能为空"); | |
| 73 | + return userType; | |
| 74 | + } | |
| 75 | + | |
| 76 | + @Override // copy 自 DaoAuthenticationProvider 的 additionalAuthenticationChecks 方法 | |
| 77 | + protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) | |
| 78 | + throws AuthenticationException { | |
| 79 | + // 校验 credentials | |
| 80 | + if (authentication.getCredentials() == null) { | |
| 81 | + this.logger.debug("Failed to authenticate since no credentials provided"); | |
| 82 | + throw new BadCredentialsException(this.messages.getMessage( | |
| 83 | + "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); | |
| 84 | + } | |
| 85 | + // 校验 password | |
| 86 | + String presentedPassword = authentication.getCredentials().toString(); | |
| 87 | + if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { | |
| 88 | + this.logger.debug("Failed to authenticate since password does not match stored value"); | |
| 89 | + throw new BadCredentialsException(this.messages.getMessage( | |
| 90 | + "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); | |
| 91 | + } | |
| 92 | + } | |
| 93 | + | |
| 94 | + // ========== SecurityAuthFrameworkService 相关 ========== | |
| 95 | + | |
| 96 | + /** | |
| 97 | + * 校验 token 的有效性,并获取用户信息 | |
| 98 | + * 通过后,刷新 token 的过期时间 | |
| 99 | + * | |
| 100 | + * @param request 请求 | |
| 101 | + * @param token token | |
| 102 | + * @return 用户信息 | |
| 103 | + */ | |
| 104 | + public LoginUser verifyTokenAndRefresh(HttpServletRequest request, String token) { | |
| 105 | + return selectService(request).verifyTokenAndRefresh(token); | |
| 106 | + } | |
| 107 | + | |
| 108 | + /** | |
| 109 | + * 模拟指定用户编号的 LoginUser | |
| 110 | + * | |
| 111 | + * @param request 请求 | |
| 112 | + * @param userId 用户编号 | |
| 113 | + * @return 登录用户 | |
| 114 | + */ | |
| 115 | + public LoginUser mockLogin(HttpServletRequest request, Long userId) { | |
| 116 | + return selectService(request).mockLogin(userId); | |
| 117 | + } | |
| 118 | + | |
| 119 | + /** | |
| 120 | + * 基于 token 退出登录 | |
| 121 | + * | |
| 122 | + * @param request 请求 | |
| 123 | + * @param token token | |
| 124 | + */ | |
| 125 | + public void logout(HttpServletRequest request, String token) { | |
| 126 | + selectService(request).logout(token); | |
| 127 | + } | |
| 128 | + | |
| 129 | + private SecurityAuthFrameworkService selectService(HttpServletRequest request) { | |
| 130 | + // 第一步,获得用户类型 | |
| 131 | + UserTypeEnum userType = getUserType(request); | |
| 132 | + // 第二步,获得 SecurityAuthFrameworkService | |
| 133 | + SecurityAuthFrameworkService service = services.get(userType); | |
| 134 | + Assert.notNull(service, "URI({}) 用户类型({}) 找不到 SecurityAuthFrameworkService 实现类", | |
| 135 | + request.getRequestURI(), userType); | |
| 136 | + return service; | |
| 137 | + } | |
| 138 | + | |
| 139 | + private UserTypeEnum getUserType(HttpServletRequest request) { | |
| 140 | + if (request.getRequestURI().startsWith(properties.getAdminApi().getPrefix())) { | |
| 141 | + return UserTypeEnum.ADMIN; | |
| 142 | + } | |
| 143 | + if (request.getRequestURI().startsWith(properties.getAppApi().getPrefix())) { | |
| 144 | + return UserTypeEnum.MEMBER; | |
| 145 | + } | |
| 146 | + throw new IllegalArgumentException(StrUtil.format("URI({}) 找不到匹配的用户类型", request.getRequestURI())); | |
| 147 | + } | |
| 148 | + | |
| 149 | +} | ... | ... |
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/authentication/MultiUsernamePasswordAuthenticationToken.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.security.core.authentication; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; | |
| 4 | +import lombok.Getter; | |
| 5 | +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | |
| 6 | +import org.springframework.security.core.GrantedAuthority; | |
| 7 | + | |
| 8 | +import java.util.Collection; | |
| 9 | + | |
| 10 | +/** | |
| 11 | + * 支持多用户的 UsernamePasswordAuthenticationToken 实现类 | |
| 12 | + * | |
| 13 | + * @author 芋道源码 | |
| 14 | + */ | |
| 15 | +@Getter | |
| 16 | +public class MultiUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken { | |
| 17 | + | |
| 18 | + /** | |
| 19 | + * 用户类型 | |
| 20 | + */ | |
| 21 | + private UserTypeEnum userType; | |
| 22 | + | |
| 23 | + public MultiUsernamePasswordAuthenticationToken(Object principal, Object credentials) { | |
| 24 | + super(principal, credentials); | |
| 25 | + } | |
| 26 | + | |
| 27 | + public MultiUsernamePasswordAuthenticationToken(Object principal, Object credentials, | |
| 28 | + Collection<? extends GrantedAuthority> authorities) { | |
| 29 | + super(principal, credentials, authorities); | |
| 30 | + } | |
| 31 | + | |
| 32 | + public MultiUsernamePasswordAuthenticationToken(Object principal, Object credentials, UserTypeEnum userType) { | |
| 33 | + super(principal, credentials); | |
| 34 | + this.userType = userType; | |
| 35 | + } | |
| 36 | + | |
| 37 | + public MultiUsernamePasswordAuthenticationToken(Object principal, Object credentials, | |
| 38 | + Collection<? extends GrantedAuthority> authorities, UserTypeEnum userType) { | |
| 39 | + super(principal, credentials, authorities); | |
| 40 | + this.userType = userType; | |
| 41 | + } | |
| 42 | + | |
| 43 | +} | ... | ... |
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/JWTAuthenticationTokenFilter.java
| ... | ... | @@ -5,10 +5,10 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 5 | 5 | import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; |
| 6 | 6 | import cn.iocoder.yudao.framework.security.config.SecurityProperties; |
| 7 | 7 | import cn.iocoder.yudao.framework.security.core.LoginUser; |
| 8 | -import cn.iocoder.yudao.framework.security.core.service.SecurityAuthService; | |
| 8 | +import cn.iocoder.yudao.framework.security.core.authentication.MultiUserDetailsAuthenticationProvider; | |
| 9 | 9 | import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; |
| 10 | 10 | import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; |
| 11 | -import lombok.AllArgsConstructor; | |
| 11 | +import lombok.RequiredArgsConstructor; | |
| 12 | 12 | import org.springframework.web.filter.OncePerRequestFilter; |
| 13 | 13 | |
| 14 | 14 | import javax.servlet.FilterChain; |
| ... | ... | @@ -23,12 +23,12 @@ import java.io.IOException; |
| 23 | 23 | * |
| 24 | 24 | * @author 芋道源码 |
| 25 | 25 | */ |
| 26 | -@AllArgsConstructor | |
| 26 | +@RequiredArgsConstructor | |
| 27 | 27 | public class JWTAuthenticationTokenFilter extends OncePerRequestFilter { |
| 28 | 28 | |
| 29 | 29 | private final SecurityProperties securityProperties; |
| 30 | 30 | |
| 31 | - private final SecurityAuthService authService; | |
| 31 | + private final MultiUserDetailsAuthenticationProvider authenticationProvider; | |
| 32 | 32 | |
| 33 | 33 | private final GlobalExceptionHandler globalExceptionHandler; |
| 34 | 34 | |
| ... | ... | @@ -40,7 +40,7 @@ public class JWTAuthenticationTokenFilter extends OncePerRequestFilter { |
| 40 | 40 | if (StrUtil.isNotEmpty(token)) { |
| 41 | 41 | try { |
| 42 | 42 | // 验证 token 有效性 |
| 43 | - LoginUser loginUser = authService.verifyTokenAndRefresh(request, token); | |
| 43 | + LoginUser loginUser = authenticationProvider.verifyTokenAndRefresh(request, token); | |
| 44 | 44 | // 模拟 Login 功能,方便日常开发调试 |
| 45 | 45 | if (loginUser == null) { |
| 46 | 46 | loginUser = this.mockLoginUser(request, token); |
| ... | ... | @@ -78,7 +78,7 @@ public class JWTAuthenticationTokenFilter extends OncePerRequestFilter { |
| 78 | 78 | return null; |
| 79 | 79 | } |
| 80 | 80 | Long userId = Long.valueOf(token.substring(securityProperties.getMockSecret().length())); |
| 81 | - return authService.mockLogin(request, userId); | |
| 81 | + return authenticationProvider.mockLogin(request, userId); | |
| 82 | 82 | } |
| 83 | 83 | |
| 84 | 84 | } | ... | ... |
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/handler/LogoutSuccessHandlerImpl.java
| ... | ... | @@ -4,7 +4,7 @@ import cn.hutool.core.util.StrUtil; |
| 4 | 4 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 5 | 5 | import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; |
| 6 | 6 | import cn.iocoder.yudao.framework.security.config.SecurityProperties; |
| 7 | -import cn.iocoder.yudao.framework.security.core.service.SecurityAuthService; | |
| 7 | +import cn.iocoder.yudao.framework.security.core.authentication.MultiUserDetailsAuthenticationProvider; | |
| 8 | 8 | import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; |
| 9 | 9 | import lombok.AllArgsConstructor; |
| 10 | 10 | import org.springframework.security.core.Authentication; |
| ... | ... | @@ -24,14 +24,14 @@ public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler { |
| 24 | 24 | |
| 25 | 25 | private final SecurityProperties securityProperties; |
| 26 | 26 | |
| 27 | - private final SecurityAuthService authService; | |
| 27 | + private final MultiUserDetailsAuthenticationProvider authenticationProvider; | |
| 28 | 28 | |
| 29 | 29 | @Override |
| 30 | 30 | public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { |
| 31 | 31 | // 执行退出 |
| 32 | 32 | String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader()); |
| 33 | 33 | if (StrUtil.isNotBlank(token)) { |
| 34 | - authService.logout(request, token); | |
| 34 | + authenticationProvider.logout(request, token); | |
| 35 | 35 | } |
| 36 | 36 | // 返回成功 |
| 37 | 37 | ServletUtils.writeJSON(response, CommonResult.success(null)); | ... | ... |
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityAuthService.java deleted
| 1 | -package cn.iocoder.yudao.framework.security.core.service; | |
| 2 | - | |
| 3 | -import cn.iocoder.yudao.framework.security.core.LoginUser; | |
| 4 | - | |
| 5 | -import javax.servlet.http.HttpServletRequest; | |
| 6 | - | |
| 7 | -/** | |
| 8 | - * 安全认证的 Service 接口,对 security 组件提供统一的 Auth 相关的方法。 | |
| 9 | - * 主要是会基于 {@link HttpServletRequest} 参数,匹配对应的 {@link SecurityAuthFrameworkService} 实现,然后调用其方法。 | |
| 10 | - * 因此,在方法的定义上,和 {@link SecurityAuthFrameworkService} 差不多。 | |
| 11 | - * | |
| 12 | - * @author 芋道源码 | |
| 13 | - */ | |
| 14 | -public interface SecurityAuthService { | |
| 15 | - | |
| 16 | - /** | |
| 17 | - * 校验 token 的有效性,并获取用户信息 | |
| 18 | - * 通过后,刷新 token 的过期时间 | |
| 19 | - * | |
| 20 | - * @param request 请求 | |
| 21 | - * @param token token | |
| 22 | - * @return 用户信息 | |
| 23 | - */ | |
| 24 | - LoginUser verifyTokenAndRefresh(HttpServletRequest request, String token); | |
| 25 | - | |
| 26 | - /** | |
| 27 | - * 模拟指定用户编号的 LoginUser | |
| 28 | - * | |
| 29 | - * @param request 请求 | |
| 30 | - * @param userId 用户编号 | |
| 31 | - * @return 登录用户 | |
| 32 | - */ | |
| 33 | - LoginUser mockLogin(HttpServletRequest request, Long userId); | |
| 34 | - | |
| 35 | - /** | |
| 36 | - * 基于 token 退出登录 | |
| 37 | - * | |
| 38 | - * @param request 请求 | |
| 39 | - * @param token token | |
| 40 | - */ | |
| 41 | - void logout(HttpServletRequest request, String token); | |
| 42 | - | |
| 43 | -} |
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityAuthServiceImpl.java deleted
| 1 | -package cn.iocoder.yudao.framework.security.core.service; | |
| 2 | - | |
| 3 | -import cn.hutool.core.lang.Assert; | |
| 4 | -import cn.hutool.core.util.StrUtil; | |
| 5 | -import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; | |
| 6 | -import cn.iocoder.yudao.framework.security.core.LoginUser; | |
| 7 | -import cn.iocoder.yudao.framework.web.config.WebProperties; | |
| 8 | - | |
| 9 | -import javax.servlet.http.HttpServletRequest; | |
| 10 | -import java.util.HashMap; | |
| 11 | -import java.util.List; | |
| 12 | -import java.util.Map; | |
| 13 | - | |
| 14 | -/** | |
| 15 | - * 安全认证的 Service 实现类,基于请求地址,计算到对应的 {@link UserTypeEnum} 枚举,从而拿到对应的 {@link SecurityAuthFrameworkService} 实现 | |
| 16 | - * | |
| 17 | - * @author 芋道源码 | |
| 18 | - */ | |
| 19 | -public class SecurityAuthServiceImpl implements SecurityAuthService { | |
| 20 | - | |
| 21 | - private final Map<UserTypeEnum, SecurityAuthFrameworkService> services = new HashMap<>(); | |
| 22 | - private final WebProperties properties; | |
| 23 | - | |
| 24 | - public SecurityAuthServiceImpl(List<SecurityAuthFrameworkService> serviceList, WebProperties properties) { | |
| 25 | - serviceList.forEach(service -> services.put(service.getUserType(), service)); | |
| 26 | - this.properties = properties; | |
| 27 | - } | |
| 28 | - | |
| 29 | - @Override | |
| 30 | - public LoginUser verifyTokenAndRefresh(HttpServletRequest request, String token) { | |
| 31 | - return selectService(request).verifyTokenAndRefresh(token); | |
| 32 | - } | |
| 33 | - | |
| 34 | - @Override | |
| 35 | - public LoginUser mockLogin(HttpServletRequest request, Long userId) { | |
| 36 | - return selectService(request).mockLogin(userId); | |
| 37 | - } | |
| 38 | - | |
| 39 | - @Override | |
| 40 | - public void logout(HttpServletRequest request, String token) { | |
| 41 | - selectService(request).logout(token); | |
| 42 | - } | |
| 43 | - | |
| 44 | - private SecurityAuthFrameworkService selectService(HttpServletRequest request) { | |
| 45 | - // 第一步,获得用户类型 | |
| 46 | - UserTypeEnum userType = getUserType(request); | |
| 47 | - // 第二步,获得 SecurityAuthFrameworkService | |
| 48 | - SecurityAuthFrameworkService service = services.get(userType); | |
| 49 | - Assert.notNull(service, "URI({}) 用户类型({}) 找不到 SecurityAuthFrameworkService 实现类", | |
| 50 | - request.getRequestURI(), userType); | |
| 51 | - return service; | |
| 52 | - } | |
| 53 | - | |
| 54 | - private UserTypeEnum getUserType(HttpServletRequest request) { | |
| 55 | - if (request.getRequestURI().startsWith(properties.getAdminApi().getPrefix())) { | |
| 56 | - return UserTypeEnum.ADMIN; | |
| 57 | - } | |
| 58 | - if (request.getRequestURI().startsWith(properties.getAppApi().getPrefix())) { | |
| 59 | - return UserTypeEnum.MEMBER; | |
| 60 | - } | |
| 61 | - throw new IllegalArgumentException(StrUtil.format("URI({}) 找不到匹配的用户类型", request.getRequestURI())); | |
| 62 | - } | |
| 63 | - | |
| 64 | -} |
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java
| ... | ... | @@ -55,6 +55,11 @@ public class WebFrameworkUtils { |
| 55 | 55 | return (Integer) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE); |
| 56 | 56 | } |
| 57 | 57 | |
| 58 | + public static Integer getLoginUserType() { | |
| 59 | + HttpServletRequest request = getRequest(); | |
| 60 | + return getLoginUserType(request); | |
| 61 | + } | |
| 62 | + | |
| 58 | 63 | public static Long getLoginUserId() { |
| 59 | 64 | HttpServletRequest request = getRequest(); |
| 60 | 65 | return getLoginUserId(request); | ... | ... |
yudao-module-member/yudao-module-member-impl/pom.xml
| ... | ... | @@ -32,6 +32,10 @@ |
| 32 | 32 | |
| 33 | 33 | <dependency> |
| 34 | 34 | <groupId>cn.iocoder.boot</groupId> |
| 35 | + <artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId> | |
| 36 | + </dependency> | |
| 37 | + <dependency> | |
| 38 | + <groupId>cn.iocoder.boot</groupId> | |
| 35 | 39 | <artifactId>yudao-spring-boot-starter-biz-sms</artifactId> |
| 36 | 40 | </dependency> |
| 37 | 41 | <dependency> | ... | ... |
yudao-module-member/yudao-module-member-impl/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.http
yudao-module-member/yudao-module-member-impl/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.java
| ... | ... | @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.member.controller.app.auth; |
| 3 | 3 | import cn.iocoder.yudao.coreservice.modules.system.service.social.SysSocialCoreService; |
| 4 | 4 | import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; |
| 5 | 5 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 6 | +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; | |
| 6 | 7 | import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated; |
| 7 | 8 | import cn.iocoder.yudao.module.member.controller.app.auth.vo.*; |
| 8 | 9 | import cn.iocoder.yudao.module.member.service.auth.AuthService; |
| ... | ... | @@ -40,6 +41,7 @@ public class AppAuthController { |
| 40 | 41 | |
| 41 | 42 | @PostMapping("/login") |
| 42 | 43 | @ApiOperation("使用手机 + 密码登录") |
| 44 | + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 | |
| 43 | 45 | public CommonResult<AppAuthLoginRespVO> login(@RequestBody @Valid AppAuthLoginReqVO reqVO) { |
| 44 | 46 | String token = authService.login(reqVO, getClientIP(), getUserAgent()); |
| 45 | 47 | // 返回结果 |
| ... | ... | @@ -48,6 +50,7 @@ public class AppAuthController { |
| 48 | 50 | |
| 49 | 51 | @PostMapping("/sms-login") |
| 50 | 52 | @ApiOperation("使用手机 + 验证码登录") |
| 53 | + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 | |
| 51 | 54 | public CommonResult<AppAuthLoginRespVO> smsLogin(@RequestBody @Valid AppAuthSmsLoginReqVO reqVO) { |
| 52 | 55 | String token = authService.smsLogin(reqVO, getClientIP(), getUserAgent()); |
| 53 | 56 | // 返回结果 |
| ... | ... | @@ -56,12 +59,13 @@ public class AppAuthController { |
| 56 | 59 | |
| 57 | 60 | @PostMapping("/send-sms-code") |
| 58 | 61 | @ApiOperation(value = "发送手机验证码") |
| 62 | + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 | |
| 59 | 63 | public CommonResult<Boolean> sendSmsCode(@RequestBody @Valid AppAuthSendSmsReqVO reqVO) { |
| 60 | 64 | smsCodeService.sendSmsCode(reqVO.getMobile(), reqVO.getScene(), getClientIP()); |
| 61 | 65 | return success(true); |
| 62 | 66 | } |
| 63 | 67 | |
| 64 | - @GetMapping("/send-sms-code-login") | |
| 68 | + @GetMapping("/send-sms-code-login") // TODO 芋艿:post 比较合理 | |
| 65 | 69 | @ApiOperation(value = "向已登录用户发送验证码",notes = "修改手机时验证原手机号使用") |
| 66 | 70 | public CommonResult<Boolean> sendSmsCodeLogin() { |
| 67 | 71 | smsCodeService.sendSmsCodeLogin(getLoginUserId()); |
| ... | ... | @@ -71,6 +75,7 @@ public class AppAuthController { |
| 71 | 75 | @PostMapping("/reset-password") |
| 72 | 76 | @ApiOperation(value = "重置密码", notes = "用户忘记密码时使用") |
| 73 | 77 | @PreAuthenticated |
| 78 | + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 | |
| 74 | 79 | public CommonResult<Boolean> resetPassword(@RequestBody @Valid AppAuthResetPasswordReqVO reqVO) { |
| 75 | 80 | authService.resetPassword(reqVO); |
| 76 | 81 | return success(true); |
| ... | ... | @@ -106,6 +111,7 @@ public class AppAuthController { |
| 106 | 111 | |
| 107 | 112 | @PostMapping("/social-login2") |
| 108 | 113 | @ApiOperation("社交登录,使用 手机号 + 手机验证码") |
| 114 | + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 | |
| 109 | 115 | public CommonResult<AppAuthLoginRespVO> socialLogin2(@RequestBody @Valid AppAuthSocialLogin2ReqVO reqVO) { |
| 110 | 116 | String token = authService.socialLogin2(reqVO, getClientIP(), getUserAgent()); |
| 111 | 117 | return success(AppAuthLoginRespVO.builder().token(token).build()); |
| ... | ... | @@ -113,6 +119,7 @@ public class AppAuthController { |
| 113 | 119 | |
| 114 | 120 | @PostMapping("/social-bind") |
| 115 | 121 | @ApiOperation("社交绑定,使用 code 授权码") |
| 122 | + @PreAuthenticated | |
| 116 | 123 | public CommonResult<Boolean> socialBind(@RequestBody @Valid AppAuthSocialBindReqVO reqVO) { |
| 117 | 124 | authService.socialBind(getLoginUserId(), reqVO); |
| 118 | 125 | return CommonResult.success(true); |
| ... | ... | @@ -120,6 +127,7 @@ public class AppAuthController { |
| 120 | 127 | |
| 121 | 128 | @DeleteMapping("/social-unbind") |
| 122 | 129 | @ApiOperation("取消社交绑定") |
| 130 | + @PreAuthenticated | |
| 123 | 131 | public CommonResult<Boolean> socialUnbind(@RequestBody AppAuthSocialUnbindReqVO reqVO) { |
| 124 | 132 | socialService.unbindSocialUser(getLoginUserId(), reqVO.getType(), reqVO.getUnionId(), UserTypeEnum.MEMBER); |
| 125 | 133 | return CommonResult.success(true); | ... | ... |
yudao-module-member/yudao-module-member-impl/src/main/java/cn/iocoder/yudao/module/member/service/auth/AuthServiceImpl.java
| ... | ... | @@ -14,6 +14,7 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; |
| 14 | 14 | import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; |
| 15 | 15 | import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; |
| 16 | 16 | import cn.iocoder.yudao.framework.security.core.LoginUser; |
| 17 | +import cn.iocoder.yudao.framework.security.core.authentication.MultiUsernamePasswordAuthenticationToken; | |
| 17 | 18 | import cn.iocoder.yudao.module.member.controller.app.auth.vo.*; |
| 18 | 19 | import cn.iocoder.yudao.module.member.convert.auth.AuthConvert; |
| 19 | 20 | import cn.iocoder.yudao.module.member.dal.dataobject.user.UserDO; |
| ... | ... | @@ -176,7 +177,8 @@ public class AuthServiceImpl implements AuthService { |
| 176 | 177 | try { |
| 177 | 178 | // 调用 Spring Security 的 AuthenticationManager#authenticate(...) 方法,使用账号密码进行认证 |
| 178 | 179 | // 在其内部,会调用到 loadUserByUsername 方法,获取 User 信息 |
| 179 | - authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); | |
| 180 | + authentication = authenticationManager.authenticate(new MultiUsernamePasswordAuthenticationToken( | |
| 181 | + username, password, getUserType())); | |
| 180 | 182 | } catch (BadCredentialsException badCredentialsException) { |
| 181 | 183 | this.createLoginLog(username, logTypeEnum, SysLoginResultEnum.BAD_CREDENTIALS); |
| 182 | 184 | throw exception(AUTH_LOGIN_BAD_CREDENTIALS); | ... | ... |
yudao-server/pom.xml deleted
| 1 | -<?xml version="1.0" encoding="UTF-8"?> | |
| 2 | -<project xmlns="http://maven.apache.org/POM/4.0.0" | |
| 3 | - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
| 4 | - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | |
| 5 | - <parent> | |
| 6 | - <artifactId>yudao</artifactId> | |
| 7 | - <groupId>cn.iocoder.boot</groupId> | |
| 8 | - <version>${revision}</version> | |
| 9 | - </parent> | |
| 10 | - <modelVersion>4.0.0</modelVersion> | |
| 11 | - | |
| 12 | - <artifactId>yudao-server</artifactId> | |
| 13 | - <packaging>jar</packaging> | |
| 14 | - | |
| 15 | - <name>${artifactId}</name> | |
| 16 | - <description> | |
| 17 | - 后端 Server 的主项目,通过引入需要 yudao-module-xxx 的依赖, | |
| 18 | - 从而实现提供 RESTful API 给 yudao-ui-admin、yudao-ui-user 等前端项目。 | |
| 19 | - 本质上来说,它就是个空壳(容器)! | |
| 20 | - </description> | |
| 21 | - | |
| 22 | -</project> |
更新日志.md
| ... | ... | @@ -4,29 +4,30 @@ |
| 4 | 4 | * 钉钉、飞书等通知 |
| 5 | 5 | * Vue3 支持 |
| 6 | 6 | |
| 7 | -## [v1.4.0] 计划 | |
| 8 | - | |
| 9 | -* 工作流 | |
| 10 | - * 修改表单为外置表单 | |
| 11 | - * 修改请假流程 | |
| 12 | - * 暂时以用户的岗位作为activiti 的用户组 | |
| 13 | - * 请假需要请假人部门下具有项目经理岗位, 部门经理, 和人事 岗位的用户 | |
| 14 | - * 新增 芋道源码部门下 用户 normal(岗位 普通用户) projectmgr(岗位 项目经理) depmgr(岗位 部门经理) hradmin (岗位 人事) | |
| 15 | - * 请假流程如下 | |
| 16 | - 1. 请假人 normal (密码 123456) 登录在我的请假表单,点击新增,填写请假表单 | |
| 17 | - 2. 如果请假天数<=3, 项目经理 进行审批. 项目经理 projectmgr(密码:123456) 登录 待办请假,中进行审批,可以查看历史跟踪,和流程图 | |
| 18 | - 3. 如果请假天数>3 需部门经理 进行审批,部门经理depmgr(密码:123456) 登录 待办请假,中进行审批,可以查看历史跟踪,和流程图 | |
| 19 | - 4. 人事登陆(用户名:hradmin 密码:123456) 登录 待办请假, 中进行审批,可以查看历史跟踪,和流程图 | |
| 20 | - 5. 流程结束 | |
| 21 | - * 我的请假中,可以查询本人的请假申请, 和进度 | |
| 22 | - | |
| 23 | 7 | ### 📝 TODO |
| 24 | 8 | |
| 25 | 9 | * 支付 |
| 26 | 10 | * 用户前台的社交登陆 |
| 27 | 11 | * 用户前台的修改手机、修改密码、忘记密码 |
| 28 | 12 | |
| 29 | -## [v1.3.0] 进行中 | |
| 13 | +## [v1.4.0] 计划,预计 2022.02.28 发布 | |
| 14 | + | |
| 15 | +### ⚠️ Warning | |
| 16 | + | |
| 17 | +### 📈 Statistic | |
| 18 | + | |
| 19 | +### ⭐ New Features | |
| 20 | + | |
| 21 | +*【优化】操作日志新增用户类型,实现 APP 端的 API 的操作日志的记录 | |
| 22 | + | |
| 23 | +### 🐞 Bug Fixes | |
| 24 | + | |
| 25 | +*【修复】用户无权限访问 指定 API 时,未返回 FORBIDDEN 结果码 | |
| 26 | + | |
| 27 | +### 🔨 Dependency Upgrades | |
| 28 | + | |
| 29 | + | |
| 30 | +## [v1.3.0] 2022.01.24 | |
| 30 | 31 | |
| 31 | 32 | ### ⚠️ Warning |
| 32 | 33 | ... | ... |