Spring security之授权

前言

本篇为大家带来Spring security的授权,首先要理解一些概念,有关于:权限、角色、安全上下文、访问控制表达式、方法级安全性、访问决策管理器

一.授权的基本介绍

Spring Security 中的授权分为两种类型:

  • 基于角色的授权:以用户所属角色为基础进行授权,如管理员、普通用户等,通过为用户分配角色来控制其对资源的访问权限。

  • 基于资源的授权:以资源为基础进行授权,如 URL、方法等,通过定义资源所需的权限,来控制对该资源的访问权限。

Spring Security 提供了多种实现授权的机制,最常用的是使用基于注解的方式,建立起访问资源和权限之间的映射关系。

其中最常用的两个注解是 @Secured@PreAuthorize@Secured 注解是更早的注解,基于角色的授权比较适用,@PreAuthorize 基于 SpEL 表达式的方式,可灵活定义所需的权限,通常用于基于资源的授权。

二.修改User配置角色和权限

方法一.

使用SQL语句的方式查询该角色的权限,并且可以对它进行修改

根据用户id查询出对应的角色信息

sql 复制代码
 SELECT
            *
        FROM
            sys_user a,
            sys_user_role b,
            sys_role_module c,
            sys_module d
        WHERE a.id = b.user_id and
            b.role_id=c.role_id and
            c.module_id = d.id and
            a.id=#{id}

根据用户ID查询出角色对应的权限信息

sql 复制代码
select
	m.url
from
	sys_user u,sys_user_role ur,sys_role r,sys_role_module rm,sys_module m
where
    u.id=ur.userid and ur.roleid=r.roleid and
    r.roleid=rm.roleid and rm.moduleid=m.id and
    u.id=#{userid} and url is not null

但是并不推荐使用这种方法,当我们在实际开发中,要考虑到不同的数据表可能来自不同的库中,使用SQL查询时就会出现链表查询不同库的表的情况,所以,更多的时候我们会使用Java利用不同的操作对表进行依次查询作为条件最终得到结果

方法二.利用Java对表单一查询然后作为查询条件,最终查询出结果

java 复制代码
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService, UserDetailsService {

    @Autowired
    private IUserRoleService userRoleService;
    @Autowired
    private IRoleService roleService;
    @Autowired
    private IRoleModuleService roleModuleService;
    @Autowired
    private IModuleService moduleService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = getOne(new QueryWrapper<User>().eq("username", username));
        if(user==null){
            throw new UsernameNotFoundException("用户名无效");
        }
        //查询出身份
        //map遍历所有对象,返回新的数据放到新的集合中
        //filter 过滤流中的内容
        //collect将流中的元素变成一个集合
        List<Integer> role_ids = userRoleService
                .list(new QueryWrapper<UserRole>().eq("user_id", user.getId()))
                .stream().map(UserRole::getRoleId)
                .collect(Collectors.toList());
        //根据身份字段查询身份对应的名字字段
        List<String> roles = roleService
                .list(new QueryWrapper<Role>())
                .stream().map(Role::getRoleName)
                .collect(Collectors.toList());
        //根据身份id查询具备的权限id
        List<Integer> module_ids = roleModuleService
                .list(new QueryWrapper<RoleModule>().in("role_id", role_ids))
                .stream().map(RoleModule::getModuleId)
                .collect(Collectors.toList());
        //根据权限id查询对应的权限
        List<String> modules = moduleService
                .list(new QueryWrapper<Module>().in("id", module_ids))
                .stream().map(Module::getUrl)
                .collect(Collectors.toList());
        // 将权限字段加到身份中
        roles.addAll(modules);
        //将当前集合内容加到权限字段中
        List<SimpleGrantedAuthority> authorities = roles.stream()
                .map(SimpleGrantedAuthority::new)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        user.setAuthorities(authorities);
        return user;
    }
}

三.SpringSecurity配置类

