SpringSecurity系列(五) Spring Security 权限设计

导读:本篇文章讲解 SpringSecurity系列(五) Spring Security 权限设计,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

1. 写在前面

权限设计无非就是:用户-角色-菜单,再加上两张中间表。
首先需要给角色赋予权限菜单,然后再把角色赋给相应的用户。比如人事部门主管用户名是hrUser,他的角色是hrRole,角色用有的权限是/hr/add、/hr/edit等,hrUser这个用户就可以操作人事相关的新增和修改操作。
在Spring Security要怎么实现呢,废话不多说,直接上代码。

2. 给用户添加角色信息

前面的文章我们已经实现了登录、获取菜单的功能,下面一段代码是为用户赋予角色。

2.1 User 实体类

在这里插入图片描述

package com.javaboy.vms.entity;

import lombok.Getter;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * 用户信息(VUser)实体类
 *
 * @author gaoyang
 * @since 2021-04-20 14:26:27
 */
@Getter
@Setter
public class VUser implements Serializable, UserDetails {
    private static final long serialVersionUID = -60957006911784869L;
    /**
     * 主键
     */
    private Integer id;
    /**
     * 姓名
     */
    private String name;
    /**
     * 手机号码
     */
    private String phone;
    /**
     * 住宅电话
     */
    private String telephone;
    /**
     * 联系地址
     */
    private String address;
    /**
     * 是否启用
     */
    private Boolean enabled;
    /**
     * 用户名
     */
    private String username;
    /**
     * 密码
     */
    private String password;
    /**
     * 头像
     */
    private String userface;
    /**
     * 备注
     */
    private String remark;

    /**
     * 用户角色
     */
    private List<VRole> roles;

    /**
     * 为用户赋予角色
     *
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>(roles.size());
        for (VRole role:roles) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

    @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.2 User 服务实现类

此处在加载用户对象时,根据 userId 查询用户的所有角色并set给用户对象。

@Service("vUserService")
public class VUserServiceImpl implements VUserService, UserDetailsService {
    @Resource
    private VUserMapper vUserMapper;

    /**
     * 根据用户名加载用户对象
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        VUser user = this.vUserMapper.loadUserByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户名不存在!");
        }
        user.setRoles(this.vUserMapper.getUserRoleById(user.getId()));
        return user;
    }
}

2.3 sql

根据用户id查询角色信息

    <select id="getUserRoleById" resultType="com.javaboy.vms.entity.VRole">
        select * from v_role r,v_user_role ur
        where r.id = ur.role_id and ur.user_id = #{id}
    </select>

好了,代码到这里就已经把角色赋给用户对象了。此时登录成功以后就可以看到相应的信息。
在这里插入图片描述
接下来我们来看具体的配置信息。

3. 权限校验

3.1 根据请求地址获取角色

用户登录以后,每发送一次请求,我们都要根据请求地址来获取这个地址所需要的角色信息,然后才可以判断该用户是否具备当前角色的权限。那么,我们直接看代码。

package com.javaboy.vms.config;

import com.javaboy.vms.entity.VMenu;
import com.javaboy.vms.entity.VRole;
import com.javaboy.vms.service.VMenuService;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;

/**
 * @author: gaoyang
 * @date: 2021-05-26 15:10
 * @description: 根据用户传来的请求地址,分析出请求需要的角色
 */
@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    @Resource
    private VMenuService menuService;
    AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        // 获取当前请求地址
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        // 获取所有的权限菜单
        List<VMenu> menus = this.menuService.getAllMenusWithRole();
        for (VMenu menu : menus) {
            // 比较当前请求地址和权限菜单地址
            if (antPathMatcher.match(menu.getUrl(), requestUrl)) {
                // 如果url匹配则获取当前请求所需要的角色
                List<VRole> roles = menu.getRoles();
                String[] str = new String[roles.size()];
                for (int i = 0; i < roles.size(); i++) {
                    str[i] = roles.get(i).getName();
                }
                return SecurityConfig.createList(str);
            }
        }
        // 没有匹配,登录之后可以访问,返回标记信息
        return SecurityConfig.createList("ROLE_LOGIN");
    }

    /**
     * 获取该SecurityMetadataSource对象中保存的针对所有安全对象的权限信息的集合。
     * 该方法的主要目的是被AbstractSecurityInterceptor用于启动时校验每个ConfigAttribute对象。
     * @return
     */
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    /**
     * 这里clazz表示安全对象的类型,该方法用于告知调用者当前SecurityMetadataSource是否支持此类安全对象,
     * 只有支持的时候,才能对这类安全对象调用getAttributes方法
     * @param aClass
     * @return
     */
    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

实现 FilterInvocationSecurityMetadataSource 接口重写 Collection 方法:

  1. object里有我们当前用户对象的基本信息,.getRequestUrl() 获取当前请求地址。
  2. 然后编写 this.menuService.getAllMenusWithRole() 方法获取所有的权限菜单
    <select id="getAllMenusWithRole" resultMap="MenuWithRole">
        select
            m.*,r.id as rid,r.name as rname,r.name_zh as rname_zh
        from
            v_menu m,v_menu_role mr,v_role r
        where
            m.id = mr.menu_id and mr.role_id = r.id
        order by
            m.id
    </select>
  1. 最后比较当前请求地址和权限菜单地址,获取当前地址所需要的角色,没有角色则返回标记信息”ROLE_LOGIN”。

3.2 判断当前用户是否具备角色

上面已经获取了当前请求路径的角色,接下来就要判断当前用户是否具备角色了。

package com.javaboy.vms.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import java.util.Collection;

