使用Spring Security实现OAuth2授权认证

  • 一、OAuth2.0基本知识
  • 1、什么是 OAuth 2?
  • 二、基于内存的用户、URL权限配置
  • 1、配置pom文件添加依赖
  • 2、配置授权服务器
  • 3、配置 Security
  • 4、添加测试接口
  • 5、 开始测试
  • 附:将令牌保存到 Redis 缓存服务器上
  • 1、增加依赖
  • 2、修改授权服务器配置
  • 3、运行测试
  • 三、基于数据库的用户配置
  • 1、配置pom文件添加依赖、配置数据库
  • 2、创建数据表(三张表用户表、角色表以及用户角色关联表)
  • 3、创建实体类
  • 4、创建数据库访问层
  • 5、mapper.xml
  • 6、创建 UserService
  • 7、配置 Spring Security
  • 8、运行测试
  • 四、基于数据库的URL权限规则配置
  • 1、数据库设计(两张表权限表,权限表和角色关联表)
  • 2、创建实体类
  • 3、创建数据库访问层
  • 4、mapper.xml
  • 5、自定义 FilterInvocationSecurityMetadataSource
  • 6、自定义 AccessDecisionManager
  • 7、配置 Spring Security
  • 8、运行测试


一、OAuth2.0基本知识

1、什么是 OAuth 2?

网上关于OAuth2.0的介绍已经很多了,这里就不做过多的介绍,不太了解的朋友可自行百度

二、基于内存的用户、URL权限配置

1、配置pom文件添加依赖

<!-- Spring Security依赖-->
 <dependency>
 	 <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-security</artifactId>
 </dependency>
 <!--spring-security-oauth2 作为单点登录应用程序的依赖支持-->
 <dependency>
     <groupId>org.springframework.security.oauth</groupId>
     <artifactId>spring-security-oauth2</artifactId>
     <version>2.3.3.RELEASE</version>
 </dependency>

2、配置授权服务器

1、下面是授权服务器配置代码。创建一个自定义类继承自 AuthorizationServerConfigurerAdapter,完成对授权服务器的配置,然后通过 @EnableAuthorizationServer 注解开启授权服务器:
@Configuration
@EnableAuthorizationServer
public class Oauth2Config extends AuthorizationServerConfigurerAdapter {

    // 该对象用来支持 password 模式
    @Autowired
    AuthenticationManager authenticationManager;

    // 该对象用来将令牌信息存储到内存中
    @Autowired(required = false)
    TokenStore inMemoryTokenStore;

    // 该对象将为刷新token提供支持
    @Autowired
    UserDetailsService userDetailsService;

    // 指定密码的加密方式
    @Bean
    PasswordEncoder passwordEncoder() {
        // 使用BCrypt强哈希函数加密方案(密钥迭代次数默认为10)
        return new BCryptPasswordEncoder();
    }

    // 配置 password 授权模式
    @Override
    public void configure(ClientDetailsServiceConfigurer clients)
            throws Exception {
        		clients.inMemory()
                .withClient("password") //客户端ID
                .authorizedGrantTypes("password", "refresh_token") //该client允许的授权模式为password和refresh_token两种
                .accessTokenValiditySeconds(1800) // 配置access_token的过期时间
                .resourceIds("rid") //配置资源id
                .scopes("all")// 允许的授权范围
                .secret("https://www.baidu.com/"); // client_secret 
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.tokenStore(inMemoryTokenStore) //配置令牌的存储(这里存放在内存中)
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        // 表示支持 client_id 和 client_secret 做登录认证
        security.allowFormAuthenticationForClients();
    }
}

3、配置 Security

