Spring security之授权
作者:mmseoamin日期:2023-12-25

前言

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

一.授权的基本介绍

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

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

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

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

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

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

    方法一.

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

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

     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查询出角色对应的权限信息

    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对表单一查询然后作为查询条件,最终查询出结果

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

    三.SpringSecurity配置类

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

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

    @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层的权限

    @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(),该方法接收HttpServletRequest、HttpServletResponse和AccessDeniedException三个参数。在handle()方法中,可以自定义响应的内容,例如返回一个自定义的错误页面或JSON响应。

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

    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;
        }
    }
    

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

    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().eq("username", request.getParameter("username")));
                user.setAccountNonLocked(false);
                userService.updateById(user);
            }
            objectMapper.writeValue(response.getOutputStream(), JsonResponseBody.other(JsonResponseStatus.LOGIN_FAILURE));
        }
    }
    

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