| | |
| | | import com.ruoyi.system.service.ISysClientAppService; |
| | | import com.ruoyi.common.utils.poi.ExcelUtil; |
| | | import com.ruoyi.common.core.page.TableDataInfo; |
| | | import com.ruoyi.common.annotation.Anonymous; |
| | | import com.ruoyi.common.utils.SecurityUtils; |
| | | |
| | | /** |
| | | * 客户应用配置Controller |
| | |
| | | public AjaxResult remove(@PathVariable Long[] appIds) { |
| | | return toAjax(sysClientAppService.deleteSysClientAppByAppIds(appIds)); |
| | | } |
| | | |
| | | @Anonymous(needSign=true) |
| | | @GetMapping("/testSign") |
| | | public AjaxResult testSign(){ |
| | | return AjaxResult.success("成功"); |
| | | } |
| | | /** |
| | | * 生成签名 |
| | | */ |
| | | @Anonymous |
| | | @GetMapping("/generateSign/{appId}") |
| | | public AjaxResult generateSign(@PathVariable("appId") String appId) |
| | | { |
| | | // 获取当前系统时间戳 |
| | | long timestamp = System.currentTimeMillis(); |
| | | |
| | | // 查询应用信息获取securityKey |
| | | SysClientApp clientApp = sysClientAppService.selectSysClientAppByAppKey(appId); |
| | | if (clientApp == null) |
| | | { |
| | | return AjaxResult.error("应用不存在"); |
| | | } |
| | | |
| | | // 生成签名 |
| | | String signStr = appId + timestamp + clientApp.getSecurityKey(); |
| | | String sign = SecurityUtils.md5(signStr); |
| | | |
| | | AjaxResult ajax = AjaxResult.success(); |
| | | ajax.put("appId", appId); |
| | | ajax.put("timestamp", String.valueOf(timestamp)); |
| | | ajax.put("sign", sign); |
| | | //ajax.put("signStr", signStr); // 用于调试,显示拼接的字符串 |
| | | return ajax; |
| | | } |
| | | } |
| | | |
| | |
| | | # 地址 |
| | | host: localhost |
| | | # 端口,默认为6379 |
| | | port: 16379 |
| | | port: 6379 |
| | | # 数据库索引 |
| | | database: 0 |
| | | # 密码 |
| | |
| | | @Documented |
| | | public @interface Anonymous |
| | | { |
| | | /** |
| | | * 是否需要签名验证及时间戳验证 |
| | | */ |
| | | boolean needSign() default false; |
| | | |
| | | } |
| | |
| | | package com.ruoyi.common.utils; |
| | | |
| | | import java.security.MessageDigest; |
| | | import java.security.NoSuchAlgorithmException; |
| | | import java.util.Collection; |
| | | import java.util.List; |
| | | import java.util.stream.Collectors; |
| | |
| | | BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); |
| | | return passwordEncoder.matches(rawPassword, encodedPassword); |
| | | } |
| | | |
| | | /** |
| | | * MD5加密 |
| | | * |
| | | * @param str 需要加密的字符串 |
| | | * @return 加密后的字符串 |
| | | */ |
| | | public static String md5(String str) { |
| | | try { |
| | | MessageDigest md = MessageDigest.getInstance("MD5"); |
| | | byte[] bytes = md.digest(str.getBytes()); |
| | | StringBuilder result = new StringBuilder(); |
| | | for (byte b : bytes) { |
| | | String temp = Integer.toHexString(b & 0xff); |
| | | if (temp.length() == 1) { |
| | | temp = "0" + temp; |
| | | } |
| | | result.append(temp); |
| | | } |
| | | return result.toString(); |
| | | } catch (NoSuchAlgorithmException e) { |
| | | throw new RuntimeException("MD5加密失败", e); |
| | | } |
| | | } |
| | | /** |
| | | * 是否为管理员 |
| | | * |
| | |
| | | import com.ruoyi.common.config.RuoYiConfig; |
| | | import com.ruoyi.common.constant.Constants; |
| | | import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor; |
| | | import com.ruoyi.framework.interceptor.AnonymousInterceptor; |
| | | |
| | | /** |
| | | * 通用配置 |
| | |
| | | { |
| | | @Autowired |
| | | private RepeatSubmitInterceptor repeatSubmitInterceptor; |
| | | |
| | | @Autowired |
| | | private AnonymousInterceptor anonymousInterceptor; |
| | | |
| | | @Override |
| | | public void addResourceHandlers(ResourceHandlerRegistry registry) |
| | |
| | | } |
| | | |
| | | /** |
| | | * 自定义拦截规则 |
| | | * 添加拦截器 |
| | | */ |
| | | @Override |
| | | public void addInterceptors(InterceptorRegistry registry) |
| | | { |
| | | // 重复提交拦截器 |
| | | registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**"); |
| | | |
| | | // 匿名访问拦截器 |
| | | registry.addInterceptor(anonymousInterceptor).addPathPatterns("/**"); |
| | | } |
| | | |
| | | /** |
| | |
| | | public CorsFilter corsFilter() |
| | | { |
| | | CorsConfiguration config = new CorsConfiguration(); |
| | | // 设置访问源地址 |
| | | config.addAllowedOriginPattern("*"); |
| | | // 设置访问源请求头 |
| | | config.addAllowedHeader("*"); |
| | | // 设置访问源请求方法 |
| | | config.addAllowedMethod("*"); |
| | | // 有效期 1800秒 |
| | | config.setMaxAge(1800L); |
| | | // 添加映射路径,拦截一切请求 |
| | | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); |
| | | source.registerCorsConfiguration("/**", config); |
| | | // 返回新的CorsFilter |
| | | return new CorsFilter(source); |
| | | } |
| | | } |
| | |
| | | import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter; |
| | | import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl; |
| | | import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl; |
| | | import com.ruoyi.common.annotation.Anonymous; |
| | | import org.springframework.security.web.util.matcher.RequestMatcher; |
| | | import org.springframework.web.method.HandlerMethod; |
| | | import org.springframework.web.servlet.mvc.method.RequestMappingInfo; |
| | | import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; |
| | | import java.util.HashSet; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | /** |
| | | * spring security配置 |
| | |
| | | @Autowired |
| | | private PermitAllUrlProperties permitAllUrl; |
| | | |
| | | @Autowired |
| | | private RequestMappingHandlerMapping requestMappingHandlerMapping; |
| | | |
| | | /** |
| | | * 获取所有标注了@Anonymous的URL |
| | | */ |
| | | private Set<String> getAnonymousUrls() { |
| | | Set<String> urls = new HashSet<>(); |
| | | Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMappingHandlerMapping.getHandlerMethods(); |
| | | for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethods.entrySet()) { |
| | | HandlerMethod handlerMethod = entry.getValue(); |
| | | Anonymous anonymous = handlerMethod.getMethodAnnotation(Anonymous.class); |
| | | if (anonymous != null) { |
| | | Set<String> patterns = entry.getKey().getPatternValues(); |
| | | urls.addAll(patterns); |
| | | } |
| | | } |
| | | return urls; |
| | | } |
| | | |
| | | /** |
| | | * 身份验证实现 |
| | | */ |
| | |
| | | @Bean |
| | | protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception |
| | | { |
| | | // 获取所有标注了@Anonymous的URL |
| | | Set<String> anonymousUrls = getAnonymousUrls(); |
| | | |
| | | return httpSecurity |
| | | // CSRF禁用,因为不使用session |
| | | .csrf(csrf -> csrf.disable()) |
| | |
| | | .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) |
| | | // 基于token,所以不需要session |
| | | .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) |
| | | // 注解标记允许匿名访问的url |
| | | .authorizeHttpRequests((requests) -> { |
| | | permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll()); |
| | | // 对于登录login 注册register 验证码captchaImage 允许匿名访问 |
| | | requests.antMatchers("/login", "/register", "/captchaImage").permitAll() |
| | | // 静态资源,可匿名访问 |
| | | .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() |
| | | .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll() |
| | | // 除上面外的所有请求全部需要鉴权认证 |
| | | .anyRequest().authenticated(); |
| | | }) |
| | | // 过滤请求 |
| | | .authorizeRequests() |
| | | // 对于登录login 注册register 验证码captchaImage 允许匿名访问 |
| | | .antMatchers("/login", "/register", "/captchaImage").permitAll() |
| | | // 添加标注了@Anonymous的URL到匿名访问列表 |
| | | .antMatchers(anonymousUrls.toArray(new String[0])).permitAll() |
| | | // 静态资源,可匿名访问 |
| | | .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() |
| | | .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll() |
| | | // 除上面外的所有请求全部需要鉴权认证 |
| | | .anyRequest().authenticated() |
| | | .and() |
| | | // 添加Logout filter |
| | | .logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler)) |
| | | // 添加JWT filter |
New file |
| | |
| | | package com.ruoyi.framework.interceptor; |
| | | |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Component; |
| | | import org.springframework.web.method.HandlerMethod; |
| | | import org.springframework.web.servlet.HandlerInterceptor; |
| | | import org.springframework.util.StringUtils; |
| | | |
| | | import com.ruoyi.common.annotation.Anonymous; |
| | | import com.ruoyi.common.exception.ServiceException; |
| | | import com.ruoyi.system.service.ISysClientAppService; |
| | | |
| | | /** |
| | | * 匿名访问拦截器 |
| | | */ |
| | | @Component |
| | | public class AnonymousInterceptor implements HandlerInterceptor { |
| | | |
| | | @Autowired |
| | | private ISysClientAppService clientAppService; |
| | | |
| | | @Override |
| | | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { |
| | | // 如果不是映射到方法,直接通过 |
| | | if (!(handler instanceof HandlerMethod)) { |
| | | return true; |
| | | } |
| | | |
| | | // 获取方法上的注解 |
| | | HandlerMethod handlerMethod = (HandlerMethod) handler; |
| | | Anonymous anonymous = handlerMethod.getMethodAnnotation(Anonymous.class); |
| | | |
| | | // 如果方法上没有注解,则获取类上的注解 |
| | | if (anonymous == null) { |
| | | anonymous = handlerMethod.getBeanType().getAnnotation(Anonymous.class); |
| | | } |
| | | |
| | | // 如果没有注解,直接通过 |
| | | if (anonymous == null) { |
| | | return true; |
| | | } |
| | | |
| | | // 获取请求参数 |
| | | String appId = request.getParameter("appId"); |
| | | String sign = request.getParameter("sign"); |
| | | String timestamp = request.getParameter("timestamp"); |
| | | if(anonymous.needSign()){ |
| | | if(appId == null || sign == null || timestamp == null){ |
| | | throw new ServiceException("缺少必要参数"); |
| | | |
| | | } |
| | | } |
| | | // 验证必要参数 |
| | | if (StringUtils.hasText(appId) && StringUtils.hasText(sign) && StringUtils.hasText(timestamp)) { |
| | | // 验证签名 |
| | | if (clientAppService.validateSign(appId, sign, timestamp)) { |
| | | return true; |
| | | } |
| | | throw new ServiceException("签名验证失败"); |
| | | } |
| | | |
| | | // 如果没有验证参数,也允许通过(适用于不需要验证的匿名接口) |
| | | return true; |
| | | } |
| | | } |
| | |
| | | import com.ruoyi.common.core.domain.BaseEntity; |
| | | import org.apache.commons.lang3.builder.ToStringBuilder; |
| | | import org.apache.commons.lang3.builder.ToStringStyle; |
| | | import com.fasterxml.jackson.annotation.JsonFormat; |
| | | |
| | | import java.util.Date; |
| | | |
| | |
| | | |
| | | /** 有效期开始时间 */ |
| | | @Excel(name = "有效期开始时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
| | | private Date validStartTime; |
| | | |
| | | /** 有效期结束时间 */ |
| | | @Excel(name = "有效期结束时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
| | | private Date validEndTime; |
| | | |
| | | /** 状态(0正常 1停用) */ |
| | |
| | | * @return 结果 |
| | | */ |
| | | public int deleteSysClientAppByAppIds(Long[] appIds); |
| | | |
| | | /** |
| | | * 根据应用标识查询应用信息 |
| | | * |
| | | * @param appKey 应用标识 |
| | | * @return 应用信息 |
| | | */ |
| | | public SysClientApp selectSysClientAppByAppKey(String appKey); |
| | | } |
| | |
| | | * @return 结果 |
| | | */ |
| | | public int deleteSysClientAppByAppId(Long appId); |
| | | |
| | | /** |
| | | * 验证签名 |
| | | * |
| | | * @param appId 应用ID |
| | | * @param sign 签名 |
| | | * @param timestamp 时间戳 |
| | | * @return 验证结果 |
| | | */ |
| | | public boolean validateSign(String appId, String sign, String timestamp); |
| | | |
| | | /** |
| | | * 通过应用标识查询客户应用配置 |
| | | * |
| | | * @param appKey 应用标识 |
| | | * @return 客户应用配置 |
| | | */ |
| | | public SysClientApp selectSysClientAppByAppKey(String appKey); |
| | | } |
| | |
| | | import com.ruoyi.system.service.ISysClientAppService; |
| | | import com.ruoyi.common.utils.SecurityUtils; |
| | | |
| | | import static com.ruoyi.common.utils.SecurityUtils.md5; |
| | | |
| | | /** |
| | | * 客户应用配置 服务层实现 |
| | | */ |
| | |
| | | @Override |
| | | public SysClientApp selectSysClientAppByAppId(Long appId) { |
| | | return sysClientAppMapper.selectSysClientAppByAppId(appId); |
| | | } |
| | | |
| | | /** |
| | | * 通过应用标识查询客户应用配置 |
| | | * |
| | | * @param appKey 应用标识 |
| | | * @return 客户应用配置 |
| | | */ |
| | | @Override |
| | | public SysClientApp selectSysClientAppByAppKey(String appKey) |
| | | { |
| | | return sysClientAppMapper.selectSysClientAppByAppKey(appKey); |
| | | } |
| | | |
| | | /** |
| | |
| | | public int deleteSysClientAppByAppId(Long appId) { |
| | | return sysClientAppMapper.deleteSysClientAppByAppId(appId); |
| | | } |
| | | |
| | | @Override |
| | | public boolean validateSign(String appId, String sign, String timestamp) { |
| | | // 根据appId获取应用信息 |
| | | SysClientApp clientApp = sysClientAppMapper.selectSysClientAppByAppKey(appId); |
| | | if (clientApp == null) { |
| | | return false; |
| | | } |
| | | |
| | | // 验证应用是否有效 |
| | | if (!"0".equals(clientApp.getStatus())) { |
| | | return false; |
| | | } |
| | | |
| | | // 验证有效期 |
| | | if (clientApp.getValidStartTime() != null && clientApp.getValidEndTime() != null) { |
| | | long currentTime = System.currentTimeMillis(); |
| | | if (currentTime < clientApp.getValidStartTime().getTime() |
| | | || currentTime > clientApp.getValidEndTime().getTime()) { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | // 生成签名 |
| | | String serverSign = generateSign(appId, clientApp.getSecurityKey(), timestamp); |
| | | |
| | | // 比较签名 |
| | | return sign.equals(serverSign); |
| | | } |
| | | |
| | | /** |
| | | * 生成签名 |
| | | * 签名规则:MD5(appId + timestamp + securityKey) |
| | | */ |
| | | private String generateSign(String appId, String securityKey, String timestamp) { |
| | | String signStr = appId + timestamp + securityKey; |
| | | return md5(signStr); |
| | | } |
| | | } |
| | |
| | | where app_id = #{appId} |
| | | </select> |
| | | |
| | | <select id="selectSysClientAppByAppKey" parameterType="String" resultMap="SysClientAppResult"> |
| | | <include refid="selectSysClientAppVo"/> |
| | | where app_key = #{appKey} and del_flag = '0' |
| | | </select> |
| | | |
| | | <insert id="insertSysClientApp" parameterType="SysClientApp" useGeneratedKeys="true" keyProperty="appId"> |
| | | insert into sys_client_app |
| | | <trim prefix="(" suffix=")" suffixOverrides=","> |
| | |
| | | submitForm() { |
| | | this.$refs["form"].validate(valid => { |
| | | if (valid) { |
| | | // 处理日期格式 |
| | | if (this.form.validStartTime) { |
| | | this.form.validStartTime = this.parseTime(this.form.validStartTime, '{y}-{m}-{d} {h}:{i}:{s}'); |
| | | } |
| | | if (this.form.validEndTime) { |
| | | this.form.validEndTime = this.parseTime(this.form.validEndTime, '{y}-{m}-{d} {h}:{i}:{s}'); |
| | | } |
| | | |
| | | if (this.form.appId != null) { |
| | | updateClientApp(this.form).then(response => { |
| | | this.$modal.msgSuccess("修改成功"); |
New file |
| | |
| | | _-- 菜单 SQL |
| | | INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, STATUS, perms, icon, create_by, create_time, update_by, update_time, remark) |
| | | VALUES('客户应用配置', '1', '1', 'clientApp', 'system/clientApp/index', 1, 0, 'C', '0', '0', 'system:clientApp:list', 'app', 'admin', SYSDATE(), '', NULL, '客户应用配置菜单'); |
| | | |
| | | -- 按钮父菜单ID |
| | | SELECT @parentId := LAST_INSERT_ID(); |
| | | |
| | | -- 按钮 SQL |
| | | INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, STATUS, perms, icon, create_by, create_time, update_by, update_time, remark) |
| | | VALUES('客户应用配置查询', @parentId, '1', '#', '', 1, 0, 'F', '0', '0', 'system:clientApp:query', '#', 'admin', SYSDATE(), '', NULL, ''); |
| | | |
| | | INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, STATUS, perms, icon, create_by, create_time, update_by, update_time, remark) |
| | | VALUES('客户应用配置新增', @parentId, '2', '#', '', 1, 0, 'F', '0', '0', 'system:clientApp:add', '#', 'admin', SYSDATE(), '', NULL, ''); |
| | | |
| | | INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, STATUS, perms, icon, create_by, create_time, update_by, update_time, remark) |
| | | VALUES('客户应用配置修改', @parentId, '3', '#', '', 1, 0, 'F', '0', '0', 'system:clientApp:edit', '#', 'admin', SYSDATE(), '', NULL, ''); |
| | | |
| | | INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, STATUS, perms, icon, create_by, create_time, update_by, update_time, remark) |
| | | VALUES('客户应用配置删除', @parentId, '4', '#', '', 1, 0, 'F', '0', '0', 'system:clientApp:remove', '#', 'admin', SYSDATE(), '', NULL, ''); |
| | | |
| | | INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, STATUS, perms, icon, create_by, create_time, update_by, update_time, remark) |
| | | VALUES('客户应用配置导出', @parentId, '5', '#', '', 1, 0, 'F', '0', '0', 'system:clientApp:export', '#', 'admin', SYSDATE(), '', NULL, ''); |