这里 Spring Security 的配置与传统的 Security 大体相同,不同在于:
   1、这里多了两个 Bean,这两个 Bean 将注入授权服务器配置类中使用。
   2、另外,这里的 HttpSecurity 配置主要是配置“oauth/**”模式的 URL,这一类的请求直接放行。
   3、 注意:在这个 Spring Security 配置和资源服务器配置中,都涉及到了 HttpSecurity。其中 Spring Security 中的配置优先级高于资源服务器中的配置,即请求地址先经过 Spring Security 的 HttpSecurity,再经过资源服务器的 HttpSecurity。
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    @Override
    protected UserDetailsService userDetailsService() {

        return super.userDetailsService();
    }
    //基于内存的用户角色信息配置
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin")
                .password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq") //123
                .roles("admin")
                .and()
                .withUser("sang")
                .password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq") //123
                .roles("user");
    }
   // 基于内存的配置 URL 访问权限
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/oauth/**").authorizeRequests()
                .antMatchers("/oauth/**").permitAll()
                .and().csrf().disable();
    }
}

4、添加测试接口

接着在 Conctoller 中添加如下三个接口用于测试,它们分别需要 admin 角色、use 角色以及登录后访问 。
@RestController
public class HelloController {
 
    @GetMapping("/admin/hello")
    public String admin() {
        return "如果你看见这句话,说明你有ROLE_admin角色";
    }
 
    @GetMapping("/user/hello")
    public String user() {
        return "如果你看见这句话,说明你有ROLE_user角色";
    }
 
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

5、 开始测试

(1)启动项目,首先通过 POST 请求获取 token:

1.请求地址:oauth/token

2.请求参数:用户名、密码、授权模式、客户端 id、scope、以及客户端密码

3.返回结果:access_token 表示获取其它资源是要用的令牌,refresh_token 用来刷新令牌,expires_in 表示 access_token 过期时间。

开启spring boot actuator 安全认证 接口放行 spring boot security oauth2_spring


(2)当 access_token 过期后,可以使用 refresh_token 重新获取新的 access_token(前提是 access_token 未过期),这里也是 POST 请求:

1.请求地址:oauth/token(不变)

2.请求参数:授权模式(变成了 refresh_token)、refresh_token、客户端 id、以及客户端密码

3.返回结果:与获取前面登录获取 token 返回的内容项一样。不过每次请求,access_token 和 access_token有效期都会变化。(3)访问资源时,我们只需要携带上 access_token 参数即可:

开启spring boot actuator 安全认证 接口放行 spring boot security oauth2_java_02

附:将令牌保存到 Redis 缓存服务器上

在上面的样例中,令牌(Access Token)是存储在内存中,这种方式在单服务上可以体现出很好特效(即并发量不大,并且它在失败的时候不会进行备份)
我们也可以将令牌保存到数据库或者 Redis 缓存服务器上。使用这中方式,可以在多个服务之间实现令牌共享。下面我通过样例演示如何将令牌存储在 Redis 缓存服务器上,同时 Redis 具有过期等功能,很适合令牌的存储。

1、增加依赖

(1)首先编辑 pom.xml 文件,在前面的基础上增加 Redis 相关依赖:

<!-- redis  -->
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis</artifactId>
     <exclusions>
         <exclusion>
             <groupId>io.lettuce</groupId>
             <artifactId>lettuce-core</artifactId>
         </exclusion>
     </exclusions>
 </dependency>
 <dependency>
     <groupId>redis.clients</groupId>
     <artifactId>jedis</artifactId>
 </dependency>

(2)接着在 application.yml中配置 Redis 连接信息:

# 基本连接信息配置
spring:
    redis:
      database: 0
      host: 127.0.0.1
      port: 6379
      password:
# 连接池信息配置
      jedis:
        pool:
          max-active: 8
          max-idle: 8
          max-wait: -1ms
          min-idle: 0

2、修改授权服务器配置

@Configuration
@EnableAuthorizationServer
public class Oauth2Config extends AuthorizationServerConfigurerAdapter {

    // 该对象用来支持 password 模式
    @Autowired
    AuthenticationManager authenticationManager;

    // 该对象用来将令牌信息存储到Redis中
    @Autowired
    RedisConnectionFactory redisConnectionFactory;
//    // 该对象用来将令牌信息存储到内存中
//    @Autowired(required = false)
//    TokenStore inMemoryTokenStore;

    // 该对象将为刷新token提供支持
    @Autowired
    UserDetailsService userDetailsService;

    // 指定密码的加密方式
    @Bean
    PasswordEncoder passwordEncoder() {
        // 使用BCrypt强哈希函数加密方案(密钥迭代次数默认为10)
        return new BCryptPasswordEncoder();
    }

    // 配置 password 授权模式
    @Override
    public void configure(ClientDetailsServiceConfigurer clients)
            throws Exception {
        clients.inMemory()
                .withClient("password") //客户端ID
                .authorizedGrantTypes("password", "refresh_token") //授权模式为password和refresh_token两种
                .accessTokenValiditySeconds(1800) // 配置access_token的过期时间
                .resourceIds("rid") //配置资源id
                .scopes("all")// 允许的授权范围
                .secret("https://www.baidu.com/"); 
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
//        endpoints.tokenStore(inMemoryTokenStore) //配置令牌的存储(这里存放在内存中
        endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory))//配置令牌存放在Redis中
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        // 表示支持 client_id 和 client_secret 做登录认证
        security.allowFormAuthenticationForClients();
    }
}

3、运行测试

启动项目,再次通过 /oauth/token 接口获取令牌。然后查看下 Redis 中的数据,可以发现令牌确实已经保存在 Redis 上了:

开启spring boot actuator 安全认证 接口放行 spring boot security oauth2_ide_03

三、基于数据库的用户配置

以上内容中,认证数据都是定义在内存里。而在真实项目中,用户的基本信息以及角色等都存储在数据库中,因此需要从数据库中获取数据进行认证。

接下来使用mybatsi连接数据库

1、配置pom文件添加依赖、配置数据库

<!--mybatis -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatisplus-spring-boot-starter</artifactId>
            <version>${mybatisplus-spring-boot-starter.version}</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generate</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>

        <!-- 数据库驱动依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- 数据库连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.21</version>
        </dependency>

2、创建数据表(三张表用户表、角色表以及用户角色关联表)

注意:角色名需要有一个默认的前缀“ROLE_”

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` int(12) NOT NULL AUTO_INCREMENT,
  `name` varchar(5) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `username` varchar(22) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 123457 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

INSERT INTO `user` VALUES (123, '黄忠', '黄忠', '123');
INSERT INTO `user` VALUES (123456, '程咬金', '程咬金', '123');
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role`  (
  `id` int(12) NOT NULL,
  `name` varchar(15) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `nameZh` varchar(15) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

INSERT INTO `role` VALUES (1, 'ROLE_admin', '射手');
INSERT INTO `role` VALUES (2, 'ROLE_user', '战士');
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role`  (
  `id` int(12) NOT NULL,
  `roleId` int(12) NULL DEFAULT NULL,
  `userId` int(12) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;


INSERT INTO `user_role` VALUES (1, 1, 123456);
INSERT INTO `user_role` VALUES (2, 2, 123456);
INSERT INTO `user_role` VALUES (3, 2, 123);

3、创建实体类

@Data// 要使用 @Data 注解要先引入lombok
public class User extends Model<User> {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    private String name;
	private String username;
    private String password;
	@TableField(exist = false)
	private List<Role> roles;
    @Override
    protected Serializable pkVal() {
        return this.id;
    }
}
@Data
public class Role extends Model<Role> {
    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    private String name;
    private String nameZh;
    @Override
    protected Serializable pkVal() {
        return this.id;
    }
}

4、创建数据库访问层

public interface UserMapper extends BaseMapper<User> {
	User findOneByUsername(@Param("username") String username);
}
public interface RoleMapper extends BaseMapper<Role> {
	List<Role> findRolesByUserId(@Param("userId") Integer integer);
}

5、mapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bobo.mapper.UserMapper">

    <!-- 开启二级缓存 -->
   <!-- <cache type="org.mybatis.caches.ehcache.LoggingEhcache"/>-->

    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.bobo.entity.User">
        <id column="id" property="id" />
        <result column="name" property="name" />
        <result column="username" property="username" />
        <result column="password" property="password" />
    </resultMap>
    <!-- 通用查询结果列 -->
    <sql id="Base_Column_List">
        id, name,username, password
    </sql>
    <select id="findOneByUsername" resultMap="BaseResultMap" parameterType="java.lang.String">
       select * from user where username=#{username}
    </select>
</mapper>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bobo.mapper.RoleMapper">

    <!-- 开启二级缓存 -->
   <!--  <cache type="org.mybatis.caches.ehcache.LoggingEhcache"/> -->

    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.bobo.entity.Role">
        <id column="id" property="id" />
        <result column="name" property="name" />
        <result column="nameZh" property="nameZh" />
    </resultMap>

    <!-- 通用查询结果列 -->
    <sql id="Base_Column_List">
        id, name, nameZh
    </sql>
	<select id="findRolesByUserId" parameterType="java.lang.Integer" resultMap="BaseResultMap">
	SELECT 
		r.* 
	FROM 
		`user` u 
	LEFT JOIN  
		user_role ur on u.id=ur.userId 
	LEFT JOIN 
		role r on r.id= ur.roleId 
	where 
		u.id=#{userId}
	</select>
</mapper>

6、创建 UserService

@Service
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    private UserMapper usermapper;
    @Autowired
    private RoleMapper roleMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = usermapper.findOneByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户名不对");
        }
        List<GrantedAuthority> auth =
                AuthorityUtils.commaSeparatedStringToAuthorityList("role_shooter");
        // 多角色
        String sysRoles = new String("");
        // 获取关联角色
        List<Role> roles = roleMapper.findRolesByUserId(user.getId());
        for (Role role : roles) {
            sysRoles += role.getName() + ",";
        }
        if (sysRoles.length() > 0) {
            sysRoles = (String) sysRoles.subSequence(0, sysRoles.length() - 1);
        }
        // 生成角色权限
        auth = AuthorityUtils.commaSeparatedStringToAuthorityList(sysRoles);
        UserDetails userDetails = new
                org.springframework.security.core.userdetails.User(user.getUsername(),
                new BCryptPasswordEncoder().encode(user.getPassword()), auth);
		 return userDetails;

    }

}

7、配置 Spring Security

Spring Security 大部分配置与前文一样,只不过这次没有配置内存用户,而是将刚刚创建好的 UserService 配置到 AuthenticationManagerBuilder 中。

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    MyUserDetailsService userService;
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }


    //基于内存的用户角色信息配置
//    @Override
//    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        auth.inMemoryAuthentication()
//                .withUser("admin")
//                .password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq") //123
//                .roles("admin")
//                .and()
//                .withUser("sang")
//                .password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq") //123
//                .roles("user");
//    }
    // 基于数据库用户及其对应的角色
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }
    //配置 URL 访问权限
//    @Override
//    protected void configure(HttpSecurity http) throws Exception {
//        http.antMatcher("/oauth/**").authorizeRequests()
//                .antMatchers("/oauth/**").permitAll()
//                .and().csrf().disable();
//    }

        // 基于内存的配置 URL 访问权限
        @Override
        protected  void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests() // 开启 HttpSecurity 配置
            		.antMatchers("/admin/**").hasRole("admin")
                	.antMatchers("/user/**").hasRole("user")
                    .anyRequest().authenticated() //表示剩余的其他接口,登录之后就能访问
                    .and().formLogin()//formLogin() 方法表示开启表单登录
                    .loginProcessingUrl("/login")// 开启表单登录并配置登录接口
                    .permitAll()//permitAll() 表示和登录相关的接口都不需要认证即可访问
                    .and().csrf().disable(); // 关闭csrf
        }
    }

8、运行测试

接下来测试一下,我们使用 admin 用户进行登录,可以访问hello、/admin/hello,而/user/hello需要user,所以admin无法访问

四、基于数据库的URL权限规则配置

1、数据库设计(两张表权限表,权限表和角色关联表)

DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission`  (
  `id` int(10) NOT NULL,
  `permission` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

INSERT INTO `permission` VALUES (1, '/admin/**');
INSERT INTO `permission` VALUES (2, '/user/**');
DROP TABLE IF EXISTS `permission_role`;
CREATE TABLE `permission_role`  (
  `id` int(11) NOT NULL,
  `permission_id` int(11) NULL DEFAULT NULL,
  `role_id` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

INSERT INTO `permission_role` VALUES (1, 1, 1);
INSERT INTO `permission_role` VALUES (2, 2, 2);

2、创建实体类

@Data
public class permission {
    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    private String permission;
    @TableField(exist = false)
    private List<Role> roles;
}

3、创建数据库访问层

@Mapper
public interface MenuMapper {
    List<Menu> getAllMenus();
}

4、mapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bobo.mapper.PermissionMapper">

    <!-- 开启二级缓存 -->
   <!-- <cache type="org.mybatis.caches.ehcache.LoggingEhcache"/>-->

    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.bobo.entity.Permission">
        <id property="id" column="id"/>
        <result property="permission" column="permission"/>
        <collection property="roles" ofType="com.bobo.entity.Role">
            <id property="id" column="rid"/>
            <result property="name" column="rname"/>
            <result property="nameZh" column="rnameZh"/>
        </collection>
    </resultMap>
    <select id="getAllMenus" resultMap="BaseResultMap">
        SELECT p.*,r.id AS rid,r.name AS rname,r.nameZh AS rnameZh
        FROM permission p LEFT JOIN permission_role pr ON p.`id`=pr.`mid` LEFT JOIN role r ON prs.`rid`=r.`id`
    </select>

</mapper>

5、自定义 FilterInvocationSecurityMetadataSource

注意:自定义 FilterInvocationSecurityMetadataSource 主要实现该接口中的 getAttributes 方法,该方法用来确定一个请求需要哪些角色。
@Component
public class CustomMetadataSource implements FilterInvocationSecurityMetadataSource {

    // 创建一个AnipathMatcher,主要用来实现ant风格的URL匹配。
    AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Autowired
    PermissionMapper permissionMapper;

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object)
            throws IllegalArgumentException {
        // 从参数中提取出当前请求的URL
        String requestUrl = ((FilterInvocation) object).getRequestUrl();

        // 从数据库中获取所有的资源信息,即本案例中的permission表以及permission所对应的role
        // 在真实项目环境中,开发者可以将资源信息缓存在Redis或者其他缓存数据库中。
        List<Permission> allMenus = permissionMapper.getAllMenus();

        // 遍历资源信息,遍历过程中获取当前请求的URL所需要的角色信息并返回。
        for (Permission permission : allMenus) {
            if (antPathMatcher.match(permission.getPermission(), requestUrl)) {
                List<Role> roles = permission.getRoles();
                String[] roleArr = new String[roles.size()];
                for (int i = 0; i < roleArr.length; i++) {
                    roleArr[i] = roles.get(i).getName();
                }
                return SecurityConfig.createList(roleArr);
            }
        }

        // 如果当前请求的URL在资源表中不存在相应的模式,就假设该请求登录后即可访问,即直接返回 ROLE_LOGIN.
        return SecurityConfig.createList("ROLE_LOGIN");
    }

    // 该方法用来返回所有定义好的权限资源,Spring Security在启动时会校验相关配置是否正确。
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        // 如果不需要校验,那么该方法直接返回null即可。
        return null;
    }

    // supports方法返回类对象是否支持校验。
    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

6、自定义 AccessDecisionManager

这里与前文的配置相比,主要是修改了 configure(HttpSecurity http) 方法的实现并添加了两个 Bean。至此我们边实现了动态权限配置,权限和资源的关系可以在 menu_role 表中动态调整。
@Component
public class UrlAccessDecisionManager
        implements AccessDecisionManager {

    // 该方法判断当前登录的用户是否具备当前请求URL所需要的角色信息
    @Override
    public void decide(Authentication auth,
                       Object object,
                       Collection<ConfigAttribute> ca){
        Collection<? extends GrantedAuthority> auths = auth.getAuthorities();

        // 如果具备权限,则不做任何事情即可
        for (ConfigAttribute configAttribute : ca) {
            // 如果需要的角色是ROLE_LOGIN,说明当前请求的URL用户登录后即可访问
            // 如果auth是UsernamePasswordAuthenticationToken的实例,说明当前用户已登录,该方法到此结束
            if ("ROLE_LOGIN".equals(configAttribute.getAttribute())
                    && auth instanceof UsernamePasswordAuthenticationToken) {
                return;
            }

            // 否则进入正常的判断流程
            for (GrantedAuthority authority : auths) {
                // 如果当前用户具备当前请求需要的角色,那么方法结束。
                if (configAttribute.getAttribute().equals(authority.getAuthority())) {
                    return;
                }
            }
        }

        // 如果不具备权限,就抛出AccessDeniedException异常
        throw new AccessDeniedException("权限不足");
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}

7、配置 Spring Security

这里与前文的配置相比,主要是修改了 configure(HttpSecurity http) 方法的实现并添加了两个 Bean。至此我们边实现了动态权限配置,权限和资源的关系可以在 menu_role 表中动态调整。
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    MyUserDetailsService userService;

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }


    //基于内存的用户角色信息配置
//    @Override
//    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        auth.inMemoryAuthentication()
//                .withUser("admin")
//                .password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq") //123
//                .roles("admin")
//                .and()
//                .withUser("sang")
//                .password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq") //123
//                .roles("user");
//    }
    // 基于数据库用户及其对应的角色
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }
    //配置 URL 访问权限
//    @Override
//    protected void configure(HttpSecurity http) throws Exception {
//        http.antMatcher("/oauth/**").authorizeRequests()
//                .antMatchers("/oauth/**").permitAll()
//                .and().csrf().disable();
//    }

    // 基于内存的配置 URL 访问权限
//    @Override
//    protected void configure(HttpSecurity http) throws Exception {
//        http.authorizeRequests() // 开启 HttpSecurity 配置
//                .antMatchers("/login").permitAll() // admin/** 模式URL必须具备ADMIN角色
//                .antMatchers("/admin/**").hasRole("admin")
//                .antMatchers("/user/**").hasRole("user")
//                .anyRequest().authenticated() // 用户访问其它URL都必须认证后访问(登录后访问)
//                .and().formLogin()//formLogin() 方法表示开启表单登录
//                .loginProcessingUrl("/login")// 开启表单登录并配置登录接口
//                .permitAll()//permitAll() 表示和登录相关的接口都不需要认证即可访问
//                .and().csrf().disable(); // 关闭csrf
//    }


    // 配置 数据库配置的URL 访问权限
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        object.setSecurityMetadataSource(cfisms());
                        object.setAccessDecisionManager(cadm());
                        return object;
                    }
                })
                .and().formLogin().loginProcessingUrl("/login").permitAll()//开启表单登录并配置登录接口
                .and().csrf().disable(); // 关闭csrf
    }

    @Bean
    CustomMetadataSource cfisms() {
        return new CustomMetadataSource();
    }

    @Bean
    UrlAccessDecisionManager cadm() {
        return new  UrlAccessDecisionManager();
    }

8、运行测试