security.springboot之授权

授权的方?p>

桨?
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(
" +
                "
" +
                "SELECT permission_id FROM t_role_permission WHERE role_id IN(
" +
                "  SELECT role_id FROM t_user_role WHERE user_id = ? 
" +
                ")
" +
                ")
";

        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
行代码跟踪。