Security方法注解权限访问控制及原理剖析

spring security官方文档:https://docs.spring.io/spring-security/site/docs/5.1.4.RELEASE/reference/html5/
哦哦,原来spring的文档都是 docs.spring.io/项目名/ 这个项目名跟直接点击项目后出现在地址栏是一样的

使用案例

在这里插入图片描述

引入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.zzhua</groupId>
    <artifactId>demo-security-anno</artifactId>
    <version>1.0-SNAPSHOT</version>

    <packaging>war</packaging>

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jsp-api</artifactId>
            <version>2.0</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>

		<!-- 用于支持springmvc写出json,自动配置MappingJackson2HttpMessageConverter -->
		<dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.0</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.9.0</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.0</version>
        </dependency>

        <!--整合spring-security-->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
            <version>5.1.4.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId>
            <version>5.1.4.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.1.4.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
        </dependency>

    </dependencies>

    <build>
        <finalName>demo-spring-security</finalName>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.tomcat.maven</groupId>
                    <artifactId>tomcat7-maven-plugin</artifactId>
                    <version>2.2</version>
                    <configuration>
                        <!-- 配置端口 -->
                        <port>8080</port>
                        <!-- 配置urlencoding -->
                        <uriEncoding>UTF-8</uriEncoding>
                        <!-- 配置项目的访问路径 -->
                        <path>/</path>
                    </configuration>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                    </configuration>
                </plugin>
                <!-- <plugin>
                     <artifactId>maven-resources-plugin</artifactId>
                     <configuration>
                         <encoding>utf-8</encoding>
                         <useDefaultDelimiters>true</useDefaultDelimiters>
                         <resources>
                             <resource>
                                 <directory>src/main/resources</directory>
                                 <filtering>true</filtering>
                                 <includes>
                                     <include>**/*</include>
                                 </includes>
                             </resource>
                             <resource>
                                 <directory>src/main/java</directory>
                                 <includes>
                                     <include>**/*.xml</include>
                                 </includes>
                             </resource>
                         </resources>
                     </configuration>
                 </plugin>-->
            </plugins>
        </pluginManagement>
    </build>

</project>


webapp文件下

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

    <context-param>
        <param-name>contextClass</param-name>
        <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </context-param>
    <!-- 使用spring 监听器 -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <!-- Spring核心配置文件 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>com.zzhua.config.AppConfig</param-value>
    </context-param>

    <!-- 配置SpringMVC -->
    <servlet>
        <servlet-name>springMvc</servlet-name>
        <servlet-class>com.zzhua.config.CustomizeDispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>com.zzhua.config.MyWebConfig</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springMvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <!-- 代理过滤器 -->
    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

</web-app>

index.jsp

<%--
  Created by IntelliJ IDEA.
  User: zzhua195
  Date: 2022/3/27
  Time: 16:24
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    欢迎来到index.jsp
</body>
</html>

配置

AppConfig

@Configuration
@ComponentScan("com.zzhua")
public class AppConfig {

}

CustomizeDispatcherServlet

public class CustomizeDispatcherServlet extends DispatcherServlet {
    public Class<?> getContextClass() {
        return AnnotationConfigWebApplicationContext.class;
    }
}

MyWebConfig

@Configuration
@EnableWebMvc
@EnableGlobalMethodSecurity(prePostEnabled = true) // 因为要控制controller中的方法访问,所以此注解要加到子容器中
@ComponentScan(basePackages = "com.zzhua.controller",
                excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,
                classes = Service.class)})
public class MyWebConfig implements WebMvcConfigurer {

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        // 开启静态资源访问
        configurer.enable();
    }

    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/view/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }

}

MySecurityConfig

