Spring Security 实现用户授权

栏目: Java · 发布时间: 5年前

内容简介:上一次,使用本次,我们通过实现十分简单,大家认真听,都能听得懂。

引言

上一次,使用 Spring SecurityAngular 实现了用户认证。 Spring Security and Angular 实现用户认证

本次,我们通过 Spring Security 的授权机制,实现用户授权。

实现十分简单,大家认真听,都能听得懂。

实现

权限设计

前台实现了菜单的权限控制,但后台接口还没进行保护,只要用户登录成功,什么接口都可以调用。

我们希望实现:用户有什么菜单的权限,只能访问后台对应该菜单的接口。

比如,用户有计算机组管理的菜单,就可以访问计算机组相关的增删改查接口,但是其他的接口都不允许访问。

Spring Security 的设计

依据 Spring Security 的设计,用户对应角色,角色对应后台接口。这是没什么问题的。

Spring Security 实现用户授权

示例

某接口添加 @Secured 注解,内部添加权限表达式。

@GetMapping
@Secured("ROLE_ADMIN")
public List<Host> getAll() {
    return hostService.getAll();
}

然后再为用户创建 Spring Security 中的角色。

这里我们为用户添加 ROLE_ADMIN 的角色授权,与 getAll 方法上的 @Secured("ROLE_ADMIN") 注解中的参数一致,表示该用户有权限访问该方法,这就是授权。

private UserDetails createUser(User user) {
    logger.debug("初始化授权列表");
    List<SimpleGrantedAuthority> authorities = new ArrayList<>();

    logger.debug("角色授权");
    authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));

    logger.debug("构建用户");
    return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities);
}

不足

作为一款优秀的安全框架而言, Spring Security 这样设计是没有任何问题的,我们只需要简简单单的几行代码就能实现接口的授权管理。

但是却不符合我们的要求。

我们要求,在我们的系统中,用户对应多角色。

但是我们的角色是要求可以进行动态配置的,今天有一个系统管理员的角色,明天可能又加一个教师的角色。

在用户授权这方面,是可以实现动态配置的,因为用户的权限列表是一个 List ,我可以从数据库查当前用户的角色,然后 add 进去。

private UserDetails createUser(User user) {
    logger.debug("初始化授权列表");
    List<SimpleGrantedAuthority> authorities = new ArrayList<>();

    logger.debug("角色授权");
    authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));

    logger.debug("构建用户");
    return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities);
}

但是在接口级别,就无法实现动态配置了。大家想想,注解里,要求的参数必须是常量,就是我们想动态配置,也实现不了啊?

@GetMapping
@Secured("ROLE_ADMIN")
public List<Host> getAll() {
    return hostService.getAll();
}

所以,我们总结,因为注解配置的限制,所以在 Spring Security 中角色是 静态 的。

重新设计

我们的角色是动态的,而 Spring Security 中的角色是静态的,所以不能将我们的角色直接映射到 Spring Security 中的角色,要映射也得拿一个我们系统中静态的对象与之对应。

角色是动态的,这个不行了。但是我们的菜单是静态的啊。

Spring Security 实现用户授权

功能模块是我们开发的,菜单就这么固定的几个,用户管理、角色管理、系统设置啥的,在我们开发期间就已经固定下来了,我们是不是可以使用菜单结合 Spring Security 进行授权呢?

Spring Security 实现用户授权

认真看这张图,看懂了这张图,你应该就明白了我的设计思想。

角色是动态的,我不用它授权,我使用静态的菜单进行授权。

静态的菜单对应 Spring Security 中静态的角色,角色再对应后台接口,如此设计,就实现了我们的设想:用户拥有哪个菜单的权限,就只拥有被该菜单调用的相应接口权限。

Spring Security 实现用户授权

编码

设计好了,一起来写代码吧。

授权注解选择

Spring Security 中有多种授权注解,个人经过对比之后选择 @Secured 注解,因为我觉得这个注解配置项更容易被人理解。

