SpringBoot 动态权限校验,优雅的实现方案

仿牛客项目SpringSecurity学习 AuthenticationManager(接口)是认证相关的核心接口,也是发起认证的出发点,因为在实际需求中,我们可能会允许用户使用用户名+密码登录,同时允许用户使用邮箱+密码,手机号码+密码登录,甚至,可能允许用户使用指纹登录(还有这样的操作?对于权限的控制是比较靠前的。getPrincipal(),最重要的身份信息,大部分情况下返回的是UserDetails接口的实现类,也是框架中的常用接口之一。由这个顶级接口,我们可以得到用户拥有的权限信息列表,密码,用户细节信息,用户身份信息,认证信息。 阅读详情

👉 这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料: 

86c7dd1dfb64a69099e03406c7bd7ce4.gif

👉这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号、CRM 等等功能:

  • Boot 仓库:https://gitee.com/zhijiantianya/ruoyi-vue-pro

  • Cloud 仓库:https://gitee.com/zhijiantianya/yudao-cloud

  • 视频教程:https://doc.iocoder.cn

【国内首批】支持 JDK 21 + SpringBoot 3.2.2、JDK 8 + Spring Boot 2.7.18 双版本 

来源:blog.youkuaiyun.com/mofsfely2
/article/details/113756506

b4feb100881d15e8e3513d12cfb62c4e.jpeg


1 背景

简单先说一下需求吧,这样也好让看的人知道到底适不适合自己。

  1. 实现自定义的登录认证。

  2. 登录成功,生成token并将token 交由redis管理。

  3. 登录后对用户访问的接口进行接口级别权限认证。

springSecurity提供的注解权限校验适合的场景是系统中仅有固定的几个角色,且角色的凭证不可修改(如果修改需要改动代码)。

@PreAuthorize("hasAuthority('ROLE_TELLER')") 
public Account post(Account account, double amount);

注:ROLE_TELLER是写死的。

后端系统的访问请求有以下几种类型:

  • 登录、登出(可自定义url)

  • 匿名用户可访问的接口(静态资源,demo示例等)

  • 其他接口(在登录的前提下,继续判断访问者是否有权限访问)

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

2 环境搭建

依赖引入,包括springSecurity、redis、redis session需要的依赖:

<!--springSecurity安全框架-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.3.4.RELEASE</version>
</dependency>
<!-- 默认通过SESSIONId改为通过请求头与redis配合验证session -->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
    <version>2.3.1.RELEASE</version>
</dependency>
<!--redis支持-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.3.4.RELEASE</version>
</dependency>

注:springBoot版本也是2.3.4.RELEASE,如果有版本对应问题,自行解决。有用到swagger,为了便于测试。

新建springSecurity配置类

新建 WebSecurityConfig.java 继承自 WebSecurityConfigurerAdapter,过滤匿名用户可访问的接口。

WebSecurityConfig作为springSecurity的主配置文件。

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    /**
     * Swagger等静态资源不进行拦截
     */
    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers(
                "/*.html",
                "/favicon.ico",
                "/**/*.html",
                "/**/*.css",
                "/**/*.js",
                "/error",
                "/webjars/**",
                "/resources/**",
                "/swagger-ui.html",
                "/swagger-resources/**",
                "/v2/api-docs");
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                //配置一些不需要登录就可以访问的接口
                .antMatchers("/demo/**", "/about/**").permitAll()
                //任何尚未匹配的URL只需要用户进行身份验证
                .anyRequest().authenticated()
                .and()
                .formLogin()//允许用户进行基于表单的认证
                .loginPage("/mylogin");
    }

}
649b88357c7819d3473ea0d2259f212b.png

注:证明可以访问静态资源不会被拦截

自定义登录认证

springSecurity是基于过滤器进行安全认证的。

我们需要自定义:

  1. 登录过滤器 :负责过滤登录请求,再交由自定义的登录认证管理器处理。

  2. 登录成功处理类 :顾名思义,登录成功后的一些处理(设置返回信息提示“登录成功!”,返回数据类型为json)。

  3. 登录失败处理类 :类似登录成功处理类。Ps:登录成功处理类和失败处理类有默认的实现可以不自定义。但是建议自定义,因为返回的信息为英文,一般情况不符合要求。

  4. 登录认证管理器 :根据过滤器传过来的登录参数,进行登录认证,认证后授权。

新建登录成功处理类

需要实现 AuthenticationSuccessHandler

@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(CustomAuthenticationSuccessHandler.class);

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
        //登录成功返回的认证体,具体格式在后面的登录认证管理器中
        String responseJson = JackJsonUtil.object2String(ResponseFactory.success(authentication));
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("登录成功!");
        }
        response.getWriter().write(responseJson);
    }
}
新建登录失败处理类

