软件编程
位置:首页>> 软件编程>> java编程>> Spring Security实现基于角色的访问控制框架

Spring Security实现基于角色的访问控制框架

作者:Wen先森  发布时间:2022-03-19 05:40:53 

标签:Spring,Security,访问控制,框架

说明

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。Spring Security是一个专注于为Java应用程序提供身份验证和授权的框架。与所有Spring项目一样,Spring安全性的真正威力在于它可以很容易地扩展以满足定制需求。

一般Web应用的需要进行认证和授权。

  • 用户认证(Authentication):验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户。

  • 用户授权(Authorization):经过认证后判断当前用户是否有权限进行某个操作。在一个系统中,不同用户所具有的权限是不同的。

Spring Security与Shiro的区别

Spring Security 是 Spring家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。相对于Shiro,在SSH/SSM中整合Spring Security都是比较麻烦的操作,但有了Spring boot之后,Spring Boot 对于 Spring Security 提供了 自动化配置方案,可以零配置使用 Spring Security。因此,一般来说常见的安全管理技术栈组合是:

  • SSM+Shiro

  • Spring Boot /Spring Clound +Spring Security

简单使用

登录校验流程

Spring Security实现基于角色的访问控制框架

引入Security

在SpringBoot项目中使用SpringSecurity只需要引入依赖即可。

<!-- spring security 安全认证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

设置用户名和密码

在application.properties中添加属性:

server.port=8080
#spring.security.user.name=root
#spring.security.user.password=123456
#mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/security?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456

但是在application.properties中添加属性意味着登录系统的用户名的密码都是固定的,不推荐。可以使用配置类,在配置类中设置,配置类的优先级更高。

使用配置类

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
   /**
    * 自定义用户认证逻辑
    */
   @Autowired
   private UserDetailsService userDetailsService;
   /**
    * 认证失败处理类
    */
   @Autowired
   private AuthenticationEntryPointImpl unauthorizedHandler;
   /**
    * 退出处理类
    */
   @Autowired
   private LogoutSuccessHandlerImpl logoutSuccessHandler;
   /**
    * token认证过滤器
    */
   @Autowired
   private JwtAuthenticationTokenFilter authenticationTokenFilter;
   /**
    * 解决 无法直接注入 AuthenticationManager
    *
    * @return
    * @throws Exception
    */
   @Bean
   @Override
   public AuthenticationManager authenticationManagerBean() throws Exception
   {
       return super.authenticationManagerBean();
   }
   /**
    * anyRequest          |   匹配所有请求路径
    * access              |   SpringEl表达式结果为true时可以访问
    * anonymous           |   匿名可以访问
    * denyAll             |   用户不能访问
    * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
    * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
    * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
    * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
    * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
    * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
    * permitAll           |   用户可以任意访问
    * rememberMe          |   允许通过remember-me登录的用户访问
    * authenticated       |   用户登录后可访问
    */
   @Override
   protected void configure(HttpSecurity httpSecurity) throws Exception
   {
       httpSecurity
               // CRSF禁用,因为不使用session
               .csrf().disable()
               // 认证失败处理类
               .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
               // 基于token,所以不需要session
               .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
               // 过滤请求
               .authorizeRequests()
               // 对于登录login 允许匿名访问
               .antMatchers("/login").anonymous()
               .antMatchers(
                       HttpMethod.GET,
                       "/*.html",
                       "/**/*.html",
                       "/**/*.css",
                       "/file/**",
                       "/**/*.js"
               ).permitAll()
               // 除上面外的所有请求全部需要鉴权认证
               .anyRequest().authenticated()
               .and()
               .headers().frameOptions().disable();
       httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
       // 添加JWT filter
       httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
   }
   /**
    * 强散列哈希加密实现
    */
   @Bean
   public BCryptPasswordEncoder bCryptPasswordEncoder()
   {
       return new BCryptPasswordEncoder();
   }
   /**
    * 身份认证接口
    */
   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception
   {
       auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
   }
}
  • @EnableGlobalMethodSecurity :当我们想要开启spring方法级安全时,只需要在任何 @Configuration实例上使用 @EnableGlobalMethodSecurity 注解就能达到此目的。

  • prePostEnabled = true 会解锁 @PreAuthorize 和 @PostAuthorize 两个注解。从名字就可以看出@PreAuthorize 注解会在方法执行前进行验证,而 @PostAuthorize 注解会在方法执行后进行验证。

