Spring Security结合RBAC+Redis+Swagger2实现菜单列表

紧接着上篇博客,上面我们已经完成了基本的登录功能,下面开始整合Swagger2、测试登录功能接着完善我们的项目。这一篇主要是实现菜单列表。

1.配置Swagger2

由于是前后端分离项目,所以接口文档是必须的。为了规范,这里我们使用Swagger2,前面在代码自动生成时已经导入了依赖,现在在config目录下写Swagger配置类。

SwaggerConfig.java

@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket createRestApi() {
        // 文档类型
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.kt.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("VRS接口文档")
                .description("VRS接口文档")
                .contact(new Contact("KamTeng", "http:localhost:8081/doc.html", "*********@qq.com"))
                .version("1.0")
                .build();
    }
}

1.1.测试Swagger2

编写一个测试类

TestController.java

@RestController
public class TestController {
    @GetMapping("/test")
    public String test() {
        return "it's a test";
    }
}

端口:http://localhost:8081/doc.html

在这里插入图片描述

出现这样的界面说明一切都没有问题。

但是点击test-controller,调试发送,结果:

在这里插入图片描述

这是因为test接口需要登录之后才可以访问,前面SecurityConfig中只放行了login、index、logout以及静态资源,所以剩下的所有资源都要经过登录之后才可以访问。

Swagger提供了全局登录功能,登录之后把JWT令牌放到全局的Authorization里面,Swagger文档相当于访问Test接口,会携带JWT令牌,携带了令牌之后经过刚刚写的登录授权拦截器(JwtAuthorizationTokenFilter),有令牌会自动登录,就能够访问接口了。

修改SwaggerConfig.java

@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.kt.controller"))
                .paths(PathSelectors.any())
                .build()
                .securityContexts(securityContexts())
                .securitySchemes(securitySchemes());
    }

    private ApiInfo apiInfo() {
        ......
    }

    private List<ApiKey> securitySchemes() {
        // 设置请求头信息
        List<ApiKey> keys = new ArrayList<>();
        ApiKey apiKey = new ApiKey("Authorization", "Authorization", "Header");
        keys.add(apiKey);
        return keys;
    }

    private List<SecurityContext> securityContexts() {
        // 设置需要登录认证的路径
        List<SecurityContext> contexts = new ArrayList<>();
        contexts.add(getContextByPath("/test/.*"));
        return contexts;
    }

    private SecurityContext getContextByPath(String path) {
        return SecurityContext.builder()
                .securityReferences(defaultAuth())
                .forPaths(PathSelectors.regex(path))
                .build();
    }

    private List<SecurityReference> defaultAuth() {
        List<SecurityReference> references = new ArrayList<>();
        // global全局accessEverything允许所以
        AuthorizationScope scope = new AuthorizationScope("global", "accessEverything");
        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
        authorizationScopes[0] = scope;
        references.add(new SecurityReference("Authorization", authorizationScopes));
        return references;
    }
}

1.2重新测试项目:

重启项目后先获取验证码,若不确定验证码可以去后端控制台查看。

在这里插入图片描述

在这里插入图片描述

username:admin

password:123456

在这里插入图片描述

参数值中输入:

Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImNyZWF0ZWQiOjE2MTQ5NTM5MjU2MjQsImV4cCI6MTYxNTU1ODcyNX0.-uFTquk9QJgvpbNcT_flEUh2l8UqOS7kovIF5EDpUvr4lkJ5M1Haqu_KZJAjZtDNaiWr3cqqbxEq2RVOwQrlIg

注意:Bearer后有一个空格

输入完后点击保存
在这里插入图片描述

获取当前登录用户的信息(直接点击发送即可)

如果有结果则一切正常。

在这里插入图片描述

2.菜单列表

根据当前登录用户id和对应的角色查询菜单列表,每个能够成功登录的用户都会带有一个角色或者几个角色(或者没有),我们的t_menu_role中就有对应的mid(菜单id)、rid(权限id),t_admin_role表中也有rid,也就是说根据t_menu表中的url找到对应的菜单id,然后根据中间表(t_role)判断需要哪些角色拥有这些url权限,最后进行比较,就知道这个用户是不是真的具有访问菜单的权限。

2.1.权限管理RBAC基本概念

RBAC是基于角色的访问控制(Role-Based Access Control),再RBAC中,权限与角色相关联,用户通过扮演适当的角色从而得到相应权限。这样的管理都是层级相互依赖的,权限赋予角色,角色又赋予用户,这样的权限设计清除,管理起来也很方便。

