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

Java
317
0
0
2023-05-12
标签   Spring
目录
  • 说明
  • Spring Security与Shiro的区别
  • 简单使用
  • 登录校验流程
  • 引入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)));
    }
}