一、前言
本篇文章将讲述Spring Security 动态分配url权限,未登录权限控制,登录过后根据登录用户角色授予访问url权限
基本环境
- spring-boot 2.1.8
- mybatis-plus 2.2.0
- mysql 数据库
- maven项目
Spring Security入门学习可参考之前文章:
- SpringBoot集成Spring Security入门体验(一)
https://blog.csdn.net/qq_38225558/article/details/101754743 - Spring Security 自定义登录认证(二)
https://blog.csdn.net/qq_38225558/article/details/102542072
二、数据库建表
表关系简介:
- 用户表
t_sys_user
关联 角色表t_sys_role
两者建立中间关系表t_sys_user_role
- 角色表
t_sys_role
关联 权限表t_sys_permission
两者建立中间关系表t_sys_role_permission
- 最终体现效果为当前登录用户所具备的角色关联能访问的所有url,只要给角色分配相应的url权限即可
温馨小提示:这里逻辑根据个人业务来定义,小编这里讲解案例只给用户对应的角色分配访问权限,像其它的 直接给用户分配权限等等可以自己实现
表模拟数据如下:
三、Spring Security 动态权限控制
1、未登录访问权限控制
自定义AdminAuthenticationEntryPoint
类实现AuthenticationEntryPoint
类
这里是认证权限入口 -> 即在未登录的情况下访问所有接口都会拦截到此(除了放行忽略接口)
温馨小提示:
ResponseUtils
和ApiResult
是小编这里模拟前后端分离情况下返回json格式数据所使用工具类,具体实现可参考文末给出的demo源码
@Component public class AdminAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) { ResponseUtils.out(response, ApiResult.fail("未登录!!!")); } }
2、自定义过滤器MyAuthenticationFilter
继承OncePerRequestFilter
实现访问鉴权
每次访问接口都会经过此,我们可以在这里记录请求参数、响应内容,或者处理前后端分离情况下,以token换用户权限信息,token是否过期,请求头类型是否正确,防止非法请求等等
logRequestBody()
方法:记录请求消息体logResponseBody()
方法:记录响应消息体
【注:请求的HttpServletRequest流只能读一次
,下一次就不能读取了,因此这里要使用自定义的MultiReadHttpServletRequest
工具解决流只能读一次的问题,响应同理,具体可参考文末demo源码实现】
@Slf4j @Component public class MyAuthenticationFilter extends OncePerRequestFilter { private final UserDetailsServiceImpl userDetailsService; protected MyAuthenticationFilter(UserDetailsServiceImpl userDetailsService) { this.userDetailsService = userDetailsService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { System.out.println("请求头类型: " + request.getContentType()); if ((request.getContentType() == null && request.getContentLength() > 0) || (request.getContentType() != null && !request.getContentType().contains(Constants.REQUEST_HEADERS_CONTENT_TYPE))) { filterChain.doFilter(request, response); return; } MultiReadHttpServletRequest wrappedRequest = new MultiReadHttpServletRequest(request); MultiReadHttpServletResponse wrappedResponse = new MultiReadHttpServletResponse(response); StopWatch stopWatch = new StopWatch(); try { stopWatch.start(); // 记录请求的消息体 logRequestBody(wrappedRequest); // String token = "123"; // 前后端分离情况下,前端登录后将token储存在cookie中,每次访问接口时通过token去拿用户权限 String token = wrappedRequest.getHeader(Constants.REQUEST_HEADER); log.debug("后台检查令牌:{}", token); if (StringUtils.isNotBlank(token)) { // 检查token SecurityUser securityUser = userDetailsService.getUserByToken(token); if (securityUser == null || securityUser.getCurrentUserInfo() == null) { throw new AccessDeniedException("TOKEN已过期,请重新登录!"); } UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities()); // 全局注入角色权限信息和登录用户基本信息 SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(wrappedRequest, wrappedResponse); } finally { stopWatch.stop(); long usedTimes = stopWatch.getTotalTimeMillis(); // 记录响应的消息体 logResponseBody(wrappedRequest, wrappedResponse, usedTimes); } } private String logRequestBody(MultiReadHttpServletRequest request) { MultiReadHttpServletRequest wrapper = request; if (wrapper != null) { try { String bodyJson = wrapper.getBodyJsonStrByJson(request); String url = wrapper.getRequestURI().replace("//", "/"); System.out.println("-------------------------------- 请求url: " + url + " --------------------------------"); Constants.URL_MAPPING_MAP.put(url, url); log.info("`{}` 接收到的参数: {}",url , bodyJson); return bodyJson; } catch (Exception e) { e.printStackTrace(); } } return null; } private void logResponseBody(MultiReadHttpServletRequest request, MultiReadHttpServletResponse response, long useTime) { MultiReadHttpServletResponse wrapper = response; if (wrapper != null) { byte[] buf = wrapper.getBody(); if (buf.length > 0) { String payload; try { payload = new String(buf, 0, buf.length, wrapper.getCharacterEncoding()); } catch (UnsupportedEncodingException ex) { payload = "[unknown]"; } log.info("`{}` 耗时:{}ms 返回的参数: {}", Constants.URL_MAPPING_MAP.get(request.getRequestURI()), useTime, payload); } } } }
3、自定义UserDetailsServiceImpl
实现UserDetailsService
和 自定义SecurityUser
实现UserDetails
认证用户详情
这个在上一篇文章中也提及过,但上次未做角色权限处理,这次我们来一起加上吧
@Service("userDetailsService") public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Autowired private RoleMapper roleMapper; @Autowired private UserRoleMapper userRoleMapper; /*** * 根据账号获取用户信息 * @param username: * @return: org.springframework.security.core.userdetails.UserDetails */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 从数据库中取出用户信息 List<User> userList = userMapper.selectList(new EntityWrapper<User>().eq("username", username)); User user; // 判断用户是否存在 if (!CollectionUtils.isEmpty(userList)) { user = userList.get(0); } else { throw new UsernameNotFoundException("用户名不存在!"); } // 返回UserDetails实现类 return new SecurityUser(user, getUserRoles(user.getId())); } /*** * 根据token获取用户权限与基本信息 * * @param token: * @return: com.zhengqing.config.security.dto.SecurityUser */ public SecurityUser getUserByToken(String token) { User user = null; List<User> loginList = userMapper.selectList(new EntityWrapper<User>().eq("token", token)); if (!CollectionUtils.isEmpty(loginList)) { user = loginList.get(0); } return user != null ? new SecurityUser(user, getUserRoles(user.getId())) : null; } /** * 根据用户id获取角色权限信息 * * @param userId * @return */ private List<Role> getUserRoles(Integer userId) { List<UserRole> userRoles = userRoleMapper.selectList(new EntityWrapper<UserRole>().eq("user_id", userId)); List<Role> roleList = new LinkedList<>(); for (UserRole userRole : userRoles) { Role role = roleMapper.selectById(userRole.getRoleId()); roleList.add(role); } return roleList; } }
这里再说下自定义SecurityUser
是因为Spring Security自带的 UserDetails
(存储当前用户基本信息) 有时候可能不满足我们的需求,因此我们可以自己定义一个来扩展我们的需求
getAuthorities()
方法:即授予当前用户角色权限信息
@Data @Slf4j public class SecurityUser implements UserDetails { /** * 当前登录用户 */ private transient User currentUserInfo; /** * 角色 */ private transient List<Role> roleList; public SecurityUser() { } public SecurityUser(User user) { if (user != null) { this.currentUserInfo = user; } } public SecurityUser(User user, List<Role> roleList) { if (user != null) { this.currentUserInfo = user; this.roleList = roleList; } } /** * 获取当前用户所具有的角色 * * @return */ @Override public Collection<? extends GrantedAuthority> getAuthorities() { Collection<GrantedAuthority> authorities = new ArrayList<>(); if (!CollectionUtils.isEmpty(this.roleList)) { for (Role role : this.roleList) { SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.getCode()); authorities.add(authority); } } return authorities; } @Override public String getPassword() { return currentUserInfo.getPassword(); } @Override public String getUsername() { return currentUserInfo.getUsername(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
Spring Security 动态url权限控制(三)(2)https://developer.aliyun.com/article/1551853