目錄
本節大綱
一、權限管理
1. 認證
2. 授權
二、授權核心概念
三、權限管理策略
1. 基于 URL 權限管理
權限表達式
2. 基于 方法 權限管理
@EnableGlobalMethodSecurity
四、基本用法
五、原理分析
六、實戰
1. 簡介
2. 庫表設計
3. 創建 springboot 應用
本節大綱
- 什么是權限管理
- 權限管理核心概念
- Spring Security 權限管理策略
- 基于 URL 地址的權限管理
- 基于方法的權限管理
- 實戰
一、權限管理
1. 認證
身份認證,就是判斷一個用戶是否為合法用戶的處理過程。Spring Security 中支持多種不同方式的認證,但是無
論開發者使用那種方式認證,都不會影響授權功能使用。因為 Spring Security 很好做到了認證和授權解耦。
2. 授權
授權,即訪問控制,控制誰能訪問哪些資源。簡單的理解授權就是根據系統提前設置好的規則,給用戶分配可以
訪問某一個資源的權限,用戶根據自己所具有權限,去執行相應操作。
二、授權核心概念
在前面學習認證過程中,我們得知認證成功之后會將當前登錄用戶信息保存到 Authentication 對象中,
Authentication 對象中有一個getAuthorities() 方法,用來返回當前登錄用戶具備的權限信息,也就是當前用戶
具有權限信息。
該方法的返回值為 Collection<? extends GrantedAuthority>,
當需要進行權限判斷時,就回根據集合返回權限信息調用相應方法進行判斷。
那么問題來了,針對于這個返回值 GrantedAuthority 應該如何理解呢? 是角色還是權限?
我們針對于授權可以是基于角色權限管理和基于資源權限管理,從設計層面上來說,角色和權限是兩個完全不同
的東西:
權限是一些具體操作,角色則是某些權限集合。
如:READ_BOOK 和 ROLE_ADMIN 是完全不同的。因此至于返回值是什么取決于你的業務設計情況:
- 基于角色權限設計就是: 用戶<=>角色<=>資源三者關系 返回就是用戶的角色
- 基于資源權限設計就是: 用戶<=>權限<=>資源 三者關系 返回就是用戶的權限
- 基于角色和資源權限設計就是: 用戶<=>角色<=>權限<=>資源 返回統稱為用戶的權限
為什么可以統稱為權限,因為從代碼層面角色和權限沒有太大不同都是權限,特別是在 Spring Security 中,角色
和權限處理方式基本上都是一樣的。唯一區別 SpringSecurity 在很多時候會自動給角色添加一個ROLE_前綴,而
權限則不會自動添加。
三、權限管理策略
Spring Security 中提供的權限管理策略主要有兩種類型:
- 基于過濾器(URL)的權限管理 (FilterSecurityInterceptor)
-
- 基于過濾器的權限管理主要是用來攔截 HTTP 請求,攔截下來之后,根據 HTTP 請求地址進行權限校驗。
- 基于 AOP (方法)的權限管理 ? (MethodSecurityInterceptor)
-
- 基于 AOP 權限管理主要是用來處理方法級別的權限問題。
當需要調用某一個方法時,通過 AOP 將操作攔截下來,然后判斷用戶是否具備相關的權限。
- 基于 AOP 權限管理主要是用來處理方法級別的權限問題。
1. 基于 URL 權限管理
- 開發 controller
@RestController
public class DemoController {@GetMapping("/admin")public String admin() {return "admin ok";}@GetMapping("/user")public String user() {return "user ok";}@GetMapping("/getInfo")public String getInfo() {return "info ok";}
}
- 配置授權
package com.blr.config;import org.springframework.context.annotation.Configuration;
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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {//創建內存數據源public UserDetailsService userDetailsService() {InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("ADMIN").build());inMemoryUserDetailsManager.createUser(User.withUsername("win7").password("{noop}123").roles("USER").build());inMemoryUserDetailsManager.createUser(User.withUsername("lisi").password("{noop}123").roles("READ_BOOK").build());return inMemoryUserDetailsManager;}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService());}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests().antMatchers("/admin/**").hasRole("ADMIN").antMatchers("/user/**").hasAnyRole("USER", "ADMIN").antMatchers("/getInfo").hasRole("READ_BOOK").anyRequest().authenticated().and().formLogin().and().csrf().disable();}
}
- 啟動項目測試
權限表達式
方法 | 說明 |
hasAuthority(String authority) | 當前用戶是否具備指定權限 |
hasAnyAuthority(String... authorities) | 當前用戶是否具備指定權限中任意一個 |
hasRole(String role) | 當前用戶是否具備指定角色 |
hasAnyRole(String... roles); | 當前用戶是否具備指定角色中任意一個 |
permitAll(); | 放行所有請求/調用 |
denyAll(); | 拒絕所有請求/調用 |
isAnonymous(); | 當前用戶是否是一個匿名用戶 |
isAuthenticated(); | 當前用戶是否已經認證成功 |
isRememberMe(); | 當前用戶是否通過 Remember-Me 自動登錄 |
isFullyAuthenticated(); | 當前用戶是否既不是匿名用戶又不是通過 Remember-Me 自動登錄的 |
hasPermission(Object targetId, Object permission); | 當前用戶是否具備指定目標的指定權限信息 |
hasPermission(Object targetId, String targetType, Object permission); | 當前用戶是否具備指定目標的指定權限信息 |
2. 基于 方法 權限管理
基于方法的權限管理主要是通過 A0P 來實現的,Spring Security 中通過 MethodSecurityInterceptor 來提供相
關的實現。
不同在于 FilterSecurityInterceptor 只是在請求之前進行前置處理,MethodSecurityInterceptor 除了前置處理
外還可以進行后置處理。
前置處理就是在請求之前判斷是否具備相應的權限,后置處理則是對方法的執行結果進行二次過濾。前置處理和
后置處理分別對應了不同的實現類。
@EnableGlobalMethodSecurity
EnableGlobalMethodSecurity 該注解是用來開啟權限注解,用法如下:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled=true,securedEnabled=true, jsr250Enabled=true)
public class SecurityConfig extends WebsecurityConfigurerAdapter{}
- perPostEnabled:開啟 Spring Security 提供的四個權限注解,@PostAuthorize、@PostFilter、@PreAuthorize 以及@PreFilter。
- securedEnabled:開啟 Spring Security 提供的 @Secured 注解支持,該注解不支持權限表達式
- jsr250Enabled:開啟 JSR-250 提供的注解,主要是@DenyAll、@PermitAll、@RolesAll 同樣這些注解也不支持權限表達式
# 以上注解含義如下:
- @PostAuthorize: 在目前標方法執行之后進行權限校驗。
- @PostFiter: 在目標方法執行之后對方法的返回結果進行過濾。
- @PreAuthorize:在目標方法執行之前進行權限校驗。
- @PreFiter:在目前標方法執行之前對方法參數進行過濾。
- @Secured:訪問目標方法必須具各相應的角色。
- @DenyAll:拒絕所有訪問。
- @PermitAll:允許所有訪問。
- @RolesAllowed:訪問目標方法必須具備相應的角色。
這些基于方法的權限管理相關的注解,一般來說只要設置 prePostEnabled=true 就夠用了。
四、基本用法
- 開啟注解使用
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled=true,securedEnabled=true, jsr250Enabled=true)
public class SecurityConfig extends WebsecurityConfigurerAdapter{}
- 使用注解
@RestController
@RequestMapping("/hello")
public class AuthorizeMethodController {@PreAuthorize("hasRole('ADMIN') and authentication.name=='root'")@GetMappingpublic String hello() {return "hello";}@PreAuthorize("authentication.name==#name")@GetMapping("/name")public String hello(String name) {return "hello:" + name;}@PreFilter(value = "filterObject.id%2!=0",filterTarget = "users")@PostMapping("/users") //filterTarget 必須是 數組 集合public void addUsers(@RequestBody List<User> users) {System.out.println("users = " + users);}@PostAuthorize("returnObject.id==1")@GetMapping("/userId")public User getUserById(Integer id) {return new User(id, "blr");}@PostFilter("filterObject.id%2==0")@GetMapping("/lists")public List<User> getAll() {List<User> users = new ArrayList<>();for (int i = 0; i < 10; i++) {users.add(new User(i, "blr:" + i));}return users;}@Secured({"ROLE_USER"}) //只能判斷角色@GetMapping("/secured")public User getUserByUsername() {return new User(99, "secured");}@Secured({"ROLE_ADMIN","ROLE_USER"}) //具有其中一個即可@GetMapping("/username")public User getUserByUsername2(String username) {return new User(99, username);}@PermitAll@GetMapping("/permitAll")public String permitAll() {return "PermitAll";}@DenyAll@GetMapping("/denyAll")public String denyAll() {return "DenyAll";}@RolesAllowed({"ROLE_ADMIN","ROLE_USER"}) //具有其中一個角色即可@GetMapping("/rolesAllowed")public String rolesAllowed() {return "RolesAllowed";}
}
五、原理分析
- ConfigAttribute 在 Spring Security 中,用戶請求一個資源(通常是一個接口或者一個 Java 方法)需要的角色會被封裝成一個ConfigAttribute 對象,在 ConfigAttribute 中只有一個 getAttribute方法,該方法返回一個 String 字符串,就是角色的名稱。
一般來說,角色名稱都帶有一個 ROLE_ 前綴,投票器 AccessDecisionVoter 所做的事情,其實就是比較用戶所具各的角色和請求某個資源所需的 ConfigAtuibute 之間的關系。 - AccesDecisionVoter 和 AccessDecisionManager 都有眾多的實現類,在 AccessDecisionManager 中會換個遍歷AccessDecisionVoter,進而決定是否允許用戶訪問,因而 AaccesDecisionVoter 和 AccessDecisionManager 兩者的關系類似于AuthenticationProvider 和 ProviderManager 的關系。
六、實戰
1. 簡介
在前面的案例中,我們配置的 URL 攔截規則和請求 URL 所需要的權限都是通過代碼來配置的,這樣就比較死
板,如果想要調整訪問某一個 URL 所需要的權限,就需要修改代碼。
動態管理權限規則就是我們將 URL 攔截規則和訪問 URI 所需要的權限都保存在數據庫中,這樣,在不修改源代碼
的情況下,只需要修改數據庫中的數據,就可以對權限進行調整。
用戶<--中間表--> 角色 <--中間表--> 菜單
2. 庫表設計
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;
3. 創建 springboot 應用
- 引入依賴
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</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>
- 配置配置文件
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;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return roles.stream().map(r -> new SimpleGrantedAuthority(r.getName())).collect(Collectors.toList());}@Overridepublic String getPassword() {return password;}@Overridepublic String getUsername() {return username;}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return !locked;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic 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 mapperPUBLIC "-//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 userwhere username = #{username};</select><select id="getUserRoleByUid" resultType="com.blr.entity.Role">select r.*from role r,user_role urwhere ur.uid = #{uid}and ur.rid = r.id</select>
</mapper>
<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 rnameZhfrom menu mleft 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;@Autowiredpublic UserService(UserMapper userMapper) {this.userMapper = userMapper;}@Overridepublic 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;@Autowiredpublic 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;@Autowiredpublic CustomSecurityMetadataSource(MenuService menuService) {this.menuService = menuService;}AntPathMatcher antPathMatcher = new AntPathMatcher();@Overridepublic 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;}@Overridepublic Collection<ConfigAttribute> getAllConfigAttributes() {return null;}@Overridepublic boolean supports(Class<?> clazz) {return FilterInvocation.class.isAssignableFrom(clazz);}
}
- 配置 Security 配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {private final CustomSecurityMetadataSource customSecurityMetadataSource;private final UserService userService;@Autowiredpublic SecurityConfig(CustomSecurityMetadataSource customSecurityMetadataSource, UserService userService) {this.customSecurityMetadataSource = customSecurityMetadataSource;this.userService = userService;}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userService);}@Overrideprotected void configure(HttpSecurity http) throws Exception {ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);http.apply(new UrlAuthorizationConfigurer<>(applicationContext)).withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {@Overridepublic <O extends FilterSecurityInterceptor> O postProcess(O object) {object.setSecurityMetadataSource(customSecurityMetadataSource);object.setRejectPublicInvocations(true);return object;}});http.formLogin().and().csrf().disable();}
}
- 啟動入口類進行測試