实现 AuthenticationFailureHandler

@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(CustomAuthenticationFailureHandler.class);

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
        String errorMsg;
        if (StringUtils.isNotBlank(e.getMessage())) {
            errorMsg = e.getMessage();
        } else {
            errorMsg = CodeMsgEnum.LOG_IN_FAIL.getMsg();
        }
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
        String responseJson = JackJsonUtil.object2String(ResponseFactory.fail(CodeMsgEnum.LOG_IN_FAIL,errorMsg));
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("认证失败!");
        }
        response.getWriter().write(responseJson);
    }

}

新建登录认证管理器

实现 AuthenticationProvider ,负责具体的身份认证(一般数据库认证,在登录过滤器过滤掉请求后传入)

@Component
public class UserVerifyAuthenticationProvider implements AuthenticationProvider {

    private PasswordEncoder passwordEncoder;
    @Autowired
    private UserService userService;
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String userName = (String) authentication.getPrincipal(); // Principal 主体,一般指用户名
        String passWord = (String) authentication.getCredentials(); //Credentials 网络凭证,一般指密码
        //通过账号去数据库查询用户以及用户拥有的角色信息
        UserRoleVo userRoleVo = userService.findUserRoleByAccount(userName);
        //数据库密码
        String encodedPassword = userRoleVo.getPassWord();
        //credentials凭证即为前端传入密码,因为前端一般用Base64加密过所以需要解密。
        String credPassword = new String(Base64Utils.decodeFromString(passWord), StandardCharsets.UTF_8);
        // 验证密码:前端明文,数据库密文
        passwordEncoder = new MD5Util();
        if (!passwordEncoder.matches(credPassword, encodedPassword)) {
            throw new AuthenticationServiceException("账号或密码错误!");
        }
        //ps:GrantedAuthority对认证主题的应用层面的授权,含当前用户的权限信息,通常使用角色表示
        List<GrantedAuthority> roles = new LinkedList<>();
        List<Role> roleList = userRoleVo.getRoleList();
        roleList.forEach(role -> {
            SimpleGrantedAuthority roleId = new SimpleGrantedAuthority(role.getRoleId().toString());
            roles.add(roleId);
        });
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userName, passWord, roles);
        token.setDetails(userRoleVo);//这里可以放用户的详细信息
        return token;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return false;
    }
}

新建登录过滤器

LoginFilter.java继承UsernamePasswordAuthenticationFilter,负责过滤登录请求并交由登录认证管理器进行具体的认证。

public class LoginFilter extends UsernamePasswordAuthenticationFilter {

    private UserVerifyAuthenticationProvider authenticationManager;

    /**
     * @param authenticationManager 认证管理器
     * @param successHandler 认证成功处理类
     * @param failureHandler 认证失败处理类
     */
    public LoginFilter(UserVerifyAuthenticationProvider authenticationManager,
                       CustomAuthenticationSuccessHandler successHandler,
                       CustomAuthenticationFailureHandler failureHandler) {
        //设置认证管理器(对登录请求进行认证和授权)
        this.authenticationManager = authenticationManager;
        //设置认证成功后的处理类
        this.setAuthenticationSuccessHandler(successHandler);
        //设置认证失败后的处理类
        this.setAuthenticationFailureHandler(failureHandler);
        //可以自定义登录请求的url
        super.setFilterProcessesUrl("/myLogin");
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            //转换请求入参
            UserDTO loginUser = new ObjectMapper().readValue(request.getInputStream(), UserDTO.class);
            //入参传入认证管理器进行认证
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(loginUser.getUserName(), loginUser.getPassWord())
            );
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}

最后配置到WebSecurityConfig中:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserVerifyAuthenticationProvider authenticationManager;//认证用户类

    @Autowired
    private CustomAuthenticationSuccessHandler successHandler;//登录认证成功处理类

    @Autowired
    private CustomAuthenticationFailureHandler failureHandler;//登录认证失败处理类

    /**
     * Swagger等静态资源不进行拦截
     */
    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers(
                "/*.html",
                "/favicon.ico",
                "/**/*.html",
                "/**/*.css",
                "/**/*.js",
                "/error",
                "/webjars/**",
                "/resources/**",
                "/swagger-ui.html",
                "/swagger-resources/**",
                "/v2/api-docs");
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                //配置一些不需要登录就可以访问的接口
                .antMatchers("/demo/**", "/about/**").permitAll()
                //任何尚未匹配的URL只需要用户进行身份验证
                .anyRequest().authenticated()
                .and()
                //配置登录过滤器
                .addFilter(new LoginFilter(authenticationManager, successHandler, failureHandler))
                .csrf().disable();
    }

}
验证配置

访问登录请求:

988826b931ee7e18d21f72456e7f0348.png

成功进入LoginFilter

