SpringSecurity和Shiro是两大权限框架,前者属于Spring家族,功能比较强,重量级的存在,新手搞的时候可能会经常遇到坑。后者比较轻量级,上手相对比较简单。这两个我都写过权限管理的博客。
前填有个朋友让我帮他把他的一个 SpringSecurity 项目改造成通过URL检查权限,之前他在控制器每个方法上加上如下注解来实现的,该方法通常是初学者使用的,但是用于公司的大型项目肯定不行,比较蠢,代码冗余,不能扩展,不利于维护。
@PreAuthorize("hasAnyAuthority('ROLE_ADMIN'") // 指定角色权限才能操作方法
现在的目标就是剔除所有的该注解,通过拦截器来判断该用户是否有该URL的权限。
主要包括用户表,角色表,权限表,用户和角色关联表,角色和权限关联表
重要字段我都用红线标明了
其中权限表(t_permission)其实也充当了菜单表的作用,其中的path字段就是请求路径,如 /post,/psot/new,/post/edit/* (我们以正则表达式的方式写,后面不限字符串以*表示),到时候比对的时候用正则表达式判断
实体类这里就不给了,这里主要还是讲核心思想
主要关注第二节的配置
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 作用就是当执行权限验证前执行,我们需要在这之前判断即可。
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;
}
}
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;
}
}
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;
}
}
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);
}
}
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;
}
}
上面 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/**