public @interface Secured {
    /**
     * Returns the list of security configuration attributes (e.g. ROLE_USER, ROLE_ADMIN).
     *
     * @return String[] The secure method attributes
     */
    public String[]value();
}

直接写一个角色的字符串数组传进去即可。

@Secured("ROLE_ADMIN")                      // 需要拥有`ROLE_ADMIN`角色才可访问
@Secured({"ROLE_ADMIN", "ROLE_TEACHER"})    // 用户拥有`ROLE_ADMIN`、`ROLE_TEACHER`二者之一即可访问

注意:这里的字符串一定是以 ROLE_ 开头, Spring Security 才把它当成角色的配置,否则无效。

启用 @Secured 注解

默认的 Spring Security 是不进行授权注解拦截的,添加注解 @EnableGlobalMethodSecurity 以启用 @Secured 注解的全局方法拦截。

@EnableGlobalMethodSecurity(securedEnabled = true)         // 启用全局方法安全,采用@Secured方式

菜单角色映射

在菜单中新建一个字段 securityRoleName 来声明我们的系统菜单对应着哪个 Spring Security 角色。

// 该菜单在Spring Security环境下的角色名称
@Column(nullable = false)
private String securityRoleName;

建一个类,用于存放所有 Spring Security 角色的配置信息,供全局调用。

这里不能用枚举, @Secured 注解中要求必须是 String 数组,如果是枚举,需要通过 YunzhiSecurityRoleEnum.ROLE_MAIN.name() 格式获取字符串信息,但很遗憾,注解中要求必须是常量。

还记得上次自定义 HTTP 状态码的时候,吃了枚举类无法扩展的亏,以后再也不用枚举了。就算用枚举,也会设计一个接口,枚举实现该接口,不用枚举声明方法的参数类型,而使用接口声明,方便扩展。

package club.yunzhi.huasoft.security;

/**
 * @author zhangxishuo on 2019-03-02
 * Yunzhi Security 角色
 * 该角色对应菜单
 */
public class YunzhiSecurityRole {

    public static final String ROLE_MAIN = "ROLE_MAIN";

    public static final String ROLE_HOST = "ROLE_HOST";

    public static final String ROLE_GROUP = "ROLE_GROUP";

    public static final String ROLE_USER = "ROLE_USER";

    public static final String ROLE_ROLE = "ROLE_ROLE";

    public static final String ROLE_SETTING = "ROLE_SETTING";

}

示例

@GetMapping
@Secured({YunzhiSecurityRole.ROLE_HOST, YunzhiSecurityRole.ROLE_GROUP})
public List<Host> getAll() {
    return hostService.getAll();
}

用户授权

代码体现授权思路:遍历当前用户的菜单,根据菜单中对应的 Security 角色名进行授权。

private UserDetails createUser(User user) {
    logger.debug("获取用户的所有授权菜单");
    Set<WebAppMenu> menus = webAppMenuService.getAllAuthMenuByUser(user);

    logger.debug("初始化授权列表");
    List<SimpleGrantedAuthority> authorities = new ArrayList<>();

    logger.debug("遍历授权菜单,进行角色授权");
    for (WebAppMenu menu : menus) {
        authorities.add(new SimpleGrantedAuthority(menu.getSecurityRoleName()));
    }

    logger.debug("构建用户");
    return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities);
}

注:这里遇到了 Hibernate 惰性加载引起的错误,启用事务防止 Hibernate 关闭 Session ,深层原理目前还在研究。

单元测试

单元测试很简单,供写相同功能的人参考。

