spring security官方文档:https://docs.spring.io/spring-security/site/docs/5.1.4.RELEASE/reference/html5/
哦哦,原来spring的文档都是 docs.spring.io/项目名/ 这个项目名跟直接点击项目后出现在地址栏是一样的
<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>
<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>
<%--
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>
@Configuration
@ComponentScan("com.zzhua")
public class AppConfig {
}
public class CustomizeDispatcherServlet extends DispatcherServlet {
public Class<?> getContextClass() {
return AnnotationConfigWebApplicationContext.class;
}
}
@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;
}
}
@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;
}
};
}
}
@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
public class IndexController {
@RequestMapping("index")
public String index() {
return "index";
}
}
@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;
}
}
@Data
public class Contact {
String name;
}
首先,security框架使用注解实现对方法的权限访问控制,本质上是基于Spring Aop代理的方式实现的,也就是说加上这些注解的bean将会被切面切到,切到之后,使用springEL解析表达式,处理过程中security依然使用它的那一套访问决策管理器啥的,进行投票,如果没有权限,将会抛出拒绝访问异常,这和FilterSecurityInterceptor的处理逻辑是类似的。但是我们应该清楚,请求要达到加上注解的方法,那肯定是需要经过了过滤器的,所有的过滤器都要放行才能到达方法。所以可以把方法注解当做一种更加细粒度的控制方式。
在看源码之前,必须至少对Spring IOC容器、Spring Aop过程熟悉,熟悉之后,只要找到关键的组件的配置位置和配置方式,那么整个配置流程和执行过程就都清楚了。
那么找哪些组件呢
我们知道:切面 = 切点 + 增强
切点负责找到要切入的方法,即哪些bean应该要被代理
增强负责目标方法执行时,想要插入自定义的执行的逻辑
切面则是把切点和增强结合起来,以配合Spring Aop框架。
@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这个选择器
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]);
}
}