前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SpringSecurity权限管理,根据请求URL鉴权

SpringSecurity权限管理,根据请求URL鉴权

作者头像
Java帮帮
发布2020-02-11 17:31:38
5.2K0
发布2020-02-11 17:31:38
举报

SpringSecurity和Shiro是两大权限框架,前者属于Spring家族,功能比较强,重量级的存在,新手搞的时候可能会经常遇到坑。后者比较轻量级,上手相对比较简单。这两个我都写过权限管理的博客。

前填有个朋友让我帮他把他的一个 SpringSecurity 项目改造成通过URL检查权限,之前他在控制器每个方法上加上如下注解来实现的,该方法通常是初学者使用的,但是用于公司的大型项目肯定不行,比较蠢,代码冗余,不能扩展,不利于维护。

代码语言:javascript
复制
@PreAuthorize("hasAnyAuthority('ROLE_ADMIN'")  // 指定角色权限才能操作方法

现在的目标就是剔除所有的该注解,通过拦截器来判断该用户是否有该URL的权限。

一、数据库设计

主要包括用户表,角色表,权限表,用户和角色关联表,角色和权限关联表

重要字段我都用红线标明了

其中权限表(t_permission)其实也充当了菜单表的作用,其中的path字段就是请求路径,如 /post,/psot/new,/post/edit/* (我们以正则表达式的方式写,后面不限字符串以*表示),到时候比对的时候用正则表达式判断

实体类这里就不给了,这里主要还是讲核心思想

主要关注第二节的配置

二、核心文件

1.SpringSecurity的配置文件

WebSecurityConfig.java

代码语言:javascript
复制
package com.liuyanzhao.sens.config.security;
import com.liuyanzhao.sens.common.utils.SecurityUtil;
import com.liuyanzhao.sens.config.security.jwt.AuthenticationFailHandler;
import com.liuyanzhao.sens.config.security.jwt.AuthenticationSuccessHandler;
import com.liuyanzhao.sens.config.security.jwt.JWTAuthenticationFilter;
import com.liuyanzhao.sens.config.security.jwt.RestAccessDeniedHandler;
import com.liuyanzhao.sens.config.security.permission.MyFilterSecurityInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
/**
 * Security 核心配置类
 * 开启注解控制权限至Controller
 * @author 言曌
 */
@Slf4j
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 @Autowired
    private IgnoredUrlsProperties ignoredUrlsProperties;
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    @Autowired
    private AuthenticationSuccessHandler successHandler;
    @Autowired
    private AuthenticationFailHandler failHandler;
    @Autowired
    private RestAccessDeniedHandler accessDeniedHandler;
    @Autowired
    private MyFilterSecurityInterceptor myFilterSecurityInterceptor;
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private SecurityUtil securityUtil;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http
                .authorizeRequests();
        //除配置文件忽略路径其它所有请求都需经过认证和授权
        for(String url:ignoredUrlsProperties.getUrls()){
            registry.antMatchers(url).permitAll();
        }
        registry.and()
                //表单登录方式
                .formLogin()
                .loginPage("/sens/common/needLogin")
                //登录请求url
                .loginProcessingUrl("/sens/login")
                .permitAll()
                //成功处理类
                .successHandler(successHandler)
                //失败
                .failureHandler(failHandler)
                .and()
                //允许网页iframe
                .headers().frameOptions().disable()
                .and()
                .logout()
                .permitAll()
                .and()
                .authorizeRequests()
                //任何请求
                .anyRequest()
                //需要身份认证
                .authenticated()
                .and()
                //关闭跨站请求防护
                .csrf().disable()
                //前后端分离采用JWT 不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //自定义权限拒绝处理类
                .exceptionHandling().accessDeniedHandler(accessDeniedHandler)
                .and() //添加自定义权限过滤器
                .addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);
    }
}

addFilterBefore 作用就是当执行权限验证前执行,我们需要在这之前判断即可。

2.自定义拦截器

MyFilterSecurityInterceptor.java

代码语言:javascript
复制
package com.liuyanzhao.sens.config.security.permission;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import java.io.IOException;
/**
 * 权限管理拦截器
 * 监控用户行为
 * @author 言曌
 */
