天天看点

SpringSecurity系列(五) Spring Security 权限设计1. 写在前面2. 给用户添加角色信息3. 权限校验4. 修改 Spring Security 配置类

1. 写在前面

权限设计无非就是:用户-角色-菜单,再加上两张中间表。

首先需要给角色赋予权限菜单,然后再把角色赋给相应的用户。比如人事部门主管用户名是hrUser,他的角色是hrRole,角色用有的权限是/hr/add、/hr/edit等,hrUser这个用户就可以操作人事相关的新增和修改操作。

在Spring Security要怎么实现呢,废话不多说,直接上代码。

2. 给用户添加角色信息

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

2.1 User 实体类

SpringSecurity系列(五) Spring Security 权限设计1. 写在前面2. 给用户添加角色信息3. 权限校验4. 修改 Spring Security 配置类
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>
           

好了,代码到这里就已经把角色赋给用户对象了。此时登录成功以后就可以看到相应的信息。

SpringSecurity系列(五) Spring Security 权限设计1. 写在前面2. 给用户添加角色信息3. 权限校验4. 修改 Spring Security 配置类

接下来我们来看具体的配置信息。

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 用户
SpringSecurity系列(五) Spring Security 权限设计1. 写在前面2. 给用户添加角色信息3. 权限校验4. 修改 Spring Security 配置类
  1. 测试菜单接口
    SpringSecurity系列(五) Spring Security 权限设计1. 写在前面2. 给用户添加角色信息3. 权限校验4. 修改 Spring Security 配置类
    SpringSecurity系列(五) Spring Security 权限设计1. 写在前面2. 给用户添加角色信息3. 权限校验4. 修改 Spring Security 配置类
    代码已传码云,有需自取:https://gitee.com/king-high/vms-master

技术交流+WX:JavaBoy_1024