SpringSecurity和Shiro是两大权限框架,前者属于Spring家族,功能比较强,重量级的存在,新手搞的时候可能会经常遇到坑。后者比较轻量级,上手相对比较简单。这两个我都写过权限管理的博客。
前填有个朋友让我帮他把他的一个 SpringSecurity 项目改造成通过URL检查权限,之前他在控制器每个方法上加上如下注解来实现的,该方法通常是初学者使用的,但是用于公司的大型项目肯定不行,比较蠢,代码冗余,不能扩展,不利于维护。
现在的目标就是剔除所有的该注解,通过拦截器来判断该用户是否有该URL的权限。
在这之前先讲一下Spring Security实现权限管理吧!
主要包括用户表,角色表,权限表,用户和角色关联表,角色和权限关联表
重要字段我都用红线标明了
其中权限表(t_permission)其实也充当了菜单表的作用,其中的path字段就是请求路径,如 /post,/psot/new,/post/edit/* (我们以正则表达式的方式写,后面不限字符串以*表示),到时候比对的时候用正则表达式判断
实体类这里就不给了,这里主要还是讲核心思想
主要关注第二节的配置
1.SpringSecurity的配置文件
WebSecurityConfig.java
addFilterBefore 作用就是当执行权限验证前执行,我们需要在这之前判断即可。
2.自定义拦截器
MyFilterSecurityInterceptor.java
3.自定义权限资源管理器
MySecurityMetadataSource.java
4.自定义角色决断器
MyAccessDecisionManager.java
1.UserDetailsServiceImpl.java
2.SecurityUserDetails.java
上面 WebSecurityConfig 中我们通过读取 application.yml 中的配置,允许匿名访问这些路径。
公司通常也是这样做的。
IgnoredUrlsProperties.java
application.yml
前填有个朋友让我帮他把他的一个 SpringSecurity 项目改造成通过URL检查权限,之前他在控制器每个方法上加上如下注解来实现的,该方法通常是初学者使用的,但是用于公司的大型项目肯定不行,比较蠢,代码冗余,不能扩展,不利于维护。
- @PreAuthorize("hasAnyAuthority('ROLE_ADMIN'") // 指定角色权限才能操作方法
现在的目标就是剔除所有的该注解,通过拦截器来判断该用户是否有该URL的权限。
在这之前先讲一下Spring Security实现权限管理吧!
一、数据库设计
主要包括用户表,角色表,权限表,用户和角色关联表,角色和权限关联表
重要字段我都用红线标明了
其中权限表(t_permission)其实也充当了菜单表的作用,其中的path字段就是请求路径,如 /post,/psot/new,/post/edit/* (我们以正则表达式的方式写,后面不限字符串以*表示),到时候比对的时候用正则表达式判断
实体类这里就不给了,这里主要还是讲核心思想
主要关注第二节的配置
二、核心文件
1.SpringSecurity的配置文件
WebSecurityConfig.java
- 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
- 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
- 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
- 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;
- }
- }
三、基本整合
1.UserDetailsServiceImpl.java
- 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);
- }
- }
2.SecurityUserDetails.java
- 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
- 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
- # 忽略鉴权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/**
您可以选择一种方式赞助本站
支付宝扫一扫赞助
微信钱包扫描赞助
赏