ed4a9628b18a903e413d60a300ebb1f9.png

安全头和登录返回token

依赖已经引入了,设置session由redis存储,只需要如下图配置:

2f10034c4e17a5301c596b1d62cd9056.png
session:
    store-type: redis
    redis:
      namespace: spring:session:admin
    # session 无操作失效时间 30 分钟
    timeout: 1800

设置token放入返回的header中需要在WebSecurityConfig中加入

/**
 * 配置 HttpSessionIdResolver Bean
 * 登录之后将会在 Response Header x-auth-token 中 返回当前 sessionToken
 * 将token存储在前端 每次调用的时候 Request Header x-auth-token 带上 sessionToken
 */
@Bean
public HttpSessionIdResolver httpSessionIdResolver() {
    return HeaderHttpSessionIdResolver.xAuthToken();
}

关于安全头信息可以参考:

  • https://docs.spring.io/spring-security/site/docs/5.2.1.BUILD-SNAPSHOT/reference/htmlsingle/#ns-headers

设置安全请求头需要设置WebSecurityConfig中加入

c733d3253f016eb9ed3bad25903bbd60.png
protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                //配置一些不需要登录就可以访问的接口
                .antMatchers("/demo/**", "/about/**").permitAll()
                //任何尚未匹配的URL只需要用户进行身份验证
                .anyRequest().authenticated()
                .and()
                //配置登录过滤器
                .addFilter(new LoginFilter(authenticationManager, successHandler, failureHandler))
                .csrf().disable();
        //配置头部
        http.headers()
                .contentTypeOptions()
                .and()
                .xssProtection()
                .and()
                //禁用缓存
                .cacheControl()
                .and()
                .httpStrictTransportSecurity()
                .and()
                //禁用页面镶嵌frame劫持安全协议  // 防止iframe 造成跨域
                .frameOptions().disable();
    }

进行登录测试,验证结果:

f4e4c5d69fd1421e06f5920f4bd3d9e6.png

注:响应中有token

查看redis。成功保存进了redis

6964cb8231b17fb1ddd32bb5855cdb59.png

接口权限校验

方式一:

如下图,详细请看链接。

f71120f4f93fd9d8b2b6f017de0b4df6.png

https://blog.youkuaiyun.com/coolwindd/article/details/104640289/

注:不使用这种方式,原因在于,需要自己判断是否匿名用户。

方法二:

参考

https://blog.youkuaiyun.com/mapleleafforest/article/details/106637052

Spring Security使用FilterSecurityInterceptor过滤器来进行URL权限校验,实际使用流程大致如下:

正常情况的接口权限判断:

返回那些可以访问当前url的角色

1、定义一个MyFilterInvocationSecurityMetadataSource实现FilterInvocationSecurityMetadataSource的类,重写getAttributes方法。

方法的作用是:返回哪些角色可以访问当前url,这个肯定是从数据库中获取。要注意的是对于PathVariable传参的url,数据库中存的是这样的:/getUserByName/{name}。但实际访问的url中name是具体的值。类似的/user/getUserById 也要可以匹配 /user/getUserById?1。

package com.aliyu.security.provider;/**
 * @author: aliyu
 * @create: 2021-02-05 14:53
 * @description:
 */

import com.aliyu.service.role.RoleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
 *@author: aliyu
 *@create:
 *@description: 第一步:数据库查询所有权限出来:
 * 之所以要所有权限,因为数据库url和实际请求url并不能直接匹配需要。比方:/user/getUserById 匹配 /user/getUserById?1
 * 第二步:通过httpUrl匹配器找出允许访问当前请求的角色列表(哪些角色可以访问此请求)
 */
@Component
public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    @Autowired
    private RoleService roleService;

    /**
     * 返回当前URL允许访问的角色列表
     * @param object
     * @return
     * @throws IllegalArgumentException
     */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        //入参转为HttpServletRequest
        FilterInvocation fi = (FilterInvocation) object;
        HttpServletRequest request = fi.getRequest();
        //从数据库中查询系统所有的权限,格式为<"权限url","能访问url的逗号分隔的roleid">
        List<Map<String, String>> allUrlRoleMap = roleService.getAllUrlRoleMap();
        for (Map<String, String> urlRoleMap : allUrlRoleMap) {
            String url = urlRoleMap.get("url");
            String roles = urlRoleMap.get("roles");
            //new AntPathRequestMatcher创建httpUrl匹配器:里面url匹配规则已经给我们弄好了,
            // 能够支持校验PathVariable传参的url(例如:/getUserByName/{name})
            // 也能支持 /user/getUserById 匹配 /user/getUserById?1
            AntPathRequestMatcher matcher = new AntPathRequestMatcher(url);
            if (matcher.matches(request)){ //当前请求与httpUrl匹配器进行匹配
                return SecurityConfig.createList(roles.split(","));
            }
        }
        return null;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

