后端接口权限设计
要利用Spring Security做动态权限控制,首先看一下数据库的权限控制的表
首先用户登录成功之后,会有用户id,根据用户id我可以查询出来他有哪些角色,根据他的角色我可以查询出来他可以操作哪些菜单,再到menu表中查看操作了哪些菜单
在进行接口设计的时候必须要和数据库中的menu表中的url属性是对应的
思路:
简单来说分为两步: 第一步,用户先从前端发起一个http请求,拿到http请求地址之后,我先去分析地址和数据库中的menu表中的哪一个url是相匹配的。就先看一下用户的请求地址跟这里边的哪一个是吻合的。
第一步的核心目的是根据用户的请求地址分析出来它所需要的角色,就是当前的请求需要哪些角色才能访问
第二步是去判断当前用户是否具备它需要的角色
注意:角色不分配给一级菜单,只分配给二级菜单,因为一级并没有一些实质性的接口
CustomFilterInvocationSecurityMetadataSource类
在config包中创建一个CustomFilterInvocationSecurityMetadataSource类,该类的作用是根据用户传来的请求地址,分析出请求需要的角色,该类需要实现FilterInvocationSecurityMetadataSource类并重写三个方法,第一个方法是最重要的。
第一个方法的Collection:当前请求需要的角色 Object:实际上是一个filterInvocation对象
从filterInvocation里面可以获取当前请求的地址,拿到地址后,我就要拿这个地址去数据库里面跟这里的每一个菜单项去匹配,看是符合哪一个模式,然后再去看这个模式需要哪些角色
String requestUrl = ((FilterInvocation) object).getRequestUrl();
@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
MenuService menuService;
AntPathMatcher antPathMatcher = new AntPathMatcher();
// collenction:当前请求需要的角色 Object:实际上是一个filterInvocation对象
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
//从filterInvocation里面可以获取当前请求的地址,拿到地址后,我就要拿这个地址去数据库里面跟这里的每一个菜单项去匹配,看是符合哪一个模式,然后再去看这个模式需要哪些角色
String requestUrl = ((FilterInvocation) object).getRequestUrl();
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
修改model中的menu实体类
新加了private List roles; 这个菜单项需要哪些角色才能访问 一个菜单项对多个角色
public class Menu implements Serializable {
private Integer id;
private String url;
private String path;
private String component;
private String name;
private String iconCls;
private Integer parentId;
private Boolean enabled;
private Meta meta;
private List<Menu> children; //children里面放的是List集合的Menu
//这个菜单项需要哪些角色才能访问
private List<Role> roles;
//省略getter和setter
# 修改service包中的MenuService类
在service包的MenuService类中添加一个根据角色获取所有菜单的方法,返回在menuMapper接口中查询到的数据
@Service
public class MenuService {
@Autowired
MenuMapper menuMapper;
public RespBean getMenusByHrId() {
return RespBean.ok("操作成功!",menuMapper.getMenusByHrId( ((Hr) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getId()));//Id从哪里来呢,前端传过来的信息是不可信,我们登录的用户信息保存在security,可以从Security里获取登录用户信息
}
/**
* 获取所有的菜单角色 一对多 一个菜单项有多个角色
* @return
*/
// @Cacheable
public List<Menu> getAllMenusWithRole(){
return menuMapper.getAllMenusWithRole();
}
}
修改mapper中的MenuMapper接口
List<Menu> getAllMenusWithRole();
这个方法先不写,现在sql数据库里面把sql语句先写好,写对了,再复制过去
定义MenuMapper.xml
<resultMap id="MenuWithRole" type="com.lqg.vhr.model.Menu" extends="BaseResultMap">
<collection property="roles" ofType="com.lqg.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>
在CustomFilterInvocationSecurityMetadataSource配置类里面注入MenuService,然后通过menuService.getAllMenusWithRole()
获取到所有的菜单数据了,这个方法大多数情况下都不会变,可以在service层的该方法上加上@Cacheable缓存
CustomUrlMyDecisionManager配置类
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
AntPathMatcher pathMatcher = new AntPathMatcher();//创建AntPathMatcher,主要用来实现ant风格的URL匹配
@Autowired
MenuService menuService;
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
//从filterInvocation里面可以获取当前请求的地址,拿到地址后,我就要拿这个地址去数据库里面跟这里的每一个菜单项去匹配,看是符合哪一个模式,然后再去看这个模式需要哪些角色
String requestUrl = ((FilterInvocation) o).getRequestUrl();
List<Menu> allMenus = menuService.getAllMenusWithRole();//从数据库中获取所有的资源信息
for (Menu menu : allMenus) {//遍历资源信息
if (pathMatcher.match(menu.getUrl(), requestUrl)) {//每个遍历的资源进行匹配当前请求的URL
List<Role> roles = menu.getRoles();//获取相匹配资源的一个或多个角色Roles对象存在List集合里
String[] rolesStr = new String[roles.size()];//创建一个字符串数组
for (int i = 0; i < roles.size(); i++) {
rolesStr[i] = roles.get(i).getName();//把角色对象的角色名放在字符串数组里面
}
//因为这个方法的需要返回collection<ConfigAttribute>所以需要把rolesStr字串数组转成List数组
return org.springframework.security.access.SecurityConfig.createList(rolesStr);
}
}
//如果都匹配不上就返回ROLE_login这个角色名
return org.springframework.security.access.SecurityConfig.createList("ROLE_login");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return false;
}
}
这样我们的第一步就完成了,第一步的核心目的:根据用户的请求地址分析出它所需要的角色
第二步自定义AccessDecisionManager并重写decide方法,在该方法中判断当前登录的用户是否具备当前请求的URL所需要的信息,如果不具备,就抛出AccessDeniedException异常。
@Configuration
public class CustomUrlMyDecisionManager implements AccessDecisionManager {
/*
decide方法有三个参数:
第一个参数Authentication authentication包含当前登录用户的信息;在User实体实现UserDetails接口的实现方法 getAuthorities方法处理了用户登录的时候存储了当前用户拥有的Role角色名。
第二个参数则是一个FilterInvocation对象,可以获取当前请求对象等。
第三个参数就是FilterInvocation中的getAttributes方法的返回值。
*/
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
//遍历需要的角色
for (ConfigAttribute configAttribute : collection) {
//它需要的角色
String needRole = configAttribute.getAttribute();
//如果它需要的角色是"ROLE_LOGIN"
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 false;
}
@Override
public boolean supports(Class<?> aClass) {
return false;
}
}
CustomUrlMyDecisionManager配置类的作用是分析用户需要的角色你是否具备,如果具备,让请求继续往下走,如果不具备,则抛异常
两个关键类定义好了,接口来在SecurityConfig配置类里面把这两个定义好的配置类引入进来
http.authorizeRequests()
//剩下的其他请求都是登录之后就能访问的
// .anyRequest().authenticated()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
//这边是将原对象传进去,执行对象的postProcess做一些修改之后在把对象返回回来
public <O extends FilterSecurityInterceptor> O postProcess(O object) {FilterSecurityInterceptor获取当前 request 对应的权限配置,调用访问控制器进行鉴权操作等都是核心功能。
object.setAccessDecisionManager(myDecisionManager);
object.setSecurityMetadataSource(metadataSource);
return object;
}
})
接下来在HelloController控制类里面写两个方法测试一下
@Controller
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "hello";
}
@GetMapping("/employee/basic/hello")
public String hello2(){
return "/emp/basic/hello";
}
@GetMapping("/employee/advanced/hello")
public String hello3(){
return "/emp/adv/hello";
}
}
打开postman准备测试
修改完之后,登录成功再访问新添加的两个接口都是403,forbidden,这是不对的
再返回看一下登录时的数据
这里为null是因为我们从头到尾都没有去处理用户角色
查看用户Hr类的返回用户的所有角色的方法的返回值为null,我要给用户搞角色,就可以在hr类里面放一个role集合属性
还要给roles赋值,因为默认登录成功之后,用户是没有角色的
package com.xyg.pojo;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Data
public class Hr implements UserDetails {
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<Role> roles;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name == null ? null : name.trim();
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone == null ? null : phone.trim();
}
public String getTelephone() {
return telephone;
}
public void setTelephone(String telephone) {
this.telephone = telephone == null ? null : telephone.trim();
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address == null ? null : address.trim();
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled;
}
public void setUsername(String username) {
this.username = username == null ? null : username.trim();
}
@Override
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password == null ? null : password.trim();
}
public String getUserface() {
return userface;
}
public void setUserface(String userface) {
this.userface = userface == null ? null : userface.trim();
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark == null ? null : remark.trim();
}
@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类里面用户登录成功之后,给用户设置角色
@Service
public class HrService implements UserDetailsService {
@Autowired
HrMapper hrMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Hr hr =hrMapper.loginByName(username);//根据名字查询用户
if(hr==null){//判断查到的用户是否为空
throw new UsernameNotFoundException("用户不存在!");
}
//登录成功之后,给用户设置角色
hr.setRoles(hrMapper.getHrRolesById(hr.getId()));
return hr;
}
}
在HrMapper接口里边加上getHrRolesById的方法
List<Role> getHrRolesById(Integer id);
在HrMapper.xml文件里面加上如下代码
<select id="getHrRolesById" resultType="com.xyg.pojo.Role">
select r.* from role r,hr_role hrr where hrr.rid=r.id and hrid=#{id}
</select>
现在再重启项目,登录成功之后访问localhost:8080/employee/basic/hello,显示如下:
解决方法:
可以在SecurityConfig配置类里面加个方法即可,代码如下:
@Override
public void configure(WebSecurity web) throws Exception {//如果访问/login页不用经过SpringSecurity
web.ignoring().antMatchers("/login");
}
出现bug没有加载菜单
出现问题在
把它们改成一样就行,就不会拦截home一级菜单了
我没有登录直接访问菜单url会出现
出现这种情况
为了用户更好的体验效果
在main里添加一个判断
如果session里没有用户信息就会跳转到登录页面,有信息说明用户登录了