第一集:如何用SpringBoot打造自己的基础业务框架?权限篇1

朋友们好!昨天刷到头条里一位条友说关于使用第三方插件实现解耦的权限控制的方法(至于是什么插件,我不记得了),依是有了写一下我自已开发使用的基础业务框架的想法,篇幅比较大,可能一篇写不完,要分几篇,在下文笔不好及精力有限,有没讲楚的,还请多见谅。作为一位多年从事软件开发外包业务的老码农,提高软件开发效率至关重大,每个应用基本上都有如角色权限、日志,短信等等的基础业务。不应该为每个新项目都去重做这些工作。自己开发一个基础的业务框架可以大大缩短项目开发周期…

选插一句,我是从C#转JAVA的,之前为东莞一间医院做过OA系统,不过目前的C#对于我来说比较陌生了,由于离开它的时间太久了。

关于我自家开发使用的基础业务框架有:
1、rock-framework:只要包含用户、角色与权限、过滤器、拦截器、用户操作日志、及定义了一些扩展的interface等
2、rock-pay:支付框架:主要集成了wechatPay、aliPay平台相关接口。
3、rock-wechat:主要集成了微信公众平台、开放平台及企业微信的相关接口。
4、rock-message:封装mqtt、websocket、TCPServer等通信
5、rock-sms:短信息,主要集成了阿里短信接口、邮件。
6、rock-file:文件系统
7、rock-cache:缓存相关,集成redis+caffeine,使用自定义注解给方法加上缓存。
8、rock-live:集成美团、抖音等本地生活相关接口
9、rock-logistics:物流相关,集成顺丰同城、闪送、达达等。
10、rock-common:基础依赖,就是一些工具类、自定义异常基类、i18n、基础entity等等。

上面这些都是做过的项目中触及到的业务功能,在做项目时觉得后来可能会复用,所以会将一些可能会复用的功能业务抽出来形成一个单独的SDK。每次有新项目把该依赖加上就可以得到相应的功能、我们只需专注于业务开发就可以,这样就可以大大提高开发效率。

第一我就说说rock-framework的框架设计思路,这是我最基础的业务框架,我每个项目都用到,新项目只要依赖它就拥有了相关的用户权限分组,用户登录、权限鉴别,用户操作日志等等功能

一、rock-framework主要依赖项为JPA、Security、MVC、jjwt-jackson、jjwt-impl等
1、先看看
WebMvcConfigurationSupport自定义配置实现

@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport  {

    @Resource
    private UriInterceptor uriInterceptor;

    @Resource
    private LogInterceptor logInterceptor;

    @Resource
    private RegisterInterceptor registerInterceptor;


    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(registerInterceptor);
        registry.addInterceptor(uriInterceptor);
        registry.addInterceptor(logInterceptor);
        super.addInterceptors(registry);
    }


    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler(WebSecurityConfig.FAVICON_FILE, WebSecurityConfig.STATIC_RESOURCES_PATH + "**")
                .addResourceLocations(
                        "classpath:" + WebSecurityConfig.STATIC_RESOURCES_PATH,
                        "file:" + IApiConfig.getStaticPath());
    }


    @Override
    protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("Setting WebMvc configure...");
        log.info("Spring boot TimeZone {}", TimeZone.getDefault().getDisplayName());
        //字符串转换器,处理Controller 返回字符串带引号问题
        List<MediaType> listString = new ArrayList<>();
        listString.add(new MediaType(MediaType.TEXT_PLAIN, StandardCharsets.UTF_8));
        listString.add(new MediaType(MediaType.TEXT_HTML, StandardCharsets.UTF_8));
        StringHttpMessageConverter stringHttpMessageConverter = stringHttpMessageConverter();
        stringHttpMessageConverter.setSupportedMediaTypes(listString);
        converters.add(stringHttpMessageConverter);

        MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
        ObjectMapper objectMapper = jackson2HttpMessageConverter.getObjectMapper();
        //将Long类型转成字符串输出
        SimpleModule simpleModule = new SimpleModule();
        simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
        simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
        objectMapper.registerModule(simpleModule);

        //时间格式化
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        objectMapper.setDateFormat(new SimpleDateFormat(DtUtils.ISO_DATETIME_FORMAT));
        objectMapper.setTimeZone(TimeZone.getDefault());
        jackson2HttpMessageConverter.setObjectMapper(objectMapper);

        //支持POST RequestBody JSON
        List<MediaType> jsonMediaTypes = new ArrayList<>();
        MediaType jsonMedia = new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8);
        jsonMediaTypes.add(jsonMedia);
        jackson2HttpMessageConverter.setSupportedMediaTypes(jsonMediaTypes);
        converters.add(jackson2HttpMessageConverter);

        //支持POST RequestBody text/xml
        MappingJackson2XmlHttpMessageConverter jackson2XmlHttpMessageConverter = new MappingJackson2XmlHttpMessageConverter();
        List<MediaType> xmlMediaTypes = new ArrayList<>();
        MediaType xmlMedia = new MediaType(MediaType.TEXT_XML, StandardCharsets.UTF_8);
        xmlMediaTypes.add(xmlMedia);
        jackson2XmlHttpMessageConverter.setSupportedMediaTypes(xmlMediaTypes);
        converters.add(jackson2XmlHttpMessageConverter);

        super.configureMessageConverters(converters);
    }

    @Bean
    public StringHttpMessageConverter stringHttpMessageConverter() {
        return new StringHttpMessageConverter(StandardCharsets.UTF_8);
    }

    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer(){
        log.info("Setting Jackson default TimeZone {}", TimeZone.getDefault().getDisplayName());
        return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault());
    }
}

