简介 官网:https://spring.io/projects/spring-security 实际应用系统中,为了安全起见,一般都必备用户认证(登录)和权限控制的功能,以识别用户是否合法,以及根据权限来控制用户是否能够执行某项操作。 Spring Security 是一个安全相关的框架,能够与 Spring 项目无缝整合,本文主要是介绍 Spring Security 默认的用户认证和权限控制的使用方法和原理,但不涉及到自定义实现。Spring Security 用户认证和权限控制(自定义实现) 这篇文章专门讲解用户认证和权限控制相关的自定义实现。 (1)用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。通俗点说就是系统认为用户是否能登录 (2)用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。通俗点讲就是系统判断用户是否有权限去做某些事情。
同款产品对比 Spring Security Spring 技术栈的组成部分。通过提供完整可扩展的认证和授权支持保护你的应用程序。 SpringSecurity 特点:
和 Spring 无缝整合。
全面的权限控制。
专门为 Web 开发而设计。
旧版本不能脱离 Web 环境使用。
新版本对整个框架进行了分层抽取,分成了核心模块和 Web 模块。单独引入核心模块就可以脱离 Web 环境。
重量级。
Shiro Apache 旗下的轻量级权限控制框架。 特点:
轻量级。Shiro 主张的理念是把复杂的事情变简单。针对对性能有更高要求的互联网应用有更好表现。
通用性。
好处:不局限于 Web 环境,可以脱离 Web 环境使用。
缺陷:在 Web 环境下一些特定的需求需要手动编写代码定制。
Spring Security 是 Spring 家族中的一个安全管理框架,实际上,在 Spring Boot 出现之前,Spring Security 就已经发展了多年了,但是使用的并不多,安全管理这个领域,一直是 Shiro 的天下。 相对于 Shiro,在 SSM 中整合 Spring Security 都是比较麻烦的操作,所以,SpringSecurity 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有 Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。 自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了自动化配置方案,可以使用更少的配置来使用 SpringSecurity。因此,一般来说,常见的安全管理技术栈的组合是这样的:
SSM + Shiro
Spring Boot/Spring Cloud + SpringSecurity
以上只是一个推荐的组合而已,如果单纯从技术上来说,无论怎么组合,都是可以运行的。
Security 如何工作 SpringSecurity 本质
本质是一个过滤器链
代码底层流程: 重点看三个过滤器:FilterSecurityInterceptor ExceptionTranslationFilter UsernamePasswordAuthenticationFilter FilterSecurityInterceptor ::是一个方法级的权限过滤器, 基本位于过滤链的最底部super.beforeInvocation(fi) 表示查看之前的 filter 是否通过。 fi.getChain().doFilter(fi.getRequest(), fi.getResponse());表示真正的调用后台的服务。 ExceptionTranslationFilter ::是个异常过滤器,用来处理在认证授权过程中抛出的异常 *UsernamePasswordAuthenticationFilter * :对/login 的 POST 请求做拦截,校验表单中用户 名,密码。
SpringSecurity 过滤器启动原理 1.使用 springsecurity 配置过滤器(DelegatingFilterProxy); 2.该类的 doFilter 方法会通过 initDelegate(wac)先获取到过滤器 bean(FilterChainProxy),通过他去初始化其他的 filter(delegate.init(this.getFilterConfig());); 3.FilterChainProxy 的 doFilter 方法会通过 getFilters(fwRequest)方法获取到 WebApplicationContext 配置中的所有配置过的的 filter; 4.紧接着开始往后加载其他的 filter
UserDetailsService 接口讲解 当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中 账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑。 如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。接口定义如下:
返回值 UserDetails 这个类是系统默认的用户”主体 “
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Collection<? extends GrantedAuthority> getAuthorities(); String getPassword () ;String getUsername () ;boolean isAccountNonExpired () ;boolean isAccountNonLocked () ;boolean isCredentialsNonExpired () ;boolean isEnabled () ;
以后我们只需要使用 User 这个实体类即可!
PasswordEncoder 接口讲解 1 2 3 4 5 6 7 8 9 10 11 String encode (CharSequence rawPassword) ;配,则返回 true ;如果不匹配,则返回 false 。第一个参数表示需要被解析的密码。第二个 参数表示存储的密码。 boolean matches (CharSequence rawPassword, String encodedPassword) ;false 。默认返回 false 。default boolean upgradeEncoding (String encodedPassword) {return false ;}
BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析器。 BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单向加密。可以通过 strength 控制加密强度,默认 10. 查用方法演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void test01 () { BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); String atguigu = bCryptPasswordEncoder.encode("atguigu" ); System.out.println("加密之后数据:\t" +atguigu); boolean result = bCryptPasswordEncoder.matches("atguigu" , atguigu); System.out.println("比较结果:\t" +result); }
集成 Security 在 SpringBoot 的基础上添加依赖即可
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency >
使用 Spring Security 为的就是写最少的代码,实现更多的功能,在定制化 Spring Security,核心思路就是:重写某个功能,然后配置。
比如你要查自己的用户表做登录,那就实现 UserDetailsService 接口;
比如前后端分离项目,登录成功和失败后返回 json,那就实现 AuthenticationFailureHandler/AuthenticationSuccessHandler 接口;
比如扩展 token 存放位置,那就实现 HttpSessionIdResolver 接口;
等等…
最后,将上述做的更改配置到 security 里。套路就是这个套路,下边咱们实战一下。
用户登录认证逻辑 从数据库中查出登录用户的信息(如密码)、角色、权限等,然后返回一个 UserDetails 类型的实体,security 会自动根据密码和用户相关状态(是否锁定、是否启停、是否过期等)判断用户登录成功或者失败。
1、创建自定义 UserDetailsService 这是实现自定义用户认证的核心逻辑,loadUserByUsername(String username)的参数就是登录时提交的用户名,返回类型是一个叫 UserDetails 的接口,需要在这里构造出他的一个实现类 User,这是 Spring security 提供的用户信息实体。
1 2 3 4 5 6 7 public class UserDetailsServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { return null ; } }
2、准备 sql
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0 ; -- ---------------------------- -- Table structure for tb_user_auth -- ---------------------------- DROP TABLE IF EXISTS `tb_user_auth`; CREATE TABLE `tb_user_auth` ( `id` int NOT NULL AUTO_INCREMENT, `user_info_id` int NOT NULL COMMENT '用户信息id' , `username` varchar(50 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名' , `password` varchar(100 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码' , `login_type` tinyint(1 ) NOT NULL COMMENT '登录类型' , `ip_address` varchar(50 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户登录ip' , `ip_source` varchar(50 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'ip来源' , `create_time` datetime NOT NULL COMMENT '创建时间' , `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间' , `last_login_time` datetime NULL DEFAULT NULL COMMENT '上次登录时间' , PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `username`(`username`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;-- ---------------------------- -- Records of tb_user_auth -- ---------------------------- INSERT INTO `tb_user_auth` VALUES (1 , 1 , 'admin@qq.com' , '$2a$10$AkxkZaqcxEXdiNE1nrgW1.ms3aS9C5ImXMf8swkWUJuFGMqDl.TPW' , 1 , '127.0.0.1' , '' , '2021-08-12 15:43:18' , '2021-10-06 16:44:41' , '2021-10-06 16:44:41' ); SET FOREIGN_KEY_CHECKS = 1 ;
3. 使用 MybatisPlus 生成实体类和 mapper 引入依赖
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.4.1</version > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-generator</artifactId > <version > 3.4.1</version > </dependency >
编写自动生成代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 public class MybatisPlusGenerator { public static String scanner (String tip) { Scanner scanner = new Scanner(System.in); StringBuilder help = new StringBuilder(); help.append("请输入" + tip + ":" ); System.out.println(help.toString()); if (scanner.hasNext()) { String ipt = scanner.next(); if (StringUtils.isNotBlank(ipt)) { return ipt; } } throw new MybatisPlusException("请输入正确的" + tip + "!" ); } public static void main (String[] args) { AutoGenerator mpg = new AutoGenerator(); GlobalConfig gc = new GlobalConfig(); String projectPath = System.getProperty("user.dir" ); gc.setOutputDir(projectPath + "/src/main/java" ); gc.setAuthor("dzgu" ); gc.setOpen(false ); gc.setFileOverride(false ); gc.setServiceName("%sService" ); gc.setIdType(IdType.AUTO); gc.setDateType(DateType.ONLY_DATE); gc.setSwagger2(true ); mpg.setGlobalConfig(gc); DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl("jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC" ); dsc.setDriverName("com.mysql.cj.jdbc.Driver" ); dsc.setUsername("root" ); dsc.setPassword("gudongzhou678" ); dsc.setDbType(DbType.MYSQL); mpg.setDataSource(dsc); PackageConfig pc = new PackageConfig(); pc.setParent("com.dzgu.myblog" ); pc.setController("controller" ); pc.setService("service" ); pc.setServiceImpl("service.impl" ); pc.setMapper("mapper" ); pc.setEntity("entity" ); mpg.setPackageInfo(pc); InjectionConfig cfg = new InjectionConfig() { @Override public void initMap () { } }; String templatePath = "/templates/mapper.xml.vm" ; List<FileOutConfig> focList = new ArrayList<>(); focList.add(new FileOutConfig(templatePath) { @Override public String outputFile (TableInfo tableInfo) { return projectPath + "/src/main/resources/mapper/" + pc.getModuleName() + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML; } }); cfg.setFileOutConfigList(focList); mpg.setCfg(cfg); StrategyConfig strategy = new StrategyConfig(); strategy.setNaming(NamingStrategy.underline_to_camel); strategy.setColumnNaming(NamingStrategy.underline_to_camel); strategy.setEntityLombokModel(true ); strategy.setRestControllerStyle(true ); strategy.setInclude(scanner("表名,多个英文逗号分割" ).split("," )); strategy.setControllerMappingHyphenStyle(true ); strategy.setTablePrefix(pc.getModuleName() + "tb_" ); mpg.setStrategy(strategy); mpg.execute(); } }
4. 制作登录实现类和自定义 UserDetails 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 @Data @Builder public class UserDetailsDTO implements UserDetails , CredentialsContainer { ……………… @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this .roleList.stream() .map(SimpleGrantedAuthority::new ) .collect(Collectors.toSet()); } @Override public String getPassword () { return this .password; } @Override public String getUsername () { return this .username; } @Override public boolean isAccountNonExpired () { return true ; } @Override public boolean isAccountNonLocked () { return this .isDisable == FALSE; } @Override public boolean isCredentialsNonExpired () { return true ; } @Override public boolean isEnabled () { return true ; } @Override public void eraseCredentials () { this .setPassword(null ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class UserDetailsServiceImpl implements UserDetailsService { @Autowired UserAuthMapper userAuthMapper; @Resource private HttpServletRequest request; @Override public UserDetails loadUserByUsername (String username) { if (StringUtils.isBlank((username))) { throw new BizException("用户名不能为空" ); } UserAuth userAuth = userAuthMapper.selectOne(new LambdaQueryWrapper<UserAuth>() .select(UserAuth::getId, UserAuth::getUserInfoId, UserAuth::getUsername, UserAuth::getPassword, UserAuth::getLoginType) .eq(UserAuth::getUsername, username)); if (Objects.isNull(userAuth)) { throw new BizException("用户不存在" ); } return convertUserDetails(userAuth, request); } private UserDetailsDTO convertUserDetails (UserAuth userAuth, HttpServletRequest request) { return UserDetailsDTO.builder() .build(); } }
5. 配置 Spring Security 5.1 接管 AuthenticationSuccessHandler 在登录认证成功后会被调用,一般要记录登录日志,然后把认证之后的用户 authentication 返给前端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Slf4j @Component public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler { @Autowired private UserAuthMapper userAuthMapper; @Override public void onAuthenticationSuccess (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { UserDetailsDTO principal = (UserDetailsDTO) authentication.getPrincipal(); UserInfoDTO userInfoDTO = BeanCopyUtils.copyObject(principal, UserInfoDTO.class); httpServletResponse.setContentType("application/json;charset=UTF-8" ); httpServletResponse.getWriter().write(JSON.toJSONString(ApiResponse.ok(userInfoDTO))); UserAuth updatedPrincipal = UserAuth.builder() .id(principal.getId()) .ipAddress(principal.getIpAddress()) .ipSource(principal.getIpSource()) .lastLoginTime(principal.getLastLoginTime()) .build(); userAuthMapper.updateById(updatedPrincipal); log.info("===用户 {} 登录成功,登录ip {} ,登录时间 {}" , principal.getUsername(), principal.getIpAddress(), principal.getLastLoginTime()); } }
5.2 接管 AuthenticationFailHandlerImpl 登录失败后,可以根据不同的 AuthenticationException,来区分是为什么登录失败,这里需要有日志打印,然后根据业务需求,返回信息给前端。比如要求是无论什么错误,都返回登录失败,这里简单的返回登陆失败
1 2 3 4 5 6 7 8 9 10 11 @Slf4j @Component public class AuthenticationFailHandlerImpl implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { String requestURI = httpServletRequest.getRequestURI(); log.warn("==用户登陆失败:" + requestURI + "==" + e.getMessage()); httpServletResponse.setContentType("application/json;charset=UTF-8" ); httpServletResponse.getWriter().write(JSON.toJSONString(ApiResponse.fail(requestURI,LOGIN_ERROR))); } }
5.3 接管 LogoutSuccessHandlerImpl 和登录成功、失败类似,记录日志,然后返回前端 json。
1 2 3 4 5 6 7 8 9 10 11 12 @Slf4j @Component public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler { @Override public void onLogoutSuccess (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { UserAuth userAuth = (UserAuth) authentication.getPrincipal(); log.warn("==用户 {} 注销成功" ,userAuth.getUsername() ); httpServletResponse.setContentType("application/json;charset=UTF-8" ); httpServletResponse.getWriter().write(JSON.toJSONString(ApiResponse.ok())); } }
5.4 接管 Spring Security 异常 使用 Spring Boot2+ Spring Security 实现用户登录权限控制等操作,但是在用户登录的时候,怎么处理 Spring Security 抛出的异常呢?我们发现使用了@RestControllerAdvice 和@ExceptionHandler 不能处理 Spring Security 抛出的异常,因为 SpringSecurity 默认自带异常处理机制。Spring Security 有两个重要的异常类 :
AuthenticationEntryPoint :认证异常: 匿名用户在认证过程中的异常
AccessDeniedException : 鉴权异常,认证用户访问无权限资源时的异常
接管 AuthenticationEntryPoint 异常
1 2 3 4 5 6 7 8 9 10 11 12 @Slf4j @Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { String requestURI = httpServletRequest.getRequestURI(); log.warn("==用户未登录" + requestURI + "==" + e.getMessage()); httpServletResponse.setContentType("application/json;charset=utf-8" ); httpServletResponse.getWriter().write(JSON.toJSONString(ApiResponse.fail(requestURI, AUTHORIZED_ERROR))); } }
接管 AccessDeniedHandler 异常 AccessDeniedHandler 用于在用户已经登录了,但是访问了其自身没有权限的资源时做出对应的处理。ExceptionTranslationFilter 拥有的 AccessDeniedHandler 默认是 AccessDeniedHandlerImpl,这个默认实现类会根据 errorPage 和状态码来判断,最终决定跳转的页面。对于前后端分离项目,我们需要返回 json 因此接管此个组件。
1 2 3 4 5 6 7 8 9 10 11 12 @Slf4j @Component public class AccessDeniedHandlerImpl implements AccessDeniedHandler { @Override public void handle (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { String requestURI = httpServletRequest.getRequestURI(); String exceptionMsg = httpServletResponse.getHeader("ServiceException" ); log.warn("==用户权限不足访问:" + requestURI + "==" + exceptionMsg); httpServletResponse.setContentType("application/json;charset=utf-8" ); httpServletResponse.getWriter().write(JSON.toJSONString(ApiResponse.fail(requestURI, AUTHORIZED_ERROR))); } }
5.5 重写鉴权方法 标准的 RABC, 权限需要支持动态配置,spring security 默认是在代码里约定好权限,真实的业务场景通常需要可以支持动态配置角色访问权限,即在运行时去配置 url 对应的访问角色。 基于 spring security,如何实现这个需求呢? 最简单的方法就是自定义一个 Filter 去完成权限判断,但这脱离了 spring security 框架,如何基于 spring security 优雅的实现呢? 这里就要提到 spring security 实现动态配置 url 权限的两种方法 。 首先 自定义 UrlFilterInvocationSecurityMetadataSource 实现 FilterInvocationSecurityMetadataSource 重写 getAttributes()方法 获取访问该 url 所需要的角色权限信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 public class FilterInvocationSecurityMetadataSourceImpl implements FilterInvocationSecurityMetadataSource { private List<ResourceRoleDTO> resourceRoleList; @Autowired private RoleMapper roleMapper; public void clearDataSource () { resourceRoleList = null ; } @Override public Collection<ConfigAttribute> getAttributes (Object object) throws IllegalArgumentException { FilterInvocation fi = (FilterInvocation) object; String method = fi.getRequest().getMethod(); String url = fi.getRequest().getRequestURI(); if (CollectionUtils.isEmpty(resourceRoleList)) { resourceRoleList = roleMapper.listResourceRoles(); } AntPathMatcher antPathMatcher = new AntPathMatcher(); for (ResourceRoleDTO resourceRoleDTO : resourceRoleList) { if (antPathMatcher.match(resourceRoleDTO.getUrl(), url) && resourceRoleDTO.getRequestMethod().equals(method)) { List<String> roleList = resourceRoleDTO.getRoleList(); if (CollectionUtils.isEmpty(roleList)) { return SecurityConfig.createList("disable" ); } return SecurityConfig.createList(roleList.toArray(new String[]{})); } } return null ; } @Override public Collection<ConfigAttribute> getAllConfigAttributes () { return null ; } @Override public boolean supports (Class<?> aClass) { return FilterInvocation.class.isAssignableFrom(aClass); } }
执行完之后到 下一步 AccessDecisionManager 中认证权限
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class AccessDecisionManagerImpl implements AccessDecisionManager { @Override public void decide (Authentication authentication, Object object, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException { List<String> permissionList = authentication.getAuthorities() .stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList()); for (ConfigAttribute item : collection) { if (permissionList.contains(item.getAttribute())){ return ; } } throw new AccessDeniedException("没有操作权限" ); } @Override public boolean supports (ConfigAttribute configAttribute) { return true ; } @Override public boolean supports (Class<?> aClass) { return true ; } }
https://www.cnblogs.com/xiaoqi/p/spring-security-rabc.html 做了这么多的准备工作后,终于到了配置的时候了,Spring Security 通过建造者模式,使得配置变得简单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthenticationEntryPointImpl authenticationEntryPoint; @Autowired private AccessDeniedHandlerImpl accessDeniedHandler; @Autowired private AuthenticationSuccessHandlerImpl authenticationSuccessHandler; @Autowired private AuthenticationFailHandlerImpl authenticationFailHandler; @Autowired private LogoutSuccessHandlerImpl logoutSuccessHandler; @Bean public PasswordEncoder passwordEncoder () { return new BCryptPasswordEncoder(); } @Bean public FilterInvocationSecurityMetadataSource securityMetadataSource () { return new FilterInvocationSecurityMetadataSourceImpl(); } @Bean public AccessDecisionManager accessDecisionManager () { return new AccessDecisionManagerImpl(); } @Bean public SessionRegistry sessionRegistry () { return new SessionRegistryImpl(); } @Bean public HttpSessionEventPublisher httpSessionEventPublisher () { return new HttpSessionEventPublisher(); } @Override protected void configure (HttpSecurity http) throws Exception { http.formLogin() .loginProcessingUrl("/login" ) .successHandler(authenticationSuccessHandler) .failureHandler(authenticationFailHandler) .and() .logout() .logoutUrl("/logout" ) .logoutSuccessHandler(logoutSuccessHandler); http.authorizeRequests() .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess (O fsi) { fsi.setSecurityMetadataSource(securityMetadataSource()); fsi.setAccessDecisionManager(accessDecisionManager()); return fsi; } }) .anyRequest().permitAll() .and() .csrf().disable().exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler) .and() .sessionManagement() .maximumSessions(2 ) .maxSessionsPreventsLogin(true ) .sessionRegistry(sessionRegistry()); } }
这里注意 FilterInvocationSecurityMetadataSourceImpl 的使用,和 accessDecisionManager 不一样,ExpressionUrlAuthorizationConfigurer 并没有提供 set 方法设置 FilterSecurityInterceptor FilterInvocationSecurityMetadataSource,how to do?发现一个扩展方法 withObjectPostProcessor,通过该方法自定义一个处理 FilterSecurityInterceptor 类型的 ObjectPostProcessor 就可以修改 FilterSecurityInterceptor。http://itboyhub.com/2021/01/25/spring-security-object-post-processor/
6. 测试访问 1 2 3 4 5 6 7 8 9 10 11 12 13 @RestController public class TestController { @GetMapping("/hi") public ApiResponse<?> test01() { return ApiResponse.ok("正常访问匿名用户可访问的hi" ); } @GetMapping("/admin") public ApiResponse<?> test02() { return ApiResponse.ok("正常访问需要admin权限的页面" ); } }
)