学习SpringBoot+Vue前后端分离项目,原项目GitHub地址,项目作者江雨一点雨博客。
该部分来自江雨一点雨
在传统的前后端不分的开发中,权限管理主要通过过滤器或者拦截器来进行(权限管理框架本身也是通过过滤器来实现功能),如果用户不具备某一个角色或者某一个权限,则无法访问某一个页面。
但是在前后端分离中,页面的跳转统统交给前端去做,后端只提供数据,这种时候,权限管理不能再按照之前的思路来。
首先要明确一点,前端是展示给用户看的,所有的菜单显示或者隐藏目的不是为了实现权限管理,而是为了给用户一个良好的体验,不能依靠前端隐藏控件来实现权限管理,即数据安全不能依靠前端。
这点就像普通的表单提交一样,前端做数据校验是为了提高效率,提高用户体验,后端才是真正的确保数据完整性。
所以,真正的数据安全管理是在后端实现的,后端在接口设计的过程中,就要确保每一个接口都是在满足某种权限的基础上才能访问,也就是说,不怕将后端数据接口地址暴露出来,即使暴露出来,只要你没有相应的角色,也是访问不了的。
前端为了良好的用户体验,需要将用户不能访问的接口或者菜单隐藏起来。
有人说,如果用户直接在地址拦输入某一个页面的路径,怎么办?此时,如果没有做任何额外的处理的话,用户确实可以通过直接输入某一个路径进入到系统中的某一个页面中,但是,不用担心数据泄露问题,因为没有相关的角色,就无法访问相关的接口。
但是,如果用户非这样操作,进入到一个空白的页面,用户体验不好,此时,我们可以使用 Vue 中的前置路由导航守卫,来监听页面跳转,如果用户想要去一个未获授权的页面,则直接在前置路由导航守卫中将之拦截下来,重定向到登录页,或者直接就停留在当前页,不让用户跳转,也可以顺手再给用户一点点未获授权的提示信息。
总而言之一句话,前端的所有操作,都是为了提高用户体验,不是为了数据安全,真正的权限校验要在后端来做,后端如果是 SSM 架构,建议使用 Shiro ,如果是 Spring Boot + 微服务,建议使用 Spring Security 。
创建src/config/CustomFilterInvocationSecurityMetadataSource
@Component //注册为组件
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
MenuService menuService;
//比对工具,这里用来比对request的url和menu的url
AntPathMatcher antPathMatcher=new AntPathMatcher();
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
//当前请求的地址
String requestUrl = ((FilterInvocation) object).getRequestUrl();
List<Menu> menus = menuService.getAllMenusWithRole();
for (Menu menu : menus) {
//match()中第一个是匹配规则,第二个是需要匹配的对象
if (antPathMatcher.match(menu.getUrl(),requestUrl)){
List<Role> 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");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
在model/Menu和Hr中添加Role及其getter、setter方法
private List<Role> roles;
修改Hr中的Collection
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities =new ArrayList<>(roles.size());
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
在HrService中给用户设置角色
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Hr hr=hrMapper.loadUserByUsername(username);
if (hr==null){
throw new UsernameNotFoundException("用户名不存在");
}
//设置角色
hr.setRoles(hrMapper.getHrRolesById(hr.getId()));
return hr;
}
在HrMapper创建getHrRolesById这个方法
List<Role> getHrRolesById(Integer id);
在HrMapper.xml写SQL语句
<select id="getHrRolesById" resultType="org.javaboy.vhr.model.Role">
SELECT r.* FROM role r,hr_role hrr WHERE hrr.`rid`=r.`id` AND hrr.`hrid`=#{id}
</select>
在MenuService中添加getAllMenusWithRole方法
//@Cacheable 缓存 后面再用
public List<Menu> getAllMenusWithRole(){
return menuMapper.getAllMenusWithRole();
}
在MenuMapper中也添加这个方法
List<Menu> getAllMenusWithRole();
在MenuMapper.xml中添加SQL语句
<resultMap id="MenuWithRole" type="org.javaboy.vhr.model.Menu" extends="BaseResultMap">
<collection property="roles" ofType="org.javaboy.vhr.model.Role">
<id column="rid" property="id" />
<result column="rname" property="name"/>
<result column="rnameZh" property="nameZh"/>
</collection>
</resultMap>
<select id="getAllMenusWithRole" resultMap="MenuWithRole">
select m.*,r.`id` as rid,r.`name` as rname,r.`nameZh` as rnameZh
from menu m,menu_role mr,role r
where m.`id`=mr.`mid` and mr.`rid`=r.`id` order by m.`id`
</select>
先在数据库中写好SQL语句,测试无误后,再写在xml中。
创建src/config/CustomUrlDecisionManager
@Component //注册为组件
public class CustomUrlDecisionManager implements AccessDecisionManager {
@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)){
//判断当前用户是否是匿名用户
if (authentication instanceof AnonymousAuthenticationToken){
throw new AccessDeniedException("尚未登录,请登录!");
}else {
return;
}
}
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 true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
再SecurityConfig中引入上面两个
@Autowired
CustomUrlDecisionManager customUrlDecisionManager;
@Autowired
CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource;
@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()
再添加一个方法
//给登录页放行 不会被拦截
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/login");
}