当我们想要开启spring方法级安全时,只需要在任何 @Configuration实例上使用@EnableGlobalMethodSecurity 注解就能达到此目的。同时这个注解为我们提供了prePostEnabledsecuredEnabledjsr250Enabled 三种不同的机制来实现同一种功能。

修改WebSecurityConfig配置类,开启基于方法的安全认证机制,也就是说在web层的controller启用注解机制的安全确认。

java 复制代码
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {

    @Autowired
    private UserServiceImpl userDetailsService;
    @Autowired
    private ObjectMapper objectMapper;
    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        //创建DaoAuthenticationProvider
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        //设置userDetailsService,基于数据库方式进行身份认证
        provider.setUserDetailsService(userDetailsService);
        //配置密码编码器
        provider.setPasswordEncoder(passwordEncoder());
        return new ProviderManager(provider);
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                //antMatchers 匹配对应的路径
                //permitAll 允许
                .antMatchers("/").permitAll()
                //anyRequest 其余所有请求
                //authenticated 登录
                .anyRequest().authenticated()
                .and()
                .formLogin()
                //loginPage 登录页面
                .loginPage("/")
                //设置处理登录请求的接口
                .loginProcessingUrl("/userLogin")
                //用户的数据的参数
                .usernameParameter("username")
                .passwordParameter("password")
                //登录成功
                .successHandler((req, resp, auth) -> {
                    Object user = auth.getPrincipal();
                    objectMapper
                            .writeValue(resp.getOutputStream(), JsonResponseBody.success(user));
                })
                //登录失败
                .failureHandler(myAuthenticationFailureHandler)
                .and()
                .exceptionHandling()
                //权限不足
                .accessDeniedHandler((req, resp, ex) -> {
                    objectMapper
                            .writeValue(resp.getOutputStream(), JsonResponseBody.other(JsonResponseStatus.NO_ACCESS));
                })
                //没有认证
                .authenticationEntryPoint((req, resp, ex) -> {
                    objectMapper
                            .writeValue(resp.getOutputStream(), JsonResponseBody.other(JsonResponseStatus.NO_LOGIN));
                })
                .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/");
        http.csrf().disable();
        return http.build();
    }

}

这里需要注意的是:@EnableGlobalMethodSecurity是Spring Security提供的一个注解,用于启用方法级别的安全性。它可以在任何@Configuration类上使用,以启用Spring Security的方法级别的安全性功能。它接受一个或多个参数,用于指定要使用的安全注解类型和其他选项。以下是一些常用的参数

  • prePostEnabled:如果设置为true,则启用@PreAuthorize@PostAuthorize注解。默认值为false

  • securedEnabled:如果设置为true,则启用@Secured注解。默认值为false

  • jsr250Enabled:如果设置为true,则启用@RolesAllowed注解。默认值为false

  • proxyTargetClass:如果设置为true,则使用CGLIB代理而不是标准的JDK动态代理。默认值为false

使用@EnableGlobalMethodSecurity注解后,可以在应用程序中使用Spring Security提供的各种注解来保护方法,例如@Secured@PreAuthorize@PostAuthorize@RolesAllowed。这些注解允许您在方法级别上定义安全规则,以控制哪些用户可以访问哪些方法。

注解介绍:

注解 说明
@PreAuthorize 用于在方法执行之前对访问进行权限验证
@PostAuthorize 用于在方法执行之后对返回结果进行权限验证
@Secured 用于在方法执行之前对访问进行权限验证
@RolesAllowed 是Java标准的注解之一,用于在方法执行之前对访问进行权限验证

四.管理控制Controller层的权限

java 复制代码
@Controller
public class IndexController {

    @RequestMapping("/")
    public String toLogin() {
        return "login";
    }

    @RequestMapping("/userLogin")
    public String userLogin() {
        return "index";
    }

    @RequestMapping("/index")
    public String toIndex() {
        return "index";
    }

    @RequestMapping("/noAccess")
    public String noAccess() {
        return "accessDenied";
    }

    @ResponseBody
    @RequestMapping("/order_add")
    @PreAuthorize("hasAuthority('order:manager:add')")
    public String order_add() {
        return "订单新增";
    }

