Spring Security

技术概述

  • Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它是用于保护基于Spring的应用程序的实际标准。Spring Security 致力于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring Security的真正强大之处在于可以轻松扩展以满足自定义要求
  • 起初学这个是为了软件工程项目,后面没用到,就纯属学习,扩宽知识面。
  • 困难的是,数据级权限控制。这是和业务直接挂钩的,最复杂,而且会经常因为客户需求表达不到位、开发人员需求理解不到位、系统框架库表结构发生变化,而不断变化的。这种变化,不仅需要编码,而且还需要重新测试。甚至这种变化会波及到其他模块,甚至整个系统。

技术详述

  • 1.在pom.xml中添加依赖

    <!-- spring security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    
  • 2.创建认证和授权对应的表以及关联表
    user、role、permission(其实也可以只用用户、角色表实现,加了权限更细一点)、user_role
    比较特殊的就是permission表

    CREATE TABLE `permission` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `url` varchar(255) DEFAULT NULL,
      `role_id` int(11) DEFAULT NULL,
      `permission` varchar(255) DEFAULT NULL,
      PRIMARY KEY (`id`),
      KEY `fk_roleId` (`role_id`),
      CONSTRAINT `fk_roleId` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
    );
    
    

    下面是几个表的一些基本数据:

    user

    role

    user_role

    permission

  • 3.根据数据库表创建对应实体类

  • 4.编写对应的Mapper、Service(Service的就不贴出来了,没有复杂的过程,就是调用了对应的Mapper接口)

    @Mapper
    public interface UserMapper {
        /**
         * @param id 用户编号
         * @return 用户信息
         */
        @Select("SELECT * FROM user WHERE id = #{id}")
        User selectById(Integer id);
    
        /**
         * @param name 用户名
         * @return 用户信息
         */
        @Select("SELECT * FROM user WHERE name = #{name}")
        User selectByName(String name);
    }
    
    @Mapper
    public interface RoleMapper {
        /**
         * @param id 角色编号
         * @return 角色信息
         */
        @Select("SELECT * FROM role WHERE id = #{id}")
        Role selectById(Integer id);
    
        /**
         * @param name 角色名
         * @return 角色信息
         */
        @Select("SELECT * FROM role WHERE name = #{name}")
        Role selectByName(String name);
    }
    
    @Mapper
    public interface PermissionMapper {
        /**
         * @param roleId 角色编号
         * @return 角色的权限集合
         */
        @Select("SELECT * FROM permission WHERE role_id=#{roleId}")
        List<Permission> listByRoleId(Integer roleId);
    }
    
    @Mapper
    public interface UserRoleMapper {
        /**
         * @param userId 用户编号
         * @return 用户的角色集合
         */
        @Select("SELECT * FROM user_role WHERE user_id = #{userId}")
        List<UserRole> listByUserId(Integer userId);
    }
    
  • 5.创建自定义的UserDetailsService,将用户信息和用户角色信息注入进来。

    @Service("userDetailsService")
    public class CustomUserDetailsService implements UserDetailsService {
        @Autowired
        private UserService userService;
    
        @Autowired
        private RoleService roleService;
    
        @Autowired
        private UserRoleService userRoleService;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            // 从数据库中取出用户信息
            User user = userService.selectByName(username);
    
            // 判断用户是否存在
            if (user == null) {
                throw new UsernameNotFoundException("用户名不存在");
            }
    
            // 添加用户的角色信息
            List<UserRole> userRoles = userRoleService.listByUserId(user.getId());
            for (UserRole userRole : userRoles) {
                Role role = roleService.selectById(userRole.getRoleId());
                authorities.add(new SimpleGrantedAuthority(role.getName()));
            }
    
            // 返回UserDetails实现类
            return new org.springframework.security.core.userdetails.User(user.getName(), user.getPassword(), authorities);
        }
    
    
    }
    
  • 6.创建自定义的PermissionEvaluator,判断访问的Url和权限与角色是否符合

    @Component
    public class CustomPermissionEvaluator implements PermissionEvaluator {
        @Autowired
        private PermissionService permissionService;
        @Autowired
        private RoleService roleService;
    
        @Override
        public boolean hasPermission(Authentication authentication, Object targetUrl, Object targetPermission) {
            // 获得loadUserByUsername()方法的结果
            User user = (User)authentication.getPrincipal();
            // 获得loadUserByUsername()中注入的角色
            Collection<GrantedAuthority> authorities = user.getAuthorities();
    
            // 遍历用户所有角色
            for(GrantedAuthority authority : authorities) {
                String roleName = authority.getAuthority();
                Integer roleId = roleService.selectByName(roleName).getId();
                // 得到角色所有的权限
                List<Permission> permissionList = permissionService.listByRoleId(roleId);
    
                // 遍历permissionList
                for(Permission sysPermission : permissionList) {
                    // 获取权限集
                    List permissions = sysPermission.getPermissions();
                    // 如果访问的Url和权限用户符合的话,返回true
                    if(targetUrl.equals(sysPermission.getUrl())
                            && permissions.contains(targetPermission)) {
                        return true;
                    }
                }
    
            }
    
            return false;
        }
    
        @Override
        public boolean hasPermission(Authentication authentication, Serializable serializable, String s, Object o) {
            return false;
        }
    }
    
  • 7.配置WebSecurityConfig,该类是 Spring Security 的配置类,该类的三个注解分别是标识该类是配置类、开启 Security 服务、开启全局 Securtiy 注解。

    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private CustomUserDetailsService userDetailsService;
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin();
            
            // 关闭CSRF跨域
            http.csrf().disable();
        }
    
        //直接载入内存
        /*@Autowired
        public void configureGlobal(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception{
            authenticationManagerBuilder.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                    .withUser("user").password(new BCryptPasswordEncoder().encode("123")).roles("USER");
        }*/
    
        //jdbc获取用户信息,密码的加密方式(5.0 版本强制要求设置),我是为了简单,就假装加密了
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService).passwordEncoder(new PasswordEncoder() {
                @Override
                public String encode(CharSequence charSequence) {
                    return charSequence.toString();
                }
    
                @Override
                public boolean matches(CharSequence charSequence, String s) {
                    return s.equals(charSequence.toString());
                }
            });
        }
    
        /*@Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService)
                    .passwordEncoder(new BCryptPasswordEncoder());
        }*/
    }
    
  • 8.编写Controller,在需要权限的操作前加注解@PreAuthorize("hasPermission(url,操作)")

    @Controller
    @Validated
    @RequestMapping("/product")
    /*@PreAuthorize("hasRole('ROLE_ADMIN')")*/
    public class ProductController {
    
        @Autowired
        ProductService productService;
    
        @Autowired
        CategoryService categoryService;
    
        @RequestMapping("/list")
        @PreAuthorize("hasPermission('/product','r')")
        public String selectAllProducts(Model model, @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "8") Integer limit){
            PageInfo<Product> pageInfo = productService.selectAllProducts(page,limit);
    
            model.addAttribute("productList",pageInfo.getList());
            model.addAttribute("pageNum", pageInfo.getPageNum());
            //获得一页显示的条数
            model.addAttribute("pageSize", pageInfo.getPageSize());
            //是否是第一页
            model.addAttribute("isFirstPage", pageInfo.isIsFirstPage());
            //获得总页数
            model.addAttribute("totalPages", pageInfo.getPages());
            //是否是最后一页
            model.addAttribute("isLastPage", pageInfo.isIsLastPage());
            return "product/list";
        }
    
        @GetMapping("/toEdit")
        public String toEdit(@Min(value = 1,message = "错误的id") Integer id, Model model) {
            model.addAttribute("product", productService.selectProductById(id));
            model.addAttribute("categories", categoryService.selectAllCategories());
            return "product/edit";
        }
    
        @RequestMapping("/edit")
        @PreAuthorize("hasPermission('/product','u')")
        public String edit( Product product) {
            productService.updateProduct(product);
            return "redirect:/product/list";
        }
    }
    