注:别人是初始化的时候加载所有权限,一次就好了。我的是每次请求都会去重新加载系统所有权限,好处就是不用担心权限修改的问题。

判断当前用户是否拥有访问当前url的角色

定义一个MyAccessDecisionManager:通过实现AccessDecisionManager接口自定义一个决策管理器,判断是否有访问权限。上一步MyFilterInvocationSecurityMetadataSource中返回的当前请求可以的访问角色列表会传到这里的decide方法里面(如果没有角色的话,不会进入decide方法。

正常情况你访问的url必然和某个角色关联,如果没有关联就不应该可以访问)。decide方法传了当前登录用户拥有的角色,通过判断用户拥有的角色中是否有一个角色和当前url可以访问的角色匹配。如果匹配,权限校验通过。

package com.aliyu.security.provider;/**
 * @author: aliyu
 * @create: 2021-02-05 15:16
 * @description:
 */
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.FilterInvocation;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.Iterator;

/**
 *@author: aliyu
 *@create:
 *@description: 接口权限判断(根据MyFilterInvocationSecurityMetadataSource获取到的请求需要的角色
 * 和当前登录人的角色进行比较)
 */
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {

    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        //循环请求需要的角色,只要当前用户拥有的角色中包含请求需要的角色中的一个,就算通过。
        Iterator<ConfigAttribute> iterator = configAttributes.iterator();
        while(iterator.hasNext()){
            ConfigAttribute configAttribute = iterator.next();
            String needCode = configAttribute.getAttribute();
            //获取到了登录用户的所有角色
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if (StringUtils.equals(authority.getAuthority(), needCode)) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("当前访问没有权限");
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return false;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}
处理匿名用户访问无权限资源

1、定义一个CustomAuthenticationEntryPoint实现AuthenticationEntryPoint处理匿名用户访问无权限资源(可以理解为未登录的用户访问,确实有些接口是可以不登录也能访问的,比较少,我们在WebSecurityConfig已经配置过了。如果多的话,需要另外考虑从数据库中获取,并且权限需要加一个标志它为匿名用户可访问)。

package com.aliyu.security.handler;

import com.aliyu.common.util.JackJsonUtil;
import com.aliyu.entity.common.vo.ResponseFactory;
import com.aliyu.security.constant.MessageConstant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

import static com.aliyu.entity.common.exception.CodeMsgEnum.MOVED_PERMANENTLY;

/**
 * 未登录重定向处理器
 * <p>
 * 未登录状态下访问需要登录的接口
 *
 * @author
 */
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private static final Logger LOGGER = LoggerFactory.getLogger(CustomAuthenticationEntryPoint.class);


    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
        //原来不需要登录的接口,现在需要登录了,所以叫永久移动
        String message = JackJsonUtil.object2String(
                ResponseFactory.fail(MOVED_PERMANENTLY, MessageConstant.NOT_LOGGED_IN)
        );
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("未登录重定向!");
        }
        response.getWriter().write(message);
    }

}
处理登陆认证过的用户访问无权限资源

2、定义一个CustomAccessDeniedHandler 实现AccessDeniedHandler处理登陆认证过的用户访问无权限资源。

package com.aliyu.security.handler;

import com.aliyu.common.util.JackJsonUtil;
import com.aliyu.entity.common.exception.CodeMsgEnum;
import com.aliyu.entity.common.vo.ResponseFactory;
import com.aliyu.security.constant.MessageConstant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;


/**
 * 拒绝访问处理器(登录状态下,访问没有权限的方法时会进入此处理器)
 *
 * @author
 */
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(CustomAccessDeniedHandler.class);

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
        String message = JackJsonUtil.object2String(
                ResponseFactory.fail(CodeMsgEnum.UNAUTHORIZED, MessageConstant.NO_ACCESS)
        );
        if(LOGGER.isDebugEnabled()){
            LOGGER.debug("没有权限访问!");
        }
        response.getWriter().write(message);
    }


}

配置到WebSecurityConfig上面去

@Autowired
private MyFilterInvocationSecurityMetadataSource securityMetadataSource;//返回当前URL允许访问的角色列表
@Autowired
private MyAccessDecisionManager accessDecisionManager;//除登录登出外所有接口的权限校验
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
     @Override
     public <O extends FilterSecurityInterceptor> O postProcess(O object) {
         object.setAccessDecisionManager(accessDecisionManager);
         object.setSecurityMetadataSource(securityMetadataSource);
         return object;
     }
 })
//用来解决匿名用户访问无权限资源时的异常
.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())
//用来解决登陆认证过的用户访问无权限资源时的异常
.accessDeniedHandler(new CustomAccessDeniedHandler())

完整的Java类:

package com.aliyu.security.config;