@Configuration
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
    	// 这个相当于是父类提供的一个方便暴露认证管理器的方法,它里面引用的认证管理器构建者正是要置入ProviderManager中的。
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated();
        http.formLogin().successHandler((request, response, authentication) -> {
            
            // 登录成功之后,写个消息
            response.setContentType("application/json;charset=utf8");
            response.getWriter().write("登录成功");
        });
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("zhangsan")
                .password(passwordEncoder().encode("123"))
                .authorities("r1","ROLE_admin") 
				 // .role("admin") 
				 // 踩坑: 在security中角色其实就是一种权限(authority),
				 // 只不过是在authority的字符串前面加了一个前缀“ROLE_”
				 // (见:SecurityExpressionRoot#hasAnyRole、User#roles(String... roles)),
				 // 如果打开这个注释,将会覆盖掉上面配置的权限,因为它们配置的是同一个嘛
                .and()
                .withUser("lisi")
                .password(passwordEncoder().encode("456"))
                .authorities("r2","ROLE_guest")
        ;
    }

    @Bean
    public PasswordEncoder passwordEncoder() { // 密码匹配器
        return new BCryptPasswordEncoder();
    }

    @Bean
    public PermissionEvaluator permissionEvaluator() { // 用于支持hasPermission表达式,
                                                       // 框架默认使用的是DenyAllPermissionEvaluator(即默认全部拒绝访问)
        return new PermissionEvaluator(){
            @Override
            public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
                System.out.println(authentication);
                System.out.println(targetDomainObject);
                System.out.println(permission);
                // 这里可以拿到hasPermission表达式的参数,
                // 可以访问到所拦截到的执行方法的参数,并且可以带上权限字符
                // 这里可以自定义逻辑,返回false标识拒绝访问
                return false;
            }

            @Override
            public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
                System.out.println(authentication);
                System.out.println(targetId);
                System.out.println(targetType);
                System.out.println(permission);
                // 这里可以拿到hasPermission表达式的参数,
                // 可以访问到所拦截到的执行方法的参数,并且可以带上权限字符
                // 这里可以自定义逻辑,返回false标识拒绝访问
                return false;
            }
        };
    }

}

PermissionService

@Service("permission")
public class PermissionService {

    @Autowired
    private IUserService userService;

    /**
     * 判断是不是管理员
     *
     * @return
     */
    public boolean admin() {
        //拿到request和response
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        String tokeKey = CookieUtils.getCookie(request, Constants.User.COOKIE_TOKE_KEY);
        //没有令牌的key,没有登录,不用往下执行了
        if (TextUtils.isEmpty(tokeKey)) {
            return false;
        }

        SobUser sobUser = userService.checkSobUser();
        if (sobUser == null) {
            return false;
        }
        if (Constants.User.ROLE_ADMIN.equals(sobUser.getRoles())) {
            //管理员
            return true;
        }
        return false;
    }

}

controller

IndexController

@Controller
public class IndexController {

    @RequestMapping("index")
    public String index() {
        return "index";
    }

}

RController

@RestController
@RequestMapping
public class RController {

    @RequestMapping()
    @PreAuthorize("denyAll()") // 全部拒绝访问
    public String denyAll() {
        return "denyAll";
    }

    @RequestMapping("permitAll")
    @PreAuthorize("permitAll()") // 全都允许访问
    public String permitAll() {
        return "permitAll";
    }

    @GetMapping("r1")
    @PreAuthorize("hasAuthority('r1')") // 必须具有r1的authority
    public String r1() {
        return " r1";
    }

    @GetMapping("r2")
    @PreAuthorize("hasAuthority('r2')") // 必须具有r2的authority
    public String r2() {
        return " r2";
    }


    @RequestMapping("admin")
    @PreAuthorize("hasRole('admin')") // 必须具有ROLE_admin的authority
    public String admin() {
        return "admin";
    }

    @RequestMapping("guest")
    @PreAuthorize("hasRole('guest')")// 必须具有ROLE_guest的authority
    public String guest() {
        return "guest";
    }

    @RequestMapping("isAnonymous") // 必须是匿名访问
    @PreAuthorize("isAnonymous()")
    public String isAnonymous() {
        return "isAnonymous";
    }

    @RequestMapping("isRememberMe")
    @PreAuthorize("isRememberMe()") // 必须是通过记住我才能访问
    public String isRememberMe() {
        return "isRememberMe";
    }

    @RequestMapping("isAuthenticated")
    @PreAuthorize("isAuthenticated()") // 必须认证通过才能访问(包括记住我)
    public String isAuthenticated() {
        return "isAuthenticated";
    }

    @RequestMapping("combineLogic")
    @PreAuthorize("isRememberMe() or isAuthenticated()") // 必须是通过记住我或者是认证通过才能访问(可以使用逻辑符)
    public String combineLogic() {
        return "combineLogic";
    }

