天天看点

websecurity连接mysql_Spring Security 动态url权限控制(三)

一、前言

本篇文章将讲述Spring Security 动态分配url权限,未登录权限控制,登录过后根据登录用户角色授予访问url权限

基本环境

spring-boot 2.1.8

mybatis-plus 2.2.0

mysql 数据库

maven项目

Spring Security入门学习可参考之前文章:

二、数据库建表

websecurity连接mysql_Spring Security 动态url权限控制(三)

表关系简介:

用户表t_sys_user 关联 角色表t_sys_role 两者建立中间关系表t_sys_user_role

角色表t_sys_role 关联 权限表t_sys_permission 两者建立中间关系表t_sys_role_permission

最终体现效果为当前登录用户所具备的角色关联能访问的所有url,只要给角色分配相应的url权限即可

温馨小提示:这里逻辑根据个人业务来定义,小编这里讲解案例只给用户对应的角色分配访问权限,像其它的 直接给用户分配权限等等可以自己实现

表模拟数据如下:

websecurity连接mysql_Spring Security 动态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;

@Override

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

// 从数据库中取出用户信息

List userList = userMapper.selectList(new EntityWrapper().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()));

}

public SecurityUser getUserByToken(String token) {

User user = null;

List loginList = userMapper.selectList(new EntityWrapper().eq("token", token));

if (!CollectionUtils.isEmpty(loginList)) {

user = loginList.get(0);

}

return user != null ? new SecurityUser(user, getUserRoles(user.getId())) : null;

}

private List getUserRoles(Integer userId) {

List userRoles = userRoleMapper.selectList(new EntityWrapper().eq("user_id", userId));

List roleList = new LinkedList<>();

for (UserRole userRole : userRoles) {

Role role = roleMapper.selectById(userRole.getRoleId());

roleList.add(role);

}

return roleList;

}

}

这里再说下自定义SecurityUser是因为Spring Security自带的 UserDetails (存储当前用户基本信息) 有时候可能不满足我们的需求,因此我们可以自己定义一个来扩展我们的需求

websecurity连接mysql_Spring Security 动态url权限控制(三)

getAuthorities()方法:即授予当前用户角色权限信息

@Data

@Slf4j

public class SecurityUser implements UserDetails {

private transient User currentUserInfo;

private transient List roleList;

public SecurityUser() { }

public SecurityUser(User user) {

if (user != null) {

this.currentUserInfo = user;

}

}

public SecurityUser(User user, List roleList) {

if (user != null) {

this.currentUserInfo = user;

this.roleList = roleList;

}

}

@Override

public Collection extends GrantedAuthority> getAuthorities() {

Collection 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;

}

}

4、自定义UrlFilterInvocationSecurityMetadataSource实现FilterInvocationSecurityMetadataSource重写getAttributes()方法 获取访问该url所需要的角色权限信息

执行完之后到 下一步 UrlAccessDecisionManager 中认证权限

@Component

public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

@Autowired

PermissionMapper permissionMapper;

@Autowired

RolePermissionMapper rolePermissionMapper;

@Autowired

RoleMapper roleMapper;

@Override

public Collection getAttributes(Object object) throws IllegalArgumentException {

// 获取当前请求url

String requestUrl = ((FilterInvocation) object).getRequestUrl();

// TODO 忽略url请放在此处进行过滤放行

if ("/login".equals(requestUrl) || requestUrl.contains("logout")) {

return null;

}

// 数据库中所有url

List permissionList = permissionMapper.selectList(null);

for (Permission permission : permissionList) {

// 获取该url所对应的权限

if (requestUrl.equals(permission.getUrl())) {

List permissions = rolePermissionMapper.selectList(new EntityWrapper().eq("permission_id", permission.getId()));

List roles = new LinkedList<>();

if (!CollectionUtils.isEmpty(permissions)){

Integer roleId = permissions.get(0).getRoleId();

Role role = roleMapper.selectById(roleId);

roles.add(role.getCode());

}

// 保存该url对应角色权限信息

return SecurityConfig.createList(roles.toArray(new String[roles.size()]));

}

}

// 如果数据中没有找到相应url资源则为非法访问,要求用户登录再进行操作

return SecurityConfig.createList(Constants.ROLE_LOGIN);

}

@Override

public Collection getAllConfigAttributes() {

return null;

}

@Override

public boolean supports(Class> aClass) {

return FilterInvocation.class.isAssignableFrom(aClass);

}

}

5、自定义UrlAccessDecisionManager实现AccessDecisionManager重写decide()方法 对访问url进行权限认证处理

此处小编的处理逻辑是只要包含其中一个角色即可访问

@Component