import com.aliyu.filter.LoginFilter;
import com.aliyu.security.handler.*;
import com.aliyu.security.provider.MyAccessDecisionManager;
import com.aliyu.security.provider.MyFilterInvocationSecurityMetadataSource;
import com.aliyu.security.provider.UserVerifyAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.session.web.http.HeaderHttpSessionIdResolver;
import org.springframework.session.web.http.HttpSessionIdResolver;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserVerifyAuthenticationProvider authenticationManager;//认证用户类

    @Autowired
    private CustomAuthenticationSuccessHandler successHandler;//登录认证成功处理类

    @Autowired
    private CustomAuthenticationFailureHandler failureHandler;//登录认证失败处理类

    @Autowired
    private MyFilterInvocationSecurityMetadataSource securityMetadataSource;//返回当前URL允许访问的角色列表
    @Autowired
    private MyAccessDecisionManager accessDecisionManager;//除登录登出外所有接口的权限校验

    /**
     * 密码加密
     * @return
     */
    @Bean
    @ConditionalOnMissingBean(PasswordEncoder.class)
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 配置 HttpSessionIdResolver Bean
     * 登录之后将会在 Response Header x-auth-token 中 返回当前 sessionToken
     * 将token存储在前端 每次调用的时候 Request Header x-auth-token 带上 sessionToken
     */
    @Bean
    public HttpSessionIdResolver httpSessionIdResolver() {
        return HeaderHttpSessionIdResolver.xAuthToken();
    }
    /**
     * Swagger等静态资源不进行拦截
     */
    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers(
                "/*.html",
                "/favicon.ico",
                "/**/*.html",
                "/**/*.css",
                "/**/*.js",
                "/error",
                "/webjars/**",
                "/resources/**",
                "/swagger-ui.html",
                "/swagger-resources/**",
                "/v2/api-docs");
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                //配置一些不需要登录就可以访问的接口
                .antMatchers("/demo/**", "/about/**").permitAll()
                //任何尚未匹配的URL只需要用户进行身份验证
                .anyRequest().authenticated()
                //登录后的接口权限校验
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        object.setAccessDecisionManager(accessDecisionManager);
                        object.setSecurityMetadataSource(securityMetadataSource);
                        return object;
                    }
                })
                .and()
                //配置登出处理
                .logout().logoutUrl("/logout")
                .logoutSuccessHandler(new CustomLogoutSuccessHandler())
                .clearAuthentication(true)
                .and()
                //用来解决匿名用户访问无权限资源时的异常
                .exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())
                //用来解决登陆认证过的用户访问无权限资源时的异常
                .accessDeniedHandler(new CustomAccessDeniedHandler())
                .and()
                //配置登录过滤器
                .addFilter(new LoginFilter(authenticationManager, successHandler, failureHandler))
                .csrf().disable();
        //配置头部
        http.headers()
                .contentTypeOptions()
                .and()
                .xssProtection()
                .and()
                //禁用缓存
                .cacheControl()
                .and()
                .httpStrictTransportSecurity()
                .and()
                //禁用页面镶嵌frame劫持安全协议  // 防止iframe 造成跨域
                .frameOptions().disable();
    }
}

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud

  • 视频教程:https://doc.iocoder.cn/video/

3其他

测试结果我就懒得写了,就这样了。

特别的,我们认为如果一个接口属于当前系统,那么它就应该有对应可以访问的角色。这样的接口才会被我们限制住。如果一个接口只是在当前系统定义了,而没有指明它的角色,这样的接口是不会被我们限制的。

注意点

下面的代码,本意是想配置一些不需要登录也可以访问的接口。

cd3488c121ebcc3b02c951bc3a0b0034.png

但是测试的时候发现,任何接口的调用都会进入这里MyFilterInvocationSecurityMetadataSource getAttriButes方法,包括我webSecurityConfig里配置的不需要登录的url。结果就是不需要登录的url和没有配置角色的接口权限一样待遇,要么都能访问,要么都不能访问!!!

b46d209a98072a15026235aecf52c629.png

所以如上图,我在这里配置了不需要登录的接口(因为不知道如何从webSercurityConfig中获取,干脆就配置在这里了),去掉了webSercurityConfig中的相应配置。


欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,“长按”或“扫描”下方二维码噢:

90d975bb2ed0cd0b2dee549385c83bbe.png

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

91cb61b7340dc1070e35d24e2a0962f9.png