RBAC授权实际上是WhoWhatHow三元组之间的关系,也就是WhoWhat进行How的操作,简单说明就是谁对什么资源做了怎样的操作。

2.2.RBAC表结构设计

2.2.1实体对应关系

用户-角色-资源实体间对应关系图分析:

在这里插入图片描述

在这里插入图片描述

这里用户与角色实体对应关系为多对多,角色与资源对应关系同样为多对多关系,所以在实体设计上用户与角色间增加用户角色实体,将多对多的对应关系拆分为一对多,同理,角色与资源多对多对应关系拆分出中间实体对象权限实体。

2.2.2.表结构设计

从上面实体对应关系分析,权限表设计分为以下基本的五张表结构:用户表(t_admin),角色表(t_role),用户角色表(t_admin_role),菜单表(t_menu),菜单权限表(t_menu_role),表结构如下:

在这里插入图片描述

t_admin_role表:

在这里插入图片描述

t_menu_role表:
在这里插入图片描述

2.3.定义子菜单和角色列表

修改我们的Menu实体类和Admin实体类,因为在返回菜单的时候可能会存在子菜单,所以只需要在Menu.java中定义Children属性即可,我们还需要为Admin、Menu都添加一个role属性,并且修改Admin中的getAuthorities()方法,后面需要对Menu和Admin中的role进行比较。

Menu.java

@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_menu")
@ApiModel(value="Menu对象", description="")
public class Menu implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "id")
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    @ApiModelProperty(value = "url")
    private String url;

    @ApiModelProperty(value = "path")
    private String path;
    ......
	@ApiModelProperty(value = "子菜单")
	@TableField(exist = false)
	private List<Menu> children;
    
    @ApiModelProperty(value = "角色列表")
    @TableField(exist = false)
    private List<Role> roles;
}

Admin.java

@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_admin")
@ApiModel(value="Admin对象", description="")
public class Admin implements Serializable, UserDetails {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "id")
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    ......

    @ApiModelProperty(value = "备注")
    private String remark;

    @ApiModelProperty(value = "角色/权限")
    @TableField(exist = false)
    private List<Role> roles;

    @Override
    @JsonDeserialize(using = CustomAuthorityDeserializer.class)
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorityList = roles
                .stream()
                .map(role -> new SimpleGrantedAuthority(role.getName()))
                .collect(Collectors.toList());
        return authorityList;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

2.4.实现查询菜单功能

用户正常登录之后,用户的相关信息会在后端通过从spring security全局对象获取我们想要的用户信息,不需要前端传。所以我们新建一个类,这个类就是专门用了从spring security中获取用户信息,方便后面设计到用户信息的接口和方法。

菜单被频繁的读取和渲染时,如果菜单数量过大,每一次的这种操作势必会重复查询数据库,为了提高菜单加载速度,我们可以将第一次通过sql语句从数据库中查询出来的菜单放到Redis里,后面直接从Redis中获取,而不会通过sql语句反复查询数据库。当然这次项目涉及到的菜单不是很多,而且Redis每次需要打开服务,所以我会把功能实现,但后面我不会使用Redis。根据自身情况可用可不用。

2.4.1.Redis相关依赖和配置

<!-- spring-data-redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- commons-pool2 对象池 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

application.yml(端口、密码等根据自身情况,yml注重格式,注意缩进)

# redis
  redis:
    # 超时时间
    timeout: 10000ms
    # 服务器地址
    host: 
    # 服务器端口
    port: 6379
    # 数据库
    database: 0
    # 密码
    password: 654321
    lettuce:
      pool:
        # 最大连接数 默认8
        max-active: 1024
        # 最大连接阻塞等待时间 默认-1
        max-wait: 10000ms
        # 最大空闲连接
        max-idle: 200
        # 最小空闲连接
        min-idle: 5

RedisConfig.java

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // String类型 key序列器
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // String类型 value序列器
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        // Hash类型 key序列器
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        // Hash value序列器
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(factory);
        return redisTemplate;
    }
}

2.4.2.从spring security中获取用户根据类

AdminUtils.java

public class AdminUtils {
    public static Admin getCurrentAdmin() {
        // 获取全局上下文,获取用户。
        return (Admin) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    }
}

2.4.3.通过用户ID查询菜单列表服务类、实现类和Mapper

IMenuService.java

public interface IMenuService extends IService<Menu> {
    /**
     * 通过用户ID查询菜单列表
     * @return
     */
    List<Menu> getMenusByAdminId();
    /**
     * 根据角色获得菜单列表
     * @return
     */
    List<Menu> getMenusWithRole();
}