上面代码添加了三个拦截器UriInterceptor(URL请求拦截器,用于请求鉴权)、LogInterceptor(用于URL请求日志记录)、RegisterInterceptor(这个是用于用户注册接口鉴别)。这里重点看看UriInterceptor

package com.rock.frame.interceptors;


import com.rock.common.IApiConfig;
import com.rock.frame.config.Config;
import com.rock.frame.config.WebSecurityConfig;
import com.rock.frame.exception.UriAuthenticationException;
import com.rock.frame.service.PermissionService;
import com.rock.frame.service.UserContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.util.List;

/**
 * description:
 * URI拦截器 功能权限控制
 * @author :rock.chen <br />
 * @date :2021/2/20 7:13 下午 <br />
 **/

@Slf4j
@Component
public class UriInterceptor implements HandlerInterceptor {

    @Resource
    private PermissionService permissionService;

    @Resource
    private Config config;

    @Override
    public boolean preHandle(HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) {
        //静态资源直接放行
        log.debug("UriInterceptor preHandle ...");
        Object isStatic = request.getAttribute(WebSecurityConfig.IS_STATIC_RESOURCES);
        if(isStatic!=null && (Boolean) isStatic){
            log.debug("UriInterceptor preHandle isStatic ...{}", request.getRequestURI());
            return true;
        }
        String uri = request.getRequestURI();
        if(this.isOpenUrl(uri)){
            log.debug("UriInterceptor preHandle isOpenUrl ...{}", request.getRequestURI());
            return true;
        }

        Object sse = request.getAttribute(WebSecurityConfig.IS_SSE_REQUEST);
        if(sse!=null && (Boolean) sse){
            log.debug("UriInterceptor preHandle Server-Sent Events...{}", request.getRequestURI());
            request.setAttribute(WebSecurityConfig.IS_SSE_REQUEST, Boolean.FALSE);
            return true;
        }

        if(uri.startsWith(IApiConfig.GUEST_URL)){
            log.debug("UriInterceptor preHandle isGuest ...{}", request.getRequestURI());
            return true;
        }

      //鉴别当前登录用户是否有权访问该URI
        log.info("To validate URI functionality permissions (URI >>> {})", request.getRequestURI());
        boolean pass = this.permissionService.checkPermission(request.getRequestURI(), UserContextHolder.getCurrentLoginUser());
        if(!pass){
            throw new UriAuthenticationException();
        }

        if(isSseRequest(uri)){
            //Server-Sent Events 首次请过通过后,表明已建立长连接,给它一个标识
            request.setAttribute(WebSecurityConfig.IS_SSE_REQUEST, Boolean.TRUE);
        }
        return true;
    }

    private boolean isSseRequest(String uri){
        List<String> uriList = config.getSseUrls();
        return uriList != null && uriList.contains(uri);
    }

    private boolean isOpenUrl(String uri){
        List<String> uriList = WebSecurityConfig.getOpenUrls();
        uriList.addAll(config.getOpenUrls());
        for(String openUrl : uriList){
            if(uri.equals(openUrl) || (openUrl.endsWith("**") && uri.startsWith(openUrl.substring(0, openUrl.indexOf("*"))))){
                log.debug("UriInterceptor preHandle isOpenUrl ...uri >>> {}, openUrl >>> {}", uri, openUrl);
                return true;
            }
        }
        return false;
    }
}

所有URI请求权限由该拦截器处理。

二、看看ISecurityConfig的实现配置

@Slf4j
@Configuration
@EnableWebSecurity
public class WebSecurityConfig implements ISecurityConfig {

    /**
     * 管理员用户 B 端验证用户
     */
    public static final String ADMIN_ROLE = "ADMIN";
    /**
     * 一般用户  C 端验证用户
     */
    public static final String USER_ROLE = "USER";
    /**
     * 系统角色前辍
     */
    public static final String ROLE_PREFIX = "ROLE_";

    public static final String IS_SSE_REQUEST = "_sseRequest";

    public static List<String> getOpenUrls(){
        List<String> permitList = new ArrayList<>();
        permitList.add(STATIC_RESOURCES_PATH + "**");
        permitList.add(GUEST_API + "**");
        permitList.add(FROM_WX_CALLBACK + "**");
        permitList.add(FROM_ALI_CALLBACK + "**");
        permitList.add(FAVICON_FILE);
        permitList.add("/login");
        permitList.add("/logout");
        permitList.add("/error");
        return permitList;
    }
   //用户登录支持
    @Resource
    private LoginProvider loginProvider;

   //登录成功处理
    @Resource
    private LoginSuccessHandler loginSuccessHandler;

   //登录失败处理
    @Resource
    private LoginFailHandler loginFailHandler;

    //登出处理
    @Resource
    private AppLogoutSuccessHandler appLogoutSuccessHandler;

    //登录异常处理
    @Resource
    private AuthenticationExceptionHandler authenticationExceptionHandler;