f03865baeaed3d32d2da1105158a9396.png61ada3640f093935cd2c70a19d756739.png6312fcef136e2a93f8badde4d6bf71ea.png5b81682f4d613ac6a5085bc288057b54.png

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
SpringBoot文件下载实现与优化指南 文件下载是Web开发中的基础功能,涉及HTTP协议、流式传输等核心技术。通过合理设置响应头(Content-Type、Content-Disposition)和采用流式IO,可以确保文件传输的可靠性和性能。在Java生态中,SpringBoot提供了HttpServletResponse和ResponseEntity两种实现方式,特别在处理大文件时需要注意内存优化和断点续传。实际开发中常遇到中文乱码、跨域等问题,需要结合RFC规范进行编码处理。该技术广泛应用于报表导出、内容分发等场景,是构建企业级应用的重要 阅读详情

相关推荐

SpringBoot+AOP+自定义注解,轻松实现权限校验

本文详细介绍了如何利用SpringBoot、AOP和自定义注解构建一套轻量、灵活且与业务代码解耦的权限校验系统。通过定义@RequiresAuth注解并结合AOP切面统一处理校验逻辑,开发者可以告别在业务方法中硬编码权限判断,实现代码的清晰分离与高效维护。该方法支持权限、角色的AND/OR逻辑校验,并能扩展支持SpEL表达式以满足动态权限需求,是提升后端开发效率的优雅实践。

weixin_29234239的博客 326

SpringBoot+Vue前后端分离,使用SpringSecurity完美处理权限问题(一)

当前后端分离时,权限问题的处理也和我们传统的处理方式有一点差异。笔者前几天刚好在负责一个项目的权限管理模块,现在权限管理模块已经做完了,我想通过5-6篇文章,来介绍一下项目中遇到的问题以及我的解决方案,希望这个系列能够给小伙伴一些帮助。本系列文章并不是手把手的教程,主要介绍了核心思路并讲解了核心代码,完整的代码小伙伴们可以在GitHub上star并clone下来研究。另外,原本计划把项目跑起来放到网

江南一点雨的专栏 9万+

​从RBAC到ABAC的进阶之路:基于jCasbin实现无侵入的SpringBoot权限校验​

引入 JCasbin 后,项目结构会发生显著变化👇优势描述逻辑解耦授权逻辑完全从业务代码中剥离灵活配置权限规则动态可改、可热更新可扩展可根据属性定义复杂条件统一决策所有权限判断走同一引擎可测试策略可单测,无需跑整套业务流程新增规则无需改代码。只要在策略表里加一条记录,就能实现全新的授权逻辑。权限系统的复杂,不在于“能不能判断”,而在于——“判断逻辑放在哪儿当项目越做越大,你会发现:真正的架构能力,不是多写逻辑,而是让逻辑有边界。

风象南的专栏 873

Spring Security+JWT+Vue 集成及权限认证

本篇简单的介绍 Vue 简单使用,主要为了测试验证与后端接口的权限功能是否能正常使用本次 Vue 之旅比较简单,只是简单的授权与权限认证、不认证(都给予对应的信息)至于全局拦截 code 后面再补上SpringBoot 后端跨域支持「由于前端与后端端口不同,会导致前后端联调时出现跨域问题」,代码补上// 1. 添加cors配置信息 CorsConfiguration config = new CorsConfiguration();

vnjohn 博主 > 主线分享后端领域业务设计知识、源码思想、架构设计 3634

SpringBoot+Vue+JWT实现轻量级RBAC权限管理方案

在前后端分离架构中,权限管理是保障系统安全的核心环节。其基本原理是通过认证与授权机制,控制用户对资源的访问。JWT(JSON Web Token)作为一种无状态的令牌技术,因其自包含、可验证的特性,成为实现分布式认证的流行方案。其技术价值在于简化了服务端会话管理,天然支持水平扩展,并能有效减少跨域问题。结合RBAC(基于角色的访问控制)模型,可以构建出清晰、可维护的权限体系。这一方案广泛应用于内部管理系统、中后台应用等场景。本文以SpringBoot和Vue技术栈为例,详细阐述了如何整合JWT与RBAC,实

weixin_30731305的博客 404

利用ENVI处理TIF文件,把多个tif整合到一个完整的tif,例如2003-2011年的tif整合到一个tif上。

利用ENVI处理TIF文件,把多个tif整合到一个完整的tif,例如2003-2011年的tif整合到一个tif上。

unique_sir的博客 1万+

Spring Boot + Vue的网上商城之springsecurity+jwt+redis实现用户权限认证实现

本文介绍了如何使用Spring Security和Vue实现用户登录和权限认证的功能。通过使用JWT和Redis存储Token,我们可以实现无状态的用户认证,提高系统的可扩展性和安全性。同时,通过使用Vue的路由导航守卫,我们可以实现前端的权限校验,确保只有登录用户才能访问受限资源。希望本文能对你理解和实现用户登录和权限认证功能有所帮助。

qq_22593423的博客 1109

基于wxWidgets框架,使用DialogBlocks建立OpenGL界面

