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

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

使用案例

Security方法注解权限访问控制及原理剖析_第1张图片

引入依赖


<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.0modelVersion>

    <groupId>com.zzhuagroupId>
    <artifactId>demo-security-annoartifactId>
    <version>1.0-SNAPSHOTversion>

    <packaging>warpackaging>

    <dependencies>
        <dependency>
            <groupId>javax.servletgroupId>
            <artifactId>javax.servlet-apiartifactId>
            <version>3.1.0version>
            <scope>providedscope>
        dependency>

        <dependency>
            <groupId>javax.servletgroupId>
            <artifactId>jsp-apiartifactId>
            <version>2.0version>
            <scope>providedscope>
        dependency>

        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-webartifactId>
            <version>5.0.2.RELEASEversion>
        dependency>
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-webmvcartifactId>
            <version>5.0.2.RELEASEversion>
        dependency>

		
		<dependency>
            <groupId>com.fasterxml.jackson.coregroupId>
            <artifactId>jackson-databindartifactId>
            <version>2.9.0version>
        dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.coregroupId>
            <artifactId>jackson-coreartifactId>
            <version>2.9.0version>
        dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.coregroupId>
            <artifactId>jackson-databindartifactId>
            <version>2.9.0version>
        dependency>

        
        <dependency>
            <groupId>org.springframework.securitygroupId>
            <artifactId>spring-security-webartifactId>
            <version>5.1.4.RELEASEversion>
        dependency>

        <dependency>
            <groupId>org.springframework.securitygroupId>
            <artifactId>spring-security-configartifactId>
            <version>5.1.4.RELEASEversion>
        dependency>

        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-jdbcartifactId>
            <version>5.1.4.RELEASEversion>
        dependency>

        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>5.1.47version>
        dependency>

        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.16version>
        dependency>

    dependencies>

    <build>
        <finalName>demo-spring-securityfinalName>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.tomcat.mavengroupId>
                    <artifactId>tomcat7-maven-pluginartifactId>
                    <version>2.2version>
                    <configuration>
                        
                        <port>8080port>
                        
                        <uriEncoding>UTF-8uriEncoding>
                        
                        <path>/path>
                    configuration>
                plugin>
                <plugin>
                    <groupId>org.apache.maven.pluginsgroupId>
                    <artifactId>maven-compiler-pluginartifactId>
                    <configuration>
                        <source>1.8source>
                        <target>1.8target>
                    configuration>
                plugin>
                
            plugins>
        pluginManagement>
    build>

project>


webapp文件下

web.xml


<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>contextClassparam-name>
        <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContextparam-value>
    context-param>
    
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class>
    listener>
    
    <context-param>
        <param-name>contextConfigLocationparam-name>
        <param-value>com.zzhua.config.AppConfigparam-value>
    context-param>

    
    <servlet>
        <servlet-name>springMvcservlet-name>
        <servlet-class>com.zzhua.config.CustomizeDispatcherServletservlet-class>
        <init-param>
            <param-name>contextConfigLocationparam-name>
            <param-value>com.zzhua.config.MyWebConfigparam-value>
        init-param>
        <load-on-startup>1load-on-startup>
    servlet>
    <servlet-mapping>
        <servlet-name>springMvcservlet-name>
        <url-pattern>/url-pattern>
    servlet-mapping>

    
    <filter>
        <filter-name>springSecurityFilterChainfilter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxyfilter-class>
    filter>
    <filter-mapping>
        <filter-name>springSecurityFilterChainfilter-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]);
	}
}

你可能感兴趣的:(#,Spring,security,security,权限校验)