/**
 * @author: gaoyang
 * @date: 2021-05-26 15:31
 * @description: 判断当前用户是否具备角色
 */
@Slf4j
@Component
public class CustomUrlDecisionManager implements AccessDecisionManager {

    /**
     *
     * @param authentication 包含了当前的用户信息,包括拥有的权限。
     *                       这里的权限来源就是前面登录时UserDetailsService中设置的authorities。
     * @param object FilterInvocation对象,可以得到request等web资源。
     * @param configAttributes 本次访问需要的权限。
     * @throws AccessDeniedException
     * @throws InsufficientAuthenticationException
     */
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
            throws AccessDeniedException, InsufficientAuthenticationException {
        for (ConfigAttribute configAttribute:configAttributes){
            // 需要的角色
            String needRole = configAttribute.getAttribute();
            if("ROLE_LOGIN".equals(needRole)){
                // AnonymousAuthenticationToken 匿名认证
                if (authentication instanceof AnonymousAuthenticationToken){
                    log.error("尚未登录,请登录!");
                    throw new AccessDeniedException("尚未登录,请登录!");
                }else {
                    return;
                }
            }
            // 获取当前登录用户的角色
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

            for (GrantedAuthority authority:authorities) {
                // 判断是否具备当前登录的角色,如果有需要的角色或者是系统管理员则返回 authority.getAuthority().equals("ROLE_admin")
                if (authority.getAuthority().equals(needRole)){
                    return;
                }
            }
        }
        log.error("权限不足,请联系管理员!");
        throw new AccessDeniedException("权限不足,请联系管理员!");
    }

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

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

}

代码注释很全,就不多做解释了。

4. 修改 Spring Security 配置类

首先,在 SecurityConfig 配置类注入刚才的两个bean:

@Resource
private CustomUrlDecisionManager customUrlDecisionManager;
@Resource
private CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource;

然后修改 configure(HttpSecurity http) 方法:

http.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
     @Override
     public <O extends FilterSecurityInterceptor> O postProcess(O object) {
         object.setAccessDecisionManager(customUrlDecisionManager);
         object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
         return object;
     }
 })

完整代码

package com.javaboy.vms.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.javaboy.vms.entity.VUser;
import com.javaboy.vms.service.impl.VUserServiceImpl;
import com.javaboy.vms.util.ResultDTO;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.*;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @author: gaoyang
 * @date: 2021-04-15 16:35
 * @description: Spring Security 配置类
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private VUserServiceImpl vUserService;
    @Resource
    private CustomUrlDecisionManager customUrlDecisionManager;
    @Resource
    private CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource;

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

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(vUserService);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/login");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                //.anyRequest().authenticated()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        object.setAccessDecisionManager(customUrlDecisionManager);
                        object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
                        return object;
                    }
                })
                .and()
                .formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/doLogin")
                .usernameParameter("username")
                .passwordParameter("password")
                // 登录成功回调
                .successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter out = resp.getWriter();
                        VUser vUser = (VUser) authentication.getPrincipal();
                        vUser.setPassword(null);
                        ResultDTO resultDTO = ResultDTO.success("登录成功", vUser);
                        String s = new ObjectMapper().writeValueAsString(resultDTO);
                        out.write(s);
                        out.flush();
                        out.close();
                    }
                })
                // 登录失败回调
                .failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException exception) throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter out = resp.getWriter();
                        ResultDTO resultDTO = ResultDTO.error("登录失败");
                        if (exception instanceof LockedException) {
                            resultDTO.setMsg("账户被锁定,请联系管理员!");
                        } else if (exception instanceof CredentialsExpiredException) {
                            resultDTO.setMsg("密码过期,请联系管理员!");
                        } else if (exception instanceof AccountExpiredException) {
                            resultDTO.setMsg("账户过期,请联系管理员!");
                        } else if (exception instanceof DisabledException) {
                            resultDTO.setMsg("账户被禁用,请联系管理员!");
                        } else if (exception instanceof BadCredentialsException) {
                            resultDTO.setMsg("用户名或者密码输入错误,请重新输入!");
                        }
                        out.write(new ObjectMapper().writeValueAsString(resultDTO));
                        out.flush();
                        out.close();
                    }
                })
                .permitAll()
                .and()
                .logout()
                // 登出回调
                .logoutSuccessHandler(new LogoutSuccessHandler() {
                    @Override
                    public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter out = resp.getWriter();
                        out.write(new ObjectMapper().writeValueAsString(ResultDTO.success("注销成功!")));
                        out.flush();
                        out.close();
                    }
                })
                .permitAll()
                .and()
                .csrf().disable()
                // 没有认证时,在这里处理结果,不要重定向
                .exceptionHandling().authenticationEntryPoint(new AuthenticationEntryPoint() {
            @Override
            public void commence(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
                resp.setContentType("application/json;charset=utf-8");
                PrintWriter out = resp.getWriter();
                ResultDTO resultDTO = ResultDTO.error("访问失败");
                if (e instanceof InsufficientAuthenticationException) {
                    resultDTO.setMsg("请求失败,请联系管理员!");
                }
                out.write(new ObjectMapper().writeValueAsString(resultDTO));
                out.flush();
                out.close();
            }
        });

    }
}

接下来我们测试一下:

  1. 登录 libai 用户

在这里插入图片描述

  1. 测试菜单接口
    在这里插入图片描述
    在这里插入图片描述
    代码已传码云,有需自取:https://gitee.com/king-high/vms-master

技术交流+WX:JavaBoy_1024

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/5421.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!