MenuServiceImpl.java

@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements IMenuService {
@Override
    public List<Menu> getMenusByAdminId() {
        Integer adminId = AdminUtils.getCurrentAdmin().getId();
        // 如果Redis未开启服务,会报RedisConnectionFailureException,那么我们就直接从数据库中获取
        try {
            ValueOperations<String, Object> opsForValue = redisTemplate.opsForValue();
            // 从redis获取菜单数据
            List<Menu> menus = (List<Menu>) opsForValue.get("menu_" + adminId);
            // 如果为空,就去数据库中获取
            if (CollectionUtils.isEmpty(menus)) {
                menus = menuMapper.getMenusByAdminId(adminId);
                // 将数据设置到redis中
                opsForValue.set("menu_" + adminId, menus);
            }
            return menus;
        } catch (RedisConnectionFailureException e) {
            List<Menu> menus = menuMapper.getMenusByAdminId(adminId);
            return menus;
        }
    }
    @Override
    public List<Menu> getMenusWithRole() {
        return menuMapper.getMenusWithRole();
    }
}

MenuMapper.java

public interface MenuMapper extends BaseMapper<Menu> {

    /**
     *通过用户ID查询菜单列表
     * @param adminId
     * @return
     */
    List<Menu> getMenusByAdminId(Integer adminId);
    /**
     * 根据角色获得菜单列表
     * @return
     */
    List<Menu> getMenusWithRole();
}

因为需要用Admin中的Role与Menu中的Role比较,那么还需要查询出我们登录的Admin对应的Role(角色)。

2.4.4.通过用户ID查询Role服务类、实现类和Mapper

在IAdminService.java添加对应接口

public interface IAdminService extends IService<Admin> {

	......
	
    /**
     * 根据用户id查询角色列表
     * @param adminId
     * @return
     */
    List<Role> getRoles(Integer adminId);
}

AdminServiceImpl.java

@Service
public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements IAdminService {

    @Resource
    private UserDetailsService userDetailsService;
    @Resource
    private PasswordEncoder passwordEncoder;

	......

    @Override
    public List<Role> getRoles(Integer adminId) {
        return roleMapper.getRoles(adminId);
    }
}

RoleMapper.java

public interface RoleMapper extends BaseMapper<Role> {
    /**
     * 根据用户id查询角色列表
     * @param adminId
     * @return
     */
    List<Role> getRoles(Integer adminId);
}

2.4.5.过滤器

在component目录下新建一个权限控制类分别是专门根据请求的url分析请求所需的角色的过滤器(CustomFilter)和判断用户角色的过滤器(CustomUrlDecisionManger)。

CustomFilter.java

@Component
public class CustomFilter implements FilterInvocationSecurityMetadataSource {

    @Resource
    private IMenuService menuService;

    AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        // 获取请求的url
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        List<Menu> menus = menuService.getMenusWithRole();
        for (Menu menu : menus
             ) {
            // 判断请求url与菜单角色是否匹配
            if (antPathMatcher.match(menu.getUrl(), requestUrl)) {
                String[] str = menu.getRoles().stream().map(Role::getName).toArray(String[]::new);
                return SecurityConfig.createList(str);
            }
        }
        // 匹配不成功,默认登录即可访问
        return SecurityConfig.createList("ROLE_LOGIN");
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return false;
    }
}

CustomUrlDecisionManger.java

@Component
public class CustomUrlDecisionManger implements AccessDecisionManager {

    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> collection)
            throws AccessDeniedException, InsufficientAuthenticationException {
        for (ConfigAttribute configAttribute : collection) {
            // 当前url所需角色
            String needRole = configAttribute.getAttribute();
            // 判断角色是否登录即可访问的角色,此角色在CustomFilter中设置
            if ("ROLE_LOGIN".equals(needRole)) {
                if (authentication instanceof AnonymousAuthenticationToken) {
                    throw new AccessDeniedException("尚未登录,请先登录!");
                } else {
                    return;
                }
            }
            // 判断用户角色是否为url所需角色
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().equals(needRole)) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("权限不足,请联系管理员。");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return false;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return false;
    }
}

写完之后配置Security

