权限管理(二)
权限功能
1、数据库
因为是权限管理所以需要根据用户的role来查找所对应的权限menu
简单来说分为两步: 第一步,用户先从前端发起一个http请求,拿到http请求地址之后,我先去分析地址和数据库中的menu表中的哪一个url是相匹配的。就先看一下用户的请求地址跟这里边的哪一个是吻合的。
第一步的核心目的是根据用户的请求地址分析出来它所需要的角色,就是当前的请求需要哪些角色才能访问
第二步是去判断当前用户是否具备它需要的角色
2、UrlFilterInvocationSecurityMetadataSource类
通过当前的请求地址,获取该地址需要的用户角色
它的主要责任就是当访问一个url时,返回这个url所需要的访问权限
这个方法需要调用FilterInvocationSecurityMetadataSource接口
第一个方法:从filterInvocation里面可以获取当前请求的地址,拿到地址后,就要拿这个地址去menu里面的每一个项去匹配, 看是符合哪一个模式,然后再去看这个模式需要哪些角色
@Slf4j
@Component
public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
MenuRepository menuRepository;
AntPathMatcher antPathMatcher = new AntPathMatcher();
/**
* 返回本次访问需要的权限,可以有多个权限
* collection:当前请求需要的角色 o:实际上是一个filterInvocation对象
* @return
* @throws IllegalArgumentException
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
String requestUrl = ((FilterInvocation) o).getRequestUrl();
//这个方法每次请求都会调用
List<Menu> allMenu = menuRepository.findAll();
//比较request跟这menus里面的url是否一致 遍历menus 借助AntPathMatcher工具进行
for(Menu menu:allMenu){
if (antPathMatcher.match(menu.getUrl(),requestUrl)
&& menu.getRoles().size()>0){
List<Role> roles = menu.getRoles();
int size = roles.size();
String[] values = new String[size];
for(int i = 0;i<size;i++){
values[i] = roles.get(i).getName();
}
log.info("当前访问路径是{},这个url所需要的访问权限是{}",requestUrl,values);
return SecurityConfig.createList(values);
}
}
//没有匹配上的资源,都是登陆访问
log.info("当前访问路径是{},这个url所需要的访问权限是{}","ROLE_LOGIN");
return SecurityConfig.createList("ROLE_LOGIN");
}
/**
* 此处方法如果做了实现,返回了定义的权限资源列表,
* Spring Security会在启动时校验每个ConfigAttribute是否配置正确,
* 如果不需要校验,这里实现方法,方法体直接返回null即可。
* @return
*/
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
/**
* 方法返回类对象是否支持校验,
* web项目一般使用FilterInvocation来判断,或者直接返回true
* @param aClass
* @return
*/
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
3、MenuService
public interface MenuService {
List<Menu> getMenusByHrId(Integer id);
}
==getMenusByHrId(Integer id)==这个方法是通过Hr表中的id查询到每个用户所对应的menu,也就是权限,这里我们需要实现这个方法
@Service
public class MenuServiceImpl implements MenuService {
@Autowired
private MenuRepository menuRepository;
@Autowired
private MenuroleRepository menuroleRepository;
@Autowired
private HrRoleRepository hrRoleRepository;
@Override
public List<Menu> getMenusByHrId(Integer hrid) {
List<hrRole> list = hrRoleRepository.findAllByHrid(hrid);
List<Integer> rids = new ArrayList<>();
for (hrRole hrrole:list){
rids.add(hrrole.getRid());
}
List<MenuRole> mids = menuroleRepository.findByRidIn(rids);
List<Integer> menus = new ArrayList<>();
for (MenuRole menuRole:mids){
menus.add(menuRole.getMid());
}
return menuRepository.findAllByIdIn(menus);
}
}
public interface MenuRepository extends JpaRepository<Menu,Integer> {
public List<Menu> findAllById(Integer id);
//根据数组中的每一个元素查询Menu类
List<Menu> findAllByIdIn(List<Integer> id);
}
public interface MenuroleRepository extends JpaRepository<MenuRole,Integer> {
public List<MenuRole> findAllById(Integer id);
public List<MenuRole> findByRidIn(List<Integer> Rids);
}
public interface HrRepository extends JpaRepository<Hr,Integer> {
public Hr findByUsername(String username);
public List<Role> findAllRolesById(Integer id);
}
4、UrlAccessDecisionManager类
判断当前用户是否具备这些角色,需要调用AccessDecisionManager类
@Component
public class UrlAccessDecisionManager implements AccessDecisionManager {
//Collection<ConfigAttribute> collection是是UrlFilterInvocationSecurityMetadataSource中的getAttributes方法传来的,
// 表示当前请求需要的角色(可能有多个)
/**
*
* @param authentication:包含了当前的用户信息,包括拥有的权限,
* 即之前UserDetailsService登录时候存储的用户对象
* @param o:就是FilterInvocation对象,可以得到request等web资源
* configAttributes 是本次访问需要的权限。
* 即上一步的 UrlFilterInvocationSecurityMetadataSource 中查询核对得到的权限列表
* @param collection
* @throws AccessDeniedException
* @throws InsufficientAuthenticationException
*/
//用户的角色在authentication里面,需要的角色在configAttributes里面,再去比较他们俩集合里面有没有包含关系就行
@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 true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
5、修改HrService类
@Service
public class HrService implements UserDetailsService {
@Autowired
HrRepository hrRepository;
@Autowired
HrRoleRepository hrRoleRepository;
@Autowired
RoleRepository roleRepository;
//通过Hr的id查询Role
public List<Role> getHrRolesByHrid(Integer hrid){
List<hrRole> list = hrRoleRepository.findAllByHrid(hrid);
List<Role> roles = new ArrayList<>();
for (hrRole hrrole:list){
roles.add(roleRepository.findById(hrrole.getRid()).get());
}
return roles;
}
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
Hr hr = hrRepository.findByUsername(s);
if (hr == null){
throw new UsernameNotFoundException("用户名不对");
}
//给用户设置角色
hr.setRoles(getHrRolesByHrid(hr.getId()));
return hr;
}
}
6、配置webSecurityConfig类
通过==.withObjectPostProcessor()==讲这两个类注入
@Configuration
public class webSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
HrService hrService;
@Autowired
UrlFilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource;
@Autowired
UrlAccessDecisionManager urlAccessDecisionManager;
@Autowired
AccessDeniedHandler accessDeniedHandler;
//明文显示密码
@Bean
public CustomPasswordEncoder passwordEncoder() {
return new CustomPasswordEncoder();
}
//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(hrService)
.passwordEncoder(new CustomPasswordEncoder());
// auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("user1").password(new BCryptPasswordEncoder().encode("123")).roles("USER");
}
/**
* 通过withObjectPostProcessor将MyFilterInvocationSecurityMetadataSource和MyAccessDecisionManager注入进来
* 此url先被UrlFilterInvocationSecurityMetadataSource处理,然后 丢给UrlAccessDecisionManager处理
* 如果不匹配,返回 MyAccessDeniedHandler
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//剩下的其他请求都是登陆之后就能访问的
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setAccessDecisionManager(urlAccessDecisionManager);
o.setSecurityMetadataSource(urlFilterInvocationSecurityMetadataSource);
return o;
}
})
//单表登陆
.and().formLogin()
//修改默认登陆的username
.usernameParameter("username")
//修改默认登陆的password
.passwordParameter("password")
//处理单表登陆的url路径
.loginProcessingUrl("/doLogin")
//默认看到的登录页面
.loginPage("/login")
//登陆成功的处理
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
//如果登陆成功就返回一段json
resp.setContentType("application/json;charset=utf-8");
//这是往出写的
PrintWriter out = resp.getWriter();
//登陆成功的hr对象
Hr hr = (Hr) authentication.getPrincipal();
RespBean ok = RespBean.ok("登陆成功",hr);
//把hr写成字符串
String s = new ObjectMapper().writeValueAsString(ok);
//把字符串写出去
out.write(s);
out.flush();
out.close();
}
})
//登陆失败的处理
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
//如果登陆失败就返回一段json
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
RespBean respBean = RespBean.error("登陆失败!");
if (e instanceof LockedException){
respBean.setMsg("账户被锁定,请联系管理员!");
}else if (e instanceof CredentialsExpiredException){
respBean.setMsg("密码过期,请联系管理员!");
}else if (e instanceof AccountExpiredException){
respBean.setMsg("账户过期,请联系管理员!");
}else if (e instanceof DisabledException){
respBean.setMsg("账户被禁用,请联系管理员!");
}else if (e instanceof BadCredentialsException){
respBean.setMsg("用户名或者密码输入错误,请联系管理员!");
}
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();
}
})
//跟登陆相关的接口就能直接访问
.permitAll()
.and()
.logout()
//注销成功后的回调
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
out.write(new ObjectMapper().writeValueAsString(RespBean.ok("注销成功!")));
out.flush();
out.close();
}
})
.permitAll()
.and()
//关闭csrf攻击
.csrf().disable();
}
}
这就是完整的webconfig了
7、测试
编写一个controller 测试一下
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "欢迎进入登陆页面";
}
@GetMapping("/employee/basic/hello")
public String hello2(){
return "/emp/basic/hello";
}
@GetMapping("/employee/advanced/hello")
public String hello3(){
return "/emp/adv/hello";
}
}
成功显示role
成功
Spring Security在这里就全部完成了