授权的方式包括
web
授权和方法授权,
web
授权是通过
url
拦截进行授权,方法授权是通过 方法拦截进行授权。他 们都会调用accessDecisionManager
进行授权决策,若为
web
授权则拦截器为
FilterSecurityInterceptor
;若为方法授权则拦截器为MethodSecurityInterceptor
。如果同时通过
web
授权和方法授权则先执行
web
授权,再执行方 法授权,最后决策通过,则允许访问资源,否则将禁止访问
数据库环境
在
t_user
数据库创建如下表:
角色表:
CREATE TABLE `t_role` (
`id` varchar(32) NOT NULL,
`role_name` varchar(255) DEFAULT NULL,
`description` varchar(255) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`status` char(1) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_role_name` (`role_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
insert into `t_role`(`id`,`role_name`,`description`,`create_time`,`update_time`,`status`) values
('1','管理员',NULL,NULL,NULL,'');
用户角色关系表:
CREATE TABLE `t_user_role` (
`user_id` varchar(32) NOT NULL,
`role_id` varchar(32) NOT NULL,
`create_time` datetime DEFAULT NULL,
`creator` varchar(255) DEFAULT NULL,
PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
insert into `t_user_role`(`user_id`,`role_id`,`create_time`,`creator`) values
('1','1',NULL,NULL);
权限表:
CREATE TABLE `t_permission` (
`id` varchar(32) NOT NULL,
`code` varchar(32) NOT NULL COMMENT '权限标识符',
`description` varchar(64) DEFAULT NULL COMMENT '描述',
`url` varchar(128) DEFAULT NULL COMMENT '请求地址',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
insert into `t_permission`(`id`,`code`,`description`,`url`) values ('1','p1','测试资源
1','/r/r1'),('2','p3','测试资源2','/r/r2');
角色权限关系表:
CREATE TABLE `t_role_permission` (
`role_id` varchar(32) NOT NULL,
`permission_id` varchar(32) NOT NULL,
PRIMARY KEY (`role_id`,`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
insert into `t_role_permission`(`role_id`,`permission_id`) values ('1','1'),('1','2');
修改UserDetailService
package com.itheima.security.springboot.dao;
import com.itheima.security.springboot.model.PermissionDto;
import com.itheima.security.springboot.model.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
/**
* @author Administrator
* @version 1.0
**/
@Repository
public class UserDao {
@Autowired
JdbcTemplate jdbcTemplate;
//根据账号查询用户信息
public UserDto getUserByUsername(String username){
String sql = "select id,username,password,fullname,mobile from t_user where username = ?";
//连接数据库查询用户
List<UserDto> list = jdbcTemplate.query(sql, new Object[]{username}, new BeanPropertyRowMapper<>(UserDto.class));
if(list !=null && list.size()==1){
return list.get(0);
}
return null;
}
//根据用户id查询用户权限
public List<String> findPermissionsByUserId(String userId){
String sql = "SELECT * FROM t_permission WHERE id IN(\n" +
"\n" +
"SELECT permission_id FROM t_role_permission WHERE role_id IN(\n" +
" SELECT role_id FROM t_user_role WHERE user_id = ? \n" +
")\n" +
")\n";
List<PermissionDto> list = jdbcTemplate.query(sql, new Object[]{userId}, new BeanPropertyRowMapper<>(PermissionDto.class));
List<String> permissions = new ArrayList<>();
list.forEach(c -> permissions.add(c.getCode()));
return permissions;
}
}
修改UserDetailService
package com.itheima.security.springboot.service;
import com.itheima.security.springboot.dao.UserDao;
import com.itheima.security.springboot.model.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author Administrator
* @version 1.0
**/
@Service
public class SpringDataUserDetailsService implements UserDetailsService {
@Autowired
UserDao userDao;
//根据 账号查询用户信息
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//将来连接数据库根据账号查询用户信息
UserDto userDto = userDao.getUserByUsername(username);
if(userDto == null){
//如果用户查不到,返回null,由provider来抛出异常
return null;
}
//根据用户的id查询用户的权限
List<String> permissions = userDao.findPermissionsByUserId(userDto.getId());
//将permissions转成数组
String[] permissionArray = new String[permissions.size()];
permissions.toArray(permissionArray);
UserDetails userDetails = User.withUsername(userDto.getUsername()).password(userDto.getPassword()).authorities(permissionArray).build();
return userDetails;
}
}
Web授权
在上面例子中我们完成了认证拦截,并对
/r/**
下的某些资源进行简单的授权保护,但是我们想进行灵活的授权控
制该怎么做呢?通过给
http.authorizeRequests()
添加多个子节点来定制需求到我们的
URL
,如下代码:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests() (1)
.antMatchers("/r/r1").hasAuthority("p1") (2)
.antMatchers("/r/r2").hasAuthority("p2") (3)
.antMatchers("/r/r3").access("hasAuthority('p1') and hasAuthority('p2')") (4)
.antMatchers("/r/**").authenticated() (5)
.anyRequest().permitAll() (6)
.and()
.formLogin()
// ...
}
(
1
)
http.authorizeRequests()
方法有多个子节点,每个
macher
按照他们的声明顺序执行。
(
2
)指定
"/r/r1"URL
,拥有
p1
权限能够访问
(
3
)指定
"/r/r2"URL
,拥有
p2
权限能够访问
(
4
)指定了
"/r/r3"URL
,同时拥有
p1
和
p2
权限才能够访问
(
5
)指定了除了
r1
、
r2
、
r3
之外
"/r/**"
资源,同时通过身份认证就能够访问,这里使用
SpEL
(
Spring Expression Language)表达式。。
(
6
)剩余的尚未匹配的资源,不做保护。
注意
:
规则的顺序是重要的
,
更具体的规则应该先写
.
现在以
/ admin
开始的所有内容都需要具有
ADMIN
角色的身份验证用 户,
即使是
/ admin / login
路径
(
因为
/ admin / login
已经被
/ admin / **
规则匹配
,
因此第二个规则被忽略
).
(错误的写法)
.antMatchers("/admin/**").hasRole("ADMIN").antMatchers("/admin/login").permitAll()
因此
,
登录页面的规则应该在
/ admin / **
规则之前
.
例如
.
(正确的)
.antMatchers("/admin/login").permitAll().antMatchers("/admin/**").hasRole("ADMIN")
保护
URL
常用的方法有:
authenticated()
保护
URL
,需要用户登录
permitAll()
指定
URL
无需保护,一般应用与静态资源文件
hasRole(String role)
限制单个角色访问,角色将被增加
“ROLE_” .
所以
”ADMIN”
将和
“ROLE_ADMIN”
进行比较
.
hasAuthority(String authority)
限制单个权限访问
hasAnyRole(String… roles)
允许多个角色访问
.
hasAnyAuthority(String… authorities)
允许多个权限访问
.
access(String attribute)
该方法使用
SpEL
表达式
,
所以可以创建复杂的限制
.
hasIpAddress(String ipaddressExpression)
限制
IP
地址或子网
方法授权
现在我们已经掌握了使用如何使用
http.authorizeRequests()
对
web
资源进行授权保护,从
Spring Security2.0
版
本开始,它支持服务层方法的安全性的支持。本节学习
@PreAuthorize,@PostAuthorize, @Secured
三类注解。
我们可以在任何
@Configuration
实例上使用
@EnableGlobalMethodSecurity
注释来启用基于注解的安全性。
以下内容将启用Spring Security的 @Secured 注释
例如:
@EnableGlobalMethodSecurity(securedEnabled = true)public class MethodSecurityConfig {// ...}
然后向方法(在类或接口上)添加注解就会限制对该方法的访问。
Spring Security
的原生注释支持为该方法定义了 一组属性。 这些将被传递给AccessDecisionManager
以供它作出实际的决定:
public interface BankService {
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();
@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}
以上配置标明
readAccount
、
findAccounts
方法可匿名访问,底层使用
WebExpressionVoter
投票器,可从 AffirmativeBased第
23
行代码跟踪。
post
方法需要有
TELLER
角色才能访问,底层使用
RoleVoter
投票器。
使用如下代码可启用prePost注解的支持
@EnableGlobalMethodSecurity(prePostEnabled = true)public class MethodSecurityConfig {// ...}
public interface BankService {
@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);
@PreAuthorize("isAnonymous()")
public Account[] findAccounts();
@PreAuthorize("hasAuthority('p_transfer') and hasAuthority('p_read_account')")
public Account post(Account account, double amount);
}
以上配置标明
readAccount
、
findAccounts
方法可匿名访问,
post
方法需要同时拥有
p_transfer
和
p_read_account
权限才能访问,底层使用
WebExpressionVoter
投票器,可从
AffirmativeBased
第
23
行代码跟踪。