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站视频里的原理解析,虽然看的很吐,不过看过后会更加清晰每一步是做什么的有什么用。