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

 更新时间:2023年04月23日 08:56:06   作者:Wen先森  
Spring Security是一个功能强大的安全框架,提供了基于角色的访问控制、身份验证、授权等安全功能,可轻松保护Web应用程序的安全,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧

说明

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

简单使用

登录校验流程

引入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(“p1”)表示需要p1权限
  • hasAnyAuthority(“p1”, “p2”)表示p1或p2权限皆可

同理:

  • hasRole(“p1”)表示需要p1角色
  • hasAnyRole(“p1”, “p2”)表示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(“/logout”).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)));
    }
}

到此这篇关于Spring Security实现基于角色的访问控制框架的文章就介绍到这了,更多相关Spring Security访问控制框架内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java中使用json与前台Ajax数据交互的方法

    Java中使用json与前台Ajax数据交互的方法

    这篇文章主要为大家详细介绍了Java中使用json与前台Ajax数据交互的方法,分享Ajax获取显示Json数据的一种方法,感兴趣的小伙伴们可以参考一下
    2016-06-06
  • MyBatis-Plus通过插件将数据库表生成Entiry,Mapper.xml,Mapper.class的方式

    MyBatis-Plus通过插件将数据库表生成Entiry,Mapper.xml,Mapper.class的方式

    今天小编就为大家分享一篇关于MyBatis-Plus通过插件将数据库表生成Entiry,Mapper.xml,Mapper.class的方式,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-02-02
  • MyBatis中多条件查询商品的三种方法及区别

    MyBatis中多条件查询商品的三种方法及区别

    本文主要介绍了MyBatis中多条件查询商品的三种方法及区别,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-01-01
  • springMarchal集成xStream的完整示例代码

    springMarchal集成xStream的完整示例代码

    这篇文章主要介绍了springMarchal集成xStream的示例代码,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-03-03
  • Java中的system.getProperty()的作用及使用方法

    Java中的system.getProperty()的作用及使用方法

    System.getProperty() 方法用于获取系统属性的值,该方法接受一个字符串参数,表示要获取的系统属性的名称,返回值为字符串类型,表示该属性的值,接下来通过本文给大家介绍Java中的system.getProperty()的作用及使用方法,感兴趣的朋友跟随小编一起看看吧
    2023-05-05
  • Java中时间戳的获取和转换的示例分析

    Java中时间戳的获取和转换的示例分析

    这篇文章主要介绍了Java中时间戳的获取和转换的示例分析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-07-07
  • macbook中springboot的jmeter压测示例

    macbook中springboot的jmeter压测示例

    这篇文章主要介绍了macbook中springboot的jmeter压测示例详解,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-11-11
  • 关于spring data jpa 模糊查询like的坑点

    关于spring data jpa 模糊查询like的坑点

    这篇文章主要介绍了关于spring data jpa 模糊查询like的坑点,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-12-12
  • Mybatisplus创建Spring Boot工程打包错误的解决方式

    Mybatisplus创建Spring Boot工程打包错误的解决方式

    最近在实战springboot遇到了一些坑,记录一下,下面这篇文章主要给大家介绍了关于Mybatisplus创建Spring Boot工程打包错误的解决方式,文中通过图文介绍的介绍的非常详细,需要的朋友可以参考下
    2023-03-03
  • 学习spring事务与消息队列

    学习spring事务与消息队列

    这篇文章主要为大家详细介绍了spring事务与消息队列,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-10-10

最新评论