@Slf4j
@Component
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
    @Autowired
    private FilterInvocationSecurityMetadataSource securityMetadataSource;
    @Autowired
    public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
        super.setAccessDecisionManager(myAccessDecisionManager);
    }
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        FilterInvocation fi = new FilterInvocation(request, response, chain);
        invoke(fi);
    }
    public void invoke(FilterInvocation fi) throws IOException, ServletException {
        InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            super.afterInvocation(token, null);
        }
    }
    @Override
    public void destroy() {
    }
    @Override
    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }
    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.securityMetadataSource;
    }
}

3.自定义权限资源管理器

MySecurityMetadataSource.java

代码语言:javascript
复制
package com.liuyanzhao.sens.config.security.permission;
import com.liuyanzhao.sens.common.constant.CommonConstant;
import com.liuyanzhao.sens.modules.base.entity.Permission;
import com.liuyanzhao.sens.modules.base.service.PermissionService;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import java.util.*;
/**
 * 权限资源管理器
 * 为权限决断器提供支持
 *
 * @author 言曌
 */
@Slf4j
@Component
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    @Autowired
    private PermissionService permissionService;
    private Map<String, Collection<ConfigAttribute>> map = null;
    /**
     * 加载权限表中所有操作请求权限
     */
    public void loadResourceDefine() {
        map = new HashMap<>(16);
        Collection<ConfigAttribute> configAttributes;
        ConfigAttribute cfg;
        // 获取启用的权限操作请求
        List<Permission> permissions = permissionService.findByTypeAndStatusOrderBySortOrder(CommonConstant.PERMISSION_OPERATION, CommonConstant.STATUS_NORMAL);
        for (Permission permission : permissions) {
            if (StrUtil.isNotBlank(permission.getTitle()) && StrUtil.isNotBlank(permission.getPath())) {
                configAttributes = new ArrayList<>();
                cfg = new SecurityConfig(permission.getTitle());
                //作为MyAccessDecisionManager类的decide的第三个参数
                configAttributes.add(cfg);
                //用权限的path作为map的key,用ConfigAttribute的集合作为value
                map.put(permission.getPath(), configAttributes);
            }
        }
    }
    /**
     * 判定用户请求的url是否在权限表中
     * 如果在权限表中,则返回给decide方法,用来判定用户是否有此权限
     * 如果不在权限表中则放行
     *
     * @param o
     * @return
     * @throws IllegalArgumentException
     */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        if (map == null) {
            loadResourceDefine();
        }
        //Object中包含用户请求request
        String url = ((FilterInvocation) o).getRequestUrl();
        PathMatcher pathMatcher = new AntPathMatcher();
        Iterator<String> iterator = map.keySet().iterator();
        while (iterator.hasNext()) {
            String resURL = iterator.next();
            if (StrUtil.isNotBlank(resURL) && pathMatcher.match(resURL, url)) {
                return map.get(resURL);
            }
        }
        return null;
    }
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }
    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

4.自定义角色决断器

MyAccessDecisionManager.java

代码语言:javascript
复制
package com.liuyanzhao.sens.config.security.permission;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Iterator;
/**
 * 权限管理决断器
 * 判断用户拥有的权限或角色是否有资源访问权限
 * @author 言曌
 */
@Slf4j
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        if(configAttributes==null){
            return;
        }
        Iterator<ConfigAttribute> iterator = configAttributes.iterator();
        while (iterator.hasNext()){
            ConfigAttribute c = iterator.next();
            String needPerm = c.getAttribute();
            for(GrantedAuthority ga : authentication.getAuthorities()) {
                // 匹配用户拥有的ga 和 系统中的needPerm
                if(needPerm.trim().equals(ga.getAuthority())) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("抱歉,您没有访问权限");
    }
    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }
    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

三、基本整合

UserDetailsServiceImpl.java

代码语言:javascript
复制
package com.liuyanzhao.sens.config.security;
import com.liuyanzhao.sens.modules.base.entity.User;
import com.liuyanzhao.sens.common.exception.LoginFailLimitException;
import com.liuyanzhao.sens.modules.base.service.UserService;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
 * @author 言曌
 */