    @RequestMapping("isFullyAuthenticated")
    @PreAuthorize("isFullyAuthenticated()") // 必须是通过登录认证(不包括记住我)
    public String isFullyAuthenticated() {
        return "isFullyAuthenticated";
    }

    @RequestMapping("useMethodArg")
    @PreAuthorize("#uname == principal.username") // 传的参数必须是登录身份的username属性(这里可以写表达式噢)
    public String useMethodArg(@P("uname") String username) {
        return "userMethodArg";
    }

    @RequestMapping("hasPermission1")
    @PreAuthorize("hasPermission(#contact,'admin')") // 使用了自定义的PermissionEvaluator来实现,#contact可以用来引用方法中的参数
    public String hasPermission1(Contact contact) {
        return "hasPermission1";
    }

    @RequestMapping("hasPermission1-1")
    @PreAuthorize("hasPermission(#contact,#age)") // 使用了自定义的PermissionEvaluator来实现,#contact可以用来引用方法中的参数
    public String hasPermission11(Contact contact,Integer age) {
        return "hasPermission1-1";
    }

    @RequestMapping("hasPermission2") // 使用了自定义的PermissionEvaluator来实现
    @PreAuthorize("hasPermission(25,'com.zzhua.entity.Contact','read')")
    public String hasPermission2(Contact contact) {
        return "hasPermission2";
    }

    @RequestMapping("postAuthorize")
    @PostAuthorize("returnObject == 'postAuthorize'") // 方法执行后,再判断的权限校验,returnObject用于引用返回的结果
    public String postAuthorize(Integer flag) {
        return flag != null ? "postAuthorize" : "";
    }

    @RequestMapping("postAuthorize2")
    @PostAuthorize("hasPermission(returnObject,#flag)") //  使用了自定义的PermissionEvaluator,方法执行后,再判断的权限校验,returnObject用于引用返回的结果,#flag引用方法参数
        return flag != null ? "postAuthorize" : "";
    }
	
	@PreAuthorize("@permission.admin()") // 引用了自定义的bean的方法,有点类似于access(el表达式)的用法
    @PostMapping("/{original}")
    public ResponseResult uploadImage(@PathVariable("original") String original, @RequestParam("file") MultipartFile file) {
        return imageService.uploadImage(original, file);
    }
    
    @RequestMapping("postFilter")
    @PostFilter("filterObject.equals('1') || filterObject.equals('4')") // 对返回的结果挨个过滤,返回false的将会被丢弃
    public List<String> postFilter() {
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "1", "2", "3", "4");
        return list;
    }

    @RequestMapping("postFilter2")
    @PostFilter("hasPermission(filterObject, 'read')") // 对返回的结果挨个过滤,返回false的将会被丢弃
    public List<String> postFilter2() {
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "1", "2", "3", "4");
        return list;
    }

    @RequestMapping("preFilter")
    @PreFilter(filterTarget="ids", value="filterObject%2==0") // 对传入的参数集合中的元素挨个过滤,返回false的将会被丢弃
    public List<Integer> preFilter(@RequestParam(value = "ids",required = false) List<Integer> ids,
                                   @RequestParam(value = "nameList", required = false) List<String> nameList) {
        return ids;
    }

    @RequestMapping("preFilter2")
    @PreFilter(filterTarget="ids", value="hasPermission(filterObject,'admin')") // 对传入的参数集合中的元素挨个过滤,返回false的将会被丢弃
    public List<Integer> preFilter2(@RequestParam(value = "ids",required = false) List<Integer> ids,
                                    @RequestParam(value = "nameList", required = false) List<String> nameList) {
        return ids;
    }


}

Contact

@Data
public class Contact {
    String name;
}

原理剖析

概述

首先,security框架使用注解实现对方法的权限访问控制,本质上是基于Spring Aop代理的方式实现的,也就是说加上这些注解的bean将会被切面切到,切到之后,使用springEL解析表达式,处理过程中security依然使用它的那一套访问决策管理器啥的,进行投票,如果没有权限,将会抛出拒绝访问异常,这和FilterSecurityInterceptor的处理逻辑是类似的。但是我们应该清楚,请求要达到加上注解的方法,那肯定是需要经过了过滤器的,所有的过滤器都要放行才能到达方法。所以可以把方法注解当做一种更加细粒度的控制方式。