修改SecurityConfig.java

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private IAdminService adminService;
    @Resource
    private RestAuthorizationEntryPoint restAuthorizationEntryPoint;

    ......

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 使用JWT,不需要csrf
        http.csrf().disable()
                // 使用JWT,不需要session
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 所有请求都要认证
                .anyRequest().authenticated()
                // 动态权限配置
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                        o.setAccessDecisionManager(customUrlDecisionManger);
                        o.setSecurityMetadataSource(customFilter);
                        return o;
                    }
                })
                .and()
                // 禁用缓存
                .headers()
                .cacheControl();
        // 添加JWT 登录授权过滤器
        http.addFilterBefore(jwtAuthorizationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        // 添加自定义未授权和未登录结果返回
        http.exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthorizationEntryPoint);
    }

    @Override
    @Bean
    public UserDetailsService userDetailsService() {
        return username -> {
            Admin admin = adminService.getAdminByUserName(username);
            if (admin != null) {
                admin.setRoles(adminService.getRoles(admin.getId()));
                return admin;
            }
            throw new UsernameNotFoundException("用户名或密码不存在");
        };
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public JwtAuthorizationTokenFilter jwtAuthorizationTokenFilter() {
        return new JwtAuthorizationTokenFilter();
    }

}

2.4.6.接口

MenuController.java

@RestController
@RequestMapping("/system/cfg")
public class MenuController {

    @Resource
    private IMenuService menuService;

    @ApiOperation(value = "通过用户ID查询菜单列表")
    @GetMapping("/menu")
    public List<Menu> getMenusByAdminId() {
        return menuService.getMenusByAdminId();
    }
}

2.5.SQL语句编写

MenuMapper.xml

<!-- 通过用户ID查询菜单列表 -->
    <select id="getMenusByAdminId" resultMap="Menus">
        SELECT DISTINCT
            m1.*,
            m2.id AS id2,
            m2.url AS url2,
            m2.path AS path2,
            m2.component AS component2,
            m2.`name` AS name2,
            m2.iconCls AS iconCls2,
            m2.requireAuth AS requireAuth2,
            m2.parentId AS parentId2,
            m2.enabled AS enabled2
        FROM
            t_menu m1,
            t_menu m2,
            t_admin_role ar,
            t_menu_role mr
        WHERE
	        m1.id = m2.parentId
        AND m2.id = mr.mid
        AND mr.rid = ar.rid
        AND ar.adminId = #{id}
        AND m2.enabled = TRUE
        ORDER BY
            m2.id
    </select>
    <!-- 根据角色获得菜单列表 -->
    <select id="getMenusWithRole" resultMap="MenusWithRole">
        SELECT
            m.*,
            r.id AS rid,
            r.`name` AS rname,
            r.nameZh AS rnameZh
        FROM
            t_menu m,
            t_menu_role mr,
            t_role r
        WHERE
            m.id = mr.mid
        AND r.id = mr.rid
        ORDER BY
	        m.id
    </select>

resultMap:Menus和MenusWithRole

<resultMap id="Menus" type="com.kt.pojo.Menu" extends="BaseResultMap">
    <collection property="children" ofType="com.kt.pojo.Menu">
        <id column="id2" property="id"/>
        <result column="url2" property="url"/>
        <result column="path2" property="path"/>
        <result column="component2" property="component"/>
        <result column="name2" property="name"/>
        <result column="iconCls2" property="iconCls"/>
        <result column="requireAuth2" property="requireAuth"/>
        <result column="parentId2" property="parentId"/>
        <result column="enabled2" property="enabled"/>
     </collection>
</resultMap>
<resultMap id="MenusWithRole" type="com.kt.pojo.Menu" extends="BaseResultMap">
    <collection property="roles" ofType="com.kt.pojo.Role">
        <id column="rid" property="id"/>
        <result column="rname" property="name"/>
        <result column="rnameZh" property="nameZh"/>
    </collection>
</resultMap>

RoleMapper.xml

    <!-- 根据用户id查询角色列表 -->
    <select id="getRoles" resultType="com.kt.pojo.Role">
        SELECT
            r.id,
            r.`name`,
            r.nameZh
        FROM
	        t_role AS r
	    LEFT JOIN t_admin_role AS ar ON r.id = ar.rid
        WHERE
	        ar.adminId = #{adminId}
    </select>

最后修改我们的登录方法

LoginController.java

@ApiOperation(value = "获取当前登录用户的信息")
@GetMapping("/admin/info")
public Admin getAdminInfo(Principal principal) {
    if (principal != null) {
        String username = principal.getName();
        Admin admin = adminService.getAdminByUserName(username);
        admin.setPassword(null);
        admin.setRoles(adminService.getRoles(admin.getId()));
        return admin;
    }
    return null;
}

到这里整合Swagger2以及根据角色权限查询菜单列表模块就完成了,由于篇幅和代码量较大,对应的测试和职位、职称模块留在下一篇博客:

👇👇👇
处理全局异常

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值