前言 之前公司要求使用wxWidgets做GUI,基于OpenGL图形内核,开发三维可视化工具。目前初步开发工作基本已经完成,剩下都是完善了。这里记录一下最开始的框架搭建过程(当然是简化版啦)。wxWidgets结合OpenGL开发可以实现跨平台应用,再结合CMake和VS Code,整体开发工具用习惯了会感觉非常顺手。 界面编辑 首先,使用DialogBlocks新建一个工程,这里工程目录...

cyberobot的博客 2751

SpringBoot+SpringSecurity+Vue实现动态权限(二)

在上一篇完成项目的构建后,完成了SpringSecurity获取用户信息和权限信息的逻辑。这一篇要实现SpringSecurity的配置,实现自己的登录认证和授权的逻辑。要完成自定义认证和授权的逻辑,就需要自己实现两个过滤器。 这两个过滤器是认证过滤器和接口访问过滤器。分别要继承UsernamePasswordAuthenticationFilter类和BasicAuthenticationFilter这两个类。在UsernamePasswordAuthenticationFilter的实现类中可以定义登录

weixin_51110958的博客 1864

ABAP - Function ALV 11 - 单元格下拉框

【代码】ABAP - Function ALV 11 - 单元格下拉框。

HeathlX的博客 1816

Oracle入门——基础语法篇

ROWID是Oracle中的一个伪列,用于唯一标识数据库中的每一行。在查询中,可以使用ROWID来定位和操作特定的行,例如,删除特定的行或更新特定的行。在Oracle中,子查询是指嵌套在另一个查询内部的查询语句,它可以作为主查询的一部分,用于提供额外的过滤条件、计算或数据源。在Oracle中,连接查询是通过使用JOIN子句将两个或多个表中的数据合并在一起,以获取满足特定条件的结果集。在Oracle数据库中,ROWID和ROWNUM都是用于对行进行标识和定位的伪列,它们在查询中的使用有一些不同之处。

麦当当的博客 3090

优雅的实现SpringBoot动态权限校验,可以这样做!

关注公众号:woniuxgg,在公众号中回复:笔记 就可以获得蜗牛为你精心准备的java实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利!

1482

YOLOv11改进 | 网络结构代码逐行解析(二) | yolov11中的Mosaic增强详解

本文给大家带来的是YOLOv10中的Mosaic增强代码的详解,可能有部分人对于这一部分比较陌生,有的读者可能知道Mosaic增强但是不知道其工作原理,具体来说Mosaic增强就是指我们的数据集中的图片在输入给模型之前的一个处理过程(我们的图片并不是直接就输入给模型了,大家的训练结果中的结果检测图片大家可以看到数据集中多个图片会组合在一起这就是简单的Mosaic增强),下面我就来讲解一下其在YOLOv10中工作原理和代码定义,下面图片为一个Mosaic增强后的图片。​。

Snu77的博客 5024

Springboot 中引入 SpEL,复杂权限控制轻松搞定,非常优雅!

对于在Springboot中,利用自定义注解+切面来实现接口权限的控制这个大家应该都很熟悉,也有大量的博客来介绍整个的实现过程,整体来说思路如下:自定义一个权限校验的注解,包含参数value配置在对应的接口上定义一个切面类,指定切点在切入的方法体里写上权限判断的逻辑但是呢,等到实际用到的时候就傻眼了,为什么呢?在实际的开发中,你会发现,对于权限校验的需求场景是很多的,比如:只要配置了任何角色,就可以访问有某个权限就可以访问放行所有请求只有超级管理员角色才可以访问只有登录后才可以访问。

m0_67847535的博客 2万+

YOLOv12超参数智能优化:基于内置Ray Tune的自动化调优实战指南

购买即可解锁300+YOLO优化文章,并且还有海量深度学习复现项目,价格仅需两杯奶茶的钱,别人有的本专栏也有! 目标检测模型的性能瓶颈往往源于超参数配置不当。传统手动调优方式不仅耗时耗力,且难以找到全局最优解。YOLOv12集成的Ray Tune框架为解决这一难题提供了完整方案,通过分布式异步优化算法,可实现超参数搜索效率的指数级提升。实际测试表明,采用Ray Tune自动调优的YOLOv12模型,在COCO数据集上mAP指标平均提升3.8%,收敛速度加快2.1倍。在VisDrone无人机数据集上的跨域测试

LXY_LOVE36的博客 119

C# WPF上位机实战:从零开发空压机监控系统(含实时监控+故障预警+数据追溯)