在看源码之前,必须至少对Spring IOC容器、Spring Aop过程熟悉,熟悉之后,只要找到关键的组件的配置位置和配置方式,那么整个配置流程和执行过程就都清楚了。

找组件

那么找哪些组件呢

我们知道:切面 = 切点 + 增强

切点负责找到要切入的方法,即哪些bean应该要被代理

增强负责目标方法执行时,想要插入自定义的执行的逻辑

切面则是把切点和增强结合起来,以配合Spring Aop框架。

@EnableGlobalMethodSecurity

@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ GlobalMethodSecuritySelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableGlobalMethodSecurity {

	boolean prePostEnabled() default false;

	boolean securedEnabled() default false;

	boolean jsr250Enabled() default false;

	boolean proxyTargetClass() default false;

	AdviceMode mode() default AdviceMode.PROXY;

	int order() default Ordered.LOWEST_PRECEDENCE;
}

使用@Import机制,导入了GlobalMethodSecuritySelector这个选择器

GlobalMethodSecuritySelector

final class GlobalMethodSecuritySelector implements ImportSelector {

	public final String[] selectImports(AnnotationMetadata importingClassMetadata) {
		Class<EnableGlobalMethodSecurity> annoType = EnableGlobalMethodSecurity.class;
		Map<String, Object> annotationAttributes = importingClassMetadata
				.getAnnotationAttributes(annoType.getName(), false);
		AnnotationAttributes attributes = AnnotationAttributes
				.fromMap(annotationAttributes);
		Assert.notNull(attributes, () -> String.format(
				"@%s is not present on importing class '%s' as expected",
				annoType.getSimpleName(), importingClassMetadata.getClassName()));

		// TODO would be nice if could use BeanClassLoaderAware (does not work)
		Class<?> importingClass = ClassUtils
				.resolveClassName(importingClassMetadata.getClassName(),
						ClassUtils.getDefaultClassLoader());
		boolean skipMethodSecurityConfiguration = GlobalMethodSecurityConfiguration.class
				.isAssignableFrom(importingClass);

		AdviceMode mode = attributes.getEnum("mode");
		boolean isProxy = AdviceMode.PROXY == mode;
		String autoProxyClassName = isProxy ? AutoProxyRegistrar.class
				.getName() : GlobalMethodSecurityAspectJAutoProxyRegistrar.class
				.getName();

		boolean jsr250Enabled = attributes.getBoolean("jsr250Enabled");

		List<String> classNames = new ArrayList<>(4);
		if (isProxy) {
			classNames.add(MethodSecurityMetadataSourceAdvisorRegistrar.class.getName());
		}

		classNames.add(autoProxyClassName);

		if (!skipMethodSecurityConfiguration) {
			classNames.add(GlobalMethodSecurityConfiguration.class.getName());
		}

		if (jsr250Enabled) {
			classNames.add(Jsr250MetadataSourceConfiguration.class.getName());
		}

		return classNames.toArray(new String[0]);
	}
}
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Security原理是通过一系列的过滤器链来保护应用程序的安全性。当一个请求进入应用程序时,Spring Security会按照事先定义的配置规则,依次执行一系列的过滤器来进行身份验证和授权操作。 下面是Spring Security中常用的控制请求访问权限方法: 1. 基于URL的访问控制:可以通过配置URL模式和相应的权限要求来限制对特定URL的访问。例如,可以配置某个URL需要具有特定角色或权限才能访问。 2. 基于注解访问控制:可以在控制方法上使用注解,例如`@PreAuthorize`、`@PostAuthorize`等,来限制对方法访问。这些注解可以使用Spring表达式语言定义更复杂的授权规则。 3. 基于方法访问控制:可以通过实现`AccessDecisionVoter`接口来自定义方法级别的访问控制规则。这样可以根据业务需求来决定是否允许用户执行某个方法。 4. 基于表达式的访问控制:Spring Security支持使用SpEL(Spring表达式语言)来定义更复杂的授权规则。可以在配置文件中使用SpEL表达式来判断用户是否具有特定权限或角色。 5. 动态访问控制:Spring Security支持通过编程方式来控制请求的访问权限。可以在代码中根据业务逻辑来判断是否允许用户访问某个资源。 总的来说,Spring Security提供了多种方法控制请求的访问权限,开发人员可以根据具体需求选择适合的方式来保护应用程序的安全性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值