   //拒绝访问处理
    @Resource
    private AccessDeniedExceptionHandler accessDeniedExceptionHandler;

   //API签名处理
    @Resource
    private RockAPIFilter rockAPIFilter;

   //Token验证处理
    @Resource
    private RockTokenFilter rockTokenFilter;

    @Resource
    private Config config;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        this.uriSecurity(http).formLogin(form -> form
                .loginPage("/login").successHandler(loginSuccessHandler).failureHandler(loginFailHandler)
        )
        .logout(logout ->
                logout.logoutUrl("/logout").clearAuthentication(true).logoutSuccessHandler(appLogoutSuccessHandler)
        )
        .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        )
        .authenticationProvider(loginProvider)
        .exceptionHandling(exception -> exception.accessDeniedHandler(accessDeniedExceptionHandler))
        .exceptionHandling(exception->exception.authenticationEntryPoint(authenticationExceptionHandler));

        this.addExtBeforeFilters(http);
        if(config.isCsrfDisable()){
            http.csrf(AbstractHttpConfigurer::disable);
        }else{
            http.csrf(httpSecurityCsrfConfigurer -> httpSecurityCsrfConfigurer.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));
        }
        return http.build();
    }

    private HttpSecurity uriSecurity(HttpSecurity http) throws Exception {
        List<String> permitList = getPermitList();
        List<String> sseUrls = config.getSseUrls();
        if(sseUrls == null || sseUrls.isEmpty()){
            return http.authorizeHttpRequests(authorize -> authorize
                    .requestMatchers(permitList.toArray(new String[0])).permitAll()
                    .requestMatchers("/api/**").hasRole(USER_ROLE)
                    .requestMatchers("/admin/**").hasRole(ADMIN_ROLE)
                    .anyRequest().authenticated()
            );
        }
        return http.authorizeHttpRequests(authorize -> authorize
                .requestMatchers(permitList.toArray(new String[0])).permitAll()
                .requestMatchers(sseUrls.toArray(new String[0])).access(sseAwareAuthorizationManager())
                .requestMatchers("/api/**").hasRole(USER_ROLE)
                .requestMatchers("/admin/**").hasRole(ADMIN_ROLE)
                .anyRequest().authenticated()
        );
    }


    private List<String> getPermitList() {
        List<String> permitList = getOpenUrls();
        List<String> openUrlList = config.getOpenUrls();
        if(Objects.nonNull(openUrlList) && !openUrlList.isEmpty()){
            permitList.addAll(openUrlList);
        }
        List<String> staticFiles = config.getStaticFiles();
        if(Objects.nonNull(staticFiles) && !staticFiles.isEmpty()){
            permitList.addAll(staticFiles);
        }
        return permitList;
    }

    private void addExtBeforeFilters(HttpSecurity httpSecurity) {
        List<ExtFilter> extFilterList = new ArrayList<>();
        Map<String, Object> list = BeanUtils.getBeansByAnnotation(ExtBeforeFilter.class);
        for (Map.Entry<String, Object> entry: list.entrySet()){
            Object filter = entry.getValue();
            if (!(filter instanceof Filter)) {
                continue;
            }
            ExtBeforeFilter extBeforeFilter = filter.getClass().getAnnotation(ExtBeforeFilter.class);
            ExtFilter extFilter = ExtFilter.builder().sort(extBeforeFilter.sort()).filter((Filter) filter).build();
            extFilterList.add(extFilter);
        }
        extFilterList.sort(Comparator.comparingInt(ExtFilter::getSort));
        for(ExtFilter filter:extFilterList){
            log.info("add ExtBeforeFilter {}...", filter.getFilter().getClass().getName());
            httpSecurity.addFilterBefore(filter.getFilter(), UsernamePasswordAuthenticationFilter.class);
        }
        httpSecurity.addFilterBefore(rockAPIFilter, UsernamePasswordAuthenticationFilter.class);
        httpSecurity.addFilterBefore(rockTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    public HttpMessageConverter<?> responseBodyConverter() {
        return new StringHttpMessageConverter(StandardCharsets.UTF_8);
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthorizationManager<RequestAuthorizationContext> sseAwareAuthorizationManager() {
        return new MyAuthorizationManager();
    }

    /**
     * 长连接使用该配置时存在一个BUG(列如拉流场景,即长连接断开时会触发重新验证URI, 而重新验证时首次请求的验证成功的信息已被清除,从而导拒绝请求报错)
     * 为了被免这一问题,对于长连接使用单独的验证管理器
     */
    private static class MyAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext>{

        @Deprecated
        @Override
        public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext context) {
            return null;
        }

        @Override
        public AuthorizationResult authorize(Supplier<Authentication> authentication, RequestAuthorizationContext context) {
            HttpServletRequest request = context.getRequest();
            String uri = request.getRequestURI();
            log.info("MyAuthorizationManager URI:{}",uri);
            return new AuthorizationDecision(
                    authentication.get() != null &&
                            authentication.get().isAuthenticated()
            );
            //return AuthorizationManager.super.authorize(authentication, context);
        }
    }

    @Getter
    @Builder
    private static class ExtFilter{
        private int sort;
        private Filter filter;
    }

}

1、先说说URI访问权限:上面配置中除了admin、api开头的URI需要系统角色(USER、ADMIN,这个角色是SpringSecurity内置的)外,其它的URI资源全部放行,所以我在写Controller时,需要权限验证的接口全部会以这/admin/**或/api/**开头。再配合自定义的权限鉴权。

2、自定义的Filter上面有RockAPIFilter及RockTokenFilter还有一个检查扩展Filter的注解 @ExtBeforeFilter(上面代码134行)用于新项目有时需要一些额外的过滤器,像我最近的项目iBootCMS就用到了,用来检查客户网站的apiKey,通过apiKey隔离客户网站。

3、限于篇幅,这里RockAPIFilter及RockTokenFilter等其它的实现留后来有时间再写,先说说Controller权限部分的实现。

三、用户、角色与权限
使用spring的开发者都超级熟悉,一个项目无非就是写controller、dao与 service三部分,而权限控制主要是鉴别用户是否有请求controller权利,而我们也不可能每个项目都使用硬编码,使用硬编码对日常维护及扩展都不友善的。那么应该怎么做?直接使用spring原生注解+自定义注解,如

package com.rock.frame.controller.admin;

import com.fasterxml.jackson.core.type.TypeReference;
import com.rock.common.view.ResultModel;
import com.rock.common.annotation.MenuGroupPoint;
import com.rock.common.annotation.SaveLog;
import com.rock.common.view.BaseController;
import com.rock.common.view.RESULT;
import com.rock.frame.model.Role;
import com.rock.frame.model.RoleDto;
import com.rock.frame.service.LoginUserService;
import com.rock.frame.service.RoleService;
import com.rock.utils.json.JSONUtils;
import org.springframework.web.bind.annotation.*;

import jakarta.annotation.Resource;
import java.util.List;

@MenuGroupPoint
@RestController
@RequestMapping(name = "角色权限", value = "/admin/role/")
public class RoleController extends BaseController {

    @Resource
    private RoleService roleService;

    @Resource
    private LoginUserService loginUserService;

    @GetMapping("list")
    public ResultModel<List<Role>> list(){
        return ResultModel.createInstance(RESULT.SUCCESS, this.roleService.findAll());
    }

    @GetMapping("detail")
    public ResultModel<RoleDto> detail(@ModelAttribute Role role){
        return this.success(this.roleService.detail(role));
    }

    @GetMapping("loadAdminRole")
    public ResultModel<List<Role>> loadAdminRole(){
        return this.success(this.roleService.getAdminRoles());
    }


    @GetMapping("loadUserRole")
    public ResultModel<List<Role>> loadUserRole(){
        return this.success(this.roleService.getUserRoles());
    }

    @GetMapping("loadUserRoleForC")
    public ResultModel<List<Role>> loadUserRoleForC(){
        return this.success(this.roleService.getUserRolesForC());
    }

    @SaveLog
    @PostMapping(name = "新增角色", value = "add")
    public ResultModel<?> add(@ModelAttribute Role role){
        return success(this.roleService.add(role));
    }

    @SaveLog
    @PostMapping(name = "修改角色", value = "edit")
    public ResultModel<?> edit(@ModelAttribute Role role){
        this.roleService.update(role);
        return ResultModel.createInstance(RESULT.SUCCESS, null);
    }

    @SaveLog
    @PostMapping(name = "删除角色", value = "delete")
    public ResultModel<?> delete(@ModelAttribute Role role){
        this.roleService.delete(role);
        return SUCCESS;
    }


    @SaveLog
    @PostMapping(name = "新增角色用户", value = "addUserToRole")
    public ResultModel<?> addUserToRole(@ModelAttribute Role role, String userIds){
        this.loginUserService.userAddToRole(role, userIds);
        return SUCCESS;
    }

    @SaveLog
    @PostMapping(name = "移除角色用户", value = "removeUserFrmRole")
    public ResultModel<?> removeUserFrmRole(@ModelAttribute Role role, String userId){
        this.loginUserService.removeUserFromRole(role, userId);
        return SUCCESS;
    }

    @SaveLog
    @PostMapping(name = "设置角色权限", value = "updateRolePermission")
    public ResultModel<?> updateRolePermission(@ModelAttribute Role role, String menuId, boolean allow){
        this.roleService.updateRolePermission(role, menuId, allow);
        return SUCCESS;
    }

    @SaveLog
    @PostMapping(name = "角色排序", value = "updateSort")
    public ResultModel<?> updateSort(@RequestParam String roleJson){
        List<Role> list = JSONUtils.stringToArrayList(roleJson, new TypeReference<List<Role>>() {});
        this.roleService.updateSort(list);
        return SUCCESS;
    }

}

如上面的示例代码中除了 @MenuGroupPoint 分组(可以理解成controller分组或菜单分组)、@SaveLog及方法注解(需要记录日志的接口加上)外,我直接使用@RequestMapping、@PostMapping、@GetMapping这些controller必需要的注解,只要加上name参数后,系统启动时会将其加入到数据库menus表中。加上@Deprecated时会使其从数据表中移除(这是可能调整接口名称时才使用,调整合理后再移除@Deprecated)

@MenuGroupPoint

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MenuGroupPoint {
  //分组ID,对应application.yml中的设置
    int value() default 0;
//用户类型(本系统将用户分为三个类型,0.营运管理端用户,1.C端用户,2.b端用户)
    int userType() default 0;
//该菜单项是否不可见()
    boolean invisible() default false;
//模块划分
    int module() default 0;
//同级分组
    String parentClass() default "";
}

数据表menu实做结构如下:

/**
 * 菜单项实体
 * @author: rock.chen <br />
 * @date: 2021/2/15 12:46 下午 <br />
 **/
@Getter
@Setter
@Entity
@Table(name = "sys_menu")
@JsonIgnoreProperties(value = { "hibernateLazyInitializer"}, ignoreUnknown = true)
public class Menu extends IIDModel implements ITreeModel {

    @Serial
    private static final long serialVersionUID = 8643763084655783766L;

    @Column(length = 50)
    private String name;
    @Column(length = 50)
    private String icon;
    @Column(length = 128, unique = true)
    private String uri;
    @Column(length = 128, unique = true)
    private String clsName;
    private boolean menu;
    //是否禁用,true禁用, false 可用
    private boolean disabled;
    //是否不可见,true不可见,false可见
    private boolean invisible;
    //可见方式是否为手动设置
    private boolean manualVisible;
    private int sortBy;

    private int groupId;
    /**
     * 0:营运端,1:C端, 2:B端
     */
    private int userType;
    private int module;
    //允许修改disabled 状态
    private boolean allowEditDisabled;

    //启开分割线
    private boolean divider;
    //父类(子菜单归类)
    private String parentClass;


    @ManyToMany(fetch = FetchType.EAGER, mappedBy = "menus", cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
    private List<Role> roles;

    @JsonIgnore
    @ManyToOne(cascade = CascadeType.REFRESH, fetch = FetchType.EAGER)
    @JoinColumn(name = "parentId", referencedColumnName = "id")
    private Menu parent;


    @Transient
    private String nodeId;

    @Transient
    private List<ITreeModel> children;

    @Transient
    private String level;

    @Override
    public void addChildren(ITreeModel sub){
        if(this.children == null){
            this.children = new ArrayList<>();
        }
        this.children.add(sub);
    }

}

1.实现ApplicationListener<ContextRefreshedEvent>接口,使应用在启动时做些事情。将Controller控制器注入到数据表menu中

/**
 * description:
 * Spring初始化完成后执行
 * @author:  rock.chen <br />
 * @date:  2021/2/17 12:29 上午 <br />
 **/
@Slf4j
@Component
@Transactional
public class ApplicationListenerImp extends RockApplicationListener {

    @Resource
    private MenuGroupRepository menuGroupRepository;

    @Resource
    private MenuRepository menuRepository;

    @Resource
    private LoginUserService loginUserService;

    @Resource
    private RoleRepository roleRepository;

    @Resource
    private Config config;

    private IAppSpringAfterServiceInit appSpringAfterServiceInit;

    private IExtPermissionService extPermissionService;

    @Override
    public void onApplicationEvent(@NonNull ContextRefreshedEvent contextRefreshedEvent) {
        log.info("Spring boot initialized and start initialize application data...");
        if(!config.isUriValidate()){
            log.warn("系统已关闭URI权限验证功能...");
        }
        if(!config.isRegister()){
            log.warn("系统已关闭管理端注册接口...");
        }
        if(config.isDebug()){
           log.warn("系统处于测试状态...");
        }
        if(Objects.isNull(extPermissionService)){
            this.extPermissionService = (IExtPermissionService) BeanUtils.getBean(IExtPermissionService.class);
        }
        super.onApplicationEvent(contextRefreshedEvent);
      //初始化管理端的注册接口
        initRegisterApi();
      //初始化菜单分组(读取application.yml中设置的分组)
        initMenuGroup();
      //初始化菜单项的接口(即controller中的方法,注入的数据库menu)
        initMenuFunctionData();
      //初始化系统角色及创建一个默人的营运端管理员用户admin
        initRolesAndAdmin();
      //创建一个虚拟的系统用户(只要用户信息推送-发送者)
        initVirtualSystemUser();
      //其它扩展项-实现了IAppSpringAfterServiceInit接口的项
        appSpringAfterServiceInit();
        if(this.appSpringAfterServiceInit!=null){
            this.appSpringAfterServiceInit.init();
        }
    }

    private void appSpringAfterServiceInit(){
        if(appSpringAfterServiceInit==null){
            appSpringAfterServiceInit = (IAppSpringAfterServiceInit) BeanUtils.getBean(IAppSpringAfterServiceInit.class);
        }
        //启动服务端的TcpServer服务
        ITCPServer itcpServer = (ITCPServer) BeanUtils.getBean(ITCPServer.class);
        if(itcpServer!=null){
            itcpServer.start();
        }
       //启动服务端连接MQTT服务器
        IMqttClientService mqttClientService = (IMqttClientService) BeanUtils.getBean(IMqttClientService.class);
        if(mqttClientService!=null){
            mqttClientService.connect();
        }
      //邮件服务
        MailConfig mailConfig = config.getMailConfig();
        if(mailConfig!=null){
            try {
                MailService.getInstance().initJavaMailSender(mailConfig);
            } catch (GeneralSecurityException e) {
                log.error("邮件服务初始化失败!", e);
            }
        }
    }

    private void initMenuGroup(){
        this.menuGroupRepository.deleteAll();
        List<MenuGroup> menuGroupList = config.getMenuGroup();
        this.menuGroupRepository.saveAll(menuGroupList);
    }

    /**
     * 初始化系统菜单及功能URI
     */
    private void initMenuFunctionData(){
        Map<String, Object> controllerMap = BeanUtils.getBeansByAnnotation(RequestMapping.class);
        for (Map.Entry<String, Object> entry : controllerMap.entrySet()) {
            Object controller = entry.getValue();
            if (!(controller instanceof IController)) {
                continue;
            }

            Deprecated deprecatedAnnotation = controller.getClass().getAnnotation(Deprecated.class);
            if(deprecatedAnnotation!=null){
                this.deleteParentMenu(controller);
                continue;
            }

            RequestMapping requestMappingAnnotation = controller.getClass().getAnnotation(RequestMapping.class);
            if (requestMappingAnnotation == null) {
                continue;
            }
            String parentName = requestMappingAnnotation.name();
            if (StringUtils.isBlank(parentName)) {
                this.deleteParentMenu(controller);
                continue;
            }
            MenuGroupPoint menuGroupPoint = controller.getClass().getAnnotation(MenuGroupPoint.class);
            int groupId = 0;
            int userType = ILoginUser.USER_TYPE_MGT;
            int module = 0;
            boolean invisible = true;
            String parentClass = null;
            if(menuGroupPoint!=null){
                groupId = menuGroupPoint.value();
                userType = menuGroupPoint.userType();
                invisible = menuGroupPoint.invisible();
                module = menuGroupPoint.module();
                parentClass = menuGroupPoint.parentClass();
            }
            MenuGroup menuGroup = this.menuGroupRepository.findByGid(groupId);
            if(menuGroup == null){
                throw new ServiceException("菜单组配置错误!");
            }

            boolean allowEditDisabled = controller.getClass().getAnnotation(NonEditDisabled.class)==null; //指定是否能修改菜单Disabled 字段状态
            String className = controller.getClass().getName();
            Menu parentMenu = this.menuRepository.findByClsName(className);
            String parentPath = requestMappingAnnotation.value()[0];
            if (parentMenu != null && parentMenu.getId() != null) {
                parentMenu.setName(parentName);
                parentMenu.setMenu(true);
                parentMenu.setUri(parentPath);
                parentMenu.setGroupId(groupId);
                parentMenu.setUserType(userType);
                parentMenu.setModule(module);
                parentMenu.setAllowEditDisabled(allowEditDisabled);
                parentMenu.setParentClass(parentClass);
                if(!parentMenu.isManualVisible()){
                    parentMenu.setInvisible(invisible);
                }
            } else {
                parentMenu = this.menuRepository.findByUri(parentPath);
                if (parentMenu != null && parentMenu.getId() != null) {
                    parentMenu.setName(parentName);
                    parentMenu.setMenu(true);
                    parentMenu.setClsName(className);
                    parentMenu.setGroupId(groupId);
                    parentMenu.setUserType(userType);
                    parentMenu.setModule(module);
                    parentMenu.setAllowEditDisabled(allowEditDisabled);
                    parentMenu.setParentClass(parentClass);
                    if(!parentMenu.isManualVisible()){
                        parentMenu.setInvisible(invisible);
                    }
                }else{
                    parentMenu = saveMenu(parentName, className, parentPath, true, null,
                            groupId, userType, module, allowEditDisabled, invisible, parentClass);
                }
            }

            Method[] methods = controller.getClass().getMethods();
            for (Method method : methods) {

                Deprecated deprecated = method.getAnnotation(Deprecated.class);
                if(deprecated!=null || !config.isUriValidate()){
                    deleteFunction(className + "." + method.getName());
                    continue;
                }

                NonEditDisabled nonEditDisabled1 = method.getAnnotation(NonEditDisabled.class);
                boolean allowEdit = nonEditDisabled1 == null;

                RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                if (requestMapping != null) {
                    if(StringUtils.isNotBlank(requestMapping.name()) && config.isUriValidate()){
                        createFunction(className, parentMenu, method, requestMapping.name(),parentPath + requestMapping.value()[0], groupId, userType, module, allowEdit);
                    }else{
                        deleteFunction(className + "." + method.getName());
                    }
                    continue;
                }

                GetMapping getMapping = method.getAnnotation(GetMapping.class);
                if (getMapping != null) {
                    if(StringUtils.isNotBlank(getMapping.name()) && config.isUriValidate()){
                        createFunction(className, parentMenu, method, getMapping.name(), parentPath + getMapping.value()[0], groupId, userType, module, allowEdit);
                    }else{
                        deleteFunction(className + "." + method.getName());
                    }
                    continue;
                }

                PostMapping postMapping = method.getAnnotation(PostMapping.class);
                if (postMapping != null) {
                    if(StringUtils.isNotBlank(postMapping.name()) && config.isUriValidate()){
                        createFunction(className, parentMenu, method, postMapping.name(), parentPath + postMapping.value()[0], groupId, userType, module, allowEdit);
                    }else{
                        deleteFunction(className + "." + method.getName());
                    }
                    continue;
                }

                PutMapping putMapping = method.getAnnotation(PutMapping.class);
                if (putMapping != null) {
                    if(StringUtils.isNotBlank(putMapping.name()) && config.isUriValidate()){
                        createFunction(className, parentMenu, method, putMapping.name(), parentPath + putMapping.value()[0], groupId, userType, module, allowEdit);
                    }else{
                        deleteFunction(className + "." + method.getName());
                    }
                    continue;
                }

                PatchMapping patchMapping = method.getAnnotation(PatchMapping.class);
                if (patchMapping != null) {
                    if(StringUtils.isNotBlank(patchMapping.name()) && config.isUriValidate()){
                        createFunction(className, parentMenu, method, patchMapping.name(), parentPath + patchMapping.value()[0], groupId, userType, module, allowEdit);
                    }else{
                        deleteFunction(className + "." + method.getName());
                    }
                    continue;
                }

                DeleteMapping deleteMapping = method.getAnnotation(DeleteMapping.class);
                if (deleteMapping != null) {
                    if(StringUtils.isNotBlank(deleteMapping.name()) && config.isUriValidate()){
                        createFunction(className, parentMenu, method, deleteMapping.name(), parentPath + deleteMapping.value()[0], groupId, userType, module, allowEdit);
                    }else{
                        deleteFunction(className + "." + method.getName());
                    }
                }
            }
        }
    }

    private void deleteParentMenu(Object controller) {
        String className = controller.getClass().getName();
        Menu parentMenu = this.menuRepository.findByClsName(className);
        if (parentMenu != null && parentMenu.getId() != null) {
            this.menuRepository.updateParentNull(parentMenu.getUri());
            this.menuRepository.deleteUriLike(parentMenu.getUri());
            if(Objects.nonNull(this.extPermissionService)){
                this.extPermissionService.onDeleteMenu(parentMenu);
            }
        }
    }

    private void deleteFunction(String funName){
        Menu funMenu = this.menuRepository.findByClsName(funName);
        if (funMenu != null && funMenu.getId() != null) {
            Collection<Role> roles = funMenu.getRoles();
            if(Objects.nonNull(roles)){
                roles.forEach(role -> role.getMenus().removeIf(m->m.getId().equals(funMenu.getId())));
                roles.clear();
            }
            this.menuRepository.delete(funMenu);
            if(Objects.nonNull(this.extPermissionService)){
                this.extPermissionService.onDeleteMenu(funMenu);
            }
        }
    }

    private void createFunction(String className, Menu parentMenu, Method method, String name,
                                String uri, int groupId, int userType, int module, boolean allowEditDisabled) {
        String functionName = className + "." + method.getName();
        Menu funMenu = this.menuRepository.findByClsName(functionName);
        if (funMenu != null && funMenu.getId() != null) {
            funMenu.setName(name);
            funMenu.setMenu(false);
            funMenu.setUri(uri);
            funMenu.setParent(parentMenu);
            funMenu.setGroupId(groupId);
            funMenu.setUserType(userType);
            funMenu.setModule(module);
            funMenu.setAllowEditDisabled(allowEditDisabled);
        } else {
            saveMenu(name, functionName, uri, false,
                    parentMenu, groupId, userType, module, allowEditDisabled, true, null);
        }
    }

    private Menu saveMenu(String menuName, String className, String uri,
                          boolean isMenu, Menu parent, int groupId, int userType, int module,
                          boolean allowEditDisabled, boolean invisible, String parentClass) {
        Menu menu = new Menu();
        menu.setMenu(isMenu);
        menu.setName(menuName);
        menu.setUri(uri);
        menu.setClsName(className);
        menu.setGroupId(groupId);
        menu.setUserType(userType);
        menu.setModule(module);
        menu.setAllowEditDisabled(allowEditDisabled);
        menu.setInvisible(invisible);
        menu.setParentClass(parentClass);
        if(parent!=null && parent.getId()!=null){
            menu.setParent(parent);
        }
        return this.menuRepository.save(menu);
    }

    private void initRolesAndAdmin(){
        String adminRoleName = WebSecurityConfig.ROLE_PREFIX + WebSecurityConfig.ADMIN_ROLE;
        Role adminRole = this.roleRepository.findRoleByNameAndRoleType(adminRoleName, Role.ROLE_SYS);
        if(adminRole==null){
            adminRole = new Role();
            adminRole.setName(adminRoleName);
            adminRole.setRoleType(Role.ROLE_SYS);
            this.roleRepository.save(adminRole);
        }
        String userRoleName = WebSecurityConfig.ROLE_PREFIX + WebSecurityConfig.USER_ROLE;
        Role userRole = this.roleRepository.findRoleByNameAndRoleType(userRoleName, Role.ROLE_SYS);
        if(userRole == null){
            userRole = new Role();
            userRole.setName(userRoleName);
            adminRole.setRoleType(Role.ROLE_SYS);
            this.roleRepository.save(userRole);
        }
        this.loginUserService.setToCRoleList(userRole);
        initAdminUser(adminRole);
    }

    /**
     * 初始化系统管理员
     */
    private void initAdminUser(Role role){
        LoginUser loginUser = this.loginUserService.findOneByUserName(Config.ADMIN_USER_NAME);
        if(loginUser == null){
            loginUser = new LoginUser();
            loginUser.setUsername(Config.ADMIN_USER_NAME);
            loginUser.setUserType(LoginUser.USER_TYPE_MGT);

            loginUser.setUserFrom(LoginUser.USER_TYPE_MGT);
            loginUser.setAccountType(ILoginUser.ACCOUNT_TYPE_GENERAL);

            loginUser.setSuperAdmin(true);
            loginUser.setName(Config.ADMIN_USER_NAME);
            loginUser.setPassword(LoginUserService.passwordEncoder().encode(Config.ADMIN_USER_PASSWORD));
            loginUser.setAccountNonExpired(true);
            loginUser.setAccountNonLocked(true);
            loginUser.setEnabled(true);
            loginUser.setCredentialsNonExpired(true);
            List<Role> roles = new ArrayList<>();
            roles.add(role);
            loginUser.setAuthorities(roles);
            loginUser.setCreateTime(Calendar.getInstance().getTime());
            this.loginUserService.save(loginUser);
            if(role.getLoginUsers() == null){
                role.setLoginUsers(new ArrayList<>());
            }
            role.getLoginUsers().add(loginUser);
        }
    }

    /**
     * 初始化一个虚拟的系统用户
     */
    private void initVirtualSystemUser(){
        LoginUser loginUser = this.loginUserService.findOneByUserName(ILoginUser.VIRTUAL_SYS_USER_NAME);
        if(loginUser == null){
            loginUser = new LoginUser();
            loginUser.setUsername(ILoginUser.VIRTUAL_SYS_USER_NAME);
            loginUser.setUserType(LoginUser.USER_VIRTUAL_SYS);
            loginUser.setUserFrom(LoginUser.USER_TYPE_MGT);
            loginUser.setAccountType(ILoginUser.ACCOUNT_TYPE_GENERAL);
            loginUser.setName("系统用户");
            loginUser.setSuperAdmin(false);
            loginUser.setPassword(LoginUserService.passwordEncoder().encode(CommonUtils.getRandomStr(8, 10).get(0)));
            loginUser.setAccountNonExpired(false);
            loginUser.setAccountNonLocked(false);
            loginUser.setEnabled(false);
            loginUser.setCredentialsNonExpired(false);
            loginUser.setCreateTime(Calendar.getInstance().getTime());
            this.loginUserService.save(loginUser);
        }
        UserContextHolder.putVsUser(loginUser);
    }


    private void initRegisterApi(){
        if(config.isRegister()) return;
        log.info("禁止管理端注册接口...");
        Map<String, Object> controllerMap = BeanUtils.getBeansByAnnotation(RequestMapping.class);
        for (Map.Entry<String, Object> entry : controllerMap.entrySet()) {
            Object controller = entry.getValue();
            if (!(controller instanceof IController)) {
                continue;
            }
            RequestMapping requestMappingAnnotation = controller.getClass().getAnnotation(RequestMapping.class);
            if (requestMappingAnnotation == null) {
                continue;
            }
            String parentPath = requestMappingAnnotation.value()[0];
            Method[] methods = controller.getClass().getDeclaredMethods();
            for (Method method : methods) {

                RegisterApi registerApi = method.getAnnotation(RegisterApi.class);
                if(registerApi == null) continue;

                RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                if(requestMapping != null){
                    String uri = parentPath + requestMapping.value()[0];
                    Config.REGISTER_URI_LIST.add(uri);
                    continue;
                }

                GetMapping getMapping = method.getAnnotation(GetMapping.class);
                if(getMapping!=null){
                    String uri = parentPath + getMapping.value()[0];
                    Config.REGISTER_URI_LIST.add(uri);
                    continue;
                }
                PostMapping postMapping = method.getAnnotation(PostMapping.class);
                if(postMapping!=null){
                    String uri = parentPath + postMapping.value()[0];
                    Config.REGISTER_URI_LIST.add(uri);
                    continue;
                }
                PutMapping putMapping = method.getAnnotation(PutMapping.class);
                if(putMapping!=null){
                    String uri = parentPath + putMapping.value()[0];
                    Config.REGISTER_URI_LIST.add(uri);
                    continue;
                }
                PatchMapping patchMapping = method.getAnnotation(PatchMapping.class);
                if(patchMapping!=null){
                    String uri = parentPath + patchMapping.value()[0];
                    Config.REGISTER_URI_LIST.add(uri);
                }
            }
        }
    }
}

2.菜单组配置application.yml中,格式如下

  menu-group:
    - gid: 0
      name: 设置
      icon: setting
      user_type: 0
      module : 0
    - gid: 1
      name: 团队
      icon: team
      user_type: 0
      module : 0
    - gid: 2
      name: 网站
      icon: web
      user_type: 0
      module: 1

#B端
    - gid: 10
      name: 设置
      icon: setting
      user_type: 2
      module: 1
    - gid: 12
      name: 内容
      icon: content
      user_type: 2
      module: 1
    - gid: 13
      name: 用户
      icon: member
      user_type: 2
      module: 1
    - gid: 14
      name: 评论
      icon: comment
      user_type: 2
      module: 1

经过上面的方法,Controller就方便的注入到数据库表中了,配合role,menu,rolemenu,loginUser表的关系即可完成相关的权限控制。由于时间问题本集先讲到这里,下集再见~

第一集:如何用SpringBoot打造自己的基础业务框架?权限篇1

iboot.fun

第一集:如何用SpringBoot打造自己的基础业务框架?权限篇1

后台权限管理

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
QL宋宋的头像 - 鹿快
评论 抢沙发

请登录后发表评论

    暂无评论内容