在工业生产场景中,空压机作为核心动力设备,常面临这些痛点:压力/温度异常导致停机、油耗超标未及时发现、设备故障追溯无数据支撑、人工巡检效率低且易遗漏。而基于C# WPF开发的空压机监控系统,能通过实时采集设备运行参数、智能分析运行状态、联动故障预警,实现“状态监控-异常告警-数据追溯-远程管控”全流程自动化,完美解决传统管理模式的弊端。今天就带大家落地这套空压机监控系统,基于C# + .NET 8 + WPF + Modbus TCP/RTU协议,整合压力、温度、油耗、运行时长等核心参数监控,搭配WPF的数

这里只有干货:从 Modbus 通信到 MES 对接,从 YOLO 训练到工控机部署,带你搞定工业软件开发全流程。 1106

SpringBoot实战:Ruoyi框架中如何优雅地跳过密码校验(附完整代码)

本文详细介绍了在SpringBoot项目中使用Ruoyi框架时,如何通过自定义认证提供者实现安全可控的密码校验旁路机制。该方案支持环境隔离与显式配置,为开发、测试等非生产环境提供便捷登录方式,同时通过强制日志审计确保安全,并附有完整代码示例。

yellow的专栏 987

Java代码实现opc ua 事件订阅和数据变化订阅

OPC UA (Open Platform Communications Unified Architecture) 提供了两种主要的订阅机制:数据变化订阅(Data Change Subscription)和事件订阅(Event Subscription)。这两种订阅机制允许客户端实时接收来自服务器的数据更新或事件通知。

洛阳泰山的博客 2370

SpringBoot中 引入 SpEL,复杂权限控制轻松搞定,非常优雅!

来源:juejin.cn/post/7226674759626571833???? 欢迎加入小哈的星球,你将获得:专属的项目实战 / Java 学习路线 /一对一提问 / 学习打卡 / 赠书福利全栈前后端分离博客项目 1.0 版本完结啦,2.0 正在更新中...,演示链接:http://116.62.199.48/,全程手摸手,后端 + 前端全栈开发,从 0 到 1 讲解每个功能点开发步骤,...

犬小哈 317

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

SpringBoot + SpEL,轻松搞定复杂权限控制,非常优雅!

大家好,我是不才陈某~对于在Springboot中,利用自定义注解+切面来实现接口权限的控制这个大家应该都很熟悉,也有大量的博客来介绍整个的实现过程,整体来说思路如下:自定义一个权限校验的注解,包含参数value配置在对应的接口上定义一个切面类,指定切点在切入的方法体里写上权限判断的逻辑乍一看,没毛病,学到了,学到了~,收藏起来。但是呢,等到实际用到的时候就傻眼了,为什么呢?在实际的开发中,你会发...

码猿技术专栏 332

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SpringBoot集成Sa-Token实现轻量级权限认证实战

权限认证是Java企业级开发的核心模块,传统方案如Shiro和Spring Security虽然功能全面但配置复杂。Sa-Token作为新兴的轻量级权限框架,通过简洁的API设计和注解驱动模式,显著降低了权限系统的实现门槛。其核心原理基于Token鉴权机制,支持登录态管理、角色权限校验等基础功能,并能无缝扩展分布式会话、单点登录等企业级特性。在SpringBoot生态中,Sa-Token通过starter依赖实现快速集成,配合Redis可轻松构建高可用的认证体系。该技术特别适合需要快速迭代的中后台系统,通过

weixin_33888907的博客 481

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

【SpringBoot 】AOP企业级权限控制方案(三)

本文介绍了一套基于注解和AOP的企业级权限控制方案,主要特点包括: 六大核心功能: 支持角色校验和权限码校验 提供AND/OR两种校验逻辑 超级管理员豁免机制 统一异常响应处理 业务代码零侵入 测试验证: 通过5个典型测试场景验证功能有效性 确认权限校验准确、异常响应统一、超管豁免正常 方案优势: 声明式权限配置,开发便捷 AOP实现零业务侵入 支持多角色/权限码组合校验 提供线程安全的数据隔离 扩展性: 可扩展数据级权限、动态权限配置 支持多级缓存优化和多租户隔离 该方案通过@RequiresPermis

海兰 356

Spring Boot 数据权限校验实战:AOP + 自定义注解的巧妙运用

摘要:本文介绍了一种基于Spring AOP和自定义注解的企业级数据权限控制方案。通过定义@DataScope注解标记需要权限过滤的方法,结合AOP切面动态生成SQL条件,实现不同角色对数据的差异化访问(如普通员工仅查看个人数据,管理员可查看全部)。方案包含自定义注解、AOP切面、查询参数封装和MyBatis SQL拼接四个核心模块,具有解耦性好、维护性强等特点,同时讨论了性能开销和SQL注入风险等考量因素。该方案为数据权限管理提供了一种优雅的编程范式。

2401_83192700的博客 1391
上一篇: 通过租户id实现的SaaS方案
下一篇: 在地铁上破解软件,被一群人围观!
公众号-芋道源码
博客等级 码龄10年 1634粉丝 377原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值