在上一篇对用户认证授权之后,用户访问资源的时候,需要获得这个资源的权限和用户的权限,然后通过决策器来比对用户是否有权限访问该资源。一般可以在Security中配置需要的权限,然后使用默认的鉴权即可。这里是使用了自定的鉴权方法和拦截器。
1.鉴权的基本流程
访问资源的时候,即访问url时,会先通过 AbstractSecurityInterceptor 拦截器拦截请求,它会调用 FilterInvocationSecurityMetadataSource 里的方法来获取拦截的url所需要的的全部权限,如果请求的url不在权限表里,那么就会放行,否则会调用决策器 AccessDecisionManager 的决策方法 decide() 来决策用户是否拥有权限访问资源。在 AccessDecisionManager 中它会获得保存在spring的全局缓存SecurityContextHolder 中的用户信息,然后在decide() 方法中比对资源所需权限和用户权限,根据所配策略(有:一票决定,一票否定,少数服从多数等)来决定用户是否有权限访问资源。
2.实现鉴权
2.1 实现FilterInvocationSecurityMetadataSource接口
MyInvocationSecurityMetadataSourceService.java
//鉴权所需要的资源载入 1.先加载权限表中的所有权限 2.查看访问的URL是否在权限表中存在 若存在则调用MyAccessDecisionManager(决策器)
//中的 decide 方法来判断用户是否有该权限
@Service
public class MyInvocationSecurityMetadataSourceService implements
FilterInvocationSecurityMetadataSource {
@Autowired
private PermissionService permissionService;
private HashMap<String, Collection<ConfigAttribute>> map =null;
/**
* 加载权限表中所有权限
*/
public void loadResourceDefine(){
map = new HashMap<>();
Collection<ConfigAttribute> array;
ConfigAttribute cfg;
List<Permission> permissions = permissionService.findall();
for(Permission permission : permissions) {
array = new ArrayList<>();
cfg = new SecurityConfig(permission.getName());
//此处只添加了用户的名字,其实还可以添加更多权限的信息,例如请求方法到ConfigAttribute的集合中去。此处添加的信息将会作为MyAccessDecisionManager类的decide的第三个参数。
array.add(cfg);
//用权限的getUrl() 作为map的key,用ConfigAttribute的集合作为 value,
map.put(permission.getUrl(), array);
}
}
//此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
if(map ==null) loadResourceDefine();
//object 中包含用户请求的request 信息
HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
AntPathRequestMatcher matcher;
String resUrl;
for(Iterator<String> iter = map.keySet().iterator(); iter.hasNext(); ) {
resUrl = iter.next();
matcher = new AntPathRequestMatcher(resUrl);
if(matcher.matches(request)) {
return map.get(resUrl);
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
2.2 实现AccessDecisionManager接口
MyAccessDecisionManager.java(决策器)
@Service
public class MyAccessDecisionManager implements AccessDecisionManager {
// decide 方法是判定是否拥有权限的决策方法,
//authentication 是用户信息中权限的集合.
//object 包含客户端发起的请求的requset信息,可转换为 HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
//configAttributes 为MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法返回的结果,此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
//configAttributes资源所需要的权限
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
if(null== configAttributes || configAttributes.size() <=0) {
return;
}
ConfigAttribute c;
String needRole;
for(Iterator<ConfigAttribute> iter = configAttributes.iterator(); iter.hasNext(); ) {
c = iter.next();
needRole = c.getAttribute();
for(GrantedAuthority ga : authentication.getAuthorities()) {
if(needRole.trim().equals(ga.getAuthority())) {
return;
}
}
}
throw new AccessDeniedException("no right");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
2.3 继承 AbstractSecurityInterceptor 并且实现Filter接口
MyFilterSecurityInterceptor.java(自定义拦截器)
@Service
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 servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
invoke(fi);
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
//fi里面有一个被拦截的url
//里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限,如果权限表中有权限,调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够,如果没有就放行,表示可以访问。
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.Security资源配置
SecurityConfig.java(配置类)
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationProvider provider; //注入我们自己的AuthenticationProvider
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyAuthenticationFailHander myAuthenticationFailHander;
@Autowired
private MyFilterSecurityInterceptor myFilterSecurityInterceptor;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.
authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable()
.formLogin().loginPage("/login").permitAll()
.loginProcessingUrl("/form")
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailHander);
http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);
}
//配置我们自定义的验证授权 让 spring security使用我们自定义的验证器 而不是默认的验证器 也就是MyAuthenticationProvider
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// TODO Auto-generated method stub
//注册一个自定义的 AuthenticationProvider
auth.authenticationProvider(provider);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
4.测试
权限表:
访问localhost:8080 自动跳转到登录页面
登录普通用户访问 localhost:8080/admin 资源 因为资源需要ROLE_ADMIN权限所以报错403
而 localhost:8080 页面只需要ROLE_USER 权限 所以能够访问
再登录ADMIN 用户 访问 localhost:8080/admin 访问成功