授权
重点剖析
AccessDecisionManager
AccessDecisionManager (访问决策管理器),用来决定此次访问是否被允许。
AccessDecisionVoter
AccessDecisionVoter (访问决定投票器),投票器会检查用户是否具备应有的角色,进而投出赞成、反对或者弃权票。
AccesDecisionVoter 和 AccessDecisionManager 都有众多的实现类,在 AccessDecisionManager 中会换个遍历 AccessDecisionVoter,进而决定是否允许用户访问,因而 AaccesDecisionVoter 和 AccessDecisionManager 两者的关系类似于 AuthenticationProvider 和 ProviderManager 的关系。
ConfigAttribute
ConfigAttribute,用来保存授权时的角色信息
在 Spring Security 中,用户请求一个资源(通常是一个接口或者一个 Java 方法)需要的角色会被封装成一个 ConfigAttribute 对象,在 ConfigAttribute 中只有一个 getAttribute方法,该方法返回一个 String 字符串,就是角色的名称。一般来说,角色名称都带有一个 ROLE_ 前缀,投票器 AccessDecisionVoter 所做的事情,其实就是比较用户所具各的角色和请求某个
资源所需的 ConfigAtuibute 之间的关系。
调用步骤
首先. 调用对应接口时,需要的角色会被封装到ConfigAttribute 对象中来保存权限信息 ,只有保存了权限信息才能进行调用 ,在ConfigAttribute中有方法名为getAttribute的,它返回的是一个字符串,就是角色名称。
接下来. 通过AccessDecisionManager (访问决策管理器) 来对不同的权限设置不同的AccessDecisionVoter (访问决定投票器),从而实现决定用户是否能够访问
所需的拦截器FilterSecurityInterceptor
实现案例
实战
在前面的案例中,我们配置的 URL 拦截规则和请求 URL 所需要的权限都是通过代码来配置的,这样就比较死板,如果想要调整访问某一个 URL 所需要的权限,就需要修改代码。
动态管理权限规则就是我们将 URL 拦截规则和访问 URI 所需要的权限都保存在数据库中,这样,在不修改源代码的情况下,只需要修改数据库中的数据,就可以对权限进行调整。
用户<--中间表--> 角色 <--中间表--> 菜单
库表设计
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for menu -- ---------------------------- DROP TABLE IF EXISTS `menu`; CREATE TABLE `menu` ( `id` int(11) NOT NULL AUTO_INCREMENT, `pattern` varchar(128) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of menu -- ---------------------------- BEGIN; INSERT INTO `menu` VALUES (1, '/admin/**'); INSERT INTO `menu` VALUES (2, '/user/**'); INSERT INTO `menu` VALUES (3, '/guest/**'); COMMIT; -- ---------------------------- -- Table structure for menu_role -- ---------------------------- DROP TABLE IF EXISTS `menu_role`; CREATE TABLE `menu_role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `mid` int(11) DEFAULT NULL, `rid` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `mid` (`mid`), KEY `rid` (`rid`), CONSTRAINT `menu_role_ibfk_1` FOREIGN KEY (`mid`) REFERENCES `menu` (`id`), CONSTRAINT `menu_role_ibfk_2` FOREIGN KEY (`rid`) REFERENCES `role` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of menu_role -- ---------------------------- BEGIN; INSERT INTO `menu_role` VALUES (1, 1, 1); INSERT INTO `menu_role` VALUES (2, 2, 2); INSERT INTO `menu_role` VALUES (3, 3, 3); INSERT INTO `menu_role` VALUES (4, 3, 2); COMMIT; -- ---------------------------- -- Table structure for role -- ---------------------------- DROP TABLE IF EXISTS `role`; CREATE TABLE `role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(32) DEFAULT NULL, `nameZh` varchar(32) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of role -- ---------------------------- BEGIN; INSERT INTO `role` VALUES (1, 'ROLE_ADMIN', '系统管理员'); INSERT INTO `role` VALUES (2, 'ROLE_USER', '普通用户'); INSERT INTO `role` VALUES (3, 'ROLE_GUEST', '游客'); COMMIT; -- ---------------------------- -- Table structure for user -- ---------------------------- DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(32) DEFAULT NULL, `password` varchar(255) DEFAULT NULL, `enabled` tinyint(1) DEFAULT NULL, `locked` tinyint(1) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of user -- ---------------------------- BEGIN; INSERT INTO `user` VALUES (1, 'admin', '{noop}123', 1, 0); INSERT INTO `user` VALUES (2, 'user', '{noop}123', 1, 0); INSERT INTO `user` VALUES (3, 'blr', '{noop}123', 1, 0); COMMIT; -- ---------------------------- -- Table structure for user_role -- ---------------------------- DROP TABLE IF EXISTS `user_role`; CREATE TABLE `user_role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `uid` int(11) DEFAULT NULL, `rid` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `uid` (`uid`), KEY `rid` (`rid`), CONSTRAINT `user_role_ibfk_1` FOREIGN KEY (`uid`) REFERENCES `user` (`id`), CONSTRAINT `user_role_ibfk_2` FOREIGN KEY (`rid`) REFERENCES `role` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of user_role -- ---------------------------- BEGIN; INSERT INTO `user_role` VALUES (1, 1, 1); INSERT INTO `user_role` VALUES (2, 1, 2); INSERT INTO `user_role` VALUES (3, 2, 2); INSERT INTO `user_role` VALUES (4, 3, 3); COMMIT; SET FOREIGN_KEY_CHECKS = 1;
创建 springboot 应用
引入依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- <dependency>--> <!-- <groupId>org.springframework.boot</groupId>--> <!-- <artifactId>spring-boot-starter-thymeleaf</artifactId>--> <!-- </dependency>--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <!--数据库相关的--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.38</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.8</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency> </dependencies>
配置配置文件
server.port=8080 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=UTF-8 spring.datasource.username=root spring.datasource.password=root mybatis.mapper-locations=classpath:com/blr/mapper/*.xml mybatis.type-aliases-package=com.blr.entity
创建实体类
public class User implements UserDetails { private Integer id; private String password; private String username; private boolean enabled; private boolean locked; private List<Role> roles; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return roles.stream().map(r -> new SimpleGrantedAuthority(r.getName())).collect(Collectors.toList()); } @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return !locked; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return enabled; } public void setId(Integer id) { this.id = id; } public void setPassword(String password) { this.password = password; } public void setUsername(String username) { this.username = username; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public void setLocked(boolean locked) { this.locked = locked; } public void setRoles(List<Role> roles) { this.roles = roles; } public Integer getId() { return id; } public List<Role> getRoles() { return roles; } }
public class Role { private Integer id; private String name; private String nameZh; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getNameZh() { return nameZh; } public void setNameZh(String nameZh) { this.nameZh = nameZh; } }
public class Menu { private Integer id; private String pattern; private List<Role> roles; public List<Role> getRoles() { return roles; } public void setRoles(List<Role> roles) { this.roles = roles; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getPattern() { return pattern; } public void setPattern(String pattern) { this.pattern = pattern; } }
创建 mapper 接口
@Mapper public interface UserMapper { List<Role> getUserRoleByUid(Integer uid); User loadUserByUsername(String username); }
@Mapper public interface MenuMapper { List<Menu> getAllMenu(); }
创建 mapper 文件
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.blr.mapper.UserMapper"> <select id="loadUserByUsername" resultType="com.blr.entity.User"> select * from user where username = #{username}; </select> <select id="getUserRoleByUid" resultType="com.blr.entity.Role"> select r.* from role r, user_role ur where ur.uid = #{uid} and ur.rid = r.id </select> </mapper>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.blr.mapper.MenuMapper"> <resultMap id="MenuResultMap" type="com.blr.entity.Menu"> <id property="id" column="id"/> <result property="pattern" column="pattern"></result> <collection property="roles" ofType="com.blr.entity.Role"> <id column="rid" property="id"/> <result column="rname" property="name"/> <result column="rnameZh" property="nameZh"/> </collection> </resultMap> <select id="getAllMenu" resultMap="MenuResultMap"> select m.*, r.id as rid, r.name as rname, r.nameZh as rnameZh from menu m left join menu_role mr on m.`id` = mr.`mid` left join role r on r.`id` = mr.`rid` </select> </mapper>
创建 service 接口
@Service public class UserService implements UserDetailsService { private final UserMapper userMapper; @Autowired public UserService(UserMapper userMapper) { this.userMapper = userMapper; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMapper.loadUserByUsername(username); if (user == null) { throw new UsernameNotFoundException("用户不存在"); } user.setRoles(userMapper.getUserRoleByUid(user.getId())); return user; } }
@Service public class MenuService { private final MenuMapper menuMapper; @Autowired public MenuService(MenuMapper menuMapper) { this.menuMapper = menuMapper; } public List<Menu> getAllMenu() { return menuMapper.getAllMenu(); } }
创建测试 controller
@RestController public class HelloController { @GetMapping("/admin/hello") public String admin() { return "hello admin"; } @GetMapping("/user/hello") public String user() { return "hello user"; } @GetMapping("/guest/hello") public String guest() { return "hello guest"; } @GetMapping("/hello") public String hello() { return "hello"; } }
创建 CustomSecurityMetadataSource
@Component public class CustomSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { private final MenuService menuService; @Autowired public CustomSecurityMetadataSource(MenuService menuService) { this.menuService = menuService; } AntPathMatcher antPathMatcher = new AntPathMatcher(); @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { String requestURI = ((FilterInvocation) object).getRequest().getRequestURI(); List<Menu> allMenu = menuService.getAllMenu(); for (Menu menu : allMenu) { if (antPathMatcher.match(menu.getPattern(), requestURI)) { String[] roles = menu.getRoles().stream().map(r -> r.getName()).toArray(String[]::new); return SecurityConfig.createList(roles); } } return null; } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> clazz) { return FilterInvocation.class.isAssignableFrom(clazz); } }
配置 Security 配置
package Authorize.config; import Authorize.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configurers.UrlAuthorizationConfigurer; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 将之前定义的权限数据源 注入 */ private final CustomerSecurityMetaSource customerSecurityMetaSource; private final UserService userService; @Autowired public SecurityConfig(CustomerSecurityMetaSource customerSecurityMetaSource, UserService userService) { this.customerSecurityMetaSource = customerSecurityMetaSource; this.userService = userService; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService); } @Override protected void configure(HttpSecurity http) throws Exception { //1. 获取工厂对象 ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class); //2. 设置自定义的url权限处理 http.apply(new UrlAuthorizationConfigurer<>(applicationContext)) .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O object) { object.setSecurityMetadataSource(customerSecurityMetaSource); object.setRejectPublicInvocations(false); return object; } }); http.formLogin() .and() .csrf().disable(); } }
启动入口类进行测试
说明
部分的代码是由@编程不良人的学习教程中提供的,仅作为自己的学习参考使用!
作者教程链接 : Spring Security 最新实战教程