SpringBoot集成Spring Security(开启菜单权限验证)

1.添加依赖


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

2.创建User以及UserService


创建实体类UserDetailDTO类,该类需要实现UserDetails接口

@Data
@Builder
publicclassUserDetailDTOimplementsUserDetails {
​
    /**
     * 用户账号id
     */
    privateIntegerid;
​
    /**
     * 用户信息id
     */
    privateIntegeruserInfoId;
​
    /**
     * 邮箱号
     */
    privateStringemail;
​
    /**
     * 登录方式
     */
    privateIntegerloginType;
​
    /**
     * 用户名
     */
    privateStringusername;
​
    /**
     * 密码
     */
    privateStringpassword;
​
    /**
     * 用户角色
     */
    privateList<String>roleList;
​
    /**
     * 用户昵称
     */
    privateStringnickname;
​
    /**
     * 用户头像
     */
    privateStringavatar;
​
    /**
     * 用户简介
     */
    privateStringintro;
​
    /**
     * 个人网站
     */
    privateStringwebSite;
​
    /**
     * 点赞文章集合
     */
    privateSet<Object>articleLikeSet;
​
    /**
     * 点赞评论集合
     */
    privateSet<Object>commentLikeSet;
​
    /**
     * 点赞说说集合
     */
    privateSet<Object>talkLikeSet;
​
    /**
     * 用户登录ip
     */
    privateStringipAddress;
​
    /**
     * ip来源
     */
    privateStringipSource;
​
    /**
     * 是否禁用
     */
    privateIntegerisDisable;
​
    /**
     * 浏览器
     */
    privateStringbrowser;
​
    /**
     * 操作系统
     */
    privateStringos;
​
    /**
     * 最近登录时间
     */
    privateLocalDateTimelastLoginTime;
​
​
    @Override
    publicCollection<?extendsGrantedAuthority>getAuthorities() {
        returnthis.roleList.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toSet());
    }
​
    @Override
    publicStringgetPassword() {
        returnthis.password;
    }
​
    @Override
    publicStringgetUsername() {
        returnthis.username;
    }
​
    @Override
    publicbooleanisAccountNonExpired() {
        returntrue;
    }
​
    @Override
    publicbooleanisAccountNonLocked() {
        returnthis.isDisable==FALSE;
    }
​
    @Override
    publicbooleanisCredentialsNonExpired() {
        returntrue;
    }
​
    @Override
    publicbooleanisEnabled() {
        returntrue;
    }
}

UserDetails中有一个方法叫做getAuthorities,该方法用来获取当前用户所具有的角色,但是UserDetailDTO中有一个roleList属性用来描述当前用户的角色,因此getAuthorities方法的实现如上,直接从roleList中获取当前用户所具有的角色,构造SimpleGrantedAuthority然后返回即可

创建好UserDetailDTO之后,需要创建UserDetailsServiceImpl,用来执行登录等操作,UserDetailsServiceImpl需要实现UserDetailsService接口

@Service
publicclassUserDetailsServiceImplimplementsUserDetailsService {
    @Autowired
    privateUserAuthMapperuserAuthMapper;
    @Autowired
    privateUserInfoMapperuserInfoMapper;
    @Autowired
    privateRoleMapperroleMapper;
    @Autowired
    privateRedisServiceredisService;
    @Resource
    privateHttpServletRequestrequest;
    @Override
    publicUserDetailsloadUserByUsername(Stringusername) throwsUsernameNotFoundException {
        if(StringUtils.isBlank(username)){
            thrownewBizException("用户名不能为空");
        }
        //查询账号是否存在
        UserAuthuserAuth=userAuthMapper.selectOne(newLambdaQueryWrapper<UserAuth>()
                .select(UserAuth::getId, UserAuth::getUserInfoId, UserAuth::getUsername, UserAuth::getPassword, UserAuth::getLoginType)
                .eq(UserAuth::getUsername, username));
        if(Objects.isNull(userAuth)){
            thrownewBizException("账号不存在");
        }
        // 封装登录信息
        returnconvertUserDetail(userAuth, request);
    }
    /**
     * 封装用户登录信息
     *
     * @param userAuth    用户账号
     * @param request 请求
     * @return 用户登录信息
     */
    privateUserDetailsconvertUserDetail(UserAuthuserAuth, HttpServletRequestrequest) {
        // 查询账号信息
        UserInfouserInfo=userInfoMapper.selectById(userAuth.getUserInfoId());
        // 查询账号角色
        List<String>roleList=roleMapper.listRolesByUserInfoId(userInfo.getId());
        System.out.println(roleList);
        // 查询账号点赞信息
        Set<Object>articleLikeSet=redisService.sMembers(ARTICLE_USER_LIKE+userInfo.getId());
        Set<Object>commentLikeSet=redisService.sMembers(COMMENT_USER_LIKE+userInfo.getId());
        Set<Object>talkLikeSet=redisService.sMembers(TALK_USER_LIKE+userInfo.getId());
        // 获取设备信息
        StringipAddress=IpUtils.getIpAddress(request);
        StringipSource=IpUtils.getIpSource(ipAddress);
        UserAgentuserAgent=IpUtils.getUserAgent(request);
        // 封装权限集合
        returnUserDetailDTO.builder()
                .id(userAuth.getId())
                .loginType(userAuth.getLoginType())
                .userInfoId(userInfo.getId())
                .username(userAuth.getUsername())
                .password(userAuth.getPassword())
                .email(userInfo.getEmail())
                .roleList(roleList)
                .nickname(userInfo.getNickname())
                .avatar(userInfo.getAvatar())
                .intro(userInfo.getIntro())
                .webSite(userInfo.getWebSite())
                .articleLikeSet(articleLikeSet)
                .commentLikeSet(commentLikeSet)
                .talkLikeSet(talkLikeSet)
                .ipAddress(ipAddress)
                .ipSource(ipSource)
                .isDisable(userInfo.getIsDisable())
                .browser(userAgent.getBrowser().getName())
                .os(userAgent.getOperatingSystem().getName())
                .lastLoginTime(LocalDateTime.now(ZoneId.of(SHANGHAI.getZone())))
                .build(); 
    }
}