@Test
public void authTest() throws Exception {
    logger.debug("获取基础菜单");
    WebAppMenu hostMenu = webAppMenuRepository.findByRoute("/host");
    WebAppMenu groupMenu = webAppMenuRepository.findByRoute("/group");
    WebAppMenu settingMenu = webAppMenuRepository.findByRoute("/setting");

    logger.debug("构造角色");
    List<Role> roleList = new ArrayList<>();

    Role roleHost = new Role();
    roleHost.setWebAppMenuList(Collections.singletonList(hostMenu));
    roleList.add(roleHost);

    Role roleGroup = new Role();
    roleGroup.setWebAppMenuList(Collections.singletonList(groupMenu));
    roleList.add(roleGroup);

    Role roleSetting = new Role();
    roleSetting.setWebAppMenuList(Collections.singletonList(settingMenu));
    roleList.add(roleSetting);

    logger.debug("保存角色");
    roleRepository.saveAll(roleList);

    logger.debug("构造用户");
    User user = userService.getOneUnSavedUser();

    logger.debug("获取用户名和密码");
    String username = user.getUsername();
    String password = user.getPassword();

    logger.debug("保存用户");
    userRepository.save(user);

    logger.debug("用户登录");
    String token = this.loginWithUsernameAndPassword(username, password);

    logger.debug("无授权用户访问host,断言403");
    this.mockMvc.perform(MockMvcRequestBuilders.get(HOST_URL)
            .header(TOKEN_KEY, token))
            .andExpect(status().isForbidden());

    logger.debug("用户授权Host菜单");
    user.getRoleList().clear();
    user.getRoleList().add(roleHost);
    userRepository.save(user);

    logger.debug("重新登录, 重新授权");
    token = this.loginWithUsernameAndPassword(username, password);

    logger.debug("授权Host用户访问,断言200");
    this.mockMvc.perform(MockMvcRequestBuilders.get(HOST_URL)
            .header(TOKEN_KEY, token))
            .andExpect(status().isOk());

    logger.debug("用户授权Group菜单");
    user.getRoleList().clear();
    user.getRoleList().add(roleGroup);
    userRepository.save(user);

    logger.debug("重新登录, 重新授权");
    token = this.loginWithUsernameAndPassword(username, password);

    logger.debug("授权Group用户访问,断言200");
    this.mockMvc.perform(MockMvcRequestBuilders.get(HOST_URL)
            .header(TOKEN_KEY, token))
            .andExpect(status().isOk());

    logger.debug("用户授权Setting菜单");
    user.getRoleList().clear();
    user.getRoleList().add(roleSetting);
    userRepository.save(user);

    logger.debug("重新登录, 重新授权");
    token = this.loginWithUsernameAndPassword(username, password);

    logger.debug("授权Setting用户访问,断言403");
    this.mockMvc.perform(MockMvcRequestBuilders.get(HOST_URL)
            .header(TOKEN_KEY, token))
            .andExpect(status().isForbidden());
}

private String loginWithUsernameAndPassword(String username, String password) throws Exception {
    logger.debug("用户登录");
    byte[] encodedBytes = Base64.encodeBase64((username + ":" + password).getBytes());
    MvcResult mvcResult = this.mockMvc.perform(MockMvcRequestBuilders.get(LOGIN_URL)
            .header("Authorization", "Basic " + new String(encodedBytes)))
            .andExpect(status().isOk())
            .andReturn();

    logger.debug("从返回体中获取token");
    String json = mvcResult.getResponse().getContentAsString();
    JSONObject jsonObject = JSON.parseObject(json);
    return jsonObject.getString("token");
}

总结

感谢开源社区,感谢 Spring Security

五行代码(不算注释),一个注解。就解决了一直以来困扰我们的权限问题。


以上所述就是小编给大家介绍的《Spring Security 实现用户授权》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Head First Rails

Head First Rails

David Griffiths / O'Reilly Media / 2008-12-30 / USD 49.99

Figure its about time that you hop on the Ruby on Rails bandwagon? You've heard that it'll increase your productivity exponentially, and allow you to created full fledged web applications with minimal......一起来看看 《Head First Rails》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

html转js在线工具
html转js在线工具

html转js在线工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具