现在流行的通用授权框架有apache的shiro和Spring家族的Spring Security,在涉及今天的微服务鉴权时,需要利用我们的授权框架搭建自己的鉴权服务,今天总理了Spring Security,便于在微服务架构体系中拓展.对于shiro之前用过,单体应用使用起来还是很灵活轻便的.
#################################################################
Spring Security是通过AccessDecisionManager进行授权管理的.就先从他开打.
在Spring Security访问决策管理中,我们能看到基于实现AccessDecisionManager ,Spring Security内部实现这个接口的有4个类,逐个查看:
AbstractAccessDecisionManager:
这个抽象类从图可以看到实际下面被继承了3个类,分别是3种不同的投票策略.这里不一一说明了,有兴趣的可以翻阅源码.
3种不同的策略分别是:
AffirmativeBased的策略:
UnanimousBased的策略:
ConsensusBased的策略:
###################################################################
源码中可以看出这三种不同的策略都用到了AccessDecisionVoter接口(访问决策投票器),接口定义了投票方法
投票方法Spring Security内部实现了2种,如图
AuthenticatedVoter:先判定用户是否通过登录认证,没有投反对票,在内部根据用户登录包含的角色信息获取用户实际的权限和当前的请求需要的角色匹配
RoleVoter:只处理ROLE_开头的配置角色参数进行投票(可通过配置rolePrefix的值进行改变)
上面已认证投票方法中需要注意的是:
当前的请求需要的角色和用户登录包含的角色信息获取用户实际的权限这2个核心参数Spring Scerity是如何管理的呢?
我们接着往下走读,查看Spring Scerity core源码
这里userdetails目录全是是Spring Security 管理的用户信息.
这里不一一描述了,需要深入了解可以查看官方源码,主要实现思路如下:
UserDetails中getAuthorities方法,用来获取当前用户所具有的角色,在实际项目中可以根据这个方法描述当前用户的角色
示例代码:
public Collection extends GrantedAuthority> getAuthorities() {
List
for (Role role : roles) {
// roles中获取当前用户所具有的角色,构造SimpleGrantedAuthority
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
#################################################################
loadUserByUsername方法:
在执行登录的过程中,这个方法将根据用户名去查找用户,如果用户不存在,则抛出UsernameNotFoundException异常,返回用户对象
示例代码:
@Service
@Transactional
public class HrService implements UserDetailsService {
@Autowired
HrMapper hrMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
Hr hr = hrMapper.loadUserByUsername(s);
if (hr == null) {
throw new UsernameNotFoundException("用户名不对");
} return hr;
}
}
源码说明:
根据用户名定位用户。在实际的实现中,搜索可能是区分大小写的,或者不区分大小写,这取决于如何配置*实现实例。在这种情况下,UserDetails对象返回的用户名可能与实际请求的不同。
DefaultFilterInvocationSecurityMetadataSource:
该类实现了FilterInvocationSecurityMetadataSource,主要功能就是通过当前的请求地址,获取该地址需要的用户角色
示例代码:
@Component
public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired MenuService menuService;
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Collection
//从getAttributes(Object o)方法的参数o中提取出当前的请求url
String requestUrl = ((FilterInvocation) o).getRequestUrl();
if ("/login_p".equals(requestUrl)) {
return null;
}
//查询数据库中url pattern和role的对应关系,Menu类有两个核心属性,一个是url pattern,即匹配规则(比如/admin/**
),还有一个是List,即这种规则的路径需要哪些角色才能访问。
List
for (Menu menu : allMenu) {
//url pattern一一对照,匹配当前的url pattern所对应的角色,遍历角色
//这里涉及到一个优先级问题,比如我的地址是/employee/basic/hello
,这个地址既能被/employee/**
匹配,
//也能被/employee/basic/**
匹配,这就要求我们从数据库查询的时候对数据进行排序,将/employee/basic/**
类型的url pattern放在集合的前面去比较
if (antPathMatcher.match(menu.getUrl(), requestUrl)&&menu.getRoles().size()>0) {
List
int size = roles.size();
String[] values = new String[size];
for (int i = 0; i < size; i++) {
values[i] = roles.get(i).getName();
}
//SecurityConfig.createList方法来创建一个角色集合
return SecurityConfig.createList(values);
}
}
//没有匹配上的资源,都是登录访问
return SecurityConfig.createList("ROLE_LOGIN");
//.如果返回null的话,意味着当前这个请求不需要任何角色就能访问,甚至不需要登录。
//return null;
}
@Override
public Collection
return null;
}
@Override
public boolean supports(Class> aClass) {
return FilterInvocation.class.isAssignableFrom(aClass);
}
}
AccessDecisionManager接口: 进行最终的访问控制(授权)决策
getAttributes(Object o)方法返回的集合最终会来到AccessDecisionManager接口中,作为传递的参数解析访问控制决策
authentication是调用此方法的主体对象,就是当前用户,保存了当前登录用户的角色信息
object:被调用的安全对象,rest资源对象
configAttributes:与安全保护相关的配置属性,即当前请求需要的角色
示例代码:
@Component
public class UrlAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o, Collection
Iterator
while (iterator.hasNext()) {
ConfigAttribute ca = iterator.next();
//当前请求需要的权限
String needRole = ca.getAttribute();
//当前请求需要的权限为/ROLE_LOGIN
则表示登录即可访问,和角色没有关系,
if ("ROLE_LOGIN".equals(needRole)) {
//需要判断authentication是不是AnonymousAuthenticationToken的一个实例,是表示当前用户没有登录,没有登录就抛一个BadCredentialsException异常,登录了就直接返回,则这个请求将被成功执行
if (authentication instanceof AnonymousAuthenticationToken) {
throw new BadCredentialsException("未登录");
} else
return;
}
//当前用户所具有的权限
Collection extends GrantedAuthority> authorities = authentication.getAuthorities();
//查看当前用户的角色列表中是否具备需要的权限,如果具备就直接返回,否则就抛异常
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)) {
return;
}
}
}
throw new AccessDeniedException("权限不足!");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class> aClass) {
return true;
}
}
AccessDeniedHandler接口:处理内部AccessDeniedException异常
示例代码:
@Component
public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse resp, AccessDeniedException e) throws IOException, ServletException {
resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
resp.setContentType("application/json;charset=UTF-8");
PrintWriter out = resp.getWriter();
out.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员!\"}");
out.flush();
out.close();
}
}
WebSecurityConfig:
configure(HttpSecurity http)方法中,通过withObjectPostProcessor将刚刚创建的UrlFilterInvocationSecurityMetadataSource和UrlAccessDecisionManager注入进来。到时候,请求都会经过刚才的过滤器(除了configure(WebSecurity web)方法忽略的请求)
successHandler中配置登录成功时返回的JSON,登录成功时返回当前用户的信息
failureHandler表示登录失败,登录失败的原因可能有多种,我们根据不同的异常输出不同的错误提示即可
示例代码:
}
最后,针对自己的项目整理Spring Security的访问鉴权策略的基本思路为:
AccessDecisionManager来判断所请求的url + httpmethod 是否符合我们数据库中的配置。然而,AccessDecisionManager并没有来判定类似需求的相关Voter, 因此,我们需要自定义一个Voter的实现(默认注册的AffirmativeBased的策略是只要有Voter投出ACCESS_GRANTED票,则判定为通过,这也正符合我们的需求)。实现voter后,有2个关键参数(Collection
##########################################################
篇外话:其实看源码最好还是从官方文档的入手,从系统策略开始在逐个深入,各个击破.