这里实现了UserDetailsService接口中的loadUserByUsername方法,在执行登录的过程中,这个方法将根据用户名去查找用户,如果用户名不存在,则抛出异常,用户名存在的话,需要封装用户登陆信息并返回。

3.自定义FilterInvocationSecurityMetadataSource


FilterInvocationSecurityMetadataSource有一个默认的实现类DefaultFilterInvocationSecurityMetadataSource,该类的主要功能就是通过当前的请求地址,获取该地址需要的用户角色。可以自己也定义一个FilterInvocationSecurityMetadataSource实现同样的功能

/**
* 接口拦截规则
*/
@Component
publicclassFilterInvocationSecurityMetadataSourceImplimplementsFilterInvocationSecurityMetadataSource {
​
    /**
     * 资源角色列表
     */
    privatestaticList<ResourceRoleDTO>resourceRoleList;
​
    @Autowired
    privateRoleMapperroleMapper;
​
    /**
     * 加载资源角色信息
     */
    @PostConstruct
    privatevoidloadDataSource() {
        resourceRoleList=roleMapper.listResourceRoles();
    }
​
    /**
     * 清空接口角色信息
     */
    publicvoidclearDataSource() {
        resourceRoleList=null;
    }
​
    @Override
    publicCollection<ConfigAttribute>getAttributes(Objectobject) throwsIllegalArgumentException {
        // 修改接口角色关系后重新加载
        if (CollectionUtils.isEmpty(resourceRoleList)) {
            this.loadDataSource();
        }
        FilterInvocationfi= (FilterInvocation) object;
        // 获取用户请求方式
        Stringmethod=fi.getRequest().getMethod();
        // 获取用户请求Url
        Stringurl=fi.getRequest().getRequestURI();
        AntPathMatcherantPathMatcher=newAntPathMatcher();
        // 获取接口角色信息,若为匿名接口则放行,若无对应角色则禁止
        for (ResourceRoleDTOresourceRoleDTO : resourceRoleList) {
            if (antPathMatcher.match(resourceRoleDTO.getUrl(), url) &&resourceRoleDTO.getRequestMethod().equals(method)) {
                List<String>roleList=resourceRoleDTO.getRoleList();
                if (CollectionUtils.isEmpty(roleList)) {
                    returnSecurityConfig.createList("disable");
                }
                returnSecurityConfig.createList(roleList.toArray(newString[]{}));
            }
        }
        returnnull;
    }
​
    @Override
    publicCollection<ConfigAttribute>getAllConfigAttributes() {
        returnnull;
    }
​
    @Override
    publicbooleansupports(Class<?>aClass) {
        returnFilterInvocation.class.isAssignableFrom(aClass);
    }
​
}
​

如果getAttributes(Object o)方法返回null的话,意味着当前这个请求不需要任何角色就能访问,甚至不需要登录。我这个项目中有匿名接口,匿名接口不需要任何角色,可以直接访问。

不是匿名接口的话,将访问接口需要的角色信息封装成Collection<ConfigAttribute>并返回,getAttributes(Object o)方法返回的集合最终会来到AccessDecisionManager类中,接下来我们再来看AccessDecisionManager类

4.自定义AccessDecisionManager


自定义AccessDecisionManagerImpl类实现AccessDecisionManager接口