@Slf4j
@Component
public class UserDetailsServiceImpl implements UserDetailsService{
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private UserService userService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        String flagKey = "loginFailFlag:" + username;
        String value = redisTemplate.opsForValue().get(flagKey);
        Long timeRest = redisTemplate.getExpire(flagKey, TimeUnit.MINUTES);
        if(StrUtil.isNotBlank(value)){
            //超过限制次数
            throw new LoginFailLimitException("登录错误次数超过限制,请"+timeRest+"分钟后再试");
        }
        User user = userService.findByUsername(username);
        return new SecurityUserDetails(user);
    }
}

SecurityUserDetails.java

代码语言:javascript
复制
package com.liuyanzhao.sens.config.security;
import com.liuyanzhao.sens.common.constant.CommonConstant;
import com.liuyanzhao.sens.modules.base.entity.Permission;
import com.liuyanzhao.sens.modules.base.entity.Role;
import com.liuyanzhao.sens.modules.base.entity.User;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
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;
/**
 * @author 言曌
 */
@Slf4j
public class SecurityUserDetails extends User implements UserDetails {
    private static final long serialVersionUID = 1L;
    public SecurityUserDetails(User user) {
        if(user!=null) {
            this.setUsername(user.getUsername());
            this.setPassword(user.getPassword());
            this.setStatus(user.getStatus());
            this.setRoles(user.getRoles());
            this.setPermissions(user.getPermissions());
        }
    }
    /**
     * 添加用户拥有的权限和角色
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorityList = new ArrayList<>();
        List<Permission> permissions = this.getPermissions();
        // 添加请求权限
        if(permissions!=null&&permissions.size()>0){
            for (Permission permission : permissions) {
                if(CommonConstant.PERMISSION_OPERATION.equals(permission.getType())
                        &&StrUtil.isNotBlank(permission.getTitle())
                        &&StrUtil.isNotBlank(permission.getPath())) {
                    authorityList.add(new SimpleGrantedAuthority(permission.getTitle()));
                }
            }
        }
        // 添加角色
        List<Role> roles = this.getRoles();
        if(roles!=null&&roles.size()>0){
            // lambda表达式
            roles.forEach(item -> {
                if(StrUtil.isNotBlank(item.getName())){
                    authorityList.add(new SimpleGrantedAuthority(item.getName()));
                }
            });
        }
        return authorityList;
    }
    /**
     * 账户是否过期
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    /**
     * 是否禁用
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
        return CommonConstant.USER_STATUS_LOCK.equals(this.getStatus()) ? false : true;
    }
    /**
     * 密码是否过期
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    /**
     * 是否启用
     * @return
     */
    @Override
    public boolean isEnabled() {
        return CommonConstant.USER_STATUS_NORMAL.equals(this.getStatus()) ? true : false;
    }
}

四、匿名访问的URL通过application.yml配置

上面 WebSecurityConfig 中我们通过读取 application.yml 中的配置,允许匿名访问这些路径。

公司通常也是这样做的。

IgnoredUrlsProperties.java

代码语言:javascript
复制
package com.liuyanzhao.sens.config.security;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
/**
 * @author 言曌
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "ignored")
public class IgnoredUrlsProperties {
    private List<String> urls = new ArrayList<>();
}

application.yml

代码语言:javascript
复制
# 忽略鉴权url
ignored:
  urls:
    - /editor-app/**
    - /sens/act/**
    - /sens/dictData/getByType/**
    - /sens/email/sendResetCode
    - /sens/email/resetByEmail
    - /sens/file/view/**
    - /sens/social/**
    - /sens/ws/**
    - /sens/user/regist
    - /sens/user/smsLogin
    - /sens/user/resetByMobile
    - /sens/common/**
    - /druid/**
    - /swagger-ui.html
    - /swagger-resources/**
    - /swagger/**
    - /**/v2/api-docs
    - /**/*.js
    - /**/*.css
    - /**/*.png
    - /**/*.ico
    - /test/**
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-12-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java帮帮 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、数据库设计
  • 二、核心文件
    • 1.SpringSecurity的配置文件
      • 2.自定义拦截器
        • 3.自定义权限资源管理器
          • 4.自定义角色决断器
          • 三、基本整合
          • 四、匿名访问的URL通过application.yml配置
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档