public class UrlAccessDecisionManager implements AccessDecisionManager {

@Override

public void decide(Authentication authentication, Object object, Collection collection) throws AccessDeniedException, AuthenticationException {

// 遍历角色

for (ConfigAttribute ca : collection) {

// ① 当前url请求需要的权限

String needRole = ca.getAttribute();

if (Constants.ROLE_LOGIN.equals(needRole)) {

if (authentication instanceof AnonymousAuthenticationToken) {

throw new BadCredentialsException("未登录!");

} else {

throw new AccessDeniedException("未授权该url!");

}

}

// ② 当前用户所具有的角色

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;

}

}

6、自定义无权限处理器 UrlAccessDeniedHandler实现AccessDeniedHandler重写handle()方法

在这里自定义403无权限响应内容,登录过后的权限处理

【 注:要和未登录时的权限处理区分开哦~ 】

@Component

public class UrlAccessDeniedHandler implements AccessDeniedHandler {

@Override

public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {

ResponseUtils.out(response, ApiResult.fail(403, e.getMessage()));

}

}

7、最后在Security 核心配置类中配置以上处理

@Configuration

@EnableWebSecurity

@EnableGlobalMethodSecurity(prePostEnabled = true)

public class SecurityConfig extends WebSecurityConfigurerAdapter {

private final MyAuthenticationFilter myAuthenticationFilter;

private final AdminAuthenticationEntryPoint adminAuthenticationEntryPoint;

private final AdminAuthenticationProcessingFilter adminAuthenticationProcessingFilter;

// 上面是登录认证相关 下面为url权限相关 - ========================================================================================

private final UrlFilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource;

private final UrlAccessDecisionManager urlAccessDecisionManager;

private final UrlAccessDeniedHandler urlAccessDeniedHandler;

public SecurityConfig(MyAuthenticationFilter myAuthenticationFilter, AdminAuthenticationEntryPoint adminAuthenticationEntryPoint, AdminAuthenticationProcessingFilter adminAuthenticationProcessingFilter, UrlFilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource, UrlAccessDeniedHandler urlAccessDeniedHandler, UrlAccessDecisionManager urlAccessDecisionManager) {

this.myAuthenticationFilter = myAuthenticationFilter;

this.adminAuthenticationEntryPoint = adminAuthenticationEntryPoint;

this.adminAuthenticationProcessingFilter = adminAuthenticationProcessingFilter;

this.urlFilterInvocationSecurityMetadataSource = urlFilterInvocationSecurityMetadataSource;

this.urlAccessDeniedHandler = urlAccessDeniedHandler;

this.urlAccessDecisionManager = urlAccessDecisionManager;

}

@Override

protected void configure(HttpSecurity http) throws Exception {

ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry = http.antMatcher("

@Override

public void configure(WebSecurity web) throws Exception {

web.ignoring().antMatchers(HttpMethod.GET,

"/favicon.ico",

"*.css",

"*.js");

}

}

四、编写测试代码

控制层:

@Slf4j

@RestController

public class IndexController {

@GetMapping("/")

public ModelAndView showHome() {

return new ModelAndView("home.html");

}

@GetMapping("/index")

public String index() {

return "Hello World ~";

}

@GetMapping("/login")

public ModelAndView login() {

return new ModelAndView("login.html");

}

@GetMapping("/home")

public String home() {

String name = SecurityContextHolder.getContext().getAuthentication().getName();

log.info("登陆人:" + name);

return "Hello~ " + name;

}

@GetMapping(value ="/admin")

// 访问路径`/admin` 具有`ADMIN`角色权限 【这种是写死方式】

// @PreAuthorize("hasPermission('/admin','ADMIN')")

public String admin() {

return "Hello~ 管理员";

}

@GetMapping("/test")

public String test() {

return "Hello~ 测试权限访问接口";

}

}

页面和其它相关代码这里就不贴出来了,具体可参考文末demo源码

五、运行访问测试效果

1、未登录时

websecurity连接mysql_Spring Security 动态url权限控制(三)

2、登录过后如果有权限则正常访问

websecurity连接mysql_Spring Security 动态url权限控制(三)

3、登录过后,没有权限

这里我们可以修改数据库角色权限关联表t_sys_role_permission来进行测试哦 ~

Security 动态url权限也就是依赖这张表来判断的,只要修改这张表分配角色对应url权限资源,用户访问url时就会动态的去判断,无需做其他处理,如果是将权限信息放在了缓存中,修改表数据时及时更新缓存即可!

websecurity连接mysql_Spring Security 动态url权限控制(三)
websecurity连接mysql_Spring Security 动态url权限控制(三)

4、登录过后,访问数据库中没有配置的url 并且 在Security中没有忽略拦截的url时

websecurity连接mysql_Spring Security 动态url权限控制(三)

六、总结

自定义未登录权限处理器AdminAuthenticationEntryPoint - 自定义未登录时访问无权限url响应内容

自定义访问鉴权过滤器MyAuthenticationFilter - 记录请求响应日志、是否合法访问,验证token过期等

自定义UrlFilterInvocationSecurityMetadataSource - 获取访问该url所需要的角色权限

自定义UrlAccessDecisionManager - 对访问url进行权限认证处理

自定义UrlAccessDeniedHandler - 登录过后访问无权限url失败处理器 - 自定义403无权限响应内容

在Security核心配置类中配置以上处理器和过滤器

Security动态权限相关代码:

websecurity连接mysql_Spring Security 动态url权限控制(三)

本文案例demo源码