/**
* 访问决策管理器
*/
@Component
publicclassAccessDecisionManagerImplimplementsAccessDecisionManager {
    @Override
    publicvoiddecide(Authenticationauthentication, Objecto, Collection<ConfigAttribute>collection) throwsAccessDeniedException, InsufficientAuthenticationException {
        // 获取当前登录用户权限列表
        List<String>permissionList=authentication.getAuthorities()
                .stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList());
​
        for (ConfigAttributeitem : collection) {
            if (permissionList.contains(item.getAttribute())) {
                return;
            }
        }
        thrownewAccessDeniedException("没有操作权限");
    }
​
    @Override
    publicbooleansupports(ConfigAttributeconfigAttribute) {
        returntrue;
    }
​
    @Override
    publicbooleansupports(Class<?>aClass) {
        returntrue;
    }
}

这里涉及到一个all和any的问题:假设当前登录用户具备角色A、角色B,当前请求需要角色B、角色C,那么是要当前用户要包含所有请求角色才算授权成功,还是只要包含一个就算授权成功?这里采用了第二种方案,即只要包含一个即可。可根据自己的实际情况调整decide方法中的逻辑。

5.自定义AccessDeniedHandler


/**
* 用户权限处理
*
​
*/
@Component
publicclassAccessDeniedHandlerImplimplementsAccessDeniedHandler {
​
    @Override
    publicvoidhandle(HttpServletRequesthttpServletRequest, HttpServletResponsehttpServletResponse, AccessDeniedExceptione) throwsIOException {
        httpServletResponse.setContentType(APPLICATION_JSON);
        httpServletResponse.getWriter().write(JSON.toJSONString(Result.fail("权限不足")));
    }
​
}

6.配置WebSecurityConfig


/**
* securiy配置类
*
*/
@Configuration
@EnableWebSecurity
publicclassWebSecurityConfigextendsWebSecurityConfigurerAdapter {
​
    @Autowired
    privateAuthenticationSuccessHandlerImplauthenticationSuccessHandler;
    @Autowired
    privateAuthenticationFailHandlerImplauthenticationFailHandler;
    @Autowired
    privateLogoutSuccessHandlerImpllogoutSuccessHandler;
    @Autowired
    privateAuthenticationEntryPointImplauthenticationEntryPoint;
    @Autowired
    privateAccessDeniedHandleraccessDeniedHandler;
​
    @Bean
    publicFilterInvocationSecurityMetadataSourcesecurityMetadataSource(){
        returnnewFilterInvocationSecurityMetadataSourceImpl();
    }
    @Bean
    publicAccessDecisionManageraccessDecisionManager(){
        returnnewAccessDecisionManagerImpl();
    }
    @Bean
    publicSessionRegistrysessionRegistry() {
        returnnewSessionRegistryImpl();
    }
​
    //防止用户重复登录
    @Bean
    publicHttpSessionEventPublisherhttpSessionEventPublisher() {
        returnnewHttpSessionEventPublisher();
    }
    /**
     * 密码加密
     *
     * @return {@link PasswordEncoder} 加密方式
     */
    @Bean
    publicPasswordEncoderpasswordEncoder() {
        returnnewBCryptPasswordEncoder();
    }
​
    /**
     * 配置权限
     * @param http
     * @throws Exception
     */
    @Override
    protectedvoidconfigure(HttpSecurityhttp) throwsException {
        //配置登录注销的路径
        http.formLogin()
                .loginProcessingUrl("/login")
                .successHandler(authenticationSuccessHandler)
                .failureHandler(authenticationFailHandler)
                .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessHandler(logoutSuccessHandler);
​
        // 配置路由权限信息
        http.authorizeRequests()
        /*
        配置路由权限信息
        通过withObjectPostProcessor将刚刚创建的FilterInvocationSecurityMetadataSourceImpl
        和AccessDecisionManagerImpl注入进来。到时候,请求都会经过刚才的过滤器(
        除了configure(WebSecurity web)方法忽略的请求)
         */
                .withObjectPostProcessor(newObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public<OextendsFilterSecurityInterceptor>OpostProcess(Ofsi) {
                        fsi.setSecurityMetadataSource(securityMetadataSource());
                        fsi.setAccessDecisionManager(accessDecisionManager());
                        returnfsi;
                    }
                })
            
                //这里permitAll一定要加,否则启动会报错
                .anyRequest().permitAll()
                .and()
                .csrf().disable().exceptionHandling()
                //未登录处理
                .authenticationEntryPoint(authenticationEntryPoint)
                //权限不足处理
                .accessDeniedHandler(accessDeniedHandler)
                .and()
                .sessionManagement()
                .maximumSessions(20)
                .sessionRegistry(sessionRegistry());
    }
}

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值