身份认证,就是判断一个用户是否为合法用户的处理过程。Spring Security中支持多种不同方式的认证,但是无论开发者使用哪种方式认证,都不会影响授权功能使用。因为Spring Security很好做到了认证和授权解耦。
授权,即访问控制,控制谁能访问哪些资源。简单的理解授权就是根据系统提前设置好的规则,给用户分配可以访问某一个资源的权限,用户根据自己所具有的权限,去执行相应操作。
认证成功之后会将当前登录用户信息保存到Authentication对象中,Authentication对象中有一个getAuthorities()方法,用来返回当前登录用户具备的权限信息,也就是当前用户具有权限信息。该方法的返回值为Collection extends GrantedAuthority>,当需要进行权限判断时,就会根据集合返回权限信息调用相应 方法进行判断。
那么问题来了,针对于这个返回值GrantedAuthority应该如何理解?是角色还是权限?
基于角色进行权限管理
基于资源进行权限管理 -->权限字符串
R(Role Resources) B(base) A(access) C(controll)
我们针对于授权可以是基于角色权限管理和基于资源权限管理,从设计层面上来说,角色和权限是两个完全不同的东西:权限是一些具体操作,角色则是某些权限集合。如:READ_BOOK和ROLE_ADMIN是完全不同的。因此至于返回值是什么取决于你的业务设计情况:
基于角色权限设计就是:用户<>角色<>资源 三者关系 返回就是用户的角色
基于资源权限设计就是: 用户<>权限<>资源 三者关系 返回就是用户的权限
基于角色和资源权限设计就是: 用户<>角色<>权限<==>资源 返回统称为用户的权限
为什么统称为权限,因为从代码层面角色和权限没有太大不同都是权限,特别是在Spring Security中。角色和权限处理方式基本上都是一样的。唯一区别SpringSecurity在很多时候会自动给角色添加一个**ROLE_**前缀,而权限则不会自动添加。
可以访问系统中哪些资源(http url method)
Spring Security中提供的权限管理策略主要有两种类型:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//创建内存数据源
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("ADMIN","USER").build());
inMemoryUserDetailsManager.createUser(User.withUsername("lisi").password("{noop}123").roles("USER").build());
inMemoryUserDetailsManager.createUser(User.withUsername("win7").password("{noop}123").authorities("READ_INFO").build());
return inMemoryUserDetailsManager;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/admin").hasRole("ADMIN") //具有 admin角色
.mvcMatchers("/user").hasRole("USER") //具有user角色
.mvcMatchers("/getInfo").hasAuthority("READ_INFO") //具有read_info权限
.antMatchers(HttpMethod.GET,"admin").hasRole("ADMIN")
// .regexMatchers().hasRole() //好处: 支持正则表达式
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf()
.disable();
}
}
基于方法的权限管理主要是通过AOP来实现的,Spring Security中通过MethodSecurityInterceptor来提供相关的实现。不同在于FilterSecurityInterceptor只是在请求之前进行前置处理,MethodSecurityInterceptor除了前置处理之外还可以进行后置处理。前置处理就是在请求之前判断是否具备相应的权限,后置处理则是对方法的执行结果进行二次过滤。前置处理和后置处理分别对应了不同的实现类。
@EnableGlobalMethodSecurity
EnableGlobalMethodSecurity该注解是用来开启权限注解,用法如下:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled=true,securedEnabled=true,jsr250Enabled=true)
public class SecurityConfig extends WebsecurityConfigurerAdapter{
}
# 以上注解含义如下:
- @PostAuthorize: 在目标方法执行之后进行权限校验。
- @PostFilter: 在目标方法执行之后对方法的返回结果进行过滤。
- @PreAuthorize: 在目标方法执行之前进行权限校验。
- @PreFilter: 在目标方法执行之前对方法参数进行过滤。
- @secured: 访问目标方法必须具备相应的角色。
- @DenyAll: 拒绝所有访问。
- @PermitAll: 允许所有访问。
- @RolesAllowed: 访问目标方法必须具备相应的角色。
这些机遇方法的权限管理相关的注解,一般来说只要设置prePostEnabled=true就够用了。
@RestController
@RequestMapping("/hello")
public class AuthorizeMethodController {
@PreAuthorize("hasRole('ADMIN') and authentication.name=='root'")
@GetMapping
public 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") //filterTarget表示要过滤的参数,必须是数组、集合类型 filterObject是数组中的对象,如User。
@PostMapping("/users")
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,"zkt");
}
@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,"zkt"+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";
}
}
ROLE_
前缀,投票器AccessDecisionVoter所做的事情,其实就是比较用户所具有的各个角色和请求某个资源所需的ConfigAttribue之间的关系。在前面的案例中,我们配置的URL拦截规则和请求URL所需要的权限都是通过代码来配置的,这样就比较死板,如果想要调整访问某一个URL所需要的权限,就需要修改代码。
动态管理权限规则就是我们将URL拦截规则和访问URI所需要的权限都保存在数据库中,这样,在不修改源代码的情况下,只需要修改数据库中的数据,就可以对权限进行调整。
用户 <----> 中间表 <----> 角色 <----> 中间表 <---->菜单
set Names utf8mb4;
set foreign_key_checks = 0;
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;
begin;
insert into `menu` values(1,'/admin/**');
insert into `menu` values(2,'/user/**');
insert into `menu` values(3,'/guest/**');
commit;
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;
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);
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;
begin;
insert into `role` values(1,'ROLE_ADMIN','系统管理员');
insert into `role` values(2,'ROLE_USER','普通用户');
insert into `role` values(3,'ROLE_GUEST','游客');
commit;
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;
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,'zkt','{noop}123',1,0);
commit;
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;
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;
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.8version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.38version>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.0version>
dependency>
# 应用名称
spring.application.name=spring-security-17dynamic-authorize
# 应用服务 WEB 访问端口
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_authorize?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root
mybatis.mapper-locations=classpath:com/zkt/mapper/*.xml
mybatis.type-aliases-package=com.zkt.entity
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 User implements UserDetails {
private Integer id;
private String username;
private String password;
private boolean enabled;
private boolean locked;
private List<Role> roles;
@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;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream().map(r ->new SimpleGrantedAuthority(r.getName())).collect(Collectors.toList());
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isLocked() {
return locked;
}
public void setLocked(boolean locked) {
this.locked = locked;
}
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
}
public class Menu {
private Integer id;
private String pattern;
private List<Role> 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;
}
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
}
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zkt.dao.UserMapper">
<select id="loadUserByUsername" resultType="com.zkt.entity.User">
select *
from user
where username = #{username}
select>
<select id="getUserRoleByUid" resultType="com.zkt.entity.Role">
select r.* from role r, user_role ur
where r.id = ur.rid
and ur.rid = #{uid}
select>
mapper>
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zkt.dao.MenuMapper">
<resultMap id="MenuResultMap" type="com.zkt.entity.Menu">
<id property="id" column="id"/>
<result property="pattern" column="pattern">result>
<collection property="roles" ofType="com.zkt.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>
@Mapper
public interface MenuMapper {
List<Menu> getAllMenu();
}
@Mapper
public interface UserMapper {
//根据用户id获取角色信息
List<Role> getUserRoleByUid(Integer uid);
//根据用户名获取用户信息
User loadUserByUsername(String username);
}
@Service
public class UserService implements UserDetailsService {
private UserMapper userMapper;
@Autowired
public UserService(UserMapper userMapper) {
this.userMapper = userMapper;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1.根据用户名查询信息
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();
}
}
@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";
}
}
@Component
public class CustomSecurityMetaSource implements FilterInvocationSecurityMetadataSource {
private MenuService menuService;
@Autowired
public CustomSecurityMetaSource(MenuService menuService) {
this.menuService = menuService;
}
AntPathMatcher antPathMatcher = new AntPathMatcher();
/*
* 自定义动态资源权限元数据信息
* */
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
//1.当前请求对象
String requestURI = ((FilterInvocation)object).getRequest().getRequestURI();
//2.查询所有菜单
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);
}
}
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled=true,securedEnabled=true,jsr250Enabled=true)
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomSecurityMetaSource customSecurityMetadataSource;
private final UserDetailsService userDetailsService;
@Autowired
public MySecurityConfig(CustomSecurityMetaSource customSecurityMetadataSource,UserDetailsService userDetailsService) {
this.customSecurityMetadataSource = customSecurityMetadataSource;
this.userDetailsService = userDetailsService;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
@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(customSecurityMetadataSource);
//是否拒绝公共资源访问
object.setRejectPublicInvocations(false);
return object;
}
});
http.formLogin().and().csrf().disable();
}
}
使用不同的用户进行登录进行访问相对应的资源。