先想一下,不让你用框架,让你自己去实现权限管理功能,你的脑海里能蹦出几个关键词?
- Filter
- Interceptor
这两个概念应该是一瞬间想到的,它们都可以帮助我们去实现权限功能,一个是servlet的规范,一个是spring的概念。在系统执行顺序中,Filter先于Interceptor执行。至于更详细的对比,网上有很多资料。
SpringSecurity对于权限这方面的实现流程
先了解下它里面比较重要的几个点:
- Filter
- AbstractSecurityInterceptor (权限管理security真正的拦截器,并绑定了AccessDecisionManager(处理权限认证)和FilterInvocationSecurityMetadataSource(提供请求路径和权限名称元数据) )
- FilterInvocationSecurityMetadataSource (主要实现加载缓存权限功能路径和名称,以及提供从请求路径查找权限名称,供后续决策管理器去判定使用。)
- AccessDecisionManager(主要判定用户是否拥有权限内的决策方法,有权限放行,无权限拒绝访问。)
- WebSecurityConfigurerAdapter (配置类)
- AccessDeniedHandler
具体代码实现
因为我是从正向流程说的整个过程,所以同学如果你按照顺序复制代码,会出现某些需要注入的属性一直显示红色,提示找不到这个类,不要着急,是因为这些类在下面的代码里,建议先整体浏览文章一遍,看懂后直接把代码全部复制进去调试。
首先实现一个我们自己的WebSecurityConfigurerAdapter ,注意 auth.userDetailsService 和 http.addFilterBefore 这两个方法,它是把我们自己实现的当前用户对应权限,和请求路径对应的权限,塞入整体流程的两个重要点。
@Configuration
@EnableWebSecurity
public class WebSecurityConfigur extends WebSecurityConfigurerAdapter {
@Autowired
private MyAbstractSecurityInterceptor myAbstractSecurityInterceptor;
@Autowired
@Qualifier("myUserDetailsService")
private UserDetailsService myUserDetailsService;
@Autowired
private CustomizeAuthenticationEntryPoint customizeAuthenticationEntryPoint;
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Autowired
MyAccessDeniedHandler myAccessDeniedHandler;
@Bean
public PasswordEncoder passwordEncoder() {
return new MyPasswordEncoder();
}
/**
* 把我们自己实现的UserDetailsService的对象放入auth中
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());
;
}
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()//开启模拟请求
.authorizeRequests()
.and()
.formLogin()
.permitAll()//登录页面用户任意访问
.successHandler(myAuthenticationSuccessHandler)//登录成功处理逻辑
.failureHandler(myAuthenticationFailureHandler)//登录失败处理逻辑
.and()
.logout()//登出
.permitAll()
.deleteCookies("JSESSIONID")//登出之后删除cookie
.and()
.headers()
.frameOptions().sameOrigin() //注销行为任意访问
.and().exceptionHandling()
.accessDeniedHandler(myAccessDeniedHandler);
//把我们自己定义的拦截器放入到HttpSecurity里
http.addFilterBefore(myAbstractSecurityInterceptor, FilterSecurityInterceptor.class)
;
}
}
再实现一个UserDetailsService的子类MyUserDetailsService,重写 loadUserByUsername() 方法,这个方法会在用户登录的时候调用, 在当前文章里全文搜索一下MyUserDetailsService 。我先走由于还没整合mybatis,所以先写死的数据,过后整合完毕,我会再修改这部分文章。
这里假设loadUserByUsername里查出来的权限就是“customer”和"all"。
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
@Qualifier("myPasswordEncoder")
private PasswordEncoder passwordEncoder;
@Autowired
ControlAccount controlAccount;
//账户未被锁定
Boolean accountNonLocked = true;
/**
* @param username
* @return
* @throws UsernameNotFoundException
* @description 此方法会在用户登录的时候执行
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//检查下当前账户是不是被锁定,三次就是锁定
Integer num = controlAccount.getLockTable().get(username);
if (null != num && 3 <= num.intValue()) {
accountNonLocked = false;
}
//根据名称查出所拥有的角儿
//根据角色查出对应的权限
//把每一个权限构造如SimpleGrantedAuthority
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
if ("account".equals(username)) {
GrantedAuthority grantedAuthority1 = new SimpleGrantedAuthority("customer");
GrantedAuthority grantedAuthority2 = new SimpleGrantedAuthority("all");
grantedAuthorities.add(grantedAuthority1);
grantedAuthorities.add(grantedAuthority2);
} else {
throw new BadCredentialsException("用户不存在");
}
//返回security的user对象
//this(username, password, true, true, true, true, authorities);
User user = new User("account", passwordEncoder.encode("wushichao")
, true, true, true, accountNonLocked, grantedAuthorities);
return user;
}
然后是重要的一个实现类:MyAbstractSecurityInterceptor,咱们要重写它的 doFilter和obtainSecurityMetadataSource方法,同时要把MyAccessDecisionManager塞入这个类里,忘记SecurityMetadataSource和AccessDecisionManager的同学,在这篇文章里全文搜索一下这俩关键词,重新回忆一下它们的功能。
@Service
public class MyAbstractSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
@Autowired
MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;
@Autowired
public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
super.setAccessDecisionManager(myAccessDecisionManager);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
FilterInvocation filterInvocation = new FilterInvocation(servletRequest, servletResponse, filterChain);
//beforeInvocation 里会调用FilterInvocationSecurityMetadataSource的getAttributes获取url对应的权限
//和调用UserDetailsService的loadUserByUsername获取用户名对应的权限
//然后调用MyAccessDecisionManager的decide去进行比对
InterceptorStatusToken interceptorStatusToken = super.beforeInvocation(filterInvocation);
try {
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
} finally {
super.afterInvocation(interceptorStatusToken, null);
}
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.myFilterInvocationSecurityMetadataSource;
}
}
**最后还剩两个实现类,其一是:MyFilterInvocationSecurityMetadataSource,重写里面的 getAttributes(XX) 方法,这个方法用来自己实现根据请求路径查出路径对应的权限,待会用来跟用户拥有的权限做对比。**别忘了supports方法要 return true.
@Service
public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
Log log = LogFactory.getLog(MyFilterInvocationSecurityMetadataSource.class);
@Autowired
PermisionDao permisionDao;
private HashMap<String, Collection<ConfigAttribute>> map = null;
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
log.info("MyFilterInvocationSecurityMetadataSource:getAttributes come in");
//1获取所有的路径对应的权限
loadAllUrlResouceAuth();
List<ConfigAttribute> list = new ArrayList<ConfigAttribute>();
//2 从过滤器中取出请求request
HttpServletRequest httpRequest = ((FilterInvocation) o).getHttpRequest();
//3 匹配
AntPathMatcher pathMatcher = new AntPathMatcher();
Iterator<String> iterator = map.keySet().iterator();
Collection<ConfigAttribute> collection = null;
while (iterator.hasNext()) {
String pattern = iterator.next();
String path = httpRequest.getRequestURI().toString();
if (pathMatcher.match(pattern, path)) {
log.info("MyFilterInvocationSecurityMetadataSource:getAttributes 有匹配的路径和权限");
collection = map.get(pattern);
}
}
//collection为null的时候,不会进入Manager
log.info("MyFilterInvocationSecurityMetadataSource:getAttributes -collection:" + JSON.toJSONString(collection));
return collection;
}
private void loadAllUrlResouceAuth() {
map = new HashMap<>();
//从数据库查出所有的权限数据集合
List<Permision> permisionList = permisionDao.findAll();
ConfigAttribute cfg;
Collection<ConfigAttribute> array;
//这里把url作为k,权限名作为v
for (Permision permision : permisionList) {
array = new ArrayList<>();
cfg = new SecurityConfig(permision.getName());
array.add(cfg);
map.put(permision.getUrl(), array);
}
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
另一个是MyAccessDecisionManager,咱们要重写它的decide方法,另外两个supports方法,注意把它的return 改为true。在decide方法中,我们需要比对请求路径对应的权限和用户拥有的权限,如果有符合的就代表有权限,没有符合的就代表无权限。
@Service
public class MyAccessDecisionManager implements AccessDecisionManager {
Log log = LogFactory.getLog(MyAccessDecisionManager.class);
@Autowired
MyUserDetailsService myUserDetailsService;
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
log.info("MyAccessDecisionManager:decide come in");
//如果请求路径没有匹配到权限表的数据,则直接放行
if (null == collection || 0 == collection.size()) {
log.info("MyAccessDecisionManager:decide collection isnull");
return;
}
ConfigAttribute configAttribute;
//把请求路径匹配到的权限和用户匹配到的权限进行比对,有一种权限就可以放行
for (Iterator<ConfigAttribute> iterator = collection.iterator(); iterator.hasNext(); ) {
configAttribute = iterator.next();
String attribute = configAttribute.getAttribute();
for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
if (attribute.trim().equals(grantedAuthority.getAuthority())) {
log.info("MyAccessDecisionManager:decide 有匹配的权限");
return;
}
}
}
//如果前面都没有拦住,最后直接报错提示,无权限。这里抛异常后,后重新发起错误处理请求,打印下MyAbstractSecurityInterceptor里的日志,会发现进入两次
//打印下httpRequest.getRequestURL()会发现,它的路径是/error
log.info("MyAccessDecisionManager:decide no right");
throw new AccessDeniedException("no right");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
请求报文
security自带的登录接口
http://localhost:8080/login?username=account&password=wushichao1
我自己写的controller的地址 /customer/login
http://localhost:8080/customer/login
结尾
好啦,以上就是实现权限的全部代码啦,由于里面设计到工具类、dao层查询的类、枚举类等,为了不影响大家的思路,这里没有贴出来,我把代码下载链接放到下面,需要的同学们可以自行去下载,里面的代码都是调试成功的。