    @ResponseBody
    @PreAuthorize("hasAuthority('book:manager:add')")
    @RequestMapping("/book_add")
    public String book_add() {
        return "书本新增";
    }

}

在当前登录的用户必须拥有当前的权限字段才能进行访问,例如:book:manager:add

五.异常处理

AccessDeniedHandler是Spring Security提供的一个接口,用于处理访问被拒绝的情况。当用户尝试访问受保护资源但没有足够的权限时,Spring Security会调用AccessDeniedHandler来处理这种情况。

AccessDeniedHandler接口只有一个方法handle(),该方法接收HttpServletRequestHttpServletResponseAccessDeniedException三个参数。在handle()方法中,可以自定义响应的内容,例如返回一个自定义的错误页面或JSON响应。

创建AccessDeniedHandlerImpl类并实现AccessDeniedHandler接口,实现自定义的JSON响应。例如:

java 复制代码
package com.yu.security.resp;

import lombok.Getter;

@Getter
public enum JsonResponseStatus {

    OK(200, "OK"),
    UN_KNOWN(500, "未知错误"),
    RESULT_EMPTY(1000, "查询结果为空"),
    NO_ACCESS(3001, "没有权限"),
    NO_LOGIN(4001, "没有登录"),
    LOGIN_FAILURE(5001, "登录失败"),
    ;

    private final Integer code;
    private final String msg;

    JsonResponseStatus(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

}

单独写一个接口进行实现,并将出现异常后的操作在里面实现

java 复制代码
package com.yu.security.config;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yu.security.pojo.User;
import com.yu.security.resp.JsonResponseBody;
import com.yu.security.resp.JsonResponseStatus;
import com.yu.security.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Autowired
    private ObjectMapper objectMapper;

    // 在redis中定义一个键当登录失败是就对那个键的值进行加一

    //如果大于三就锁住

    @Autowired
    private IUserService userService;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        if(1==2){
            User user = userService.getOne(new QueryWrapper<User>().eq("username", request.getParameter("username")));
            user.setAccountNonLocked(false);
            userService.updateById(user);
        }
        objectMapper.writeValue(response.getOutputStream(), JsonResponseBody.other(JsonResponseStatus.LOGIN_FAILURE));
    }

}

在当前例子中:我们通过在配置类引入当前接口,并实现当前接口,在实现类中,对登录失败进行 对应的操作,在Redis中定义一个键当登录失败是就对那个键的值进行加一,如果大于三就对当前账号进行冻结

复制代码
相关推荐
没有bug.的程序员4 分钟前
电商系统分布式架构实战:从单体到微服务的演进之路
java·分布式·微服务·云原生·架构·监控体系·指标采集
Query*13 分钟前
Java 设计模式——代理模式:从静态代理到 Spring AOP 最优实现
java·设计模式·代理模式
梵得儿SHI15 分钟前
Java 反射机制深度解析:从对象创建到私有成员操作
java·开发语言·class对象·java反射机制·操作类成员·三大典型·反射的核心api
JAVA学习通18 分钟前
Spring AI 核心概念
java·人工智能·spring·springai
望获linux20 分钟前
【实时Linux实战系列】实时 Linux 在边缘计算网关中的应用
java·linux·服务器·前端·数据库·操作系统
绝无仅有29 分钟前
面试真实经历某商银行大厂数据库MYSQL问题和答案总结(二)
后端·面试·github
绝无仅有30 分钟前
通过编写修复脚本修复 Docker 启动失败(二)
后端·面试·github
..Cherry..33 分钟前
【java】jvm
java·开发语言·jvm
老K的Java兵器库42 分钟前
并发集合踩坑现场:ConcurrentHashMap size() 阻塞、HashSet 并发 add 丢数据、Queue 伪共享
java·后端·spring
冷冷的菜哥1 小时前
go邮件发送——附件与图片显示
开发语言·后端·golang·邮件发送·smtp发送邮件