/**
* 自定义权限实现,ss取自SpringSecurity首字母
*/
@Service("ss")
public class PermissionService
{
   /** 所有权限标识 */
   private static final String ALL_PERMISSION = "*:*:*";
   /** 管理员角色权限标识 */
   private static final String SUPER_ADMIN = "admin";
   private static final String ROLE_DELIMETER = ",";
   private static final String PERMISSION_DELIMETER = ",";
   @Autowired
   private TokenService tokenService;
   /**
    * 验证用户是否具备某权限
    *
    * @param permission 权限字符串
    * @return 用户是否具备某权限
    */
   public boolean hasPermi(String permission)
   {
       if (StringUtils.isEmpty(permission))
       {
           return false;
       }
       LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
       if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
       {
           return false;
       }
       return hasPermissions(loginUser.getPermissions(), permission);
   }
   /**
    * 验证用户是否不具备某权限,与 hasPermi逻辑相反
    *
    * @param permission 权限字符串
    * @return 用户是否不具备某权限
    */
   public boolean lacksPermi(String permission)
   {
       return hasPermi(permission) != true;
   }
   /**
    * 验证用户是否具有以下任意一个权限
    *
    * @param permissions 以 PERMISSION_NAMES_DELIMETER 为分隔符的权限列表
    * @return 用户是否具有以下任意一个权限
    */
   public boolean hasAnyPermi(String permissions)
   {
       if (StringUtils.isEmpty(permissions))
       {
           return false;
       }
       LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
       if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
       {
           return false;
       }
       Set<String> authorities = loginUser.getPermissions();
       for (String permission : permissions.split(PERMISSION_DELIMETER))
       {
           if (permission != null && hasPermissions(authorities, permission))
           {
               return true;
           }
       }
       return false;
   }
   /**
    * 判断用户是否拥有某个角色
    *
    * @param role 角色字符串
    * @return 用户是否具备某角色
    */
   public boolean hasRole(String role)
   {
       if (StringUtils.isEmpty(role))
       {
           return false;
       }
       LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
       if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles()))
       {
           return false;
       }
       for (SysRole sysRole : loginUser.getUser().getRoles())
       {
           String roleKey = sysRole.getRoleKey();
           if (SUPER_ADMIN.contains(roleKey) || roleKey.contains(StringUtils.trim(role)))
           {
               return true;
           }
       }
       return false;
   }
   /**
    * 验证用户是否不具备某角色,与 isRole逻辑相反。
    *
    * @param role 角色名称
    * @return 用户是否不具备某角色
    */
   public boolean lacksRole(String role)
   {
       return hasRole(role) != true;
   }
   /**
    * 验证用户是否具有以下任意一个角色
    *
    * @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表
    * @return 用户是否具有以下任意一个角色
    */
   public boolean hasAnyRoles(String roles)
   {
       if (StringUtils.isEmpty(roles))
       {
           return false;
       }
       LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
       if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles()))
       {
           return false;
       }
       for (String role : roles.split(ROLE_DELIMETER))
       {
           if (hasRole(role))
           {
               return true;
           }
       }
       return false;
   }
   /**
    * 判断是否包含权限
    *
    * @param permissions 权限列表
    * @param permission 权限字符串
    * @return 用户是否具备某权限
    */
   private boolean hasPermissions(Set<String> permissions, String permission)
   {
       return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
   }
}
/**
    * 获取用户列表
    */
   @PreAuthorize("@ss.hasPermi('system:user:list')")
   @GetMapping("/list")
   public TableDataInfo list(SysUser user)
   {
       startPage();
       List<SysUser> list = userService.selectUserList(user);
       return getDataTable(list);
   }
  • antMatchers():可传入多个String参数,用于匹配多个请求API。结合permitAll(),可同时对多个请求设置忽略认证。

  • and():用于表示上一项配置已经结束,下一项配置将开始。

创建一个Service文件用于Security查询用户信息:

@Service
public class UserDetailsServiceImpl implements UserDetailsService
{
   private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
   @Autowired
   private ISysUserService userService;
   @Autowired
   private SysPermissionService permissionService;
   @Override
   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
   {
   // 根据账户号查询用户信息
       SysUser user = userService.selectUserByUserName(username);
       if (StringUtils.isNull(user))
       {
           log.info("登录用户:{} 不存在.", username);
           throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
       }
       else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
       {
           log.info("登录用户:{} 已被删除.", username);
           throw new BaseException("对不起,您的账号:" + username + " 已被删除");
       }
       else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
       {
           log.info("登录用户:{} 已被停用.", username);
           throw new BaseException("对不起,您的账号:" + username + " 已停用");
       }
       return createLoginUser(user);
   }
   public UserDetails createLoginUser(SysUser user)
   {
   // 获取用户权限
       return new LoginUser(user, permissionService.getMenuPermission(user));
   }
}

