文章目录
权限管理
13.1什么是权限管理
13.2Spring security权限管理策略
13.3核心概念
13.3.1角色与权限
public class Role implements GrantedAuthority {
private String name;
private List<SimpleGrantedAuthority> allowedOperations = new ArrayList<>();
@Override
public String getAuthority() {
return name;
}
// 省略getter/setter
}
public class User implements UserDetails {
private List<Role> roles = new ArrayList<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities= new ArrayList<>();
for (Role role : roles) {
authorities.addAll(role.getAllowedOperations());
}
return authorities.stream().distinct().collect(Collectors.toList());
}
// 省略getter/setter
}
13.3.2角色继承
public interface RoleHierarchy {
/**
* 该方法返回用户真正可触达的权限。例如,假设用户定义了ROLE_ADMIN继承自ROLE_USER,ROLE_USER继承自ROLE_GUEST,
* 现在当前用户角色是ROLE_ADMIN,但是它实际可访问的资源也包含ROLE_USER和ROLE_GUEST能访问的资源。该方法就是根据
* 当前用户所具有的角色,从角色层级映射中解析出用户真正可触达的权限。
*/
Collection<? extends GrantedAuthority> getReachableGrantedAuthorities(
Collection<? extends GrantedAuthority> authorities);
}
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
// ROLE_A继承自ROLE_B,ROLE_B继承自ROLE_C,ROLE_C继承自ROLE_D
hierarchy.setHierarchy("ROLE_A > ROLE_B > ROLE_C > ROLE_D");
return hierarchy;
}
ROLE_A -> ROLE_B
ROLE_B -> ROLE_C
ROLE_C -> ROLE_D
ROLE_A -> [ROLE_B, ROLE_C, ROLE_D]
ROLE_B -> [ROLE_C, ROLE_D]
ROLE_C -> ROLE_D
13.3.3两种处理器
13.3.4前置处理器
投票器
public interface AccessDecisionVoter<S> {
// 表示投票通过
int ACCESS_GRANTED = 1;
// 表示弃权
int ACCESS_ABSTAIN = 0;
// 表示拒绝
int ACCESS_DENIED = -1;
// 用来判断是否支持处理ConfigAttribute对象
boolean supports(ConfigAttribute attribute);
// 用来判断是否支持处理受保护的安全对象
boolean supports(Class<?> clazz);
/**
* 具体的投票方法,根据用户所具有的权限以及当前请求需要的权限进行投票。
* @param authentication 进行调用的调用者,可以提取出来当前用户所具备的权限
* @param object 受保护的安全对象,如果受保护的是URL地址,则object就是一个FilterInvocation对象;如果受保护的是一个
* 方法,则object就是一个MethodInvocation对象
* @param attributes 访问受保护对象所需要的权限
* @return 定义的三个常量之一
*/
int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);
}
决策器
public interface AccessDecisionManager {
// 核心的决策方法,在这个方法中判断是否允许当前URL或者方法的调用,如果不允许,则抛出AccessDeniedException异常
void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException;
// 用来判断是否支持处理ConfigAttribute对象
boolean supports(ConfigAttribute attribute);
// 用来判断是否支持当前安全对象
boolean supports(Class<?> clazz);
}
13.3.5后置处理器
// 和AccessDecisionManager高度相似
public interface AfterInvocationManager {
/**
* 主要的区别在于decide方法的参数和返回值。当后置处理器执行时,被权限保护的方法以及执行完毕,后置处理器主要是对执行的结果
* 进行过滤,所以decide方法中有一个returnedObject参数,这就是目标方法的执行结果,decide方法的返回值就是对returnedObject
* 对象进行过滤/鉴权后的结果
*/
Object decide(Authentication authentication, Object object, Collection<ConfigAttribute> attributes,
Object returnedObject) throws AccessDeniedException;
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
}
13.3.6权限元数据
ConfigAttribute
public interface ConfigAttribute extends Serializable {
String getAttribute();
}
SecurityMetadataSource
public interface SecurityMetadataSource extends AopInfrastructureBean {
/**
* 根据传入的安全对象参数返回其所需要的权限。如果受保护的对象是一个URL地址,那么传入的参数object就是一个FilterInvocation
* 对象;如果受保护的是一个方法,那么object就是一个MethodInvocation对象。
*/
Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException;
// 返回所有的角色/权限,以便验证是否支持。不过这个方法不是必须的,也可以直接返回null
Collection<ConfigAttribute> getAllConfigAttributes();
// 返回当前的SecurityMetadataSource是否支持受保护的对象如FilterInvocation或者MethodInvocation
boolean supports(Class<?> clazz);
}
private final Map<RequestMatcher, Collection<ConfigAttribute>> requestMap;
http.autorizeRequests()
// 访问/admin/**格式的URL地址需要admin角色
.antMatchers("/admin/**").hasRole("admin")
// 访问/user/**格式的URL地址需要user角色
.antMatchers("/user/**").access("hasRole('user')")
// 其余地址认证后即可访问
.anyRequest().access("isAuthenticated()")
13.3.7权限表达式
配置类名称
作用
hasRole(String role)
当前用户是否具备指定角色
hasAnyRole(String... roles)
当前用户是否具备指定角色中的任意一个
hasAuthority(String authority)
当前用户是否具备指定的权限
hasAnyAuthority(String... authorities)
当前用户是否具备指定权限中的任意一个
principal
代表当前登录主体Principal
authentication
这个是从SecurityContext
中获取到的Authentication
对象
permitAll
允许所有的请求/调用
denyAll
拒绝所有的请求/调用
isAnonymouse()
当前用户是否是一个匿名用户
isRememberMe()
当前用户是否是通过remember-me自动登录
isAuthenticated()
当前用户是否已经认证成功
isFullyAuthenticated()
当前用户是否既不是匿名用户又不是通过remember-me自动登录的
hasPermission(Object target, Object permission)
当前用户是否具备指定目标的指定权限
hasPermission(Object targetId, String targetType, Object permission)
当前用户是否具备指定目标的指定权限
hasIpAddress(String ipAddress)
当前请求IP地址是否为指定IP
public interface SecurityExpressionOperations {
Authentication getAuthentication();
boolean hasAuthority(String authority);
boolean hasAnyAuthority(String... authorities);
boolean hasRole(String role);
boolean hasAnyRole(String... roles);
boolean permitAll();
boolean denyAll();
boolean isAnonymous();
boolean isAuthenticated();
boolean isRememberMe();
boolean isFullyAuthenticated();
boolean hasPermission(Object target, Object permission);
boolean hasPermission(Object targetId, String targetType, Object permission);
}
SecurityExpressionRoot
WebSecurityExpressionRoot
MethodSecurityExpressionOperations
MethodSecurityExpressionRoot
13.4基于URL地址的权限管理
13.4.1基本用法
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 定义用户以及相应的角色和权限。
* 对于复杂的权限管理系统,用户和角色关联,角色和权限关联,权限和资源关联;对于简单的权限管理系统,
* 用户和权限关联,权限和资源关联。无论是哪种,用户都不会和角色以及权限同时直接关联。反应到代码上
* 就是roles方法和authorities方法不能同时调用,如果同时调用,后者会覆盖前者(可以自行查看源码,
* 最终都会调用authorities(Collection<? extends GrantedAuthority> authorities)方法)。
* 需要注意的是,spring security会自动给用户的角色加上ROLE_前缀。
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
// javaboy具有ADMIN角色
.withUser("javaboy").password("{noop}123").roles("ADMIN")
.and()
// zhangsan具有USER角色
.withUser("zhangsan").password("{noop}123").roles("USER")
.and()
// itboyhub具有READ_INFO权限
.withUser("itboyhub").password("{noop}123").authorities("READ_INFO");
}
/**
* 配置拦截规则
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 用户必须具备ADMIN角色才可以访问/admin/**格式的地址
.antMatchers("/admin/**").hasRole("ADMIN")
// 用户必须具备USER和ADMIN任意一个角色,才可以访问/user/**格式的地址
.antMatchers("/user/**").access("hasAnyRole('USER', 'ADMIN')")
// 用户必须具备READ_INFO权限,才可以访问/getInfo接口
.antMatchers("/getInfo").hasAuthority("READ_INFO")
// 剩余的请求只要是认证后的用户就可以访问
.anyRequest().access("isAuthenticated()")
.and()
.formLogin()
.and()
.csrf().disable();
}
}
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello";
}
@GetMapping("/admin/hello")
public String admin() {
return "Hello admin";
}
@GetMapping("/user/hello")
public String user() {
return "Hello user";
}
@GetMapping("/getInfo")
public String getInfo() {
return "GetInfo";
}
}
13.4.2角色继承
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 如果需要配置角色继承,则只需要提供一个RoleHierarchy的实例即可
*/
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
return hierarchy;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 省略
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
// ROLE_ADMIN继承自ROLE_USER,因此可以直接访问/user/**格式的地址
.antMatchers("/user/**").access("hasRole('USER')")
.antMatchers("/getInfo").hasAuthority("READ_INFO")
.anyRequest().access("isAuthenticated()")
.and()
.formLogin()
.and()
.csrf().disable();
}
}
13.4.3自定义表达式
@RestController
public class HelloController {
/**
* 第一个接口:参数userId必须是偶数方可请求成功。
*/
@GetMapping("/hello/{userId}")
public String hello(@PathVariable Integer userId) {
return "Hello " + userId;
}
/**
* 第二个接口:参数username必须是javaboy方可请求成功,同时这两个接口必须认证后才能访问。
*/
@GetMapping("/hi")
public String hello2User(String username) {
return "Hello " + username;
}
}
/**
* 自定义PermissionExpression类并注册到spring容器中,然后定义相应的方法。
*/
@Component
public class PermissionExpression {
public boolean checkId(Authentication authentication, Integer userId) {
if (authentication.isAuthenticated()) {
return userId % 2 == 0;
}
return false;
}
public boolean check(HttpServletRequest request) {
return "javaboy".equals(request.getParameter("username"));
}
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 省略
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 省略其他
// 在access方法中,可以通过@符号引用一个bean并调用其中的方法。在checkId方法调用时,
// #userId表示前面的userId参数
.antMatchers("/hello/{userId}").access("@permissionExpression.checkId(authentication, #userId)")
// 需要同时满足isAuthenticated和check方法都为true,该请求才会通过
.antMatchers("/hi").access("isAuthenticated() and @permissionExpression.check(request)")
// 省略其他
}
}
13.4.4原理剖析
AbstractSecurityInterceptor
protected InterceptorStatusToken beforeInvocation(Object object) {
if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException("Security invocation attempted for object " + object.getClass().getName()
+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
+ getSecureObjectClass());
}
// 首先调用obtainSecurityMetadataSource方法获取SecurityMetadataSource对象,然后调用其getAttributes方法获取
// 受保护对象所需要的权限
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
// 如果获取到的权限值为空
if (CollectionUtils.isEmpty(attributes)) {
// 则发布PublicInvocationEvent事件,该事件在调用公共安全对象(没有定义ConfigAttributes的对象)时生成
publishEvent(new PublicInvocationEvent(object));
// 此时直接返回null即可
return null; // no further work post-invocation
}
// 查看当前用户的认证信息是否存在
if (SecurityContextHolder.getContext().getAuthentication() == null) {
credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound",
"An Authentication object was not found in the SecurityContext"), object, attributes);
}
// 检查当前用户是否已经登录
Authentication authenticated = authenticateIfRequired();
// 尝试授权
attemptAuthorization(object, attributes, authenticated);
if (this.publishAuthorizationSuccess) {
publishEvent(new AuthorizedEvent(object, attributes, authenticated));
}
// 临时替换用户身份,不过默认情况下,runAsManager的实例是NullRunAsManager,即不做任何替换,所以runAs为null
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);
if (runAs != null) {
// 如果runAs不为空,则将SecurityContext中保存的用户信息修改为替换的用户对象,然后返回一个InterceptorStatusToken。
// InterceptorStatusToken对象中保存了当前用户的SecurityContext对象,假如进行了临时用户替换,在替换完成后,最终
// 还是要恢复成当前用户身份的
SecurityContext origCtx = SecurityContextHolder.getContext();
SecurityContext newCtx = SecurityContextHolder.createEmptyContext();
newCtx.setAuthentication(runAs);
SecurityContextHolder.setContext(newCtx);
// need to revert to token.Authenticated post-invocation
return new InterceptorStatusToken(origCtx, true, attributes, object);
}
// 如果runAs为空,则直接创建一个InterceptorStatusToken对象返回即可
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);
}
private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes,
Authentication authenticated) {
try {
// 核心功能:进行决策,该方法中会调用投票器进行投票,如果该方法执行抛出异常,则说明权限不足
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException ex) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, ex));
throw ex;
}
}
/**
* 如果临时替换了用户身份,那么最终要将用户身份恢复,finallyInvocation方法所做的事情就是恢复用户身份。这里的参数token就是
* beforeInvocation方法的返回值,用户原始的身份信息都保存在token中,从token中取出用户身份信息,并设置到SecurityContextHolder
* 中去即可。
*/
protected void finallyInvocation(InterceptorStatusToken token) {
if (token != null && token.isContextHolderRefreshRequired()) {
SecurityContextHolder.setContext(token.getSecurityContext());
}
}
/**
* 该方法接收两个参数,第一个参数token就是beforeInvocation方法的返回值,第二个参数returnedObject则是受保护对象的返回值,
* afterInvocation方法的核心工作就是调用afterInvocationManager.decide方法对returnedObject进行过滤,然后将过滤后的
* 结果返回。
*/
protected Object afterInvocation(InterceptorStatusToken token, Object returnedObject) {
if (token == null) {
// public object
return returnedObject;
}
finallyInvocation(token); // continue to clean in this method for passivity
if (this.afterInvocationManager != null) {
// Attempt after invocation handling
try {
returnedObject = this.afterInvocationManager.decide(token.getSecurityContext().getAuthentication(),
token.getSecureObject(), token.getAttributes(), returnedObject);
}
catch (AccessDeniedException ex) {
publishEvent(new AuthorizationFailureEvent(token.getSecureObject(), token.getAttributes(),
token.getSecurityContext().getAuthentication(), ex));
throw ex;
}
}
return returnedObject;
}
FilterSecurityInterceptor
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 构建受保护对象FilterInvocation,然后调用invoke方法
invoke(new FilterInvocation(request, response, chain));
}
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
// 判断当前过滤器是否已经执行过,如果是,则继续执行剩下的过滤器
if (isApplied(filterInvocation) && this.observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
return;
}
// first time this request being called, so perform security checking
if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
// 调用父类的beforeInvocation方法进行权限校验
InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
try {
// 校验通过后,继续执行剩余的过滤器
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
}
finally {
// 调用父类的finallyInvocation方法
super.finallyInvocation(token);
}
// 最后调用父类的afterInvocation方法,可以看到,前置处理器和后置处理器都是在invoke方法中触发的
super.afterInvocation(token, null);
}
AbstractInterceptUrlConfigurer
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").access("hasAnyRole('USER', 'ADMIN')")
// ...
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.and()
.formLogin()
.and()
.csrf().disable();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
// 开发者手动创建一个UrlAuthorizationConfigurer对象出来,并调用其getRegistry方法去开启URL路径和权限之间映射关系的配置。
// 由于UrlAuthorizationConfigurer中使用的投票器是RoleVoter和AuthenticatedVoter,所以这里的角色需要自带ROLE_前缀
// (因为RoleVoter的supports方法中会判断角色是否带有ROLE_前缀)
http.apply(new UrlAuthorizationConfigurer<>(applicationContext))
.getRegistry()
.mvcMatchers("/admin/**").access("ROLE_ADMIN")
.mvcMatchers("/user/**").access("ROLE_USER");
http.formLogin()
.and()
.csrf().disable();
}
13.4.5动态管理权限规则
13.4.5.1数据库设计
CREATE TABLE `menu` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`pattern` varchar(128) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `menu` (`id`, `pattern`)
VALUES
(1,'/admin/**'),
(2,'/user/**'),
(3,'/guest/**');
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 DEFAULT CHARSET=utf8;
INSERT INTO `menu_role` (`id`, `mid`, `rid`)
VALUES
(1,1,1),
(2,2,2),
(3,3,3),
(4,3,2);
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 DEFAULT CHARSET=utf8;
INSERT INTO `role` (`id`, `name`, `nameZh`)
VALUES
(1,'ROLE_ADMIN','系统管理员'),
(2,'ROLE_USER','普通用户'),
(3,'ROLE_GUEST','游客');
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 DEFAULT CHARSET=utf8;
INSERT INTO `user` (`id`, `username`, `password`, `enabled`, `locked`)
VALUES
(1,'admin','{noop}123',1,0),
(2,'user','{noop}123',1,0),
(3,'javaboy','{noop}123',1,0);
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 DEFAULT CHARSET=utf8;
INSERT INTO `user_role` (`id`, `uid`, `rid`)
VALUES
(1,1,1),
(2,1,2),
(3,2,2),
(4,3,3);
13.4.5.2实战
项目创建
创建实体类
public class Role {
private Integer id;
private String name;
private String nameZh;
// 省略getter/setter
}
public class Menu {
private Integer id;
private String pattern;
private List<Role> roles;
// 省略getter/setter
}
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 boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return !locked;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled;
}
// 省略其他getter/setter
}
创建service
@Service
public class UserService implements UserDetailsService {
@Autowired
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;
}
}
@Mapper
public interface UserMapper {
List<Role> getUserRoleByUid(Integer uid);
User loadUserByUsername(String username);
}
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.fuxiao.part13_4_5.mapper.UserMapper">
<select id="loadUserByUsername" resultType="com.fuxiao.part13_4_5.bean.User">
select * from user where username=#{username};
</select>
<select id="getUserRoleByUid" resultType="com.fuxiao.part13_4_5.bean.Role">
select r.* from role r,user_role ur where ur.uid=#{uid} and ur.rid=r.id
</select>
</mapper>
@Service
public class MenuService {
@Autowired
MenuMapper menuMapper;
public List<Menu> getAllMenu() {
return menuMapper.getAllMenu();
}
}
@Mapper
public interface MenuMapper {
List<Menu> getAllMenu();
}
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.fuxiao.part13_4_5.mapper.MenuMapper">
<resultMap id="MenuResultMap" type="com.fuxiao.part13_4_5.bean.Menu">
<id property="id" column="id"/>
<result property="pattern" column="pattern"></result>
<collection property="roles" ofType="com.fuxiao.part13_4_5.bean.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>
配置spring security
/**
* SecurityMetadataSource接口负责提供受保护对象所需要的权限。由于该案例中,受保护对象所需要的权限保存在数据库中,所以可以通过自定义类继承自
* FilterInvocationSecurityMetadataSource,并重写getAttributes方法来提供受保护对象所需要的权限。
*/
@Component
public class CustomSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
MenuService menuService;
AntPathMatcher antPathMatcher = new AntPathMatcher();
/**
* 在基于URL地址的权限控制中,受保护对象就是FilterInvocation。
* @param object 受保护对象
* @return 受保护对象所需要的权限
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
// 从受保护对象FilterInvocation中提取出当前请求的URI地址,例如/admin/hello
String requestURI = ((FilterInvocation) object).getRequest().getRequestURI();
// 查询所有菜单数据(每条数据中都包含访问该条记录所需要的权限)
List<Menu> allMenu = menuService.getAllMenu();
// 遍历菜单数据,如果当前请求的URL地址和菜单中某一条记录的pattern属性匹配上了(例如/admin/hello匹配上/admin/**)
// 那么就可以获取当前请求所需要的权限;如果没有匹配上,则返回null。需要注意的是,如果AbstractSecurityInterceptor
// 中的rejectPublicInvocations属性为false(默认值)时,则表示当getAttributes返回null时,允许访问受保护对象
for (Menu menu : allMenu) {
if (antPathMatcher.match(menu.getPattern(), requestURI)) {
String[] roles = menu.getRoles().stream().map(Role::getName).toArray(String[]::new);
return SecurityConfig.createList(roles);
}
}
return null;
}
/**
* 方便在项目启动阶段做校验,如果不需要校验,则直接返回null即可。
* @return 所有的权限属性
*/
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
/**
* 表示当前对象支持处理的受保护对象是FilterInvocation。
*/
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
CustomSecurityMetadataSource customSecurityMetadataSource;
@Autowired
UserService userService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
http.apply(new UrlAuthorizationConfigurer<>(applicationContext))
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
// 使用配置好的CustomSecurityMetadataSource来代替默认的SecurityMetadataSource对象
object.setSecurityMetadataSource(customSecurityMetadataSource);
// 将rejectPublicInvocations设置为true,表示当getAttributes返回null时,不允许访问受保护对象
object.setRejectPublicInvocations(true);
return object;
}
});
http.formLogin()
.and()
.csrf().disable();
}
}
测试
@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";
}
/**
* 由于rejectPublicInvocations设置为true,因此,只有具备该接口权限的用户才能访问
*/
@GetMapping("/hello")
public String hello() {
return "Hello";
}
}
13.5基于方法的权限管理
13.5.1注解介绍
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
13.5.2基本用法
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig {
}
public class User {
private Integer id;
private String username;
// 省略构造器、toString和getter/setter
}
@PreAuthorize
@Service
public class HelloService {
// 执行该方法必须具备ADMIN角色才可以访问
@PreAuthorize("hasRole('ADMIN')")
public String preAuthorizeTest01() {
return "Hello";
}
// 访问者名称必须是javaboy,而且还需要同事具备ADMIN角色才可以访问
@PreAuthorize("hasRole('ADMIN') and authentication.name == 'javaboy'")
public String preAuthorizeTest02() {
return "Hello";
}
// 通过#引用方法参数,并对其进行校验,表示请求者的用户名必须等于方法参数name的值,方法才可以被执行
@PreAuthorize("authentication.name == #name")
public String preAuthorizeTest03(String name) {
return "Hello: " + name;
}
}
@SpringBootTest
class BasedOnMethodApplicationTests {
@Autowired
HelloService helloService;
@Test
// 通过该注解设定当前执行的用户角色是ADMIN
@WithMockUser(roles = "ADMIN")
void preAuthorizeTest01() {
String hello = helloService.preAuthorizeTest01();
Assertions.assertNotNull(hello);
Assertions.assertEquals("Hello", hello);
}
@Test
@WithMockUser(roles = "ADMIN", username = "javaboy")
void preAuthorizeTest02() {
String hello = helloService.preAuthorizeTest02();
Assertions.assertNotNull(hello);
Assertions.assertEquals("Hello", hello);
}
@Test
@WithMockUser(username = "javaboy")
void preAuthorizeTest03() {
String hello = helloService.preAuthorizeTest03("javaboy");
Assertions.assertNotNull(hello);
Assertions.assertEquals("Hello: javaboy", hello);
}
}
@PreFilter
@Service
public class HelloService {
/**
* PreFilter主要是对方法的请求参数进行过滤,它里边包含了一个内置对象filterObject,表示具体元素。
* 如果方法只有一个参数,则内置的filterObject对象就代表该参数;如果有多个参数,则需要通过filterTarget
* 来指定filterObject到底代表哪个对象。
* 表示只保留id为奇数的user对象。
*/
@PreFilter(filterTarget = "users", value = "filterObject.id % 2 != 0")
public void preFilterTest(List<User> users) {
System.out.println("users = " + users);
}
}
@Test
@WithMockUser(username = "javaboy")
void preFilterTest() {
List<User> users = new ArrayList<>();
for (int i = 0; i < 10; i++) {
users.add(new User(i, "javaboy: " + i));
}
helloService.preFilterTest(users);
}
@PostAuthorize
@Service
public class HelloService {
/**
* PostAuthorize是在目标方法执行之后进行权限校验,其实这个主要是在ACL权限模型中会用到,目标方法执行完毕后,
* 通过该注解去校验目标方法的返回值是否满足相应的权限要求。从技术角度来讲,该注解中也可以使用权限表达式,但是
* 在实际开发中权限表达式一般都是结合PreAuthorize注解一起使用的。PostAuthorize包含一个内置对象returnObject,
* 表示方法的返回值,开发中可以对返回值进行校验。
*/
@PostAuthorize("returnObject.id == 1")
public User postAuthorizeTest(Integer id) {
return new User(id, "javaboy");
}
}
@Test
@WithMockUser(username = "javaboy")
void postAuthorizeTest() {
User user = helloService.postAuthorizeTest(1);
Assertions.assertNotNull(user);
Assertions.assertEquals(1, user.getId());
Assertions.assertEquals("javaboy", user.getUsername());
}
@PostFilter
@Service
public class HelloService {
/**
* PostFilter注解在目标方法执行之后,对目标方法的返回结果进行过滤,该注解中包含了一个内置对象filterObject,
* 表示目标方法返回的集合/数组中的具体元素。
*/
@PostFilter("filterObject.id % 2 == 0")
public List<User> postFilterTest() {
List<User> users = new ArrayList<>();
for (int i = 0; i < 10; i++) {
users.add(new User(i, "javaboy: " + i));
}
return users;
}
}
@Test
@WithMockUser(roles = "ADMIN")
void postFilterTest() {
List<User> all = helloService.postFilterTest();
Assertions.assertNotNull(all);
Assertions.assertEquals(5, all.size());
Assertions.assertEquals(2, all.get(1).getId());
}
@Secured
@Service
public class HelloService {
// 该注解不支持权限表达式,只能做一些简单的权限描述
@Secured({"ROLE_ADMIN", "ROLE_USER"})
public User securedTest(String username) {
return new User(99, username);
}
}
@Test
@WithMockUser(roles = "ADMIN")
void securedTest() {
User user = helloService.securedTest("javaboy");
Assertions.assertNotNull(user);
Assertions.assertEquals(99, user.getId());
Assertions.assertEquals("javaboy", user.getUsername());
}
@DenyAll
@Service
public class HelloService {
// JSR-250:拒绝所有访问
@DenyAll
public void denyAllTest() {
}
}
@Test
@WithMockUser(username = "javaboy")
void denyAllTest() {
helloService.denyAllTest();
}
@PermitAll
@Service
public class HelloService {
// JSR-250:允许所有访问
@PermitAll
public void permitAllTest() {
}
}
@Test
@WithMockUser(username = "javaboy")
void permitAllTest() {
helloService.permitAllTest();
}
@RolesAllowed
@Service
public class HelloService {
/**
* JSR-250:可以添加在方法上或类上,当添加在类上时,表示该注解对类中的所有方法生效;如果类上和方法上都有该注解,
* 并且起冲突,则以方法上的注解为准。
*/
@RolesAllowed({"ADMIN", "USER"})
public String rolesAllowedTest() {
return "RolesAllowed";
}
}
@Test
@WithMockUser(roles = "ADMIN")
void rolesAllowedTest() {
String s = helloService.rolesAllowedTest();
Assertions.assertNotNull(s);
Assertions.assertEquals("RolesAllowed", s);
}
13.5.3原理剖析
MethodSecurityInterceptor
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
// 调用父类的beforeInvocation方法进行权限校验
InterceptorStatusToken token = super.beforeInvocation(mi);
Object result;
try {
// 校验通过后,调用mi.proceed()方法继续执行目标方法
result = mi.proceed();
}
finally {
// 在finally块中调用finallyInvocation方法完成一些清理工作
super.finallyInvocation(token);
}
// 最后调用父类的afterInvocation方法进行请求结果的过滤
return super.afterInvocation(token, result);
}
EnableGlobalMethodSecurity
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
// 引入了一个配置GlobalMethodSecuritySelector,该类的作用主要是用来导入外部配置类
@Import({ GlobalMethodSecuritySelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableGlobalMethodSecurity {
// 省略其他
}
final class GlobalMethodSecuritySelector implements ImportSelector {
// importingClassMetadata中保存了@EnableGlobalMethodSecurity注解的元数据,包括各个属性的值、注解是加在哪个配置类上等
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
Class<EnableGlobalMethodSecurity> annoType = EnableGlobalMethodSecurity.class;
Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(annoType.getName(),
false);
AnnotationAttributes attributes = AnnotationAttributes.fromMap(annotationAttributes);
// TODO would be nice if could use BeanClassLoaderAware (does not work)
Class<?> importingClass = ClassUtils.resolveClassName(importingClassMetadata.getClassName(),
ClassUtils.getDefaultClassLoader());
boolean skipMethodSecurityConfiguration = GlobalMethodSecurityConfiguration.class
.isAssignableFrom(importingClass);
AdviceMode mode = attributes.getEnum("mode");
boolean isProxy = AdviceMode.PROXY == mode;
String autoProxyClassName = isProxy ? AutoProxyRegistrar.class.getName()
: GlobalMethodSecurityAspectJAutoProxyRegistrar.class.getName();
boolean jsr250Enabled = attributes.getBoolean("jsr250Enabled");
List<String> classNames = new ArrayList<>(4);
if (isProxy) {
classNames.add(MethodSecurityMetadataSourceAdvisorRegistrar.class.getName());
}
classNames.add(autoProxyClassName);
if (!skipMethodSecurityConfiguration) {
classNames.add(GlobalMethodSecurityConfiguration.class.getName());
}
if (jsr250Enabled) {
classNames.add(Jsr250MetadataSourceConfiguration.class.getName());
}
return classNames.toArray(new String[0]);
}
}
class MethodSecurityMetadataSourceAdvisorRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 首先定义BeanDefinitionBuilder
BeanDefinitionBuilder advisor = BeanDefinitionBuilder
.rootBeanDefinition(MethodSecurityMetadataSourceAdvisor.class);
// 然后给目标对象MethodSecurityMetadataSourceAdvisor的构造方法设置参数
advisor.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
// 要引用的MethodInterceptor对象名
advisor.addConstructorArgValue("methodSecurityInterceptor");
// 要引用的MethodSecurityMetadataSource对象名
advisor.addConstructorArgReference("methodSecurityMetadataSource");
// 和第二个一样,只不过一个是引用,一个是字符串
advisor.addConstructorArgValue("methodSecurityMetadataSource");
MultiValueMap<String, Object> attributes = importingClassMetadata
.getAllAnnotationAttributes(EnableGlobalMethodSecurity.class.getName());
Integer order = (Integer) attributes.getFirst("order");
if (order != null) {
advisor.addPropertyValue("order", order);
}
// 所有属性都配置好之后,将其注册到spring容器中
registry.registerBeanDefinition("metaDataSourceAdvisor", advisor.getBeanDefinition());
}
}
/**
* 继承自AbstractPointcutAdvisor,主要定义了AOP的pointcut和advice,poincut也就是切点,可以简单理解为方法的拦截规则,
* 即哪些方法需要拦截,哪些方法不需要拦截;advice也就是增强/通知,就是将方法拦截下来之后要增强的功能
*/
public class MethodSecurityMetadataSourceAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {
private transient MethodSecurityMetadataSource attributeSource;
private transient MethodInterceptor interceptor;
// 这里的pointcut对象就是内部类MethodSecurityMetadataSourcePointcut,在它的matches方法中,定义了具体的拦截规则
private final Pointcut pointcut = new MethodSecurityMetadataSourcePointcut();
private BeanFactory beanFactory;
private final String adviceBeanName;
private final String metadataSourceBeanName;
private transient volatile Object adviceMonitor = new Object();
// 构造方法所需的三个参数就是MethodSecurityMetadataSourceAdvisorRegistrar类中提供的三个参数
public MethodSecurityMetadataSourceAdvisor(String adviceBeanName, MethodSecurityMetadataSource attributeSource,
String attributeSourceBeanName) {
this.adviceBeanName = adviceBeanName;
this.attributeSource = attributeSource;
this.metadataSourceBeanName = attributeSourceBeanName;
}
@Override
public Pointcut getPointcut() {
return this.pointcut;
}
/**
* Advice由getAdvice方法返回,在该方法内部,就是去spring容器中查找一个名为
* methodSecurityInterceptor的MethodInterceptor对象。
*/
@Override
public Advice getAdvice() {
synchronized (this.adviceMonitor) {
if (this.interceptor == null) {
this.interceptor = this.beanFactory.getBean(this.adviceBeanName, MethodInterceptor.class);
}
return this.interceptor;
}
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
this.adviceMonitor = new Object();
this.attributeSource = this.beanFactory.getBean(this.metadataSourceBeanName,
MethodSecurityMetadataSource.class);
}
class MethodSecurityMetadataSourcePointcut extends StaticMethodMatcherPointcut implements Serializable {
@Override
public boolean matches(Method m, Class<?> targetClass) {
MethodSecurityMetadataSource source = MethodSecurityMetadataSourceAdvisor.this.attributeSource;
// 通过attributeSource.getAttributes方法去查看目标方法上有没有相应的权限注解,
// 如果有,则返回true,目标方法就被拦截下来;如果没有,则返回false,目标方法就
// 不会被拦截,这里的attributeSource实际上就是MethodSecurityMetadataSource对象,
// 也就是提供权限元数据的类
return !CollectionUtils.isEmpty(source.getAttributes(m, targetClass));
}
}
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public MethodSecurityMetadataSource methodSecurityMetadataSource() {
// 创建List集合,用来保存所有的MethodSecurityMetadataSource对象
List<MethodSecurityMetadataSource> sources = new ArrayList<>();
ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory(
getExpressionHandler());
// 然后调用customMethodSecurityMetadataSource方法去获取自定义的MethodSecurityMetadataSource,
// 默认情况下该方法返回null,如果想买有需要,开发者可以重写该方法来提供自定义的MethodSecurityMetadataSource对象
MethodSecurityMetadataSource customMethodSecurityMetadataSource = customMethodSecurityMetadataSource();
if (customMethodSecurityMetadataSource != null) {
sources.add(customMethodSecurityMetadataSource);
}
// 接下来就是根据注解中配置的属性值,来向sources集合中添加相应的MethodSecurityMetadataSource对象
boolean hasCustom = customMethodSecurityMetadataSource != null;
boolean isPrePostEnabled = prePostEnabled();
boolean isSecuredEnabled = securedEnabled();
boolean isJsr250Enabled = jsr250Enabled();
// 如果@EnableGlobalMethodSecurity注解配置了prePostEnabled=true,
// 则加入PrePostAnnotationSecurityMetadataSource对象来解析相应的注解
if (isPrePostEnabled) {
sources.add(new PrePostAnnotationSecurityMetadataSource(attributeFactory));
}
// 如果@EnableGlobalMethodSecurity注解配置了securedEnabled=true,
// 则加入SecuredAnnotationSecurityMetadataSource对象来解析相应的注解
if (isSecuredEnabled) {
sources.add(new SecuredAnnotationSecurityMetadataSource());
}
// 如果@EnableGlobalMethodSecurity注解配置了jsr250Enabled=true,
// 则加入Jsr250MethodSecurityMetadataSource对象来解析相应的注解
if (isJsr250Enabled) {
GrantedAuthorityDefaults grantedAuthorityDefaults = getSingleBeanOrNull(GrantedAuthorityDefaults.class);
Jsr250MethodSecurityMetadataSource jsr250MethodSecurityMetadataSource = this.context
.getBean(Jsr250MethodSecurityMetadataSource.class);
if (grantedAuthorityDefaults != null) {
jsr250MethodSecurityMetadataSource.setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix());
}
sources.add(jsr250MethodSecurityMetadataSource);
}
// 最后构建一个代理对象DelegatingMethodSecurityMetadataSource返回即可
return new DelegatingMethodSecurityMetadataSource(sources);
}
@Bean
public MethodInterceptor methodSecurityInterceptor(MethodSecurityMetadataSource methodSecurityMetadataSource) {
// 查看代理的方式,默认使用spring自带的AOP,所以使用MethodSecurityInterceptor来创建对应的MethodInterceptor实例
this.methodSecurityInterceptor = isAspectJ() ? new AspectJMethodSecurityInterceptor()
: new MethodSecurityInterceptor();
// 然后给methodSecurityInterceptor设置AccessDecisionManager决策管理器
this.methodSecurityInterceptor.setAccessDecisionManager(accessDecisionManager());
// 接下来给methodSecurityInterceptor配置后置处理器
this.methodSecurityInterceptor.setAfterInvocationManager(afterInvocationManager());
// 最后再把前面创建好的MethodSecurityMetadataSource对象配置给methodSecurityInterceptor
this.methodSecurityInterceptor.setSecurityMetadataSource(methodSecurityMetadataSource);
RunAsManager runAsManager = runAsManager();
if (runAsManager != null) {
this.methodSecurityInterceptor.setRunAsManager(runAsManager);
}
return this.methodSecurityInterceptor;
}
protected AccessDecisionManager accessDecisionManager() {
// 默认的决策管理器是AffirmativeBased
List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
// 根据@EnableGlobalMethodSecurity注解的配置,配置不同的投票器
if (prePostEnabled()) {
ExpressionBasedPreInvocationAdvice expressionAdvice = new ExpressionBasedPreInvocationAdvice();
expressionAdvice.setExpressionHandler(getExpressionHandler());
decisionVoters.add(new PreInvocationAuthorizationAdviceVoter(expressionAdvice));
}
if (jsr250Enabled()) {
decisionVoters.add(new Jsr250Voter());
}
RoleVoter roleVoter = new RoleVoter();
GrantedAuthorityDefaults grantedAuthorityDefaults = getSingleBeanOrNull(GrantedAuthorityDefaults.class);
if (grantedAuthorityDefaults != null) {
roleVoter.setRolePrefix(grantedAuthorityDefaults.getRolePrefix());
}
decisionVoters.add(roleVoter);
decisionVoters.add(new AuthenticatedVoter());
return new AffirmativeBased(decisionVoters);
}
protected AfterInvocationManager afterInvocationManager() {
// 如果@EnableGlobalMethodSecurity注解配置了prePostEnabled=true,则添加一个后置处理器
// PostInvocationAdviceProvider,该类用来处理@PostAuthorize和@PostFilter两个注解
if (prePostEnabled()) {
AfterInvocationProviderManager invocationProviderManager = new AfterInvocationProviderManager();
ExpressionBasedPostInvocationAdvice postAdvice = new ExpressionBasedPostInvocationAdvice(
getExpressionHandler());
PostInvocationAdviceProvider postInvocationAdviceProvider = new PostInvocationAdviceProvider(postAdvice);
List<AfterInvocationProvider> afterInvocationProviders = new ArrayList<>();
afterInvocationProviders.add(postInvocationAdviceProvider);
invocationProviderManager.setProviders(afterInvocationProviders);
return invocationProviderManager;
}
return null;
}
最后
深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
小编已加密:aHR0cHM6Ly9kb2NzLnFxLmNvbS9kb2MvRFVrVm9aSGxQZUVsTlkwUnc==出于安全原因,我们把网站通过base64编码了,大家可以通过base64解码把网址获取下来。