流程图

技术使用中遇到的问题和解决过程

  • 问题:如果只用用户角色来进行权限验证的话,角色名不能随便乱起

    解决:Spring Security规定角色格式为ROLE_XXX

  • 问题:SpringSecurity 5.0+ 版本变动较多,且不兼容之前版本

    解决:确保 SpringBoot 版本为 2.0,避免掉大部分的坑

  • 问题:userDetailsService注入进来不指定密码加密方式不行

    解决:Spring Security 5.0 版本强制要求设置密码的加密方式,

    ​ 通过 auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder()(加密方式))解决

总结

  • spring security 的核心功能主要包括:
    • 认证 (你是谁)
    • 授权 (你能干什么)
    • 攻击防护 (防止伪造身份)
  • 第一次接触 Spring Security是在Github实训上(虽然不是我写的,但是看上去很牛的样子),然后软工实践项目原来打算使用Spring Security 的(虽然最后没有用),但是我还是去学了一下,早就听闻 Spring Security 功能强大但上手困难,但其实写一遍下来就大概知道是怎么一回事了,有能力的话可以看看B站视频里的原理解析,虽然看的很吐,不过看过后会更加清晰每一步是做什么的有什么用。

列出参考文献、参考博客

posted @ 2021-06-28 16:26  WiLLyy  阅读(84)  评论(2编辑  收藏  举报