当用户请求时,Security便会拦截请求,取出其中的username字段,从Service中查询该账户,并将信息填充到一个userDetails对象中返回。这样Security便拿到了填充后的userDetails对象,也就是获取到了包括权限在内的用户信息。

过滤规则

WebSecurityConfigurerAdapter.configure(HttpSecurity http)

授权方式

Security的授权方式有两种:

  • WEB授权(基于请求URL),在configure(HttpSecurity httpSecurity)中设置。

  • 方法授权(在方法上使用注解授权)

WEB授权

@Override
httpSecurity.authorizeRequests() // 对请求进行授权
   .antMatchers("/test1").hasAuthority("p1") // 设置/test1接口需要p1权限
   .antMatchers("/test2").hasAnyRole("admin", "manager") // 设置/test2接口需要admin或manager角色
   .and()
   .headers().frameOptions().disable();

其中:

  • hasAuthority(&ldquo;p1&rdquo;)表示需要p1权限

  • hasAnyAuthority(&ldquo;p1&rdquo;, &ldquo;p2&rdquo;)表示p1或p2权限皆可

同理:

  • hasRole(&ldquo;p1&rdquo;)表示需要p1角色

  • hasAnyRole(&ldquo;p1&rdquo;, &ldquo;p2&rdquo;)表示p1或p2角色皆可

方法授权

/**
    * 获取用户列表
    */
   @PreAuthorize("@ss.hasPermi('system:user:list')")
   @GetMapping("/list")
   public TableDataInfo list(SysUser user)
   {
       startPage();
       List<SysUser> list = userService.selectUserList(user);
       return getDataTable(list);
   }

顺序优先级

校验是按照配置的顺序从上往下进行。若某个条件已匹配过,则后续同条件的匹配将不再被执行。也就是说,若多条规则是相互矛盾的,则只有第一条规则生效。典型地:

// 只有具有权限a的用户才能访问/get/resource
httpSecurity.authorizeRequests()
   .antMatchers("/get/resource").hasAuthority("a")
   .anyRequest().permitAll();
// 所有用户都能访问/get/resource
httpSecurity.authorizeRequests()
   .anyRequest().permitAll()
   .antMatchers("/get/resource").hasAuthority("a");

但若是包含关系,则需要将细粒度的规则放在前面:

// /get/resource需要权限a,除此之外所有的/get/**都可以随意访问
httpSecurity.authorizeRequests()
   .antMatchers("/get/resource").hasAuthority("a")
   .antMatchers("/get/**").permitAll();

登出

httpSecurity.logoutUrl()和httpSecurity.addLogoutHandler()是冲突的,通常只用其中之一。对于使用token且服务端不进行存储的情况,不需要请求服务端进行登出,直接由前端将token丢弃即可。

httpSecurity.logout().logoutUrl(&ldquo;/logout&rdquo;).logoutSuccessHandler(logoutSuccessHandler);

/**
* 自定义退出处理类 返回成功
*/
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
{
   @Autowired
   private TokenService tokenService;
   /**
    * 退出处理
    *
    * @return
    */
   @Override
   public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
           throws IOException, ServletException
   {
       LoginUser loginUser = tokenService.getLoginUser(request);
       if (StringUtils.isNotNull(loginUser))
       {
           String userName = loginUser.getUsername();
           // 删除用户缓存记录
           tokenService.delLoginUser(loginUser.getToken());
       }
       ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(HttpStatus.SUCCESS, "退出成功")));
   }
}

跨域

httpSecurity.cors()
.and()
.csrf().disable();

cors()允许跨域资源访问。

csrf().disable()禁用跨域安全验证。

认证失败处理类

/**
* 认证失败处理类 返回未授权
*/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable
{
   private static final long serialVersionUID = -8970718410437077606L;
   @Override
   public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
           throws IOException
   {
       int code = HttpStatus.UNAUTHORIZED;
       String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
       ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
   }
}

来源:https://blog.csdn.net/qq_41596778/article/details/129854644

0
投稿

猜你喜欢

手机版 软件编程 asp之家 www.aspxhome.com